自定义PhotoView实现突破预览

在各种APP中,我们会经常看中都会涉及到一个图片预览的功能。研究了android的手势和滑动处理,自定义实现一个PhotoView。支持图片双击放大,拖动,放大边界控制,双指放大缩小等功能 。话不多说,先看效果图。


效果图1

上图演示了图片的双击放大和缩小。项目我已经放大我的GitHub上,地址如下:
https://github.com/hugoca/PhotoView

本博文主要分析具体实现流程。

第一步 创建自定义View,初始化画笔和图片。
public class MyPhotoView extends View {
    private static final float IMAGE_WIDTH=Utils.dpToPixel(300);
    private static final float SCALE_FACTOR=1.5f;
    private Bitmap mBitmap;
    private Paint mPaint;
    
    public MyPhotoView(Context context) {
        super(context);
        init(context);
    }

    public MyPhotoView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public MyPhotoView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private void init(Context context){
        mBitmap= Utils.getPhoto(getResources(), (int) IMAGE_WIDTH);
        mPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
    }

}

工具类Util代码

工具类中提供两个方法, 一个是处理dp转换的,一个是获取图片Bitmap的。

public class Utils {
    public static float dpToPixel(float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
                Resources.getSystem().getDisplayMetrics());
    }

    public static Bitmap getPhoto(Resources resources,int width){
        BitmapFactory.Options options=new BitmapFactory.Options();
        options.inJustDecodeBounds=true;
        BitmapFactory.decodeResource(resources,R.drawable.photo,options);
        options.inJustDecodeBounds=false;
        options.inDensity=options.outWidth;
        options.inTargetDensity=width;
        return BitmapFactory.decodeResource(resources,R.drawable.photo,options);
    }
}
第三步重写onSizeChanged方法
@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        originalOffsetX=(getWidth()-mBitmap.getWidth())/2f;
        originalOffsetY=(getHeight()-mBitmap.getHeight())/2f;

        if((float)mBitmap.getWidth()/mBitmap.getHeight()>(float) getWidth()/getHeight()){
            smallScale=(float) getWidth()/mBitmap.getWidth();
            bigScale=(float)getHeight()/mBitmap.getHeight()*SCALE_FACTOR;
        }else {
            smallScale=(float)getHeight()/mBitmap.getHeight();
            bigScale=(float) getWidth()/mBitmap.getWidth()*SCALE_FACTOR;

        }
        curScale=smallScale;
    }
第四步 重写onDraw方法

重写onDraw方法进行图片的绘制

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        scaleFactor=(curScale-smallScale)/(bigScale-smallScale);
        //当前放大比例为small时,scaleFactor=0 不偏移
        //通过设置所放量来决定bitmap显示的偏移量
        canvas.translate(offsetX*scaleFactor,offsetY*scaleFactor);
        canvas.scale(curScale,curScale,getWidth()/2f,getHeight()/2f);
        canvas.drawBitmap(mBitmap,originalOffsetX,originalOffsetY,mPaint);
    }

    private float getMaxWidth(){
        return (mBitmap.getWidth()*bigScale-getWidth())/2;
    }

    private float getMaxHeight(){
        return (mBitmap.getHeight()*bigScale-getHeight())/2;
    }
第五步 添加手势控制

手势类中要处理的一个方法进行事件
1.将onDown事件返回值设置成true
2.处理双击事件,进行图片缩放
3.处理滑动事件
4.处理惯性滑动

class photoGestureDetector extends GestureDetector.SimpleOnGestureListener {
        @Override
        public boolean onDown(MotionEvent e) {
            return true;
        }

        @Override
        public boolean onDoubleTap(MotionEvent e) {
            isEnLarge=!isEnLarge;
            if(isEnLarge){
                offsetX=0;
                offsetY=0;
                offsetX=(e.getX()-getWidth()/2f)-(e.getX()-getWidth()/2f)*bigScale/smallScale;
                offsetY=(e.getY()-getHeight()/2f)-(e.getY()-getHeight()/2f)*bigScale/smallScale;
                getScaleAnimation().start();
            }else {
                getScaleAnimation().reverse();
            }
            return super.onDoubleTap(e);
        }

        /**
         *
         * @param e1        手指按下
         * @param e2        当前的
         * @param distanceX 旧位置 - 新位置
         * @param distanceY
         * @return
         */
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            if(isEnLarge){
                if(distanceX>0){
                    offsetX=Math.max(offsetX-distanceX,-getMaxWidth());
                }else {
                    offsetX=Math.min(offsetX-distanceX,getMaxWidth());
                }
                if(distanceY>0){
                    offsetY=Math.max(offsetY-distanceY,-getMaxHeight());
                }else {
                    offsetY=Math.min(offsetY-distanceY,getMaxHeight());
                }
                invalidate();
            }
            return super.onScroll(e1, e2, distanceX, distanceY);
        }

        @Override //惯性滑动
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            if(isEnLarge){
                overScroller.fling((int)offsetX,(int)offsetY,
                        (int)velocityX,(int)velocityY,
                        -(int)getMaxWidth(),(int)getMaxWidth()
                        ,-(int)getMaxHeight(),(int)getMaxHeight(),300,300);
                postOnAnimation(new FingRunnable());
            }
            return super.onFling(e1, e2, velocityX, velocityY);
        }
    }

惯性滑动用到的runnable

class FingRunnable implements Runnable{

        @Override
        public void run() {
            if (overScroller.computeScrollOffset()) {
                offsetX=overScroller.getCurrX();
                offsetY=overScroller.getCurrY();
                invalidate();
                postOnAnimation(this);
            }
        }
    }
第六步 添加双指操作处理
photoview2.gif
class PhotoScakeGestrueListener implements ScaleGestureDetector.OnScaleGestureListener{

        private float beginScale; //操作前的缩放比例
        @Override
        public boolean onScale(ScaleGestureDetector scaleGestureDetector) {
            if((curScale>smallScale&&!isEnLarge)||(curScale==smallScale&&!isEnLarge)){
                isEnLarge= true;
            }
            curScale=beginScale*scaleGestureDetector.getScaleFactor(); //缩放因子
            invalidate();
            return false;
        }

        @Override
        public boolean onScaleBegin(ScaleGestureDetector scaleGestureDetector) {
            beginScale=curScale;
            return true;
        }

        @Override
        public void onScaleEnd(ScaleGestureDetector scaleGestureDetector) {

        }
    }

到这里基本上自定义View已经实现了。为了方便实现,涉及到除了工具类都做的内部类实现,方便处理逻辑。最后添加相关类的手势的初始化

public class MyPhotoView extends View {
    private static final float IMAGE_WIDTH=Utils.dpToPixel(300);
    private static final float SCALE_FACTOR=1.5f;
    private Bitmap mBitmap;
    private Paint mPaint;

    //图片起始位置偏移坐标
    private float originalOffsetX;
    private float originalOffsetY;

     private float smallScale; //横向缩放填充
     private float bigScale;  //纵向缩放填充

    private float curScale; //当前缩放
    private boolean isEnLarge; //是否已经放大

    private GestureDetector gestureDetector; //手势操作
    private ObjectAnimator scaleAnimator; //处理缩放的动画
    private OverScroller overScroller; //处理惯性滑动
    private ScaleGestureDetector scaleGestureDetector;

  private void init(Context context){
        mBitmap=Utils.getPhoto(getResources(), (int) IMAGE_WIDTH);
        mPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
        gestureDetector=new GestureDetector(context, new photoGestureDetector());
        // 关闭长按响应
//        gestureDetector.setIsLongpressEnabled(false);
        overScroller=new OverScroller(context);
        scaleGestureDetector=new ScaleGestureDetector(context,new PhotoScakeGestrueListener());
    }

布局文件中添加

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:background="@color/colorPrimaryDark"
   tools:context=".MainActivity">

   <com.example.photoview.MyPhotoView
       android:layout_width="match_parent"
       android:layout_height="match_parent"/>



</FrameLayout>

到这里自定义View便实现了,完整项目在github(MyPhotoView 地址)上,在项目中我还放了一个处理多指滑动的view。希望对你有所帮助,还望不吝点赞!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,192评论 6 511
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,858评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,517评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,148评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,162评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,905评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,537评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,439评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,956评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,083评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,218评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,899评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,565评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,093评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,201评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,539评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,215评论 2 358