«

Android自定义PhotoView使用的方法是什么

时间:2024-5-19 09:48     作者:韩俊     分类: Android


这篇“Android自定义PhotoView使用的方法是什么”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“Android自定义PhotoView使用的方法是什么”文章吧。

准备工作

自定义PhotoView

自定义 PhotoView 继承(extends)自 View。并在最中间显示后面操作的图片。绘制图片可以重写 onDraw()方法,并在里面通过Canvas.drawBitmap()来要绘制图片。

drawBitmap()的四个参数:

    bitmap: 要在 Canvas 中绘制的位图

    letf:正在绘制的位图左侧的位置

    top:正在绘制的位图顶部的位置

    paint: 画笔

其中 (left, top) 是要绘制图片的起始坐标。要将图片绘制在中间,我们就需要计算 left/top 的位置。我们重写 onSizeChanged() 函数,该函数在onDraw之前调用,且尺寸改变时也要调用。

其中:(下面代码中是用 originalOffsetX/originalOffsetY 来代替的)

left = (getWidth() - bitmap.getWidth()) / 2;

top =(getHeight() - bitmap.getHeight()) / 2;

public class PhotoView extends View {
    private static final float IMAGE_WIDTH = Utils.dpToPixel(300);
    private Bitmap bitmap;
    private Paint paint; // 画笔
    private float originalOffsetX;
    private float originalOffsetY;
    public PhotoView(Context context) {
        this(context, null);
    }
    public PhotoView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public PhotoView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }
    /**
     * 初始化操作
     */
    private void init() {
        bitmap = Utils.getPhoto(getResources(), (int) IMAGE_WIDTH); // 获取到图片
        paint = new Paint();
    }
    /**
     * TODO 在onDraw之前调用,且尺寸改变时也要调用
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        originalOffsetX = (getWidth() - bitmap.getWidth()) / 2f;
        originalOffsetY = (getHeight() - bitmap.getHeight()) / 2f;
    }
    /**
     * 画出图片
     * @param canvas 画布
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawBitmap(bitmap, originalOffsetX, originalOffsetY, paint);
    }
}

    xml 布局

xml 布局中最外层是 FragmeLayout,里面只有一个自定义的 PhotoView 用来展示图片。

<?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"
    tools:context=".MainActivity">
    <com.example.photoview2.PhotoView
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</FrameLayout>

    Utils 工具类

Utils 工具类里主要有两个函数。dpToPixel() 将 dp 转换为像素;getPhot() 加载 Drawable 下的图片,并返回为 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 res, int width) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, R.drawable.photo, options);
        options.inJustDecodeBounds = false;
        options.inDensity = options.outWidth;
        options.inTargetDensity = width;
        return BitmapFactory.decodeResource(res, R.drawable.photo, options);
    }
}

1、双击放大和缩小

    设置图片的缩放比例

如下图的三种情况,左边的是原图;中间是小放大(smallScale),即图片左右两边贴进屏幕;右边是大放大(bigScale),即图片沾满整个屏幕。

根据上面的描述,设置两个变量即 smallScale 和 bigScale 分别代表上图"中"和“右”的缩放比例,smallScale 是初始样式,bigSmall 是双击后的样式。将 smallScale 和 bigScale 的设置放在 onSizeChanged() 函数里设值。如下图所示

/**
     * TODO 在onDraw之前调用,且尺寸改变时也要调用
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        originalOffsetX = (getWidth() - bitmap.getWidth()) / 2f;
        originalOffsetY = (getHeight() - bitmap.getHeight()) / 2f;
        // TODO 判断 bitmap 是扁的还是长的
        if ((float)bitmap.getWidth() / bitmap.getHeight() > (float) getWidth() / getHeight()) {
            // bitmap 的 width > height
            smallScale = (float) getWidth() / bitmap.getWidth();
            bigScale = (float) getHeight() / bitmap.getHeight()  * OVER_SCALE_FACTOR;
         }else {
            // bitmap 的 height > width
            smallScale = (float) getHeight() / bitmap.getHeight();
            bigScale = (float) getWidth() / bitmap.getWidth()  * OVER_SCALE_FACTOR;
        }
        currentScale = smallScale;
    }

注意 if 里的判断条件,判断图片是扁平还是长的。如下图理解,当然我们这里用的图是扁平的。currentScale 是当前的缩放比例,smallScale <= currentScale <= bigScale 。

最后设置了 smallScale 和 bigScale 后,我们还要在 onDraw 里将 smallScale 放大的图片绘制出来。这里用 currentScale ,因为在 onSizeChanged 函数里,我们将 smallScale 赋值给了 currentScale 。使用 Canvas.scale 函数进行缩放。

// TODO 图片放大,
// 第1,2个参数是放大比例,第3,4个参数是缩放的起始点,默认是(0,0)
canvas.scale(currentScale, currentScale, getWidth() / 2f, getHeight() / 2f);

    双击击缩放

Android 为我们提供了一个 GestureDetector 类来实现双击、单击、滑动、惯性滑动等。在 init 函数里添加如下代码,初始化 GestureDetector。gestureDectector 是一个全局变量。

gestureDetector = new GestureDetector(context, new photoGestureListener());

GestureDetector 的第二个参数是一个 Listener ,所以我们写了个内部类 photoGestureListener 继承GestureDetector.SimpleOnGestureListener。SimpleOnGestureListener 是一个 interface, 所以我们重写里面的方法,其中onDoubleTap() 就是实现写双击缩放的。

注意:onDown() 方法要返回 true 才能响应到双击事件

/**
     * TODO 单击/双击/惯性滑动的监听
     */
    class photoGestureListener extends GestureDetector.SimpleOnGestureListener{
        // up 时触发,单击或者双击的第一次会触发 --- up时,如果不是双击的得二次点击,不是长按,则触发
        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            return super.onSingleTapUp(e);
        }
        // 长按 默认300ms后触发
        @Override
        public void onLongPress(MotionEvent e) {
            super.onLongPress(e);
        }
        /**
         * 滚动 --move
         * @param e1 手指按下
         * @param e2 当前动作
         * @param distanceX 就位置 - 新位置
         * @param distanceY
         * @return
         */
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            return super.onScroll(e1, e2, distanceX, distanceY);
        }
        /**
         * 惯性滑动
         * @param velocityX X轴方向运动速度 像素/s
         * @param velocityY Y轴方向运动速度 像素/s
         * @return
         */
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            return super.onFling(e1, e2, velocityX, velocityY);
        }
        // 处理点击效果 --延时 100ms 触发
        @Override
        public void onShowPress(MotionEvent e) {
            super.onShowPress(e);
        }
        // 只需要关注 onDown 的返回值,默认返回 false
        @Override
        public boolean onDown(MotionEvent e) {
            return true;
        }
        // 双击的第二次点击 down 时触发 双击 40ms -- 300ms 之间
        @Override
        public boolean onDoubleTap(MotionEvent e) {
//            // TODO 第一版,这种直接放大/缩小有点深硬,不平滑
//            isEnlarge = !isEnlarge;
//            if (isEnlarge) {
//                currentScale = bigScale; // 双击放大
//            }else {
//                currentScale = smallScale; // 再双击时放小
//            }
//            invalidate(); // 刷新
            //TODO 第二版,借助属性动画实现
            isEnlarge = !isEnlarge;
            if (isEnlarge) {
                // TODO 双击时计算偏移,双击那个位置,就放大那个位置 / (e.getX(), e.getY()) 当前点击的位置
                offsetX = (e.getX() - getWidth() / 2f)
                        - (e.getX() - getWidth() / 2f) * bigScale / smallScale;
                offsetY = (e.getY() - getHeight() / 2f)
                        - (e.getY() - getHeight() / 2f) * bigScale / smallScale;
                fitOffsets(); // 解决点击图片外时放大空白部分
                getScaleAnimator().start();
            }else {
                getScaleAnimator().reverse();
            }
            return super.onDoubleTap(e);
        }
        // 双击的第二次down, move, up 都触发
        @Override
        public boolean onDoubleTapEvent(MotionEvent e) {
            return super.onDoubleTapEvent(e);
        }
        // 单击按下时触发,双击时不触发/ down, up时都可能触发(不会同时触发)
        // 延时300ms触发TAP事件
        // 300ms 以内抬手  -- 才会触发TAP -- onSingleTapConfirmed
        // 300ms 以后抬手 -- 不是双击或长按,则触发
        @Override
        public boolean onSingleTapConfirmed(MotionEvent e) {
            return super.onSingleTapConfirmed(e);
        }
    }

onDoubleTap() 里的第一版代码里 currentScale 直接由 smallScale 变到 bigscale,一下子就放大了,就很生硬不平滑。为了实现平滑的效果,我们使用 属性动画(ObjectAnimator),使得currentScale 由 smallScale 逐步变化到 bigScale,即 currentScale

(smallScale, bigScale)

private ObjectAnimator getScaleAnimator(){
        if (scaleAnimator == null) {
            scaleAnimator = ObjectAnimator.ofFloat(this, "currentScale", 0);
        }
        // TODO 平滑的范围,从 smallScale --> bigScale
        scaleAnimator.setFloatValues(smallScale, bigScale);
        return scaleAnimator;
    }
    public float getCurrentScale() {
        return currentScale;
    }
    public void setCurrentScale(float currentScale) {
        this.currentScale = currentScale;
        // 每一次在 smallScale -- bigScale 直接变化时都刷新
        invalidate();
    }

注意:上面代码里的 offsetX / offsetY 两个变量这里没讲,是因为它们是滑动里用到的变量,所以我们放到下一小节里讲,这里用它们是为了实现双击那个位置,就放大那个位置。如果把下面两句代码注释掉,会发现双击的时候永远是从中间位置放大。实现原理就是 offsetX / offsetY 是两个偏移量,我们从中间放大后再移到 offsetX / offsetY 的位置,就实现了点击哪里就放大哪里。

offsetX = (e.getX() - getWidth() / 2f)
           - (e.getX() - getWidth() / 2f) * bigScale / smallScale;
 offsetY = (e.getY() - getHeight() / 2f)
           - (e.getY() - getHeight() / 2f) * bigScale / smallScale;
fitOffsets(); // 解决点击图片外时放大空白部分

完成上面的代码,当我们运行程序然后双击屏幕时发现图片并没有放大,为什么?因为我们双击的时候触发的是 photoView 的 onTouchEvent(),而双击时需要触发 GestureDetector 的 onToucEvent()才能实现效果,所以我们再 photoView 里重写 onTouchEvent ,并用 GestureDetector 的 onTouchEvent() 来强制接管。

/** TODO 我们点击图片时,触发的是 PhotoView 里的 onTouchEvent,
     *  TODO 并没有触发 GestureDetector 里的onTouchEvent, 所以才需要强制接管
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return gestureDetector.onTouchEvent(event);
        //return super.onTouchEvent(event);
    }

2、滑动和惯性滑动

当我们双击放大图片后,可以通过手指滑动查看屏幕外面的内容,或者用力往某个方向滑动,实现惯性滑动的效果。

    手指滑动

在上面一节提到的 SimpleOnGestureListener 接口,里面的 onScroll 函数实现滑动。offsetX offsetY 是滑动的偏移量,即滑动到了图片的那个位置,在绘制的时候才能把滑动到的位置的图片绘制出来。

        /**
         * 滚动 --move
         * @param e1 手指按下
         * @param e2 当前动作
         * @param distanceX 就位置 - 新位置
         * @param distanceY
         * @return
         */
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            // 图片放大时,才可以滑动,即改变 offsetX offsetY
            if (isEnlarge) {
                offsetX -= distanceX;
                offsetY -= distanceY;
                fitOffsets();
                invalidate();
            }
            return super.onScroll(e1, e2, distanceX, distanceY);
        }

if 里的判断条件是确保在图片放大的情况下才进行滑动。fitOffsets() 是一个功能函数,计算图片滑动到边界的情况,放大后图片的边界滑动到屏幕边界时就滑不动了。

    /**
     * 计算图片滑动的边界情况
     * TODO 当往某个方向滑动图片时,放大后的图片边界与手机屏幕边界重合时,就不能滑动了
     */
    private void fitOffsets(){
        offsetX = Math.min(offsetX, (bitmap.getWidth() * bigScale - getWidth()) / 2);
        offsetX = Math.max(offsetX, -(bitmap.getWidth() * bigScale - getWidth()) / 2);
        offsetY = Math.min(offsetY, (bitmap.getHeight() * bigScale - getHeight()) / 2);
        offsetY = Math.max(offsetY, -(bitmap.getHeight() * bigScale - getHeight()) / 2);
    }

对 offsetX 取值用 Math.min()和 Math.max() 的情况可以如下图理解。offsetY 同理。

设置好了 onScroll() 函数后,我们还要将滑动的图片绘制出来,所以我们还要在 onDraw 函数里调用 Canvas.translate(), 将滑动的偏移 offsetX / offsetY 设置进去。

// TODO 图片滑动查看隐藏部分
canvas.translate(offsetX, offsetY);

惯性滑动

SimpleOnGestureListener 接口里的 onFling 函数实现惯性滑动。通过 OverScroll.fling() 来实现,filing 函数的最后两个参数表示当滑动到边界时,如果还有速度,则会将边界外的空白部分拉出200像素,然后立马回弹回去的那种效果。可以尝试将这两个参数去掉对比两种情况的效果。

        /**
         * 惯性滑动
         * @param velocityX X轴方向运动速度 像素/s
         * @param velocityY Y轴方向运动速度 像素/s
         * @return
         */
        @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) (bitmap.getWidth() * bigScale - getWidth()) / 2,
                        (int) (bitmap.getWidth() * bigScale - getWidth()) /2,
                        -(int) (bitmap.getHeight() * bigScale - getHeight()) / 2,
                        (int) (bitmap.getHeight() * bigScale - getHeight()) /2,
                        200, 200);
                // TODO 我们要不断的刷新界面,不断的改变 offsetX, offsetY, 参数:Runnable接口
                // postOnAnimation 下一帧动画的时候执行
                postOnAnimation(new flingRunner());
            }
            return super.onFling(e1, e2, velocityX, velocityY);
        }

我们在惯性滑动时要不断的刷新界面,不断改变 offsetX / offsetY 。我们使用 postOnAnimation(),里面传入一个 filingRunner 接口,继承自Runnable 。然后在filingRunner 里再调用postOnAnimation() 实现循环的效果。用 overScroller.computeScrollOffset() 函数计算当前的偏移并赋值给 offsetX/offsetY,实现不断改变它的功能。当computeScrollOffset() 返回 false,则表明当前的惯性速度为0,惯性滑动就结束,则结束循环。

class flingRunner implements Runnable{
        @Override
        public void run() {
            // TODO 用 overScroller 计算当前的偏移,并赋值给offsetX, offsetY
            if (overScroller.computeScrollOffset()) {
                // computeScrollOffset()会返回一个boolean值,为true, 说明动作还没完成,以此来作为循环结束条件
                offsetX = overScroller.getCurrX();
                offsetY = overScroller.getCurrY();
                invalidate();
                //在上面的onFling 方法里面,postOnAnimation 只会调用一次,所以我们这里再调用,参数:自己(flingRunner)
                //TODO postOnAnimation 下一帧动画的时候执行
                postOnAnimation(this);
            }
        }
    }

注意:写到这里,就有了一个小 bug ,就是当我们滑动了图片后再双击放小,会发现图片不会显示在正中间了,只需在 onDraw() 函数里做如下修改:我们在 offsetX / offsetY 上乘以一个平移因子,当双击缩小的时候,currentScale == smallScale ,则 scaleFaction == 0 --> offsetX / offsetY ==0 ,就相当于没有平移了,所以双击缩小时就能显示在原位置。

        // 解决:当位置移动后,双击缩小,让图片显示在最初的位置
        // 双击缩小时,currentScale = smallScale, 所以 scaleFunction = 0, 所以 translate就相当于没有平移
        float scaleFaction = (currentScale - smallScale) / (bigScale - smallScale);
        // TODO 图片滑动查看隐藏部分
        canvas.translate(offsetX * scaleFaction, offsetY * scaleFaction);

3、双指放大和缩小

Android 为我们提供了一个 ScaleGestureDetector 类来实现双指缩放功能。在 init() 函数里初始化。

scaleGestureDetector = new ScaleGestureDetector(context, new photoScaleGestureListener());

photoScaleGestureListener() 实现了ScaleGestureDetector.onScaleGestureListener 接口,实现里面的三个方法。

    onScale:处理正在缩放

    onScaleBegin: 开始缩放

    onScaleEnd: 结束缩放

/**
     * TODO 双指缩放大的监听
     */
    class photoScaleGestureListener implements ScaleGestureDetector.OnScaleGestureListener{
       float initScale;
        // 处理正在缩放
        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            if (currentScale >= smallScale && !isEnlarge) {
                isEnlarge = !isEnlarge;
            }
            // 缩放因子 缩放后 / 缩放前
            // eg 放大后=10,放大前=5, 缩放因子 == 10 / 5 == 2
            currentScale = initScale * detector.getScaleFactor();
            invalidate();
            return false;
        }
        // 开始缩放
        @Override
        public boolean onScaleBegin(ScaleGestureDetector detector) {
            initScale = currentScale;
            return true;
        }
        //结束缩放
        @Override
        public void onScaleEnd(ScaleGestureDetector detector) {
 
        }
    }

同理,ScaleGestureDetector 的触发也需要在 photoView 里的 onTouchEvent 里强制接管,所以修改 onTouchEvnet() 里的代码如下:

    /** TODO 我们点击图片时,触发的是 PhotoView 里的 onTouchEvent,
     *  TODO 并没有触发 GestureDetector 里的onTouchEvent, 所以才需要强制接管
     *  TODO 同理,ScaleGestureDetector 也需要接管
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // TODO 响应事件以双指缩放优先
        boolean result = scaleGestureDetector.onTouchEvent(event);
        if(!scaleGestureDetector.isInProgress()){
            // TODO 不是双指缩放,则用 GestureDetector 的 onTouchEvent 强制接管
            result = gestureDetector.onTouchEvent(event);
        }
        return result;
        //return super.onTouchEvent(event);
    }

4、完整DEMO

完整的 photoView 代码(MainActivity里没写什么)

package com.example.photoview;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.widget.OverScroller;
import androidx.annotation.Nullable;
public class PhotoView extends View {
    private static final float IMAGE_WIDTH = Utils.dpToPixel(300);
    private Bitmap bitmap;
    private Paint paint;
    float originalOffsetX;
    float originalOffsetY;
    private float smallScale;
    private float bigScale;
    private float currentScale; //当前缩放值
    private float OVER_SCALE_FACTOR = 1.5f;
    private boolean isEnlarge = false; //双击时放大/缩小的标志位
    private ObjectAnimator scaleAnimator; // 双击放大/缩小时,通过属性动画做出平滑的效果
    private GestureDetector gestureDetector; // android 提高的手势探测器,TODO 判断是单价还是双击
    private ScaleGestureDetector scaleGestureDetector; // TODO 实现双指缩放
    private float offsetX; // 图片放大后,手指滑动图片查看隐藏部分
    private float offsetY;
    private OverScroller overScroller; // TODO 实现惯性滑动
    public PhotoView(Context context) {
        this(context, null);
    }
    public PhotoView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public PhotoView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }
    private void init(Context context){
        bitmap = Utils.getPhoto(getResources(), (int) IMAGE_WIDTH);
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        gestureDetector = new GestureDetector(context, new photoGestureListener());
        scaleGestureDetector = new ScaleGestureDetector(context, new photoScaleGestureListener());
        //&nb

标签: android

热门推荐