自定义View实现图片的拖动和缩放

参考文章
1. Android 手势检测实战 打造支持缩放平移的图片预览效果(上)
2. Android 手势检测实战 打造支持缩放平移的图片预览效果(下)
3. 我的Android进阶之旅——>android Matrix图片随意的放大缩小,拖动

整体思路:
1. 实现缩放功能:
(1) 创建ScaleGestureDetector对象,实现ScaleGestureDetector.OnScaleGestureListener接口;
(2) 在onScale方法中实现缩放逻辑 , 相关逻辑包括获取缩放比例的初始值、定义放大的上限比例;
(3) setOnTouchListener(this),实现OnTouchListener接口,接收触摸事件。
2. 实现拖动功能:
(1) 计算、修正拖动距离
(2) 实现拖动

更多细节呈现在代码中。

代码实现

public class ScalableImageView extends ImageView implements ScaleGestureDetector.OnScaleGestureListener, ViewTreeObserver.OnGlobalLayoutListener {

    private static final String TAG = "ScalableImageView";

    private int gesture;

    private static final int GESTURE_DRAG = 1;
    private static final int GESTURE_ZOOM = 2;

    //  最大的缩放比例
    private static final float MAX_SCALE = 4.0f;

    //  初始化时的缩放比例,如果图片宽或高大于屏幕,此值将小于1(HongYang大神的博客上有笔误)
    private float initScale = 1.0f;

    private Matrix mMatrix = new Matrix();

    private ScaleGestureDetector mScaleGestureDetector;
    private int viewWidth;
    private int viewHeight;
    private Drawable mDrawable;

    public ScalableImageView(Context context) {
        this(context, null);
    }

    public ScalableImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
//      这一步很关键
        super.setScaleType(ScaleType.MATRIX);
        mScaleGestureDetector = new ScaleGestureDetector(context, this);
    }

    @Override
    public boolean onScale(ScaleGestureDetector scaleGestureDetector) {
//      全局缩放比例
        float scale = getScale();
//      上一次缩放事件到当前事件的缩放比例(微分缩放比例)
        float factor = scaleGestureDetector.getScaleFactor();
//      第一次获取的factor偏小,会引起缩放手势触屏的一瞬间图片缩小
        if (resetFactor) {
            factor = 1.0f;
            resetFactor = false;
        }

//      drawable为空或者缩放比例超出范围,拒执行
        if (getDrawable() == null || scale * factor > MAX_SCALE || scale * factor < initScale) {
            return false;
        } else {
            mMatrix.postScale(factor, factor, getWidth() / 2, getHeight() / 2);
            setImageMatrix(mMatrix);
            return true;
        }

//        以下是HongYang大神的思路
//        在INIT_VALUE到MAX_SCALE范围内缩放(放大不超过MAX_SCALE,缩小不小于INIT_SCALE)
//        if ((scale < MAX_SCALE && factor >= 1.0f) || (scale > initScale && factor <= 1.0f)) {
//            if (scale * factor > MAX_SCALE) {//放大超过MAX_SCALE的处理
//                factor = MAX_SCALE / scale;
//            } else if (scale * factor < initScale) {//缩小超过INIT_SCALE的处理
//                factor = initScale / scale;
//            }
//            mMatrix.postScale(factor, factor, getWidth() / 2, getHeight() / 2);
//            setImageMatrix(mMatrix);
//        }
//        return true;
    }

    private final float[] matrixValues = new float[9];

    //  获取全局缩放比例(相对于未缩放时的缩放比例),和直接用getScaleX()有区别!
    private float getScale() {
        mMatrix.getValues(matrixValues);
        return matrixValues[Matrix.MSCALE_X];
    }

    //  缩放开始时:可用于过滤一些手势,比如从有效区以外的区域划进来的手势
    @Override
    public boolean onScaleBegin(ScaleGestureDetector scaleGestureDetector) {
        return true;
    }

    //  缩放结束时
    @Override
    public void onScaleEnd(ScaleGestureDetector scaleGestureDetector) {

    }

    private float mLastX;
    private float mLastY;
    private boolean resetFactor;
    private boolean onZoomFinished;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();

        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                gesture = GESTURE_DRAG;
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                gesture = GESTURE_ZOOM;
                resetFactor = true;
                break;
            case MotionEvent.ACTION_POINTER_UP:
                gesture = GESTURE_DRAG;
                onZoomFinished = true;
                break;
            case MotionEvent.ACTION_MOVE:
                switch (gesture) {
                    case GESTURE_DRAG:
                        // 屏蔽缩放转换为拖动时的跳动,此时dx、dy偏大。
                        if (onZoomFinished) {
                            onZoomFinished = false;
                            break;
                        }
                        float dx = x - mLastX;
                        float dy = y - mLastY;
                        // 对偏移值做适当修正,避免图片与视图边间产生空白区域
                        PointF dragDelta = amendDelta(dx, dy);
                        mMatrix.postTranslate(dragDelta.x, dragDelta.y);
                        setImageMatrix(mMatrix);
                        break;
                    case GESTURE_ZOOM:
                        mScaleGestureDetector.onTouchEvent(event);
                        break;
                }
                break;
            case MotionEvent.ACTION_UP:
                // 消除缩放造成的图片距视图边的空白
                PointF zoomDelta = amendDelta(0, 0);
                mMatrix.postTranslate(zoomDelta.x, zoomDelta.y);
                setImageMatrix(mMatrix);
                break;
        }
        mLastX = x;
        mLastY = y;
        return true;
    }

    private PointF amendDelta(float dx, float dy) {
        RectF rectF = getRectF();
        if (rectF.width() > viewWidth) {// 图片宽度超过视图宽度
            if (rectF.left + dx > 0) {// 拖动会引起图片左边出现空白
                dx = -rectF.left;
            } else if (rectF.right + dx < viewWidth) {// 拖动会引起图片右边出现空白
                dx = viewWidth - rectF.right;
            }
        } else {// 图片宽度不及视图宽度,不允许图片宽度方向可视区域离开边界
            if (rectF.left + dx < 0) {
                dx = -rectF.left;
            } else if (rectF.right + dx > viewWidth) {
                dx = viewWidth - rectF.right;
            }
        }

        if (rectF.height() > viewHeight) {
            if (rectF.top + dy > 0) {
                dy = -rectF.top;
            } else if (rectF.bottom + dy < viewHeight) {
                dy = viewHeight - rectF.bottom;
            }
        } else {
            if (rectF.top + dy < 0) {
                dy = -rectF.top;
            } else if (rectF.bottom + dy > viewHeight) {
                dy = viewHeight - rectF.bottom;
            }
        }
        return new PointF(dx, dy);
    }

    private RectF mRectF;

    public RectF getRectF() {
        if (mDrawable == null) {
            mDrawable = getDrawable();
        }
        if (mRectF == null) {
            mRectF = new RectF();
        }
        if (mDrawable != null) {
            mRectF.set(0, 0, mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
            mMatrix.mapRect(mRectF);
        }
        return mRectF;
    }

    //  onGlobalLayoutListener可能会多次触发
    private boolean isFirstTime = true;

    // 观察布局变化,目的是获取View的尺寸,在测量之前执行onMeasure getWidth()和getHeight()可能为0
    @Override
    public void onGlobalLayout() {
        if (isFirstTime) {
            Drawable drawable = getDrawable();
            if (drawable == null) {
                return;
            }
            isFirstTime = false;
            int drawableWidth = drawable.getIntrinsicWidth();
            int drawableHeight = drawable.getIntrinsicHeight();
            viewWidth = getWidth();
            viewHeight = getHeight();
//          图片长宽超出View的可见范围的处理
            if (drawableWidth > viewWidth || drawableHeight > viewHeight) {
                initScale = Math.min(viewWidth * 1.0f / drawableWidth, viewHeight * 1.0f / drawableHeight);
            }
//          将图片偏移到中心位置
            mMatrix.postTranslate((viewWidth - drawableWidth) / 2, (viewHeight - drawableHeight) / 2);
//          初始化缩放
            mMatrix.postScale(initScale, initScale, viewWidth / 2, viewHeight / 2);
            setImageMatrix(mMatrix);
        }
    }

//  当View附加到Window上时调用,这时候它的绘画面板已存在,即将开始绘制。注意,该方法能保证发生在onDraw之前,但可能发生在onMeasure之前或之后。
//  HongYang大神写在此方法中,考虑到上述原因,这里写在onMeasure中,见仁见智。
//    @Override
//    protected void onAttachedToWindow() {
//        super.onAttachedToWindow();
//        getViewTreeObserver().addOnGlobalLayoutListener(this);
//    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        getViewTreeObserver().addOnGlobalLayoutListener(this);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            getViewTreeObserver().removeOnGlobalLayoutListener(this);
        }
    }
}

关于偏移量修正的示意图如下,以宽度方向为例,当图片宽度小于View的宽度时,无论如何,图片都会与View都某一边产生距离的,这时候要求伸出View的图片部分缩回来;当图片宽度大于View的宽度时,如果与View边缘产生距离,要求将该空白填充满。总之一个原则,充分利用View的区域,尽可能显示更多的图片内容。

这里写图片描述

这时候控件中还没有加入边界回弹功能,要添加边界回弹功能,请看下一篇:自定义ImageView实现图片的拖动、缩放和边界回弹

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
当然,我将为您提供一个更详细的代码示例来实现加载图片、拖拽、缩放和涂鸦功能的自定义控件。以下是一个完整的示例: ```java import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.util.AttributeSet; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.View; public class CustomImageView extends View { private Bitmap imageBitmap; private float imageX, imageY; private float scaleFactor = 1.0f; private Path drawingPath; private Paint drawingPaint; private float lastTouchX, lastTouchY; private GestureDetector gestureDetector; private ScaleGestureDetector scaleGestureDetector; public CustomImageView(Context context) { super(context); init(); } public CustomImageView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public CustomImageView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { // 初始化画笔和绘制路径 drawingPaint = new Paint(); drawingPaint.setAntiAlias(true); drawingPaint.setStyle(Paint.Style.STROKE); drawingPaint.setStrokeWidth(5); drawingPath = new Path(); // 初始化手势检测器 gestureDetector = new GestureDetector(getContext(), new MyGestureListener()); scaleGestureDetector = new ScaleGestureDetector(getContext(), new MyScaleGestureListener()); } public void loadImage(int resId) { // 加载图片 imageBitmap = BitmapFactory.decodeResource(getResources(), resId); invalidate(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 绘制图片 if (imageBitmap != null) { canvas.save(); canvas.scale(scaleFactor, scaleFactor); canvas.drawBitmap(imageBitmap, imageX, imageY, null); canvas.restore(); } // 绘制涂鸦 canvas.drawPath(drawingPath, drawingPaint); } @Override public boolean onTouchEvent(MotionEvent event) { // 处理触摸事件 gestureDetector.onTouchEvent(event); scaleGestureDetector.onTouchEvent(event); float touchX = event.getX() / scaleFactor; float touchY = event.getY() / scaleFactor; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // 记录上一次的触摸位置 lastTouchX = touchX; lastTouchY = touchY; // 开始涂鸦 drawingPath.moveTo(touchX, touchY); break; case MotionEvent.ACTION_MOVE: // 计算涂鸦路径 float dx = Math.abs(touchX - lastTouchX); float dy = Math.abs(touchY - lastTouchY); if (dx >= 4 || dy >= 4) { drawingPath.quadTo(lastTouchX, lastTouchY, (touchX + lastTouchX) / 2, (touchY + lastTouchY) / 2); lastTouchX = touchX; lastTouchY = touchY; } break; case MotionEvent.ACTION_UP: // 结束涂鸦 drawingPath.lineTo(touchX, touchY); break; } invalidate(); return true; } private class MyGestureListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { // 实现拖拽功能的逻辑 imageX -= distanceX; imageY -= distanceY; invalidate(); return true; } } private class MyScaleGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { @Override public boolean onScale(ScaleGestureDetector detector) { // 实现缩放功能的逻辑 scaleFactor *= detector.getScaleFactor(); scaleFactor = Math.max(0.1f, Math.min(scaleFactor, 5.0f)); invalidate(); return true; } } } ``` 在这个示例中,我们创建了一个名为 `CustomImageView` 的自定义控件。我们使用 `Bitmap` 类来存储加载的图片,并使用 `Path` 和 `Paint` 类来实现涂鸦功能。在触摸事件处理中,我们通过手势检测器来实现拖拽和缩放功能。 您可以在您的项目中使用此代码作为起点,并根据您的需求进行修改和扩展。希望对您有所帮助!如果您有任何其他问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值