注册

自定义View模仿即刻点赞数字切换效果

即刻点赞展示

c5acfd2b4fed4d93b799a9541a3b5dad~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp?

点赞的数字增加和减少并不是整个替换,而是差异化替换。再加上动画效果就看的很舒服。

自己如何实现这种数字切换呢?

下面用一张图来展示我的思路:

29ed8c20ea944349847b91eeb0f1100d~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp?

现在只需要根据这张图,写出对应的动画即可。 分为2种场景:

  • 数字+1:

    • 差异化的数字从3号区域由渐变动画(透明度 0- 255) + 偏移动画 (3号区域绘制文字的基线,2号区域绘制文字的基线),将数字移动到2号位置处

    • 差异化的数字从2号区域由渐变动画(透明度 255- 0) + 偏移动画(2号区域绘制文字的基线,1号区域绘制文字的基线),将数字移动到1号位置处

  • 数字-1

    • 差异化的数字从1号区域由渐变动画(透明度 0- 255) + 偏移动画 (1号区域绘制文字的基线,2号区域绘制文字的基线),将数字移动到2号位置处

    • 差异化的数字从2号区域由渐变动画(透明度 255- 0) + 偏移动画(2号区域绘制文字的基线,3号区域绘制文字的基线),将数字移动到3号位置处

公共部分就是: 不变的文字不需要做任何处理,绘制在2号区域就行。绘制差异化文字时,需要加上不变的文字的宽度就行。

效果展示

380763a2f0494f9aa12bfe11f62bb2a7~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp?

源码

class LikeView @JvmOverloads constructor(
  context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

  private val paint = Paint().also {
      it.isAntiAlias = true
      it.textSize = 200f
  }

  private val textRect0 = Rect(300, 100, 800, 300)
  private val textRect1 = Rect(300, 300, 800, 500)
  private val textRect2 = Rect(300, 500, 800, 700)

  private var nextNumberAlpha: Int = 0
      set(value) {
          field = value
          invalidate()
      }

  private var currentNumberAlpha: Int = 255
      set(value) {
          field = value
          invalidate()
      }

  private var offsetPercent = 0f
      set(value) {
          field = value
          invalidate()
      }

  private val fontMetrics: FontMetrics = paint.fontMetrics
  private var currentNumber = 99
  private var nextNumber = 0
  private var motionLess = currentNumber.toString()
  private var currentMotion = ""
  private var nextMotion = ""

  private val animator: ObjectAnimator by lazy {
      val nextNumberAlphaAnimator = PropertyValuesHolder.ofInt("nextNumberAlpha", 0, 255)
      val offsetPercentAnimator = PropertyValuesHolder.ofFloat("offsetPercent", 0f, 1f)
      val currentNumberAlphaAnimator = PropertyValuesHolder.ofInt("currentNumberAlpha", 255, 0)
      val animator = ObjectAnimator.ofPropertyValuesHolder(
          this,
          nextNumberAlphaAnimator,
          offsetPercentAnimator,
          currentNumberAlphaAnimator
      )
      animator.duration = 200
      animator.interpolator = DecelerateInterpolator()
      animator.addListener(
          onEnd = {
              currentNumber = nextNumber
          }
      )
      animator
  }

  override fun onDraw(canvas: Canvas) {
      paint.alpha = 255

      paint.color = Color.LTGRAY
      canvas.drawRect(textRect0, paint)

      paint.color = Color.RED
      canvas.drawRect(textRect1, paint)

      paint.color = Color.GREEN
      canvas.drawRect(textRect2, paint)

      paint.color = Color.BLACK
      if (motionLess.isNotEmpty()) {
          drawText(canvas, motionLess, textRect1, 0f)
      }

      if (nextMotion.isEmpty() || currentMotion.isEmpty()) {
          return
      }

      val textHorizontalOffset =
          if (motionLess.isNotEmpty()) paint.measureText(motionLess) else 0f
      if (nextNumber > currentNumber) {
          paint.alpha = currentNumberAlpha
          drawText(canvas, currentMotion, textRect1, textHorizontalOffset, -offsetPercent)
          paint.alpha = nextNumberAlpha
          drawText(canvas, nextMotion, textRect2, textHorizontalOffset, -offsetPercent)
      } else {
          paint.alpha = nextNumberAlpha
          drawText(canvas, nextMotion, textRect0, textHorizontalOffset, offsetPercent)
          paint.alpha = currentNumberAlpha
          drawText(canvas, currentMotion, textRect1, textHorizontalOffset, offsetPercent)
      }
  }

  private fun drawText(
      canvas: Canvas,
      text: String,
      rect: Rect,
      textHorizontalOffset: Float = 0f,
      offsetPercent: Float = 0f
  ) {
      canvas.drawText(
          text,
          rect.left.toFloat() + textHorizontalOffset,
          rect.top + (rect.bottom - rect.top) / 2f - (fontMetrics.bottom + fontMetrics.top) / 2f + offsetPercent * 200,
          paint
      )
  }


  override fun onDetachedFromWindow() {
      super.onDetachedFromWindow()
      animator.end()
  }

  fun plus() {
      if (currentNumber == Int.MAX_VALUE) {
          return
      }
      nextNumber = currentNumber + 1

      processText(findEqualsStringIndex())

      if (animator.isRunning) {
          return
      }
      animator.start()
  }

  fun minus() {
      if (currentNumber == 0) {
          return
      }
      nextNumber = currentNumber - 1
      processText(findEqualsStringIndex())
      if (animator.isRunning) {
          return
      }
      animator.start()
  }

  private fun findEqualsStringIndex(): Int {
      var equalIndex = -1
      val nextNumberStr = nextNumber.toString()
      val currentNumberStr = currentNumber.toString()

      val endIndex = min(currentNumberStr.length, nextNumberStr.length) - 1

      for (index in 0..endIndex) {
          if (nextNumberStr[index] != currentNumberStr[index]) {
              break
          }
          equalIndex = index
      }
      return equalIndex
  }

  private fun processText(index: Int) {
      val currentNumberStr = currentNumber.toString()
      val nextNumberStr = nextNumber.toString()
      if (index == -1) {
          motionLess = ""
          currentMotion = currentNumberStr
          nextMotion = nextNumberStr
      } else {
          motionLess = currentNumberStr.substring(0, index + 1)
          currentMotion = currentNumberStr.substring(index + 1)
          nextMotion = nextNumberStr.substring(index + 1)
      }
  }
}

作者:timer
来源:juejin.cn/post/7179181214530551867

0 个评论

要回复文章请先登录注册