SHARE
TWEET

Untitled

a guest Dec 9th, 2019 89 Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. class AvatarImageView @JvmOverloads constructor(
  2.     context: Context,
  3.     attrs: AttributeSet? = null,
  4.     defStyleAttr: Int = 0
  5. ) : ImageView(context, attrs, defStyleAttr) {
  6.  
  7.     companion object {
  8.         private const val DEFAULT_SIZE = 40
  9.         private const val DEFAULT_BORDER_WIDTH = 2
  10.         private const val DEFAULT_BORDER_COLOR = Color.WHITE
  11.  
  12.         // background в режими инициалов
  13.         val bgColor = arrayOf(
  14.             Color.parseColor("#7BC862"),
  15.             Color.parseColor("#E17076"),
  16.             Color.parseColor("#FAA774"),
  17.             Color.parseColor("#6EC9CB"),
  18.             Color.parseColor("#65AADD"),
  19.             Color.parseColor("#A695E7"),
  20.             Color.parseColor("#EE7AAE"),
  21.             Color.parseColor("#2196F3")
  22.         )
  23.     }
  24.  
  25.     private var size = 0
  26.     private var initials: String = "??"
  27.     @Px
  28.     var borderWidth: Float = context.dpToPx(DEFAULT_BORDER_WIDTH)
  29.     @ColorInt
  30.     private var borderColor: Int = Color.WHITE
  31.  
  32.     // наш бордер
  33.     private val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG)
  34.     private val avatarPaint = Paint(Paint.ANTI_ALIAS_FLAG)
  35.     private val initialsPaint = Paint(Paint.ANTI_ALIAS_FLAG)
  36.     // размер нашей View
  37.     private val viewRect = Rect()
  38.     private val borderRect = Rect()
  39.  
  40.     // аватар или инициалы
  41.     private var isAvatarMode = true
  42.  
  43.     init {
  44.         val ta = context.obtainStyledAttributes(attrs, R.styleable.AvatarImageView)
  45.         try {
  46.             initials = ta.getString(R.styleable.AvatarImageView_aiv_initials) ?: "??"
  47.             borderWidth = ta.getDimension(
  48.                 R.styleable.AvatarImageView_aiv_borderWidth,
  49.                 context.dpToPx(DEFAULT_BORDER_WIDTH)
  50.             )
  51.             borderColor =
  52.                 ta.getColor(R.styleable.AvatarImageView_aiv_borderColor, DEFAULT_BORDER_COLOR)
  53.  
  54.             scaleType = ScaleType.CENTER_CROP
  55.             setup()
  56.         } finally {
  57.             ta.recycle()
  58.         }
  59.  
  60.         setOnLongClickListener {
  61.             handleLongClick()
  62.         }
  63.     }
  64.  
  65.     private fun setup() {
  66.         with(borderPaint) {
  67.             // чисто обводка
  68.             style = Paint.Style.STROKE
  69.             // то, что определили в init()
  70.             color = borderColor
  71.             strokeWidth = borderWidth
  72.         }
  73.     }
  74.  
  75.     // Будем вызывать этот метод, если находимся в avatar-моде
  76.     private fun prepareShader(w: Int, h: Int) {
  77.         if (w == 0 || drawable == null) {
  78.             return
  79.         }
  80.         val srcBm = drawable.toBitmap(w, h, Bitmap.Config.ARGB_8888)
  81.         avatarPaint.shader = BitmapShader(srcBm, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
  82.     }
  83.  
  84.     // Не стоит производить всякие вычисления, особенно, свзяанные с размерами в callbackах onMeasure(),
  85.     // так как измерение View происходит в несколько подходов, и будут вызываться в несколько раз
  86.     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
  87.         val initSize = resolveDefultSize(widthMeasureSpec)
  88.         setMeasuredDimension(max(initSize, size), max(initSize, size))
  89.     }
  90.  
  91.     // Все изменения, связанные с размерами View, все вычисления, мы можем прекрасно производить в этом методе,
  92.     // потому что в нём мы уже точно знаем, какие размеры имеет наше View
  93.     override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
  94.         super.onSizeChanged(w, h, oldw, oldh)
  95.         Log.d("AvatarImageView", "onSizeChanged()")
  96.  
  97.         if (w == 0) {
  98.             return
  99.         }
  100.         // Когда уже знаем ширину и высоту BitMap, инициализируем размеры нашего примоугольника,
  101.         // которые ограничивает нашу View
  102.         with(viewRect) {
  103.             top = 0
  104.             left = 0
  105.             right = w
  106.             bottom = h
  107.         }
  108.         prepareShader(w, h)
  109.     }
  110.  
  111.     // Не нужен, т.к. у нашей вьюшки не будет детей
  112.     override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
  113.         super.onLayout(changed, left, top, right, bottom)
  114.         Log.d("AvatarImageView", "onLayout()")
  115.     }
  116.  
  117.     override fun onDraw(canvas: Canvas) {
  118. //        super.onDraw(canvas)
  119.         Log.d("AvatarImageView", "onDraw()")
  120.         // В onDraw() не надо создавать никаких объектов. Это надо делать на более ранних этапах. Например, в setup()
  121.         // Тут только отрисовываем объекты. Иначе будем наблюдать вибрации при изменении размеров и цвеирв.
  122.         // Это связано с тем, что GC будет вычищать объекты, которые были аллоцированы, и это будет
  123.         // снижать производительность нашей View
  124.  
  125.         if (drawable != null && isAvatarMode) {
  126.             drawAvatar(canvas)
  127.         } else {
  128.             drawInitials(canvas)
  129.         }
  130.  
  131.         // уменьшим размер border, а то он чёт вылазит за границы :((
  132.         // но при лонг клике тогда будет уменьшаться каждый раз. Чтобы исправить это, созданим новый квадрат
  133.         val half = (borderWidth / 2).toInt()
  134.         borderRect.set(viewRect)
  135.         borderRect.inset(half, half)
  136.         // нарисуем овал исходной ширины поверх нашего исображения
  137.         canvas.drawOval(borderRect.toRectF(), borderPaint)
  138.     }
  139.  
  140.     // Чтобы текущее состояние сохранялось - нужно обязательно указать нашей View ID!!!
  141.     override fun onSaveInstanceState(): Parcelable? {
  142.         Log.d("AvatarImageView", "onSaveInstanceState() $id")
  143.         val savedState = SavedState(super.onSaveInstanceState())
  144.         savedState.isAvatarMode = isAvatarMode
  145.         savedState.borderWidth = borderWidth
  146.         savedState.borderColor = borderColor
  147.         return savedState
  148.     }
  149.  
  150.     override fun onRestoreInstanceState(state: Parcelable?) {
  151.         Log.d("AvatarImageView", "onRestoreInstanceState() $id")
  152.         if (state is SavedState) {
  153.             super.onRestoreInstanceState(state)
  154.             isAvatarMode = state.isAvatarMode
  155.             borderWidth = state.borderWidth
  156.             borderColor = state.borderColor
  157.  
  158.             with(borderPaint) {
  159.                 color = borderColor
  160.                 strokeWidth = borderWidth
  161.             }
  162.         } else {
  163.             // Возвращаем родительский метод
  164.             super.onRestoreInstanceState(state)
  165.         }
  166.     }
  167.  
  168.     override fun setImageBitmap(bm: Bitmap?) {
  169.         super.setImageBitmap(bm)
  170.         if (isAvatarMode) {
  171.             prepareShader(width, height)
  172.         }
  173.         Log.d("AvatarImageView", "setImageBitmap()")
  174.     }
  175.  
  176.     override fun setImageDrawable(drawable: Drawable?) {
  177.         super.setImageDrawable(drawable)
  178.         if (isAvatarMode) {
  179.             prepareShader(width, height)
  180.         }
  181.         Log.d("AvatarImageView", "setImageDrawable()")
  182.     }
  183.  
  184.     override fun setImageResource(resId: Int) {
  185.         super.setImageResource(resId)
  186.         if (isAvatarMode) {
  187.             prepareShader(width, height)
  188.         }
  189.         Log.d("AvatarImageView", "setImageResource()")
  190.     }
  191.  
  192.     fun setInitials(initials: String) {
  193.         this.initials = initials
  194.         if (!isAvatarMode) {
  195.             invalidate()
  196.         }
  197.     }
  198.  
  199.     fun setBorderColor(@ColorInt borderColor: Int) {
  200.         this.borderColor = borderColor
  201.         borderPaint.color = borderColor
  202.         invalidate()
  203.     }
  204.  
  205.     fun setBorderWidth(@Dimension width: Int) {
  206.         borderWidth = context.dpToPx(width)
  207.         // strokeWidth задает тощину обводки для фигуры
  208.         borderPaint.strokeWidth = borderWidth
  209.         invalidate()
  210.     }
  211.  
  212.     private fun resolveDefultSize(spec: Int): Int {
  213.         return when (MeasureSpec.getMode(spec)) {
  214.             MeasureSpec.UNSPECIFIED -> {
  215.                 context.dpToPx(DEFAULT_SIZE).toInt()
  216.             }
  217.             else -> MeasureSpec.getSize(spec)
  218.         }
  219.     }
  220.  
  221.     private fun drawAvatar(canvas: Canvas) {
  222.         canvas.drawOval(viewRect.toRectF(), avatarPaint)
  223.     }
  224.  
  225.     private fun initialsToColor(letters: String) : Int{
  226.         val b = letters[0].toByte()
  227.         val len = bgColor.size
  228.         val d = b / len.toDouble()
  229.         val index = ((d - truncate(d)) * len).toInt()
  230.         return bgColor[index]
  231.     }
  232.  
  233.     private fun drawInitials(canvas: Canvas) {
  234.         initialsPaint.color = initialsToColor(initials)
  235.         canvas.drawOval(viewRect.toRectF(), initialsPaint)
  236.         with(initialsPaint) {
  237.             color = Color.WHITE
  238.             textAlign = Paint.Align.CENTER
  239.             textSize = height * 0.33f
  240.         }
  241.         val offsetY = (initialsPaint.descent() + initialsPaint.ascent()) / 2
  242.         // Сейчас отрисовка инициалов начинается с верхней части центра. Надо исправить.
  243.         // Исправим путём опускания на половину текущего размера шрифта
  244.         canvas.drawText(initials, viewRect.exactCenterX(), viewRect.exactCenterY() - offsetY, initialsPaint)
  245.     }
  246.  
  247.     private fun toggleMode() {
  248.         isAvatarMode = !isAvatarMode
  249.         invalidate()
  250.     }
  251.  
  252.     // При долгом клике на аватарку она должна превратиться в инициалы
  253.     private fun handleLongClick() : Boolean {
  254.         // анимируем изменение нашей ширины, которая будет изменяться в 2 раза
  255.         val va = ValueAnimator.ofInt(width, 2 * width).apply {
  256.             duration = 600
  257.             // линейное увеличение ширины
  258.             interpolator = LinearInterpolator()
  259.             // чтобы не view не расширялся безгранично. как только изменит нашу ширину и высоту,
  260.             // то начнёт возвращаться в то же положение, из которого пришёл
  261.             repeatMode = ValueAnimator.REVERSE
  262.             repeatCount = 1
  263.  
  264.         }
  265.         va.addUpdateListener {
  266.             // ещё одно свойство AvatarImageView
  267.             size = it.animatedValue as Int
  268.             // когда изменяем size на каждом шаге анимации, нужно это зарегистрировать
  269.             requestLayout()
  270.         }
  271.         // Появился в KTX. Когда анимация начнёт повторяться == ровно середина нашей анимации
  272.         va.doOnRepeat {
  273.             toggleMode()
  274.         }
  275.         va.start()
  276.  
  277.         return true
  278.     }
  279.  
  280.     private class SavedState : BaseSavedState, Parcelable {
  281.         var isAvatarMode: Boolean = true
  282.         var borderWidth: Float = 0f
  283.         var borderColor: Int = 0
  284.  
  285.         constructor(superState: Parcelable?) : super(superState)
  286.  
  287.         // Восстановим наше значение из парсела
  288.         constructor(src: Parcel) : super(src) {
  289.             isAvatarMode = src.readInt() == 1
  290.             borderWidth = src.readFloat()
  291.             borderColor = src.readInt()
  292.         }
  293.  
  294.         override fun writeToParcel(dst: Parcel, flags: Int) {
  295.             super.writeToParcel(dst, flags)
  296.             // Записываем в Parcel
  297.             // Порядок считывания и записи из парсела должен соответствовать друг другу
  298.             dst.writeInt(if (isAvatarMode) 1 else 0)
  299.             dst.writeFloat(borderWidth)
  300.             dst.writeInt(borderColor)
  301.         }
  302.  
  303.         override fun describeContents() = 0
  304.  
  305.         companion object CREATOR : Parcelable.Creator<SavedState> {
  306.             override fun createFromParcel(parcel: Parcel) = SavedState(parcel)
  307.             override fun newArray(size: Int): Array<SavedState?> = arrayOfNulls(size)
  308.         }
  309.     }
  310. }
RAW Paste Data
We use cookies for various purposes including analytics. By continuing to use Pastebin, you agree to our use of cookies as described in the Cookies Policy. OK, I Understand
 
Top