---
# System prepended metadata

title: GestureZoom
tags: [Android, Library, Java]

---

###### 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