方案一:ZoomScrollLayout
- ZoomScrollLayout:在内部EditText获取焦点时,会使布局滑动发生偏移
package com.mustang4w.readusbinfo.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.*;
import android.widget.RelativeLayout;
/**
* 可缩放Layout
*/
public class ZoomScrollLayout extends RelativeLayout implements ScaleGestureDetector.OnScaleGestureListener {
private ScaleGestureDetector mScaleDetector;
private GestureDetector mGestureDetector;
private static final float MIN_ZOOM = 0.3f;
private static final float MAX_ZOOM = 3.0f;
private Integer mLeft, mTop, mRight, mBottom;
private int centerX, centerY;
private float mLastScale = 1.0f;
private float totleScale = 1.0f;
// childview
private View mChildView;
// 拦截滑动事件
float mDistansX, mDistansY, mTouchSlop;
private enum MODE {
ZOOM, DRAG, NONE
}
private MODE mode;
boolean touchDown;
public ZoomScrollLayout(Context context) {
super(context);
init(context);
}
public ZoomScrollLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public ZoomScrollLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
public void init(Context context) {
mScaleDetector = new ScaleGestureDetector(context, this);
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (mode == MODE.DRAG) {
if (mChildView == null) {
mChildView = getChildAt(0);
centerX = getWidth() / 2;
centerY = getHeight() / 2;
}
if (mLeft == null) {
mLeft = mChildView.getLeft();
mTop = mChildView.getTop();
mRight = mChildView.getRight();
mBottom = mChildView.getBottom();
}
// 防抖动
if (touchDown) {
touchDown = false;
return true;
}
mLeft = mLeft - (int) distanceX;
mTop = mTop - (int) distanceY;
mRight = mRight - (int) distanceX;
mBottom = mBottom - (int) distanceY;
mChildView.layout(mLeft, mTop, mRight, mBottom);
}
return true;
}
@Override
public boolean onDown(MotionEvent e) {
touchDown = true;
return super.onDown(e);
}
});
// 系统最小滑动距离
mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
int action = e.getActionMasked();
int currentX = (int) e.getX();
int currentY = (int) e.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
//记录上次滑动的位置
mDistansX = currentX;
mDistansY = currentY;
//将当前的坐标保存为起始点
mode = MODE.DRAG;
break;
case MotionEvent.ACTION_MOVE:
if (Math.abs(mDistansX - currentX) >= mTouchSlop || Math.abs(mDistansY - currentY) >= mTouchSlop) { //父容器拦截
return true;
}
break;
//指点杆保持按下,并且进行位移
//有手指抬起,将模式设为NONE
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
mode = MODE.NONE;
break;
}
return super.onInterceptTouchEvent(e);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mScaleDetector.onTouchEvent(event);
mGestureDetector.onTouchEvent(event);
return true;
}
@Override
public boolean onScale(ScaleGestureDetector scaleGestureDetector) {
if (mode == MODE.ZOOM) {
float scaleFactor = scaleGestureDetector.getScaleFactor();
float tempScale = mLastScale * scaleFactor;
if (tempScale <= MAX_ZOOM && tempScale >= MIN_ZOOM) {
totleScale = tempScale;
applyScale(totleScale);
}
}
return false;
}
/**
* 执行缩放操作
*/
public void applyScale(float scale) {
mChildView.setScaleX(scale);
mChildView.setScaleY(scale);
}
@Override
public boolean onScaleBegin(ScaleGestureDetector scaleGestureDetector) {
mode = MODE.ZOOM;
if (mode == MODE.ZOOM) {
if (mChildView == null) {
mChildView = getChildAt(0);
centerX = getWidth() / 2;
centerY = getHeight() / 2;
}
mLeft = mChildView.getLeft();
mTop = mChildView.getTop();
mRight = mChildView.getRight();
mBottom = mChildView.getBottom();
}
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector scaleGestureDetector) {
mLastScale = totleScale;
}
}
方案二:ZoomLayout
- ZoomLayout:内部只能有一个容器,支持双击缩放。可以在初始化时最大最小缩放以及双击缩放比例
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.widget.LinearLayout;
import android.widget.OverScroller;
import com.mustang4w.readusbinfo.R;
import com.mustang4w.readusbinfo.util.ScaleHelper;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
/**
* 简介:与业务无关的缩放 ViewGroup,只能有一个直接子 View
*
* 实现过程:结合自身业务需要,参考了 LargeImageView、ScrollView 来实现
*
* 作用:
* 1、单指、多指滑动及惯性滑动
* 2、双击缩放
* 3、多指缩放
*
* 注意点:
* 1、如果子 View 宽、高小于 ZoomLayout,会将子 View 在宽、高方向上居中
*
*/
public class ZoomLayout extends LinearLayout {
private static final String TAG = "ZoomLayout";
private static final float DEFAULT_MIN_ZOOM = 1.0f;
private static final float DEFAULT_MAX_ZOOM = 4.0f;
private static final float DEFAULT_DOUBLE_CLICK_ZOOM = 2.0f;
private float mDoubleClickZoom;
private float mMinZoom;
private float mMaxZoom;
private float mCurrentZoom = 1;
private int mMinimumVelocity;
private int mMaximumVelocity;
private boolean mScrollBegin; // 是否已经开始滑动
private ScaleGestureDetector mScaleDetector;
private GestureDetector mGestureDetector;
private OverScroller mOverScroller;
private ScaleHelper mScaleHelper;
private AccelerateInterpolator mAccelerateInterpolator;
private DecelerateInterpolator mDecelerateInterpolator;
private ZoomLayoutGestureListener mZoomLayoutGestureListener;
private int mLastChildHeight;
private int mLastChildWidth;
private int mLastHeight;
private int mLastWidth;
private int mLastCenterX;
private int mLastCenterY;
private boolean mNeedReScale;
public ZoomLayout(Context context) {
super(context);
init(context, null);
}
public ZoomLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public ZoomLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}
private void init(Context context, @Nullable AttributeSet attrs) {
mScaleDetector = new ScaleGestureDetector(context, mSimpleOnScaleGestureListener);
mGestureDetector = new GestureDetector(context, mSimpleOnGestureListener);
mOverScroller = new OverScroller(getContext());
mScaleHelper = new ScaleHelper();
final ViewConfiguration configuration = ViewConfiguration.get(getContext());
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
setWillNotDraw(false);
if (attrs != null) {
TypedArray array = null;
try {
array = context.obtainStyledAttributes(attrs, R.styleable.ZoomLayout);
mMinZoom = array.getFloat(R.styleable.ZoomLayout_min_zoom, DEFAULT_MIN_ZOOM);
mMaxZoom = array.getFloat(R.styleable.ZoomLayout_max_zoom, DEFAULT_MAX_ZOOM);
mDoubleClickZoom = array.getFloat(R.styleable.ZoomLayout_double_click_zoom, DEFAULT_DOUBLE_CLICK_ZOOM);
if (mDoubleClickZoom > mMaxZoom) {
mDoubleClickZoom = mMaxZoom;
}
} catch (Exception e) {
Log.e(TAG, TAG, e);
} finally {
if (array != null) {
array.recycle();
}
}
}
}
private ScaleGestureDetector.SimpleOnScaleGestureListener mSimpleOnScaleGestureListener = new ScaleGestureDetector.SimpleOnScaleGestureListener() {
@Override
public boolean onScale(ScaleGestureDetector detector) {
if (!isEnabled()) {
return false;
}
float newScale;
newScale = mCurrentZoom * detector.getScaleFactor();
if (newScale > mMaxZoom) {
newScale = mMaxZoom;
} else if (newScale < mMinZoom) {
newScale = mMinZoom;
}
setScale(newScale, (int) detector.getFocusX(), (int) detector.getFocusY());
return true;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
if (mZoomLayoutGestureListener != null) {
mZoomLayoutGestureListener.onScaleGestureBegin();
}
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
}
};
private GestureDetector.SimpleOnGestureListener mSimpleOnGestureListener = new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
if (!mOverScroller.isFinished()) {
mOverScroller.abortAnimation();
}
return true;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
float newScale;
if (mCurrentZoom < 1) {
newScale = 1;
} else if (mCurrentZoom < mDoubleClickZoom) {
newScale = mDoubleClickZoom;
} else {
newScale = 1;
}
smoothScale(newScale, (int) e.getX(), (int) e.getY());
if (mZoomLayoutGestureListener != null) {
mZoomLayoutGestureListener.onDoubleTap();
}
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (!isEnabled()) {
return false;
}
if (!mScrollBegin) {
mScrollBegin = true;
if (mZoomLayoutGestureListener != null) {
mZoomLayoutGestureListener.onScrollBegin();
}
}
processScroll((int) distanceX, (int) distanceY, getScrollRangeX(), getScrollRangeY());
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (!isEnabled()) {
return false;
}
fling((int) -velocityX, (int) -velocityY);
return true;
}
};
private boolean fling(int velocityX, int velocityY) {
if (Math.abs(velocityX) < mMinimumVelocity) {
velocityX = 0;
}
if (Math.abs(velocityY) < mMinimumVelocity) {
velocityY = 0;
}
final int scrollY = getScrollY();
final int scrollX = getScrollX();
final boolean canFlingX = (scrollX > 0 || velocityX > 0) &&
(scrollX < getScrollRangeX() || velocityX < 0);
final boolean canFlingY = (scrollY > 0 || velocityY > 0) &&
(scrollY < getScrollRangeY() || velocityY < 0);
boolean canFling = canFlingY || canFlingX;
if (canFling) {
velocityX = Math.max(-mMaximumVelocity, Math.min(velocityX, mMaximumVelocity));
velocityY = Math.max(-mMaximumVelocity, Math.min(velocityY, mMaximumVelocity));
int height = getHeight() - getPaddingBottom() - getPaddingTop();
int width = getWidth() - getPaddingRight() - getPaddingLeft();
int bottom = getContentHeight();
int right = getContentWidth();
mOverScroller.fling(getScrollX(), getScrollY(), velocityX, velocityY, 0, Math.max(0, right - width), 0,
Math.max(0, bottom - height), 0, 0);
notifyInvalidate();
return true;
}
return false;
}
public void smoothScale(float newScale, int centerX, int centerY) {
if (mCurrentZoom > newScale) {
if (mAccelerateInterpolator == null) {
mAccelerateInterpolator = new AccelerateInterpolator();
}
mScaleHelper.startScale(mCurrentZoom, newScale, centerX, centerY, mAccelerateInterpolator);
} else {
if (mDecelerateInterpolator == null) {
mDecelerateInterpolator = new DecelerateInterpolator();
}
mScaleHelper.startScale(mCurrentZoom, newScale, centerX, centerY, mDecelerateInterpolator);
}
notifyInvalidate();
}
private void notifyInvalidate() {
// 效果和 invalidate 一样,但是会使得动画更平滑
ViewCompat.postInvalidateOnAnimation(this);
}
public void setScale(float scale, int centerX, int centerY) {
mLastCenterX = centerX;
mLastCenterY = centerY;
float preScale = mCurrentZoom;
mCurrentZoom = scale;
int sX = getScrollX();
int sY = getScrollY();
int dx = (int) ((sX + centerX) * (scale / preScale - 1));
int dy = (int) ((sY + centerY) * (scale / preScale - 1));
if (getScrollRangeX() < 0) {
child().setPivotX(child().getWidth() / 2);
child().setTranslationX(0);
} else {
child().setPivotX(0);
int willTranslateX = -(child().getLeft());
child().setTranslationX(willTranslateX);
}
if (getScrollRangeY() < 0) {
child().setPivotY(child().getHeight() / 2);
child().setTranslationY(0);
} else {
int willTranslateY = -(child().getTop());
child().setTranslationY(willTranslateY);
child().setPivotY(0);
}
child().setScaleX(mCurrentZoom);
child().setScaleY(mCurrentZoom);
processScroll(dx, dy, getScrollRangeX(), getScrollRangeY());
notifyInvalidate();
}
private void processScroll(int deltaX, int deltaY,
int scrollRangeX, int scrollRangeY) {
int oldScrollX = getScrollX();
int oldScrollY = getScrollY();
int newScrollX = oldScrollX + deltaX;
int newScrollY = oldScrollY + deltaY;
final int left = 0;
final int right = scrollRangeX;
final int top = 0;
final int bottom = scrollRangeY;
if (newScrollX > right) {
newScrollX = right;
} else if (newScrollX < left) {
newScrollX = left;
}
if (newScrollY > bottom) {
newScrollY = bottom;
} else if (newScrollY < top) {
newScrollY = top;
}
if (newScrollX < 0) {
newScrollX = 0;
}
if (newScrollY < 0) {
newScrollY = 0;
}
Log.e(TAG, "newScrollX = " + newScrollX + " ,newScrollY = " + newScrollY);
scrollTo(newScrollX, newScrollY);
}
private int getScrollRangeX() {
final int contentWidth = getWidth() - getPaddingRight() - getPaddingLeft();
return (getContentWidth() - contentWidth);
}
private int getContentWidth() {
return (int) (child().getWidth() * mCurrentZoom);
}
private int getScrollRangeY() {
final int contentHeight = getHeight() - getPaddingBottom() - getPaddingTop();
return getContentHeight() - contentHeight;
}
private int getContentHeight() {
return (int) (child().getHeight() * mCurrentZoom);
}
private View child() {
return getChildAt(0);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (mNeedReScale) {
// 需要重新刷新,因为宽高已经发生变化
setScale(mCurrentZoom, mLastCenterX, mLastCenterY);
mNeedReScale = false;
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
child().setClickable(true);
if (child().getHeight() < getHeight() || child().getWidth() < getWidth()) {
setGravity(Gravity.CENTER);
} else {
setGravity(Gravity.TOP);
}
if (mLastChildWidth != child().getWidth() || mLastChildHeight != child().getHeight() || mLastWidth != getWidth()
|| mLastHeight != getHeight()) {
// 宽高变化后,记录需要重新刷新,放在下次 onLayout 处理,避免 View 的一些配置:比如 getTop() 没有初始化好
// 下次放在 onLayout 处理的原因是 setGravity 会在 onLayout 确定完位置,这时候去 setScale 导致位置的变化就不会导致用户看到
// 闪一下的问题
mNeedReScale = true;
}
mLastChildWidth = child().getWidth();
mLastChildHeight = child().getHeight();
mLastWidth = child().getWidth();
mLastHeight = getHeight();
if (mNeedReScale) {
notifyInvalidate();
}
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScaleHelper.computeScrollOffset()) {
setScale(mScaleHelper.getCurScale(), mScaleHelper.getStartX(), mScaleHelper.getStartY());
}
if (mOverScroller.computeScrollOffset()) {
int oldX = getScrollX();
int oldY = getScrollY();
int x = mOverScroller.getCurrX();
int y = mOverScroller.getCurrY();
if (oldX != x || oldY != y) {
final int rangeY = getScrollRangeY();
final int rangeX = getScrollRangeX();
processScroll(x - oldX, y - oldY, rangeX, rangeY);
}
if (!mOverScroller.isFinished()) {
notifyInvalidate();
}
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_UP) {
// 最后一根手指抬起的时候,重置 mScrollBegin 为 false
mScrollBegin = false;
}
mGestureDetector.onTouchEvent(ev);
mScaleDetector.onTouchEvent(ev);
return super.dispatchTouchEvent(ev);
}
@Override
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int usedTotal = getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin +
heightUsed;
final int childHeightMeasureSpec;
if (lp.height == WRAP_CONTENT) {
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
MeasureSpec.UNSPECIFIED);
} else {
childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
}
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
/**
* 是否可以在水平方向上滚动
* 举例: ViewPager 通过这个方法判断子 View 是否可以水平滚动,从而解决滑动冲突
*/
@Override
public boolean canScrollHorizontally(int direction) {
if (direction > 0) {
return getScrollX() < getScrollRangeX();
} else {
return getScrollX() > 0 && getScrollRangeX() > 0;
}
}
/**
* 是否可以在竖直方向上滚动
* 举例: ViewPager 通过这个方法判断子 View 是否可以竖直滚动,从而解决滑动冲突
*/
@Override
public boolean canScrollVertically(int direction) {
if (direction > 0) {
return getScrollY() < getScrollRangeY();
} else {
return getScrollY() > 0 && getScrollRangeY() > 0;
}
}
public void setZoomLayoutGestureListener(ZoomLayoutGestureListener zoomLayoutGestureListener) {
mZoomLayoutGestureListener = zoomLayoutGestureListener;
}
public interface ZoomLayoutGestureListener {
void onScrollBegin();
void onScaleGestureBegin();
void onDoubleTap();
}
}
ScaleHelper类
package com.mustang4w.readusbinfo.util;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
public class ScaleHelper {
private long mStartTime;
private Interpolator mInterpolator;
private float mScale;
private float mToScale;
private int mStartX;
private int mDuration;
private boolean mFinished = true;
private int mStartY;
public void startScale(float scale, float toScale, int x, int y, Interpolator interpolator) {
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mInterpolator = interpolator;
mScale = scale;
mToScale = toScale;
mStartX = x;
mStartY = y;
float d;
if (toScale > scale) {
d = toScale / scale;
} else {
d = scale / toScale;
}
if (d > 4) {
d = 4;
}
//倍数差值越大 执行时间越久 280 - 340
mDuration = (int) (220 + Math.sqrt(d * 3600));
mFinished = false;
}
/**
* Call this when you want to know the new location. If it returns true, the
* animation is not yet finished.
*/
public boolean computeScrollOffset() {
if (isFinished()) {
return false;
}
long time = AnimationUtils.currentAnimationTimeMillis();
// Any scroller can be used for time, since they were started
// together in scroll mode. We use X here.
final long elapsedTime = time - mStartTime;
final int duration = mDuration;
if (elapsedTime < duration) {
final float q = mInterpolator.getInterpolation(elapsedTime / (float) duration);
mScale = mScale + q * (mToScale - mScale);
} else {
mScale = mToScale;
mFinished = true;
}
return true;
}
private boolean isFinished() {
return mFinished;
}
public float getCurScale() {
return mScale;
}
public int getStartX() {
return mStartX;
}
public int getStartY() {
return mStartY;
}
}