###### tags: `Android` `Java` `Library` # GestureZoom 使用情境:進行手勢縮放(畫面、元件)、縮放畫面滾動、保留原本元件的監聽事件 ## 實作說明 ### 創建 GestureViewBinder 類別 ```java= public class GestureViewBinder { private ScaleGestureBinder scaleGestureBinder; private ScrollGestureBinder scrollGestureBinder; private ScaleGestureListener scaleGestureListener; private ScrollGestureListener scrollGestureListener; private View targetView; private ViewGroup viewGroup; private boolean isScaleEnd = true; private OnScaleListener onScaleListener; private boolean isFullGroup = false; public static GestureViewBinder bind(Context context, ViewGroup viewGroup, View targetView) { return new GestureViewBinder(context, viewGroup, targetView); } private GestureViewBinder(Context context, ViewGroup viewGroup, View targetView) { this.targetView = targetView; this.viewGroup = viewGroup; scaleGestureListener = new ScaleGestureListener(targetView, viewGroup); scrollGestureListener = new ScrollGestureListener(targetView, viewGroup); scaleGestureBinder = new ScaleGestureBinder(context, scaleGestureListener); scrollGestureBinder = new ScrollGestureBinder(context, scrollGestureListener); targetView.setClickable(false); viewGroup.setOnTouchListener(new View.OnTouchListener() { @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouch(View v, MotionEvent event) { if (event.getPointerCount() == 1 && isScaleEnd) { return scrollGestureBinder.onTouchEvent(event); } else if (event.getPointerCount() == 2 || !isScaleEnd) { isScaleEnd = event.getAction() == MotionEvent.ACTION_UP; if (isScaleEnd) { scaleGestureListener.onActionUp(); } scrollGestureListener.setScale(scaleGestureListener.getScale()); if (onScaleListener != null) { onScaleListener.onScale(scaleGestureListener.getScale()); } return scaleGestureBinder.onTouchEvent(event); } return false; } }); } private void fullGroup() { targetView.getViewTreeObserver().addOnPreDrawListener( new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { targetView.getViewTreeObserver().removeOnPreDrawListener(this); float viewWidth = targetView.getWidth(); float viewHeight = targetView.getHeight(); float groupWidth = viewGroup.getWidth(); float groupHeight = viewGroup.getHeight(); ViewGroup.LayoutParams layoutParams = targetView.getLayoutParams(); float widthFactor = groupWidth / viewWidth; float heightFactor = groupHeight / viewHeight; if (viewWidth < groupWidth && widthFactor * viewHeight <= groupHeight) { layoutParams.width = (int) groupWidth; layoutParams.height = (int) (widthFactor * viewHeight); } else if (viewHeight < groupHeight && heightFactor * viewWidth <= groupWidth) { layoutParams.height = (int) groupHeight; layoutParams.width = (int) (heightFactor * viewWidth); } targetView.setLayoutParams(layoutParams); return true; } }); } public boolean isFullGroup() { return isFullGroup; } public void setFullGroup(boolean fullGroup) { isFullGroup = fullGroup; scaleGestureListener.setFullGroup(fullGroup); scrollGestureListener.setFullGroup(fullGroup); fullGroup(); } public void setOnScaleListener(OnScaleListener onScaleListener) { this.onScaleListener = onScaleListener; } public interface OnScaleListener { void onScale(float scale); } /** * 縮放 */ class ScaleGestureBinder extends ScaleGestureDetector { ScaleGestureBinder(Context context, ScaleGestureListener scaleGestureListener) { super(context, scaleGestureListener); } @Override public boolean onTouchEvent(MotionEvent event) { return super.onTouchEvent(event); } } class ScaleGestureListener implements ScaleGestureDetector.OnScaleGestureListener/*, GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener */ { private View targetView; private float scale = 1; private float scaleTemp = 1; private boolean isFullGroup = false; ScaleGestureListener(View targetView, ViewGroup viewGroup) { this.targetView = targetView; } @Override public boolean onScale(ScaleGestureDetector detector) { scale = scaleTemp * detector.getScaleFactor(); targetView.setScaleX(scale); targetView.setScaleY(scale); return false; } @Override public boolean onScaleBegin(ScaleGestureDetector detector) { return true; } @Override public void onScaleEnd(ScaleGestureDetector detector) { scaleTemp = scale; } float getScale() { return scale; } public boolean isFullGroup() { return isFullGroup; } void setFullGroup(boolean fullGroup) { isFullGroup = fullGroup; } void onActionUp() { if (isFullGroup && scaleTemp < 1) { scale = 1; targetView.setScaleX(scale); targetView.setScaleY(scale); scaleTemp = scale; } } } /** * 滑動 */ class ScrollGestureBinder extends GestureDetector { ScrollGestureBinder(Context context, ScrollGestureListener scrollGestureListener) { super(context, scrollGestureListener); } } class ScrollGestureListener extends GestureDetector.SimpleOnGestureListener { private float scale = 1; private View targetView; private ViewGroup viewGroup; private float distanceXTemp = 0; private float distanceYTemp = 0; private float viewWidthReal = 0; private float viewHeightReal = 0; private float viewWidthRealTemp = 0; private float viewHeightRealTemp = 0; private boolean isCalculate = false; private int viewWidthNormal = 0; private int viewHeightNormal = 0; private int groupWidth = 0; private int groupHeight = 0; private float maxTranslationLeft = 0; private float maxTranslationTop = 0; private float maxTranslationRight = 0; private float maxTranslationBottom = 0; private boolean isFullGroup = false; ScrollGestureListener(View targetView, ViewGroup viewGroup) { this.targetView = targetView; this.viewGroup = viewGroup; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { distanceX = -distanceX; distanceY = -distanceY; if (isFullGroup || scale > 1) { if (viewWidthReal > groupWidth) { translationXOnScrollEvent(distanceX); } if (viewHeightReal > groupHeight) { translationYOnScrollEvent(distanceY); } } else { translationXOnScrollEvent(distanceX); translationYOnScrollEvent(distanceY); } return super.onScroll(e1, e2, distanceX, distanceY); } private void translationXOnScrollEvent(float distanceX) { //最大移动距离全部为正数,所以需要通过判断distanceX的正负,来判断是向左移动还是向右移动, // 然后通过取distanceX的绝对值来和相应移动方向的最大移动距离比较 if ((distanceX < 0 && Math.abs(distanceXTemp + distanceX) < maxTranslationLeft) || (distanceX > 0 && distanceXTemp + distanceX < maxTranslationRight)) { distanceXTemp += distanceX; targetView.setTranslationX(distanceXTemp); //如果超出边界,就移动到最大距离,防止边界有剩余量 } else if ((distanceX < 0 && Math.abs(distanceXTemp + distanceX) > maxTranslationLeft)) { distanceXTemp = -maxTranslationLeft; targetView.setTranslationX(-maxTranslationLeft); } else if ((distanceX > 0 && distanceXTemp + distanceX > maxTranslationRight)) { distanceXTemp = maxTranslationRight; targetView.setTranslationX(maxTranslationRight); } } private void translationYOnScrollEvent(float distanceY) { if ((distanceY < 0 && Math.abs(distanceYTemp + distanceY) < maxTranslationTop) || (distanceY > 0 && distanceYTemp + distanceY < maxTranslationBottom)) { distanceYTemp += distanceY; targetView.setTranslationY(distanceYTemp); //如果超出边界,就移动到最大距离,防止边界有剩余量 } else if ((distanceY < 0 && Math.abs(distanceYTemp + distanceY) > maxTranslationTop)) { distanceYTemp = -maxTranslationTop; targetView.setTranslationY(-maxTranslationTop); } else if ((distanceY > 0 && distanceYTemp + distanceY > maxTranslationBottom)) { distanceYTemp = maxTranslationBottom; targetView.setTranslationY(maxTranslationBottom); } } @Override public boolean onDown(MotionEvent e) { //计算能移动的最大距离 if (!isCalculate) { isCalculate = true; maxTranslationLeft = targetView.getLeft(); maxTranslationTop = targetView.getTop(); maxTranslationRight = viewGroup.getWidth() - targetView.getRight(); maxTranslationBottom = viewGroup.getHeight() - targetView.getBottom(); viewWidthNormal = targetView.getWidth(); viewHeightNormal = targetView.getHeight(); viewWidthRealTemp = viewWidthNormal; viewHeightRealTemp = viewHeightNormal; viewWidthReal = viewWidthNormal; viewHeightReal = viewHeightNormal; groupWidth = viewGroup.getWidth(); groupHeight = viewGroup.getHeight(); } return true; } void setScale(float scale) { viewWidthReal = viewWidthNormal * scale; viewHeightReal = viewHeightNormal * scale; //如果view比group小 if (viewWidthReal < groupWidth) { if (isFullGroup) { distanceXTemp = 0; targetView.setTranslationX(0); } maxTranslationLeft = targetView.getLeft() - (viewWidthReal - viewWidthNormal) / 2; maxTranslationRight = (viewGroup.getWidth() - targetView.getRight()) - (viewWidthReal - viewWidthNormal) / 2; //如果移动距离超过最大可移动距离 if (scale > this.scale && distanceXTemp < 0 && -distanceXTemp > maxTranslationLeft) { float translate = (viewWidthReal - viewWidthRealTemp) / 2; targetView.setTranslationX(targetView.getTranslationX() + translate); distanceXTemp = distanceXTemp + translate; } else if (scale > this.scale && distanceXTemp > 0 && distanceXTemp > maxTranslationRight) { float translate = (viewWidthReal - viewWidthRealTemp) / 2; targetView.setTranslationX(targetView.getTranslationX() - translate); distanceXTemp = distanceXTemp - translate; } } else { maxTranslationLeft = (viewWidthReal - viewWidthNormal) / 2 - (viewGroup.getWidth() - targetView.getRight()); maxTranslationRight = (viewWidthReal - viewWidthNormal) / 2 - targetView.getLeft(); if (scale < this.scale && distanceXTemp < 0 && -distanceXTemp > maxTranslationLeft) { float translate = (viewWidthRealTemp - viewWidthReal) / 2; targetView.setTranslationX(targetView.getTranslationX() + translate); distanceXTemp = distanceXTemp + translate; } else if (scale < this.scale && distanceXTemp > 0 && distanceXTemp > maxTranslationRight) { float translate = (viewWidthRealTemp - viewWidthReal) / 2; targetView.setTranslationX(targetView.getTranslationX() - translate); distanceXTemp = distanceXTemp - translate; } } if (viewHeightReal < groupHeight) { maxTranslationTop = targetView.getTop() - (viewHeightReal - viewHeightNormal) / 2; maxTranslationBottom = (viewGroup.getHeight() - targetView.getBottom()) - (viewHeightReal - viewHeightNormal) / 2; if (isFullGroup) { distanceYTemp = 0; targetView.setTranslationY(0); } //如果移动距离超过最大可移动距离 if (scale > this.scale && distanceYTemp < 0 && -distanceYTemp > maxTranslationTop) { float translate = (viewHeightReal - viewHeightRealTemp) / 2; targetView.setTranslationY(targetView.getTranslationY() + translate); distanceYTemp = distanceYTemp + translate; } else if (scale > this.scale && distanceYTemp > 0 && distanceYTemp > maxTranslationBottom) { float translate = (viewHeightReal - viewHeightRealTemp) / 2; targetView.setTranslationY(targetView.getTranslationY() - translate); distanceYTemp = distanceYTemp - translate; } } else { maxTranslationTop = (viewHeightReal - viewHeightNormal) / 2 - (viewGroup.getHeight() - targetView.getBottom()); maxTranslationBottom = (viewHeightReal - viewHeightNormal) / 2 - targetView.getTop(); if (scale < this.scale && distanceYTemp < 0 && -distanceYTemp > maxTranslationTop) { float translate = (viewHeightRealTemp - viewHeightReal) / 2; targetView.setTranslationY(targetView.getTranslationY() + translate); distanceYTemp = distanceYTemp + translate; } else if (scale < this.scale && distanceYTemp > 0 && distanceYTemp > maxTranslationBottom) { float translate = (viewHeightRealTemp - viewHeightReal) / 2; targetView.setTranslationY(targetView.getTranslationY() - translate); distanceYTemp = distanceYTemp - translate; } } viewWidthRealTemp = viewWidthReal; viewHeightRealTemp = viewHeightReal; this.scale = scale; } @Override public boolean onSingleTapUp(MotionEvent e) { float left = viewWidthReal > groupWidth ? 0 : (targetView.getLeft() - ((viewWidthReal - viewWidthNormal) / 2)); float top = viewHeightReal > groupHeight ? 0 : (targetView.getTop() - ((viewHeightReal - viewHeightNormal) / 2)); float right = viewWidthReal > groupWidth ? groupWidth : viewGroup.getWidth() - ((viewGroup.getWidth() - targetView.getRight()) - (viewWidthReal - viewWidthNormal) / 2); float bottom = viewHeightReal > groupHeight ? groupHeight : viewGroup.getHeight() - ((viewGroup.getHeight() - targetView.getBottom()) - (viewHeightReal - viewHeightNormal) / 2); RectF rectF = new RectF(left, top, right, bottom); if (rectF.contains(e.getX(), e.getY())) { targetView.performClick(); } return super.onSingleTapUp(e); } public boolean isFullGroup() { return isFullGroup; } void setFullGroup(boolean fullGroup) { isFullGroup = fullGroup; } } } ``` ### 修改 activity_main.xml 主要是新增 ==群組畫面、縮放目標畫面== ```xml= <?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/groupView" tools:context=".MainActivity"> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/targetView" android:background="@color/colorPrimary" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> <ImageView android:id="@+id/imageView" android:layout_width="150dp" android:layout_height="150dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:srcCompat="@android:color/holo_green_dark" /> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:text="Click" app:layout_constraintEnd_toEndOf="@+id/imageView" app:layout_constraintStart_toStartOf="@+id/imageView" app:layout_constraintTop_toBottomOf="@+id/imageView" /> </androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout> ``` ### 修改 MainActivity.java 在 onCreate 中,新增以下程式碼 主要是設定要綁定的群組、目標 完成後就可以進行縮放,同時元件的監聽也不會消失 ```java= ConstraintLayout groupView = findViewById(R.id.groupView); ConstraintLayout targetView = findViewById(R.id.targetView); //綁定 群組畫面 與 目標畫面 GestureViewBinder bind = GestureViewBinder.bind(this, groupView, targetView); //讓目標填滿群組,且不可小於群組大小 bind.setFullGroup(true); //縮放倍數監聽 bind.setOnScaleListener(scale -> Log.i("onScale", scale + "")); findViewById(R.id.button).setOnClickListener(view -> { Toast.makeText(this, "Click", Toast.LENGTH_SHORT).show(); }); ``` ## 參考來源 > https://github.com/Jarvis-Lau/GestureViewBinder