ภาพรวม
บทความนี้สาธิตวิธีเพิ่มฟีเจอร์ 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, Animate | Production 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