+ * TODO: should do a best-fit match. + */ + private static void choosePreviewSize(Camera.Parameters parms, int width, int height) { + // We should make sure that the requested MPEG size is less than the preferred + // size, and has the same aspect ratio. + Camera.Size ppsfv = parms.getPreferredPreviewSizeForVideo(); + if (VERBOSE && ppsfv != null) { + Log.d(TAG, "Camera preferred preview size for video is " + + ppsfv.width + "x" + ppsfv.height); + } + + for (Camera.Size size : parms.getSupportedPreviewSizes()) { + if (size.width == width && size.height == height) { + parms.setPreviewSize(width, height); + return; + } + } + + Log.w(TAG, "Unable to set preview size to " + width + "x" + height); + if (ppsfv != null) { + parms.setPreviewSize(ppsfv.width, ppsfv.height); + } } /** - * Wraps encodeCameraToMpeg(). This is necessary because SurfaceTexture will try to use - * the looper in the current thread if one exists, and the CTS tests create one on the - * test thread. - * - * The wrapper propagates exceptions thrown by the worker thread back to the caller. + * test entry point */ - private static class CameraToMpegWrapper implements Runnable { - private Throwable mThrowable; - private CameraToMpegTest mTest; - - private CameraToMpegWrapper(CameraToMpegTest test) { - mTest = test; - } - - @Override - public void run() { - try { - mTest.encodeCameraToMpeg(); - } catch (Throwable th) { - mThrowable = th; - } - } - - /** Entry point. */ - public static void runTest(CameraToMpegTest obj) throws Throwable { - CameraToMpegWrapper wrapper = new CameraToMpegWrapper(obj); - Thread th = new Thread(wrapper, "codec test"); - th.start(); - th.join(); - if (wrapper.mThrowable != null) { - throw wrapper.mThrowable; - } - } + @Test + public void testEncodeCameraToMp4() throws Throwable { + CameraToMpegWrapper.runTest(this); } /** @@ -261,35 +261,6 @@ public class CameraToMpegTest extends AndroidTestCase { Log.d(TAG, "Camera preview size is " + size.width + "x" + size.height); } - /** - * Attempts to find a preview size that matches the provided width and height (which - * specify the dimensions of the encoded video). If it fails to find a match it just - * uses the default preview size. - *
- * TODO: should do a best-fit match. - */ - private static void choosePreviewSize(Camera.Parameters parms, int width, int height) { - // We should make sure that the requested MPEG size is less than the preferred - // size, and has the same aspect ratio. - Camera.Size ppsfv = parms.getPreferredPreviewSizeForVideo(); - if (VERBOSE && ppsfv != null) { - Log.d(TAG, "Camera preferred preview size for video is " + - ppsfv.width + "x" + ppsfv.height); - } - - for (Camera.Size size : parms.getSupportedPreviewSizes()) { - if (size.width == width && size.height == height) { - parms.setPreviewSize(width, height); - return; - } - } - - Log.w(TAG, "Unable to set preview size to " + width + "x" + height); - if (ppsfv != null) { - parms.setPreviewSize(ppsfv.width, ppsfv.height); - } - } - /** * Stops camera preview, and releases the camera to the system. */ @@ -497,6 +468,43 @@ public class CameraToMpegTest extends AndroidTestCase { } } + /** + * Wraps encodeCameraToMpeg(). This is necessary because SurfaceTexture will try to use + * the looper in the current thread if one exists, and the CTS tests create one on the + * test thread. + *
+ * The wrapper propagates exceptions thrown by the worker thread back to the caller.
+ */
+ private static class CameraToMpegWrapper implements Runnable {
+ private Throwable mThrowable;
+ private CameraToMpegTest mTest;
+
+ private CameraToMpegWrapper(CameraToMpegTest test) {
+ mTest = test;
+ }
+
+ /**
+ * Entry point.
+ */
+ public static void runTest(CameraToMpegTest obj) throws Throwable {
+ CameraToMpegWrapper wrapper = new CameraToMpegWrapper(obj);
+ Thread th = new Thread(wrapper, "codec test");
+ th.start();
+ th.join();
+ if (wrapper.mThrowable != null) {
+ throw wrapper.mThrowable;
+ }
+ }
+
+ @Override
+ public void run() {
+ try {
+ mTest.encodeCameraToMpeg();
+ } catch (Throwable th) {
+ mThrowable = th;
+ }
+ }
+ }
/**
* Holds state associated with a Surface used for MediaCodec encoder input.
@@ -754,16 +762,6 @@ public class CameraToMpegTest extends AndroidTestCase {
private static final int TRIANGLE_VERTICES_DATA_STRIDE_BYTES = 5 * FLOAT_SIZE_BYTES;
private static final int TRIANGLE_VERTICES_DATA_POS_OFFSET = 0;
private static final int TRIANGLE_VERTICES_DATA_UV_OFFSET = 3;
- private final float[] mTriangleVerticesData = {
- // X, Y, Z, U, V
- -1.0f, -1.0f, 0, 0.f, 0.f,
- 1.0f, -1.0f, 0, 1.f, 0.f,
- -1.0f, 1.0f, 0, 0.f, 1.f,
- 1.0f, 1.0f, 0, 1.f, 1.f,
- };
-
- private FloatBuffer mTriangleVertices;
-
private static final String VERTEX_SHADER =
"uniform mat4 uMVPMatrix;\n" +
"uniform mat4 uSTMatrix;\n" +
@@ -774,7 +772,6 @@ public class CameraToMpegTest extends AndroidTestCase {
" gl_Position = uMVPMatrix * aPosition;\n" +
" vTextureCoord = (uSTMatrix * aTextureCoord).xy;\n" +
"}\n";
-
private static final String FRAGMENT_SHADER =
"#extension GL_OES_EGL_image_external : require\n" +
"precision mediump float;\n" + // highp here doesn't seem to matter
@@ -783,7 +780,14 @@ public class CameraToMpegTest extends AndroidTestCase {
"void main() {\n" +
" gl_FragColor = texture2D(sTexture, vTextureCoord);\n" +
"}\n";
-
+ private final float[] mTriangleVerticesData = {
+ // X, Y, Z, U, V
+ -1.0f, -1.0f, 0, 0.f, 0.f,
+ 1.0f, -1.0f, 0, 1.f, 0.f,
+ -1.0f, 1.0f, 0, 0.f, 1.f,
+ 1.0f, 1.0f, 0, 1.f, 1.f,
+ };
+ private FloatBuffer mTriangleVertices;
private float[] mMVPMatrix = new float[16];
private float[] mSTMatrix = new float[16];
@@ -796,13 +800,19 @@ public class CameraToMpegTest extends AndroidTestCase {
public STextureRender() {
mTriangleVertices = ByteBuffer.allocateDirect(
- mTriangleVerticesData.length * FLOAT_SIZE_BYTES)
+ mTriangleVerticesData.length * FLOAT_SIZE_BYTES)
.order(ByteOrder.nativeOrder()).asFloatBuffer();
mTriangleVertices.put(mTriangleVerticesData).position(0);
Matrix.setIdentityM(mSTMatrix, 0);
}
+ public static void checkLocation(int location, String label) {
+ if (location < 0) {
+ throw new RuntimeException("Unable to locate '" + label + "' in program");
+ }
+ }
+
public int getTextureId() {
return mTextureID;
}
@@ -952,11 +962,5 @@ public class CameraToMpegTest extends AndroidTestCase {
throw new RuntimeException(op + ": glError " + error);
}
}
-
- public static void checkLocation(int location, String label) {
- if (location < 0) {
- throw new RuntimeException("Unable to locate '" + label + "' in program");
- }
- }
}
}
\ No newline at end of file
diff --git a/EasyPlayerPro/src/main/AndroidManifest.xml b/EasyPlayerPro/src/main/AndroidManifest.xml
index ed4da98..b030871 100644
--- a/EasyPlayerPro/src/main/AndroidManifest.xml
+++ b/EasyPlayerPro/src/main/AndroidManifest.xml
@@ -1,18 +1,25 @@
* Created by apple on 2017/2/11.
*/
public class ProVideoView extends IjkVideoView implements VideoControllerView.FullScreenAbleMediaPlayerControl {
@@ -53,19 +54,21 @@ public class ProVideoView extends IjkVideoView implements VideoControllerView.Fu
return native_active_days(context, key);
}
- /** ================ super override ================ */
+ /**
+ * ================ super override ================
+ */
public void startRecord(String path, int seconds) {
if (mMediaPlayer == null) {
return;
}
- super.startRecord(path,seconds);
+ super.startRecord(path, seconds);
mRecordPath = path;
}
public void stopRecord() {
- if (mMediaPlayer == null){
+ if (mMediaPlayer == null) {
return;
}
@@ -73,7 +76,9 @@ public class ProVideoView extends IjkVideoView implements VideoControllerView.Fu
mRecordPath = null;
}
- /** ================ FullScreenAbleMediaPlayerControl ================ */
+ /**
+ * ================ FullScreenAbleMediaPlayerControl ================
+ */
@Override
public boolean isFullScreen() {
@@ -131,7 +136,7 @@ public class ProVideoView extends IjkVideoView implements VideoControllerView.Fu
@Override
public void reStart() {
super.reStart();
- if (mRecordPath != null){
+ if (mRecordPath != null) {
toggleRecord();
toggleRecord();
}
@@ -142,8 +147,8 @@ public class ProVideoView extends IjkVideoView implements VideoControllerView.Fu
if (getContext() instanceof ProVideoActivity) {
ProVideoActivity pro = (ProVideoActivity) getContext();
- if (ActivityCompat.checkSelfPermission(pro, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){
- ActivityCompat.requestPermissions(pro, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, ProVideoActivity.REQUEST_WRITE_STORAGE +1);
+ if (ActivityCompat.checkSelfPermission(pro, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+ ActivityCompat.requestPermissions(pro, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, ProVideoActivity.REQUEST_WRITE_STORAGE + 1);
return;
}
}
@@ -184,8 +189,8 @@ public class ProVideoView extends IjkVideoView implements VideoControllerView.Fu
@Override
public void setSpeed(float speed) {
- if (mMediaPlayer == null ) {
- return ;
+ if (mMediaPlayer == null) {
+ return;
}
if (mMediaPlayer instanceof IjkMediaPlayer) {
@@ -196,7 +201,7 @@ public class ProVideoView extends IjkVideoView implements VideoControllerView.Fu
@Override
public void takePicture() {
- if (getContext() instanceof ProVideoActivity){
+ if (getContext() instanceof ProVideoActivity) {
ProVideoActivity pro = (ProVideoActivity) getContext();
pro.onTakePicture(null);
}
diff --git a/EasyPlayerPro/src/main/java/org/easydarwin/easyplayer/views/SquareImageView.java b/EasyPlayerPro/src/main/java/org/easydarwin/easyplayer/views/SquareImageView.java
index a30dbae..ad320dd 100755
--- a/EasyPlayerPro/src/main/java/org/easydarwin/easyplayer/views/SquareImageView.java
+++ b/EasyPlayerPro/src/main/java/org/easydarwin/easyplayer/views/SquareImageView.java
@@ -3,12 +3,11 @@ package org.easydarwin.easyplayer.views;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
-import android.widget.ImageView;
/**
* Created by John on 2014/11/11.
*/
-public class SquareImageView extends ImageView {
+public class SquareImageView extends androidx.appcompat.widget.AppCompatImageView {
public SquareImageView(Context context) {
super(context);
diff --git a/EasyPlayerPro/src/main/res/layout/activity_about.xml b/EasyPlayerPro/src/main/res/layout/activity_about.xml
index ae76471..3b57487 100644
--- a/EasyPlayerPro/src/main/res/layout/activity_about.xml
+++ b/EasyPlayerPro/src/main/res/layout/activity_about.xml
@@ -1,17 +1,15 @@
-
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package com.github.chrisbanes.photoview;
+
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.util.AttributeSet;
+import android.view.GestureDetector;
+
+import androidx.appcompat.widget.AppCompatImageView;
+
+/**
+ * A zoomable ImageView. See {@link PhotoViewAttacher} for most of the details on how the zooming
+ * is accomplished
+ */
+@SuppressWarnings("unused")
+public class PhotoView extends AppCompatImageView {
+
+ private PhotoViewAttacher attacher;
+ private ScaleType pendingScaleType;
+
+ public PhotoView(Context context) {
+ this(context, null);
+ }
+
+ public PhotoView(Context context, AttributeSet attr) {
+ this(context, attr, 0);
+ }
+
+ public PhotoView(Context context, AttributeSet attr, int defStyle) {
+ super(context, attr, defStyle);
+ init();
+ }
+
+ private void init() {
+ attacher = new PhotoViewAttacher(this);
+ //We always pose as a Matrix scale type, though we can change to another scale type
+ //via the attacher
+ super.setScaleType(ScaleType.MATRIX);
+ //apply the previously applied scale type
+ if (pendingScaleType != null) {
+ setScaleType(pendingScaleType);
+ pendingScaleType = null;
+ }
+ }
+
+ /**
+ * Get the current {@link PhotoViewAttacher} for this view. Be wary of holding on to references
+ * to this attacher, as it has a reference to this view, which, if a reference is held in the
+ * wrong place, can cause memory leaks.
+ *
+ * @return the attacher.
+ */
+ public PhotoViewAttacher getAttacher() {
+ return attacher;
+ }
+
+ @Override
+ public ScaleType getScaleType() {
+ return attacher.getScaleType();
+ }
+
+ @Override
+ public Matrix getImageMatrix() {
+ return attacher.getImageMatrix();
+ }
+
+ @Override
+ public void setOnLongClickListener(OnLongClickListener l) {
+ attacher.setOnLongClickListener(l);
+ }
+
+ @Override
+ public void setOnClickListener(OnClickListener l) {
+ attacher.setOnClickListener(l);
+ }
+
+ @Override
+ public void setScaleType(ScaleType scaleType) {
+ if (attacher == null) {
+ pendingScaleType = scaleType;
+ } else {
+ attacher.setScaleType(scaleType);
+ }
+ }
+
+ @Override
+ public void setImageDrawable(Drawable drawable) {
+ super.setImageDrawable(drawable);
+ // setImageBitmap calls through to this method
+ if (attacher != null) {
+ attacher.update();
+ }
+ }
+
+ @Override
+ public void setImageResource(int resId) {
+ super.setImageResource(resId);
+ if (attacher != null) {
+ attacher.update();
+ }
+ }
+
+ @Override
+ public void setImageURI(Uri uri) {
+ super.setImageURI(uri);
+ if (attacher != null) {
+ attacher.update();
+ }
+ }
+
+ @Override
+ protected boolean setFrame(int l, int t, int r, int b) {
+ boolean changed = super.setFrame(l, t, r, b);
+ if (changed) {
+ attacher.update();
+ }
+ return changed;
+ }
+
+ public void setRotationTo(float rotationDegree) {
+ attacher.setRotationTo(rotationDegree);
+ }
+
+ public void setRotationBy(float rotationDegree) {
+ attacher.setRotationBy(rotationDegree);
+ }
+
+ public boolean isZoomable() {
+ return attacher.isZoomable();
+ }
+
+ public void setZoomable(boolean zoomable) {
+ attacher.setZoomable(zoomable);
+ }
+
+ public RectF getDisplayRect() {
+ return attacher.getDisplayRect();
+ }
+
+ public void getDisplayMatrix(Matrix matrix) {
+ attacher.getDisplayMatrix(matrix);
+ }
+
+ @SuppressWarnings("UnusedReturnValue") public boolean setDisplayMatrix(Matrix finalRectangle) {
+ return attacher.setDisplayMatrix(finalRectangle);
+ }
+
+ public void getSuppMatrix(Matrix matrix) {
+ attacher.getSuppMatrix(matrix);
+ }
+
+ public boolean setSuppMatrix(Matrix matrix) {
+ return attacher.setDisplayMatrix(matrix);
+ }
+
+ public float getMinimumScale() {
+ return attacher.getMinimumScale();
+ }
+
+ public float getMediumScale() {
+ return attacher.getMediumScale();
+ }
+
+ public float getMaximumScale() {
+ return attacher.getMaximumScale();
+ }
+
+ public float getScale() {
+ return attacher.getScale();
+ }
+
+ public void setAllowParentInterceptOnEdge(boolean allow) {
+ attacher.setAllowParentInterceptOnEdge(allow);
+ }
+
+ public void setMinimumScale(float minimumScale) {
+ attacher.setMinimumScale(minimumScale);
+ }
+
+ public void setMediumScale(float mediumScale) {
+ attacher.setMediumScale(mediumScale);
+ }
+
+ public void setMaximumScale(float maximumScale) {
+ attacher.setMaximumScale(maximumScale);
+ }
+
+ public void setScaleLevels(float minimumScale, float mediumScale, float maximumScale) {
+ attacher.setScaleLevels(minimumScale, mediumScale, maximumScale);
+ }
+
+ public void setOnMatrixChangeListener(OnMatrixChangedListener listener) {
+ attacher.setOnMatrixChangeListener(listener);
+ }
+
+ public void setOnPhotoTapListener(OnPhotoTapListener listener) {
+ attacher.setOnPhotoTapListener(listener);
+ }
+
+ public void setOnOutsidePhotoTapListener(OnOutsidePhotoTapListener listener) {
+ attacher.setOnOutsidePhotoTapListener(listener);
+ }
+
+ public void setOnViewTapListener(OnViewTapListener listener) {
+ attacher.setOnViewTapListener(listener);
+ }
+
+ public void setOnViewDragListener(OnViewDragListener listener) {
+ attacher.setOnViewDragListener(listener);
+ }
+
+ public void setScale(float scale) {
+ attacher.setScale(scale);
+ }
+
+ public void setScale(float scale, boolean animate) {
+ attacher.setScale(scale, animate);
+ }
+
+ public void setScale(float scale, float focalX, float focalY, boolean animate) {
+ attacher.setScale(scale, focalX, focalY, animate);
+ }
+
+ public void setZoomTransitionDuration(int milliseconds) {
+ attacher.setZoomTransitionDuration(milliseconds);
+ }
+
+ public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener onDoubleTapListener) {
+ attacher.setOnDoubleTapListener(onDoubleTapListener);
+ }
+
+ public void setOnScaleChangeListener(OnScaleChangedListener onScaleChangedListener) {
+ attacher.setOnScaleChangeListener(onScaleChangedListener);
+ }
+
+ public void setOnSingleFlingListener(OnSingleFlingListener onSingleFlingListener) {
+ attacher.setOnSingleFlingListener(onSingleFlingListener);
+ }
+}
diff --git a/photoview/src/main/java/com/github/chrisbanes/photoview/PhotoViewAttacher.java b/photoview/src/main/java/com/github/chrisbanes/photoview/PhotoViewAttacher.java
new file mode 100644
index 0000000..55965b8
--- /dev/null
+++ b/photoview/src/main/java/com/github/chrisbanes/photoview/PhotoViewAttacher.java
@@ -0,0 +1,823 @@
+/*
+ Copyright 2011, 2012 Chris Banes.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package com.github.chrisbanes.photoview;
+
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.Matrix.ScaleToFit;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnLongClickListener;
+import android.view.ViewParent;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+import android.widget.OverScroller;
+
+/**
+ * The component of {@link PhotoView} which does the work allowing for zooming, scaling, panning, etc.
+ * It is made public in case you need to subclass something other than AppCompatImageView and still
+ * gain the functionality that {@link PhotoView} offers
+ */
+public class PhotoViewAttacher implements View.OnTouchListener,
+ View.OnLayoutChangeListener {
+
+ private static float DEFAULT_MAX_SCALE = 3.0f;
+ private static float DEFAULT_MID_SCALE = 1.75f;
+ private static float DEFAULT_MIN_SCALE = 1.0f;
+ private static int DEFAULT_ZOOM_DURATION = 200;
+
+ private static final int HORIZONTAL_EDGE_NONE = -1;
+ private static final int HORIZONTAL_EDGE_LEFT = 0;
+ private static final int HORIZONTAL_EDGE_RIGHT = 1;
+ private static final int HORIZONTAL_EDGE_BOTH = 2;
+ private static final int VERTICAL_EDGE_NONE = -1;
+ private static final int VERTICAL_EDGE_TOP = 0;
+ private static final int VERTICAL_EDGE_BOTTOM = 1;
+ private static final int VERTICAL_EDGE_BOTH = 2;
+ private static int SINGLE_TOUCH = 1;
+
+ private Interpolator mInterpolator = new AccelerateDecelerateInterpolator();
+ private int mZoomDuration = DEFAULT_ZOOM_DURATION;
+ private float mMinScale = DEFAULT_MIN_SCALE;
+ private float mMidScale = DEFAULT_MID_SCALE;
+ private float mMaxScale = DEFAULT_MAX_SCALE;
+
+ private boolean mAllowParentInterceptOnEdge = true;
+ private boolean mBlockParentIntercept = false;
+
+ private ImageView mImageView;
+
+ // Gesture Detectors
+ private GestureDetector mGestureDetector;
+ private CustomGestureDetector mScaleDragDetector;
+
+ // These are set so we don't keep allocating them on the heap
+ private final Matrix mBaseMatrix = new Matrix();
+ private final Matrix mDrawMatrix = new Matrix();
+ private final Matrix mSuppMatrix = new Matrix();
+ private final RectF mDisplayRect = new RectF();
+ private final float[] mMatrixValues = new float[9];
+
+ // Listeners
+ private OnMatrixChangedListener mMatrixChangeListener;
+ private OnPhotoTapListener mPhotoTapListener;
+ private OnOutsidePhotoTapListener mOutsidePhotoTapListener;
+ private OnViewTapListener mViewTapListener;
+ private View.OnClickListener mOnClickListener;
+ private OnLongClickListener mLongClickListener;
+ private OnScaleChangedListener mScaleChangeListener;
+ private OnSingleFlingListener mSingleFlingListener;
+ private OnViewDragListener mOnViewDragListener;
+
+ private FlingRunnable mCurrentFlingRunnable;
+ private int mHorizontalScrollEdge = HORIZONTAL_EDGE_BOTH;
+ private int mVerticalScrollEdge = VERTICAL_EDGE_BOTH;
+ private float mBaseRotation;
+
+ private boolean mZoomEnabled = true;
+ private ScaleType mScaleType = ScaleType.FIT_CENTER;
+
+ private OnGestureListener onGestureListener = new OnGestureListener() {
+ @Override
+ public void onDrag(float dx, float dy) {
+ if (mScaleDragDetector.isScaling()) {
+ return; // Do not drag if we are already scaling
+ }
+ if (mOnViewDragListener != null) {
+ mOnViewDragListener.onDrag(dx, dy);
+ }
+ mSuppMatrix.postTranslate(dx, dy);
+ checkAndDisplayMatrix();
+
+ /*
+ * Here we decide whether to let the ImageView's parent to start taking
+ * over the touch event.
+ *
+ * First we check whether this function is enabled. We never want the
+ * parent to take over if we're scaling. We then check the edge we're
+ * on, and the direction of the scroll (i.e. if we're pulling against
+ * the edge, aka 'overscrolling', let the parent take over).
+ */
+ ViewParent parent = mImageView.getParent();
+ if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isScaling() && !mBlockParentIntercept) {
+ if (mHorizontalScrollEdge == HORIZONTAL_EDGE_BOTH
+ || (mHorizontalScrollEdge == HORIZONTAL_EDGE_LEFT && dx >= 1f)
+ || (mHorizontalScrollEdge == HORIZONTAL_EDGE_RIGHT && dx <= -1f)
+ || (mVerticalScrollEdge == VERTICAL_EDGE_TOP && dy >= 1f)
+ || (mVerticalScrollEdge == VERTICAL_EDGE_BOTTOM && dy <= -1f)) {
+ if (parent != null) {
+ parent.requestDisallowInterceptTouchEvent(false);
+ }
+ }
+ } else {
+ if (parent != null) {
+ parent.requestDisallowInterceptTouchEvent(true);
+ }
+ }
+ }
+
+ @Override
+ public void onFling(float startX, float startY, float velocityX, float velocityY) {
+ mCurrentFlingRunnable = new FlingRunnable(mImageView.getContext());
+ mCurrentFlingRunnable.fling(getImageViewWidth(mImageView),
+ getImageViewHeight(mImageView), (int) velocityX, (int) velocityY);
+ mImageView.post(mCurrentFlingRunnable);
+ }
+
+ @Override
+ public void onScale(float scaleFactor, float focusX, float focusY) {
+ onScale(scaleFactor, focusX, focusY, 0, 0);
+ }
+
+ @Override
+ public void onScale(float scaleFactor, float focusX, float focusY, float dx, float dy) {
+ if (getScale() < mMaxScale || scaleFactor < 1f) {
+ if (mScaleChangeListener != null) {
+ mScaleChangeListener.onScaleChange(scaleFactor, focusX, focusY);
+ }
+ mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);
+ mSuppMatrix.postTranslate(dx, dy);
+ checkAndDisplayMatrix();
+ }
+ }
+ };
+
+ public PhotoViewAttacher(ImageView imageView) {
+ mImageView = imageView;
+ imageView.setOnTouchListener(this);
+ imageView.addOnLayoutChangeListener(this);
+ if (imageView.isInEditMode()) {
+ return;
+ }
+ mBaseRotation = 0.0f;
+ // Create Gesture Detectors...
+ mScaleDragDetector = new CustomGestureDetector(imageView.getContext(), onGestureListener);
+ mGestureDetector = new GestureDetector(imageView.getContext(), new GestureDetector.SimpleOnGestureListener() {
+
+ // forward long click listener
+ @Override
+ public void onLongPress(MotionEvent e) {
+ if (mLongClickListener != null) {
+ mLongClickListener.onLongClick(mImageView);
+ }
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2,
+ float velocityX, float velocityY) {
+ if (mSingleFlingListener != null) {
+ if (getScale() > DEFAULT_MIN_SCALE) {
+ return false;
+ }
+ if (e1.getPointerCount() > SINGLE_TOUCH
+ || e2.getPointerCount() > SINGLE_TOUCH) {
+ return false;
+ }
+ return mSingleFlingListener.onFling(e1, e2, velocityX, velocityY);
+ }
+ return false;
+ }
+ });
+ mGestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() {
+ @Override
+ public boolean onSingleTapConfirmed(MotionEvent e) {
+ if (mOnClickListener != null) {
+ mOnClickListener.onClick(mImageView);
+ }
+ final RectF displayRect = getDisplayRect();
+ final float x = e.getX(), y = e.getY();
+ if (mViewTapListener != null) {
+ mViewTapListener.onViewTap(mImageView, x, y);
+ }
+ if (displayRect != null) {
+ // Check to see if the user tapped on the photo
+ if (displayRect.contains(x, y)) {
+ float xResult = (x - displayRect.left)
+ / displayRect.width();
+ float yResult = (y - displayRect.top)
+ / displayRect.height();
+ if (mPhotoTapListener != null) {
+ mPhotoTapListener.onPhotoTap(mImageView, xResult, yResult);
+ }
+ return true;
+ } else {
+ if (mOutsidePhotoTapListener != null) {
+ mOutsidePhotoTapListener.onOutsidePhotoTap(mImageView);
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onDoubleTap(MotionEvent ev) {
+ try {
+ float scale = getScale();
+ float x = ev.getX();
+ float y = ev.getY();
+ if (scale < getMediumScale()) {
+ setScale(getMediumScale(), x, y, true);
+ } else if (scale >= getMediumScale() && scale < getMaximumScale()) {
+ setScale(getMaximumScale(), x, y, true);
+ } else {
+ setScale(getMinimumScale(), x, y, true);
+ }
+ } catch (ArrayIndexOutOfBoundsException e) {
+ // Can sometimes happen when getX() and getY() is called
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onDoubleTapEvent(MotionEvent e) {
+ // Wait for the confirmed onDoubleTap() instead
+ return false;
+ }
+ });
+ }
+
+ public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener newOnDoubleTapListener) {
+ this.mGestureDetector.setOnDoubleTapListener(newOnDoubleTapListener);
+ }
+
+ public void setOnScaleChangeListener(OnScaleChangedListener onScaleChangeListener) {
+ this.mScaleChangeListener = onScaleChangeListener;
+ }
+
+ public void setOnSingleFlingListener(OnSingleFlingListener onSingleFlingListener) {
+ this.mSingleFlingListener = onSingleFlingListener;
+ }
+
+ @Deprecated
+ public boolean isZoomEnabled() {
+ return mZoomEnabled;
+ }
+
+ public RectF getDisplayRect() {
+ checkMatrixBounds();
+ return getDisplayRect(getDrawMatrix());
+ }
+
+ public boolean setDisplayMatrix(Matrix finalMatrix) {
+ if (finalMatrix == null) {
+ throw new IllegalArgumentException("Matrix cannot be null");
+ }
+ if (mImageView.getDrawable() == null) {
+ return false;
+ }
+ mSuppMatrix.set(finalMatrix);
+ checkAndDisplayMatrix();
+ return true;
+ }
+
+ public void setBaseRotation(final float degrees) {
+ mBaseRotation = degrees % 360;
+ update();
+ setRotationBy(mBaseRotation);
+ checkAndDisplayMatrix();
+ }
+
+ public void setRotationTo(float degrees) {
+ mSuppMatrix.setRotate(degrees % 360);
+ checkAndDisplayMatrix();
+ }
+
+ public void setRotationBy(float degrees) {
+ mSuppMatrix.postRotate(degrees % 360);
+ checkAndDisplayMatrix();
+ }
+
+ public float getMinimumScale() {
+ return mMinScale;
+ }
+
+ public float getMediumScale() {
+ return mMidScale;
+ }
+
+ public float getMaximumScale() {
+ return mMaxScale;
+ }
+
+ public float getScale() {
+ return (float) Math.sqrt((float) Math.pow(getValue(mSuppMatrix, Matrix.MSCALE_X), 2) + (float) Math.pow
+ (getValue(mSuppMatrix, Matrix.MSKEW_Y), 2));
+ }
+
+ public ScaleType getScaleType() {
+ return mScaleType;
+ }
+
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int
+ oldRight, int oldBottom) {
+ // Update our base matrix, as the bounds have changed
+ if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) {
+ updateBaseMatrix(mImageView.getDrawable());
+ }
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent ev) {
+ boolean handled = false;
+ if (mZoomEnabled && Util.hasDrawable((ImageView) v)) {
+ switch (ev.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ ViewParent parent = v.getParent();
+ // First, disable the Parent from intercepting the touch
+ // event
+ if (parent != null) {
+ parent.requestDisallowInterceptTouchEvent(true);
+ }
+ // If we're flinging, and the user presses down, cancel
+ // fling
+ cancelFling();
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ // If the user has zoomed less than min scale, zoom back
+ // to min scale
+ if (getScale() < mMinScale) {
+ RectF rect = getDisplayRect();
+ if (rect != null) {
+ v.post(new AnimatedZoomRunnable(getScale(), mMinScale,
+ rect.centerX(), rect.centerY()));
+ handled = true;
+ }
+ } else if (getScale() > mMaxScale) {
+ RectF rect = getDisplayRect();
+ if (rect != null) {
+ v.post(new AnimatedZoomRunnable(getScale(), mMaxScale,
+ rect.centerX(), rect.centerY()));
+ handled = true;
+ }
+ }
+ break;
+ }
+ // Try the Scale/Drag detector
+ if (mScaleDragDetector != null) {
+ boolean wasScaling = mScaleDragDetector.isScaling();
+ boolean wasDragging = mScaleDragDetector.isDragging();
+ handled = mScaleDragDetector.onTouchEvent(ev);
+ boolean didntScale = !wasScaling && !mScaleDragDetector.isScaling();
+ boolean didntDrag = !wasDragging && !mScaleDragDetector.isDragging();
+ mBlockParentIntercept = didntScale && didntDrag;
+ }
+ // Check to see if the user double tapped
+ if (mGestureDetector != null && mGestureDetector.onTouchEvent(ev)) {
+ handled = true;
+ }
+
+ }
+ return handled;
+ }
+
+ public void setAllowParentInterceptOnEdge(boolean allow) {
+ mAllowParentInterceptOnEdge = allow;
+ }
+
+ public void setMinimumScale(float minimumScale) {
+ Util.checkZoomLevels(minimumScale, mMidScale, mMaxScale);
+ mMinScale = minimumScale;
+ }
+
+ public void setMediumScale(float mediumScale) {
+ Util.checkZoomLevels(mMinScale, mediumScale, mMaxScale);
+ mMidScale = mediumScale;
+ }
+
+ public void setMaximumScale(float maximumScale) {
+ Util.checkZoomLevels(mMinScale, mMidScale, maximumScale);
+ mMaxScale = maximumScale;
+ }
+
+ public void setScaleLevels(float minimumScale, float mediumScale, float maximumScale) {
+ Util.checkZoomLevels(minimumScale, mediumScale, maximumScale);
+ mMinScale = minimumScale;
+ mMidScale = mediumScale;
+ mMaxScale = maximumScale;
+ }
+
+ public void setOnLongClickListener(OnLongClickListener listener) {
+ mLongClickListener = listener;
+ }
+
+ public void setOnClickListener(View.OnClickListener listener) {
+ mOnClickListener = listener;
+ }
+
+ public void setOnMatrixChangeListener(OnMatrixChangedListener listener) {
+ mMatrixChangeListener = listener;
+ }
+
+ public void setOnPhotoTapListener(OnPhotoTapListener listener) {
+ mPhotoTapListener = listener;
+ }
+
+ public void setOnOutsidePhotoTapListener(OnOutsidePhotoTapListener mOutsidePhotoTapListener) {
+ this.mOutsidePhotoTapListener = mOutsidePhotoTapListener;
+ }
+
+ public void setOnViewTapListener(OnViewTapListener listener) {
+ mViewTapListener = listener;
+ }
+
+ public void setOnViewDragListener(OnViewDragListener listener) {
+ mOnViewDragListener = listener;
+ }
+
+ public void setScale(float scale) {
+ setScale(scale, false);
+ }
+
+ public void setScale(float scale, boolean animate) {
+ setScale(scale,
+ (mImageView.getRight()) / 2,
+ (mImageView.getBottom()) / 2,
+ animate);
+ }
+
+ public void setScale(float scale, float focalX, float focalY,
+ boolean animate) {
+ // Check to see if the scale is within bounds
+ if (scale < mMinScale || scale > mMaxScale) {
+ throw new IllegalArgumentException("Scale must be within the range of minScale and maxScale");
+ }
+ if (animate) {
+ mImageView.post(new AnimatedZoomRunnable(getScale(), scale,
+ focalX, focalY));
+ } else {
+ mSuppMatrix.setScale(scale, scale, focalX, focalY);
+ checkAndDisplayMatrix();
+ }
+ }
+
+ /**
+ * Set the zoom interpolator
+ *
+ * @param interpolator the zoom interpolator
+ */
+ public void setZoomInterpolator(Interpolator interpolator) {
+ mInterpolator = interpolator;
+ }
+
+ public void setScaleType(ScaleType scaleType) {
+ if (Util.isSupportedScaleType(scaleType) && scaleType != mScaleType) {
+ mScaleType = scaleType;
+ update();
+ }
+ }
+
+ public boolean isZoomable() {
+ return mZoomEnabled;
+ }
+
+ public void setZoomable(boolean zoomable) {
+ mZoomEnabled = zoomable;
+ update();
+ }
+
+ public void update() {
+ if (mZoomEnabled) {
+ // Update the base matrix using the current drawable
+ updateBaseMatrix(mImageView.getDrawable());
+ } else {
+ // Reset the Matrix...
+ resetMatrix();
+ }
+ }
+
+ /**
+ * Get the display matrix
+ *
+ * @param matrix target matrix to copy to
+ */
+ public void getDisplayMatrix(Matrix matrix) {
+ matrix.set(getDrawMatrix());
+ }
+
+ /**
+ * Get the current support matrix
+ */
+ public void getSuppMatrix(Matrix matrix) {
+ matrix.set(mSuppMatrix);
+ }
+
+ private Matrix getDrawMatrix() {
+ mDrawMatrix.set(mBaseMatrix);
+ mDrawMatrix.postConcat(mSuppMatrix);
+ return mDrawMatrix;
+ }
+
+ public Matrix getImageMatrix() {
+ return mDrawMatrix;
+ }
+
+ public void setZoomTransitionDuration(int milliseconds) {
+ this.mZoomDuration = milliseconds;
+ }
+
+ /**
+ * Helper method that 'unpacks' a Matrix and returns the required value
+ *
+ * @param matrix Matrix to unpack
+ * @param whichValue Which value from Matrix.M* to return
+ * @return returned value
+ */
+ private float getValue(Matrix matrix, int whichValue) {
+ matrix.getValues(mMatrixValues);
+ return mMatrixValues[whichValue];
+ }
+
+ /**
+ * Resets the Matrix back to FIT_CENTER, and then displays its contents
+ */
+ private void resetMatrix() {
+ mSuppMatrix.reset();
+ setRotationBy(mBaseRotation);
+ setImageViewMatrix(getDrawMatrix());
+ checkMatrixBounds();
+ }
+
+ private void setImageViewMatrix(Matrix matrix) {
+ mImageView.setImageMatrix(matrix);
+ // Call MatrixChangedListener if needed
+ if (mMatrixChangeListener != null) {
+ RectF displayRect = getDisplayRect(matrix);
+ if (displayRect != null) {
+ mMatrixChangeListener.onMatrixChanged(displayRect);
+ }
+ }
+ }
+
+ /**
+ * Helper method that simply checks the Matrix, and then displays the result
+ */
+ private void checkAndDisplayMatrix() {
+ if (checkMatrixBounds()) {
+ setImageViewMatrix(getDrawMatrix());
+ }
+ }
+
+ /**
+ * Helper method that maps the supplied Matrix to the current Drawable
+ *
+ * @param matrix - Matrix to map Drawable against
+ * @return RectF - Displayed Rectangle
+ */
+ private RectF getDisplayRect(Matrix matrix) {
+ Drawable d = mImageView.getDrawable();
+ if (d != null) {
+ mDisplayRect.set(0, 0, d.getIntrinsicWidth(),
+ d.getIntrinsicHeight());
+ matrix.mapRect(mDisplayRect);
+ return mDisplayRect;
+ }
+ return null;
+ }
+
+ /**
+ * Calculate Matrix for FIT_CENTER
+ *
+ * @param drawable - Drawable being displayed
+ */
+ private void updateBaseMatrix(Drawable drawable) {
+ if (drawable == null) {
+ return;
+ }
+ final float viewWidth = getImageViewWidth(mImageView);
+ final float viewHeight = getImageViewHeight(mImageView);
+ final int drawableWidth = drawable.getIntrinsicWidth();
+ final int drawableHeight = drawable.getIntrinsicHeight();
+ mBaseMatrix.reset();
+ final float widthScale = viewWidth / drawableWidth;
+ final float heightScale = viewHeight / drawableHeight;
+ if (mScaleType == ScaleType.CENTER) {
+ mBaseMatrix.postTranslate((viewWidth - drawableWidth) / 2F,
+ (viewHeight - drawableHeight) / 2F);
+
+ } else if (mScaleType == ScaleType.CENTER_CROP) {
+ float scale = Math.max(widthScale, heightScale);
+ mBaseMatrix.postScale(scale, scale);
+ mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F,
+ (viewHeight - drawableHeight * scale) / 2F);
+
+ } else if (mScaleType == ScaleType.CENTER_INSIDE) {
+ float scale = Math.min(1.0f, Math.min(widthScale, heightScale));
+ mBaseMatrix.postScale(scale, scale);
+ mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F,
+ (viewHeight - drawableHeight * scale) / 2F);
+
+ } else {
+ RectF mTempSrc = new RectF(0, 0, drawableWidth, drawableHeight);
+ RectF mTempDst = new RectF(0, 0, viewWidth, viewHeight);
+ if ((int) mBaseRotation % 180 != 0) {
+ mTempSrc = new RectF(0, 0, drawableHeight, drawableWidth);
+ }
+ switch (mScaleType) {
+ case FIT_CENTER:
+ mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.CENTER);
+ break;
+ case FIT_START:
+ mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.START);
+ break;
+ case FIT_END:
+ mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.END);
+ break;
+ case FIT_XY:
+ mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.FILL);
+ break;
+ default:
+ break;
+ }
+ }
+ resetMatrix();
+ }
+
+ private boolean checkMatrixBounds() {
+ final RectF rect = getDisplayRect(getDrawMatrix());
+ if (rect == null) {
+ return false;
+ }
+ final float height = rect.height(), width = rect.width();
+ float deltaX = 0, deltaY = 0;
+ final int viewHeight = getImageViewHeight(mImageView);
+ if (height <= viewHeight) {
+ switch (mScaleType) {
+ case FIT_START:
+ deltaY = -rect.top;
+ break;
+ case FIT_END:
+ deltaY = viewHeight - height - rect.top;
+ break;
+ default:
+ deltaY = (viewHeight - height) / 2 - rect.top;
+ break;
+ }
+ mVerticalScrollEdge = VERTICAL_EDGE_BOTH;
+ } else if (rect.top > 0) {
+ mVerticalScrollEdge = VERTICAL_EDGE_TOP;
+ deltaY = -rect.top;
+ } else if (rect.bottom < viewHeight) {
+ mVerticalScrollEdge = VERTICAL_EDGE_BOTTOM;
+ deltaY = viewHeight - rect.bottom;
+ } else {
+ mVerticalScrollEdge = VERTICAL_EDGE_NONE;
+ }
+ final int viewWidth = getImageViewWidth(mImageView);
+ if (width <= viewWidth) {
+ switch (mScaleType) {
+ case FIT_START:
+ deltaX = -rect.left;
+ break;
+ case FIT_END:
+ deltaX = viewWidth - width - rect.left;
+ break;
+ default:
+ deltaX = (viewWidth - width) / 2 - rect.left;
+ break;
+ }
+ mHorizontalScrollEdge = HORIZONTAL_EDGE_BOTH;
+ } else if (rect.left > 0) {
+ mHorizontalScrollEdge = HORIZONTAL_EDGE_LEFT;
+ deltaX = -rect.left;
+ } else if (rect.right < viewWidth) {
+ deltaX = viewWidth - rect.right;
+ mHorizontalScrollEdge = HORIZONTAL_EDGE_RIGHT;
+ } else {
+ mHorizontalScrollEdge = HORIZONTAL_EDGE_NONE;
+ }
+ // Finally actually translate the matrix
+ mSuppMatrix.postTranslate(deltaX, deltaY);
+ return true;
+ }
+
+ private int getImageViewWidth(ImageView imageView) {
+ return imageView.getWidth() - imageView.getPaddingLeft() - imageView.getPaddingRight();
+ }
+
+ private int getImageViewHeight(ImageView imageView) {
+ return imageView.getHeight() - imageView.getPaddingTop() - imageView.getPaddingBottom();
+ }
+
+ private void cancelFling() {
+ if (mCurrentFlingRunnable != null) {
+ mCurrentFlingRunnable.cancelFling();
+ mCurrentFlingRunnable = null;
+ }
+ }
+
+ private class AnimatedZoomRunnable implements Runnable {
+
+ private final float mFocalX, mFocalY;
+ private final long mStartTime;
+ private final float mZoomStart, mZoomEnd;
+
+ public AnimatedZoomRunnable(final float currentZoom, final float targetZoom,
+ final float focalX, final float focalY) {
+ mFocalX = focalX;
+ mFocalY = focalY;
+ mStartTime = System.currentTimeMillis();
+ mZoomStart = currentZoom;
+ mZoomEnd = targetZoom;
+ }
+
+ @Override
+ public void run() {
+ float t = interpolate();
+ float scale = mZoomStart + t * (mZoomEnd - mZoomStart);
+ float deltaScale = scale / getScale();
+ onGestureListener.onScale(deltaScale, mFocalX, mFocalY);
+ // We haven't hit our target scale yet, so post ourselves again
+ if (t < 1f) {
+ Compat.postOnAnimation(mImageView, this);
+ }
+ }
+
+ private float interpolate() {
+ float t = 1f * (System.currentTimeMillis() - mStartTime) / mZoomDuration;
+ t = Math.min(1f, t);
+ t = mInterpolator.getInterpolation(t);
+ return t;
+ }
+ }
+
+ private class FlingRunnable implements Runnable {
+
+ private final OverScroller mScroller;
+ private int mCurrentX, mCurrentY;
+
+ public FlingRunnable(Context context) {
+ mScroller = new OverScroller(context);
+ }
+
+ public void cancelFling() {
+ mScroller.forceFinished(true);
+ }
+
+ public void fling(int viewWidth, int viewHeight, int velocityX,
+ int velocityY) {
+ final RectF rect = getDisplayRect();
+ if (rect == null) {
+ return;
+ }
+ final int startX = Math.round(-rect.left);
+ final int minX, maxX, minY, maxY;
+ if (viewWidth < rect.width()) {
+ minX = 0;
+ maxX = Math.round(rect.width() - viewWidth);
+ } else {
+ minX = maxX = startX;
+ }
+ final int startY = Math.round(-rect.top);
+ if (viewHeight < rect.height()) {
+ minY = 0;
+ maxY = Math.round(rect.height() - viewHeight);
+ } else {
+ minY = maxY = startY;
+ }
+ mCurrentX = startX;
+ mCurrentY = startY;
+ // If we actually can move, fling the scroller
+ if (startX != maxX || startY != maxY) {
+ mScroller.fling(startX, startY, velocityX, velocityY, minX,
+ maxX, minY, maxY, 0, 0);
+ }
+ }
+
+ @Override
+ public void run() {
+ if (mScroller.isFinished()) {
+ return; // remaining post that should not be handled
+ }
+ if (mScroller.computeScrollOffset()) {
+ final int newX = mScroller.getCurrX();
+ final int newY = mScroller.getCurrY();
+ mSuppMatrix.postTranslate(mCurrentX - newX, mCurrentY - newY);
+ checkAndDisplayMatrix();
+ mCurrentX = newX;
+ mCurrentY = newY;
+ // Post On animation
+ Compat.postOnAnimation(mImageView, this);
+ }
+ }
+ }
+}
diff --git a/photoview/src/main/java/com/github/chrisbanes/photoview/Util.java b/photoview/src/main/java/com/github/chrisbanes/photoview/Util.java
new file mode 100644
index 0000000..2e3e5ad
--- /dev/null
+++ b/photoview/src/main/java/com/github/chrisbanes/photoview/Util.java
@@ -0,0 +1,37 @@
+package com.github.chrisbanes.photoview;
+
+import android.view.MotionEvent;
+import android.widget.ImageView;
+
+class Util {
+
+ static void checkZoomLevels(float minZoom, float midZoom,
+ float maxZoom) {
+ if (minZoom >= midZoom) {
+ throw new IllegalArgumentException(
+ "Minimum zoom has to be less than Medium zoom. Call setMinimumZoom() with a more appropriate value");
+ } else if (midZoom >= maxZoom) {
+ throw new IllegalArgumentException(
+ "Medium zoom has to be less than Maximum zoom. Call setMaximumZoom() with a more appropriate value");
+ }
+ }
+
+ static boolean hasDrawable(ImageView imageView) {
+ return imageView.getDrawable() != null;
+ }
+
+ static boolean isSupportedScaleType(final ImageView.ScaleType scaleType) {
+ if (scaleType == null) {
+ return false;
+ }
+ switch (scaleType) {
+ case MATRIX:
+ throw new IllegalStateException("Matrix scale type is not supported");
+ }
+ return true;
+ }
+
+ static int getPointerIndex(int action) {
+ return (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+ }
+}
diff --git a/photoview/src/test/java/com/github/chrisbanes/photoview/ExampleUnitTest.java b/photoview/src/test/java/com/github/chrisbanes/photoview/ExampleUnitTest.java
new file mode 100644
index 0000000..0dc410f
--- /dev/null
+++ b/photoview/src/test/java/com/github/chrisbanes/photoview/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package com.github.chrisbanes.photoview;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index c5a1f8a..2d6ba43 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,2 +1,25 @@
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+dependencyResolutionManagement {
+ repositories {
+ google()
+ mavenCentral()
+ maven { url 'https://jitpack.io' }
+ }
+}
+
include ':EasyPlayerPro'
include ':ijkplayer-java'
+include ':photoview'