1 package net.osdn.gokigen.gr2control.playback.detail;
3 import android.content.Context;
4 import android.graphics.Bitmap;
5 import android.graphics.Matrix;
6 import android.graphics.drawable.Drawable;
7 import android.net.Uri;
8 import android.util.AttributeSet;
9 import android.view.GestureDetector;
10 import android.view.MotionEvent;
11 import android.view.View;
13 import androidx.appcompat.widget.AppCompatImageView;
19 public class ScalableImageView extends AppCompatImageView
22 private enum GestureMode
29 private Context mContext;
30 private GestureDetector mDoubleTapDetector;
31 private GestureMode mGestureMode;
33 /** The affine transformation matrix. */
34 private Matrix mMatrix;
36 /** The horizontal moving factor after scaling. */
38 /** The vertical moving factor after scaling. */
40 /** The X-coordinate origin for calculating the amount of movement. */
41 private float mMovingBaseX;
42 /** The Y-coordinate origin for calculating the amount of movement. */
43 private float mMovingBaseY;
45 /** The scaling factor. */
47 /** The minimum value of scaling factor. */
48 private float mScaleMin;
49 /** The maximum value of scaling factor. */
50 private float mScaleMax;
51 /** The distance from the center for determining the amount of scaling. */
52 private float mScalingBaseDistance;
53 /** The center X-coordinate to determine the amount of scaling. */
54 private float mScalingCenterX;
55 /** The center Y-coordinate to determine the amount of scaling. */
56 private float mScalingCenterY;
58 /** The width of the view. */
59 private int mViewWidth;
60 /** The height of the view. */
61 private int mViewHeight;
62 /** The width of the image. */
63 private int mImageWidth;
64 /** The height of the image. */
65 private int mImageHeight;
69 public void setImageDrawable(Drawable drawable)
71 super.setImageDrawable(drawable);
76 public void setImageBitmap(Bitmap bm)
78 super.setImageBitmap(bm);
83 public void setImageURI(Uri uri)
85 super.setImageURI(uri);
91 * Constructs a new CapturedImageView.
94 public ScalableImageView(Context context)
102 * Constructs a new CapturedImageView.
105 public ScalableImageView(Context context, AttributeSet attrs)
107 super(context, attrs);
113 * Constructs a new CapturedImageView.
116 public ScalableImageView(Context context, AttributeSet attrs, int defStyle)
118 super(context, attrs, defStyle);
124 * Initializes this instance.
126 private void init() {
127 this.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
128 this.setScaleType(ScaleType.MATRIX);
129 mMatrix = new Matrix();
134 mGestureMode = GestureMode.None;
141 // Setups touch gesture.
142 mDoubleTapDetector = new GestureDetector(mContext, new GestureDetector.SimpleOnGestureListener() {
144 public boolean onDoubleTap(MotionEvent e) {
145 if (mScale != 1.0f) {
146 // Zooms at tapped point.
147 updateScaleWithBasePoint(1.0f, e.getX(), e.getY());
159 * Resets current scaling.
161 private void reset() {
162 Drawable drawable = this.getDrawable();
163 if (drawable != null) {
164 mImageWidth = drawable.getIntrinsicWidth();
165 mImageHeight = drawable.getIntrinsicHeight();
172 protected boolean setFrame(int l, int t, int r, int b) {
175 if (this.getDrawable() != null) {
179 return super.setFrame(l, t, r, b);
183 * Returns a scaled X offset.
185 * @param scale A scaling factor.
186 * @param moveX A horizontal moving factor.
187 * @return A scaled X offset.
189 private float computeOffsetX(float scale, float moveX) {
190 // Offsets in order to center the image.
191 float scaledWidth = scale * mImageWidth;
192 float offsetX = (mViewWidth - scaledWidth) / 2;
193 // Moves specified offset.
199 * Returns a scaled Y offset.
201 * @param scale A scaling factor.
202 * @param moveY A vertical moving factor.
203 * @return A scaled Y offset.
205 private float computeOffsetY(float scale, float moveY) {
206 // Offsets in order to center the image.
207 float scaledHeight = scale * mImageHeight;
208 float offsetY = (mViewHeight - scaledHeight) / 2;
209 // Moves specified offset.
215 * Updates affine transformation matrix to display the image.
217 private void updateMatrix() {
218 // Creates new matrix.
220 mMatrix.postScale(mScale, mScale);
221 mMatrix.postTranslate(computeOffsetX(mScale, mMoveX), computeOffsetY(mScale, mMoveY));
223 // Updates the matrix.
224 this.setImageMatrix(mMatrix);
229 * Calculates zoom scale. (for the image size to fit screen size).
231 private void fitScreen() {
232 if ((mImageWidth == 0) || (mImageHeight == 0) || (mViewWidth == 0) || (mViewHeight == 0)) {
236 // Clears the moving factors.
239 // Gets scaling ratio.
240 float scaleX = (float)mViewWidth / mImageWidth;
241 float scaleY = (float)mViewHeight / mImageHeight;
243 // Updates the scaling factor so that the image will not be larger than the screen size.
244 mScale = Math.min(scaleX, scaleY);
246 // 4 times of original image size or 4 times of the screen size.
247 mScaleMax = Math.max(4.f, mScale * 4);
251 * Updates the moving factors.
253 * @param moveX A horizontal moving factor.
254 * @param moveY A vertical moving factor.
256 protected void updateMove(float moveX, float moveY) {
261 float scaledWidth = mImageWidth * mScale;
262 float scaledHeight = mImageHeight * mScale;
264 // Clips the moving factors.
265 if (scaledWidth <= mViewWidth) {
268 float minMoveX = -(scaledWidth - mViewWidth) / 2;
269 float maxMoveX = +(scaledWidth - mViewWidth) / 2;
270 mMoveX = Math.min(Math.max(mMoveX, minMoveX), maxMoveX);
272 if (scaledHeight <= mViewHeight) {
275 float minMoveY = -(scaledHeight - mViewHeight) / 2;
276 float maxMoveY = +(scaledHeight - mViewHeight) / 2;
277 mMoveY = Math.min(Math.max(mMoveY, minMoveY), maxMoveY);
282 * Updates the scaling factor. The specified point doesn't change in appearance.
284 * @param newScale The new scaling factor.
285 * @param baseX The center position of scaling.
286 * @param baseY The center position of scaling.
288 protected void updateScaleWithBasePoint(float newScale, float baseX, float baseY) {
289 float lastScale = mScale;
290 float lastOffsetX = computeOffsetX(mScale, mMoveX);
291 float lastOffsetY = computeOffsetY(mScale, mMoveY);
293 // Updates the scale with clipping.
294 mScale = Math.min(Math.max(newScale, mScaleMin), mScaleMax);
295 mScalingCenterX = baseX;
296 mScalingCenterY = baseY;
298 // Gets scaling base point on the image world.
299 float scalingCenterXOnImage = (mScalingCenterX - lastOffsetX) / lastScale;
300 float scalingCenterYOnImage = (mScalingCenterY - lastOffsetY) / lastScale;
301 // Gets scaling base point on the scaled image world.
302 float scalingCenterXOnScaledImage = scalingCenterXOnImage * mScale;
303 float scalingCenterYOnScaledImage = scalingCenterYOnImage * mScale;
304 // Gets scaling base point on the view world.
305 float scalingCenterXOnView = computeOffsetX(mScale, 0) + scalingCenterXOnScaledImage;
306 float scalingCenterYOnView = computeOffsetY(mScale, 0) + scalingCenterYOnScaledImage;
309 updateMove(mScalingCenterX - scalingCenterXOnView, mScalingCenterY - scalingCenterYOnView);
313 public boolean onTouchEvent(MotionEvent event)
315 if (mDoubleTapDetector.onTouchEvent(event))
320 int action = event.getAction() & MotionEvent.ACTION_MASK;
321 int touchCount = event.getPointerCount();
324 case MotionEvent.ACTION_DOWN:
325 if (mScale > mScaleMin)
327 // Starts to move the image and takes in the start point.
328 mGestureMode = GestureMode.Move;
329 mMovingBaseX = event.getX();
330 mMovingBaseY = event.getY();
334 case MotionEvent.ACTION_POINTER_DOWN:
335 if (touchCount >= 2) {
336 // Starts zooming and takes in the center point.
337 mGestureMode = GestureMode.Zoom;
338 mScalingBaseDistance = (float)Math.hypot(event.getX(0) - event.getX(1), event.getY(0) - event.getY(1));
339 mScalingCenterX = (event.getX(0) + event.getX(1)) / 2;
340 mScalingCenterY = (event.getY(0) + event.getY(1)) / 2;
344 case MotionEvent.ACTION_MOVE:
345 if (mGestureMode == GestureMode.Move) {
346 // Moves the image and updates the start point.
347 float moveX = event.getX() - mMovingBaseX;
348 float moveY = event.getY() - mMovingBaseY;
349 mMovingBaseX = event.getX();
350 mMovingBaseY = event.getY();
351 updateMove(mMoveX + moveX, mMoveY + moveY);
353 } else if ((mGestureMode == GestureMode.Zoom) && (touchCount >= 2)) {
354 // Zooms the image and updates the distance from the center point.
355 float distance = (float)Math.hypot(event.getX(0) - event.getX(1), event.getY(0) - event.getY(1));
356 float scale = distance / mScalingBaseDistance;
357 mScalingBaseDistance = distance;
358 updateScaleWithBasePoint(mScale * scale, mScalingCenterX, mScalingCenterY);
363 case MotionEvent.ACTION_UP:
364 case MotionEvent.ACTION_POINTER_UP:
365 // Finishes all gestures.
366 mGestureMode = GestureMode.None;
377 public boolean performClick()
379 return (super.performClick());
382 // The content in view can scroll to horizontal.
383 public boolean canHorizontalScroll()
385 return (!((mScale == mScaleMin)||(mGestureMode == GestureMode.None)));