几个自定义拖拽的View

432 阅读16分钟

DragHelperGridView

主要功能是在网格布局中显示子视图,并通过 ViewDragHelper 实现子视图的拖拽效果。
具体功能包括:

  1. 网格布局排列子视图

    • 根据预先定义的列数 (COLUMNS) 和行数 (ROWS) 计算每个子视图的尺寸和位置,然后将它们依次排列成一个网格。
  2. 拖拽交互

    • 使用 ViewDragHelper 拦截触摸事件,并允许用户拖拽任意子视图。
    • 拖拽时,子视图会跟随手指移动。
    • 当用户释放拖拽视图时,视图会通过动画平滑地回到它原先的位置(即布局中对应的网格位置)。
// 假设 COLUMNS 和 ROWS 是全局定义的常量,决定了网格的列数和行数
// 例如:
// private const val COLUMNS = 2
// private const val ROWS = 3

/**
 * 自定义的网格布局视图,支持拖拽操作。
 */
class DragHelperGridView(context: Context, attrs: AttributeSet?) : ViewGroup(context, attrs) {

  // 创建 ViewDragHelper 实例,用于处理拖拽操作,传入自定义的 DragCallback 回调
  private var dragHelper = ViewDragHelper.create(this, DragCallback())

  /**
   * onMeasure:计算 ViewGroup 和子视图的尺寸
   * 每个子视图的宽度 = 父容器宽度 / COLUMNS
   * 每个子视图的高度 = 父容器高度 / ROWS
   */
  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    // 获取父容器的宽度和高度
    val specWidth = MeasureSpec.getSize(widthMeasureSpec)
    val specHeight = MeasureSpec.getSize(heightMeasureSpec)
    // 根据网格计算每个子视图的尺寸
    val childWidth = specWidth / COLUMNS
    val childHeight = specHeight / ROWS
    // 测量所有子视图,要求子视图精确为 childWidth 和 childHeight 大小
    measureChildren(
      MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY),
      MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY)
    )
    // 设置当前 ViewGroup 的尺寸
    setMeasuredDimension(specWidth, specHeight)
  }

  /**
   * onLayout:为每个子视图布局
   * 子视图将按照网格排列,根据索引计算每个视图的左边和顶部位置
   */
  override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
    var childLeft: Int
    var childTop: Int
    // 每个子视图的宽度和高度
    val childWidth = width / COLUMNS
    val childHeight = height / ROWS
    // 遍历所有子视图,并依次进行布局
    for ((index, child) in children.withIndex()) {
      // 计算当前子视图在网格中的列和行位置
      childLeft = index % COLUMNS * childWidth
      childTop = index / COLUMNS * childHeight
      // 布局子视图到对应位置
      child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight)
    }
  }

  /**
   * onInterceptTouchEvent:拦截触摸事件,交由 dragHelper 判断是否需要拦截
   */
  override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
    return dragHelper.shouldInterceptTouchEvent(ev)
  }

  /**
   * onTouchEvent:将触摸事件传递给 dragHelper 处理拖拽逻辑
   */
  override fun onTouchEvent(event: MotionEvent): Boolean {
    dragHelper.processTouchEvent(event)
    return true
  }

  /**
   * computeScroll:在拖拽过程中持续调用,保证拖拽视图平滑回弹
   */
  override fun computeScroll() {
    // 如果 dragHelper 正在继续处理 settle 动画,则请求重绘
    if (dragHelper.continueSettling(true)) {
      ViewCompat.postInvalidateOnAnimation(this)
    }
  }

  /**
   * 内部类 DragCallback:实现 ViewDragHelper.Callback,定义拖拽时的行为
   */
  private inner class DragCallback : ViewDragHelper.Callback() {
    // 记录被捕获视图的初始 left 和 top 坐标
    var capturedLeft = 0f
    var capturedTop = 0f

    /**
     * 尝试捕获一个子视图用于拖拽
     * 这里允许所有子视图都可以被拖拽
     */
    override fun tryCaptureView(child: View, pointerId: Int): Boolean {
      return true
    }

    /**
     * 限制子视图在水平方向的拖动位置
     * 这里不做额外限制,直接返回传入的 left 值
     */
    override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int): Int {
      return left
    }

    /**
     * 限制子视图在垂直方向的拖动位置
     * 这里不做额外限制,直接返回传入的 top 值
     */
    override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int {
      return top
    }

    /**
     * 当视图被捕获拖拽时调用
     * 记录视图的初始位置,并提高其 elevation 使其显示在最上层
     */
    override fun onViewCaptured(capturedChild: View, activePointerId: Int) {
      // 提高捕获视图的层级,确保拖拽时显示在其它视图之上
      capturedChild.elevation = elevation + 1
      // 记录捕获时视图的初始 left 和 top 坐标
      capturedLeft = capturedChild.left.toFloat()
      capturedTop = capturedChild.top.toFloat()
    }

    /**
     * 当视图位置发生变化时调用
     * 当前没有实现额外逻辑
     */
    override fun onViewPositionChanged(changedView: View, left: Int, top: Int, dx: Int, dy: Int) {
      // 可以在此处理拖动过程中的其他逻辑(如实时更新位置等),目前为空
    }

    /**
     * 当拖拽释放时调用
     * 将视图通过动画平滑返回其初始位置
     */
    override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) {
      // 使用 settleCapturedViewAt 使视图回到记录的初始位置
      dragHelper.settleCapturedViewAt(capturedLeft.toInt(), capturedTop.toInt())
      // 请求重绘,确保动画能够顺利进行
      postInvalidateOnAnimation()
    }
  }
}

重要逻辑代码说明:

  1. onMeasure()

    • onMeasure 中,计算了每个子视图的宽高,并通过 measureChildren() 来测量所有的子视图。
  2. onLayout()

    • onLayout 中,将每个子视图按照网格排列的方式布局在父视图中,使用行列计算每个子视图的位置。
  3. onInterceptTouchEvent()onTouchEvent()

    • 通过 ViewDragHelper 来拦截和处理触摸事件,ViewDragHelper 会判断是否需要拦截当前的触摸事件并处理拖拽。
  4. DragCallback 内部类

    • tryCaptureView() :决定哪些子视图可以被拖拽。这里返回 true 表示所有的子视图都可以被拖拽。

    • clampViewPositionHorizontal()clampViewPositionVertical() :决定拖拽视图的位置限制。当前的实现没有限制拖动区域。clampViewPositionHorizontal()clampViewPositionVertical() 方法通过返回 lefttop,意味着它们允许视图继续移动到计算出来的位置。如果希望限制视图的拖动范围,可以在这两个方法中调整返回值,例如:

      • 限制最小值:Math.max(minX, left)
      • 限制最大值:Math.min(maxX, left)

但在当前实现中,返回 lefttop 表示视图可以根据拖动过程自由地在水平方向和垂直方向上移动,而没有设置任何约束。

  • onViewCaptured() :当一个视图被捕获时,记录该视图的初始位置,并提高该视图的elevation,确保它在其他视图之上。
  • onViewReleased() :当视图释放时,视图会返回到初始位置。

预览图:

screenrecord.gif

DragListenerGridView

提供一个固定网格布局,并允许用户通过拖拽改变子视图的位置顺序,完成一个直观的拖拽排序效果。

// 定义网格的列数和行数
private const val COLUMNS = 2
private const val ROWS = 3

/**
 * 自定义 ViewGroup,用于实现一个可以拖拽排序的网格布局
 */
class DragListenerGridView(context: Context, attrs: AttributeSet?) : ViewGroup(context, attrs) {
  
  // 创建拖拽监听器,负责处理子视图的拖拽事件
  private var dragListener: OnDragListener = HenDragListener()
  
  // 保存当前正在被拖拽的视图
  private var draggedView: View? = null
  
  // 用于记录子视图的顺序,初始顺序即为添加到布局时的顺序
  private var orderedChildren: MutableList<View> = ArrayList()

  init {
    // 开启子视图绘制顺序的自定义排序功能
    isChildrenDrawingOrderEnabled = true
  }

  /**
   * 当布局中的所有子视图都被加载完毕后调用
   * 初始化子视图的排序和设置拖拽相关的事件监听器
   */
  override fun onFinishInflate() {
    super.onFinishInflate()
    // 遍历所有子视图
    for (child in children) {
      // 将每个子视图添加到 orderedChildren 列表中,记录初始顺序
      orderedChildren.add(child)
      
      // 为每个子视图设置长按监听器,触发拖拽操作
      child.setOnLongClickListener { v ->
        // 记录当前正在被拖拽的视图
        draggedView = v
        // 启动拖拽操作(不传递额外数据,使用默认的拖拽阴影)
        v.startDrag(null, DragShadowBuilder(v), v, 0)
        false // 返回 false,表示不消耗点击事件
      }
      
      // 为每个子视图设置拖拽监听器,处理拖拽过程中的事件
      child.setOnDragListener(dragListener)
    }
  }

  /**
   * 重写 onDragEvent,可以在这里对整个 ViewGroup 的拖拽事件进行处理
   * 当前直接调用父类实现
   */
  override fun onDragEvent(event: DragEvent?): Boolean {
    return super.onDragEvent(event)
  }

  /**
   * 测量子视图的尺寸,每个子视图均分父容器的宽度和高度
   */
  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    // 获取父容器宽度和高度的尺寸
    val specWidth = MeasureSpec.getSize(widthMeasureSpec)
    val specHeight = MeasureSpec.getSize(heightMeasureSpec)
    // 计算每个子视图的宽度和高度
    val childWidth = specWidth / COLUMNS
    val childHeight = specHeight / ROWS
    // 测量所有子视图,确保其尺寸为 EXACTLY 指定的宽高
    measureChildren(
      MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY),
      MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY)
    )
    // 设置当前 ViewGroup 的尺寸
    setMeasuredDimension(specWidth, specHeight)
  }

  /**
   * 布局子视图
   * 每个子视图初始位置均为 (0,0)-(childWidth,childHeight),然后通过 translationX/Y 移动到正确的位置
   */
  override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
    var childLeft: Int
    var childTop: Int
    // 根据网格计算每个子视图的宽度和高度
    val childWidth = width / COLUMNS
    val childHeight = height / ROWS
    // 遍历所有子视图,使用索引计算其目标位置
    for ((index, child) in children.withIndex()) {
      // 根据列和行计算目标坐标
      childLeft = index % 2 * childWidth
      childTop = index / 2 * childHeight
      // 布局子视图到初始位置(这里都放在 (0,0)-(childWidth,childHeight))
      child.layout(0, 0, childWidth, childHeight)
      // 利用平移属性将子视图移动到正确的网格位置
      child.translationX = childLeft.toFloat()
      child.translationY = childTop.toFloat()
    }
  }

  /**
   * 内部拖拽监听器,用于处理各个拖拽事件
   */
  private inner class HenDragListener : OnDragListener {
    override fun onDrag(v: View, event: DragEvent): Boolean {
      // 根据拖拽事件的不同阶段做出不同的响应
      when (event.action) {
        // 当拖拽操作开始时
        DragEvent.ACTION_DRAG_STARTED -> 
          // 如果当前拖拽的数据来源就是此视图,则将其隐藏(拖拽期间显示阴影)
          if (event.localState === v) {
            v.visibility = View.INVISIBLE
          }
        
        // 当拖拽进入某个视图区域时
        DragEvent.ACTION_DRAG_ENTERED -> 
          // 如果当前拖拽的数据来源不是这个视图,则触发排序逻辑
          if (event.localState !== v) {
            sort(v)
          }
        
        // 拖拽退出某个视图区域时(此处没有做处理)
        DragEvent.ACTION_DRAG_EXITED -> {
          // 可在这里添加拖拽退出时的逻辑
        }
        
        // 当拖拽操作结束时
        DragEvent.ACTION_DRAG_ENDED -> 
          // 如果当前视图是拖拽操作的源视图,则恢复其可见性
          if (event.localState === v) {
            v.visibility = View.VISIBLE
          }
      }
      return true // 返回 true 表示该事件已被处理
    }
  }

  /**
   * 根据目标视图重新排列 orderedChildren 列表中的子视图顺序,并通过动画调整位置
   * @param targetView 拖拽进入的目标视图
   */
  private fun sort(targetView: View) {
    var draggedIndex = -1
    var targetIndex = -1
    // 遍历 orderedChildren,找到拖拽视图和目标视图的索引
    for ((index, child) in orderedChildren.withIndex()) {
      if (targetView === child) {
        targetIndex = index
      } else if (draggedView === child) {
        draggedIndex = index
      }
    }
    // 移除原先拖拽视图的位置
    orderedChildren.removeAt(draggedIndex)
    // 将拖拽视图插入到目标视图所在的位置
    orderedChildren.add(targetIndex, draggedView!!)
    
    // 重新计算每个子视图在网格中的位置,并通过动画平滑移动到目标位置
    var childLeft: Int
    var childTop: Int
    val childWidth = width / COLUMNS
    val childHeight = height / ROWS
    for ((index, child) in orderedChildren.withIndex()) {
      // 根据新顺序计算每个子视图的目标位置
      childLeft = index % 2 * childWidth
      childTop = index / 2 * childHeight
      // 使用动画平滑地移动视图到新位置
      child.animate()
        .translationX(childLeft.toFloat())
        .translationY(childTop.toFloat())
        .setDuration(150)
    }
  }
}

子视图按照固定的网格(2 列 × 3 行)排列,并支持如下操作:

  1. 初始化布局和顺序:

    • onFinishInflate() 方法中,将所有子视图按初始顺序存入 orderedChildren 列表,并为每个子视图设置长按事件监听器和拖拽监听器。
  2. 启动拖拽操作:

    • 当用户长按某个子视图时,该视图会启动拖拽操作,同时保存为 draggedView。拖拽过程中,该视图会临时变为不可见。
  3. 拖拽过程中的排序:

    • 当拖拽的视图进入其他子视图区域(即触发 ACTION_DRAG_ENTERED 事件),会调用 sort(targetView) 方法。这个方法根据当前拖拽的视图和目标视图在 orderedChildren 中的位置,重新调整列表中各个视图的顺序。
  4. 动画调整位置:

    • sort 方法中,根据新的顺序,计算每个子视图在网格中的目标位置,然后通过动画效果(调整 translationXtranslationY)使它们平滑地移动到新的位置。
  5. 布局绘制:

    • onMeasure 中,根据父容器的尺寸将每个子视图的尺寸均分为固定大小。
    • onLayout 中,所有子视图都被放置在相同的初始位置(通过调用 layout(0, 0, childWidth, childHeight)),然后利用平移属性(translationXtranslationY)将它们移动到各自的网格位置。

预览图:

screenrecord_20250205_194127.gif

DragToCollectLayout

实现了一个自定义的 DragToCollectLayout 类,继承自 ConstraintLayout,用于处理拖放操作。

  • 这个布局支持拖放操作,用户可以长按 avatarViewlogoView 开始拖拽,并将拖拽的内容(contentDescription)放入一个 LinearLayoutcollectorLayout)。
  • 当拖拽操作完成时,会在目标 LinearLayout 中添加一个新的 TextView 来显示拖拽的数据内容。
class DragToCollectLayout(context: Context, attrs: AttributeSet?) : ConstraintLayout(context, attrs) {

  // 创建一个 OnLongClickListener,用于启动拖拽操作
  private var dragStarter = OnLongClickListener { v ->
    // 创建一个 ClipData,包含视图的 contentDescription 数据,用于传递拖拽的数据
    val imageData = ClipData.newPlainText("name", v.contentDescription)
    // 启动拖拽操作,传递视图、拖拽数据、拖拽阴影构建器、目标视图(null)和拖拽标志(0)
    ViewCompat.startDragAndDrop(v, imageData, DragShadowBuilder(v), null, 0)
  }

  // 创建一个 OnDragListener,用于处理拖拽事件
  private var dragListener: OnDragListener = CollectListener()

  override fun onFinishInflate() {
    super.onFinishInflate()
    // 给 avatarView 和 logoView 设置长按监听器,启动拖拽操作
    avatarView.setOnLongClickListener(dragStarter)
    logoView.setOnLongClickListener(dragStarter)
    // 给 collectorLayout 设置拖拽监听器,处理拖拽事件
    collectorLayout.setOnDragListener(dragListener)
  }

  // 定义一个内嵌类,用于处理拖拽事件
  inner class CollectListener : OnDragListener {
    override fun onDrag(v: View, event: DragEvent): Boolean {
      when (event.action) {
        // 当发生拖放事件时(ACTION_DROP)
        DragEvent.ACTION_DROP -> if (v is LinearLayout) {
          // 创建一个新的 TextView,用于显示拖拽的数据
          val textView = TextView(context)
          textView.textSize = 16f  // 设置文本大小
          // 设置 TextView 的文本为拖拽数据中的内容
          textView.text = event.clipData.getItemAt(0).text
          // 将创建的 TextView 添加到目标 LinearLayout 中
          v.addView(textView)
        }
      }
      return true
    }
  }
}

包括以下几个主要功能:

  1. 拖拽启动:

    • dragStarter 是一个 OnLongClickListener,它监听视图(avatarViewlogoView)的长按事件。
    • 当用户在这些视图上执行长按时,代码会创建一个 ClipData,其中包含视图的 contentDescription(即拖动的内容),然后启动拖拽操作。
  2. 拖拽监听:

    • dragListener 是一个 OnDragListener,它监听拖拽过程的事件。这个监听器被绑定到 collectorLayout 上。

    • 当发生拖拽事件时,CollectListener 类中的 onDrag() 方法会处理不同的拖拽动作。特别是,当发生 ACTION_DROP(即拖放操作完成)时:

      • 如果目标视图是 LinearLayout,会创建一个新的 TextView,将其文本设置为拖拽数据中的文本内容,并将这个 TextView 添加到目标 LinearLayout 中。

预览效果:

screenrecord_20250205_192221.gif

DragUpDownLayout

一个可以垂直拖动子视图的布局,类似于一个可拖动的面板。核心功能是通过 ViewDragHelper 实现子视图的上下拖动,并在用户释放时根据拖动的速度和位置自动将视图回弹到顶部或底部。

class DragUpDownLayout(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) {
  
  // 创建一个 ViewDragHelper.Callback 对象,用于处理拖动事件的回调
  private var dragListener: ViewDragHelper.Callback = DragCallback()

  // 创建一个 ViewDragHelper 实例,它将根据拖动事件来处理视图的拖动逻辑
  private var dragHelper: ViewDragHelper = ViewDragHelper.create(this, dragListener)

  // 获取系统的 ViewConfiguration,用来获取一些拖动和滚动的参数(如最小滑动速度)
  private var viewConfiguration: ViewConfiguration = ViewConfiguration.get(context)

  // 重写 onInterceptTouchEvent 方法,用来判断是否需要拦截触摸事件
  override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
    // 使用 ViewDragHelper 来判断是否应该拦截当前的触摸事件
    return dragHelper.shouldInterceptTouchEvent(ev)
  }

  // 处理触摸事件,传递给 ViewDragHelper 来处理
  override fun onTouchEvent(event: MotionEvent): Boolean {
    // 将触摸事件交给 dragHelper 处理,进行视图拖动
    dragHelper.processTouchEvent(event)
    return true  // 返回 true 表示事件已经被处理
  }

  // 计算滚动,确保视图可以继续滚动(例如回弹动画等)
  override fun computeScroll() {
    // 如果 dragHelper 仍然在处理滚动(例如视图回弹),继续重绘
    if (dragHelper.continueSettling(true)) {
      ViewCompat.postInvalidateOnAnimation(this)  // 继续请求重绘来实现动画效果
    }
  }

  // 内部类 DragCallback 继承自 ViewDragHelper.Callback,负责处理具体的拖动逻辑
  internal inner class DragCallback : ViewDragHelper.Callback() {
    // 尝试捕获视图,只有当拖动的视图是被指定的视图时,才允许进行拖动
    override fun tryCaptureView(child: View, pointerId: Int): Boolean {
      // 只有指定的 draggedView 可以被捕获进行拖动
      return child === draggedView
    }

    // 限制子视图的垂直位置,确保它只能在上下方向内拖动
    override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int {
      // 直接返回 top,表示没有限制拖动范围
      return top
    }

    // 视图释放时的回调,处理视图回弹到顶部或底部的逻辑
    override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) {
      // 如果拖动的速度超过了最小速度阈值,则按速度决定回弹的方向
      if (Math.abs(yvel) > viewConfiguration.scaledMinimumFlingVelocity) {
        if (yvel > 0) {
          // 如果速度大于 0,则将视图回弹到底部
          dragHelper.settleCapturedViewAt(0, height - releasedChild.height)
        } else {
          // 如果速度小于 0,则将视图回弹到顶部
          dragHelper.settleCapturedViewAt(0, 0)
        }
      } else {
        // 如果拖动速度较慢,根据当前位置决定回弹位置
        if (releasedChild.top < height - releasedChild.bottom) {
          // 如果视图的上边距小于距离底部的距离,则回弹到顶部
          dragHelper.settleCapturedViewAt(0, 0)
        } else {
          // 否则回弹到底部
          dragHelper.settleCapturedViewAt(0, height - releasedChild.height)
        }
      }
      // 请求重新绘制,以确保回弹动画的效果
      postInvalidateOnAnimation()
    }
  }
}

代码关键功能:

  1. onInterceptTouchEvent 用于拦截触摸事件,如果 ViewDragHelper 判断该事件需要处理,它会返回 true,使得 onTouchEvent 中的事件由 ViewDragHelper 继续处理。

  2. onTouchEvent 处理触摸事件,将所有触摸事件交给 dragHelper 处理,确保视图可以拖动。

  3. computeScroll 计算是否继续滚动,保证视图可以平滑地回弹到目标位置。

  4. DragCallback 类:

    • tryCaptureView:只有指定的视图(draggedView)才会被捕获进行拖动。
    • clampViewPositionVertical:限制视图在垂直方向的拖动位置。
    • onViewReleased:处理视图释放时的回弹逻辑,根据拖动速度或释放位置决定回弹位置。

预览图: screenrecord_20250205_193117.gif

postInvalidateOnAnimation()animate()的区别

虽然 postInvalidateOnAnimation()animate() 都会导致视图在下一帧被重绘(从而调用 onDraw() 方法),但它们的设计目的和使用场景是不同的,主要区别如下:

  1. 抽象层次不同

    • animate()

      • 属于高层次的属性动画 API(ViewPropertyAnimator),它允许我们以声明式的方式对视图的各种属性(如 translationXalphascaleX 等)进行动画处理。
      • 当调用 animate() 并设置动画参数时,系统会自动计算每一帧的中间值,并更新视图属性,最终导致系统自动重绘视图。
      • 开发者只需关心动画的起始值、结束值、持续时间和插值器等,而不需要手动控制每一帧的绘制。
    • postInvalidateOnAnimation()

      • 属于低层次的重绘调度 API。它的主要作用是请求系统在下一帧调用视图的 onDraw() 方法。
      • 这种方法常用于自定义视图中,当我们自己管理动画逻辑或状态更新(例如在 computeScroll() 或自定义的动画循环中)时,需要显式地告诉系统“我的视图状态改变了,请在下一帧重绘”。
      • 它并不自动计算属性的变化,而是依赖开发者手动更新状态后,再通过重绘显示这些变化。
  2. 使用场景不同

    • animate() 适用于简单、常见的属性动画场景。

      • 当只需要改变视图的某个或几个属性,并希望系统自动处理动画过程时,使用 animate() 更为便捷。
      • 系统内部会自动调度每一帧的更新和重绘,无需关心底层细节。
    • postInvalidateOnAnimation() 则适用于需要完全自定义动画逻辑的场景。

      • 在自定义绘制或复杂动画场景中,可能需要自己计算每一帧的中间状态,然后通过 postInvalidateOnAnimation() 来确保每一帧都重新绘制视图以反映最新状态。
      • 这种方式给予开发者更高的控制权,但同时也需要开发者处理更多底层细节。

总结

  • animate()

    • 是一个高层次的属性动画 API,简化了动画开发过程。
    • 自动更新视图属性和调度重绘,适合大多数常见动画需求。
  • postInvalidateOnAnimation()

    • 是一个低层次的重绘调度方法,用于请求下一帧的重绘。
    • 适用于自定义动画逻辑,开发者需要自己管理状态和绘制。

因此,尽管两者都会触发 onDraw(),它们服务于不同的抽象层次和使用场景。提供两个 API 可以让开发者根据需求选择最适合的工具:简单动画直接用 animate(),而复杂或自定义动画则使用 postInvalidateOnAnimation() 来手动控制绘制过程。