精华内容
下载资源
问答
  • Android图片随手势放大缩小,效果非常不错,下载后可以直接运行项目观看。
  • 1、首先下载npm install hammerjs 2、在main.ts引入import ‘hammerjs’; 3、在app.module.ts对手势进行配置; import {HammerGestureConfig, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser'; export...

    基于angular的项目引入hammerjs手势操作。
    1、首先下载npm install hammerjs
    2、在main.ts引入import ‘hammerjs’;
    3、在app.module.ts对手势进行配置;

     import {HammerGestureConfig, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
     
     export class MyHammerConfig extends HammerGestureConfig {
        overrides: any = {
            // override hammerjs default configuration
            'pan': {threshold: 5},
            'swipe': {
                velocity: 0.4,
                threshold: 20,
                direction: 31
            },
            'press': {}
        };
    }
    
    
    	@NgModule({
    		providers:[
    		 {
    	            provide: HAMMER_GESTURE_CONFIG,
    	            useClass: MyHammerConfig
    	        }
    		]
    	})
    

    下面进入正题:我是把这个操作封装成指令了,引用的时候引入这个指令,然后在标签添加appDoubleGesture属性

    import {Directive, Input, ElementRef, Renderer2} from '@angular/core';
    import {DomController} from "@ionic/angular";
    
    
    @Directive({
      selector: '[appDoubleGesture]'
    })
    export class DoubleGestureDirective {
      tMatrix:Array<number> = [1,0,0,1,0,0]; //x缩放,无,无,y缩放,x平移,y平移
      initScale: number = 1;//初始化scale
      el:any;
      ticking:boolean = false;
      poscenter:any;
      duration:string='';//设置过渡效果,用于双击缩放效果
      lastTranslate:any;
      lastcenter:any;
      center:any;
      reqAnimationFrame:Function;
      constructor(public element: ElementRef, public renderer: Renderer2, public domCtrl: DomController) {
      }
    
      
      ngAfterViewInit() {
        let mc = new window['Hammer'](this.element.nativeElement);
        this.el =  this.element.nativeElement;//获取元素
        this.poscenter = this.point2D(0,0);//缓存双指的中心坐标
        this.lastTranslate = this.point2D(0,0);//记录上次的偏移值
        this.lastcenter= this.point2D(this.el.offsetWidth/2,this.el.offsetHeight/2)//图像的中心点,用于对比双指中心点
        this.center = this.lastcenter;
        mc.get('pan').set({ threshold: 0, pointers: 1 });//平移
        mc.get('pinch').set({ threshold: 0,enable: true});//捏放,默认禁止,所以要开启enable:true
        mc.get('tap').set({ event: 'doubletap', taps: 2 });//点击
        mc.on("panmove", this.onPan.bind(this));
        mc.on("panstart",this.onPanStart.bind(this))
        mc.on("pinchmove", this.onPinch.bind(this));
        mc.on("pinchstart",this.onPinchStart.bind(this));
        mc.on("doubletap",this.onDoubleTap.bind(this));//双击放大
        this.reqAnimationFrame = (function() {
          return window[Hammer.prefixed(window, 'requestAnimationFrame')] || function (callback) {
            window.setTimeout(callback, 1000 / 60);
          };
        })();
        this.requestElementUpdate();
      }
    
     
    
      point2D(x,y){
        return {x : x,y : y};
      }
    
      onPanStart(ev){
        this.lastTranslate = this.point2D(this.tMatrix[4],this.tMatrix[5])//缓存上一次的偏移值
      }
      onPan(ev){	
          this.duration = ''
          this.el.className = ''			
          this.tMatrix[4] = this.lastTranslate.x + ev.deltaX
          this.tMatrix[5] = this.lastTranslate.y + ev.deltaY
          this.requestElementUpdate('onpan');
        
      }
      onPinchStart(ev){
        this.duration = '';
        this.lastTranslate = this.point2D(this.tMatrix[4],this.tMatrix[5])//记录上一次的偏移值
        this.initScale = this.tMatrix[0] || 1;
        this.poscenter = this.point2D(ev.center.x, ev.center.y)
        
        this.lastcenter = this.point2D(this.center.x + this.lastTranslate.x,this.center.y + this.lastTranslate.y)//重新计算放大后的中心坐标
        this.poscenter =  this.point2D(ev.center.x -  this.lastcenter.x, ev.center.y- this.lastcenter.y)
        // console.log("center", this.lastcenter.x, this.lastcenter.y)
        
        this.requestElementUpdate('onpinchStart');
      }
      onPinch(ev){
        console.log(ev.scale);
        var nowScale =  this.tMatrix[0] =  this.tMatrix[3] =  this.initScale * ev.scale;
        var composscal = (1 - ev.scale) 
        //tMatrix[4] = poscenter.x - ((poscenter.x - lastcenter.x) *  ev.scale + lastcenter.x)  + lastTranslate.x//最后加上上一次的偏移值
        //tMatrix[5] = poscenter.y - ((poscenter.y - lastcenter.y) *  ev.scale + lastcenter.y)  + lastTranslate.y
        this.tMatrix[4] = (1 - ev.scale) *  this.poscenter.x +  this.lastTranslate.x
        this.tMatrix[5] = (1 - ev.scale) *  this.poscenter.y +  this.lastTranslate.y
        this.requestElementUpdate('onpinch');	
      }
      
      onDoubleTap(ev){
        this.duration = ".3s ease all";
        var nowScale =  this.tMatrix[0];
        if(nowScale != 1 ||  this.tMatrix[4]!= 0){
          //scale不等于1,要重回1
          this.tMatrix[0] =  this.tMatrix[3] = 1;
          this.tMatrix[4] =  this.tMatrix[5] = 0;
        }else{
          var pointer = ev.center
          var scale = 2
          this.tMatrix[0] =  this.tMatrix[3] = scale
          //var last = point2D
          //tMatrix[4] = pointer.x - ((pointer.x-lastcenter.x) * scale + lastcenter.x);
          //tMatrix[5] = pointer.y - ((pointer.y-lastcenter.y) * scale + lastcenter.y);
          this. tMatrix[4] = (1 - scale) * ( pointer.x -  this.center.x) 
          this.tMatrix[5] = (1 - scale) * ( pointer.y -  this.center.y)
        }
        this.requestElementUpdate('doubleTap');
      }
      
      updateElementTransform() {
        this.el.style.transition =  this.duration;
        var tmp =  this.tMatrix.join(',')
        // console.log(tmp)
        this.el.style.transform = 'matrix(' + tmp + ')';
        this.ticking = false;
      }
      
      requestElementUpdate(arg?:string) {
        // arg && console.log(arg)
      
          if(! this.ticking) {
            this.reqAnimationFrame( this.updateElementTransform.bind(this));//改变this的指向
            this.ticking = true;
          }
      }
    }
    

    可参考:https://blog.csdn.net/qq_29729735/article/details/88578949

    展开全文
  • * 图片最大放大比例 */ private static final float MAX_SCALE = 4f; ////////////////////////////////监听器//////////////////////////////// /** * 外界点击事件 * * @see #setOnClickListener...

    样式

    进入正题:

    1查看照片页面的activity.

    package com.unopenbox.client.activity.photo;
    
    import android.content.Context;
    import android.support.v4.view.ViewPager;
    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.widget.ImageView;
    import android.widget.TextView;
    
    import com.unopenbox.client.R;
    import com.unopenbox.client.adapter.ChatViewPagerAdapter;
    import com.unopenbox.client.network.OkHttpClientManager;
    import com.unopenbox.client.util.DownloadImage;
    import com.unopenbox.client.util.ToastShowUtils;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class ShowItemsPicActivity extends AppCompatActivity {
        private ImageView backHome;
        private TextView tittle;
        private ViewPager mViewPager;
        private LayoutInflater layoutInflater;
        private List<View> views = new ArrayList<>();
        private List<String> listData = new ArrayList<>();
        private int flag = 0;
        private Context context = this;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_show_items_pic);
            initWeb();
            initData();
        }
    
        private void initWeb() {
            //初始化组件
            //初始化组件
            backHome = (ImageView) findViewById(R.id.normal_title_blue_left_iv);
            tittle = (TextView) findViewById(R.id.normal_title_blue_center_tv);
            tittle.setText("相册");
            backHome.setVisibility(View.VISIBLE);
            backHome.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    finish();
    
                }
            });
            mViewPager = (ViewPager) findViewById(R.id.chat_show_load_image_viewpager);
        }
    
        private void initData() {
            //初始化数据
            listData = (List<String>) getIntent().getSerializableExtra("datas");
            //LogUtils.v("ChatShowLoadImage:"+listData.size());
            flag = getIntent().getIntExtra("flag", 0);
            LayoutInflater inflater3 = LayoutInflater.from(this);
            if (listData.size() > 0) {
                for (int i = 0; i < listData.size(); i++) {
                    final String url = listData.get(i);
                    View view = inflater3.inflate(R.layout.chat_show_load_image_item, null);
                    ImageView iv = (ImageView) view.findViewById(R.id.chat_show_load_image_download);
                    views.add(view);
    //                view.setOnClickListener(new View.OnClickListener() {
    //                    @Override
    //                    public void onClick(View v) {
    //                        finish();
    //                    }
    //                });
                    iv.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            ToastShowUtils.show(context, "下载图片", 5);
                            //LogUtils.v("下载图片");
                            new DownloadImage(ShowItemsPicActivity.this).execute(OkHttpClientManager.photoip + url);
                        }
                    });
                }
            }
            ChatViewPagerAdapter cvp = new ChatViewPagerAdapter(this, views, listData);
            mViewPager.setAdapter(cvp);
            if (flag >= listData.size()) {
                flag = 0;
            }
            mViewPager.setCurrentItem(flag);
        }
    }
    

    2查看照片页面的布局文件.

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
    
        <android.support.v4.view.ViewPager
            android:id="@+id/chat_show_load_image_viewpager"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </LinearLayout>
    

    3查看照片页面的activity的适配器.

    package com.unopenbox.client.adapter;
    
    import android.content.Context;
    import android.support.v4.view.PagerAdapter;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.ImageView;
    
    import com.bumptech.glide.Glide;
    import com.bumptech.glide.load.engine.DiskCacheStrategy;
    import com.unopenbox.client.R;
    import com.unopenbox.client.network.OkHttpClientManager;
    import com.unopenbox.client.util.PinchImageView;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * Created by Administrator on 2017/11/9.
     */
    
    public class ChatViewPagerAdapter extends PagerAdapter {
    
        private List<View> views;
        private Context context;
        private List<String> listData = new ArrayList<>();
    
        public ChatViewPagerAdapter(Context context1, List<View> views1, List<String> listData1) {
            this.context = context1;
            this.views = views1;
            listData = listData1;
        }
    
        @Override
        public int getCount() {
            if (views != null) {
                return views.size();
            }
            return 0;
        }
    
        @Override
        public boolean isViewFromObject(View view, Object object) {
            return view == object;
        }
    
    
        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            container.removeView(views.get(position));
        }
    
        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            container.addView(views.get(position));
            View view = views.get(position);
            PinchImageView iv = (PinchImageView) view.findViewById(R.id.chat_show_load_image_item_im);
            Glide.with(context)
                    .load(listData.get(position))
                    .fitCenter()
                    .placeholder(R.mipmap.moren_huise)
                    .error(R.mipmap.moren_huise)
                    .diskCacheStrategy(DiskCacheStrategy.ALL)
                    .into(iv);
            return views.get(position);
        }
    }
    

    4查看照片页面的activity的适配器的布局文件:

    .

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/chat_show_load_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/black">
    
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">
    
            <com.unopenbox.client.util.PinchImageView
                android:id="@+id/chat_show_load_image_item_im"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="fitCenter"
                android:layout_gravity="center_vertical" />
    
    
            <ImageView
                android:id="@+id/chat_show_load_image_download"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:src="@mipmap/chatloadimage"
                android:layout_marginRight="30dp"
                android:layout_marginEnd="55dp"
                android:layout_marginBottom="30dp"
                android:layout_alignParentBottom="true"
                android:layout_alignParentRight="true"
                android:layout_alignParentEnd="true" />
    
        </RelativeLayout>
    </LinearLayout>
    

    5.图片手势缩放的工具类:

    package com.unopenbox.client.util;
    
    import android.animation.ValueAnimator;
    import android.annotation.SuppressLint;
    import android.content.Context;
    import android.graphics.Canvas;
    import android.graphics.Matrix;
    import android.graphics.PointF;
    import android.graphics.RectF;
    import android.util.AttributeSet;
    import android.view.GestureDetector;
    import android.view.MotionEvent;
    import android.widget.ImageView;
    
    import java.util.ArrayList;
    import java.util.LinkedList;
    import java.util.List;
    import java.util.Queue;
    
    /**
     * 手势图片控件
     *
     * @author clifford
     */
    
    public class PinchImageView extends android.support.v7.widget.AppCompatImageView {
    
    
        配置参数
    
        /**
         * 图片缩放动画时间
         */
        public static final int SCALE_ANIMATOR_DURATION = 200;
    
        /**
         * 惯性动画衰减参数
         */
        public static final float FLING_DAMPING_FACTOR = 0.9f;
    
        /**
         * 图片最大放大比例
         */
        private static final float MAX_SCALE = 4f;
    
    
        监听器
    
        /**
         * 外界点击事件
         *
         * @see #setOnClickListener(OnClickListener)
         */
        private OnClickListener mOnClickListener;
    
        /**
         * 外界长按事件
         *
         * @see #setOnLongClickListener(OnLongClickListener)
         */
        private OnLongClickListener mOnLongClickListener;
    
        @Override
        public void setOnClickListener(OnClickListener l) {
            //默认的click会在任何点击情况下都会触发,所以搞成自己的
            mOnClickListener = l;
        }
    
        @Override
        public void setOnLongClickListener(OnLongClickListener l) {
            //默认的long click会在任何长按情况下都会触发,所以搞成自己的
            mOnLongClickListener = l;
        }
    
    
        公共状态获取
    
        /**
         * 手势状态:自由状态
         *
         * @see #getPinchMode()
         */
        public static final int PINCH_MODE_FREE = 0;
    
        /**
         * 手势状态:单指滚动状态
         *
         * @see #getPinchMode()
         */
        public static final int PINCH_MODE_SCROLL = 1;
    
        /**
         * 手势状态:双指缩放状态
         *
         * @see #getPinchMode()
         */
        public static final int PINCH_MODE_SCALE = 2;
    
        /**
         * 外层变换矩阵,如果是单位矩阵,那么图片是fit center状态
         *
         * @see #getOuterMatrix(Matrix)
         * @see #outerMatrixTo(Matrix, long)
         */
        private Matrix mOuterMatrix = new Matrix();
    
        /**
         * 矩形遮罩
         *
         * @see #getMask()
         * @see #zoomMaskTo(RectF, long)
         */
        private RectF mMask;
    
        /**
         * 当前手势状态
         *
         * @see #getPinchMode()
         * @see #PINCH_MODE_FREE
         * @see #PINCH_MODE_SCROLL
         * @see #PINCH_MODE_SCALE
         */
        private int mPinchMode = PINCH_MODE_FREE;
    
        /**
         * 获取外部变换矩阵.
         * <p>
         * 外部变换矩阵记录了图片手势操作的最终结果,是相对于图片fit center状态的变换.
         * 默认值为单位矩阵,此时图片为fit center状态.
         *
         * @param matrix 用于填充结果的对象
         * @return 如果传了matrix参数则将matrix填充后返回, 否则new一个填充返回
         */
        public Matrix getOuterMatrix(Matrix matrix) {
            if (matrix == null) {
                matrix = new Matrix(mOuterMatrix);
            } else {
                matrix.set(mOuterMatrix);
            }
            return matrix;
        }
    
        /**
         * 获取内部变换矩阵.
         * <p>
         * 内部变换矩阵是原图到fit center状态的变换,当原图尺寸变化或者控件大小变化都会发生改变
         * 当尚未布局或者原图不存在时,其值无意义.所以在调用前需要确保前置条件有效,否则将影响计算结果.
         *
         * @param matrix 用于填充结果的对象
         * @return 如果传了matrix参数则将matrix填充后返回, 否则new一个填充返回
         */
        public Matrix getInnerMatrix(Matrix matrix) {
            if (matrix == null) {
                matrix = new Matrix();
            } else {
                matrix.reset();
            }
            if (isReady()) {
                //原图大小
                RectF tempSrc = MathUtils.rectFTake(0, 0, getDrawable().getIntrinsicWidth(), getDrawable().getIntrinsicHeight());
                //控件大小
                RectF tempDst = MathUtils.rectFTake(0, 0, getWidth(), getHeight());
                //计算fit center矩阵
                matrix.setRectToRect(tempSrc, tempDst, Matrix.ScaleToFit.CENTER);
                //释放临时对象
                MathUtils.rectFGiven(tempDst);
                MathUtils.rectFGiven(tempSrc);
            }
            return matrix;
        }
    
        /**
         * 获取图片总变换矩阵.
         * <p>
         * 总变换矩阵为内部变换矩阵x外部变换矩阵,决定了原图到所见最终状态的变换
         * 当尚未布局或者原图不存在时,其值无意义.所以在调用前需要确保前置条件有效,否则将影响计算结果.
         *
         * @param matrix 用于填充结果的对象
         * @return 如果传了matrix参数则将matrix填充后返回, 否则new一个填充返回
         * @see #getOuterMatrix(Matrix)
         * @see #getInnerMatrix(Matrix)
         */
        public Matrix getCurrentImageMatrix(Matrix matrix) {
            //获取内部变换矩阵
            matrix = getInnerMatrix(matrix);
            //乘上外部变换矩阵
            matrix.postConcat(mOuterMatrix);
            return matrix;
        }
    
        /**
         * 获取当前变换后的图片位置和尺寸
         * <p>
         * 当尚未布局或者原图不存在时,其值无意义.所以在调用前需要确保前置条件有效,否则将影响计算结果.
         *
         * @param rectF 用于填充结果的对象
         * @return 如果传了rectF参数则将rectF填充后返回, 否则new一个填充返回
         * @see #getCurrentImageMatrix(Matrix)
         */
        public RectF getImageBound(RectF rectF) {
            if (rectF == null) {
                rectF = new RectF();
            } else {
                rectF.setEmpty();
            }
            if (!isReady()) {
                return rectF;
            } else {
                //申请一个空matrix
                Matrix matrix = MathUtils.matrixTake();
                //获取当前总变换矩阵
                getCurrentImageMatrix(matrix);
                //对原图矩形进行变换得到当前显示矩形
                rectF.set(0, 0, getDrawable().getIntrinsicWidth(), getDrawable().getIntrinsicHeight());
                matrix.mapRect(rectF);
                //释放临时matrix
                MathUtils.matrixGiven(matrix);
                return rectF;
            }
        }
    
        /**
         * 获取当前设置的mask
         *
         * @return 返回当前的mask对象副本, 如果当前没有设置mask则返回null
         */
        public RectF getMask() {
            if (mMask != null) {
                return new RectF(mMask);
            } else {
                return null;
            }
        }
    
        /**
         * 获取当前手势状态
         *
         * @see #PINCH_MODE_FREE
         * @see #PINCH_MODE_SCROLL
         * @see #PINCH_MODE_SCALE
         */
        public int getPinchMode() {
            return mPinchMode;
        }
    
        /**
         * 与ViewPager结合的时候使用
         *
         * @param direction
         * @return
         */
        @Override
        public boolean canScrollHorizontally(int direction) {
            if (mPinchMode == PinchImageView.PINCH_MODE_SCALE) {
                return true;
            }
            RectF bound = getImageBound(null);
            if (bound == null) {
                return false;
            }
            if (bound.isEmpty()) {
                return false;
            }
            if (direction > 0) {
                return bound.right > getWidth();
            } else {
                return bound.left < 0;
            }
        }
    
        /**
         * 与ViewPager结合的时候使用
         *
         * @param direction
         * @return
         */
        @Override
        public boolean canScrollVertically(int direction) {
            if (mPinchMode == PinchImageView.PINCH_MODE_SCALE) {
                return true;
            }
            RectF bound = getImageBound(null);
            if (bound == null) {
                return false;
            }
            if (bound.isEmpty()) {
                return false;
            }
            if (direction > 0) {
                return bound.bottom > getHeight();
            } else {
                return bound.top < 0;
            }
        }
    
    
        公共状态设置
    
        /**
         * 执行当前outerMatrix到指定outerMatrix渐变的动画
         * <p>
         * 调用此方法会停止正在进行中的手势以及手势动画.
         * 当duration为0时,outerMatrix值会被立即设置而不会启动动画.
         *
         * @param endMatrix 动画目标矩阵
         * @param duration  动画持续时间
         * @see #getOuterMatrix(Matrix)
         */
        public void outerMatrixTo(Matrix endMatrix, long duration) {
            if (endMatrix == null) {
                return;
            }
            //将手势设置为PINCH_MODE_FREE将停止后续手势的触发
            mPinchMode = PINCH_MODE_FREE;
            //停止所有正在进行的动画
            cancelAllAnimator();
            //如果时间不合法立即执行结果
            if (duration <= 0) {
                mOuterMatrix.set(endMatrix);
                dispatchOuterMatrixChanged();
                invalidate();
            } else {
                //创建矩阵变化动画
                mScaleAnimator = new ScaleAnimator(mOuterMatrix, endMatrix, duration);
                mScaleAnimator.start();
            }
        }
    
        /**
         * 执行当前mask到指定mask的变化动画
         * <p>
         * 调用此方法不会停止手势以及手势相关动画,但会停止正在进行的mask动画.
         * 当前mask为null时,则不执行动画立即设置为目标mask.
         * 当duration为0时,立即将当前mask设置为目标mask,不会执行动画.
         *
         * @param mask     动画目标mask
         * @param duration 动画持续时间
         * @see #getMask()
         */
        public void zoomMaskTo(RectF mask, long duration) {
            if (mask == null) {
                return;
            }
            //停止mask动画
            if (mMaskAnimator != null) {
                mMaskAnimator.cancel();
                mMaskAnimator = null;
            }
            //如果duration为0或者之前没有设置过mask,不执行动画,立即设置
            if (duration <= 0 || mMask == null) {
                if (mMask == null) {
                    mMask = new RectF();
                }
                mMask.set(mask);
                invalidate();
            } else {
                //执行mask动画
                mMaskAnimator = new MaskAnimator(mMask, mask, duration);
                mMaskAnimator.start();
            }
        }
    
        /**
         * 重置所有状态
         * <p>
         * 重置位置到fit center状态,清空mask,停止所有手势,停止所有动画.
         * 但不清空drawable,以及事件绑定相关数据.
         */
        public void reset() {
            //重置位置到fit
            mOuterMatrix.reset();
            dispatchOuterMatrixChanged();
            //清空mask
            mMask = null;
            //停止所有手势
            mPinchMode = PINCH_MODE_FREE;
            mLastMovePoint.set(0, 0);
            mScaleCenter.set(0, 0);
            mScaleBase = 0;
            //停止所有动画
            if (mMaskAnimator != null) {
                mMaskAnimator.cancel();
                mMaskAnimator = null;
            }
            cancelAllAnimator();
            //重绘
            invalidate();
        }
    
    
        对外广播事件
    
        /**
         * 外部矩阵变化事件通知监听器
         */
        public interface OuterMatrixChangedListener {
    
            /**
             * 外部矩阵变化回调
             * <p>
             * 外部矩阵的任何变化后都收到此回调.
             * 外部矩阵变化后,总变化矩阵,图片的展示位置都将发生变化.
             *
             * @param pinchImageView
             * @see #getOuterMatrix(Matrix)
             * @see #getCurrentImageMatrix(Matrix)
             * @see #getImageBound(RectF)
             */
            void onOuterMatrixChanged(PinchImageView pinchImageView);
        }
    
        /**
         * 所有OuterMatrixChangedListener监听列表
         *
         * @see #addOuterMatrixChangedListener(OuterMatrixChangedListener)
         * @see #removeOuterMatrixChangedListener(OuterMatrixChangedListener)
         */
        private List<OuterMatrixChangedListener> mOuterMatrixChangedListeners;
    
        /**
         * 当mOuterMatrixChangedListeners被锁定不允许修改时,临时将修改写到这个副本中
         *
         * @see #mOuterMatrixChangedListeners
         */
        private List<OuterMatrixChangedListener> mOuterMatrixChangedListenersCopy;
    
        /**
         * mOuterMatrixChangedListeners的修改锁定
         * <p>
         * 当进入dispatchOuterMatrixChanged方法时,被加1,退出前被减1
         *
         * @see #dispatchOuterMatrixChanged()
         * @see #addOuterMatrixChangedListener(OuterMatrixChangedListener)
         * @see #removeOuterMatrixChangedListener(OuterMatrixChangedListener)
         */
        private int mDispatchOuterMatrixChangedLock;
    
        /**
         * 添加外部矩阵变化监听
         *
         * @param listener
         */
        public void addOuterMatrixChangedListener(OuterMatrixChangedListener listener) {
            if (listener == null) {
                return;
            }
            //如果监听列表没有被修改锁定直接将监听添加到监听列表
            if (mDispatchOuterMatrixChangedLock == 0) {
                if (mOuterMatrixChangedListeners == null) {
                    mOuterMatrixChangedListeners = new ArrayList<OuterMatrixChangedListener>();
                }
                mOuterMatrixChangedListeners.add(listener);
            } else {
                //如果监听列表修改被锁定,那么尝试在监听列表副本上添加
                //监听列表副本将会在锁定被解除时替换到监听列表里
                if (mOuterMatrixChangedListenersCopy == null) {
                    if (mOuterMatrixChangedListeners != null) {
                        mOuterMatrixChangedListenersCopy = new ArrayList<OuterMatrixChangedListener>(mOuterMatrixChangedListeners);
                    } else {
                        mOuterMatrixChangedListenersCopy = new ArrayList<OuterMatrixChangedListener>();
                    }
                }
                mOuterMatrixChangedListenersCopy.add(listener);
            }
        }
    
        /**
         * 删除外部矩阵变化监听
         *
         * @param listener
         */
        public void removeOuterMatrixChangedListener(OuterMatrixChangedListener listener) {
            if (listener == null) {
                return;
            }
            //如果监听列表没有被修改锁定直接在监听列表数据结构上修改
            if (mDispatchOuterMatrixChangedLock == 0) {
                if (mOuterMatrixChangedListeners != null) {
                    mOuterMatrixChangedListeners.remove(listener);
                }
            } else {
                //如果监听列表被修改锁定,那么就在其副本上修改
                //其副本将会在锁定解除时替换回监听列表
                if (mOuterMatrixChangedListenersCopy == null) {
                    if (mOuterMatrixChangedListeners != null) {
                        mOuterMatrixChangedListenersCopy = new ArrayList<OuterMatrixChangedListener>(mOuterMatrixChangedListeners);
                    }
                }
                if (mOuterMatrixChangedListenersCopy != null) {
                    mOuterMatrixChangedListenersCopy.remove(listener);
                }
            }
        }
    
        /**
         * 触发外部矩阵修改事件
         * <p>
         * 需要在每次给外部矩阵设置值时都调用此方法.
         *
         * @see #mOuterMatrix
         */
        private void dispatchOuterMatrixChanged() {
            if (mOuterMatrixChangedListeners == null) {
                return;
            }
            //增加锁
            //这里之所以用计数器做锁定是因为可能在锁定期间又间接调用了此方法产生递归
            //使用boolean无法判断递归结束
            mDispatchOuterMatrixChangedLock++;
            //在列表循环过程中不允许修改列表,否则将引发崩溃
            for (OuterMatrixChangedListener listener : mOuterMatrixChangedListeners) {
                listener.onOuterMatrixChanged(this);
            }
            //减锁
            mDispatchOuterMatrixChangedLock--;
            //如果是递归的情况,mDispatchOuterMatrixChangedLock可能大于1,只有减到0才能算列表的锁定解除
            if (mDispatchOuterMatrixChangedLock == 0) {
                //如果期间有修改列表,那么副本将不为null
                if (mOuterMatrixChangedListenersCopy != null) {
                    //将副本替换掉正式的列表
                    mOuterMatrixChangedListeners = mOuterMatrixChangedListenersCopy;
                    //清空副本
                    mOuterMatrixChangedListenersCopy = null;
                }
            }
        }
    
    
        用于重载定制
    
        /**
         * 获取图片最大可放大的比例
         * <p>
         * 如果放大大于这个比例则不被允许.
         * 在双手缩放过程中如果图片放大比例大于这个值,手指释放将回弹到这个比例.
         * 在双击放大过程中不允许放大比例大于这个值.
         * 覆盖此方法可以定制不同情况使用不同的最大可放大比例.
         *
         * @return 缩放比例
         * @see #scaleEnd()
         * @see #doubleTap(float, float)
         */
        protected float getMaxScale() {
            return MAX_SCALE;
        }
    
        /**
         * 计算双击之后图片接下来应该被缩放的比例
         * <p>
         * 如果值大于getMaxScale或者小于fit center尺寸,则实际使用取边界值.
         * 通过覆盖此方法可以定制不同的图片被双击时使用不同的放大策略.
         *
         * @param innerScale 当前内部矩阵的缩放值
         * @param outerScale 当前外部矩阵的缩放值
         * @return 接下来的缩放比例
         * @see #doubleTap(float, float)
         * @see #getMaxScale()
         */
        protected float calculateNextScale(float innerScale, float outerScale) {
            float currentScale = innerScale * outerScale;
            if (currentScale < MAX_SCALE) {
                return MAX_SCALE;
            } else {
                return innerScale;
            }
        }
    
    
        初始化
    
        public PinchImageView(Context context) {
            super(context);
            initView();
        }
    
        public PinchImageView(Context context, AttributeSet attrs) {
            super(context, attrs);
            initView();
        }
    
        public PinchImageView(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
            initView();
        }
    
        private void initView() {
            //强制设置图片scaleType为matrix
            super.setScaleType(ScaleType.MATRIX);
        }
    
        //不允许设置scaleType,只能用内部设置的matrix
        @Override
        public void setScaleType(ScaleType scaleType) {
        }
    
    
        绘制
    
        @Override
        protected void onDraw(Canvas canvas) {
            //在绘制前设置变换矩阵
            if (isReady()) {
                Matrix matrix = MathUtils.matrixTake();
                setImageMatrix(getCurrentImageMatrix(matrix));
                MathUtils.matrixGiven(matrix);
            }
            //对图像做遮罩处理
            if (mMask != null) {
                canvas.save();
                canvas.clipRect(mMask);
                super.onDraw(canvas);
                canvas.restore();
            } else {
                super.onDraw(canvas);
            }
        }
    
    
        有效性判断
    
        /**
         * 判断当前情况是否能执行手势相关计算
         * <p>
         * 包括:是否有图片,图片是否有尺寸,控件是否有尺寸.
         *
         * @return 是否能执行手势相关计算
         */
        private boolean isReady() {
            return getDrawable() != null && getDrawable().getIntrinsicWidth() > 0 && getDrawable().getIntrinsicHeight() > 0
                    && getWidth() > 0 && getHeight() > 0;
        }
    
    
        mask动画处理
    
        /**
         * mask修改的动画
         * <p>
         * 和图片的动画相互独立.
         *
         * @see #zoomMaskTo(RectF, long)
         */
        private MaskAnimator mMaskAnimator;
    
        /**
         * mask变换动画
         * <p>
         * 将mask从一个rect动画到另外一个rect
         */
        private class MaskAnimator extends ValueAnimator implements ValueAnimator.AnimatorUpdateListener {
    
            /**
             * 开始mask
             */
            private float[] mStart = new float[4];
    
            /**
             * 结束mask
             */
            private float[] mEnd = new float[4];
    
            /**
             * 中间结果mask
             */
            private float[] mResult = new float[4];
    
            /**
             * 创建mask变换动画
             *
             * @param start    动画起始状态
             * @param end      动画终点状态
             * @param duration 动画持续时间
             */
            public MaskAnimator(RectF start, RectF end, long duration) {
                super();
                setFloatValues(0, 1f);
                setDuration(duration);
                addUpdateListener(this);
                //将起点终点拷贝到数组方便计算
                mStart[0] = start.left;
                mStart[1] = start.top;
                mStart[2] = start.right;
                mStart[3] = start.bottom;
                mEnd[0] = end.left;
                mEnd[1] = end.top;
                mEnd[2] = end.right;
                mEnd[3] = end.bottom;
            }
    
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                //获取动画进度,0-1范围
                float value = (Float) animation.getAnimatedValue();
                //根据进度对起点终点之间做插值
                for (int i = 0; i < 4; i++) {
                    mResult[i] = mStart[i] + (mEnd[i] - mStart[i]) * value;
                }
                //期间mask有可能被置空了,所以判断一下
                if (mMask == null) {
                    mMask = new RectF();
                }
                //设置新的mask并绘制
                mMask.set(mResult[0], mResult[1], mResult[2], mResult[3]);
                invalidate();
            }
        }
    
    
        手势动画处理
    
        /**
         * 在单指模式下:
         * 记录上一次手指的位置,用于计算新的位置和上一次位置的差值.
         * <p>
         * 双指模式下:
         * 记录两个手指的中点,作为和mScaleCenter绑定的点.
         * 这个绑定可以保证mScaleCenter无论如何都会跟随这个中点.
         *
         * @see #mScaleCenter
         * @see #scale(PointF, float, float, PointF)
         * @see #scaleEnd()
         */
        private PointF mLastMovePoint = new PointF();
    
        /**
         * 缩放模式下图片的缩放中点.
         * <p>
         * 为其指代的点经过innerMatrix变换之后的值.
         * 其指代的点在手势过程中始终跟随mLastMovePoint.
         * 通过双指缩放时,其为缩放中心点.
         *
         * @see #saveScaleContext(float, float, float, float)
         * @see #mLastMovePoint
         * @see #scale(PointF, float, float, PointF)
         */
        private PointF mScaleCenter = new PointF();
    
        /**
         * 缩放模式下的基础缩放比例
         * <p>
         * 为外层缩放值除以开始缩放时两指距离.
         * 其值乘上最新的两指之间距离为最新的图片缩放比例.
         *
         * @see #saveScaleContext(float, float, float, float)
         * @see #scale(PointF, float, float, PointF)
         */
        private float mScaleBase = 0;
    
        /**
         * 图片缩放动画
         * <p>
         * 缩放模式把图片的位置大小超出限制之后触发.
         * 双击图片放大或缩小时触发.
         * 手动调用outerMatrixTo触发.
         *
         * @see #scaleEnd()
         * @see #doubleTap(float, float)
         * @see #outerMatrixTo(Matrix, long)
         */
        private ScaleAnimator mScaleAnimator;
    
        /**
         * 滑动产生的惯性动画
         *
         * @see #fling(float, float)
         */
        private FlingAnimator mFlingAnimator;
    
        /**
         * 常用手势处理
         * <p>
         * 在onTouchEvent末尾被执行.
         */
        private GestureDetector mGestureDetector = new GestureDetector(PinchImageView.this.getContext(), new GestureDetector.SimpleOnGestureListener() {
    
            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
                //只有在单指模式结束之后才允许执行fling
                if (mPinchMode == PINCH_MODE_FREE && !(mScaleAnimator != null && mScaleAnimator.isRunning())) {
                    fling(velocityX, velocityY);
                }
                return true;
            }
    
            public void onLongPress(MotionEvent e) {
                //触发长按
                if (mOnLongClickListener != null) {
                    mOnLongClickListener.onLongClick(PinchImageView.this);
                }
            }
    
            public boolean onDoubleTap(MotionEvent e) {
                //当手指快速第二次按下触发,此时必须是单指模式才允许执行doubleTap
                if (mPinchMode == PINCH_MODE_SCROLL && !(mScaleAnimator != null && mScaleAnimator.isRunning())) {
                    doubleTap(e.getX(), e.getY());
                }
                return true;
            }
    
            public boolean onSingleTapConfirmed(MotionEvent e) {
                //触发点击
                if (mOnClickListener != null) {
                    mOnClickListener.onClick(PinchImageView.this);
                }
                return true;
            }
        });
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            super.onTouchEvent(event);
            int action = event.getAction() & MotionEvent.ACTION_MASK;
            //最后一个点抬起或者取消,结束所有模式
            if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
                //如果之前是缩放模式,还需要触发一下缩放结束动画
                if (mPinchMode == PINCH_MODE_SCALE) {
                    scaleEnd();
                }
                mPinchMode = PINCH_MODE_FREE;
            } else if (action == MotionEvent.ACTION_POINTER_UP) {
                //多个手指情况下抬起一个手指,此时需要是缩放模式才触发
                if (mPinchMode == PINCH_MODE_SCALE) {
                    //抬起的点如果大于2,那么缩放模式还有效,但是有可能初始点变了,重新测量初始点
                    if (event.getPointerCount() > 2) {
                        //如果还没结束缩放模式,但是第一个点抬起了,那么让第二个点和第三个点作为缩放控制点
                        if (event.getAction() >> 8 == 0) {
                            saveScaleContext(event.getX(1), event.getY(1), event.getX(2), event.getY(2));
                            //如果还没结束缩放模式,但是第二个点抬起了,那么让第一个点和第三个点作为缩放控制点
                        } else if (event.getAction() >> 8 == 1) {
                            saveScaleContext(event.getX(0), event.getY(0), event.getX(2), event.getY(2));
                        }
                    }
                    //如果抬起的点等于2,那么此时只剩下一个点,也不允许进入单指模式,因为此时可能图片没有在正确的位置上
                }
                //第一个点按下,开启滚动模式,记录开始滚动的点
            } else if (action == MotionEvent.ACTION_DOWN) {
                //在矩阵动画过程中不允许启动滚动模式
                if (!(mScaleAnimator != null && mScaleAnimator.isRunning())) {
                    //停止所有动画
                    cancelAllAnimator();
                    //切换到滚动模式
                    mPinchMode = PINCH_MODE_SCROLL;
                    //保存触发点用于move计算差值
                    mLastMovePoint.set(event.getX(), event.getY());
                }
                //非第一个点按下,关闭滚动模式,开启缩放模式,记录缩放模式的一些初始数据
            } else if (action == MotionEvent.ACTION_POINTER_DOWN) {
                //停止所有动画
                cancelAllAnimator();
                //切换到缩放模式
                mPinchMode = PINCH_MODE_SCALE;
                //保存缩放的两个手指
                saveScaleContext(event.getX(0), event.getY(0), event.getX(1), event.getY(1));
            } else if (action == MotionEvent.ACTION_MOVE) {
                if (!(mScaleAnimator != null && mScaleAnimator.isRunning())) {
                    //在滚动模式下移动
                    if (mPinchMode == PINCH_MODE_SCROLL) {
                        //每次移动产生一个差值累积到图片位置上
                        scrollBy(event.getX() - mLastMovePoint.x, event.getY() - mLastMovePoint.y);
                        //记录新的移动点
                        mLastMovePoint.set(event.getX(), event.getY());
                        //在缩放模式下移动
                    } else if (mPinchMode == PINCH_MODE_SCALE && event.getPointerCount() > 1) {
                        //两个缩放点间的距离
                        float distance = MathUtils.getDistance(event.getX(0), event.getY(0), event.getX(1), event.getY(1));
                        //保存缩放点中点
                        float[] lineCenter = MathUtils.getCenterPoint(event.getX(0), event.getY(0), event.getX(1), event.getY(1));
                        mLastMovePoint.set(lineCenter[0], lineCenter[1]);
                        //处理缩放
                        scale(mScaleCenter, mScaleBase, distance, mLastMovePoint);
                    }
                }
            }
            //无论如何都处理各种外部手势
            mGestureDetector.onTouchEvent(event);
            return true;
        }
    
        /**
         * 让图片移动一段距离
         * <p>
         * 不能移动超过可移动范围,超过了就到可移动范围边界为止.
         *
         * @param xDiff 移动距离
         * @param yDiff 移动距离
         * @return 是否改变了位置
         */
        private boolean scrollBy(float xDiff, float yDiff) {
            if (!isReady()) {
                return false;
            }
            //原图方框
            RectF bound = MathUtils.rectFTake();
            getImageBound(bound);
            //控件大小
            float displayWidth = getWidth();
            float displayHeight = getHeight();
            //如果当前图片宽度小于控件宽度,则不能移动
            if (bound.right - bound.left < displayWidth) {
                xDiff = 0;
                //如果图片左边在移动后超出控件左边
            } else if (bound.left + xDiff > 0) {
                //如果在移动之前是没超出的,计算应该移动的距离
                if (bound.left < 0) {
                    xDiff = -bound.left;
                    //否则无法移动
                } else {
                    xDiff = 0;
                }
                //如果图片右边在移动后超出控件右边
            } else if (bound.right + xDiff < displayWidth) {
                //如果在移动之前是没超出的,计算应该移动的距离
                if (bound.right > displayWidth) {
                    xDiff = displayWidth - bound.right;
                    //否则无法移动
                } else {
                    xDiff = 0;
                }
            }
            //以下同理
            if (bound.bottom - bound.top < displayHeight) {
                yDiff = 0;
            } else if (bound.top + yDiff > 0) {
                if (bound.top < 0) {
                    yDiff = -bound.top;
                } else {
                    yDiff = 0;
                }
            } else if (bound.bottom + yDiff < displayHeight) {
                if (bound.bottom > displayHeight) {
                    yDiff = displayHeight - bound.bottom;
                } else {
                    yDiff = 0;
                }
            }
            MathUtils.rectFGiven(bound);
            //应用移动变换
            mOuterMatrix.postTranslate(xDiff, yDiff);
            dispatchOuterMatrixChanged();
            //触发重绘
            invalidate();
            //检查是否有变化
            if (xDiff != 0 || yDiff != 0) {
                return true;
            } else {
                return false;
            }
        }
    
        /**
         * 记录缩放前的一些信息
         * <p>
         * 保存基础缩放值.
         * 保存图片缩放中点.
         *
         * @param x1 缩放第一个手指
         * @param y1 缩放第一个手指
         * @param x2 缩放第二个手指
         * @param y2 缩放第二个手指
         */
        private void saveScaleContext(float x1, float y1, float x2, float y2) {
            //记录基础缩放值,其中图片缩放比例按照x方向来计算
            //理论上图片应该是等比的,x和y方向比例相同
            //但是有可能外部设定了不规范的值.
            //但是后续的scale操作会将xy不等的缩放值纠正,改成和x方向相同
            mScaleBase = MathUtils.getMatrixScale(mOuterMatrix)[0] / MathUtils.getDistance(x1, y1, x2, y2);
            //两手指的中点在屏幕上落在了图片的某个点上,图片上的这个点在经过总矩阵变换后和手指中点相同
            //现在我们需要得到图片上这个点在图片是fit center状态下在屏幕上的位置
            //因为后续的计算都是基于图片是fit center状态下进行变换
            //所以需要把两手指中点除以外层变换矩阵得到mScaleCenter
            float[] center = MathUtils.inverseMatrixPoint(MathUtils.getCenterPoint(x1, y1, x2, y2), mOuterMatrix);
            mScaleCenter.set(center[0], center[1]);
        }
    
        /**
         * 对图片按照一些手势信息进行缩放
         *
         * @param scaleCenter mScaleCenter
         * @param scaleBase   mScaleBase
         * @param distance    手指两点之间距离
         * @param lineCenter  手指两点之间中点
         * @see #mScaleCenter
         * @see #mScaleBase
         */
        private void scale(PointF scaleCenter, float scaleBase, float distance, PointF lineCenter) {
            if (!isReady()) {
                return;
            }
            //计算图片从fit center状态到目标状态的缩放比例
            float scale = scaleBase * distance;
            Matrix matrix = MathUtils.matrixTake();
            //按照图片缩放中心缩放,并且让缩放中心在缩放点中点上
            matrix.postScale(scale, scale, scaleCenter.x, scaleCenter.y);
            //让图片的缩放中点跟随手指缩放中点
            matrix.postTranslate(lineCenter.x - scaleCenter.x, lineCenter.y - scaleCenter.y);
            //应用变换
            mOuterMatrix.set(matrix);
            MathUtils.matrixGiven(matrix);
            dispatchOuterMatrixChanged();
            //重绘
            invalidate();
        }
    
        /**
         * 双击后放大或者缩小
         * <p>
         * 将图片缩放比例缩放到nextScale指定的值.
         * 但nextScale值不能大于最大缩放值不能小于fit center情况下的缩放值.
         * 将双击的点尽量移动到控件中心.
         *
         * @param x 双击的点
         * @param y 双击的点
         * @see #calculateNextScale(float, float)
         * @see #getMaxScale()
         */
        private void doubleTap(float x, float y) {
            if (!isReady()) {
                return;
            }
            //获取第一层变换矩阵
            Matrix innerMatrix = MathUtils.matrixTake();
            getInnerMatrix(innerMatrix);
            //当前总的缩放比例
            float innerScale = MathUtils.getMatrixScale(innerMatrix)[0];
            float outerScale = MathUtils.getMatrixScale(mOuterMatrix)[0];
            float currentScale = innerScale * outerScale;
            //控件大小
            float displayWidth = getWidth();
            float displayHeight = getHeight();
            //最大放大大小
            float maxScale = getMaxScale();
            //接下来要放大的大小
            float nextScale = calculateNextScale(innerScale, outerScale);
            //如果接下来放大大于最大值或者小于fit center值,则取边界
            if (nextScale > maxScale) {
                nextScale = maxScale;
            }
            if (nextScale < innerScale) {
                nextScale = innerScale;
            }
            //开始计算缩放动画的结果矩阵
            Matrix animEnd = MathUtils.matrixTake(mOuterMatrix);
            //计算还需缩放的倍数
            animEnd.postScale(nextScale / currentScale, nextScale / currentScale, x, y);
            //将放大点移动到控件中心
            animEnd.postTranslate(displayWidth / 2f - x, displayHeight / 2f - y);
            //得到放大之后的图片方框
            Matrix testMatrix = MathUtils.matrixTake(innerMatrix);
            testMatrix.postConcat(animEnd);
            RectF testBound = MathUtils.rectFTake(0, 0, getDrawable().getIntrinsicWidth(), getDrawable().getIntrinsicHeight());
            testMatrix.mapRect(testBound);
            //修正位置
            float postX = 0;
            float postY = 0;
            if (testBound.right - testBound.left < displayWidth) {
                postX = displayWidth / 2f - (testBound.right + testBound.left) / 2f;
            } else if (testBound.left > 0) {
                postX = -testBound.left;
            } else if (testBound.right < displayWidth) {
                postX = displayWidth - testBound.right;
            }
            if (testBound.bottom - testBound.top < displayHeight) {
                postY = displayHeight / 2f - (testBound.bottom + testBound.top) / 2f;
            } else if (testBound.top > 0) {
                postY = -testBound.top;
            } else if (testBound.bottom < displayHeight) {
                postY = displayHeight - testBound.bottom;
            }
            //应用修正位置
            animEnd.postTranslate(postX, postY);
            //清理当前可能正在执行的动画
            cancelAllAnimator();
            //启动矩阵动画
            mScaleAnimator = new ScaleAnimator(mOuterMatrix, animEnd);
            mScaleAnimator.start();
            //清理临时变量
            MathUtils.rectFGiven(testBound);
            MathUtils.matrixGiven(testMatrix);
            MathUtils.matrixGiven(animEnd);
            MathUtils.matrixGiven(innerMatrix);
        }
    
        /**
         * 当缩放操作结束动画
         * <p>
         * 如果图片超过边界,找到最近的位置动画恢复.
         * 如果图片缩放尺寸超过最大值或者最小值,找到最近的值动画恢复.
         */
        private void scaleEnd() {
            if (!isReady()) {
                return;
            }
            //是否修正了位置
            boolean change = false;
            //获取图片整体的变换矩阵
            Matrix currentMatrix = MathUtils.matrixTake();
            getCurrentImageMatrix(currentMatrix);
            //整体缩放比例
            float currentScale = MathUtils.getMatrixScale(currentMatrix)[0];
            //第二层缩放比例
            float outerScale = MathUtils.getMatrixScale(mOuterMatrix)[0];
            //控件大小
            float displayWidth = getWidth();
            float displayHeight = getHeight();
            //最大缩放比例
            float maxScale = getMaxScale();
            //比例修正
            float scalePost = 1f;
            //位置修正
            float postX = 0;
            float postY = 0;
            //如果整体缩放比例大于最大比例,进行缩放修正
            if (currentScale > maxScale) {
                scalePost = maxScale / currentScale;
            }
            //如果缩放修正后整体导致第二层缩放小于1(就是图片比fit center状态还小),重新修正缩放
            if (outerScale * scalePost < 1f) {
                scalePost = 1f / outerScale;
            }
            //如果缩放修正不为1,说明进行了修正
            if (scalePost != 1f) {
                change = true;
            }
            //尝试根据缩放点进行缩放修正
            Matrix testMatrix = MathUtils.matrixTake(currentMatrix);
            testMatrix.postScale(scalePost, scalePost, mLastMovePoint.x, mLastMovePoint.y);
            RectF testBound = MathUtils.rectFTake(0, 0, getDrawable().getIntrinsicWidth(), getDrawable().getIntrinsicHeight());
            //获取缩放修正后的图片方框
            testMatrix.mapRect(testBound);
            //检测缩放修正后位置有无超出,如果超出进行位置修正
            if (testBound.right - testBound.left < displayWidth) {
                postX = displayWidth / 2f - (testBound.right + testBound.left) / 2f;
            } else if (testBound.left > 0) {
                postX = -testBound.left;
            } else if (testBound.right < displayWidth) {
                postX = displayWidth - testBound.right;
            }
            if (testBound.bottom - testBound.top < displayHeight) {
                postY = displayHeight / 2f - (testBound.bottom + testBound.top) / 2f;
            } else if (testBound.top > 0) {
                postY = -testBound.top;
            } else if (testBound.bottom < displayHeight) {
                postY = displayHeight - testBound.bottom;
            }
            //如果位置修正不为0,说明进行了修正
            if (postX != 0 || postY != 0) {
                change = true;
            }
            //只有有执行修正才执行动画
            if (change) {
                //计算结束矩阵
                Matrix animEnd = MathUtils.matrixTake(mOuterMatrix);
                animEnd.postScale(scalePost, scalePost, mLastMovePoint.x, mLastMovePoint.y);
                animEnd.postTranslate(postX, postY);
                //清理当前可能正在执行的动画
                cancelAllAnimator();
                //启动矩阵动画
                mScaleAnimator = new ScaleAnimator(mOuterMatrix, animEnd);
                mScaleAnimator.start();
                //清理临时变量
                MathUtils.matrixGiven(animEnd);
            }
            //清理临时变量
            MathUtils.rectFGiven(testBound);
            MathUtils.matrixGiven(testMatrix);
            MathUtils.matrixGiven(currentMatrix);
        }
    
        /**
         * 执行惯性动画
         * <p>
         * 动画在遇到不能移动就停止.
         * 动画速度衰减到很小就停止.
         * <p>
         * 其中参数速度单位为 像素/秒
         *
         * @param vx x方向速度
         * @param vy y方向速度
         */
        private void fling(float vx, float vy) {
            if (!isReady()) {
                return;
            }
            //清理当前可能正在执行的动画
            cancelAllAnimator();
            //创建惯性动画
            //FlingAnimator单位为 像素/帧,一秒60帧
            mFlingAnimator = new FlingAnimator(vx / 60f, vy / 60f);
            mFlingAnimator.start();
        }
    
        /**
         * 停止所有手势动画
         */
        private void cancelAllAnimator() {
            if (mScaleAnimator != null) {
                mScaleAnimator.cancel();
                mScaleAnimator = null;
            }
            if (mFlingAnimator != null) {
                mFlingAnimator.cancel();
                mFlingAnimator = null;
            }
        }
    
        /**
         * 惯性动画
         * <p>
         * 速度逐渐衰减,每帧速度衰减为原来的FLING_DAMPING_FACTOR,当速度衰减到小于1时停止.
         * 当图片不能移动时,动画停止.
         */
        private class FlingAnimator extends ValueAnimator implements ValueAnimator.AnimatorUpdateListener {
    
            /**
             * 速度向量
             */
            private float[] mVector;
    
            /**
             * 创建惯性动画
             * <p>
             * 参数单位为 像素/帧
             *
             * @param vectorX 速度向量
             * @param vectorY 速度向量
             */
            public FlingAnimator(float vectorX, float vectorY) {
                super();
                setFloatValues(0, 1f);
                setDuration(1000000);
                addUpdateListener(this);
                mVector = new float[]{vectorX, vectorY};
            }
    
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                //移动图像并给出结果
                boolean result = scrollBy(mVector[0], mVector[1]);
                //衰减速度
                mVector[0] *= FLING_DAMPING_FACTOR;
                mVector[1] *= FLING_DAMPING_FACTOR;
                //速度太小或者不能移动了就结束
                if (!result || MathUtils.getDistance(0, 0, mVector[0], mVector[1]) < 1f) {
                    animation.cancel();
                }
            }
        }
    
        /**
         * 缩放动画
         * <p>
         * 在给定时间内从一个矩阵的变化逐渐动画到另一个矩阵的变化
         */
        private class ScaleAnimator extends ValueAnimator implements ValueAnimator.AnimatorUpdateListener {
    
            /**
             * 开始矩阵
             */
            private float[] mStart = new float[9];
    
            /**
             * 结束矩阵
             */
            private float[] mEnd = new float[9];
    
            /**
             * 中间结果矩阵
             */
            private float[] mResult = new float[9];
    
            /**
             * 构建一个缩放动画
             * <p>
             * 从一个矩阵变换到另外一个矩阵
             *
             * @param start 开始矩阵
             * @param end   结束矩阵
             */
            public ScaleAnimator(Matrix start, Matrix end) {
                this(start, end, SCALE_ANIMATOR_DURATION);
            }
    
            /**
             * 构建一个缩放动画
             * <p>
             * 从一个矩阵变换到另外一个矩阵
             *
             * @param start    开始矩阵
             * @param end      结束矩阵
             * @param duration 动画时间
             */
            public ScaleAnimator(Matrix start, Matrix end, long duration) {
                super();
                setFloatValues(0, 1f);
                setDuration(duration);
                addUpdateListener(this);
                start.getValues(mStart);
                end.getValues(mEnd);
            }
    
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                //获取动画进度
                float value = (Float) animation.getAnimatedValue();
                //根据动画进度计算矩阵中间插值
                for (int i = 0; i < 9; i++) {
                    mResult[i] = mStart[i] + (mEnd[i] - mStart[i]) * value;
                }
                //设置矩阵并重绘
                mOuterMatrix.setValues(mResult);
                dispatchOuterMatrixChanged();
                invalidate();
            }
        }
    
    
        防止内存抖动复用对象
    
        /**
         * 对象池
         * <p>
         * 防止频繁new对象产生内存抖动.
         * 由于对象池最大长度限制,如果吞度量超过对象池容量,仍然会发生抖动.
         * 此时需要增大对象池容量,但是会占用更多内存.
         *
         * @param <T> 对象池容纳的对象类型
         */
        private static abstract class ObjectsPool<T> {
    
            /**
             * 对象池的最大容量
             */
            private int mSize;
    
            /**
             * 对象池队列
             */
            private Queue<T> mQueue;
    
            /**
             * 创建一个对象池
             *
             * @param size 对象池最大容量
             */
            public ObjectsPool(int size) {
                mSize = size;
                mQueue = new LinkedList<T>();
            }
    
            /**
             * 获取一个空闲的对象
             * <p>
             * 如果对象池为空,则对象池自己会new一个返回.
             * 如果对象池内有对象,则取一个已存在的返回.
             * take出来的对象用完要记得调用given归还.
             * 如果不归还,让然会发生内存抖动,但不会引起泄漏.
             *
             * @return 可用的对象
             * @see #given(Object)
             */
            public T take() {
                //如果池内为空就创建一个
                if (mQueue.size() == 0) {
                    return newInstance();
                } else {
                    //对象池里有就从顶端拿出来一个返回
                    return resetInstance(mQueue.poll());
                }
            }
    
            /**
             * 归还对象池内申请的对象
             * <p>
             * 如果归还的对象数量超过对象池容量,那么归还的对象就会被丢弃.
             *
             * @param obj 归还的对象
             * @see #take()
             */
            public void given(T obj) {
                //如果对象池还有空位子就归还对象
                if (obj != null && mQueue.size() < mSize) {
                    mQueue.offer(obj);
                }
            }
    
            /**
             * 实例化对象
             *
             * @return 创建的对象
             */
            abstract protected T newInstance();
    
            /**
             * 重置对象
             * <p>
             * 把对象数据清空到就像刚创建的一样.
             *
             * @param obj 需要被重置的对象
             * @return 被重置之后的对象
             */
            abstract protected T resetInstance(T obj);
        }
    
        /**
         * 矩阵对象池
         */
        private static class MatrixPool extends ObjectsPool<Matrix> {
    
            public MatrixPool(int size) {
                super(size);
            }
    
            @Override
            protected Matrix newInstance() {
                return new Matrix();
            }
    
            @Override
            protected Matrix resetInstance(Matrix obj) {
                obj.reset();
                return obj;
            }
        }
    
        /**
         * 矩形对象池
         */
        private static class RectFPool extends ObjectsPool<RectF> {
    
            public RectFPool(int size) {
                super(size);
            }
    
            @Override
            protected RectF newInstance() {
                return new RectF();
            }
    
            @Override
            protected RectF resetInstance(RectF obj) {
                obj.setEmpty();
                return obj;
            }
        }
    
    
        数学计算工具类
    
        /**
         * 数学计算工具类
         */
        public static class MathUtils {
    
            /**
             * 矩阵对象池
             */
            private static MatrixPool mMatrixPool = new MatrixPool(16);
    
            /**
             * 获取矩阵对象
             */
            public static Matrix matrixTake() {
                return mMatrixPool.take();
            }
    
            /**
             * 获取某个矩阵的copy
             */
            public static Matrix matrixTake(Matrix matrix) {
                Matrix result = mMatrixPool.take();
                if (matrix != null) {
                    result.set(matrix);
                }
                return result;
            }
    
            /**
             * 归还矩阵对象
             */
            public static void matrixGiven(Matrix matrix) {
                mMatrixPool.given(matrix);
            }
    
            /**
             * 矩形对象池
             */
            private static RectFPool mRectFPool = new RectFPool(16);
    
            /**
             * 获取矩形对象
             */
            public static RectF rectFTake() {
                return mRectFPool.take();
            }
    
            /**
             * 按照指定值获取矩形对象
             */
            public static RectF rectFTake(float left, float top, float right, float bottom) {
                RectF result = mRectFPool.take();
                result.set(left, top, right, bottom);
                return result;
            }
    
            /**
             * 获取某个矩形的副本
             */
            public static RectF rectFTake(RectF rectF) {
                RectF result = mRectFPool.take();
                if (rectF != null) {
                    result.set(rectF);
                }
                return result;
            }
    
            /**
             * 归还矩形对象
             */
            public static void rectFGiven(RectF rectF) {
                mRectFPool.given(rectF);
            }
    
            /**
             * 获取两点之间距离
             *
             * @param x1 点1
             * @param y1 点1
             * @param x2 点2
             * @param y2 点2
             * @return 距离
             */
            public static float getDistance(float x1, float y1, float x2, float y2) {
                float x = x1 - x2;
                float y = y1 - y2;
                return (float) Math.sqrt(x * x + y * y);
            }
    
            /**
             * 获取两点的中点
             *
             * @param x1 点1
             * @param y1 点1
             * @param x2 点2
             * @param y2 点2
             * @return float[]{x, y}
             */
            public static float[] getCenterPoint(float x1, float y1, float x2, float y2) {
                return new float[]{(x1 + x2) / 2f, (y1 + y2) / 2f};
            }
    
            /**
             * 获取矩阵的缩放值
             *
             * @param matrix 要计算的矩阵
             * @return float[]{scaleX, scaleY}
             */
            public static float[] getMatrixScale(Matrix matrix) {
                if (matrix != null) {
                    float[] value = new float[9];
                    matrix.getValues(value);
                    return new float[]{value[0], value[4]};
                } else {
                    return new float[2];
                }
            }
    
            /**
             * 计算点除以矩阵的值
             * <p>
             * matrix.mapPoints(unknownPoint) -> point
             * 已知point和matrix,求unknownPoint的值.
             *
             * @param point
             * @param matrix
             * @return unknownPoint
             */
            public static float[] inverseMatrixPoint(float[] point, Matrix matrix) {
                if (point != null && matrix != null) {
                    float[] dst = new float[2];
                    //计算matrix的逆矩阵
                    Matrix inverse = matrixTake();
                    matrix.invert(inverse);
                    //用逆矩阵变换point到dst,dst就是结果
                    inverse.mapPoints(dst, point);
                    //清除临时变量
                    matrixGiven(inverse);
                    return dst;
                } else {
                    return new float[2];
                }
            }
    
            /**
             * 计算两个矩形之间的变换矩阵
             * <p>
             * unknownMatrix.mapRect(to, from)
             * 已知from矩形和to矩形,求unknownMatrix
             *
             * @param from
             * @param to
             * @param result unknownMatrix
             */
            public static void calculateRectTranslateMatrix(RectF from, RectF to, Matrix result) {
                if (from == null || to == null || result == null) {
                    return;
                }
                if (from.width() == 0 || from.height() == 0) {
                    return;
                }
                result.reset();
                result.postTranslate(-from.left, -from.top);
                result.postScale(to.width() / from.width(), to.height() / from.height());
                result.postTranslate(to.left, to.top);
            }
    
            /**
             * 计算图片在某个ImageView中的显示矩形
             *
             * @param container ImageView的Rect
             * @param srcWidth  图片的宽度
             * @param srcHeight 图片的高度
             * @param scaleType 图片在ImageView中的ScaleType
             * @param result    图片应该在ImageView中展示的矩形
             */
            public static void calculateScaledRectInContainer(RectF container, float srcWidth, float srcHeight, ScaleType scaleType, RectF result) {
                if (container == null || result == null) {
                    return;
                }
                if (srcWidth == 0 || srcHeight == 0) {
                    return;
                }
                //默认scaleType为fit center
                if (scaleType == null) {
                    scaleType = ScaleType.FIT_CENTER;
                }
                result.setEmpty();
                if (ScaleType.FIT_XY.equals(scaleType)) {
                    result.set(container);
                } else if (ScaleType.CENTER.equals(scaleType)) {
                    Matrix matrix = matrixTake();
                    RectF rect = rectFTake(0, 0, srcWidth, srcHeight);
                    matrix.setTranslate((container.width() - srcWidth) * 0.5f, (container.height() - srcHeight) * 0.5f);
                    matrix.mapRect(result, rect);
                    rectFGiven(rect);
                    matrixGiven(matrix);
                    result.left += container.left;
                    result.right += container.left;
                    result.top += container.top;
                    result.bottom += container.top;
                } else if (ScaleType.CENTER_CROP.equals(scaleType)) {
                    Matrix matrix = matrixTake();
                    RectF rect = rectFTake(0, 0, srcWidth, srcHeight);
                    float scale;
                    float dx = 0;
                    float dy = 0;
                    if (srcWidth * container.height() > container.width() * srcHeight) {
                        scale = container.height() / srcHeight;
                        dx = (container.width() - srcWidth * scale) * 0.5f;
                    } else {
                        scale = container.width() / srcWidth;
                        dy = (container.height() - srcHeight * scale) * 0.5f;
                    }
                    matrix.setScale(scale, scale);
                    matrix.postTranslate(dx, dy);
                    matrix.mapRect(result, rect);
                    rectFGiven(rect);
                    matrixGiven(matrix);
                    result.left += container.left;
                    result.right += container.left;
                    result.top += container.top;
                    result.bottom += container.top;
                } else if (ScaleType.CENTER_INSIDE.equals(scaleType)) {
                    Matrix matrix = matrixTake();
                    RectF rect = rectFTake(0, 0, srcWidth, srcHeight);
                    float scale;
                    float dx;
                    float dy;
                    if (srcWidth <= container.width() && srcHeight <= container.height()) {
                        scale = 1f;
                    } else {
                        scale = Math.min(container.width() / srcWidth, container.height() / srcHeight);
                    }
                    dx = (container.width() - srcWidth * scale) * 0.5f;
                    dy = (container.height() - srcHeight * scale) * 0.5f;
                    matrix.setScale(scale, scale);
                    matrix.postTranslate(dx, dy);
                    matrix.mapRect(result, rect);
                    rectFGiven(rect);
                    matrixGiven(matrix);
                    result.left += container.left;
                    result.right += container.left;
                    result.top += container.top;
                    result.bottom += container.top;
                } else if (ScaleType.FIT_CENTER.equals(scaleType)) {
                    Matrix matrix = matrixTake();
                    RectF rect = rectFTake(0, 0, srcWidth, srcHeight);
                    RectF tempSrc = rectFTake(0, 0, srcWidth, srcHeight);
                    RectF tempDst = rectFTake(0, 0, container.width(), container.height());
                    matrix.setRectToRect(tempSrc, tempDst, Matrix.ScaleToFit.CENTER);
                    matrix.mapRect(result, rect);
                    rectFGiven(tempDst);
                    rectFGiven(tempSrc);
                    rectFGiven(rect);
                    matrixGiven(matrix);
                    result.left += container.left;
                    result.right += container.left;
                    result.top += container.top;
                    result.bottom += container.top;
                } else if (ScaleType.FIT_START.equals(scaleType)) {
                    Matrix matrix = matrixTake();
                    RectF rect = rectFTake(0, 0, srcWidth, srcHeight);
                    RectF tempSrc = rectFTake(0, 0, srcWidth, srcHeight);
                    RectF tempDst = rectFTake(0, 0, container.width(), container.height());
                    matrix.setRectToRect(tempSrc, tempDst, Matrix.ScaleToFit.START);
                    matrix.mapRect(result, rect);
                    rectFGiven(tempDst);
                    rectFGiven(tempSrc);
                    rectFGiven(rect);
                    matrixGiven(matrix);
                    result.left += container.left;
                    result.right += container.left;
                    result.top += container.top;
                    result.bottom += container.top;
                } else if (ScaleType.FIT_END.equals(scaleType)) {
                    Matrix matrix = matrixTake();
                    RectF rect = rectFTake(0, 0, srcWidth, srcHeight);
                    RectF tempSrc = rectFTake(0, 0, srcWidth, srcHeight);
                    RectF tempDst = rectFTake(0, 0, container.width(), container.height());
                    matrix.setRectToRect(tempSrc, tempDst, Matrix.ScaleToFit.END);
                    matrix.mapRect(result, rect);
                    rectFGiven(tempDst);
                    rectFGiven(tempSrc);
                    rectFGiven(rect);
                    matrixGiven(matrix);
                    result.left += container.left;
                    result.right += container.left;
                    result.top += container.top;
                    result.bottom += container.top;
                } else {
                    result.set(container);
                }
            }
        }
    }

    6Banner的点击事件.

    banner.setOnBannerListener(new OnBannerListener() {
        @Override
        public void OnBannerClick(int position) {
            Intent intent = new Intent(context, ShowItemsPicActivity.class);
            intent.putExtra("datas", (Serializable) lunboss);//需要传的图片集合
            intent.putExtra("flag", position);//在第几张图片点击的  打开时直接显示点击的那张图
            startActivity(intent);
        }
    });

    搞定   

    展开全文
  • Android实现图片手势缩放、移动、双击放大缩小 原文地址:https://blog.csdn.net/wuqingsen1/article/details/84029503 自定义图片查看器,下面是效果图(分别是:原图,双指放大,缩小,移动): 下面主要的代码为...

    Android实现图片手势缩放、移动、双击放大缩小

    原文地址:https://blog.csdn.net/wuqingsen1/article/details/84029503

    自定义图片查看器,下面是效果图(分别是:原图,双指放大,缩小,移动):

    下面主要的代码为(复制即可使用):

    三个工具类
    在 Activity 和 xml 文件中设置
    CSDN 下载地址:https://download.csdn.net/download/wuqingsen1/10782132

    GitHub 下载地址:https://github.com/wuqingsen/PhotoView

    1. 三个工具类:
      1.1 PhotoView 工具类(注意包的导入):

    import android.annotation.SuppressLint;
    import android.content.Context;
    import android.graphics.Canvas;
    import android.graphics.Matrix;
    import android.graphics.PointF;
    import android.graphics.RectF;
    import android.graphics.drawable.Drawable;
    import android.util.AttributeSet;
    import android.view.GestureDetector;
    import android.view.MotionEvent;
    import android.view.ScaleGestureDetector;
    import android.view.View;
    import android.view.ViewGroup;
    import android.view.ViewParent;
    import android.view.animation.DecelerateInterpolator;
    import android.view.animation.Interpolator;
    import android.widget.ImageView;
    import android.widget.OverScroller;
    import android.widget.Scroller;

    /**

    • author: wu
    • date: on 2018/11/13.
    • describe:图片查看器工具类,包括:
      • 双击放大、缩小;
      • 根据两指进行缩放;
      • 缩放后回弹;
      • 根据手指上下左右移动;
    • 注意上面的包不要导入错了
      */

    @SuppressLint(“AppCompatCustomView”)
    public class PhotoView extends ImageView{

    private final static int MIN_ROTATE = 35;
    private final static int ANIMA_DURING = 340;
    private final static float MAX_SCALE = 2.5f;
    
    private int mMinRotate;
    private int mAnimaDuring;
    private float mMaxScale;
    
    private int MAX_OVER_SCROLL = 0;
    private int MAX_FLING_OVER_SCROLL = 0;
    private int MAX_OVER_RESISTANCE = 0;
    private int MAX_ANIM_FROM_WAITE = 500;
    
    private Matrix mBaseMatrix = new Matrix();
    private Matrix mAnimaMatrix = new Matrix();
    private Matrix mSynthesisMatrix = new Matrix();
    private Matrix mTmpMatrix = new Matrix();
    
    private RotateGestureDetector mRotateDetector;
    private GestureDetector mDetector;
    private ScaleGestureDetector mScaleDetector;
    private OnClickListener mClickListener;
    
    private ScaleType mScaleType;
    
    private boolean hasMultiTouch;
    private boolean hasDrawable;
    private boolean isKnowSize;
    private boolean hasOverTranslate;
    private boolean isEnable = false;
    private boolean isRotateEnable = false;
    private boolean isInit;
    private boolean mAdjustViewBounds;
    // 当前是否处于放大状态
    private boolean isZoonUp;
    private boolean canRotate;
    
    private boolean imgLargeWidth;
    private boolean imgLargeHeight;
    
    private float mRotateFlag;
    private float mDegrees;
    private float mScale = 1.0f;
    private int mTranslateX;
    private int mTranslateY;
    
    private float mHalfBaseRectWidth;
    private float mHalfBaseRectHeight;
    
    private RectF mWidgetRect = new RectF();
    private RectF mBaseRect = new RectF();
    private RectF mImgRect = new RectF();
    private RectF mTmpRect = new RectF();
    private RectF mCommonRect = new RectF();
    
    private PointF mScreenCenter = new PointF();
    private PointF mScaleCenter = new PointF();
    private PointF mRotateCenter = new PointF();
    
    private Transform mTranslate = new Transform();
    
    private RectF mClip;
    private Info mFromInfo;
    private long mInfoTime;
    private Runnable mCompleteCallBack;
    
    private OnLongClickListener mLongClick;
    
    public PhotoView(Context context) {
        super(context);
        init();
    }
    
    public PhotoView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    
    public PhotoView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }
    
    private void init() {
        super.setScaleType(ScaleType.MATRIX);
        if (mScaleType == null) mScaleType = ScaleType.CENTER_INSIDE;
        mRotateDetector = new RotateGestureDetector(mRotateListener);
        mDetector = new GestureDetector(getContext(), mGestureListener);
        mScaleDetector = new ScaleGestureDetector(getContext(), mScaleListener);
        float density = getResources().getDisplayMetrics().density;
        MAX_OVER_SCROLL = (int) (density * 30);
        MAX_FLING_OVER_SCROLL = (int) (density * 30);
        MAX_OVER_RESISTANCE = (int) (density * 140);
    
        mMinRotate = MIN_ROTATE;
        mAnimaDuring = ANIMA_DURING;
        mMaxScale = MAX_SCALE;
    }
    
    /**
     * 获取默认的动画持续时间
     */
    public int getDefaultAnimaDuring() {
        return ANIMA_DURING;
    }
    
    @Override
    public void setOnClickListener(OnClickListener l) {
        super.setOnClickListener(l);
        mClickListener = l;
    }
    
    @Override
    public void setScaleType(ScaleType scaleType) {
        if (scaleType == ScaleType.MATRIX) return;
    
        if (scaleType != mScaleType) {
            mScaleType = scaleType;
    
            if (isInit) {
                initBase();
            }
        }
    }
    
    @Override
    public void setOnLongClickListener(OnLongClickListener l) {
        mLongClick = l;
    }
    
    /**
     * 设置动画的插入器
     */
    public void setInterpolator(Interpolator interpolator) {
        mTranslate.setInterpolator(interpolator);
    }
    
    /**
     * 获取动画持续时间
     */
    public int getAnimaDuring() {
        return mAnimaDuring;
    }
    
    /**
     * 设置动画的持续时间
     */
    public void setAnimaDuring(int during) {
        mAnimaDuring = during;
    }
    
    /**
     * 设置最大可以缩放的倍数
     */
    public void setMaxScale(float maxScale) {
        mMaxScale = maxScale;
    }
    
    /**
     * 获取最大可以缩放的倍数
     */
    public float getMaxScale() {
        return mMaxScale;
    }
    
    /**
     * 启用缩放功能
     */
    public void enable() {
        isEnable = true;
    }
    
    /**
     * 禁用缩放功能
     */
    public void disenable() {
        isEnable = false;
    }
    
    /**
     * 启用旋转功能
     */
    public void enableRotate() {
        isRotateEnable = true;
    }
    
    /**
     * 禁用旋转功能
     */
    public void disableRotate() {
        isRotateEnable = false;
    }
    
    /**
     */
    public void setMaxAnimFromWaiteTime(int wait) {
        MAX_ANIM_FROM_WAITE = wait;
    }
    
    @Override
    public void setImageResource(int resId) {
        Drawable drawable = null;
        try {
            drawable = getResources().getDrawable(resId);
        } catch (Exception e) {
        }
    
        setImageDrawable(drawable);
    }
    
    @Override
    public void setImageDrawable(Drawable drawable) {
        super.setImageDrawable(drawable);
    
        if (drawable == null) {
            hasDrawable = false;
            return;
        }
    
        if (!hasSize(drawable))
            return;
    
        if (!hasDrawable) {
            hasDrawable = true;
        }
    
        initBase();
    }
    
    private boolean hasSize(Drawable d) {
        if ((d.getIntrinsicHeight() <= 0 || d.getIntrinsicWidth() <= 0)
                && (d.getMinimumWidth() <= 0 || d.getMinimumHeight() <= 0)
                && (d.getBounds().width() <= 0 || d.getBounds().height() <= 0)) {
            return false;
        }
        return true;
    }
    
    private static int getDrawableWidth(Drawable d) {
        int width = d.getIntrinsicWidth();
        if (width <= 0) width = d.getMinimumWidth();
        if (width <= 0) width = d.getBounds().width();
        return width;
    }
    
    private static int getDrawableHeight(Drawable d) {
        int height = d.getIntrinsicHeight();
        if (height <= 0) height = d.getMinimumHeight();
        if (height <= 0) height = d.getBounds().height();
        return height;
    }
    
    private void initBase() {
        if (!hasDrawable) return;
        if (!isKnowSize) return;
    
        mBaseMatrix.reset();
        mAnimaMatrix.reset();
    
        isZoonUp = false;
    
        Drawable img = getDrawable();
    
        int w = getWidth();
        int h = getHeight();
        int imgw = getDrawableWidth(img);
        int imgh = getDrawableHeight(img);
    
        mBaseRect.set(0, 0, imgw, imgh);
    
        // 以图片中心点居中位移
        int tx = (w - imgw) / 2;
        int ty = (h - imgh) / 2;
    
        float sx = 1;
        float sy = 1;
    
        // 缩放,默认不超过屏幕大小
        if (imgw > w) {
            sx = (float) w / imgw;
        }
    
        if (imgh > h) {
            sy = (float) h / imgh;
        }
    
        float scale = sx < sy ? sx : sy;
    
        mBaseMatrix.reset();
        mBaseMatrix.postTranslate(tx, ty);
        mBaseMatrix.postScale(scale, scale, mScreenCenter.x, mScreenCenter.y);
        mBaseMatrix.mapRect(mBaseRect);
    
        mHalfBaseRectWidth = mBaseRect.width() / 2;
        mHalfBaseRectHeight = mBaseRect.height() / 2;
    
        mScaleCenter.set(mScreenCenter);
        mRotateCenter.set(mScaleCenter);
    
        executeTranslate();
    
        switch (mScaleType) {
            case CENTER:
                initCenter();
                break;
            case CENTER_CROP:
                initCenterCrop();
                break;
            case CENTER_INSIDE:
                initCenterInside();
                break;
            case FIT_CENTER:
                initFitCenter();
                break;
            case FIT_START:
                initFitStart();
                break;
            case FIT_END:
                initFitEnd();
                break;
            case FIT_XY:
                initFitXY();
                break;
        }
    
        isInit = true;
    
        if (mFromInfo != null && System.currentTimeMillis() - mInfoTime < MAX_ANIM_FROM_WAITE) {
            animaFrom(mFromInfo);
        }
    
        mFromInfo = null;
    }
    
    private void initCenter() {
        if (!hasDrawable) return;
        if (!isKnowSize) return;
    
        Drawable img = getDrawable();
    
        int imgw = getDrawableWidth(img);
        int imgh = getDrawableHeight(img);
    
        if (imgw > mWidgetRect.width() || imgh > mWidgetRect.height()) {
            float scaleX = imgw / mImgRect.width();
            float scaleY = imgh / mImgRect.height();
    
            mScale = scaleX > scaleY ? scaleX : scaleY;
    
            mAnimaMatrix.postScale(mScale, mScale, mScreenCenter.x, mScreenCenter.y);
    
            executeTranslate();
    
            resetBase();
        }
    }
    
    private void initCenterCrop() {
        if (mImgRect.width() < mWidgetRect.width() || mImgRect.height() < mWidgetRect.height()) {
            float scaleX = mWidgetRect.width() / mImgRect.width();
            float scaleY = mWidgetRect.height() / mImgRect.height();
    
            mScale = scaleX > scaleY ? scaleX : scaleY;
    
            mAnimaMatrix.postScale(mScale, mScale, mScreenCenter.x, mScreenCenter.y);
    
            executeTranslate();
            resetBase();
        }
    }
    
    private void initCenterInside() {
        if (mImgRect.width() > mWidgetRect.width() || mImgRect.height() > mWidgetRect.height()) {
            float scaleX = mWidgetRect.width() / mImgRect.width();
            float scaleY = mWidgetRect.height() / mImgRect.height();
    
            mScale = scaleX < scaleY ? scaleX : scaleY;
    
            mAnimaMatrix.postScale(mScale, mScale, mScreenCenter.x, mScreenCenter.y);
    
            executeTranslate();
            resetBase();
        }
    }
    
    private void initFitCenter() {
        if (mImgRect.width() < mWidgetRect.width()) {
            mScale = mWidgetRect.width() / mImgRect.width();
    
            mAnimaMatrix.postScale(mScale, mScale, mScreenCenter.x, mScreenCenter.y);
    
            executeTranslate();
            resetBase();
        }
    }
    
    private void initFitStart() {
        initFitCenter();
    
        float ty = -mImgRect.top;
        mAnimaMatrix.postTranslate(0, ty);
        executeTranslate();
        resetBase();
        mTranslateY += ty;
    }
    
    private void initFitEnd() {
        initFitCenter();
    
        float ty = (mWidgetRect.bottom - mImgRect.bottom);
        mTranslateY += ty;
        mAnimaMatrix.postTranslate(0, ty);
        executeTranslate();
        resetBase();
    }
    
    private void initFitXY() {
        float scaleX = mWidgetRect.width() / mImgRect.width();
        float scaleY = mWidgetRect.height() / mImgRect.height();
    
        mAnimaMatrix.postScale(scaleX, scaleY, mScreenCenter.x, mScreenCenter.y);
    
        executeTranslate();
        resetBase();
    }
    
    private void resetBase() {
        Drawable img = getDrawable();
        int imgw = getDrawableWidth(img);
        int imgh = getDrawableHeight(img);
        mBaseRect.set(0, 0, imgw, imgh);
        mBaseMatrix.set(mSynthesisMatrix);
        mBaseMatrix.mapRect(mBaseRect);
        mHalfBaseRectWidth = mBaseRect.width() / 2;
        mHalfBaseRectHeight = mBaseRect.height() / 2;
        mScale = 1;
        mTranslateX = 0;
        mTranslateY = 0;
        mAnimaMatrix.reset();
    }
    
    private void executeTranslate() {
        mSynthesisMatrix.set(mBaseMatrix);
        mSynthesisMatrix.postConcat(mAnimaMatrix);
        setImageMatrix(mSynthesisMatrix);
    
        mAnimaMatrix.mapRect(mImgRect, mBaseRect);
    
        imgLargeWidth = mImgRect.width() > mWidgetRect.width();
        imgLargeHeight = mImgRect.height() > mWidgetRect.height();
    }
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (!hasDrawable) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            return;
        }
    
        Drawable d = getDrawable();
        int drawableW = getDrawableWidth(d);
        int drawableH = getDrawableHeight(d);
    
        int pWidth = MeasureSpec.getSize(widthMeasureSpec);
        int pHeight = MeasureSpec.getSize(heightMeasureSpec);
    
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    
        int width = 0;
        int height = 0;
    
        ViewGroup.LayoutParams p = getLayoutParams();
    
        if (p == null) {
            p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        }
    
        if (p.width == ViewGroup.LayoutParams.MATCH_PARENT) {
            if (widthMode == MeasureSpec.UNSPECIFIED) {
                width = drawableW;
            } else {
                width = pWidth;
            }
        } else {
            if (widthMode == MeasureSpec.EXACTLY) {
                width = pWidth;
            } else if (widthMode == MeasureSpec.AT_MOST) {
                width = drawableW > pWidth ? pWidth : drawableW;
            } else {
                width = drawableW;
            }
        }
    
        if (p.height == ViewGroup.LayoutParams.MATCH_PARENT) {
            if (heightMode == MeasureSpec.UNSPECIFIED) {
                height = drawableH;
            } else {
                height = pHeight;
            }
        } else {
            if (heightMode == MeasureSpec.EXACTLY) {
                height = pHeight;
            } else if (heightMode == MeasureSpec.AT_MOST) {
                height = drawableH > pHeight ? pHeight : drawableH;
            } else {
                height = drawableH;
            }
        }
    
        if (mAdjustViewBounds && (float) drawableW / drawableH != (float) width / height) {
    
            float hScale = (float) height / drawableH;
            float wScale = (float) width / drawableW;
    
            float scale = hScale < wScale ? hScale : wScale;
            width = p.width == ViewGroup.LayoutParams.MATCH_PARENT ? width : (int) (drawableW * scale);
            height = p.height == ViewGroup.LayoutParams.MATCH_PARENT ? height : (int) (drawableH * scale);
        }
    
        setMeasuredDimension(width, height);
    }
    
    @Override
    public void setAdjustViewBounds(boolean adjustViewBounds) {
        super.setAdjustViewBounds(adjustViewBounds);
        mAdjustViewBounds = adjustViewBounds;
    }
    
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
    
        mWidgetRect.set(0, 0, w, h);
        mScreenCenter.set(w / 2, h / 2);
    
        if (!isKnowSize) {
            isKnowSize = true;
            initBase();
        }
    }
    
    @Override
    public void draw(Canvas canvas) {
        if (mClip != null) {
            canvas.clipRect(mClip);
            mClip = null;
        }
        super.draw(canvas);
    }
    
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        if (isEnable) {
            final int Action = event.getActionMasked();
            if (event.getPointerCount() >= 2) hasMultiTouch = true;
    
            mDetector.onTouchEvent(event);
            if (isRotateEnable) {
                mRotateDetector.onTouchEvent(event);
            }
            mScaleDetector.onTouchEvent(event);
    
            if (Action == MotionEvent.ACTION_UP || Action == MotionEvent.ACTION_CANCEL) onUp();
    
            return true;
        } else {
            return super.dispatchTouchEvent(event);
        }
    }
    
    private void onUp() {
        if (mTranslate.isRuning) return;
    
        if (canRotate || mDegrees % 90 != 0) {
            float toDegrees = (int) (mDegrees / 90) * 90;
            float remainder = mDegrees % 90;
    
            if (remainder > 45)
                toDegrees += 90;
            else if (remainder < -45)
                toDegrees -= 90;
    
            mTranslate.withRotate((int) mDegrees, (int) toDegrees);
    
            mDegrees = toDegrees;
        }
    
        float scale = mScale;
    
        if (mScale < 1) {
            scale = 1;
            mTranslate.withScale(mScale, 1);
        } else if (mScale > mMaxScale) {
            scale = mMaxScale;
            mTranslate.withScale(mScale, mMaxScale);
        }
    
        float cx = mImgRect.left + mImgRect.width() / 2;
        float cy = mImgRect.top + mImgRect.height() / 2;
    
        mScaleCenter.set(cx, cy);
        mRotateCenter.set(cx, cy);
    
        mTranslateX = 0;
        mTranslateY = 0;
    
        mTmpMatrix.reset();
        mTmpMatrix.postTranslate(-mBaseRect.left, -mBaseRect.top);
        mTmpMatrix.postTranslate(cx - mHalfBaseRectWidth, cy - mHalfBaseRectHeight);
        mTmpMatrix.postScale(scale, scale, cx, cy);
        mTmpMatrix.postRotate(mDegrees, cx, cy);
        mTmpMatrix.mapRect(mTmpRect, mBaseRect);
    
        doTranslateReset(mTmpRect);
        mTranslate.start();
    }
    
    private void doTranslateReset(RectF imgRect) {
        int tx = 0;
        int ty = 0;
    
        if (imgRect.width() <= mWidgetRect.width()) {
            if (!isImageCenterWidth(imgRect))
                tx = -(int) ((mWidgetRect.width() - imgRect.width()) / 2 - imgRect.left);
        } else {
            if (imgRect.left > mWidgetRect.left) {
                tx = (int) (imgRect.left - mWidgetRect.left);
            } else if (imgRect.right < mWidgetRect.right) {
                tx = (int) (imgRect.right - mWidgetRect.right);
            }
        }
    
        if (imgRect.height() <= mWidgetRect.height()) {
            if (!isImageCenterHeight(imgRect))
                ty = -(int) ((mWidgetRect.height() - imgRect.height()) / 2 - imgRect.top);
        } else {
            if (imgRect.top > mWidgetRect.top) {
                ty = (int) (imgRect.top - mWidgetRect.top);
            } else if (imgRect.bottom < mWidgetRect.bottom) {
                ty = (int) (imgRect.bottom - mWidgetRect.bottom);
            }
        }
    
        if (tx != 0 || ty != 0) {
            if (!mTranslate.mFlingScroller.isFinished()) mTranslate.mFlingScroller.abortAnimation();
            mTranslate.withTranslate(mTranslateX, mTranslateY, -tx, -ty);
        }
    }
    
    private boolean isImageCenterHeight(RectF rect) {
        return Math.abs(Math.round(rect.top) - (mWidgetRect.height() - rect.height()) / 2) < 1;
    }
    
    private boolean isImageCenterWidth(RectF rect) {
        return Math.abs(Math.round(rect.left) - (mWidgetRect.width() - rect.width()) / 2) < 1;
    }
    
    private OnRotateListener mRotateListener = new OnRotateListener() {
    
        @Override
        public void onRotate(float degrees, float focusX, float focusY) {
            mRotateFlag += degrees;
            if (canRotate) {
                mDegrees += degrees;
                mAnimaMatrix.postRotate(degrees, focusX, focusY);
            } else {
                if (Math.abs(mRotateFlag) >= mMinRotate) {
                    canRotate = true;
                    mRotateFlag = 0;
                }
            }
        }
    };
    
    private ScaleGestureDetector.OnScaleGestureListener mScaleListener = new ScaleGestureDetector.OnScaleGestureListener() {
        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            float scaleFactor = detector.getScaleFactor();
    
            if (Float.isNaN(scaleFactor) || Float.isInfinite(scaleFactor))
                return false;
    
            mScale *= scaleFactor;
    

    // mScaleCenter.set(detector.getFocusX(), detector.getFocusY());
    mAnimaMatrix.postScale(scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusY());
    executeTranslate();
    return true;
    }

        public boolean onScaleBegin(ScaleGestureDetector detector) {
            return true;
        }
    
        public void onScaleEnd(ScaleGestureDetector detector) {
    
        }
    };
    
    private float resistanceScrollByX(float overScroll, float detalX) {
        float s = detalX * (Math.abs(Math.abs(overScroll) - MAX_OVER_RESISTANCE) / (float) MAX_OVER_RESISTANCE);
        return s;
    }
    
    private float resistanceScrollByY(float overScroll, float detalY) {
        float s = detalY * (Math.abs(Math.abs(overScroll) - MAX_OVER_RESISTANCE) / (float) MAX_OVER_RESISTANCE);
        return s;
    }
    
    /**
     * 匹配两个Rect的共同部分输出到out,若无共同部分则输出0,0,0,0
     */
    private void mapRect(RectF r1, RectF r2, RectF out) {
    
        float l, r, t, b;
    
        l = r1.left > r2.left ? r1.left : r2.left;
        r = r1.right < r2.right ? r1.right : r2.right;
    
        if (l > r) {
            out.set(0, 0, 0, 0);
            return;
        }
    
        t = r1.top > r2.top ? r1.top : r2.top;
        b = r1.bottom < r2.bottom ? r1.bottom : r2.bottom;
    
        if (t > b) {
            out.set(0, 0, 0, 0);
            return;
        }
    
        out.set(l, t, r, b);
    }
    
    private void checkRect() {
        if (!hasOverTranslate) {
            mapRect(mWidgetRect, mImgRect, mCommonRect);
        }
    }
    
    private Runnable mClickRunnable = new Runnable() {
        @Override
        public void run() {
            if (mClickListener != null) {
                mClickListener.onClick(PhotoView.this);
            }
        }
    };
    
    private GestureDetector.OnGestureListener mGestureListener = new GestureDetector.SimpleOnGestureListener() {
    
        @Override
        public void onLongPress(MotionEvent e) {
            if (mLongClick != null) {
                mLongClick.onLongClick(PhotoView.this);
            }
        }
    
        @Override
        public boolean onDown(MotionEvent e) {
            hasOverTranslate = false;
            hasMultiTouch = false;
            canRotate = false;
            removeCallbacks(mClickRunnable);
            return false;
        }
    
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            if (hasMultiTouch) return false;
            if (!imgLargeWidth && !imgLargeHeight) return false;
            if (mTranslate.isRuning) return false;
    
            float vx = velocityX;
            float vy = velocityY;
    
            if (Math.round(mImgRect.left) >= mWidgetRect.left || Math.round(mImgRect.right) <= mWidgetRect.right) {
                vx = 0;
            }
    
            if (Math.round(mImgRect.top) >= mWidgetRect.top || Math.round(mImgRect.bottom) <= mWidgetRect.bottom) {
                vy = 0;
            }
    
            if (canRotate || mDegrees % 90 != 0) {
                float toDegrees = (int) (mDegrees / 90) * 90;
                float remainder = mDegrees % 90;
    
                if (remainder > 45)
                    toDegrees += 90;
                else if (remainder < -45)
                    toDegrees -= 90;
    
                mTranslate.withRotate((int) mDegrees, (int) toDegrees);
    
                mDegrees = toDegrees;
            }
    
            doTranslateReset(mImgRect);
    
            mTranslate.withFling(vx, vy);
    
            mTranslate.start();
            // onUp(e2);
            return super.onFling(e1, e2, velocityX, velocityY);
        }
    
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            if (mTranslate.isRuning) {
                mTranslate.stop();
            }
    
            if (canScrollHorizontallySelf(distanceX)) {
                if (distanceX < 0 && mImgRect.left - distanceX > mWidgetRect.left)
                    distanceX = mImgRect.left;
                if (distanceX > 0 && mImgRect.right - distanceX < mWidgetRect.right)
                    distanceX = mImgRect.right - mWidgetRect.right;
    
                mAnimaMatrix.postTranslate(-distanceX, 0);
                mTranslateX -= distanceX;
            } else if (imgLargeWidth || hasMultiTouch || hasOverTranslate) {
                checkRect();
                if (!hasMultiTouch) {
                    if (distanceX < 0 && mImgRect.left - distanceX > mCommonRect.left)
                        distanceX = resistanceScrollByX(mImgRect.left - mCommonRect.left, distanceX);
                    if (distanceX > 0 && mImgRect.right - distanceX < mCommonRect.right)
                        distanceX = resistanceScrollByX(mImgRect.right - mCommonRect.right, distanceX);
                }
    
                mTranslateX -= distanceX;
                mAnimaMatrix.postTranslate(-distanceX, 0);
                hasOverTranslate = true;
            }
    
            if (canScrollVerticallySelf(distanceY)) {
                if (distanceY < 0 && mImgRect.top - distanceY > mWidgetRect.top)
                    distanceY = mImgRect.top;
                if (distanceY > 0 && mImgRect.bottom - distanceY < mWidgetRect.bottom)
                    distanceY = mImgRect.bottom - mWidgetRect.bottom;
    
                mAnimaMatrix.postTranslate(0, -distanceY);
                mTranslateY -= distanceY;
            } else if (imgLargeHeight || hasOverTranslate || hasMultiTouch) {
                checkRect();
                if (!hasMultiTouch) {
                    if (distanceY < 0 && mImgRect.top - distanceY > mCommonRect.top)
                        distanceY = resistanceScrollByY(mImgRect.top - mCommonRect.top, distanceY);
                    if (distanceY > 0 && mImgRect.bottom - distanceY < mCommonRect.bottom)
                        distanceY = resistanceScrollByY(mImgRect.bottom - mCommonRect.bottom, distanceY);
                }
    
                mAnimaMatrix.postTranslate(0, -distanceY);
                mTranslateY -= distanceY;
                hasOverTranslate = true;
            }
    
            executeTranslate();
            return true;
        }
    
        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            postDelayed(mClickRunnable, 250);
            return false;
        }
    
        @Override
        public boolean onDoubleTap(MotionEvent e) {
    
            mTranslate.stop();
    
            float from = 1;
            float to = 1;
    
            float imgcx = mImgRect.left + mImgRect.width() / 2;
            float imgcy = mImgRect.top + mImgRect.height() / 2;
    
            mScaleCenter.set(imgcx, imgcy);
            mRotateCenter.set(imgcx, imgcy);
            mTranslateX = 0;
            mTranslateY = 0;
    
            if (isZoonUp) {
                from = mScale;
                to = 1;
            } else {
                from = mScale;
                to = mMaxScale;
    
                mScaleCenter.set(e.getX(), e.getY());
            }
    
            mTmpMatrix.reset();
            mTmpMatrix.postTranslate(-mBaseRect.left, -mBaseRect.top);
            mTmpMatrix.postTranslate(mRotateCenter.x, mRotateCenter.y);
            mTmpMatrix.postTranslate(-mHalfBaseRectWidth, -mHalfBaseRectHeight);
            mTmpMatrix.postRotate(mDegrees, mRotateCenter.x, mRotateCenter.y);
            mTmpMatrix.postScale(to, to, mScaleCenter.x, mScaleCenter.y);
            mTmpMatrix.postTranslate(mTranslateX, mTranslateY);
            mTmpMatrix.mapRect(mTmpRect, mBaseRect);
            doTranslateReset(mTmpRect);
    
            isZoonUp = !isZoonUp;
            mTranslate.withScale(from, to);
            mTranslate.start();
    
            return false;
        }
    };
    
    public boolean canScrollHorizontallySelf(float direction) {
        if (mImgRect.width() <= mWidgetRect.width()) return false;
        if (direction < 0 && Math.round(mImgRect.left) - direction >= mWidgetRect.left)
            return false;
        if (direction > 0 && Math.round(mImgRect.right) - direction <= mWidgetRect.right)
            return false;
        return true;
    }
    
    public boolean canScrollVerticallySelf(float direction) {
        if (mImgRect.height() <= mWidgetRect.height()) return false;
        if (direction < 0 && Math.round(mImgRect.top) - direction >= mWidgetRect.top)
            return false;
        if (direction > 0 && Math.round(mImgRect.bottom) - direction <= mWidgetRect.bottom)
            return false;
        return true;
    }
    
    @Override
    public boolean canScrollHorizontally(int direction) {
        if (hasMultiTouch) return true;
        return canScrollHorizontallySelf(direction);
    }
    
    @Override
    public boolean canScrollVertically(int direction) {
        if (hasMultiTouch) return true;
        return canScrollVerticallySelf(direction);
    }
    
    private class InterpolatorProxy implements Interpolator {
    
        private Interpolator mTarget;
    
        private InterpolatorProxy() {
            mTarget = new DecelerateInterpolator();
        }
    
        public void setTargetInterpolator(Interpolator interpolator) {
            mTarget = interpolator;
        }
    
        @Override
        public float getInterpolation(float input) {
            if (mTarget != null) {
                return mTarget.getInterpolation(input);
            }
            return input;
        }
    }
    
    private class Transform implements Runnable {
    
        boolean isRuning;
    
        OverScroller mTranslateScroller;
        OverScroller mFlingScroller;
        Scroller mScaleScroller;
        Scroller mClipScroller;
        Scroller mRotateScroller;
    
        ClipCalculate C;
    
        int mLastFlingX;
        int mLastFlingY;
    
        int mLastTranslateX;
        int mLastTranslateY;
    
        RectF mClipRect = new RectF();
    
        InterpolatorProxy mInterpolatorProxy = new InterpolatorProxy();
    
        Transform() {
            Context ctx = getContext();
            mTranslateScroller = new OverScroller(ctx, mInterpolatorProxy);
            mScaleScroller = new Scroller(ctx, mInterpolatorProxy);
            mFlingScroller = new OverScroller(ctx, mInterpolatorProxy);
            mClipScroller = new Scroller(ctx, mInterpolatorProxy);
            mRotateScroller = new Scroller(ctx, mInterpolatorProxy);
        }
    
        public void setInterpolator(Interpolator interpolator) {
            mInterpolatorProxy.setTargetInterpolator(interpolator);
        }
    
        void withTranslate(int startX, int startY, int deltaX, int deltaY) {
            mLastTranslateX = 0;
            mLastTranslateY = 0;
            mTranslateScroller.startScroll(0, 0, deltaX, deltaY, mAnimaDuring);
        }
    
        void withScale(float form, float to) {
            mScaleScroller.startScroll((int) (form * 10000), 0, (int) ((to - form) * 10000), 0, mAnimaDuring);
        }
    
        void withClip(float fromX, float fromY, float deltaX, float deltaY, int d, ClipCalculate c) {
            mClipScroller.startScroll((int) (fromX * 10000), (int) (fromY * 10000), (int) (deltaX * 10000), (int) (deltaY * 10000), d);
            C = c;
        }
    
        void withRotate(int fromDegrees, int toDegrees) {
            mRotateScroller.startScroll(fromDegrees, 0, toDegrees - fromDegrees, 0, mAnimaDuring);
        }
    
        void withRotate(int fromDegrees, int toDegrees, int during) {
            mRotateScroller.startScroll(fromDegrees, 0, toDegrees - fromDegrees, 0, during);
        }
    
        void withFling(float velocityX, float velocityY) {
            mLastFlingX = velocityX < 0 ? Integer.MAX_VALUE : 0;
            int distanceX = (int) (velocityX > 0 ? Math.abs(mImgRect.left) : mImgRect.right - mWidgetRect.right);
            distanceX = velocityX < 0 ? Integer.MAX_VALUE - distanceX : distanceX;
            int minX = velocityX < 0 ? distanceX : 0;
            int maxX = velocityX < 0 ? Integer.MAX_VALUE : distanceX;
            int overX = velocityX < 0 ? Integer.MAX_VALUE - minX : distanceX;
    
            mLastFlingY = velocityY < 0 ? Integer.MAX_VALUE : 0;
            int distanceY = (int) (velocityY > 0 ? Math.abs(mImgRect.top) : mImgRect.bottom - mWidgetRect.bottom);
            distanceY = velocityY < 0 ? Integer.MAX_VALUE - distanceY : distanceY;
            int minY = velocityY < 0 ? distanceY : 0;
            int maxY = velocityY < 0 ? Integer.MAX_VALUE : distanceY;
            int overY = velocityY < 0 ? Integer.MAX_VALUE - minY : distanceY;
    
            if (velocityX == 0) {
                maxX = 0;
                minX = 0;
            }
    
            if (velocityY == 0) {
                maxY = 0;
                minY = 0;
            }
    
            mFlingScroller.fling(mLastFlingX, mLastFlingY, (int) velocityX, (int) velocityY, minX, maxX, minY, maxY, Math.abs(overX) < MAX_FLING_OVER_SCROLL * 2 ? 0 : MAX_FLING_OVER_SCROLL, Math.abs(overY) < MAX_FLING_OVER_SCROLL * 2 ? 0 : MAX_FLING_OVER_SCROLL);
        }
    
        void start() {
            isRuning = true;
            postExecute();
        }
    
        void stop() {
            removeCallbacks(this);
            mTranslateScroller.abortAnimation();
            mScaleScroller.abortAnimation();
            mFlingScroller.abortAnimation();
            mRotateScroller.abortAnimation();
            isRuning = false;
        }
    
        @Override
        public void run() {
    
            // if (!isRuning) return;
    
            boolean endAnima = true;
    
            if (mScaleScroller.computeScrollOffset()) {
                mScale = mScaleScroller.getCurrX() / 10000f;
                endAnima = false;
            }
    
            if (mTranslateScroller.computeScrollOffset()) {
                int tx = mTranslateScroller.getCurrX() - mLastTranslateX;
                int ty = mTranslateScroller.getCurrY() - mLastTranslateY;
                mTranslateX += tx;
                mTranslateY += ty;
                mLastTranslateX = mTranslateScroller.getCurrX();
                mLastTranslateY = mTranslateScroller.getCurrY();
                endAnima = false;
            }
    
            if (mFlingScroller.computeScrollOffset()) {
                int x = mFlingScroller.getCurrX() - mLastFlingX;
                int y = mFlingScroller.getCurrY() - mLastFlingY;
    
                mLastFlingX = mFlingScroller.getCurrX();
                mLastFlingY = mFlingScroller.getCurrY();
    
                mTranslateX += x;
                mTranslateY += y;
                endAnima = false;
            }
    
            if (mRotateScroller.computeScrollOffset()) {
                mDegrees = mRotateScroller.getCurrX();
                endAnima = false;
            }
    
            if (mClipScroller.computeScrollOffset() || mClip != null) {
                float sx = mClipScroller.getCurrX() / 10000f;
                float sy = mClipScroller.getCurrY() / 10000f;
                mTmpMatrix.setScale(sx, sy, (mImgRect.left + mImgRect.right) / 2, C.calculateTop());
                mTmpMatrix.mapRect(mClipRect, mImgRect);
    
                if (sx == 1) {
                    mClipRect.left = mWidgetRect.left;
                    mClipRect.right = mWidgetRect.right;
                }
    
                if (sy == 1) {
                    mClipRect.top = mWidgetRect.top;
                    mClipRect.bottom = mWidgetRect.bottom;
                }
    
                mClip = mClipRect;
            }
    
            if (!endAnima) {
                applyAnima();
                postExecute();
            } else {
                isRuning = false;
    
                // 修复动画结束后边距有些空隙,
                boolean needFix = false;
    
                if (imgLargeWidth) {
                    if (mImgRect.left > 0) {
                        mTranslateX -= mImgRect.left;
                    } else if (mImgRect.right < mWidgetRect.width()) {
                        mTranslateX -= (int) (mWidgetRect.width() - mImgRect.right);
                    }
                    needFix = true;
                }
    
                if (imgLargeHeight) {
                    if (mImgRect.top > 0) {
                        mTranslateY -= mImgRect.top;
                    } else if (mImgRect.bottom < mWidgetRect.height()) {
                        mTranslateY -= (int) (mWidgetRect.height() - mImgRect.bottom);
                    }
                    needFix = true;
                }
    
                if (needFix) {
                    applyAnima();
                }
    
                invalidate();
    
                if (mCompleteCallBack != null) {
                    mCompleteCallBack.run();
                    mCompleteCallBack = null;
                }
            }
        }
    
        private void applyAnima() {
            mAnimaMatrix.reset();
            mAnimaMatrix.postTranslate(-mBaseRect.left, -mBaseRect.top);
            mAnimaMatrix.postTranslate(mRotateCenter.x, mRotateCenter.y);
            mAnimaMatrix.postTranslate(-mHalfBaseRectWidth, -mHalfBaseRectHeight);
            mAnimaMatrix.postRotate(mDegrees, mRotateCenter.x, mRotateCenter.y);
            mAnimaMatrix.postScale(mScale, mScale, mScaleCenter.x, mScaleCenter.y);
            mAnimaMatrix.postTranslate(mTranslateX, mTranslateY);
            executeTranslate();
        }
    
    
        private void postExecute() {
            if (isRuning) post(this);
        }
    }
    
    public Info getInfo() {
        RectF rect = new RectF();
        int[] p = new int[2];
        getLocation(this, p);
        rect.set(p[0] + mImgRect.left, p[1] + mImgRect.top, p[0] + mImgRect.right, p[1] + mImgRect.bottom);
        return new Info(rect, mImgRect, mWidgetRect, mBaseRect, mScreenCenter, mScale, mDegrees, mScaleType);
    }
    
    public static Info getImageViewInfo(ImageView imgView) {
        int[] p = new int[2];
        getLocation(imgView, p);
    
        Drawable drawable = imgView.getDrawable();
    
        Matrix matrix = imgView.getImageMatrix();
    
        int width = getDrawableWidth(drawable);
        int height = getDrawableHeight(drawable);
    
        RectF imgRect = new RectF(0, 0, width, height);
        matrix.mapRect(imgRect);
    
        RectF rect = new RectF(p[0] + imgRect.left, p[1] + imgRect.top, p[0] + imgRect.right, p[1] + imgRect.bottom);
        RectF widgetRect = new RectF(0, 0, imgView.getWidth(), imgView.getHeight());
        RectF baseRect = new RectF(widgetRect);
        PointF screenCenter = new PointF(widgetRect.width() / 2, widgetRect.height() / 2);
    
        return new Info(rect, imgRect, widgetRect, baseRect, screenCenter, 1, 0, imgView.getScaleType());
    }
    
    private static void getLocation(View target, int[] position) {
    
        position[0] += target.getLeft();
        position[1] += target.getTop();
    
        ViewParent viewParent = target.getParent();
        while (viewParent instanceof View) {
            final View view = (View) viewParent;
    
            if (view.getId() == android.R.id.content) return;
    
            position[0] -= view.getScrollX();
            position[1] -= view.getScrollY();
    
            position[0] += view.getLeft();
            position[1] += view.getTop();
    
            viewParent = view.getParent();
        }
    
        position[0] = (int) (position[0] + 0.5f);
        position[1] = (int) (position[1] + 0.5f);
    }
    
    private void reset() {
        mAnimaMatrix.reset();
        executeTranslate();
        mScale = 1;
        mTranslateX = 0;
        mTranslateY = 0;
    }
    
    public interface ClipCalculate {
        float calculateTop();
    }
    
    public class START implements ClipCalculate {
        public float calculateTop() {
            return mImgRect.top;
        }
    }
    
    public class END implements ClipCalculate {
        public float calculateTop() {
            return mImgRect.bottom;
        }
    }
    
    public class OTHER implements ClipCalculate {
        public float calculateTop() {
            return (mImgRect.top + mImgRect.bottom) / 2;
        }
    }
    
    /**
     * 在PhotoView内部还没有图片的时候同样可以调用该方法
     * <p></p>
     * 此时并不会播放动画,当给PhotoView设置图片后会自动播放动画。
     * <p></p>
     * 若等待时间过长也没有给控件设置图片,则会忽略该动画,若要再次播放动画则需要重新调用该方法
     * (等待的时间默认500毫秒,可以通过setMaxAnimFromWaiteTime(int)设置最大等待时间)
     */
    public void animaFrom(Info info) {
        if (isInit) {
            reset();
    
            Info mine = getInfo();
    
            float scaleX = info.mImgRect.width() / mine.mImgRect.width();
            float scaleY = info.mImgRect.height() / mine.mImgRect.height();
            float scale = scaleX < scaleY ? scaleX : scaleY;
    
            float ocx = info.mRect.left + info.mRect.width() / 2;
            float ocy = info.mRect.top + info.mRect.height() / 2;
    
            float mcx = mine.mRect.left + mine.mRect.width() / 2;
            float mcy = mine.mRect.top + mine.mRect.height() / 2;
    
            mAnimaMatrix.reset();
            // mAnimaMatrix.postTranslate(-mBaseRect.left, -mBaseRect.top);
            mAnimaMatrix.postTranslate(ocx - mcx, ocy - mcy);
            mAnimaMatrix.postScale(scale, scale, ocx, ocy);
            mAnimaMatrix.postRotate(info.mDegrees, ocx, ocy);
            executeTranslate();
    
            mScaleCenter.set(ocx, ocy);
            mRotateCenter.set(ocx, ocy);
    
            mTranslate.withTranslate(0, 0, (int) -(ocx - mcx), (int) -(ocy - mcy));
            mTranslate.withScale(scale, 1);
            mTranslate.withRotate((int) info.mDegrees, 0);
    
            if (info.mWidgetRect.width() < info.mImgRect.width() || info.mWidgetRect.height() < info.mImgRect.height()) {
                float clipX = info.mWidgetRect.width() / info.mImgRect.width();
                float clipY = info.mWidgetRect.height() / info.mImgRect.height();
                clipX = clipX > 1 ? 1 : clipX;
                clipY = clipY > 1 ? 1 : clipY;
    
                ClipCalculate c = info.mScaleType == ScaleType.FIT_START ? new START() : info.mScaleType == ScaleType.FIT_END ? new END() : new OTHER();
    
                mTranslate.withClip(clipX, clipY, 1 - clipX, 1 - clipY, mAnimaDuring / 3, c);
    
                mTmpMatrix.setScale(clipX, clipY, (mImgRect.left + mImgRect.right) / 2, c.calculateTop());
                mTmpMatrix.mapRect(mTranslate.mClipRect, mImgRect);
                mClip = mTranslate.mClipRect;
            }
    
            mTranslate.start();
        } else {
            mFromInfo = info;
            mInfoTime = System.currentTimeMillis();
        }
    }
    
    public void animaTo(Info info, Runnable completeCallBack) {
        if (isInit) {
            mTranslate.stop();
    
            mTranslateX = 0;
            mTranslateY = 0;
    
            float tcx = info.mRect.left + info.mRect.width() / 2;
            float tcy = info.mRect.top + info.mRect.height() / 2;
    
            mScaleCenter.set(mImgRect.left + mImgRect.width() / 2, mImgRect.top + mImgRect.height() / 2);
            mRotateCenter.set(mScaleCenter);
    
            // 将图片旋转回正常位置,用以计算
            mAnimaMatrix.postRotate(-mDegrees, mScaleCenter.x, mScaleCenter.y);
            mAnimaMatrix.mapRect(mImgRect, mBaseRect);
    
            // 缩放
            float scaleX = info.mImgRect.width() / mBaseRect.width();
            float scaleY = info.mImgRect.height() / mBaseRect.height();
            float scale = scaleX > scaleY ? scaleX : scaleY;
    
            mAnimaMatrix.postRotate(mDegrees, mScaleCenter.x, mScaleCenter.y);
            mAnimaMatrix.mapRect(mImgRect, mBaseRect);
    
            mDegrees = mDegrees % 360;
    
            mTranslate.withTranslate(0, 0, (int) (tcx - mScaleCenter.x), (int) (tcy - mScaleCenter.y));
            mTranslate.withScale(mScale, scale);
            mTranslate.withRotate((int) mDegrees, (int) info.mDegrees, mAnimaDuring * 2 / 3);
    
            if (info.mWidgetRect.width() < info.mRect.width() || info.mWidgetRect.height() < info.mRect.height()) {
                float clipX = info.mWidgetRect.width() / info.mRect.width();
                float clipY = info.mWidgetRect.height() / info.mRect.height();
                clipX = clipX > 1 ? 1 : clipX;
                clipY = clipY > 1 ? 1 : clipY;
    
                final float cx = clipX;
                final float cy = clipY;
                final ClipCalculate c = info.mScaleType == ScaleType.FIT_START ? new START() : info.mScaleType == ScaleType.FIT_END ? new END() : new OTHER();
    
                postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        mTranslate.withClip(1, 1, -1 + cx, -1 + cy, mAnimaDuring / 2, c);
                    }
                }, mAnimaDuring / 2);
            }
    
            mCompleteCallBack = completeCallBack;
            mTranslate.start();
        }
    }
    
    public void rotate(float degrees) {
        mDegrees += degrees;
        int centerX = (int) (mWidgetRect.left + mWidgetRect.width() / 2);
        int centerY = (int) (mWidgetRect.top + mWidgetRect.height() / 2);
    
        mAnimaMatrix.postRotate(degrees, centerX, centerY);
        executeTranslate();
    }
    

    }
    1.2 Info 工具类:

    /**

    • author: wu
    • date: on 2018/11/13.
    • describe:图片查看器
      */

    public class Info {

    // 内部图片在整个手机界面的位置
    RectF mRect = new RectF();
    
    // 控件在窗口的位置
    RectF mImgRect = new RectF();
    
    RectF mWidgetRect = new RectF();
    
    RectF mBaseRect = new RectF();
    
    PointF mScreenCenter = new PointF();
    
    float mScale;
    
    float mDegrees;
    
    ImageView.ScaleType mScaleType;
    
    public Info(RectF rect, RectF img, RectF widget, RectF base, PointF screenCenter, float scale, float degrees, ImageView.ScaleType scaleType) {
        mRect.set(rect);
        mImgRect.set(img);
        mWidgetRect.set(widget);
        mScale = scale;
        mScaleType = scaleType;
        mDegrees = degrees;
        mBaseRect.set(base);
        mScreenCenter.set(screenCenter);
    }
    

    }
    1.3 工具类 RotateGestureDetector:

    /**

    • author: wu
    • date: on 2018/11/13.
    • describe:图片查看器
      */

    public class RotateGestureDetector {

    private static final int MAX_DEGREES_STEP = 120;
    
    private OnRotateListener mListener;
    
    private float mPrevSlope;
    private float mCurrSlope;
    
    private float x1;
    private float y1;
    private float x2;
    private float y2;
    
    public RotateGestureDetector(OnRotateListener l) {
        mListener = l;
    }
    
    public void onTouchEvent(MotionEvent event) {
    
        final int Action = event.getActionMasked();
    
        switch (Action) {
            case MotionEvent.ACTION_POINTER_DOWN:
            case MotionEvent.ACTION_POINTER_UP:
                if (event.getPointerCount() == 2) mPrevSlope = caculateSlope(event);
                break;
            case MotionEvent.ACTION_MOVE:
                if (event.getPointerCount() > 1) {
                    mCurrSlope = caculateSlope(event);
    
                    double currDegrees = Math.toDegrees(Math.atan(mCurrSlope));
                    double prevDegrees = Math.toDegrees(Math.atan(mPrevSlope));
    
                    double deltaSlope = currDegrees - prevDegrees;
    
                    if (Math.abs(deltaSlope) <= MAX_DEGREES_STEP) {
                        mListener.onRotate((float) deltaSlope, (x2 + x1) / 2, (y2 + y1) / 2);
                    }
                    mPrevSlope = mCurrSlope;
                }
                break;
            default:
                break;
        }
    }
    
    private float caculateSlope(MotionEvent event) {
        x1 = event.getX(0);
        y1 = event.getY(0);
        x2 = event.getX(1);
        y2 = event.getY(1);
        return (y2 - y1) / (x2 - x1);
    }
    

    }

    interface OnRotateListener {
    void onRotate(float degrees, float focusX, float focusY);
    }
    2.在 Activity 和 xml 文件中设置
    首先在 xml 中添加代码:

    <com.example.qd.photolook.utils.PhotoView
        android:id="@+id/photoView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@drawable/icon"
        android:scaleType="centerInside"/>
    
    最后再对应的 Activity 中初始化控件,调用 enable() 方法即可:

    public class MainActivity extends AppCompatActivity {
    private PhotoView photoView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        photoView = findViewById(R.id.photoView);
    
        //调用方法
        photoView.enable();
    }
    

    }
    ————————————————
    版权声明:本文为CSDN博主「吴庆森」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/wuqingsen1/article/details/84029503

    展开全文
  • 之前也未接触过,找了很久,都没有找到我想要的效果,不是只能查看图库点击放大,要不就只是左右滑动的demo,于是修改了两个大神的代码(来源不明请见谅,因为是在网上下载的源代码合集),下面介绍下我修改好的相册...

    最近在做一个项目,需要用到点击选择相册图片和展示好友说说图片,之前也未接触过,找了很久,都没有找到我想要的效果,不是只能查看图库点击放大,要不就只是左右滑动的demo,于是修改了两个大神的代码(来源不明请见谅,因为是在网上下载的源代码合集),下面介绍下我修改好的相册功能:

    注:项目使用了开源框架Universal-Image-Loader

    • 显示本地图库所有照片
    • 点击放大,单击退出
    • 双击放大缩小
    • 支持左右滑动查看图片
    • 支持手势放大缩小图片

    下面看下项目结构:

      

    接下来看下demo的效果:

    手势放大这里不好演示,需要的同学可以下载源码略作参考下

    源码:http://download.csdn.net/detail/an_illusion/9534461


    展开全文
  • 在我们项目module build.gradle中配置compile 'com.bm.photoview:library:1.4.1'然后右上角Sync Now一下,去下载这个包。直接在布局文件里面添加&lt;com.bm.library.PhotoView android:id="@+id/photoview...
  • 职场小白迷上优美句子: 最近看了一部电影 《无问西东》,剧中台词比较喜欢,分享给大家: 如果有机会提前了解你的人生,知道青春也不过只有这些日子,...工具类下载地址,只有工具类,使用方式请查看博客,上传...
  • 图片的下载缓存用的是imageloader框架,可加载大量图片,不会出现内存溢出。
  • 一个图片浏览器,支持超大图、超长图、支持手势放大、支持查看原图、下载、加载百分比进度显示。采用区块复用加载,优化内存占用,有效避免OOM, 注意:支持网络图片、本地图片。 更新日志 v0.0.5新增:可设置缩放...
  • 选择界面: 预览界面(可以根据手势放大缩小):已经封装成一个lib包,直接添加项目依赖就能使用:下载依赖包地址源码:https://download.csdn.net/download/zqr772791008/10300742项目大概长这样:对类的说明:...
  • 平常项目学习积累。动画效果比较多,所以文件压缩包13M,上传不了。可以到我的GitHub去下载。 这是下载链接: https://github.com/shenn13/PictureScanView.git 欢迎给star。
  • Android手势操作实例

    2013-07-29 20:21:37
    Android手势操作实例,Andorid手机的一个特点是可以感应手势动作,在此基础上可派生出很多人性化的效果,比如缓冲滚动、图片放大等,最低层的手势动作是如何实现的呢?请下载本源码学习。
  • 该源码由源码天堂IOS源码频道免费提供下载。实现用Pinch手势撕破图片的效果。用pinch手势放大就可以看到效果了,是一个很好的学习例子,喜欢的朋友可以下载学习看看。
  • html移动开发手势缩放(纯干货)(基于hammer velocity) div实现手势缩放 移动 实现点击按钮放大缩小 在Android、ios端均可使用 上次为demo 下载后即可使用
  • 该项目在CAD系统CATIA中提供了基于手势的零件对象运动。 可以旋转,移动或放大或缩小。 此外,还具有基本的语音识别功能,可以更改旋转轴或执行其他一些有用的操作。 KinectCAD已用Visual C#2010编写。该软件包包括...
  • 根据github上demo修改的,支持手势滑动,多点触控放大缩小,支持viewpager中放大所有,支持网络下载图片...完整并且完美的例子,很好用的...
  • 在android的webView中,点击和双击是正常的,但是拖动和放大缩小手势没有用。换了个腾讯内核后恢复正常。 附上腾讯浏览器内核网站: https://x5.tencent.com/tbs/guide/sdkInit.html 接入就下载jar文件,然后在你...
  • 在某个point我是非常需要像百度地图那些自定义手势动作,能够放大,缩小,移动,旋转,但是在网上却找不到一个立马下载能使用的包,不说那么多,我先上接口代码: import android.view.MotionEvent; /** * ...
  • android 实现可以放大缩小的TextView

    千次阅读 2013-07-14 18:38:42
    我们在浏览网页时,网页的文本可以放大缩小,android两点手势,两点距离靠近时缩小,两点距离远离时候是放大。那么若果不用android的WebView控件,单纯的TextView能否做到放大缩小呢,其实也是可以的。只要响应和...
  • hammer使用: 代码:捏合、捏开、... (手机手势插件) 捏合、捏开、图片放大 的一个手机图片“放大缩小可拖动”的小效果。 相关js 到http://www.bootcdn.cn/ 查找和下载。 hammer.js的版本是v2.0.4 效果说明:...
  • 用scrollerView实现图片的放大缩小

    千次阅读 2013-01-27 13:17:27
    在进行图片处理的过程中,经常遇到类似于图片缩放的需求,以前做个一个用pinch手势对imageView进行缩放的例子,demo下载 下面就scrollView自带的图片缩放功能进行简单的介绍: 首先实现UIScrollerViewDelegate协议...
  • 实现类似微博配图浏览功能: ...双指拖拉图片放大缩小 滑动浏览多个配图或上下滑动浏览 下载查看原图 保存到手机相册等 (该Demo也包含ImageView、ScrollView、PageControl组合使用方法相信也有一定参考价值。)
  • android 图片浏览器 ...3.支持手势操作 4.完全自定义布局 项目源码请到GitHub下载:https://github.com/cyuanyang/imagebrowser 记得给星哦 转载于:https://www.cnblogs.com/pigface/p/5632734.html...
  • 放大 缩小 聚拉 手势 IOS 代码实例

空空如也

空空如也

1 2 3 4 5
收藏数 92
精华内容 36
关键字:

下载放大手势