Advertisement
Guest User

Untitled

a guest
Dec 9th, 2019
137
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 12.59 KB | None | 0 0
  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. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement