package com.application.zhangshi_app_android.widget; import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Canvas; import android.util.AttributeSet; import android.view.GestureDetector; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.View; import android.widget.OverScroller; import android.widget.RelativeLayout; import androidx.annotation.Nullable; import com.application.zhangshi_app_android.R; /** * @author Ljj * @date 2023.04.08. 20:03 * @desc */ public class TreeView extends RelativeLayout { private Context mContext; // 最小缩放比例 float smallScale = 0f; // 正常缩放比例 float normalScale = 0f; // 最大缩放比例 float bigScale = 0f; // 当前比例 float currentScale = 0f; // 缩放倍数 private static final float ZOOM_SCALE = 2f; private final View treeView; private final GestureDetector mGestureDetector; // 双指操作 private final ScaleGestureDetector mScaleGestureDetector; // 惯性滑动 private final OverScroller mOverScroller; private final FlingRunner mFlingRunner; // 是否放大 [默认是缩小,双击改变状态] boolean isZoomIn = false; // 放大后手指移动位置 private final OffSet moveOffset = new OffSet(); private boolean isDoubleClickZoomIn = false; public TreeView(Context context) { this(context, null); } public TreeView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public TreeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public TreeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); setWillNotDraw(false); mContext = context; treeView = LayoutInflater.from(context).inflate(R.layout.layout_tree_view,this,false); LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); layoutParams.alignWithParent = true; layoutParams.addRule(CENTER_IN_PARENT); addView(treeView,layoutParams); mGestureDetector = new GestureDetector(context, new MyGestureListener()); mScaleGestureDetector = new ScaleGestureDetector(context, new MyScaleGestureListener()); mOverScroller = new OverScroller(context); mFlingRunner = new FlingRunner(); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // view比例 float viewScale = (float) getWidth() / (float) getHeight(); // 图片比例 float bitScale = (float) treeView.getMeasuredWidth() / (float) treeView.getMeasuredHeight(); // // 如果图片比例大于view比例 // if (bitScale > viewScale) { // // 横向图片 // smallScale = (float) getWidth() / (float) treeView.getMeasuredWidth(); // bigScale = (float) getHeight() / (float) treeView.getMeasuredHeight() * ZOOM_SCALE; // } else { // // 纵向图片 // smallScale = (float) getHeight() / (float) treeView.getMeasuredHeight(); // bigScale = (float) getWidth() / (float) treeView.getMeasuredWidth() * ZOOM_SCALE; // } smallScale = (float) getWidth() / (float) treeView.getMeasuredWidth(); smallScale = (float) getHeight() / (float) treeView.getMeasuredHeight(); bigScale = (float) getHeight() / (float) treeView.getMeasuredHeight() * ZOOM_SCALE; // 当前缩放比例 = 缩放前的比例 currentScale = smallScale; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.scale(currentScale, currentScale, getWidth() / 2f, getHeight() / 2f); // 移动画布 if (isDoubleClickZoomIn){ if (currentScale <= bigScale) {//双击改变状态时偏移量随着缩放比例变化 float a = (currentScale - smallScale) / (bigScale - smallScale); moveOffset.setPhyX(moveOffset.getPhyX() * a); moveOffset.setPhyY(moveOffset.getPhyY() * a); } } canvas.translate(moveOffset.getLogX(currentScale), moveOffset.getLogY(currentScale)); } @Override public boolean onTouchEvent(MotionEvent event) { // 双指操作 boolean scaleTouchEvent = mScaleGestureDetector.onTouchEvent(event); // 是否是双指操作 if (mScaleGestureDetector.isInProgress()) { return scaleTouchEvent; } // 双击操作 return mGestureDetector.onTouchEvent(event); } private class MyGestureListener extends GestureDetector.SimpleOnGestureListener { // 单击情况 : 抬起[ACTION_UP]时候触发 // 双击情况 : 第二次抬起[ACTION_POINTER_UP]时候触发 @Override public boolean onSingleTapUp(MotionEvent e) { return super.onSingleTapUp(e); } // 长按时触发 [300ms] @Override public void onLongPress(MotionEvent e) { super.onLongPress(e); } // 滑动时候触发 类似 ACTION_MOVE 事件 @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { // 如果是放大状态才能移动 // if (isZoomIn) { moveOffset.setPhyX(moveOffset.getPhyX() - distanceX); moveOffset.setPhyY(moveOffset.getPhyY() - distanceY); // 修正偏移量,防止图片移动时边缘出现白色背景 fixOffset(); invalidate(); // } return super.onScroll(e1, e2, distanceX, distanceY); } // 滑翔/飞翔 [惯性滑动] @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { // mOverScroller.fling( // (int) moveOffset.getX(), // (int) moveOffset.getY(), // (int) velocityX, // (int) velocityY, // (int) (-(treeView.getMeasuredWidth() * bigScale - getWidth()) / 2), // (int) ((treeView.getMeasuredHeight() * bigScale - getWidth()) / 2), // (int) (-(treeView.getMeasuredHeight() * bigScale - getHeight()) / 2), // (int) ((treeView.getMeasuredHeight() * bigScale - getHeight()) / 2), // 300, // 300 // ); // // 设置fling效果 // mFlingRunner.run(); return super.onFling(e1, e2, velocityX, velocityY); } // 延时触发 [100ms] -- 常用与水波纹等效果 @Override public void onShowPress(MotionEvent e) { super.onShowPress(e); } // 按下 这里必须返回true 因为所有事件都是由按下出发的 @Override public boolean onDown(MotionEvent e) { return true; } // 双击 -- 第二次按下时候触发 (40ms - 300ms) [小于40ms是为了防止抖动] @Override public boolean onDoubleTap(MotionEvent e) { if (isDoubleClickZoomIn){ // 判断双击的位置是否在treeView上 float fixedWidth = treeView.getWidth()*(1-currentScale)/2; float fixedHeight = treeView.getHeight()*(1-currentScale)/2; if (treeView.getLeft()+fixedWidth <= e.getX() && e.getX() <= treeView.getRight()-fixedWidth && treeView.getTop()+fixedHeight <= e.getY() && e.getY() <= treeView.getBottom()-fixedHeight){ // 先改为放大,第一次双击是放大效果 isZoomIn = !isZoomIn; if (isZoomIn) { // 双击放大时定位到放大后双击的位置 // float currentX = e.getX() - (float) treeView.getWidth() / 2f; // float currentY = e.getY() - (float) treeView.getHeight() / 2f; // moveOffset.setX(currentX - currentX * bigScale); // moveOffset.setY(currentY - currentY * bigScale); // // 修正偏移量,防止图片移动时边缘出现白色背景 // fixOffset(); // 放大 放大到最大比例 //添加动画,更加流畅 scaleAnimation(currentScale, bigScale).start(); } else { // 缩小 缩小为原先留白的比例 scaleAnimation(bigScale, smallScale).start(); } } } return super.onDoubleTap(e); } // 双击 第二次的事件处理 DOWN MOVE UP 都会执行到这里 @Override public boolean onDoubleTapEvent(MotionEvent e) { return super.onDoubleTapEvent(e); } // 单击时触发 双击时不触发 @Override public boolean onSingleTapConfirmed(MotionEvent e) { return super.onSingleTapConfirmed(e); } } // 缩放动画 public ObjectAnimator scaleAnimation(float start, float end) { ObjectAnimator animator = ObjectAnimator.ofFloat(this, "currentScale", start, end); // 动画时间 animator.setDuration(500); return animator; } // 属性动画的关键!! 内部通过反射调用set方法来赋值 public void setCurrentScale(float currentScale) { this.currentScale = currentScale; invalidate(); } public void fixOffset() { // 当前图片放大后的宽 float currentWidth = treeView.getMeasuredWidth() * currentScale; // 当前图片放大后的高 float currentHeight = treeView.getMeasuredHeight() * currentScale; System.out.println("currentScale = " + currentScale); System.out.println("moveOffset.getLogX(currentScale) = " + moveOffset.getLogX(currentScale)); System.out.println("moveOffset.getLogY(currentScale) = " + moveOffset.getLogY(currentScale)); System.out.println("moveOffset.getPhyX() = " + moveOffset.getPhyX()); System.out.println("moveOffset.getPhyY() = " + moveOffset.getPhyY()); System.out.println("currentWidth = " + currentWidth); System.out.println("currentHeight = " + currentHeight); System.out.println(-(currentWidth - getWidth()) / 2 / bigScale); System.out.println(-(currentWidth - getWidth()) / 2 / currentScale); System.out.println(-(currentWidth - getWidth()) / 2 ); System.out.println(moveOffset.getPhyX()); // 右侧限制 moveOffset.setPhyX(Math.max(moveOffset.getPhyX(), -(currentWidth - getWidth()) / 2)); // 左侧限制 [左侧moveOffset.getX()为负数] moveOffset.setPhyX(Math.min(moveOffset.getPhyX(), (currentWidth - getWidth()) / 2 )); // 下侧限制 moveOffset.setPhyY(Math.max(moveOffset.getPhyY(), -(currentHeight - getHeight()) / 2 )); // 上侧限制 [上侧moveOffset.getY()为负数] moveOffset.setPhyY(Math.min(moveOffset.getPhyY(), (currentHeight - getHeight()) / 2 )); } class MyScaleGestureListener implements ScaleGestureDetector.OnScaleGestureListener { // 在双指操作开始时候获取当前缩放值 private float scaleFactor = 0f; // 双指操作 @Override public boolean onScale(ScaleGestureDetector detector) { // detector.getScaleFactor 缩放因子 currentScale = scaleFactor * detector.getScaleFactor(); // 刷新 invalidate(); return false; } // 双指操作开始 @Override public boolean onScaleBegin(ScaleGestureDetector detector) { scaleFactor = currentScale; // 注意这里要为true 表示开始双指操作 return true; } // 双指操作结束 @Override public void onScaleEnd(ScaleGestureDetector detector) { // 当前图片宽 float currentWidth = treeView.getMeasuredWidth() * currentScale; // 缩放前的图片宽 float smallWidth = treeView.getMeasuredWidth() * smallScale; // 缩放后的图片宽 float bigWidth = treeView.getMeasuredWidth() * bigScale; // 如果当前图片 < 缩放前的图片 if (currentWidth < smallWidth) { // 图片缩小 isZoomIn = false; scaleAnimation(currentScale, smallScale).start(); }else if (currentWidth > bigWidth) {// 如果当前状态 > 缩放后的图片 那么就让他改变为最大的状态 scaleAnimation(currentScale, bigScale).start(); // 双击时候 图片放大 isZoomIn = true; } else{ // 图片缩小 isZoomIn = true; } } } // 惯性滑动辅助 class FlingRunner implements Runnable { @Override public void run() { // 判断当前是否是执行 if (mOverScroller.computeScrollOffset()) { // 设置fling的值 moveOffset.setPhyX(mOverScroller.getCurrX()); moveOffset.setPhyY(mOverScroller.getCurrY()); // 继续执行FlingRunner.run postOnAnimation(this); // 刷新 invalidate(); } } } private static class OffSet { float phyX; float phyY; public OffSet() { } public float getPhyX() { return phyX; } public void setPhyX(float phyX) { this.phyX = phyX; } public float getPhyY() { return phyY; } public void setPhyY(float phyY) { this.phyY = phyY; } public float getLogX(float scale) { return phyX / scale; } public float getLogY(float scale) { return phyY / scale; } } /** * 是否双击放大 */ public void setDoubleClickZoomIn(boolean doubleClickZoomIn) { isDoubleClickZoomIn = doubleClickZoomIn; } }