自定义控件---仿PhotoView实现图片查看

前言

我们在对图片查看的时候,经常会选择开源项目PhotoView去实现,现在进行对PhotoView的一个简单实现。

自定义控件准备

自己实现图片查看,并且可以随手势放大缩小,用的的核心类如下

Matrix

进行图片处理,经常会使用到Matrix,首先稍微介绍下这个Matrix:3维矩阵,内部存储:new Float[9]

{  
MSCALE_X, MSKEW_X, MTRANS_X,    
        MSKEW_Y, MSCALE_Y, MTRANS_Y,    
        MPERSP_0, MPERSP_1, MPERSP_2    
};   

可以实现:

Translate           平移变换
Rotate              旋转变换
Scale               缩放变换
Skew                错切变换

简单操作:
你想要设置matrix的偏移量为200,100

Matrix transMatrix = new Matrix();  
float[] values = new float[] { 1.0, 0, 200, 0, 1.0, 100, 0, 0, 1.0 };  
transMatrix.setValues(values);  

也可以这样:

Matrix transMatrix = new Matrix();  
transMatrix.postTranslate(200, 100);  

获取缩放级别:

public final float getScale()  {  
    scaleMatrix.getValues(matrixValues);  
    return matrixValues[Matrix.MSCALE_X];  
}  

想详细了解,可以观看这里:Matrix详解

GestureDetector

识别手势解析类,我们需要在自定义控件的onTouch()或者onTouchEvent()方法中,调用GestureDetector.onTouchEvent(),并把MotionEvent传递进去即可。对于各种手势的回调,可以通过GestureDetector中的接口OnGestureListener来完成。

@Override
public boolean onTouch(View v, MotionEvent event) {
     mGestureDetector.onTouchEvent(event);
}

本例使用到的接口回调方法:onDoubleTap,双击时,最后一次点击调用,详细在demo查看

ScaleGestureDetector

处理缩放的工具类,用法与GestureDetector类似,需要在自定义控件的onTouch()或者onTouchEvent()方法中,调用ScaleGestureDetector .onTouchEvent(),并把MotionEvent传递进去即可。在ScaleGestureDetector 中的接口OnScaleGestureListener进行缩放的操作。

@Override
public boolean onTouch(View v, MotionEvent event) {
     mScaleGestureDetector.onTouchEvent(event);
}

在OnScaleGestureListener的回调方法中,主要有三个方法:

onScale
缩放时。返回值代表本次缩放事件是否已被处理。如果已被处理,那么detector就会重置缩放事件;如果未被处理,detector会继续进行计算,修改getScaleFactor()的返回值,直到被处理为止。因此,它常用在判断只有缩放值达到一定数值时才进行缩放
onScaleBegin
缩放开始。该detector是否处理后继的缩放事件。返回false时,不会执行onScale()。
onScaleEnd
缩放结束时。

开发之路

先上效果图:
效果图

既然是做对图片查看功能的自定义控件,这里我们选择继承已有控件ImageView

手势缩放实现

主要要实现的功能:当图片加载时,将图片在屏幕中居中;图片宽或高大于屏幕的,缩小至屏幕大小;自由对图片进行方法或缩小。下面是代码,注释相当详细,相信你可以看懂的

public class ZoomImageView extends ImageView implements ScaleGestureDetector.OnScaleGestureListener,
        View.OnTouchListener, ViewTreeObserver.OnGlobalLayoutListener {

    // 检测两个手指在屏幕上做缩放的手势工具类
    private ScaleGestureDetector mScaleGestureDetector;

    private float scale;

    // 图片缩放工具操作类Matrix
    private Matrix mScaleMatrix;

    private float[] matrixValues;

    // 图片放大的最大值
    public static final float SCALE_MAX = 10.0f;

    //初始化时的缩放比例,如果图片宽或高大于屏幕,此值将小于0
    private float initScale = 1.0f;

    // 是否是初次加载
    private boolean once = true;
    private RectF matrixRectF;

    //记录上次触摸点个数
    int lastPointerCount;

    private boolean isCanDrag;
    private float mLastX;
    private float mLastY;
    private boolean isCheckLeftAndRight;
    private boolean isCheckTopAndBottom;
    private double mTouchSlop;
    private GestureDetector mGestureDetector;
    boolean isAutoScale;
    //自动缩放中的节点
    private float SCALE_MID = 2f;
    public ZoomImageView(Context context) {
        this(context, null);
    }

    public ZoomImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ZoomImageView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        mScaleMatrix = new Matrix();
        matrixValues = new float[9];
        mScaleGestureDetector = new ScaleGestureDetector(context, this);
        //获得的是触发移动事件的最短距离,如果小于这个距离就不触发移动控件
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        super.setScaleType(ScaleType.MATRIX);
        this.setOnTouchListener(this);
    }
    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        float scale = getScale();
        // 前一个伸缩事件至当前伸缩事件的伸缩比率
        float scaleFactor = detector.getScaleFactor();
        if (getDrawable() == null) {
            return true;
        }
        //缩放的范围控制
        if ((scale < SCALE_MAX &&  scaleFactor > 1.0f)||(scale > initScale && scaleFactor <1.0f) ){

            //最大值最小值判断
            if (scaleFactor * scale <initScale){
                scaleFactor = initScale / scale;
            }
            if (scaleFactor * scale > SCALE_MAX){
                scaleFactor = SCALE_MAX / scale;
            }
            mScaleMatrix.postScale(scaleFactor,scaleFactor,detector.getFocusX(),detector.getFocusY());
            setImageMatrix(mScaleMatrix);
        }
        return true;
    }
    @Override  
    public boolean onScaleBegin(ScaleGestureDetector detector){  
        //消费事件
        return true;  
    }  

    @Override  
    public void onScaleEnd(ScaleGestureDetector detector)  {  }  

    @Override  
    public boolean onTouch(View v, MotionEvent event)  {  
        return mScaleGestureDetector.onTouchEvent(event);  

    }  
    /**
     * 当View加载完成时可能通过OnGlobalLayoutListener监听,在布局加载完成后获得一个view的宽高。
     */
    @Override
    public void onGlobalLayout() {
        if (once){
            Drawable d = getDrawable();
            if (d == null){
                return;
            }
            // 获取控件的宽度和高度
            int width = getWidth();
            int height = getHeight();
            // 获取到ImageView对应图片的宽度和高度
            int dw = d.getIntrinsicWidth();// 图片固有宽度
            int dh = d.getIntrinsicHeight();
            float scale = 1.0f;
            // 图片宽度大于控件宽度 & 图片的高度小于控件高度
            if (dw > width && dh <= height){
                scale = width *1.0f / dw;
            }
            // 图片高度大于控件高度 & 图片的宽度小于控件的宽度
            if (dh > height && dw <= width){
                scale = height * 1.0f / dh;
            }
            // 图片宽度大于控件宽度 & 图片高度大于控件高度
            if (dw >width && dh >height){
                scale = Math.min(dw * 1.0f /width,dh*1.0f/height);
            }
            initScale = scale;
            // 将图片移动到手机屏幕的中间位置
            mScaleMatrix.postTranslate((width - dw) / 2,(height - dh) /2);
            mScaleMatrix.postScale(scale,scale,getWidth()/2,getHeight()/2);
            setImageMatrix(mScaleMatrix);
            once = false;
        }
    }

    /**
     * 当view被附着到一个窗口时触发
     */
    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        getViewTreeObserver().addOnGlobalLayoutListener(this);
    }

    /**
     * 当view离开附着的窗口时触发
     */
    @SuppressWarnings("deprecation")
    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        getViewTreeObserver().removeGlobalOnLayoutListener(this);
    }

    /**
     * 获取当前的缩放比例
     * @return
     */
    public float getScale() {
        mScaleMatrix.getValues(matrixValues);
        // 变化的倍数
        return matrixValues[Matrix.MSCALE_X];
    }
}

我们在代码中设置了中心,但是这样会导致图片的位置的变化,最终导致,图片宽高大于屏幕时,图片与屏幕间出现白边;图片小于屏幕,但是不居中。

@Override
public boolean onScale(ScaleGestureDetector detector) {
    ......
    if ((scale < SCALE_MAX &&  scaleFactor > 1.0f)||(scale > initScale && scaleFactor <1.0f) ){
    ......
mScaleMatrix.postScale(scaleFactor,scaleFactor,detector.getFocusX(),detector.getFocusY());
        setImageMatrix(mScaleMatrix);
    }
    return true;
}

所以,我们在缩放的时候需要手动控制下范围,


/**
 * 在缩放时,进行图片显示范围的控制
 */
private void checkBorderAndCenterWhenScale() {
    RectF rect = getMatrixRectF();
    float deltaX = 0;
    float deltaY = 0;
    int width = getWidth();
    int height = getHeight();
    //如果宽或高大于屏幕,则控制范围,防止出现白边
    if (rect.width() >= width){
        if (rect.left >0){
            deltaX = -rect.left;
        }
        if (rect.right < width){
            deltaX = width - rect.right;
        }
    }
    if (rect.height() >= height){
        if (rect.top > 0){
            deltaY = -rect.top;
        }
        if (rect.bottom < height){
            deltaY = height - rect.bottom;
        }
    }
    // 如果宽度或者高度小于控件的宽或者高;则让其居中
    if (rect.width() < width){
        deltaX = width *0.5f -rect.right + 0.5f*rect.width();
    }
    if (rect.height() < height){
        deltaY = height * 0.5f -rect.bottom + 0.5f *rect.height();
    }
    mScaleMatrix.postTranslate(deltaX,deltaY);
}
/**
 * 根据当前图片的Matrix获取图片的范围
 * @return
 */
public RectF getMatrixRectF() {
    Matrix matrix = mScaleMatrix;
    RectF rect = new RectF();
    Drawable d = getDrawable();
    if (d != null){
        rect.set(0,0,d.getIntrinsicWidth(),d.getIntrinsicHeight());
        matrix.mapRect(rect);
    }
    return rect;
}

在onScale里面记得调用

@Override
public boolean onScale(ScaleGestureDetector detector) {
           ...
         //缩放的范围控制
        if ((scale < SCALE_MAX &&  scaleFactor > 1.0f)||(scale > initScale && scaleFactor <1.0f) ){
            checkBorderAndCenterWhenScale();
       }
    return true;
}

自由的进行移动

在图片长或宽大于屏幕,我们设置图片可以移动。

首先我们需要拿到触摸点的个数,

    int pointerCount = event.getPointerCount();

然后求出多个触摸点的平均值,设置给我们的mLastX , mLastY ,具体代码如下

@Override
public boolean onTouch(View v, MotionEvent event) {
    RectF rectF = getMatrixRectF();
    mScaleGestureDetector.onTouchEvent(event);
    mGestureDetector.onTouchEvent(event);
    float x = 0,y = 0;
    // 拿到触摸点的个数
    int pointerCount = event.getPointerCount();
    // 得到多个触摸点的x与y均值
    for (int i = 0; i < pointerCount; i++) {
        x += event.getX();
        y += event.getY();
    }
    x = x / pointerCount;
    y = y / pointerCount;
    //每当触摸点发生变化时,重置mLasX , mLastY
    if (pointerCount != lastPointerCount){
        isCanDrag = false;
        mLastX = x;
        mLastY = y;
    }
    lastPointerCount = pointerCount;
    switch (event.getAction()) {
        case MotionEvent.ACTION_MOVE:
            float dx = x - mLastX;
            float dy = y - mLastY;
            if (!isCanDrag){
                isCanDrag = isCanDrag(dx,dy);
            }
            if (isCanDrag){
                if (getDrawable() != null){
                    isCheckLeftAndRight = isCheckTopAndBottom = true;
                    // 如果宽度小于屏幕宽度,则禁止左右移动
                    if (rectF.width() < getWidth()){
                        dx = 0;
                        isCheckLeftAndRight = false;
                    }
                    // 如果高度小于屏幕高度,则禁止上下移动
                    if (rectF.height() < getHeight()){
                        dy = 0;
                        isCheckTopAndBottom = false;
                    }
                    if (rectF.left == 0 && dx > 0) {
                        getParent().requestDisallowInterceptTouchEvent(false);
                    }

                    if (rectF.right == getWidth() && dx < 0) {
                        getParent().requestDisallowInterceptTouchEvent(false);
                    }
                    mScaleMatrix.postTranslate(dx,dy);
                    checkMatrixBounds();
                    setImageMatrix(mScaleMatrix);
                }
            }
            mLastX = x;
            mLastY = y;
            if (rectF.width() > getWidth() || rectF.height() > getHeight()) {
                getParent().requestDisallowInterceptTouchEvent(true);
            }
            break;
        case MotionEvent.ACTION_DOWN:
            rectF =  getMatrixRectF();
            if (rectF.width() > getWidth() || rectF.height() > getHeight()) {
                getParent().requestDisallowInterceptTouchEvent(true);
            }
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            lastPointerCount = 0;
    }
    return true;
}

/**
 * 移动时,进行边界判断,主要判断宽或高大于屏幕的
 */
private void checkMatrixBounds() {
    RectF rect = getMatrixRectF();
    float deltaX = 0,deltaY = 0;
    float viewWidth = getWidth();
    float viewHeight = getHeight();
    // 判断移动或缩放后,图片显示是否超出屏幕边界
    if (rect.top >0 && isCheckTopAndBottom){
        deltaY = -rect.top;
    }
    if (rect.bottom < viewHeight && isCheckTopAndBottom){
        deltaY = viewHeight - rect.bottom;
    }
    if (rect.left > 0 && isCheckLeftAndRight){
        deltaX = -rect.left;
    }
    if (rect.right < viewWidth && isCheckLeftAndRight){
        deltaX = viewWidth - rect.right;
    }
    mScaleMatrix.postTranslate(deltaX,deltaY);
}

/**
 * 是否是推动行为
 * @param dx
 * @param dy
 * @return
 */
private boolean isCanDrag(float dx, float dy) {
    return Math.sqrt((dx * dx) + (dy *dy)) >= mTouchSlop;

}

双击放大与缩小

这个需要为GestureDetector设置监听器OnGestureListener,由于OnGestureListener默认要实现的方法很多,我们只需要onDoubleTap一个方法实现双击后的操作,所以我们实现一个SimpleOnGestureListener,重写onDoubleTap方法

public ZoomImageView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    mScaleMatrix = new Matrix();
    matrixValues = new float[9];
    mScaleGestureDetector = new ScaleGestureDetector(context, this);
    mGestureDetector = new GestureDetector(context,new GestureDetector.SimpleOnGestureListener(){
//            private boolean isAutoScale;
        @Override
        public boolean onDoubleTap(MotionEvent e) {

//                只缩放一次
//                if (isAutoScale == true){
//                    return true;
//                }
            float x = e.getX();
            float y = e.getY();
            //如果是小于2的,我们双击直接到变为原图的2倍
            if (getScale() < SCALE_MID){
                ZoomImageView.this.postDelayed(new AutoScaleRunnable(SCALE_MID, x, y),16);
//                    isAutoScale = true;
            }else if (getScale() >= SCALE_MID && getScale() <SCALE_MAX){
                ZoomImageView.this.postDelayed(
                        new AutoScaleRunnable(SCALE_MAX, x, y), 16);
//                    isAutoScale = true;
            }else{
                //还原
                ZoomImageView.this.postDelayed(
                        new AutoScaleRunnable(initScale, x, y), 16);
//                    isAutoScale = true;
            }
            return true;
        }
    });
    //获得的是触发移动事件的最短距离,如果小于这个距离就不触发移动控件
    mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    super.setScaleType(ScaleType.MATRIX);
    this.setOnTouchListener(this);
}

这里有个自动缩放的任务

/**
* 自动缩放的任务
*/
private class AutoScaleRunnable implements Runnable{

   static final float BIGGER = 1.07f;
   static final float SMALLER = 0.93f;
   private float mTargetScale;
   private float tmpScale;
   /**
    * 缩放的中心
    */
   private float x;
   private float y;

   /**
    * 传入目标缩放值,根据目标值与当前值,判断应该放大还是缩小
    *
    * @param targetScale
    */
   public AutoScaleRunnable(float targetScale, float x, float y) {
       this.mTargetScale = targetScale;
       this.x = x;
       this.y = y;
       if (getScale() < mTargetScale){
           tmpScale = BIGGER;
       }else{
           tmpScale = SMALLER;
       }
   }

   @Override
   public void run() {
       //进行缩放
       mScaleMatrix.postScale(tmpScale,tmpScale,x,y);
       checkBorderAndCenterWhenScale();
       setImageMatrix(mScaleMatrix);

       float currentScale = getScale();
       //如果值在合法范围内,继续缩放
       if (((tmpScale > 1f) && (currentScale < mTargetScale))|| ((tmpScale <1f) && (mTargetScale < currentScale))){
           ZoomImageView.this.postDelayed(this,16);
       }else{//设置为目标的缩放比例
           float deltaScale = mTargetScale / currentScale;
           mScaleMatrix.postScale(deltaScale , deltaScale , x , y);
           checkBorderAndCenterWhenScale();
           setImageMatrix(mScaleMatrix);
           isAutoScale = false;
       }
   }
}

代码中注释比较详细,如有需要请下载demo查阅

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值