1. ホーム
  2. アンドロイド

Androidのマルチタッチ技術が活躍、画像を自在にズーム&ムーブ

2022-03-16 16:41:02

前回は、Androidの滝のフォトウォール効果をご紹介しました。とてもクールな効果ですが、フォトウォールの写真はすべて見ることはできてもクリックすることはできないので、まだ半完成品に過ぎないのです。そこで今回は、この機能を改善し、画像をクリックして大きく表示したり、マルチタッチで拡大・縮小したりする機能を追加します。

まだご覧になっていない方は  Android滝のフォトウォール実装、不規則な配置の美しさを体験してください  今回は前回から完全に発展させたコードになっていますので、この記事を読む前にまず目を通してみてください。

では、さっそく始めてみましょう。まず、前回の PhotoWallFallsDemo プロジェクトを開き、以下のコードのように、大きな画像表示とマルチタッチによるズームに使用する ZoomImageView クラスを追加します。

public class ZoomImageView extends View {

	/*
	 * Initialize state constants
	 */
	public static final int STATUS_INIT = 1;

	/**
	 * Image zoom status constant
	 */
	public static final int STATUS_ZOOM_OUT = 2;

	/**
	 * Image zoom out state constant
	 */
	public static final int STATUS_ZOOM_IN = 3;

	/**
	 * Image dragging status constants
	 */
	public static final int STATUS_MOVE = 4;

	/**
	 * Matrix for moving and scaling transformations of the image
	 */
	private Matrix matrix = new Matrix();

	/**
	 * The Bitmap object to be displayed
	 */
	private Bitmap sourceBitmap;

	/**
	 * Record the status of the current operation, the optional values are STATUS_INIT, STATUS_ZOOM_OUT, STATUS_ZOOM_IN and STATUS_MOVE
	 */
	private int currentStatus;

	/**
	 * The width of the ZoomImageView control
	 */
	private int width;

	/**
	 * The height of the ZoomImageView control
	 */
	private int height;

	/**
	 * Record the value of the horizontal coordinate of the center point when two fingers are placed on the screen at the same time
	 */
	private float centerPointX;

	/**
	 * Record the vertical coordinate of the center point when two fingers are placed on the screen at the same time
	 */
	private float centerPointY;

	/**
	 * Record the width of the current image, this value will change together when the image is scaled
	 */
	private float currentBitmapWidth;

	/**
	 * Record the height of the current image, this value will be changed when the image is scaled.
	 */
	private float currentBitmapHeight;

	/**
	 * Record the horizontal coordinate of the last finger movement
	 */
	private float lastXMove = -1;

	/**
	 * Record the vertical coordinate of the last finger movement
	 */
	private float lastYMove = -1;

	/**
	 * Record the distance the finger moved in the direction of the horizontal coordinate
	 */
	private float movedDistanceX;

	/**
	 * Record the distance of the finger in the vertical direction
	 */
	private float movedDistanceY;

	/**
	 * Record the horizontal offset value of the image on the matrix
	 */
	private float totalTranslateX;

	/**
	 * Record the vertical offset of the image on the matrix
	 */
	private float totalTranslateY;

	/**
	 * Record the total scaling of the image on the matrix
	 */
	private float totalRatio;

	/**
	 * Record the scaling caused by the distance the finger moves
	 */
	private float scaledRatio;

	/**
	 * Record the scaled ratio when the image is initialized
	 */
	private float initRatio;

	/**
	 * Record the distance between the last two fingers
	 */
	private double lastFingerDis;

	/**
	 * ZoomImageView constructor, set the current operation state to STATUS_INIT.
	 * 
	 * @param context
	 * @param attrs
	 */
	public ZoomImageView(Context context, AttributeSet attrs) {
		super(context, attrs);
		currentStatus = STATUS_INIT;
	}

	/**
	 * Set the image to be displayed in.
	 * 
	 * @param bitmap
	 * The Bitmap object to be displayed.
	 */
	public void setImageBitmap(Bitmap bitmap) {
		sourceBitmap = bitmap;
		invalidate();
	}

	@Override
	protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
		super.onLayout(changed, left, top, right, bottom);
		if (changed) {
			// Get the width and height of the ZoomImageView, respectively
			width = getWidth();
			height = getHeight();
		}
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		switch (event.getActionMasked()) {
		case MotionEvent.ACTION_POINTER_DOWN:
			if (event.getPointerCount() == 2) {
				// When two fingers are pressed on the screen, calculate the distance between the two fingers
				lastFingerDis = distanceBetweenFingers(event);
			}
			break;
		case MotionEvent.ACTION_MOVE:
			if (event.getPointerCount() == 1) {
				// Drag state when only one finger is pressed on the screen to move
				float xMove = event.getX();
				float yMove = event.getY();
				if (lastXMove == -1 && lastYMove == -1) {
					lastXMove = xMove;
					lastYMove = yMove;
				}
				currentStatus = STATUS_MOVE;
				movedDistanceX = xMove - lastXMove;
				movedDistanceY = yMove - lastYMove;
				// Boundary check, do not allow to drag the image out of the boundary
				if (totalTranslateX + movedDistanceX > 0) {
					movedDistanceX = 0;
				} else if (width - (totalTranslateX + movedDistanceX) > currentBitmapWidth) {
					movedDistanceX = 0;
				}
				if (totalTranslateY + movedDistanceY > 0) {
					movedDistanceY = 0;
				} else if (height - (totalTransla
				// Call the onDraw() method to draw the image
				invalidate();
				lastXMove = xMove;
				lastYMove = yMove;
			} else if (event.getPointerCount() == 2) {
				// Zoom state when two fingers are pressed on the screen to move
				centerPointBetweenFingers(event);
				double fingerDis = distanceBetweenFingers(event);
				if (fingerDis > lastFingerDis) {
					currentStatus = STATUS_ZOOM_OUT;
				} else {
					currentStatus = STATUS_ZOOM_IN;
				}
				// Perform a zoom check, allowing only a maximum of 4x enlargement of the image and a minimum reduction to the initialized scale
				if ((currentStatus == STATUS_ZOOM_OUT && totalRatio < 4 * initRatio)
						|| (currentStatus == STATUS_ZOOM_IN && totalRatio > initRatio)) {
					scaledRatio = (float) (fingerDis / lastFingerDis);
					totalRatio = totalRatio * scaledRatio;
					if (totalRatio > 4 * initRatio) {
						totalRatio = 4 * initRatio;
					} else if (totalRatio < initRatio) {
						totalRatio = initRatio;
					}
					// Call the onDraw() method to draw the image
					invalidate();
					lastFingerDis = fingerDis;
				}
			}
			break;
		case MotionEvent.ACTION_POINTER_UP:
			if (event.getPointerCount() == 2) {
				// restore the temporary value when the finger leaves the screen
				lastXMove = -1;
				lastYMove = -1;
			}
			break;
		case MotionEvent.ACTION_UP:
			// restore the temporary value when the finger leaves the screen
			lastXMove = -1;
			lastYMove = -1;
			break;
		default:
			break;
		}
		return true;
	}

	/**
	 * Determines what kind of drawing operation is applied to the image based on the value of currentStatus.
	 */
	@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);
		switch (currentStatus) {
		case STATUS_ZOOM_OUT:
		case STATUS_ZOOM_IN:
			zoom(canvas);
			break;
		case STATUS_MOVE:
			move(canvas);
			break;
		case STATUS_INIT:
			initBitmap(canvas);
		default:
			canvas.drawBitmap(sourceBitmap, matrix, null);
			break;
		}
	}

	/**
	 * Scale the image.
	 * 
	 * @param canvas
	 */
	private void zoom(Canvas canvas) {
		matrix.reset();
		// Scale the image to the total scale
		matrix.postScale(totalRatio, totalRatio);
		float scaledWidth = sourceBitmap.getWidth() * totalRatio;
		float scaledHeight = sourceBitmap.getHeight() * totalRatio;
		float translateX = 0f;
		float translateY = 0f;
		// If the current image width is smaller than the screen width, then it is scaled horizontally by the horizontal coordinate of the center of the screen. Otherwise, scale horizontally by the horizontal coordinate of the center of the two fingers
		if (currentBitmapWidth < width) {
			translateX = (width - scaledWidth) / 2f;
		} else {
			translateX = totalTranslateX * scaledRatio + centerPointX * (1 - scaledRatio);
			// Boundary check to ensure that the image is not horizontally shifted off the screen after scaling
			if (translateX > 0) {
				translateX = 0;
			} else if (width - translateX > scaledWidth) {
				translateX = width - scaledWidth;
			}
		}
		// If the current image height is smaller than the screen height, then scale vertically by the vertical coordinate of the center of the screen. Otherwise, the vertical scaling is based on the vertical coordinate of the center of the two fingers
		if (currentBitmapHeight < height) {
			translateY = (height - scaledHeight) / 2f;
		} else {
			translateY = totalTranslateY * scaledRatio + centerPointY * (1 - scaledRatio);
			// Boundary check to ensure that the image is not shifted off the screen vertically after scaling
			if (translateY > 0) {
				translateY = 0;
			} else if (height - translateY > scaledHeight) {
				translateY = height - scaledHeight;
			}
		}
		// offset the image after scaling to ensure that the center point remains the same after scaling
		matrix.postTranslate(translateX, translateY);
		totalTranslateX = translateX;
		totalTranslateY = translateY;
		currentBitmapWidth = scaledWidth;
		currentBitmapHeight = scaledHeight;
		canvas.drawBitmap(sourceBitmap, matrix, null);
	}

	/**
	 * Panning of the image
	 * 
	 * @param canvas
	 */
	private void move(Canvas canvas) {
		matrix.reset();
		// Calculate the total offset based on the distance the finger has moved
		float translateX = totalTranslateX + movedDistanceX;
		float translateY = totalTranslateY + movedDistanceY;
		// first scale the image according to the existing scale
		matrix.postScale(totalRatio, totalRatio);
		// then offset according to the moved distance
		matrix.postTranslate(translateX, translateY);
		totalTranslateX = translateX;
		totalTranslateY = translateY;
		canvas.drawBitmap(sourceBitmap, matrix, null);
	}

	/**
	 * Perform initialization operations on the image, including

このクラスは、マルチタッチ・ズーム機能全体の核となるクラスなので、ここで詳しく説明することにする。まずZoomImageViewでは、STATUS_INIT、STATUS_ZOOM_OUT、STATUS_ZOOM_IN、STATUS_MOVEの4つの状態を定義し、それぞれ初期化、ズームイン、ズームアウト、移動の動作を表しています。 コンストラクタで状態を定義します。次に、setImageBitmap() メソッドを呼び出して表示する画像オブジェクトを渡せば、このメソッドは現在のViewを無効にするので、onDraw() メソッドが実行されることになります。そして、onDraw()メソッドは、現在の状態が初期化されていると判断し、initBitmap()メソッドを呼び出して初期化します。

では、initBitmap()メソッドを見てみましょう。まず、画像のサイズを決定し、画像の幅と高さがともに画面の幅と高さより小さければ、画像を画面の中央に配置できるように直接オフセットします。画像の幅が画面の幅より大きい場合、または画像の高さが画面の高さより大きい場合は、画像の幅または高さが画面の幅または高さに正確に等しくなるように、初期化状態で画像が完全に表示できるように、画像を等比級数圧縮する。ここでは、オフセットとスケーリングの操作はすべてマトリクスを介して行われます。スケーリングとオフセットされる値をマトリクスに格納し、画像を描画するときにマトリクスオブジェクトを渡します。

画像の初期化が完了したら、次は画像の拡大縮小です。ここでクリックイベントを判断するonTouchEvent()メソッドでは、同時に画面上に押された2本の指を見つけた場合(判断するためにevent.getPointerCount()を使用)、現在の状態をズームに設定し、スケーリングを計算するために2本の指の間の距離を取得するdistanceBetweenFingers()を呼び出します。そしてそれを無効にし、onDraw()メソッドでzoom()メソッドが呼び出されます。その後、このメソッドで現在のズーム比と中心点の位置に応じて、画像の拡大縮小とオフセットが行われます。

そして、画面上で指が1本だけ押されたときに、現在の状態を移動に設定し、その後、指の距離を計算し、画像が画面外にずれないように境界チェックを処理する。その後、現在のビューを無効にしてonDraw()メソッドに進み、現在の状態がmovingであると判断してmove()メソッドを呼び出します。move()は非常にシンプルで、指の移動距離に基づいて画像をオフセットさせるだけです。

ZoomImageViewを導入した後、新しいレイアウトimage_details.xmlを作成し、レイアウト内で作成したZoomImageViewを直接参照するようにします。

<?xml version="1.0" encoding="utf-8"? >
<com.example.photowallfallsdemo.ZoomImageView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/zoom_image_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#000000" >

</com.example.photowallfallsdemo.ZoomImageView>

次に、image_detailsレイアウトを読み込むためのActivityを作成します。以下のコードで、新しいImageDetailsActivityを作成します。

public class ImageDetailsActivity extends Activity {

	private ZoomImageView zoomImageView;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		requestWindowFeature(Window.FEATURE_NO_TITLE);
		setContentView(R.layout.image_details);
		zoomImageView = (ZoomImageView) findViewById(R.id.zoom_image_view);
		String imagePath = getIntent().getStringExtra("image_path");
		Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
		zoomImageView.setImageBitmap(bitmap);
	}
	
}

このように、まずZoomImageViewのインスタンスを取得し、Intentで表示したい画像のパスを取得し、BitmapFactoryでパス下の画像をメモリにロードし、ZoomImageViewのsetImageBitmap()メソッドを呼んで画像を渡せば、この画像を表示させることができるようになるのです。

次に考えるべきことは、フォトウォールの画像にクリックイベントを追加して、ImageDetailsActivityを起動できるようにする方法です。これは非常に簡単で、画像を動的に追加する際に、ImageViewの各インスタンスに対してクリックイベントを登録するだけです。MyScrollViewのaddImage()メソッドのコードを以下のように修正します。

private void addImage(Bitmap bitmap, int imageWidth, int imageHeight) {
	LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(imageWidth,
			LayoutParams(imageWidth, imageHeight);
	if (mImageView ! = null) {
		mImageView.setImageBitmap(bitmap);
	} else {
		ImageView imageView = new ImageView(getContext());
		imageView.setLayoutParams(params);
		imageView.setImageBitmap(bitmap);
		imageView.setScaleType(ScaleType.FIT_XY);
		imageView.setPadding(5, 5, 5, 5);
		imageView.setTag(R.string.image_url, mImageUrl);
		imageView.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				Intent intent = new Intent(getContext(), ImageDetailsActivity.class);
				intent.putExtra("image_path", getImagePath(mImageUrl));
				getContext().startActivity(intent);
			}
		});
		findColumnToAdd(imageView, imageHeight).addView(imageView);
		imageViewList.add(imageView);
	}
}

ご覧のように、ここでは ImageView の setOnClickListener() メソッドを呼び出して画像にクリックイベントを追加し、ユーザーがフォトウォールの任意の画像をクリックすると ImageDetailsActivity を起動して画像へのパスを渡しています。

新しいActivityを追加したら、AndroidManifest.xmlファイルに登録することを忘れないでください。

<?xml version="1.0" encoding="utf-8"? >
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.photowallfallsdemo"
    android:versionCode="1"
    android:versionName="1.0" >

    <use-sdk
        android:minSdkVersion="14"
        android:targetSdkVersion="17" />

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.example.photowallfallsdemo.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name="com.example.photowallfallsdemo.ImageDetailsActivity" >
        </activity>
    </application>

</manifest>


さて、プログラムを実行すると、再び見慣れたフォトウォールのインターフェイスが表示されます。画像をクリックすると、対応する拡大画像が表示され、以下のようにマルチタッチで拡大・縮小したり、拡大後に指一本で画像を移動させたりすることができます。