steps

  1. The onDraw method draws an image by drawing a bitmap

  2. Layout the image into the middle of the screen by dynamically calculating the x and y offsets

  3. Calculate the minimum and maximum size ratio of the picture

  4. Zoom test, test the effect of picture scaling

  5. GestureDetectorCompat onTouchEvent to implement the GestureDetector.

    • private gestureDetector = GestureDetectorCompat(context,gestureListener)
    • OnDown: overridden, to return true
    • OnShowPress: a callback method combining pressed and pre-pressed
    • OnSingleTapUp: single lift, implementation logic is slightly different from onClick, in this case can be used as an alternative to onClick, override, return true or false does not affect the process (our event is consumed depending on onDown)
    • OnScroll: to slide, to monitor the slide distance, overwrite, to return true or false regardless (since we are scaling the ImageView in this case)
    • OnScroll is a scroll in which a finger is dragged and a finger is released. In this case, the onScroll scroll is not in place
    • OnLongPress: Similar to onLongClick
    override fun onDown(e: MotionEvent): Return true} Override fun onShowPress(e: override fun onShowPress(e: } Override fun onSingleTapUp(e: MotionEvent): Return false} Override fun onScroll(downEvent: override fun onScroll(downEvent: MotionEvent, currentEvent: MotionEvent, distanceX: Float, distanceY: Float): Boolean {// called when the user slides // The first event is ACTION_DOWN when the user presses, Return false} Override Fun onLongPress(e: override fun onLongPress) MotionEvent) {// This 500ms is now 600ms in GestureDetectorCompat. } override fun onFling(downEvent: MotionEvent, currentEvent: MotionEvent, velocityX: Float, velocityY: Float): Boolean {// used when sliding is quickly lifted, used when the user wants the control to slide inertia return false}Copy the code
  6. Add double click listener GestureDetector OnDoubleTapListener

    • gestureDetector.setOnDoubleTapListener(doubleTapListener);
    • So why implement OnGestureListener: you need to use the onDown callback
    • OnDoubleTap: Two clicks greater than 40ms and less than 300ms are considered double clicks, otherwise false
      • Code ` if (deltaTime > 300 | | deltaTime < 40) {return false.

      } `

    • OnDoubleTapEvent: called when the user double-clicks the second press, when the user moves after the second press, and when the user lifts after the second press
    • OnSingleTapConfirmed: Click to confirm the callback, onSingleTapConfirmed is more accurate than onSingleTapConfirmed because onSingleTapConfirmed takes 300ms
    override fun onSingleTapConfirmed(e: MotionEvent): Boolean {// is called when the user clicks onSingltTapUp(). The difference between onSingltTapUp() and onSingltTapUp() is that this method is not called immediately after the user clicks onSingltTapUp(). Return false} Override fun onDoubleTap(e: MotionEvent): Return false} Override fun onDoubleTapEvent(e: MotionEvent): Override fun onDoubleTapEvent(e: MotionEvent): Boolean {// The user will be called when the user double clicks the second press, when the user moves the second press, and when the user lifts the second press.Copy the code
  7. Mark the state to redraw the picture in the double click event callback

    override fun onDoubleTap(e: MotionEvent?) : Boolean {//7. Mark state redraw picture in double click event callback big =! big invalidate() return true }Copy the code
  8. Objectanimator. ofFloat(this,”scaleFraction”,0f,1f)

  9. Add picture slide support, listen to slide events and processing

    • So I’m going to listen in onScroll for the slide distance
    • Then move the image with Canvas. Translate (x offset,y offset) while drawing
  10. Now double click the picture and touch the moving picture to find that the picture will have a white edge. Now you need to calculate the slide offset by overScroller, set the inertia slide distance of the picture, calculate the offset, and refresh it in the next frame

  11. The offset is reset at the end of the animation callback

  12. Now, double clicking on the image, no matter where it is, double clicking on it will only zoom in along the center point, and what we want to do is zoom in from there, so double clicking on the offset of the contact in the double clicking event, right

  13. The edge correction

Now, we basically have double click to zoom in and out, now we’re going to zoom in and out, save the ScalableImageView code, and create a new ScalableImageView2 to zoom in and out

  1. The interface out for inner class, effectively reduce useless interface implementation code DSHGuestureListener: GestureDetector. SimpleOnGestureListener

  2. Add the pinch gesture action listener

    • ① The inner class listens for the pinch gesture event
    • ② Replace gestureDetector with scaleGestureDetector to listen for onTouch events
    • ③ To change the currentScale scale factor value, onDraw and onSizeChanged should also be changed
    • ④ onScale dynamically calculates the value of currentScale in the onScale listening event
  3. OffsetX and offsetY were fixed in onScaleBegin

  4. The scaling ratio should not be infinitely large or infinitesimal. The range should be limited between minScale and maxScale, otherwise it can be infinitely enlarged or reduced

  5. In onScale, the event consumption return value is processed only if the scale ratio is within a reasonable range, and the code for steps 15, 4, and 17 is noted out

  6. Double click and pinch gesture compatible processing, pinch has a higher priority, priority processing pinch touch events, and note out 15② code

The complete code is as follows

/** 双击和捏撑可以放大缩小的ImageView
 * @author dongshuhuan
 * date 2020/12/18
 * version
 */
private val IMG_SIEZ = 300.dp.toInt()
private const val EXTRA_SACLE_FACTOR = 1.5F

class ScalableImageView2(context: Context?, attrs: AttributeSet?) : View(context, attrs) {

  private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
  private val bitmap = getAvatar(resources,IMG_SIEZ)
  private var originalOffsetX = 0f //原始横向偏移量
  private var originalOffsetY = 0f //原始纵向偏移量
  private var offsetX = 0f //额外滑动横向偏移量
  private var offsetY = 0f //额外滑动纵向偏移量
  private var minScale = 0f //最小缩放比
  private var maxScale = 0f //最大缩放比
  private val dshScaleGestureListener = DSHScaleGestureListener()
  private val dshGuestureListener = DSHGuestureListener()
  private val dshFlingRunnner = DshFlingRunnner()
  private val scaleGestureDetector = ScaleGestureDetector(context,dshScaleGestureListener)
  var big = false//放大了么
  //5&6 手势监听
  private val gestureDetector = GestureDetectorCompat(context,dshGuestureListener)
  //15 ③ 放缩系数值要改变,onDraw和onSizeChanged也需要修改
  private var currentScale = 0f
    set(value) {
      field = value
      invalidate()
    }
  private val scaleAnimator:ObjectAnimator = ObjectAnimator.ofFloat(this,"currentScale",minScale,maxScale)

  private val scroller = OverScroller(context)


  override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    val scaleFraction = (currentScale-minScale)/(maxScale-minScale)
    //9.移动图片②绘制时横移
    canvas.translate(offsetX*scaleFraction,offsetY*scaleFraction)
    //4. 缩放
    canvas.scale(currentScale,currentScale,width/2f,height/2f)
    //1. 绘制图片
    canvas.drawBitmap(bitmap,originalOffsetX,originalOffsetY,paint)
  }


  override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    //2. 图片位置x,y位置偏移量动态计算
    originalOffsetX = (width- IMG_SIEZ)/2f
    originalOffsetY = (height- IMG_SIEZ)/2f
    //3. 要算出较小缩放比值和较大缩放比值
    //宽高比大于屏幕宽高比的,胖图
    if (bitmap.width/bitmap.height.toFloat()>width/height.toFloat()){
      minScale = width/bitmap.width.toFloat()
      maxScale = height/bitmap.height.toFloat()*EXTRA_SACLE_FACTOR
    }else{
      //瘦图
      minScale = height/bitmap.height.toFloat()
      maxScale = width/bitmap.width.toFloat()*EXTRA_SACLE_FACTOR
    }
    currentScale = minScale
    scaleAnimator.setFloatValues(minScale,maxScale)
  }

  override fun onTouchEvent(event: MotionEvent?): Boolean {
//    //5. 覆盖原生onTouchEvent, 挂载手势监听事件
////    return gestureDetector.onTouchEvent(event)
//    //15 ②用scaleGestureDetector替代gestureDetector,监听onTouch事件
//    return scaleGestureDetector.onTouchEvent(event)

    //19. 双击和放缩手势兼容处理,放缩具有较高的优先级,优先处理放缩触摸事件
    scaleGestureDetector.onTouchEvent(event)
    if (!scaleGestureDetector.isInProgress){
      gestureDetector.onTouchEvent(event)
    }
    return true

  }

  private fun fixOffset() {
    //横向滑动边界限制
    offsetX = min(offsetX,(bitmap.width*maxScale-width)/2)
    offsetX = max(offsetX,-(bitmap.width*maxScale-width)/2)
    //纵向滑动边界限制
    offsetY = min(offsetY, (bitmap.height * maxScale - height) / 2)
    offsetY = max(offsetY, -(bitmap.height * maxScale - height) / 2)
  }


  //14 将多个接口抽离为一个类
  inner class DSHGuestureListener:GestureDetector.SimpleOnGestureListener(){

    override fun onDown(e: MotionEvent?): Boolean {
      return true
    }

    override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float): Boolean {
      // 惯性滑动
      // 或快速滑动 如微信列表快速滑动,当我们抬起之后,列表继续滑动,这时候会触发onFling
      if (big){
        //10. ①防止白边处理,设置图片惯性滑动距离,并在下一帧刷新
        scroller.fling(offsetX.toInt(),offsetY.toInt(),velocityX.toInt(),velocityY.toInt(),
            -((bitmap.width*maxScale-width)/2).toInt(), ((bitmap.width*maxScale-width)/2).toInt(),
            -((bitmap.height*maxScale-height)/2).toInt(),((bitmap.height*maxScale-height)/2).toInt(),
            40.dp.toInt(),40.dp.toInt())//两个40.dp代表惯性回弹距离
        //10. ②下一帧刷新
        ViewCompat.postOnAnimation(this@ScalableImageView2,dshFlingRunnner)
      }
      return false
    }

    override fun onScroll(
      downEvent: MotionEvent?,
      currentEvent: MotionEvent?,
      distanceX: Float,
      distanceY: Float): Boolean {
      //9.移动图片①监听滑动距离
      if (big){
        offsetX -= distanceX
        offsetY -= distanceY
        fixOffset()
        invalidate()
      }
      return false;
    }

    override fun onDoubleTap(e: MotionEvent): Boolean {
      //两次点击大于40ms并且小于300ms被认定是双击,否则false
      //注意:第二次触摸到屏幕时就调用,而不是抬起时
      //7. 在双击事件回调中标记状态重新绘制图片
      big = !big
      //8. 放缩动画
      if (big){
        //12. 双击触点偏移修正(防止在任何地方双击都从中心点开始放大),偏移量处理
        offsetX = (e.x-width/2f)*(1-maxScale/minScale)
        offsetY = (e.y-height/2f)*(1-maxScale/minScale)
        //12 end
        //13 边缘修正
        fixOffset()
        scaleAnimator.start()
      }else{
        scaleAnimator.reverse()
      }
      return true
    }
  }

  inner class DshFlingRunnner:Runnable{
    override fun run() {
      if (scroller.computeScrollOffset()) {
        // 把此时的位置应用于界面
        offsetX = scroller.currX.toFloat()
        offsetY = scroller.currY.toFloat()
        invalidate()
        // 下一帧刷新
        ViewCompat.postOnAnimation(this@ScalableImageView2,this)
      }
    }

  }

  //15 ①内部类监听放缩手势事件
  inner class DSHScaleGestureListener:ScaleGestureDetector.OnScaleGestureListener{
    override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
      //16 放缩触点偏移量修正
      offsetX = (detector.focusX-width/2f)*(1-maxScale/minScale)
      offsetY = (detector.focusY-height/2f)*(1-maxScale/minScale)
      return true
    }

    override fun onScaleEnd(detector: ScaleGestureDetector?) {

    }

    override fun onScale(detector: ScaleGestureDetector): Boolean {
      //18 事件消费返回值处理 只有放缩比在合理范围才消费
      val tempCurrentScale = currentScale * detector.scaleFactor
      if (tempCurrentScale<minScale||tempCurrentScale>maxScale){
        return false
      }else{
        currentScale *= detector.scaleFactor//0 1; 0 无穷
        return true
      }
//      //15 ④onScale监听事件中动态计算currentScale的值
//      currentScale *= detector.scaleFactor//0 1; 0 无穷
//      //17 放缩比不能无限大或无限小,要限制范围
//      //coerceAtLeast等效Math.min, coerceAtMost等效Math.max
//      currentScale = currentScale.coerceAtLeast(minScale).coerceAtMost(maxScale)
//      return true
    }

  }

}
Copy the code