Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Android

วิธีเพิ่มฟีเจอร์ Zoom รูปภาพ (Pinch to Zoom) ใน Android

ภาพรวม

บทความนี้สาธิตวิธีเพิ่มฟีเจอร์ Zoom (Pinch to Zoom) สำหรับรูปภาพใน Android โดยใช้ ScaleGestureDetector ซึ่งเป็นวิธีพื้นฐาน สำหรับ Production แนะนำให้ใช้ Library เช่น PhotoView หรือ TouchImageView ที่จัดการ Edge Cases, Pan, Double-tap Zoom, และ Fling ให้ครบถ้วน

ขั้นตอนที่ 1: สร้างโปรเจกต์ใหม่

สร้างโปรเจกต์ Android ใหม่ใน Android Studio

ขั้นตอนที่ 2: Layout (res/layout/activity_main.xml)

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="https://schemas.android.com/apk/res/android"
    xmlns:tools="https://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="matrix"
        android:src="@drawable/your_image"
        android:contentDescription="Zoomable Image" />

</FrameLayout>

ขั้นตอนที่ 3: Custom ImageView พร้อม Zoom (ZoomableImageView.java)

import android.content.Context;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.widget.ImageView;

public class ZoomableImageView extends ImageView {

    private Matrix matrix = new Matrix();
    private float[] matrixValues = new float[9];
    
    private ScaleGestureDetector scaleGestureDetector;
    private float scaleFactor = 1f;
    private float minScale = 1f;
    private float maxScale = 5f;
    
    private PointF lastTouchPoint = new PointF();
    private boolean isDragging = false;
    
    private int imageWidth, imageHeight;
    private int viewWidth, viewHeight;

    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) {
        setScaleType(ScaleType.MATRIX);
        scaleGestureDetector = new ScaleGestureDetector(context, new ScaleListener());
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        viewWidth = getWidth();
        viewHeight = getHeight();
        resetZoom();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        scaleGestureDetector.onTouchEvent(event);
        
        // Handle Drag/Pan
        switch (event.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
                lastTouchPoint.set(event.getX(), event.getY());
                isDragging = false;
                break;
                
            case MotionEvent.ACTION_MOVE:
                float dx = event.getX() - lastTouchPoint.x;
                float dy = event.getY() - lastTouchPoint.y;
                
                if (!scaleGestureDetector.isInProgress()) {
                    if (Math.abs(dx) > 10 || Math.abs(dy) > 10) {
                        isDragging = true;
                        panBy(dx, dy);
                        lastTouchPoint.set(event.getX(), event.getY());
                    }
                }
                break;
                
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                isDragging = false;
                break;
        }
        return true;
    }

    private void panBy(float dx, float dy) {
        matrix.postTranslate(dx, dy);
        checkMatrixBounds();
        setImageMatrix(matrix);
    }

    private void checkMatrixBounds() {
        matrix.getValues(matrixValues);
        float transX = matrixValues[Matrix.MTRANS_X];
        float transY = matrixValues[Matrix.MTRANS_Y];
        float scaleX = matrixValues[Matrix.MSCALE_X];
        float scaleY = matrixValues[Matrix.MSCALE_Y];

        float scaledImageWidth = imageWidth * scaleX;
        float scaledImageHeight = imageHeight * scaleY;

        // จำกัดการ Pan ไม่ให้เกินขอบรูป
        if (scaledImageWidth < viewWidth) {
            transX = (viewWidth - scaledImageWidth) / 2f;
        } else {
            if (transX > 0) transX = 0;
            if (transX < viewWidth - scaledImageWidth) transX = viewWidth - scaledImageWidth;
        }

        if (scaledImageHeight < viewHeight) {
            transY = (viewHeight - scaledImageHeight) / 2f;
        } else {
            if (transY > 0) transY = 0;
            if (transY < viewHeight - scaledImageHeight) transY = viewHeight - scaledImageHeight;
        }

        matrix.setTranslate(transX, transY);
        matrix.postScale(scaleX, scaleY, viewWidth / 2f, viewHeight / 2f);
    }

    private void resetZoom() {
        if (getDrawable() == null) return;
        
        imageWidth = getDrawable().getIntrinsicWidth();
        imageHeight = getDrawable().getIntrinsicHeight();
        
        if (imageWidth == 0 || imageHeight == 0) return;

        float scaleX = (float) viewWidth / imageWidth;
        float scaleY = (float) viewHeight / imageHeight;
        float scale = Math.min(scaleX, scaleY); // Fit to screen

        minScale = scale;
        maxScale = scale * 5f; // Zoom สูงสุด 5x
        scaleFactor = scale;

        matrix.reset();
        matrix.postScale(scale, scale, viewWidth / 2f, viewHeight / 2f);
        matrix.postTranslate(
            (viewWidth - imageWidth * scale) / 2f,
            (viewHeight - imageHeight * scale) / 2f
        );
        setImageMatrix(matrix);
    }

    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            float focusX = detector.getFocusX();
            float focusY = detector.getFocusY();
            
            scaleFactor *= detector.getScaleFactor();
            scaleFactor = Math.max(minScale, Math.min(scaleFactor, maxScale));
            
            matrix.getValues(matrixValues);
            float currentScale = matrixValues[Matrix.MSCALE_X];
            float targetScale = scaleFactor / currentScale;
            
            matrix.postScale(targetScale, targetScale, focusX, focusY);
            checkMatrixBounds();
            setImageMatrix(matrix);
            return true;
        }
    }
}

ขั้นตอนที่ 4: ใช้งานใน MainActivity.java

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // ZoomableImageView ถูก inflate จาก XML แล้ว
    }
}

วิธีใช้ใน XML Layout

<com.yourpackage.ZoomableImageView
    android:id="@+id/imageView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:scaleType="matrix"
    android:src="@drawable/your_image" />

แนะนำ: ใช้ Library PhotoView (Production Ready)

build.gradle

dependencies {
    implementation "com.github.chrisbanes:PhotoView:2.3.0"
}

Layout

<com.github.chrisbanes.photoview.PhotoView
    android:id="@+id/photoView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:scaleType="centerCrop"
    android:src="@drawable/your_image" />

MainActivity

PhotoView photoView = findViewById(R.id.photoView);
photoView.setImageResource(R.drawable.your_image);
// PhotoView จัดการ Pinch Zoom, Double Tap, Fling, Pan ให้ครบเอง

เปรียบเทียบวิธีต่างๆ

วิธีความซับซ้อนฟีเจอร์เหมาะสำหรับ
ScaleGestureDetector (Manual)สูงBasic Zoom/Panเรียนรู้, ใช้งานง่าย
PhotoView Libraryต่ำZoom, Pan, Double-tap, Fling, AnimateProduction App
TouchImageView (Single File)ปานกลางZoom, Pan, Double-tapไม่อยากเพิ่ม Dependency
Coil/Glide + Zoomableต่ำLoad + Zoom รวมModern App (Kotlin)

ข้อควรระวัง

  • ScaleType.MATRIX: จำเป็นต้องตั้งค่านี้ เพื่อให้ Matrix ทำงาน
  • Large Images: ใช้ inSampleSize หรือ Library (Glide/Coil) โหลดรูปขนาดเหมาะสม
  • Memory: รูปขนาดใหญ่ + Zoom อาจทำให้ OOM - ใช้ android:largeHeap="true" ใน Manifest ถ้าจำเป็น
  • Parent Scrolling: ถ้าอยู่ใน ScrollView/NestedScrollView ต้องจัดการ Touch Event ให้ไม่ conflito