自定义手势缩放的Recyclerview

自定义手势缩放的Recyclerview

最近做了一个类似腾讯动漫的漫画的阅读器,用Recyclerview作为基础的控件展示漫画。因为漫画需要支持手势缩放,但是原生Recyclerview并不支持,而且开源的缩放Recyclerview也没有找到,只能自己造一个轮子。这篇文章记录了一些思路。

效果预览图:https://github.com/PortgasAce/ZoomRecyclerView

基本原理

通过重写Recyclerview的dispatchDraw()方法,操作canvas缩放和平移实现手势缩放功能。如果对矩阵熟练的话,可以给canvas设置矩阵实现,但是我不熟练。。所以只能通过最基本的canvas平移和缩放实现。

  protected void dispatchDraw(@NonNull Canvas canvas) {

    canvas.save();
    canvas.translate(mTranX, mTranY);
    canvas.scale(mScaleFactor, mScaleFactor);

    // 所有子view都会缩放和平移
    super.dispatchDraw(canvas);
    canvas.restore();
  }

计算过程

通过上面的代码片段可知,只需要x,y方向的偏移量(mTranx,mTranY)和缩放系数(mScaleFactor)就可以实现缩放。

偏移量的计算

偏移量与另一个值相关,就是缩放中心(双击的触摸点 或者 是双指触摸的中心)。设想一下双击屏幕的的左上角和右下角,缩放系数的值相同,但是偏移量不同,双击左上角的偏移量为(0,0),而右下角的偏移量则为(-MaxTranX,-MaxTranY)。

双击屏幕上一点的示例图如下:

demo


总偏移量:
MaxTranX = X1+X2 = W1(S2-S1)
MaxTranY = Y1+Y2 = H1(S2-S1)
X方向偏移量比总偏移量 等于 缩放中心比屏幕宽度
X1/MaxTranX = X1/W1(S2-S1) = Tx/W1
Y1/MaxTranY = Y1/H1(S2-S1) = Ty/H1

X1 = W1(S2-S1)*(Tx/W1) = (S2-S1)*Tx
Y1 = H1(S2-21)*(Ty/H1) = (S2-S1)*Ty
最终A2点的坐标因为坐标系的原因需要加一个负号:
A2 = (-X1,-Y1) = (-(S2-S1)*Tx,-(S2-S1)*Ty)

缩放中心

双击缩放通过GestureDetector实现,缩放中心在onDoubleTap()方法中直接通过MotionEvent的getX()和getY()获取。

双指缩放通过ScaleDetector实现,缩放中心通过ScaleGestureDetector的getFocusX()和getFocusY()获取。

缩放系数

双击缩放时,如果当前的缩放系数不等于1则缩放系数为1,如果当前缩放系数为1,则缩放系数等于最大缩放系数。

扫描二维码关注公众号,回复: 1476244 查看本文章

双指缩放时,缩放系数为当前缩放系数 乘 onScale回调中detector.getScaleFactor()。

代码

/**
 * 默认缩放比只能为1
 * 缩放动画时长暂时没有根据缩放比例改动
 */
@SuppressWarnings("UnnecessaryLocalVariable")
@SuppressLint("ClickableViewAccessibility")
public class ZoomRecyclerView extends RecyclerView {

  private static final String TAG = "999";

  // constant
  private static final int DEFAULT_SCALE_DURATION = 300;
  private static final float DEFAULT_SCALE_FACTOR = 1.f;
  private static final float DEFAULT_MAX_SCALE_FACTOR = 2.0f;
  private static final float DEFAULT_MIN_SCALE_FACTOR = 0.5f;
  private static final String PROPERTY_SCALE = "scale";
  private static final String PROPERTY_TRANX = "tranX";
  private static final String PROPERTY_TRANY = "tranY";
  private static final float INVALID_TOUCH_POSITION = -1;

  // touch detector
  ScaleGestureDetector mScaleDetector;
  GestureDetectorCompat mGestureDetector;

  // draw param
  float mViewWidth;       // 宽度
  float mViewHeight;      // 高度
  float mTranX;           // x偏移量
  float mTranY;           // y偏移量
  float mScaleFactor;     // 缩放系数

  // touch param
  int mActivePointerId = INVALID_POINTER_ID;  // 有效的手指id
  float mLastTouchX;      // 上一次触摸位置 X
  float mLastTouchY;      // 上一次触摸位置 Y

  // control param
  boolean isScaling = false;    // 是否正在缩放
  boolean isEnableScale = false;// 是否支持缩放

  // zoom param
  ValueAnimator mScaleAnimator; //缩放动画
  float mScaleCenterX;    // 缩放中心 X
  float mScaleCenterY;    // 缩放中心 Y
  float mMaxTranX;        // 当前缩放系数下最大的X偏移量
  float mMaxTranY;        // 当前缩放系数下最大的Y偏移量

  // config param
  float mMaxScaleFactor;      // 最大缩放系数
  float mMinScaleFactor;      // 最小缩放系数
  float mDefaultScaleFactor;  // 默认缩放系数 双击缩小后的缩放系数 暂不支持小于1
  int mScaleDuration;         // 缩放时间 ms

  public ZoomRecyclerView(Context context) {
    super(context);
    init(null);
  }

  public ZoomRecyclerView(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
    init(attrs);
  }

  public ZoomRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    init(attrs);
  }

  private void init(AttributeSet attr) {
    mScaleDetector = new ScaleGestureDetector(getContext(), new ScaleListener());
    mGestureDetector = new GestureDetectorCompat(getContext(), new GestureListener());

    if (attr != null) {
      TypedArray a = getContext()
          .obtainStyledAttributes(attr, R.styleable.ZoomRecyclerView, 0, 0);
      mMinScaleFactor =
          a.getFloat(R.styleable.ZoomRecyclerView_min_scale, DEFAULT_MIN_SCALE_FACTOR);
      mMaxScaleFactor =
          a.getFloat(R.styleable.ZoomRecyclerView_max_scale, DEFAULT_MAX_SCALE_FACTOR);
      mDefaultScaleFactor = a
          .getFloat(R.styleable.ZoomRecyclerView_default_scale, DEFAULT_SCALE_FACTOR);
      mScaleFactor = mDefaultScaleFactor;
      mScaleDuration = a.getInteger(R.styleable.ZoomRecyclerView_zoom_duration,
          DEFAULT_SCALE_DURATION);
      a.recycle();
    } else {
      //init param with default
      mMaxScaleFactor = DEFAULT_MAX_SCALE_FACTOR;
      mMinScaleFactor = DEFAULT_MIN_SCALE_FACTOR;
      mDefaultScaleFactor = DEFAULT_SCALE_FACTOR;
      mScaleFactor = mDefaultScaleFactor;
      mScaleDuration = DEFAULT_SCALE_DURATION;
    }
  }

  @Override
  protected void onDetachedFromWindow() {
    super.onDetachedFromWindow();
  }

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    mViewWidth = MeasureSpec.getSize(widthMeasureSpec);
    mViewHeight = MeasureSpec.getSize(heightMeasureSpec);
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  }

  @Override
  public boolean onTouchEvent(@NonNull MotionEvent ev) {

    if (!isEnableScale) {
      return super.onTouchEvent(ev);
    }

    boolean retVal = mScaleDetector.onTouchEvent(ev);
    retVal = mGestureDetector.onTouchEvent(ev) || retVal;

    int action = ev.getActionMasked();

    switch (action) {
      case ACTION_DOWN: {
        final int pointerIndex = ev.getActionIndex();
        final float x = ev.getX(pointerIndex);
        final float y = ev.getY(pointerIndex);
        // Remember where we started (for dragging)
        mLastTouchX = x;
        mLastTouchY = y;
        // Save the ID of this pointer (for dragging)
        mActivePointerId = ev.getPointerId(0);
        break;
      }
      case ACTION_MOVE: {
        try {
          // Find the index of the active pointer and fetch its position
          final int pointerIndex = ev.findPointerIndex(mActivePointerId);

          final float x = ev.getX(pointerIndex);
          final float y = ev.getY(pointerIndex);

          if (!isScaling && mScaleFactor > 1) { // 缩放时不做处理
            // Calculate the distance moved
            final float dx = x - mLastTouchX;
            final float dy = y - mLastTouchY;

            setTranslateXY(mTranX + dx, mTranY + dy);
            correctTranslateXY();
          }

          invalidate();
          // Remember this touch position for the next move event
          mLastTouchX = x;
          mLastTouchY = y;
        } catch (Exception e) {
          final float x = ev.getX();
          final float y = ev.getY();

          if (!isScaling && mScaleFactor > 1 && mLastTouchX != INVALID_TOUCH_POSITION) { // 缩放时不做处理
            // Calculate the distance moved
            final float dx = x - mLastTouchX;
            final float dy = y - mLastTouchY;

            setTranslateXY(mTranX + dx, mTranY + dy);
            correctTranslateXY();
          }

          invalidate();
          // Remember this touch position for the next move event
          mLastTouchX = x;
          mLastTouchY = y;
        }
        break;
      }
      case ACTION_UP:
      case ACTION_CANCEL:
        mActivePointerId = INVALID_POINTER_ID;
        mLastTouchX = INVALID_TOUCH_POSITION;
        mLastTouchY = INVALID_TOUCH_POSITION;
        break;
      case ACTION_POINTER_UP: {
        final int pointerIndex = ev.getActionIndex();
        final int pointerId = ev.getPointerId(pointerIndex);
        if (pointerId == mActivePointerId) {
          // This was our active pointer going up. Choose a new
          // active pointer and adjust accordingly.
          final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
          mLastTouchX = ev.getX(newPointerIndex);
          mLastTouchY = ev.getY(newPointerIndex);
          mActivePointerId = ev.getPointerId(newPointerIndex);
        }
        break;
      }
    }

    return super.onTouchEvent(ev) || retVal;
  }

  @SuppressLint("WrongConstant")
  @Override
  protected void dispatchDraw(@NonNull Canvas canvas) {

    canvas.save();
    canvas.translate(mTranX, mTranY);
    canvas.scale(mScaleFactor, mScaleFactor);

    // 所有子view都会缩放和平移
    super.dispatchDraw(canvas);
    canvas.restore();
  }

  private void setTranslateXY(float tranX, float tranY) {
    mTranX = tranX;
    mTranY = tranY;
  }

  //当scale 大于 1 时修正action move的位置
  private void correctTranslateXY() {
    float[] correctXY = correctTranslateXY(mTranX, mTranY);
    mTranX = correctXY[0];
    mTranY = correctXY[1];
  }

  private float[] correctTranslateXY(float x, float y) {
    if (mScaleFactor <= 1) {
      return new float[]{x, y};
    }

    if (x > 0.0f) {
      x = 0.0f;
    } else if (x < mMaxTranX) {
      x = mMaxTranX;
    }

    if (y > 0.0f) {
      y = 0.0f;
    } else if (y < mMaxTranY) {
      y = mMaxTranY;
    }
    return new float[]{x, y};
  }

  private void zoom(float startVal, float endVal) {
    if (mScaleAnimator == null) {
      newZoomAnimation();
    }

    if (mScaleAnimator.isRunning()) {
      return;
    }

    //set Value
    mMaxTranX = mViewWidth - (mViewWidth * endVal);
    mMaxTranY = mViewHeight - (mViewHeight * endVal);

    float startTranX = mTranX;
    float startTranY = mTranY;
    float endTranX = mTranX - (endVal - startVal) * mScaleCenterX;
    float endTranY = mTranY - (endVal - startVal) * mScaleCenterY;
    float[] correct = correctTranslateXY(endTranX, endTranY);
    endTranX = correct[0];
    endTranY = correct[1];

    PropertyValuesHolder scaleHolder = PropertyValuesHolder
        .ofFloat(PROPERTY_SCALE, startVal, endVal);
    PropertyValuesHolder tranXHolder = PropertyValuesHolder
        .ofFloat(PROPERTY_TRANX, startTranX, endTranX);
    PropertyValuesHolder tranYHolder = PropertyValuesHolder
        .ofFloat(PROPERTY_TRANY, startTranY, endTranY);
    mScaleAnimator.setValues(scaleHolder, tranXHolder, tranYHolder);
    mScaleAnimator.setDuration(mScaleDuration);
    mScaleAnimator.start();
  }

  private void newZoomAnimation() {
    mScaleAnimator = new ValueAnimator();
    mScaleAnimator.setInterpolator(new DecelerateInterpolator());
    mScaleAnimator.addUpdateListener(new AnimatorUpdateListener() {
      @Override
      public void onAnimationUpdate(ValueAnimator animation) {
        //update scaleFactor & tranX & tranY
        mScaleFactor = (float) animation.getAnimatedValue(PROPERTY_SCALE);
        setTranslateXY(
            (float) animation.getAnimatedValue(PROPERTY_TRANX),
            (float) animation.getAnimatedValue(PROPERTY_TRANY)
        );
        invalidate();
      }
    });

    // set listener to update scale flag
    mScaleAnimator.addListener(new AnimatorListenerAdapter() {

      @Override
      public void onAnimationStart(Animator animation) {
        isScaling = true;
      }

      @Override
      public void onAnimationEnd(Animator animation) {
        isScaling = false;
      }

      @Override
      public void onAnimationCancel(Animator animation) {
        isScaling = false;
      }

    });

  }

  // handle scale event
  private class ScaleListener implements OnScaleGestureListener {

    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {
      return true;
    }

    @Override
    public boolean onScale(ScaleGestureDetector detector) {
      final float mLastScale = mScaleFactor;
      mScaleFactor *= detector.getScaleFactor();
      //修正scaleFactor
      mScaleFactor = Math.max(mMinScaleFactor, Math.min(mScaleFactor, mMaxScaleFactor));

      mMaxTranX = mViewWidth - (mViewWidth * mScaleFactor);
      mMaxTranY = mViewHeight - (mViewHeight * mScaleFactor);

      mScaleCenterX = detector.getFocusX();
      mScaleCenterY = detector.getFocusY();

      float offsetX = mScaleCenterX * (mLastScale - mScaleFactor);
      float offsetY = mScaleCenterY * (mLastScale - mScaleFactor);

      setTranslateXY(mTranX + offsetX, mTranY + offsetY);

      isScaling = true;
      invalidate();
      return true;
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {
      if (mScaleFactor <= mDefaultScaleFactor) {
        mScaleCenterX = -mTranX / (mScaleFactor - 1);
        mScaleCenterY = -mTranY / (mScaleFactor - 1);
        mScaleCenterX = Float.isNaN(mScaleCenterX) ? 0 : mScaleCenterX;
        mScaleCenterY = Float.isNaN(mScaleCenterY) ? 0 : mScaleCenterY;
        zoom(mScaleFactor, mDefaultScaleFactor);
      }
      isScaling = false;
    }
  }

  private class GestureListener extends GestureDetector.SimpleOnGestureListener {

    @Override
    public boolean onDoubleTap(MotionEvent e) {
      float startFactor = mScaleFactor;
      float endFactor;

      if (mScaleFactor == mDefaultScaleFactor) {
        mScaleCenterX = e.getX();
        mScaleCenterY = e.getY();
        endFactor = mMaxScaleFactor;
      } else {
        mScaleCenterX = mScaleFactor == 1 ? e.getX() : -mTranX / (mScaleFactor - 1);
        mScaleCenterY = mScaleFactor == 1 ? e.getY() : -mTranY / (mScaleFactor - 1);
        endFactor = mDefaultScaleFactor;
      }
      zoom(startFactor, endFactor);
      boolean retVal = super.onDoubleTap(e);
      return retVal;
    }
  }

  // public method
  public void setEnableScale(boolean enable) {
    if (isEnableScale == enable) {
      return;
    }
    this.isEnableScale = enable;
    // 禁用了 恢复比例1
    if (!isEnableScale && mScaleFactor != 1) {
      zoom(mScaleFactor, 1);
    }
  }

  public boolean isEnableScale() {
    return isEnableScale;
  }

}

Github:
https://github.com/PortgasAce/ZoomRecyclerView

以上。

参考

google developer scale
google developer scroll

猜你喜欢

转载自blog.csdn.net/qq_15602635/article/details/80157633
今日推荐