素材巴巴 > 程序开发 >

自定义手势缩放的Recyclerview

程序开发 2023-09-06 14:09:16

自定义手势缩放的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,则缩放系数等于最大缩放系数。

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

代码

/*** 默认缩放比只能为1* 缩放动画时长暂时没有根据缩放比例改动*/
 @SuppressWarnings("UnnecessaryLocalVariable")
 @SuppressLint("ClickableViewAccessibility")
 public class ZoomRecyclerView extends RecyclerView {private static final String TAG = "999";// constantprivate 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 detectorScaleGestureDetector mScaleDetector;GestureDetectorCompat mGestureDetector;// draw paramfloat mViewWidth;       // 宽度float mViewHeight;      // 高度float mTranX;           // x偏移量float mTranY;           // y偏移量float mScaleFactor;     // 缩放系数// touch paramint mActivePointerId = INVALID_POINTER_ID;  // 有效的手指idfloat mLastTouchX;      // 上一次触摸位置 Xfloat mLastTouchY;      // 上一次触摸位置 Y// control paramboolean isScaling = false;    // 是否正在缩放boolean isEnableScale = false;// 是否支持缩放// zoom paramValueAnimator mScaleAnimator; //缩放动画float mScaleCenterX;    // 缩放中心 Xfloat mScaleCenterY;    // 缩放中心 Yfloat mMaxTranX;        // 当前缩放系数下最大的X偏移量float mMaxTranY;        // 当前缩放系数下最大的Y偏移量// config paramfloat mMaxScaleFactor;      // 最大缩放系数float mMinScaleFactor;      // 最小缩放系数float mDefaultScaleFactor;  // 默认缩放系数 双击缩小后的缩放系数 暂不支持小于1int mScaleDuration;         // 缩放时间 mspublic 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 defaultmMaxScaleFactor = DEFAULT_MAX_SCALE_FACTOR;mMinScaleFactor = DEFAULT_MIN_SCALE_FACTOR;mDefaultScaleFactor = DEFAULT_SCALE_FACTOR;mScaleFactor = mDefaultScaleFactor;mScaleDuration = DEFAULT_SCALE_DURATION;}}@Overrideprotected void onDetachedFromWindow() {super.onDetachedFromWindow();}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {mViewWidth = MeasureSpec.getSize(widthMeasureSpec);mViewHeight = MeasureSpec.getSize(heightMeasureSpec);super.onMeasure(widthMeasureSpec, heightMeasureSpec);}@Overridepublic 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 positionfinal int pointerIndex = ev.findPointerIndex(mActivePointerId);final float x = ev.getX(pointerIndex);final float y = ev.getY(pointerIndex);if (!isScaling && mScaleFactor > 1) { // 缩放时不做处理// Calculate the distance movedfinal float dx = x - mLastTouchX;final float dy = y - mLastTouchY;setTranslateXY(mTranX + dx, mTranY + dy);correctTranslateXY();}invalidate();// Remember this touch position for the next move eventmLastTouchX = 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 movedfinal float dx = x - mLastTouchX;final float dy = y - mLastTouchY;setTranslateXY(mTranX + dx, mTranY + dy);correctTranslateXY();}invalidate();// Remember this touch position for the next move eventmLastTouchX = 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")@Overrideprotected 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 ValuemMaxTranX = 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() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {//update scaleFactor & tranX & tranYmScaleFactor = (float) animation.getAnimatedValue(PROPERTY_SCALE);setTranslateXY((float) animation.getAnimatedValue(PROPERTY_TRANX),(float) animation.getAnimatedValue(PROPERTY_TRANY));invalidate();}});// set listener to update scale flagmScaleAnimator.addListener(new AnimatorListenerAdapter() {@Overridepublic void onAnimationStart(Animator animation) {isScaling = true;}@Overridepublic void onAnimationEnd(Animator animation) {isScaling = false;}@Overridepublic void onAnimationCancel(Animator animation) {isScaling = false;}});}// handle scale eventprivate class ScaleListener implements OnScaleGestureListener {@Overridepublic boolean onScaleBegin(ScaleGestureDetector detector) {return true;}@Overridepublic boolean onScale(ScaleGestureDetector detector) {final float mLastScale = mScaleFactor;mScaleFactor *= detector.getScaleFactor();//修正scaleFactormScaleFactor = 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;}@Overridepublic 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 {@Overridepublic 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 methodpublic void setEnableScale(boolean enable) {if (isEnableScale == enable) {return;}this.isEnableScale = enable;// 禁用了 恢复比例1if (!isEnableScale && mScaleFactor != 1) {zoom(mScaleFactor, 1);}}public boolean isEnableScale() {return isEnableScale;}}

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

以上。

参考

google developer scale
google developer scroll


标签:

上一篇: Weblogic安装与配置详解 下一篇:
素材巴巴 Copyright © 2013-2021 http://www.sucaibaba.com/. Some Rights Reserved. 备案号:备案中。