OSDN Git Service

Android SDK を 33 に更新。
[gokigen/Gr2Control.git] / app / src / main / java / net / osdn / gokigen / gr2control / playback / detail / ScalableImageView.java
1 package net.osdn.gokigen.gr2control.playback.detail;
2
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;
12
13 import androidx.appcompat.widget.AppCompatImageView;
14
15 /**
16  *  イメージを表示する
17  *
18  */
19 public class ScalableImageView extends AppCompatImageView
20 {
21
22     private enum GestureMode
23     {
24         None,
25         Move,
26         Zoom,
27     }
28
29     private Context mContext;
30     private GestureDetector mDoubleTapDetector;
31     private GestureMode mGestureMode;
32
33     /** The affine transformation matrix. */
34     private Matrix mMatrix;
35
36     /** The horizontal moving factor after scaling. */
37     private float mMoveX;
38     /** The vertical moving factor after scaling. */
39     private float mMoveY;
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;
44
45     /** The scaling factor. */
46     private float mScale;
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;
57
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;
66
67
68     @Override
69     public void setImageDrawable(Drawable drawable)
70     {
71         super.setImageDrawable(drawable);
72         reset();
73     }
74
75     @Override
76     public void setImageBitmap(Bitmap bm)
77     {
78         super.setImageBitmap(bm);
79         reset();
80     }
81
82     @Override
83     public void setImageURI(Uri uri)
84     {
85         super.setImageURI(uri);
86         reset();
87     }
88
89
90     /**
91      * Constructs a new CapturedImageView.
92      *
93      */
94     public ScalableImageView(Context context)
95     {
96         super(context);
97         mContext = context;
98         init();
99     }
100
101     /**
102      * Constructs a new CapturedImageView.
103      *
104      */
105     public ScalableImageView(Context context, AttributeSet attrs)
106     {
107         super(context, attrs);
108         mContext = context;
109         init();
110     }
111
112     /**
113      * Constructs a new CapturedImageView.
114      *
115      */
116     public ScalableImageView(Context context, AttributeSet attrs, int defStyle)
117     {
118         super(context, attrs, defStyle);
119         mContext = context;
120         init();
121     }
122
123     /**
124      * Initializes this instance.
125      */
126     private void init() {
127         this.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
128         this.setScaleType(ScaleType.MATRIX);
129         mMatrix = new Matrix();
130         mViewWidth = 0;
131         mViewHeight = 0;
132         mImageWidth = 0;
133         mImageHeight = 0;
134         mGestureMode = GestureMode.None;
135         mMoveX = 0;
136         mMoveY = 0;
137         mScale = 1.f;
138         mScaleMin = 1.f;
139         mScaleMax = 4.f;
140
141         // Setups touch gesture.
142         mDoubleTapDetector = new GestureDetector(mContext, new GestureDetector.SimpleOnGestureListener() {
143             @Override
144             public boolean onDoubleTap(MotionEvent e) {
145                 if (mScale != 1.0f) {
146                     // Zooms at tapped point.
147                     updateScaleWithBasePoint(1.0f, e.getX(), e.getY());
148                 } else {
149                     // Zooms out.
150                     fitScreen();
151                 }
152                 updateMatrix();
153                 return true;
154             }
155         });
156     }
157
158     /**
159      * Resets current scaling.
160      */
161     private void reset() {
162         Drawable drawable = this.getDrawable();
163         if (drawable != null) {
164             mImageWidth = drawable.getIntrinsicWidth();
165             mImageHeight = drawable.getIntrinsicHeight();
166             fitScreen();
167             updateMatrix();
168         }
169     }
170
171     @Override
172     protected boolean setFrame(int l, int t, int r, int b) {
173         mViewWidth = r - l;
174         mViewHeight = b - t;
175         if (this.getDrawable() != null) {
176             fitScreen();
177         }
178         updateMatrix();
179         return super.setFrame(l, t, r, b);
180     }
181
182     /**
183      * Returns a scaled X offset.
184      *
185      * @param scale A scaling factor.
186      * @param moveX A horizontal moving factor.
187      * @return A scaled X offset.
188      */
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.
194         offsetX += moveX;
195         return offsetX;
196     }
197
198     /**
199      * Returns a scaled Y offset.
200      *
201      * @param scale A scaling factor.
202      * @param moveY A vertical moving factor.
203      * @return A scaled Y offset.
204      */
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.
210         offsetY += moveY;
211         return offsetY;
212     }
213
214     /**
215      * Updates affine transformation matrix to display the image.
216      */
217     private void updateMatrix() {
218         // Creates new matrix.
219         mMatrix.reset();
220         mMatrix.postScale(mScale, mScale);
221         mMatrix.postTranslate(computeOffsetX(mScale, mMoveX), computeOffsetY(mScale, mMoveY));
222
223         // Updates the matrix.
224         this.setImageMatrix(mMatrix);
225         this.invalidate();
226     }
227
228     /**
229      * Calculates zoom scale. (for the image size to fit screen size).
230      */
231     private void fitScreen() {
232         if ((mImageWidth == 0) || (mImageHeight == 0) || (mViewWidth == 0) || (mViewHeight == 0)) {
233             return;
234         }
235
236         // Clears the moving factors.
237         updateMove(0, 0);
238
239         // Gets scaling ratio.
240         float scaleX = (float)mViewWidth / mImageWidth;
241         float scaleY = (float)mViewHeight / mImageHeight;
242
243         // Updates the scaling factor so that the image will not be larger than the screen size.
244         mScale = Math.min(scaleX, scaleY);
245         mScaleMin = mScale;
246         // 4 times of original image size or 4 times of the screen size.
247         mScaleMax = Math.max(4.f, mScale * 4);
248     }
249
250     /**
251      * Updates the moving factors.
252      *
253      * @param moveX A horizontal moving factor.
254      * @param moveY A vertical moving factor.
255      */
256     protected void updateMove(float moveX, float moveY) {
257         mMoveX = moveX;
258         mMoveY = moveY;
259
260         // Gets scaled size.
261         float scaledWidth = mImageWidth * mScale;
262         float scaledHeight = mImageHeight * mScale;
263
264         // Clips the moving factors.
265         if (scaledWidth <= mViewWidth) {
266             mMoveX = 0;
267         } else {
268             float minMoveX = -(scaledWidth - mViewWidth) / 2;
269             float maxMoveX = +(scaledWidth - mViewWidth) / 2;
270             mMoveX = Math.min(Math.max(mMoveX, minMoveX), maxMoveX);
271         }
272         if (scaledHeight <= mViewHeight) {
273             mMoveY = 0;
274         } else {
275             float minMoveY = -(scaledHeight - mViewHeight) / 2;
276             float maxMoveY = +(scaledHeight - mViewHeight) / 2;
277             mMoveY = Math.min(Math.max(mMoveY, minMoveY), maxMoveY);
278         }
279     }
280
281     /**
282      * Updates the scaling factor. The specified point doesn't change in appearance.
283      *
284      * @param newScale The new scaling factor.
285      * @param baseX The center position of scaling.
286      * @param baseY The center position of scaling.
287      */
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);
292
293         // Updates the scale with clipping.
294         mScale = Math.min(Math.max(newScale, mScaleMin), mScaleMax);
295         mScalingCenterX = baseX;
296         mScalingCenterY = baseY;
297
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;
307
308         // Updates moving.
309         updateMove(mScalingCenterX - scalingCenterXOnView, mScalingCenterY - scalingCenterYOnView);
310     }
311
312     @Override
313     public boolean onTouchEvent(MotionEvent event)
314     {
315         if (mDoubleTapDetector.onTouchEvent(event))
316         {
317             return (true);
318         }
319
320         int action = event.getAction() & MotionEvent.ACTION_MASK;
321         int touchCount = event.getPointerCount();
322         switch (action)
323         {
324             case MotionEvent.ACTION_DOWN:
325                 if (mScale > mScaleMin)
326                 {
327                     // Starts to move the image and takes in the start point.
328                     mGestureMode = GestureMode.Move;
329                     mMovingBaseX = event.getX();
330                     mMovingBaseY = event.getY();
331                 }
332                 break;
333
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;
341                 }
342                 break;
343
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);
352                     updateMatrix();
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);
359                     updateMatrix();
360                 }
361                 break;
362
363             case MotionEvent.ACTION_UP:
364             case MotionEvent.ACTION_POINTER_UP:
365                 // Finishes all gestures.
366                 mGestureMode = GestureMode.None;
367                 break;
368
369             default:
370                 performClick();
371                 break;
372         }
373         return (true);
374     }
375
376     @Override
377     public boolean performClick()
378     {
379         return (super.performClick());
380     }
381
382     // The content in view can scroll to horizontal.
383     public boolean canHorizontalScroll()
384     {
385         return (!((mScale == mScaleMin)||(mGestureMode == GestureMode.None)));
386     }
387 }