安卓原生开发实现图片双指放大预览功能

1.封装组件ZoomableImageView.java

复制代码
package org.yan.app.components;

import android.content.Context;
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.ViewTreeObserver;

import androidx.appcompat.widget.AppCompatImageView;

/**
 * 支持双指缩放、单指平移、双击缩放的ImageView
 * 用于文件预览对话框中的图片预览
 */
public class ZoomableImageView extends AppCompatImageView implements
        ScaleGestureDetector.OnScaleGestureListener,
        GestureDetector.OnGestureListener,
        GestureDetector.OnDoubleTapListener,
        View.OnTouchListener {

    // 缩放范围限制
    private static final float MIN_SCALE = 0.5f;
    private static final float MAX_SCALE = 5.0f;
    private static final float MID_SCALE = 2.0f;

    // 手势检测器
    private ScaleGestureDetector mScaleDetector;
    private GestureDetector mGestureDetector;

    // 变换矩阵
    private Matrix mMatrix = new Matrix();
    private float[] mMatrixValues = new float[9];

    // 触摸状态
    private int mMode = MODE_NONE;
    private static final int MODE_NONE = 0;
    private static final int MODE_DRAG = 1;
    private static final int MODE_ZOOM = 2;

    // 拖动相关
    private PointF mLastTouch = new PointF();
    private PointF mStartTouch = new PointF();

    // 初始缩放比例(适应屏幕)
    private float mInitScale = 1f;
    private boolean mIsInitialized = false;

    public ZoomableImageView(Context context) {
        super(context);
        init(context);
    }

    public ZoomableImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public ZoomableImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }


    private void init(Context context) {
        mScaleDetector = new ScaleGestureDetector(context, this);
        mGestureDetector = new GestureDetector(context, this);
        mGestureDetector.setOnDoubleTapListener(this);

        setScaleType(ScaleType.MATRIX);
        setOnTouchListener(this);

        // 监听布局完成后初始化矩阵
        getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                if (!mIsInitialized) {
                    initMatrix();
                    mIsInitialized = true;
                }
            }
        });
    }

    /**
     * 初始化矩阵,使图片居中并适应屏幕
     */
    private void initMatrix() {
        Drawable drawable = getDrawable();
        if (drawable == null) return;

        int viewWidth = getWidth();
        int viewHeight = getHeight();
        int drawableWidth = drawable.getIntrinsicWidth();
        int drawableHeight = drawable.getIntrinsicHeight();

        if (viewWidth == 0 || viewHeight == 0 || drawableWidth == 0 || drawableHeight == 0) {
            return;
        }

        // 计算适应屏幕的缩放比例
        float scaleX = (float) viewWidth / drawableWidth;
        float scaleY = (float) viewHeight / drawableHeight;
        mInitScale = Math.min(scaleX, scaleY);

        // 计算居中偏移
        float dx = (viewWidth - drawableWidth * mInitScale) / 2f;
        float dy = (viewHeight - drawableHeight * mInitScale) / 2f;

        mMatrix.reset();
        mMatrix.postScale(mInitScale, mInitScale);
        mMatrix.postTranslate(dx, dy);
        setImageMatrix(mMatrix);
    }

    @Override
    public void setImageDrawable(Drawable drawable) {
        super.setImageDrawable(drawable);
        mIsInitialized = false;
        if (getWidth() > 0 && getHeight() > 0) {
            initMatrix();
            mIsInitialized = true;
        }
    }

    /**
     * 获取当前缩放比例
     */
    private float getCurrentScale() {
        mMatrix.getValues(mMatrixValues);
        return mMatrixValues[Matrix.MSCALE_X];
    }

    /**
     * 获取图片当前显示的矩形区域
     */
    private RectF getImageRect() {
        Drawable drawable = getDrawable();
        if (drawable == null) return new RectF();

        RectF rect = new RectF(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
        mMatrix.mapRect(rect);
        return rect;
    }

    // ==================== OnTouchListener ====================

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        // 先让手势检测器处理
        mScaleDetector.onTouchEvent(event);
        mGestureDetector.onTouchEvent(event);

        switch (event.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
                mLastTouch.set(event.getX(), event.getY());
                mStartTouch.set(event.getX(), event.getY());
                mMode = MODE_DRAG;
                break;

            case MotionEvent.ACTION_POINTER_DOWN:
                mMode = MODE_ZOOM;
                break;

            case MotionEvent.ACTION_MOVE:
                if (mMode == MODE_DRAG && !mScaleDetector.isInProgress()) {
                    float dx = event.getX() - mLastTouch.x;
                    float dy = event.getY() - mLastTouch.y;

                    // 只有缩放后才允许平移
                    float currentScale = getCurrentScale();
                    if (currentScale > mInitScale) {
                        // 检查边界
                        RectF rect = getImageRect();
                        float[] delta = checkBounds(dx, dy, rect);
                        mMatrix.postTranslate(delta[0], delta[1]);
                        setImageMatrix(mMatrix);
                    }

                    mLastTouch.set(event.getX(), event.getY());
                }
                break;

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_POINTER_UP:
                mMode = MODE_NONE;
                break;
        }

        return true;
    }


    /**
     * 检查平移边界,防止图片移出可视区域
     */
    private float[] checkBounds(float dx, float dy, RectF rect) {
        int viewWidth = getWidth();
        int viewHeight = getHeight();

        // 水平方向边界检查
        if (rect.width() <= viewWidth) {
            dx = 0;
        } else {
            if (rect.left + dx > 0) {
                dx = -rect.left;
            }
            if (rect.right + dx < viewWidth) {
                dx = viewWidth - rect.right;
            }
        }

        // 垂直方向边界检查
        if (rect.height() <= viewHeight) {
            dy = 0;
        } else {
            if (rect.top + dy > 0) {
                dy = -rect.top;
            }
            if (rect.bottom + dy < viewHeight) {
                dy = viewHeight - rect.bottom;
            }
        }

        return new float[]{dx, dy};
    }

    // ==================== ScaleGestureDetector.OnScaleGestureListener ====================

    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        float scaleFactor = detector.getScaleFactor();
        float currentScale = getCurrentScale();
        float targetScale = currentScale * scaleFactor;

        // 限制缩放范围
        if (targetScale < MIN_SCALE) {
            scaleFactor = MIN_SCALE / currentScale;
        } else if (targetScale > MAX_SCALE) {
            scaleFactor = MAX_SCALE / currentScale;
        }

        // 以手指中心点为缩放中心
        mMatrix.postScale(scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusY());

        // 检查并修正边界
        checkAndFixBounds();

        setImageMatrix(mMatrix);
        return true;
    }

    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {
        mMode = MODE_ZOOM;
        return true;
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {
        // 如果缩放比例小于初始比例,恢复到初始状态
        float currentScale = getCurrentScale();
        if (currentScale < mInitScale) {
            resetToInitState();
        }
    }

    /**
     * 检查并修正边界
     */
    private void checkAndFixBounds() {
        RectF rect = getImageRect();
        int viewWidth = getWidth();
        int viewHeight = getHeight();

        float dx = 0, dy = 0;

        // 水平方向
        if (rect.width() <= viewWidth) {
            dx = (viewWidth - rect.width()) / 2f - rect.left;
        } else {
            if (rect.left > 0) {
                dx = -rect.left;
            }
            if (rect.right < viewWidth) {
                dx = viewWidth - rect.right;
            }
        }

        // 垂直方向
        if (rect.height() <= viewHeight) {
            dy = (viewHeight - rect.height()) / 2f - rect.top;
        } else {
            if (rect.top > 0) {
                dy = -rect.top;
            }
            if (rect.bottom < viewHeight) {
                dy = viewHeight - rect.bottom;
            }
        }

        mMatrix.postTranslate(dx, dy);
    }

    /**
     * 恢复到初始状态
     */
    private void resetToInitState() {
        mMatrix.reset();
        initMatrix();
    }

    // ==================== GestureDetector.OnDoubleTapListener ====================

    @Override
    public boolean onSingleTapConfirmed(MotionEvent e) {
        return false;
    }

    @Override
    public boolean onDoubleTap(MotionEvent e) {
        float currentScale = getCurrentScale();
        float targetScale;

        // 双击切换缩放状态
        if (currentScale < MID_SCALE) {
            targetScale = MID_SCALE;
        } else {
            targetScale = mInitScale;
        }

        // 以点击位置为中心缩放
        float scaleFactor = targetScale / currentScale;
        mMatrix.postScale(scaleFactor, scaleFactor, e.getX(), e.getY());

        checkAndFixBounds();
        setImageMatrix(mMatrix);

        return true;
    }

    @Override
    public boolean onDoubleTapEvent(MotionEvent e) {
        return false;
    }

    // ==================== GestureDetector.OnGestureListener ====================

    @Override
    public boolean onDown(MotionEvent e) {
        return true;
    }

    @Override
    public void onShowPress(MotionEvent e) {
    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        return false;
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        return false;
    }

    @Override
    public void onLongPress(MotionEvent e) {
    }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        return false;
    }

    /**
     * 重置图片到初始状态
     */
    public void reset() {
        mIsInitialized = false;
        initMatrix();
        mIsInitialized = true;
    }
}

2.将普通的.xml的ImageView 替换为 ZoomableImageView

复制代码
  <FrameLayout
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:background="#1A1A1A">

            <org.yan.app.components.ZoomableImageView
                android:id="@+id/iv_preview"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />

            <ProgressBar
                android:id="@+id/progress_loading"
                android:layout_width="48dp"
                android:layout_height="48dp"
                android:layout_gravity="center"
                android:visibility="gone" />
        </FrameLayout>

3.在.java文件中引用

复制代码
import org.yan.app.components.ZoomableImageView;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

 private ZoomableImageView ivPreview;
 private void initViews() {

   ivPreview = findViewById(R.id.iv_preview);
}


    /**
     * 加载图片(支持本地和网络)
     */
    private void loadImage() {
        progressLoading.setVisibility(View.VISIBLE);

        new Thread(() -> {
            try {
                Bitmap bitmap = null;

                // 优先从本地加载
                if (fileItem.isLocal() && fileItem.getLocalPath() != null) {
                    bitmap = BitmapFactory.decodeFile(fileItem.getLocalPath());
                }

                // 本地没有则从网络加载
                if (bitmap == null && fileItem.getRemoteUrl() != null) {
                    String imageUrl = fileItem.getRemoteUrl();
                    if (!imageUrl.startsWith("http")) {
                        imageUrl = MINIO_URL + imageUrl;
                    }
                    Log.d(TAG, "加载图片: " + imageUrl);

                    URL url = new URL(imageUrl);
                    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                    conn.setConnectTimeout(10000);
                    conn.setReadTimeout(15000);
                    conn.setDoInput(true);
                    conn.connect();

                    InputStream is = conn.getInputStream();
                    bitmap = BitmapFactory.decodeStream(is);
                    is.close();
                    conn.disconnect();
                }

                final Bitmap finalBitmap = bitmap;
                ivPreview.post(() -> {
                    progressLoading.setVisibility(View.GONE);
                    if (finalBitmap != null) {
                        ivPreview.setImageBitmap(finalBitmap);
                    } else {
                        ivPreview.setImageResource(R.mipmap.img_log);
                    }
                });
            } catch (Exception e) {
                Log.e(TAG, "加载图片失败", e);
                ivPreview.post(() -> {
                    progressLoading.setVisibility(View.GONE);
                    ivPreview.setImageResource(R.mipmap.img_log);
                });
            }
        }).start();
    }
相关推荐
2501_915106323 小时前
如何在iPad上高效管理本地文件的完整指南
android·ios·小程序·uni-app·iphone·webview·ipad
似霰3 小时前
AIDL Hal 开发笔记5----实现AIDL HAL
android·framework·hal
2501_915106324 小时前
iOS 成品包加固,在只有 IPA 的情况下,能做那些操作
android·ios·小程序·https·uni-app·iphone·webview
LcVong5 小时前
Android 25(API 25)+ JDK 17 环境搭建
android·java·开发语言
AALoveTouch5 小时前
某麦APP抢票技术解析实现
android·ios
stevenzqzq6 小时前
Android 自定义View迁移Compose实战指南
android·compose
似霰8 小时前
AIDL Hal 开发笔记6----添加硬件访问服务
android·framework·hal
诸神黄昏EX8 小时前
Android OTA 之 升级包编译机制
android
2501_915909069 小时前
苹果iOS应用上架详细流程与注意事项解析
android·ios·小程序·https·uni-app·iphone·webview