version:1.9.4

fix:
update:作业页面完善
This commit is contained in:
2024-09-23 15:04:12 +08:00
parent 2c4af66ad8
commit 5a64967c57
143 changed files with 9023 additions and 509 deletions

View File

@@ -0,0 +1,2 @@
<manifest package="com.wgw.photo.preview"
xmlns:android="http://schemas.android.com/apk/res/android"/>

View File

@@ -0,0 +1,39 @@
/*
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.custom;
import android.annotation.TargetApi;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.view.View;
class Compat {
private static final int SIXTY_FPS_INTERVAL = 1000 / 60;
public static void postOnAnimation(View view, Runnable runnable) {
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
postOnAnimationJellyBean(view, runnable);
} else {
view.postDelayed(runnable, SIXTY_FPS_INTERVAL);
}
}
@TargetApi(16)
private static void postOnAnimationJellyBean(View view, Runnable runnable) {
view.postOnAnimation(runnable);
}
}

View File

@@ -0,0 +1,211 @@
/*
Copyright 2011, 2012 Chris Banes.
<p/>
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
<p/>
http://www.apache.org/licenses/LICENSE-2.0
<p/>
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.custom;
import android.content.Context;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.VelocityTracker;
import android.view.ViewConfiguration;
/**
* Does a whole lot of gesture detecting.
*/
class CustomGestureDetector {
private static final int INVALID_POINTER_ID = -1;
private int mActivePointerId = INVALID_POINTER_ID;
private int mActivePointerIndex = 0;
private final ScaleGestureDetector mDetector;
private VelocityTracker mVelocityTracker;
private boolean mIsDragging;
private float mLastTouchX;
private float mLastTouchY;
private final float mTouchSlop;
private final float mMinimumVelocity;
private final OnGestureListener mListener;
// new add
private boolean mZooming;
CustomGestureDetector(Context context, OnGestureListener listener) {
final ViewConfiguration configuration = ViewConfiguration
.get(context);
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
mTouchSlop = configuration.getScaledTouchSlop();
mListener = listener;
ScaleGestureDetector.OnScaleGestureListener mScaleListener = new ScaleGestureDetector.OnScaleGestureListener() {
@Override
public boolean onScale(ScaleGestureDetector detector) {
mZooming = true;
float scaleFactor = detector.getScaleFactor();
if (Float.isNaN(scaleFactor) || Float.isInfinite(scaleFactor))
return false;
if (scaleFactor >= 0) {
mListener.onScale(scaleFactor,
detector.getFocusX(), detector.getFocusY());
}
return true;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
// TODO: 11/23/20 wanggaowan 如果当前正在拖拽则不允许缩放
return !mIsDragging;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
// NO-OP
}
};
mDetector = new ScaleGestureDetector(context, mScaleListener);
}
private float getActiveX(MotionEvent ev) {
try {
return ev.getX(mActivePointerIndex);
} catch (Exception e) {
return ev.getX();
}
}
private float getActiveY(MotionEvent ev) {
try {
return ev.getY(mActivePointerIndex);
} catch (Exception e) {
return ev.getY();
}
}
public boolean isScaling() {
return mDetector.isInProgress() || mZooming;
}
public boolean isDragging() {
return mIsDragging;
}
public boolean onTouchEvent(MotionEvent ev) {
try {
mDetector.onTouchEvent(ev);
return processTouchEvent(ev);
} catch (IllegalArgumentException e) {
// Fix for support lib bug, happening when onDestroy is called
return true;
}
}
private boolean processTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
mActivePointerId = ev.getPointerId(0);
mVelocityTracker = VelocityTracker.obtain();
if (null != mVelocityTracker) {
mVelocityTracker.addMovement(ev);
}
mLastTouchX = getActiveX(ev);
mLastTouchY = getActiveY(ev);
mIsDragging = false;
mZooming = false;
break;
case MotionEvent.ACTION_MOVE:
final float x = getActiveX(ev);
final float y = getActiveY(ev);
final float dx = x - mLastTouchX, dy = y - mLastTouchY;
if (!mZooming && !mIsDragging && ev.getPointerCount() == 1) {
// TODO: 11/23/20 wanggaowan 如果已经开始缩放则不允许拖拽
// Use Pythagoras to see if drag length is larger than
// touch slop
mIsDragging = Math.sqrt((dx * dx) + (dy * dy)) >= mTouchSlop;
}
if (mIsDragging) {
mListener.onDrag(dx, dy);
mLastTouchX = x;
mLastTouchY = y;
if (null != mVelocityTracker) {
mVelocityTracker.addMovement(ev);
}
}
break;
case MotionEvent.ACTION_CANCEL:
mActivePointerId = INVALID_POINTER_ID;
// Recycle Velocity Tracker
if (null != mVelocityTracker) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
case MotionEvent.ACTION_UP:
mActivePointerId = INVALID_POINTER_ID;
if (mIsDragging) {
if (null != mVelocityTracker) {
mLastTouchX = getActiveX(ev);
mLastTouchY = getActiveY(ev);
// Compute velocity within the last 1000ms
mVelocityTracker.addMovement(ev);
mVelocityTracker.computeCurrentVelocity(1000);
final float vX = mVelocityTracker.getXVelocity(), vY = mVelocityTracker
.getYVelocity();
// If the velocity is greater than minVelocity, call
// listener
if (Math.max(Math.abs(vX), Math.abs(vY)) >= mMinimumVelocity) {
mListener.onFling(mLastTouchX, mLastTouchY, -vX, -vY);
}
}
}
// Recycle Velocity Tracker
if (null != mVelocityTracker) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
case MotionEvent.ACTION_POINTER_UP:
final int pointerIndex = Util.getPointerIndex(ev.getAction());
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mActivePointerId = ev.getPointerId(newPointerIndex);
mLastTouchX = ev.getX(newPointerIndex);
mLastTouchY = ev.getY(newPointerIndex);
}
break;
}
mActivePointerIndex = ev
.findPointerIndex(mActivePointerId != INVALID_POINTER_ID ? mActivePointerId
: 0);
return true;
}
}

View File

@@ -0,0 +1,27 @@
/*
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.custom;
interface OnGestureListener {
void onDrag(float dx, float dy);
void onFling(float startX, float startY, float velocityX,
float velocityY);
void onScale(float scaleFactor, float focusX, float focusY);
}

View File

@@ -0,0 +1,22 @@
package com.github.chrisbanes.photoview.custom;
import android.graphics.RectF;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
/**
* Interface definition for a callback to be invoked when the internal Matrix has changed for
* this View.
*/
@RestrictTo(Scope.LIBRARY)
public interface OnMatrixChangedListener {
/**
* Callback for when the Matrix displaying the Drawable has changed. This could be because
* the View's bounds have changed, or the user has zoomed.
*
* @param rect - Rectangle displaying the Drawable's new bounds.
*/
void onMatrixChanged(RectF rect);
}

View File

@@ -0,0 +1,18 @@
package com.github.chrisbanes.photoview.custom;
import android.widget.ImageView;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
/**
* Callback when the user tapped outside of the photo
*/
@RestrictTo(Scope.LIBRARY)
public interface OnOutsidePhotoTapListener {
/**
* The outside of the photo has been tapped
*/
void onOutsidePhotoTap(ImageView imageView);
}

View File

@@ -0,0 +1,26 @@
package com.github.chrisbanes.photoview.custom;
import android.widget.ImageView;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
/**
* A callback to be invoked when the Photo is tapped with a single
* tap.
*/
@RestrictTo(Scope.LIBRARY)
public interface OnPhotoTapListener {
/**
* A callback to receive where the user taps on a photo. You will only receive a callback if
* the user taps on the actual photo, tapping on 'whitespace' will be ignored.
*
* @param view ImageView the user tapped.
* @param x where the user tapped from the of the Drawable, as percentage of the
* Drawable width.
* @param y where the user tapped from the top of the Drawable, as percentage of the
* Drawable height.
*/
void onPhotoTap(ImageView view, float x, float y);
}

View File

@@ -0,0 +1,21 @@
package com.github.chrisbanes.photoview.custom;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
/**
* Interface definition for callback to be invoked when attached ImageView scale changes
*/
@RestrictTo(Scope.LIBRARY)
public interface OnScaleChangedListener {
/**
* Callback for when the scale changes
*
* @param scaleFactor the scale factor (less than 1 for zoom out, greater than 1 for zoom in)
* @param focusX focal point X position
* @param focusY focal point Y position
*/
void onScaleChange(float scaleFactor, float focusX, float focusY);
}

View File

@@ -0,0 +1,25 @@
package com.github.chrisbanes.photoview.custom;
import android.view.MotionEvent;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
/**
* A callback to be invoked when the ImageView is flung with a single
* touch
*/
@RestrictTo(Scope.LIBRARY)
public interface OnSingleFlingListener {
/**
* A callback to receive where the user flings on a ImageView. You will receive a callback if
* the user flings anywhere on the view.
*
* @param e1 MotionEvent the user first touch.
* @param e2 MotionEvent the user last touch.
* @param velocityX distance of user's horizontal fling.
* @param velocityY distance of user's vertical fling.
*/
boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
}

View File

@@ -0,0 +1,21 @@
package com.github.chrisbanes.photoview.custom;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
/**
* Interface definition for a callback to be invoked when the photo is experiencing a drag event
*/
@RestrictTo(Scope.LIBRARY)
public interface OnViewDragListener {
/**
* Callback for when the photo is experiencing a drag event. This cannot be invoked when the
* user is scaling.
*
* @param dx The change of the coordinates in the x-direction
* @param dy The change of the coordinates in the y-direction
* @return 返回值表示是否消费此次事件
*/
boolean onDrag(float dx, float dy);
}

View File

@@ -0,0 +1,20 @@
package com.github.chrisbanes.photoview.custom;
import android.view.View;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
@RestrictTo(Scope.LIBRARY)
public interface OnViewTapListener {
/**
* A callback to receive where the user taps on a ImageView. You will receive a callback if
* the user taps anywhere on the view, tapping on 'whitespace' will not be ignored.
*
* @param view - View the user tapped.
* @param x - where the user tapped from the left of the View.
* @param y - where the user tapped from the top of the View.
*/
void onViewTap(View view, float x, float y);
}

View File

@@ -0,0 +1,264 @@
/*
Copyright 2011, 2012 Chris Banes.
<p>
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
<p>
http://www.apache.org/licenses/LICENSE-2.0
<p>
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.custom;
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.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.appcompat.widget.AppCompatImageView;
/**
* A zoomable ImageView. See {@link PhotoViewAttacher} for most of the details on how the zooming
* is accomplished
*
* copy form v2.3.0 by wanggaowan
*
* 本来打算引用库的,但是有些逻辑不修改源码,难以实现。仅限本框架使用,有些逻辑只适用于当前库
*/
@SuppressWarnings("unused")
@RestrictTo(Scope.LIBRARY)
public class PhotoView extends AppCompatImageView {
protected 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);
}
}

View File

@@ -0,0 +1,874 @@
/*
Copyright 2011, 2012 Chris Banes.
<p>
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
<p>
http://www.apache.org/licenses/LICENSE-2.0
<p>
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.custom;
import android.annotation.SuppressLint;
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;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
/**
* 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
*/
@RestrictTo(Scope.LIBRARY)
public class PhotoViewAttacher implements View.OnTouchListener,
View.OnLayoutChangeListener {
public static float DEFAULT_MAX_SCALE = 3.0f;
public static float DEFAULT_MID_SCALE = 1.75f;
public static float DEFAULT_MIN_SCALE = 1.0f;
private static final int DEFAULT_ZOOM_DURATION = 200;
// 图片左右边缘包含在ImageView宽度内
public static final int HORIZONTAL_EDGE_INSIDE = -2;
// 图片左右边缘超出ImageView宽度
public static final int HORIZONTAL_EDGE_OUTSIDE = -1;
// 图片左边缘靠近ImageView左边缘
public static final int HORIZONTAL_EDGE_LEFT = 0;
// 图片右边缘靠近ImageView右边缘
public static final int HORIZONTAL_EDGE_RIGHT = 1;
// 图片左右边缘靠近ImageView左右边缘此时图片宽度等于ImageView宽度
public static final int HORIZONTAL_EDGE_BOTH = 2;
// 图片上下边缘包含在ImageView高度内
public static final int VERTICAL_EDGE_INSIDE = -2;
// 图片上下边缘超出ImageView高度
public static final int VERTICAL_EDGE_OUTSIDE = -1;
// 图片上边缘靠近ImageView上边缘
public static final int VERTICAL_EDGE_TOP = 0;
// 图片下边缘靠近ImageView下边缘
public static final int VERTICAL_EDGE_BOTTOM = 1;
// 图片上下边缘靠近ImageView上下边缘此时图片高度等于ImageView高度
public static final int VERTICAL_EDGE_BOTH = 2;
private static final 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 final 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 final OnGestureListener onGestureListener = new OnGestureListener() {
@Override
public void onDrag(float dx, float dy) {
// TODO: 11/23/20 wanggaowan 该逻辑已经调整到CustomGestureDetector处理
// if (mScaleDragDetector.isScaling()) {
// return; // Do not drag if we are already scaling
// }
mSuppMatrix.postTranslate(dx, dy);
checkAndDisplayMatrix();
if (mOnViewDragListener != null) {
boolean consume = mOnViewDragListener.onDrag(dx, dy);
if (consume) {
return;
}
}
/*
* 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) {
// TODO: 11/29/20 wanggaowan 逻辑判断调整,增加 mHorizontalScrollEdge == HORIZONTAL_EDGE_INSIDE时也让父类拦截
if (mHorizontalScrollEdge == HORIZONTAL_EDGE_BOTH
|| mHorizontalScrollEdge == HORIZONTAL_EDGE_INSIDE // 说明图片实际宽度小于View的宽度
|| (mHorizontalScrollEdge == HORIZONTAL_EDGE_LEFT && dx >= 1f)
|| (mHorizontalScrollEdge == HORIZONTAL_EDGE_RIGHT && dx <= -1f)
// 本项目只结合ViewPager,只有左右滑动冲突,因此不做垂直处理
// || mVerticalScrollEdge == VERTICAL_EDGE_BOTH
// || (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) {
if (getScale() < mMaxScale || scaleFactor < 1f) {
if (mScaleChangeListener != null) {
mScaleChangeListener.onScaleChange(scaleFactor, focusX, focusY);
}
mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);
checkAndDisplayMatrix();
}
}
};
@SuppressLint("ClickableViewAccessibility")
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;
}
} else if (ev.getAction() == MotionEvent.ACTION_DOWN) {
handled = mOnClickListener != null;
} else if (ev.getAction() == MotionEvent.ACTION_UP) {
if (mOnClickListener != null) {
mOnClickListener.onClick(v);
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
// TODO: 11/23/20 wanggaowan 预览需要设置倍率为0~1因此不做倍率限制
// 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;
}
// TODO: 11/29/20 wanggaowan 调整逻辑只有图片高度等于view高度才设置为VERTICAL_EDGE_BOTH
if (height == viewHeight) {
mVerticalScrollEdge = VERTICAL_EDGE_BOTH;
} else {
mVerticalScrollEdge = VERTICAL_EDGE_INSIDE;
}
} 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_OUTSIDE;
}
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;
}
// TODO: 11/29/20 wanggaowan 调整逻辑只有图片宽度等于view宽度才设置为HORIZONTAL_EDGE_BOTH
if (width == viewWidth) {
mHorizontalScrollEdge = HORIZONTAL_EDGE_BOTH;
} else {
mHorizontalScrollEdge = HORIZONTAL_EDGE_INSIDE;
}
} 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_OUTSIDE;
}
// 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;
}
}
public int getVerticalScrollEdge() {
return mVerticalScrollEdge;
}
public int getHorizontalScrollEdge() {
return mHorizontalScrollEdge;
}
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);
}
}
}
}

View File

@@ -0,0 +1,37 @@
package com.github.chrisbanes.photoview.custom;
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;
}
}

View File

@@ -0,0 +1,175 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* 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.wgw.photo.preview;
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.graphics.Outline;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.cardview.widget.CardView;
import androidx.transition.Transition;
import androidx.transition.TransitionValues;
/**
* 图形变换,目前只有圆角变换
*/
class ChangeShape extends Transition {
private static final String PROPNAME_RADIUS = "android:ChangeShape:radius";
private static final String[] sTransitionProperties = {
PROPNAME_RADIUS,
};
private static class Property extends android.util.Property<View, Float> {
private ViewOutlineProvider mProvider;
private final float startValue;
private final float endValue;
private float offset;
/**
* A constructor that takes an identifying name and {@link #getType() type} for the property.
*
* @param startValue 属性改变的起始值
* @param endValue 属性改变的结束值
*/
public Property(float startValue, float endValue) {
super(Float.class, "radius");
this.startValue = startValue;
this.endValue = endValue;
float maxValue = Math.max(startValue, endValue);
offset = 0.01f;
if (maxValue >= 20 && maxValue <= 30) {
offset += 0.005f + (30 - maxValue) * 0.001f;
} else {
offset = 0.2f;
}
}
@Override
public void set(View view, Float value) {
if (value == null || (startValue <= endValue && value < endValue * offset) // 退出预览此时圆角小于endValue * offset不做处理
|| (startValue > endValue && value < startValue * offset)) { // 打开预览此时圆角小于startValue * offset不做处理
// TODO: 12/21/20 wanggaowan 如果不做此判断,那么动画结束时会闪屏,目前不清楚为什么出现该情况
return;
}
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
if (mProvider == null) {
mProvider = new ViewOutlineProvider();
}
mProvider.setRadius(value);
view.setOutlineProvider(mProvider);
} else if (view instanceof CardView) {
((CardView) view).setRadius(value);
}
}
@Override
public Float get(View object) {
return null;
}
}
@RequiresApi(api = VERSION_CODES.LOLLIPOP)
static class ViewOutlineProvider extends android.view.ViewOutlineProvider {
private float radius = 0f;
public void setRadius(float radius) {
this.radius = radius;
}
@Override
public void getOutline(View view, Outline outline) {
// 采用此种裁剪方式需要内容填满View比如ImageView控件为正方形设置缩放模式非完全填充比如fit_center,
// 那么图片不是正方形时此时图片无法完全占满ImageView控件而此时裁剪是对ImageView进行裁剪最终裁剪效果与
// 先裁剪图片再设置图片将不一致比如Glide框架.后续待完善
int left = view.getLeft();
int top = view.getTop();
int width = view.getWidth();
int height = view.getHeight();
outline.setRoundRect(left, top,
left + width,
top + height,
radius);
}
}
private final float startRadius;
private final float endRadius;
public ChangeShape(float startRadius, float endRadius) {
this.startRadius = startRadius;
this.endRadius = endRadius;
}
@Nullable
@Override
public String[] getTransitionProperties() {
return sTransitionProperties;
}
@Override
public void captureStartValues(@NonNull TransitionValues transitionValues) {
transitionValues.values.put(PROPNAME_RADIUS, startRadius);
}
@Override
public void captureEndValues(@NonNull TransitionValues transitionValues) {
transitionValues.values.put(PROPNAME_RADIUS, endRadius);
}
@Override
@Nullable
public Animator createAnimator(@NonNull final ViewGroup sceneRoot,
@Nullable TransitionValues startValues, @Nullable TransitionValues endValues) {
if (startValues == null || endValues == null) {
return null;
}
Float startRadius = (Float) startValues.values.get(PROPNAME_RADIUS);
Float endRadius = (Float) endValues.values.get(PROPNAME_RADIUS);
if (startRadius == null || endRadius == null || startRadius.equals(endRadius)) {
return null;
}
return ofFloat(endValues.view, startRadius, endRadius);
}
private ObjectAnimator ofFloat(View target, float startValue, float endValue) {
if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
target.setClipToOutline(true);
return ObjectAnimator.ofFloat(target, new Property(startValue, endValue), startValue, endValue);
}
if (target instanceof CardView) {
return ObjectAnimator.ofFloat((CardView) target, "radius", startValue, endValue);
}
return null;
}
}

View File

@@ -0,0 +1,124 @@
package com.wgw.photo.preview;
import android.graphics.drawable.Drawable;
import com.wgw.photo.preview.interfaces.ImageLoader;
import com.wgw.photo.preview.interfaces.OnDismissListener;
import com.wgw.photo.preview.interfaces.OnLongClickListener;
import java.util.List;
import androidx.annotation.Nullable;
import androidx.viewpager.widget.ViewPager.OnPageChangeListener;
/**
* 预览配置
*
* @author Created by wanggaowan on 11/20/20 10:33 PM
*/
public class Config {
@Nullable
public ImageLoader imageLoader;
public int indicatorType = IndicatorType.DOT;
public int maxIndicatorDot = 9;
public int selectIndicatorColor = 0xFFFFFFFF/*白色*/;
public int normalIndicatorColor = 0xFFAAAAAA/*灰色*/;
@Nullable
public Drawable progressDrawable/*ProgressBar默认样式*/;
@Nullable
public Integer progressColor;
public long delayShowProgressTime = 100;
@Nullable
public OnLongClickListener onLongClickListener;
@Nullable
public OnDismissListener onDismissListener;
@Nullable
public Boolean fullScreen/*默认跟随打开预览的界面显示模式*/;
@Nullable
public List<?> sources;
public int defaultShowPosition = 0;
@Nullable
public Long animDuration/*打开和退出预览时的过度动画时间*/;
/**
* 图形变换类型,可选值参考{@link ShapeTransformType}
*/
@Nullable
public Integer shapeTransformType;
/**
* 图形变换设置为{@link ShapeTransformType#ROUND_RECT}时圆角半径
*/
public int shapeCornerRadius = 0;
/**
* 是否展示缩略图蒙层,如果设置为{@code true},则预览动画执行时,缩略图不显示,预览更沉浸
*/
public boolean showThumbnailViewMask = true;
/**
* 是否在打开预览动画执行开始的时候执行状态栏隐藏/显示操作。如果该值设置为true
* 那么预览动画打开时,由于状态栏退出/进入有动画,可能导致预览动画卡顿(预览动画时间大于状态栏动画时间时发生)。
*/
public boolean openAnimStartHideOrShowStatusBar = false;
/**
* 是否在关闭预览动画执行开始的时候执行状态栏显示/隐藏操作。如果该值设置为false
* 那么预览动画结束后,对于非沉浸式界面,由于要显示/隐藏状态栏,此时会有强烈的顿挫感。
* 因此设置为{@code false}时,建议采用沉浸式
*/
boolean exitAnimStartHideOrShowStatusBar = true;
/**
* 图片切换监听
*/
public OnPageChangeListener onPageChangeListener;
public void apply(Config config) {
if (config == null) {
return;
}
this.imageLoader = config.imageLoader;
this.indicatorType = config.indicatorType;
this.maxIndicatorDot = config.maxIndicatorDot;
this.selectIndicatorColor = config.selectIndicatorColor;
this.normalIndicatorColor = config.normalIndicatorColor;
this.progressDrawable = config.progressDrawable;
this.progressColor = config.progressColor;
this.delayShowProgressTime = config.delayShowProgressTime;
this.onLongClickListener = config.onLongClickListener;
this.onDismissListener = config.onDismissListener;
this.fullScreen = config.fullScreen;
this.sources = config.sources;
this.defaultShowPosition = config.defaultShowPosition;
this.animDuration = config.animDuration;
this.shapeTransformType = config.shapeTransformType;
this.shapeCornerRadius = config.shapeCornerRadius;
this.showThumbnailViewMask = config.showThumbnailViewMask;
this.openAnimStartHideOrShowStatusBar = config.openAnimStartHideOrShowStatusBar;
this.exitAnimStartHideOrShowStatusBar = config.exitAnimStartHideOrShowStatusBar;
this.onPageChangeListener = config.onPageChangeListener;
}
void release() {
this.imageLoader = null;
this.indicatorType = IndicatorType.DOT;
this.maxIndicatorDot = 9;
this.selectIndicatorColor = 0xFFFFFFFF;
this.normalIndicatorColor = 0xFFAAAAAA;
this.progressDrawable = null;
this.progressColor = null;
this.delayShowProgressTime = 100;
this.onLongClickListener = null;
this.onDismissListener = null;
this.fullScreen = null;
this.sources = null;
this.defaultShowPosition = 0;
this.animDuration = null;
this.shapeTransformType = null;
this.shapeCornerRadius = 0;
this.showThumbnailViewMask = true;
this.openAnimStartHideOrShowStatusBar = false;
this.exitAnimStartHideOrShowStatusBar = true;
this.onPageChangeListener = null;
}
}

View File

@@ -0,0 +1,263 @@
package com.wgw.photo.preview;
import android.annotation.SuppressLint;
import android.content.res.ColorStateList;
import android.graphics.RectF;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.ProgressBar;
import com.wgw.photo.preview.PhotoPreviewHelper.OnOpenListener;
import com.wgw.photo.preview.interfaces.OnImageLongClickListener;
import com.wgw.photo.preview.interfaces.OnLongClickListener;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.viewpager.widget.PagerAdapter;
/**
* 预览图片适配器
*
* @author Created by wanggaowan on 3/24/21 11:17 AM
*/
class ImagePagerAdapter extends PagerAdapter {
private final ShareData ShareData;
private final PhotoPreviewHelper mHelper;
public ImagePagerAdapter(PhotoPreviewHelper helper, ShareData shareData) {
ShareData = shareData;
mHelper = helper;
}
@Override
public int getCount() {
List<?> sources = ShareData.config.sources;
return sources == null ? 0 : sources.size();
}
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return object instanceof ViewHolder && view == ((ViewHolder) object).root;
}
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
return new ViewHolder(mHelper, ShareData, container, position);
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
ViewHolder holder = (ViewHolder) object;
holder.destroy();
container.removeView(holder.root);
}
@Override
public int getItemPosition(@NonNull Object object) {
if (getCount() == 0) {
return POSITION_NONE;
}
return POSITION_UNCHANGED;
}
static class ViewHolder {
View root;
private final PhotoView photoView;
private final ProgressBar loading;
private final PhotoPreviewHelper helper;
private final ShareData shareData;
// 记录预览界面图片缩放倍率为1时图片真实绘制大小
private final float[] mNoScaleImageActualSize = new float[2];
private PhotoPreviewHelper.OnOpenListener openListener;
private PhotoPreviewHelper.OnExitListener exitListener;
@SuppressLint("InflateParams")
public ViewHolder(PhotoPreviewHelper helper, ShareData shareData, ViewGroup container, int position) {
this.helper = helper;
this.shareData = shareData;
root = LayoutInflater.from(container.getContext()).inflate(R.layout.fragment_preview, container, false);
container.addView(root);
root.setTag(position);
root.setTag(R.id.view_holder, this);
photoView = root.findViewById(R.id.photoView);
loading = root.findViewById(R.id.loading);
setPhotoViewVisibility();
photoView.setPhotoPreviewHelper(helper);
photoView.setStartView(position == 0);
List<?> sources = shareData.config.sources;
int size = sources == null ? 0 : sources.size();
photoView.setEndView(position == size - 1);
initEvent(position);
initLoading();
loadImage(photoView, position);
}
/**
* 根据预览动画设置大图显示与隐藏
*/
private void setPhotoViewVisibility() {
if (helper.isOpenAnimEnd()) {
photoView.setVisibility(View.VISIBLE);
}
openListener = new OnOpenListener() {
@Override
public void onStartPre() {
}
@Override
public void onStart() {
photoView.setVisibility(View.INVISIBLE);
}
@Override
public void onEnd() {
photoView.setVisibility(View.VISIBLE);
}
};
helper.addOnOpenListener(openListener);
exitListener = new PhotoPreviewHelper.OnExitListener() {
@Override
public void onStartPre() {
}
@Override
public void onStart() {
photoView.setVisibility(View.INVISIBLE);
}
@Override
public void onExit() {
}
};
helper.addOnExitListener(exitListener);
}
private void destroy() {
root.setTag(null);
helper.removeOnOpenListener(openListener);
helper.removeOnExitListener(exitListener);
}
private void initEvent(int position) {
photoView.setOnLongClickListener(v -> {
if (shareData != null) {
OnImageLongClickListener listener = shareData.onLongClickListener;
if (listener != null) {
listener.onLongClick(position, photoView);
}
}
return true;
});
photoView.setOnClickListener(v -> helper.exit());
}
/**
* 初始化loading
*/
private void initLoading() {
photoView.setOnMatrixChangeListener(this :: getPreviewDrawableSize);
photoView.setImageChangeListener(drawable -> {
if (drawable != null) {
loading.setVisibility(View.GONE);
}
});
if (shareData.config.delayShowProgressTime < 0) {
loading.setVisibility(View.GONE);
return;
}
if (shareData.config.progressDrawable != null) {
loading.setIndeterminateDrawable(shareData.config.progressDrawable);
}
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP && shareData.config.progressColor != null) {
loading.setIndeterminateTintList(ColorStateList.valueOf(shareData.config.progressColor));
}
// loading.setVisibility(!helper.isAnimStart() && shareData.config.delayShowProgressTime == 0 ? View.VISIBLE : View.GONE);
loading.setVisibility(shareData.config.delayShowProgressTime == 0 ? View.VISIBLE : View.GONE);
if (shareData.config.delayShowProgressTime > 0) {
// 监听指定延迟后图片是否加载成功
photoView.postDelayed(() -> {
if (photoView.getDrawable() == null) {
loading.setVisibility(View.VISIBLE);
}
}, shareData.config.delayShowProgressTime);
}
}
/**
* 获取预览图片真实大小,由于图片刚进入时,需要等待绘制,所以可能不能及时获取到准确的大小
*/
private void getPreviewDrawableSize(RectF rectF) {
if (photoView.getScale() != 1) {
return;
}
// 用于退出时计算移动后最终图像坐标使用
// 刚设置图片就获取,此时可能获取不成功
mNoScaleImageActualSize[0] = rectF.width();
mNoScaleImageActualSize[1] = rectF.height();
if (mNoScaleImageActualSize[0] > 0) {
// 计算最大缩放倍率,屏幕大小的三倍
double ceil = Math.ceil(root.getWidth() / mNoScaleImageActualSize[0]);
float maxScale = (float) (ceil * 3f);
if (maxScale < photoView.getMaximumScale()) {
return;
}
float midScale = (maxScale + photoView.getMinimumScale()) / 2;
photoView.setScaleLevels(photoView.getMinimumScale(), midScale, maxScale);
}
}
/**
* 加载图片
*/
private void loadImage(ImageView imageView, int position) {
if (shareData.config.imageLoader != null) {
if (shareData.config.sources != null && position < shareData.config.sources.size() && position >= 0) {
shareData.config.imageLoader.onLoadImage(position, shareData.config.sources.get(position), imageView);
} else {
shareData.config.imageLoader.onLoadImage(position, null, imageView);
}
}
}
public PhotoView getPhotoView() {
return photoView;
}
public ProgressBar getLoading() {
return loading;
}
public float[] getNoScaleImageActualSize() {
return mNoScaleImageActualSize;
}
}
}

View File

@@ -0,0 +1,28 @@
package com.wgw.photo.preview;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import androidx.annotation.IntDef;
/**
* 图片指示器类型
*
* @author Created by wanggaowan on 2019/3/6 0006 17:15
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.SOURCE)
@IntDef({IndicatorType.DOT, IndicatorType.TEXT})
public @interface IndicatorType {
/**
* 圆点,如果图片多于{@link Config#maxIndicatorDot}则采用{@link #TEXT}
*/
int DOT = 0;
/**
* 文本
*/
int TEXT = 1;
}

View File

@@ -0,0 +1,70 @@
package com.wgw.photo.preview;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import com.github.chrisbanes.photoview.custom.PhotoView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.viewpager.widget.ViewPager;
/**
* 捕获触摸异常,主要是{@link PhotoView}与Viewpager结合使用有bug目前作者未修复给出捕获异常解决方案
*
* @author Created by wanggaowan on 2019/2/28 0028 11:44
*/
class NoTouchExceptionViewPager extends ViewPager {
private boolean mTouchEnable;
public NoTouchExceptionViewPager(@NonNull Context context) {
super(context);
}
public NoTouchExceptionViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
try {
if (!mTouchEnable) {
return false;
}
return super.dispatchTouchEvent(ev);
} catch (Exception e) {
return false;
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
try {
return super.onInterceptTouchEvent(ev);
} catch (Exception e) {
return false;
}
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent ev) {
try {
if (ev.getPointerCount() > 1) {
return false;
}
return super.onTouchEvent(ev);
} catch (Exception e) {
return false;
}
}
public void setTouchEnable(boolean touchEnable) {
mTouchEnable = touchEnable;
}
}

View File

@@ -0,0 +1,712 @@
package com.wgw.photo.preview;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.view.View;
import android.widget.ImageView;
import com.wgw.photo.preview.interfaces.IFindThumbnailView;
import com.wgw.photo.preview.interfaces.ImageLoader;
import com.wgw.photo.preview.interfaces.OnDismissListener;
import com.wgw.photo.preview.interfaces.OnLongClickListener;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.Lifecycle.Event;
import androidx.lifecycle.Lifecycle.State;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;
import androidx.viewpager.widget.ViewPager.OnPageChangeListener;
/**
* 图片预览,支持预览单张,多张图片。
* 每个Activity持有同一个预览对象因此{@link PhotoPreview#PhotoPreview(FragmentActivity)}、
* {@link PhotoPreview#with(FragmentActivity)}对于同一个activity操作的是同一个对象
*
* @author Created by wanggaowan on 2019/2/26 0026 16:55
*/
public class PhotoPreview {
/**
* 全局图片加载器
*/
private static ImageLoader globalImageLoader = null;
/**
* 图片预览池一个Activity持有一个预览对象
*/
private static final Map<String, WeakReference<PreviewDialogFragment>> DIALOG_POOL = new HashMap<>();
private final FragmentActivity mFragmentActivity;
private final Fragment mFragment;
private final Config mConfig;
/**
* 设置图片全局加载器
*/
public static void setGlobalImageLoader(ImageLoader imageLoader) {
globalImageLoader = imageLoader;
}
private static PreviewDialogFragment getDialog(final FragmentActivity activity, boolean noneCreate) {
Fragment fragmentByTag = activity.getSupportFragmentManager().findFragmentByTag(PreviewDialogFragment.FRAGMENT_TAG);
if (fragmentByTag instanceof PreviewDialogFragment) {
return (PreviewDialogFragment) fragmentByTag;
}
final String name = activity.toString();
WeakReference<PreviewDialogFragment> reference = DIALOG_POOL.get(name);
PreviewDialogFragment fragment = reference == null ? null : reference.get();
if (fragment == null) {
if (noneCreate) {
fragment = new PreviewDialogFragment();
reference = new WeakReference<>(fragment);
DIALOG_POOL.put(name, reference);
activity.getLifecycle().addObserver(new LifecycleObserver() {
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
public void onDestroy() {
activity.getLifecycle().removeObserver(this);
DIALOG_POOL.remove(name);
}
});
} else {
DIALOG_POOL.remove(name);
}
}
return fragment;
}
private static PreviewDialogFragment getDialog(final Fragment parentFragment, boolean noneCreate) {
Fragment fragmentByTag = parentFragment.getChildFragmentManager().findFragmentByTag(PreviewDialogFragment.FRAGMENT_TAG);
if (fragmentByTag instanceof PreviewDialogFragment) {
return (PreviewDialogFragment) fragmentByTag;
}
final String name = parentFragment.toString();
WeakReference<PreviewDialogFragment> reference = DIALOG_POOL.get(name);
PreviewDialogFragment fragment = reference == null ? null : reference.get();
if (fragment == null) {
if (noneCreate) {
fragment = new PreviewDialogFragment();
reference = new WeakReference<>(fragment);
DIALOG_POOL.put(name, reference);
parentFragment.getLifecycle().addObserver(new LifecycleObserver() {
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
public void onDestroy() {
parentFragment.getLifecycle().removeObserver(this);
DIALOG_POOL.remove(name);
}
});
} else {
DIALOG_POOL.remove(name);
}
}
return fragment;
}
/**
* 创建构建器,链式调用
*/
public static Builder with(@NonNull FragmentActivity activity) {
Objects.requireNonNull(activity);
return new Builder(activity);
}
/**
* 创建构建器,链式调用
*/
public static Builder with(@NonNull Fragment fragment) {
Objects.requireNonNull(fragment);
return new Builder(fragment);
}
/**
* 创建构建器,链式调用
*/
public static Builder with(@NonNull Object activityOrFragment) {
Objects.requireNonNull(activityOrFragment);
if (activityOrFragment instanceof FragmentActivity) {
return new Builder((FragmentActivity) activityOrFragment);
} else if (activityOrFragment instanceof Fragment) {
return new Builder((Fragment) activityOrFragment);
}
throw new IllegalArgumentException("activityOrFragment must be FragmentActivity or Fragment");
}
public PhotoPreview(@NonNull Builder builder) {
Objects.requireNonNull(builder);
mFragmentActivity = builder.activity;
mFragment = builder.fragment;
mConfig = builder.mConfig;
}
/**
* @param activity 当前图片预览所处Activity
*/
public PhotoPreview(@NonNull FragmentActivity activity) {
Objects.requireNonNull(activity);
mFragmentActivity = activity;
mFragment = null;
mConfig = new Config();
}
/**
* @param fragment 当前图片预览所处fragment
*/
public PhotoPreview(@NonNull Fragment fragment) {
Objects.requireNonNull(fragment);
mFragmentActivity = null;
mFragment = fragment;
mConfig = new Config();
}
/**
* 应用其它配置
*/
public void setConfig(Config config) {
mConfig.apply(config);
}
/**
* 设置图片加载器
*/
public void setImageLoader(ImageLoader imageLoader) {
mConfig.imageLoader = imageLoader;
}
/**
* 设置图片长按监听
*/
public void setLongClickListener(OnLongClickListener listener) {
mConfig.onLongClickListener = listener;
}
/**
* 设置预览关闭监听
*/
public void setOnDismissListener(OnDismissListener listener) {
mConfig.onDismissListener = listener;
}
/**
* 设置图片数量指示器样式,默认{@link IndicatorType#DOT},如果图片数量超过9则不论设置何种模式均为{@link IndicatorType#TEXT}
*/
public void setIndicatorType(@IndicatorType int indicatorType) {
mConfig.indicatorType = indicatorType;
}
/**
* 多图预览时,当前预览的图片指示器颜色
*/
public void setSelectIndicatorColor(@ColorInt int color) {
mConfig.selectIndicatorColor = color;
}
/**
* 多图预览时,非当前预览的图片指示器颜色
*/
public void setNormalIndicatorColor(@ColorInt int color) {
mConfig.normalIndicatorColor = color;
}
/**
* 在调用{@link ImageLoader#onLoadImage(int, Object, ImageView)}时延迟展示loading框的时间
* < 0:不展示,=0:立即显示,>0:延迟给定时间显示默认延迟100ms显示如果在此时间内加载完成则不显示否则显示
*/
public void setDelayShowProgressTime(long delay) {
mConfig.delayShowProgressTime = delay;
}
/**
* 设置图片加载框的颜色API >= 21配置才生效
*/
public void setProgressColor(@ColorInt int progressColor) {
mConfig.progressColor = progressColor;
}
/**
* 设置图片加载框Drawable
*/
public void setProgressDrawable(Drawable progressDrawable) {
mConfig.progressDrawable = progressDrawable;
}
/**
* 是否全屏预览,如果全屏预览,在某些手机上(特别是异形屏)可能会出全屏非全屏切换顿挫
*
* @param fullScreen <ul>
* <li>null:跟随打开预览的Activity是否全屏决定预览界面是否全屏</li>
* <li>true:全屏预览</li>
* <li>null:非全屏预览</li>
* </ul>
*/
public void setFullScreen(Boolean fullScreen) {
mConfig.fullScreen = fullScreen;
}
/**
* 设置打开预览界面默认展示位置
*/
public void setDefaultShowPosition(int position) {
mConfig.defaultShowPosition = position;
}
/**
* 设置图片地址
*/
public void setSource(@NonNull Object... sources) {
Objects.requireNonNull(sources);
setSource(Arrays.asList(sources));
}
/**
* 设置图片地址
*/
public void setSource(@NonNull List<?> sources) {
Objects.requireNonNull(sources);
mConfig.sources = sources;
}
/**
* 设置动画执行时间
*
* @param duration <ul>
* <li>null: 使用默认动画时间</li>
* <li><=0: 不执行动画</li>
* </ul>
*/
public void setAnimDuration(Long duration) {
mConfig.animDuration = duration;
}
/**
* 当{@link #setIndicatorType(int)}为{@link IndicatorType#DOT}时设置DOT最大数量
* 如果{@link #setSource(List)}或{@link #setSource(Object...)}超出最大值,则采用{@link IndicatorType#TEXT}
*/
public void setMaxIndicatorDot(int maxSize) {
mConfig.maxIndicatorDot = maxSize;
}
/**
* 设置缩略图图形变换类型,比如缩列图是圆形或圆角矩形
*
* @param shapeTransformType 目前仅提供{@link ShapeTransformType#CIRCLE}和{@link ShapeTransformType#ROUND_RECT}
*/
public void setShapeTransformType(@ShapeTransformType int shapeTransformType) {
mConfig.shapeTransformType = shapeTransformType;
}
/**
* 仅当{@link #setShapeTransformType(int)}设置为{@link ShapeTransformType#ROUND_RECT}时,此值配置缩略图圆角矩形圆角半径
*/
public void setShapeCornerRadius(int radius) {
mConfig.shapeCornerRadius = radius;
}
/**
* 是否展示缩略图蒙层,如果设置为{@code true},则预览动画执行时,缩略图不显示,预览更沉浸
*
* @param show 是否显示蒙层,默认{@code true}
*/
public void setShowThumbnailViewMask(boolean show) {
mConfig.showThumbnailViewMask = show;
}
/**
* 是否在打开预览动画执行开始的时候执行状态栏隐藏/显示操作。如果该值设置为true
* 那么预览动画打开时,由于状态栏退出/进入有动画,可能导致预览动画卡顿(预览动画时间大于状态栏动画时间时发生)。
*
* @param doOP 是否执行操作,默认{@code false}
*/
public void setOpenAnimStartHideOrShowStatusBar(boolean doOP) {
mConfig.openAnimStartHideOrShowStatusBar = doOP;
}
// /**
// * 是否在关闭预览动画执行开始的时候执行状态栏显示/隐藏操作。如果该值设置为false
// * 那么预览动画结束后,对于非沉浸式界面,由于要显示/隐藏状态栏,此时会有强烈的顿挫感。
// * 因此设置为{@code false}时,建议采用沉浸式
// *
// * @param doOP 是否执行操作,默认{@code true}
// */
// public void setExitAnimStartHideOrShowStatusBar(boolean doOP) {
// mConfig.exitAnimStartHideOrShowStatusBar = doOP;
// }
/**
* 多图预览时,左右滑动监听
*/
public void setOnPageChangeListener(OnPageChangeListener listener) {
mConfig.onPageChangeListener = listener;
}
/**
* 不设置缩略图,预览界面打开关闭将只有从中心缩放动画
*/
public void show() {
show((View) null);
}
/**
* 展示预览
*
* @param thumbnailView 缩略图{@link View},建议传{@link ImageView}对象,这样过度效果更好。
* 如果多图预览,请使用{@link #show(IFindThumbnailView)}。如果thumbnailView
* 是在列表中且预览过程可能发生thumbnailView变更请使用{@link #show(IFindThumbnailView)}。
*/
public void show(final View thumbnailView) {
show(thumbnailView, null);
}
/**
* 展示预览
*
* @param findThumbnailView 多图预览时,打开和关闭预览时用于提供缩略图对象,用于过度动画
*/
public void show(final IFindThumbnailView findThumbnailView) {
show(null, findThumbnailView);
}
private void show(final View thumbnailView, final IFindThumbnailView findThumbnailView) {
correctConfig();
final PreviewDialogFragment fragment
= mFragment == null ? getDialog(Objects.requireNonNull(mFragmentActivity), true) : getDialog(mFragment, true);
final Lifecycle lifecycle = mFragment == null ? mFragmentActivity.getLifecycle() : mFragment.getLifecycle();
if (lifecycle.getCurrentState().isAtLeast(State.CREATED)) {
Context context = mFragment == null ? mFragmentActivity : mFragment.getContext();
FragmentManager fragmentManager
= mFragment == null ? mFragmentActivity.getSupportFragmentManager() : mFragment.getChildFragmentManager();
if (thumbnailView != null) {
fragment.show(context, fragmentManager, mConfig, thumbnailView);
} else {
fragment.show(context, fragmentManager, mConfig, findThumbnailView);
}
} else if (lifecycle.getCurrentState() != State.DESTROYED) {
lifecycle.addObserver(new LifecycleObserver() {
@OnLifecycleEvent(Event.ON_CREATE)
public void onCreate() {
lifecycle.removeObserver(this);
Context context = mFragment == null ? mFragmentActivity : mFragment.getContext();
FragmentManager fragmentManager
= mFragment == null ? mFragmentActivity.getSupportFragmentManager() : mFragment.getChildFragmentManager();
if (thumbnailView != null) {
fragment.show(context, fragmentManager, mConfig, thumbnailView);
} else {
fragment.show(context, fragmentManager, mConfig, findThumbnailView);
}
}
});
}
}
/**
* 纠正可能的错误配置
*/
private void correctConfig() {
int sourceSize = mConfig.sources == null ? 0 : mConfig.sources.size();
if (sourceSize == 0) {
mConfig.defaultShowPosition = 0;
} else if (mConfig.defaultShowPosition >= sourceSize) {
mConfig.defaultShowPosition = sourceSize - 1;
} else if (mConfig.defaultShowPosition < 0) {
mConfig.defaultShowPosition = 0;
}
if (mConfig.imageLoader == null) {
mConfig.imageLoader = globalImageLoader;
}
if (mConfig.shapeTransformType != null
&& mConfig.shapeTransformType != ShapeTransformType.CIRCLE
&& mConfig.shapeTransformType != ShapeTransformType.ROUND_RECT) {
mConfig.shapeTransformType = null;
}
}
/**
* 关闭预览界面
*/
public void dismiss() {
dismiss(true);
}
/**
* 关闭预览界面
*
* @param callBack 是否需要执行{@link OnDismissListener}回调
*/
public void dismiss(boolean callBack) {
PreviewDialogFragment fragment
= mFragment == null ? getDialog(Objects.requireNonNull(mFragmentActivity), false) : getDialog(mFragment, false);
if (fragment != null) {
fragment.dismiss(callBack);
}
}
public static class Builder {
final FragmentActivity activity;
final Fragment fragment;
Config mConfig;
private Builder(FragmentActivity activity) {
this.activity = activity;
this.fragment = null;
mConfig = new Config();
}
private Builder(Fragment fragment) {
this.fragment = fragment;
this.activity = null;
mConfig = new Config();
}
/**
* 应用其它配置
*/
public Builder config(Config config) {
mConfig.apply(config);
return this;
}
/**
* 图片加载器
*/
public Builder imageLoader(ImageLoader imageLoader) {
mConfig.imageLoader = imageLoader;
return this;
}
/**
* 多图预览时,指示器类型
*
* @param indicatorType {@link IndicatorType#DOT}、{@link IndicatorType#TEXT}
*/
public Builder indicatorType(@IndicatorType int indicatorType) {
mConfig.indicatorType = indicatorType;
return this;
}
/**
* 多图预览时,当前预览的图片指示器颜色
*/
public Builder selectIndicatorColor(@ColorInt int color) {
mConfig.selectIndicatorColor = color;
return this;
}
/**
* 多图预览时,非当前预览的图片指示器颜色
*/
public Builder normalIndicatorColor(@ColorInt int color) {
mConfig.normalIndicatorColor = color;
return this;
}
/**
* 设置图片加载loading drawable
*/
public Builder progressDrawable(Drawable progressDrawable) {
mConfig.progressDrawable = progressDrawable;
return this;
}
/**
* 设置图片加载loading颜色该颜色作用于{@link #setProgressDrawable(Drawable)}上
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public Builder progressColor(@ColorInt int color) {
mConfig.progressColor = color;
return this;
}
/**
* 在调用{@link ImageLoader#onLoadImage(int, Object, ImageView)}时延迟展示loading框的时间
* < 0:不展示,=0:立即显示,>0:延迟给定时间显示默认延迟100ms显示如果在此时间内加载完成则不显示否则显示
*/
public Builder delayShowProgressTime(long delay) {
mConfig.delayShowProgressTime = delay;
return this;
}
/**
* 设置预览界面长按点检监听
*/
public Builder onLongClickListener(OnLongClickListener listener) {
mConfig.onLongClickListener = listener;
return this;
}
/**
* 设置预览关闭监听
*/
public Builder onDismissListener(OnDismissListener listener) {
mConfig.onDismissListener = listener;
return this;
}
/**
* 是否全屏预览,如果全屏预览,在某些手机上(特别是异形屏)可能会出全屏非全屏切换顿挫
*
* @param fullScreen <ul>
* <li>null:跟随打开预览的Activity是否全屏决定预览界面是否全屏</li>
* <li>true:全屏预览</li>
* <li>null:非全屏预览</li>
* </ul>
*/
public Builder fullScreen(Boolean fullScreen) {
mConfig.fullScreen = fullScreen;
return this;
}
/**
* 数据源
*/
public Builder sources(@NonNull Object... sources) {
Objects.requireNonNull(sources);
return sources(Arrays.asList(sources));
}
/**
* 数据源
*/
public Builder sources(@NonNull List<?> sources) {
Objects.requireNonNull(sources);
mConfig.sources = sources;
return this;
}
/**
* 设置打开预览界面初始展示位置
*/
public Builder defaultShowPosition(int position) {
mConfig.defaultShowPosition = position;
return this;
}
/**
* 设置动画执行时间
*
* @param duration <ul>
* <li>null: 使用默认动画时间</li>
* <li><=0: 不执行动画</li>
* </ul>
*/
public Builder animDuration(Long duration) {
mConfig.animDuration = duration;
return this;
}
/**
* 当{@link #indicatorType(int)}为{@link IndicatorType#DOT}时设置DOT最大数量
* 如果{@link #sources(List)}或{@link #sources(Object...)}超出最大值,则采用{@link IndicatorType#TEXT}
*/
public Builder maxIndicatorDot(int maxSize) {
mConfig.maxIndicatorDot = maxSize;
return this;
}
/**
* 设置缩略图图形变换类型,比如缩列图是圆形或圆角矩形
*
* @param shapeTransformType 目前仅提供{@link ShapeTransformType#CIRCLE}和{@link ShapeTransformType#ROUND_RECT}
*/
public Builder shapeTransformType(@ShapeTransformType int shapeTransformType) {
mConfig.shapeTransformType = shapeTransformType;
return this;
}
/**
* 仅当{@link #shapeTransformType(int)}设置为{@link ShapeTransformType#ROUND_RECT}时,此值配置缩略图圆角矩形圆角半径
*/
public Builder shapeCornerRadius(int radius) {
mConfig.shapeCornerRadius = radius;
return this;
}
/**
* 是否展示缩略图蒙层,如果设置为{@code true},则预览动画执行时,缩略图不显示,预览更沉浸
*
* @param show 是否显示蒙层,默认{@code true}
*/
public Builder showThumbnailViewMask(boolean show) {
mConfig.showThumbnailViewMask = show;
return this;
}
/**
* 是否在打开预览动画执行开始的时候执行状态栏隐藏/显示操作。如果该值设置为true
* 那么预览动画打开时,由于状态栏退出/进入有动画,可能导致预览动画卡顿(预览动画时间大于状态栏动画时间时发生)。
*
* @param doOP 是否执行操作,默认{@code false}
*/
public Builder openAnimStartHideOrShowStatusBar(boolean doOP) {
mConfig.openAnimStartHideOrShowStatusBar = doOP;
return this;
}
// /**
// * 是否在关闭预览动画执行开始的时候执行状态栏显示/隐藏操作。如果该值设置为false
// * 那么预览动画结束后,对于非沉浸式界面,由于要显示/隐藏状态栏,此时会有强烈的顿挫感。
// * 因此设置为{@code false}时,建议采用沉浸式
// *
// * @param doOP 是否执行操作,默认{@code true}
// */
// public Builder exitAnimStartHideOrShowStatusBar(boolean doOP) {
// mConfig.exitAnimStartHideOrShowStatusBar = doOP;
// return this;
// }
/**
* 多图预览时,左右滑动监听
*/
public Builder onPageChangeListener(OnPageChangeListener listener) {
mConfig.onPageChangeListener = listener;
return this;
}
public PhotoPreview build() {
return new PhotoPreview(this);
}
/**
* 不设置缩略图,预览界面打开关闭将只有从中心缩放动画
*/
public void show() {
build().show();
}
/**
* 展示预览
*
* @param thumbnailView 缩略图{@link View},建议传{@link ImageView}对象,这样过度效果更好。
* 如果多图预览,请使用{@link #show(IFindThumbnailView)}。如果thumbnailView
* 是在列表中且预览过程可能发生thumbnailView变更请使用{@link #show(IFindThumbnailView)}。
*/
public void show(final View thumbnailView) {
build().show(thumbnailView, null);
}
/**
* 展示预览
*
* @param findThumbnailView 多图预览时,打开和关闭预览时用于提供缩略图对象,用于过度动画
*/
public void show(final IFindThumbnailView findThumbnailView) {
build().show(null, findThumbnailView);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,340 @@
package com.wgw.photo.preview;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import android.view.ViewParent;
import android.widget.Scroller;
import com.github.chrisbanes.photoview.custom.OnScaleChangedListener;
import com.github.chrisbanes.photoview.custom.OnViewDragListener;
import com.github.chrisbanes.photoview.custom.PhotoViewAttacher;
/**
* A zoomable ImageView. See {@link PhotoViewAttacher} for most of the details on how the zooming
* is accomplished.<br>
*
* @author Created by wanggaowan on 11/19/20 11:23 PM
*/
class PhotoView extends com.github.chrisbanes.photoview.custom.PhotoView implements OnScaleChangedListener, OnViewDragListener {
private static final int RESET_ANIM_TIME = 100;
private final Scroller mScroller;
// 是否是预览的第一个View
private boolean mStartView = false;
// 是否是预览的最后一个View
private boolean mEndView = false;
private PhotoPreviewHelper mHelper;
private ImageChangeListener mImageChangeListener;
private final ViewConfiguration mViewConfiguration;
// 当前是否正在拖拽
private boolean mDragging;
private boolean mBgAnimStart;
// 透明度
private int mIntAlpha = 255;
// 记录缩放后垂直方向边界判定值
private int mScaleVerticalScrollEdge = PhotoViewAttacher.VERTICAL_EDGE_INSIDE;
// 记录缩放后水平方向边界判定值
private int mScaleHorizontalScrollEdge = PhotoViewAttacher.HORIZONTAL_EDGE_INSIDE;
private OnScaleChangedListener mOnScaleChangedListener;
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);
super.setOnScaleChangeListener(this);
setOnViewDragListener(this);
mScroller = new Scroller(context);
mViewConfiguration = ViewConfiguration.get(context);
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
onFingerUp();
break;
}
return super.dispatchTouchEvent(event);
}
private void onFingerUp() {
mDragging = false;
if (getScale() > 1) {
if (Math.abs(getScrollX()) > 0 || Math.abs(getScrollY()) > 0) {
reset();
}
return;
}
// 这里恢复位置和透明度
if (mIntAlpha != 255 && getScale() < 0.8) {
mHelper.exit();
} else {
reset();
}
}
private void reset() {
mIntAlpha = 255;
mBgAnimStart = true;
mHelper.dragScaleChange(1f);
mHelper.doViewBgAnim(Color.BLACK, RESET_ANIM_TIME, new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mBgAnimStart = false;
}
});
mScroller.startScroll(
getScrollX(),
getScrollY(),
-getScrollX(),
-getScrollY(), RESET_ANIM_TIME
);
invalidate();
}
@Override
public void setOnScaleChangeListener(OnScaleChangedListener onScaleChangedListener) {
mOnScaleChangedListener = onScaleChangedListener;
}
@Override
public void onScaleChange(float scaleFactor, float focusX, float focusY) {
mScaleVerticalScrollEdge = attacher.getVerticalScrollEdge();
mScaleHorizontalScrollEdge = attacher.getHorizontalScrollEdge();
if (mOnScaleChangedListener != null) {
mOnScaleChangedListener.onScaleChange(scaleFactor, focusX, focusY);
}
}
@Override
public boolean onDrag(float dx, float dy) {
boolean intercept = mBgAnimStart
|| Math.sqrt((dx * dx) + (dy * dy)) < mViewConfiguration.getScaledTouchSlop()
|| !hasVisibleDrawable();
if (!mDragging && intercept) {
return false;
}
if (getScale() > 1) {
return dragWhenScaleThanOne(dx, dy);
}
if (!mDragging && Math.abs(dx) > Math.abs(dy)) {
return false;
}
if (!mDragging) {
// 执行拖拽操作,请求父类不要拦截请求
ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
mDragging = true;
float scale = getScale();
// 移动图像
scrollBy(((int) -dx), ((int) -dy));
float scrollY = getScrollY();
if (scrollY >= 0) {
scale = 1f;
mIntAlpha = 255;
} else {
scale -= dy * 0.001f;
mIntAlpha -= dy * 0.03;
}
if (scale > 1) {
scale = 1f;
} else if (scale < 0) {
scale = 0f;
}
if (mIntAlpha < 200) {
mIntAlpha = 200;
} else if (mIntAlpha > 255) {
mIntAlpha = 255;
}
mHelper.mRootViewBgMask.getBackground().setAlpha(mIntAlpha);
mHelper.showThumbnailViewMask(mIntAlpha >= 255);
if (scrollY < 0 && scale >= 0.6) {
// 更改大小
setScale(scale);
mHelper.dragScaleChange(scale);
}
return true;
}
/**
* 处理图片如果超出控件大小时的滑动
*/
private boolean dragWhenScaleThanOne(float dx, float dy) {
boolean dxBigDy = Math.abs(dx) > Math.abs(dy);
if (mDragging) {
dx *= 0.2f;
dy *= 0.2f;
int scrollX = (int) (getScrollX() - dx);
int scrollY = (int) (getScrollY() - dy);
int width = (int) (getWidth() * 0.2);
int height = (int) (getHeight() * 0.2);
if (Math.abs(scrollX) > width) {
dx = 0;
}
if (Math.abs(scrollY) > height) {
dy = 0;
}
if (dxBigDy) {
dy = 0;
} else {
dx = 0;
}
// 移动图像
scrollBy(((int) -dx), ((int) -dy));
return true;
} else {
int verticalScrollEdge = attacher.getVerticalScrollEdge();
int horizontalScrollEdge = attacher.getHorizontalScrollEdge();
boolean isTop = verticalScrollEdge == PhotoViewAttacher.VERTICAL_EDGE_TOP
|| verticalScrollEdge == PhotoViewAttacher.VERTICAL_EDGE_BOTH;
boolean isBottom = verticalScrollEdge == PhotoViewAttacher.VERTICAL_EDGE_BOTTOM
|| verticalScrollEdge == PhotoViewAttacher.VERTICAL_EDGE_BOTH;
boolean isStart = horizontalScrollEdge == PhotoViewAttacher.HORIZONTAL_EDGE_LEFT
|| horizontalScrollEdge == PhotoViewAttacher.HORIZONTAL_EDGE_BOTH;
boolean isEnd = horizontalScrollEdge == PhotoViewAttacher.HORIZONTAL_EDGE_RIGHT
|| horizontalScrollEdge == PhotoViewAttacher.HORIZONTAL_EDGE_BOTH;
boolean isVerticalScroll = !dxBigDy && ((isTop && dy > 0) || (isBottom && dy < 0));
boolean isHorizontalScroll = dxBigDy && ((mStartView && isStart && dx > 0) || (mEndView && isEnd && dx < 0));
if ((isVerticalScroll && mScaleVerticalScrollEdge == PhotoViewAttacher.VERTICAL_EDGE_OUTSIDE)
|| (isHorizontalScroll && mScaleHorizontalScrollEdge == PhotoViewAttacher.VERTICAL_EDGE_OUTSIDE)) {
// 执行拖拽操作,请求父类不要拦截请求
ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
mDragging = true;
// 移动图像
scrollBy(((int) -dx), ((int) -dy));
return true;
}
}
return false;
}
/**
* 是否存在可观察的图像
*/
private boolean hasVisibleDrawable() {
if (getDrawable() == null) {
return false;
}
Drawable drawable = getDrawable();
// 获得ImageView中Image的真实宽高
int dw = drawable.getBounds().width();
int dh = drawable.getBounds().height();
return dw > 0 && dh > 0;
}
@Override
public float getAlpha() {
return mIntAlpha;
}
@Override
public void setImageDrawable(Drawable drawable) {
super.setImageDrawable(drawable);
if (mImageChangeListener != null) {
mImageChangeListener.onChange(getDrawable());
}
}
@Override
public void setImageResource(int resId) {
super.setImageResource(resId);
if (mImageChangeListener != null) {
mImageChangeListener.onChange(getDrawable());
}
}
@Override
public void setImageURI(Uri uri) {
super.setImageURI(uri);
if (mImageChangeListener != null) {
mImageChangeListener.onChange(getDrawable());
}
}
@Override
public void setImageBitmap(Bitmap bm) {
super.setImageBitmap(bm);
if (mImageChangeListener != null) {
mImageChangeListener.onChange(getDrawable());
}
}
void setPhotoPreviewHelper(PhotoPreviewHelper helper) {
mHelper = helper;
}
void setImageChangeListener(ImageChangeListener listener) {
mImageChangeListener = listener;
}
public void setStartView(boolean isStartView) {
mStartView = isStartView;
}
public void setEndView(boolean isEndView) {
mEndView = isEndView;
}
/**
* 设置的图片发生更改
*/
interface ImageChangeListener {
/**
* 图片发生更改,但是此时并不一定绘制到界面
*/
void onChange(Drawable drawable);
}
}

View File

@@ -0,0 +1,61 @@
package com.wgw.photo.preview;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.appcompat.widget.AppCompatImageView;
/**
* 仅用于辅助加载,获取图片
*
* @author Created by wanggaowan on 12/10/20 3:08 PM
*/
@RestrictTo(Scope.LIBRARY)
public class PreloadImageView extends AppCompatImageView {
private DrawableLoadListener mListener;
public PreloadImageView(@NonNull Context context) {
super(context);
}
public PreloadImageView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public PreloadImageView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onDraw(Canvas canvas) {
}
@Override
public void setImageDrawable(@Nullable Drawable drawable) {
if (mListener != null) {
mListener.onLoad(drawable);
}
}
@Override
public void setImageURI(@Nullable Uri uri) {
super.setImageURI(uri);
}
public void setDrawableLoadListener(DrawableLoadListener listener) {
mListener = listener;
}
interface DrawableLoadListener {
void onLoad(Drawable drawable);
}
}

View File

@@ -0,0 +1,688 @@
package com.wgw.photo.preview;
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Configuration;
import android.graphics.Color;
import android.graphics.PorterDuff.Mode;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowInsetsController;
import android.view.WindowManager.LayoutParams;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.wgw.photo.preview.PreloadImageView.DrawableLoadListener;
import com.wgw.photo.preview.interfaces.IFindThumbnailView;
import com.wgw.photo.preview.interfaces.OnDismissListener;
import com.wgw.photo.preview.util.SpannableString;
import com.wgw.photo.preview.util.Utils;
import com.wgw.photo.preview.util.notch.CutOutMode;
import com.wgw.photo.preview.util.notch.NotchAdapterUtils;
import java.util.Objects;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.Lifecycle.State;
import androidx.viewpager.widget.ViewPager.SimpleOnPageChangeListener;
/**
* 预览界面根布局
*
* @author Created by wanggaowan on 2019/3/20 0020 17:45
*/
@RestrictTo(Scope.LIBRARY)
public class PreviewDialogFragment extends DialogFragment {
static final String FRAGMENT_TAG = "PhotoPreview:59bd2d0f-8474-451d-9bee-3cca00182b31";
FrameLayout mRootView;
NoTouchExceptionViewPager mViewPager;
private LinearLayout mLlDotIndicator;
private ImageView mIvSelectDot;
private TextView mTvTextIndicator;
private FrameLayout mLlCustom;
/**
* 用于当前Fragment与预览Fragment之间的通讯
*/
@NonNull
ShareData mShareData;
/**
* 当前展示预览图下标
*/
private int mCurrentPagerIndex = 0;
/**
* 是否添加到Activity
*/
private boolean mAdd;
/**
* 是否已经Dismiss
*/
private boolean mDismiss;
/**
* 界面关闭时是否需要调用{@link OnDismissListener}
*/
private boolean mCallOnDismissListener = true;
/**
* 是否在当前界面OnDismiss调用{@link OnDismissListener}
*/
private boolean mCallOnDismissListenerInThisOnDismiss;
/**
* 是否自己主动调用Dismiss(包括用户主动关闭、程序主动调用dismiss相关方法)
*/
private Boolean mSelfDismissDialog;
private PhotoPreviewHelper mPhotoPreviewHelper;
public PreviewDialogFragment() {
setCancelable(false);
// 全屏处理
setStyle(STYLE_NO_TITLE, 0);
mShareData = new ShareData();
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
if (savedInstanceState != null) {
// 说明是被回收后恢复,此时不恢复
super.onActivityCreated(savedInstanceState);
return;
}
if (getDialog() == null || getDialog().getWindow() == null) {
super.onActivityCreated(null);
return;
}
Window window = getDialog().getWindow();
// 无论是否全屏显示,都允许内容绘制到耳朵区域
NotchAdapterUtils.adapter(window, CutOutMode.ALWAYS);
super.onActivityCreated(null);
// 以下代码必须在super.onActivityCreated之后调用才有效
boolean isParentFullScreen = isParentFullScreen();
window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
// 需要设置这个才能设置状态栏和导航栏颜色,此时布局内容可绘制到状态栏之下
window.addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.setStatusBarColor(Color.TRANSPARENT);
window.setNavigationBarColor(Color.TRANSPARENT);
}
LayoutParams lp = window.getAttributes();
lp.dimAmount = 0;
lp.flags |= LayoutParams.FLAG_DIM_BEHIND;
if (mShareData.config.fullScreen == null) {
// 跟随父窗口
if (isParentFullScreen) {
lp.flags |= LayoutParams.FLAG_FULLSCREEN;
} else {
lp.flags |= LayoutParams.FLAG_FORCE_NOT_FULLSCREEN;
}
}
lp.width = LayoutParams.MATCH_PARENT;
lp.height = LayoutParams.MATCH_PARENT;
window.setAttributes(lp);
// 沉浸式处理
// OPPO ANDROID P 之后的系统需要设置沉浸式配合异形屏适配才能将内容绘制到耳朵区域
// 防止系统栏隐藏时内容区域大小发生变化
int uiFlags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
// window全屏显示但状态栏不会被隐藏状态栏依然可见内容可绘制到状态栏之下
uiFlags |= View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
// window全屏显示但导航栏不会被隐藏导航栏依然可见内容可绘制到导航栏之下
uiFlags |= View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
// 对于OPPO ANDROID P 之后的系统,一定需要清除此标志,否则异形屏无法绘制到耳朵区域下面
window.clearFlags(LayoutParams.FLAG_TRANSLUCENT_STATUS);
// 设置之后不会通过触摸屏幕调出导航栏
// uiFlags |= View.SYSTEM_UI_FLAG_IMMERSIVE; // 通过系统上滑或者下滑拉出导航栏后不会自动隐藏
uiFlags |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; // 通过系统上滑或者下滑拉出导航栏后会自动隐藏
}
if (mShareData.config.fullScreen == null && isParentFullScreen) {
// 隐藏状态栏
uiFlags |= View.INVISIBLE;
}
View decorView = window.getDecorView();
decorView.setSystemUiVisibility(uiFlags);
decorView.setPadding(0, 0, 0, 0);
initEvent();
initViewData();
}
/**
* 初始化是否全屏展示
*/
void initFullScreen(boolean start) {
if (mShareData.config.fullScreen == null) {
return;
}
boolean isParentFullScreen = isParentFullScreen();
if (isParentFullScreen == mShareData.config.fullScreen) {
return;
}
Dialog dialog = getDialog();
if (dialog == null) {
return;
}
Window window = dialog.getWindow();
if (window == null) {
return;
}
if (start) {
if (mShareData.config.fullScreen) {
window.clearFlags(LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
window.addFlags(LayoutParams.FLAG_FULLSCREEN);
View decorView = window.getDecorView();
decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() | View.INVISIBLE);
} else {
window.clearFlags(LayoutParams.FLAG_FULLSCREEN);
window.addFlags(LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
}
} else if (isParentFullScreen()) {
window.clearFlags(LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
window.addFlags(LayoutParams.FLAG_FULLSCREEN);
View decorView = window.getDecorView();
decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() | View.INVISIBLE);
} else {
window.clearFlags(LayoutParams.FLAG_FULLSCREEN);
window.addFlags(LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
// if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) {
// // android13 之后需要主动显示dialog的statusBar否则缩略图所处activity需要等到预览界面关闭才展示状态栏
// // 这样会出现强烈的顿挫感
// View decorView = window.getDecorView();
// WindowInsetsController controller = decorView.getWindowInsetsController();
// controller.show(WindowInsets.Type.statusBars());
// }
}
}
@SuppressLint("InflateParams")
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
if (mRootView == null) {
mRootView = (FrameLayout) inflater.inflate(R.layout.view_preview_root, null);
mViewPager = mRootView.findViewById(R.id.viewpager);
mLlDotIndicator = mRootView.findViewById(R.id.ll_dot_indicator_photo_preview);
mIvSelectDot = mRootView.findViewById(R.id.iv_select_dot_photo_preview);
mTvTextIndicator = mRootView.findViewById(R.id.tv_text_indicator_photo_preview);
mLlCustom = mRootView.findViewById(R.id.fl_custom);
}
if (mSelfDismissDialog == null && savedInstanceState == null) {
mDismiss = false;
} else if (savedInstanceState != null || !mSelfDismissDialog) {
// 被回收后恢复,则关闭弹窗
dismissAllowingStateLoss();
}
return mRootView;
}
@Override
public void onDestroyView() {
super.onDestroyView();
mLlCustom.removeAllViews();
if (mRootView != null) {
ViewParent parent = mRootView.getParent();
if (parent instanceof ViewGroup) {
// 为了下次重用mRootView
((ViewGroup) parent).removeView(mRootView);
}
}
if (mSelfDismissDialog == null) {
mSelfDismissDialog = false;
}
}
@Override
public void onDismiss(@NonNull DialogInterface dialog) {
super.onDismiss(dialog);
mSelfDismissDialog = null;
mAdd = false;
mDismiss = true;
if (mShareData.config.onDismissListener != null
&& mCallOnDismissListenerInThisOnDismiss
&& mCallOnDismissListener) {
mShareData.config.onDismissListener.onDismiss();
}
mShareData.release();
}
/**
* 父窗口是否全屏显示
*/
boolean isParentFullScreen() {
FragmentActivity activity = getActivity();
if (activity == null || activity.getWindow() == null) {
return true;
}
// 跟随打开预览界面的显示状态
return (activity.getWindow().getAttributes().flags & LayoutParams.FLAG_FULLSCREEN) != 0;
}
/**
* 父窗口是否高亮状态栏,此时字体是黑色的
*/
private boolean isParentLightStatusBar() {
if (VERSION.SDK_INT < VERSION_CODES.M) {
return false;
}
FragmentActivity activity = getActivity();
if (activity == null || activity.getWindow() == null) {
return false;
}
View decorView = activity.getWindow().getDecorView();
if (decorView == null) {
return false;
}
// 跟随打开预览界面的显示状态
return (decorView.getSystemUiVisibility() & View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) != 0;
}
/**
* 父窗口是否高亮状态栏,此时字体是黑色的
*/
private void setLightStatusBar() {
if (VERSION.SDK_INT < VERSION_CODES.M) {
return;
}
Dialog dialog = getDialog();
if (dialog == null) {
return;
}
Window window = dialog.getWindow();
if (window == null) {
return;
}
View decorView = window.getDecorView();
decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
}
public void show(Context context, FragmentManager fragmentManager, Config config, View thumbnailView) {
mShareData.applyConfig(config);
mShareData.findThumbnailView = null;
mShareData.thumbnailView = thumbnailView;
showInner(context, fragmentManager);
}
public void show(Context context, FragmentManager fragmentManager, Config config, IFindThumbnailView findThumbnailView) {
mShareData.applyConfig(config);
mShareData.thumbnailView = null;
mShareData.findThumbnailView = findThumbnailView;
showInner(context, fragmentManager);
}
private void showInner(Context context, FragmentManager fragmentManager) {
// 预加载启动图图片
PreloadImageView imageView = new PreloadImageView(context);
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(displayMetrics.widthPixels, displayMetrics.heightPixels);
imageView.setLayoutParams(params);
imageView.setDrawableLoadListener(drawable -> {
mShareData.preLoadDrawable = drawable;
DrawableLoadListener listener = mShareData.preDrawableLoadListener;
if (listener != null) {
listener.onLoad(drawable);
}
});
loadImage(imageView);
mSelfDismissDialog = null;
mShareData.showNeedAnim = getDialog() == null || !getDialog().isShowing();
if (isStateSaved()) {
dismissAllowingStateLoss();
} else if (isAdded() || mAdd) {
// isAdded()并不一定靠谱可能存在一定的延时性为此当前对象在创建时已经优先返回fragmentManager存在的对象
// 对象获取逻辑查看PhotoPreview getDialog相关方法
if (!getLifecycle().getCurrentState().isAtLeast(State.INITIALIZED)) {
dismissAllowingStateLoss();
} else if (mRootView != null) {
initViewData();
initEvent();
return;
}
}
mAdd = true;
showNow(fragmentManager, FRAGMENT_TAG);
}
/**
* 加载图片
*/
private void loadImage(ImageView imageView) {
if (mShareData.config.imageLoader != null) {
int mPosition = mShareData.config.defaultShowPosition;
if (mShareData.config.sources != null && mPosition < mShareData.config.sources.size() && mPosition >= 0) {
mShareData.config.imageLoader.onLoadImage(mPosition, mShareData.config.sources.get(mPosition), imageView);
} else {
mShareData.config.imageLoader.onLoadImage(mPosition, null, imageView);
}
}
}
/**
* 退出预览
*
* @param callBack 是否需要执行{@link OnDismissListener}回调
*/
public void dismiss(boolean callBack) {
if (mSelfDismissDialog != null || mDismiss || !getLifecycle().getCurrentState().isAtLeast(State.CREATED)) {
return;
}
mSelfDismissDialog = true;
mCallOnDismissListener = callBack;
if (mPhotoPreviewHelper == null) {
mCallOnDismissListenerInThisOnDismiss = true;
dismissAllowingStateLoss();
} else {
boolean exit = mPhotoPreviewHelper.exit();
if (!exit) {
mCallOnDismissListenerInThisOnDismiss = true;
dismissAllowingStateLoss();
}
}
}
private void initViewData() {
mCurrentPagerIndex = mShareData.config.defaultShowPosition;
mPhotoPreviewHelper = new PhotoPreviewHelper(this, mCurrentPagerIndex);
mLlDotIndicator.setVisibility(View.GONE);
mIvSelectDot.setVisibility(View.GONE);
mTvTextIndicator.setVisibility(View.GONE);
setIndicatorVisible(false);
prepareIndicator();
prepareViewPager();
}
private void initEvent() {
mShareData.onOpenListener = new PhotoPreviewHelper.OnOpenListener() {
@Override
public void onStartPre() {
if (Boolean.TRUE.equals(mShareData.config.fullScreen)) {
if (isParentLightStatusBar()) {
setLightStatusBar();
}
}
if (mShareData.config.openAnimStartHideOrShowStatusBar) {
initFullScreen(true);
}
}
@Override
public void onStart() {
// 对于强制指定是否全屏需要此处初始化状态栏隐藏逻辑否则在MIUI系统上从嵌套多层的Fragment预览会出现卡顿
mViewPager.setTouchEnable(false);
}
@Override
public void onEnd() {
if (!mShareData.config.openAnimStartHideOrShowStatusBar) {
initFullScreen(true);
}
setIndicatorVisible(true);
mViewPager.setTouchEnable(true);
}
};
mShareData.onExitListener = new PhotoPreviewHelper.OnExitListener() {
@Override
public void onStartPre() {
if (isParentLightStatusBar()) {
setLightStatusBar();
}
if (mShareData.config.exitAnimStartHideOrShowStatusBar) {
initFullScreen(false);
}
}
@Override
public void onStart() {
setIndicatorVisible(false);
mViewPager.setTouchEnable(false);
}
@Override
public void onExit() {
if (!mShareData.config.exitAnimStartHideOrShowStatusBar) {
initFullScreen(false);
}
mViewPager.setTouchEnable(true);
if (mSelfDismissDialog != null) {
return;
}
mSelfDismissDialog = true;
OnDismissListener onDismissListener = mShareData.config.onDismissListener;
dismissAllowingStateLoss();
if (onDismissListener != null && mCallOnDismissListener) {
onDismissListener.onDismiss();
}
}
};
mShareData.onLongClickListener = (pos, v) -> {
if (mShareData.config.onLongClickListener != null) {
return mShareData.config.onLongClickListener.onLongClick(pos, mLlCustom, v);
}
return false;
};
}
/**
* 准备用于展示预览图的ViePager数据
*/
private void prepareViewPager() {
// 每次预览的时候如果不动态修改每个ViewPager的Id
// 那么预览多张图片时如果第一次点击位置1预览然后关闭再点击位置2预览图片打开的还是位置1预览图
mViewPager.setTouchEnable(false);
if (mViewPager.getId() == R.id.view_pager_id) {
mViewPager.setId(R.id.view_pager_id_next);
} else {
mViewPager.setId(R.id.view_pager_id);
}
ImagePagerAdapter adapter = new ImagePagerAdapter(mPhotoPreviewHelper, mShareData);
mViewPager.addOnPageChangeListener(new SimpleOnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
if (mLlDotIndicator.getVisibility() == View.VISIBLE) {
float dx = mLlDotIndicator.getChildAt(1).getX() - mLlDotIndicator.getChildAt(0).getX();
mIvSelectDot.setTranslationX((position * dx) + positionOffset * dx);
}
if (mShareData.config.onPageChangeListener != null) {
mShareData.config.onPageChangeListener.onPageScrolled(position, positionOffset, positionOffsetPixels);
}
}
@Override
public void onPageSelected(int position) {
mCurrentPagerIndex = position;
mPhotoPreviewHelper.setPosition(position);
// 设置文字版本当前页的值
if (mTvTextIndicator.getVisibility() == View.VISIBLE) {
updateTextIndicator();
}
if (mShareData.config.onPageChangeListener != null) {
mShareData.config.onPageChangeListener.onPageSelected(position);
}
}
@Override
public void onPageScrollStateChanged(int state) {
super.onPageScrollStateChanged(state);
if (mShareData.config.onPageChangeListener != null) {
mShareData.config.onPageChangeListener.onPageScrollStateChanged(state);
}
}
});
mViewPager.setAdapter(adapter);
mViewPager.setCurrentItem(mCurrentPagerIndex);
}
/**
* 准备滑动指示器数据
*/
private void prepareIndicator() {
int sourceSize = mShareData.config.sources == null ? 0 : mShareData.config.sources.size();
if (sourceSize >= 2 && sourceSize <= mShareData.config.maxIndicatorDot
&& IndicatorType.DOT == mShareData.config.indicatorType) {
mLlDotIndicator.removeAllViews();
Context context = requireContext();
if (mShareData.config.selectIndicatorColor != 0xFFFFFFFF) {
Drawable drawable = mIvSelectDot.getDrawable();
GradientDrawable gradientDrawable;
if (drawable instanceof GradientDrawable) {
gradientDrawable = (GradientDrawable) drawable;
} else {
gradientDrawable = (GradientDrawable) ContextCompat.getDrawable(context, R.drawable.selected_dot);
}
Objects.requireNonNull(gradientDrawable).setColorFilter(mShareData.config.selectIndicatorColor, Mode.SRC_OVER);
mIvSelectDot.setImageDrawable(gradientDrawable);
}
final LinearLayout.LayoutParams dotParams = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
// 未选中小圆点的间距
dotParams.rightMargin = Utils.dp2px(context, 12);
// 创建未选中的小圆点
for (int i = 0; i < sourceSize; i++) {
AppCompatImageView iv = new AppCompatImageView(context);
GradientDrawable shapeDrawable = (GradientDrawable) ContextCompat.getDrawable(context, R.drawable.no_selected_dot);
if (mShareData.config.normalIndicatorColor != 0xFFAAAAAA) {
Objects.requireNonNull(shapeDrawable).setColorFilter(mShareData.config.normalIndicatorColor, Mode.SRC_OVER);
}
iv.setImageDrawable(shapeDrawable);
iv.setLayoutParams(dotParams);
mLlDotIndicator.addView(iv);
}
mLlDotIndicator.post(() -> {
View childAt = mLlDotIndicator.getChildAt(0);
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mIvSelectDot.getLayoutParams();
// 设置选中小圆点的左边距
params.leftMargin = (int) childAt.getX();
mIvSelectDot.setLayoutParams(params);
float tx = (dotParams.rightMargin * mCurrentPagerIndex + childAt.getWidth() * mCurrentPagerIndex);
mIvSelectDot.setTranslationX(tx);
});
} else if (sourceSize > 1) {
updateTextIndicator();
}
}
private void setIndicatorVisible(boolean visible) {
int sourceSize = mShareData.config.sources == null ? 0 : mShareData.config.sources.size();
if (sourceSize >= 2 && sourceSize <= mShareData.config.maxIndicatorDot
&& IndicatorType.DOT == mShareData.config.indicatorType) {
int visibility = visible ? View.VISIBLE : View.INVISIBLE;
mLlDotIndicator.setVisibility(visibility);
mIvSelectDot.setVisibility(visibility);
mTvTextIndicator.setVisibility(View.GONE);
} else if (sourceSize > 1) {
mLlDotIndicator.setVisibility(View.GONE);
mIvSelectDot.setVisibility(View.GONE);
mTvTextIndicator.setVisibility(visible ? View.VISIBLE : View.GONE);
} else {
mLlDotIndicator.setVisibility(View.GONE);
mIvSelectDot.setVisibility(View.GONE);
mTvTextIndicator.setVisibility(View.GONE);
}
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (mLlDotIndicator.getVisibility() == View.VISIBLE) {
mLlDotIndicator.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mLlDotIndicator.getViewTreeObserver().removeOnGlobalLayoutListener(this);
View childAt = mLlDotIndicator.getChildAt(0);
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mIvSelectDot.getLayoutParams();
// 设置选中小圆点的左边距
params.leftMargin = (int) childAt.getX();
mIvSelectDot.setLayoutParams(params);
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) childAt.getLayoutParams();
float tx = (layoutParams.rightMargin * mCurrentPagerIndex + childAt.getWidth() * mCurrentPagerIndex);
mIvSelectDot.setTranslationX(tx);
}
});
}
}
private void updateTextIndicator() {
int sourceSize = mShareData.config.sources == null ? 0 : mShareData.config.sources.size();
SpannableString.Builder.appendMode()
.addSpan(String.valueOf(mCurrentPagerIndex + 1))
.color(mShareData.config.selectIndicatorColor)
.addSpan(" / " + sourceSize)
.color(mShareData.config.normalIndicatorColor)
.apply(mTvTextIndicator);
}
}

View File

@@ -0,0 +1,28 @@
package com.wgw.photo.preview;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import androidx.annotation.IntDef;
/**
* 图形变换类型
*
* @author Created by wanggaowan on 12/21/20 6:50 PM
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.SOURCE)
@IntDef({ShapeTransformType.CIRCLE, ShapeTransformType.ROUND_RECT})
public @interface ShapeTransformType {
/**
* 切圆形,预览动画从圆形变换为矩形
*/
int CIRCLE = 0;
/**
* 切圆角矩形,预览动画从圆角矩形变换为矩形
*/
int ROUND_RECT = 1;
}

View File

@@ -0,0 +1,98 @@
package com.wgw.photo.preview;
import android.graphics.drawable.Drawable;
import android.view.View;
import com.wgw.photo.preview.PhotoPreviewHelper.OnExitListener;
import com.wgw.photo.preview.PhotoPreviewHelper.OnOpenListener;
import com.wgw.photo.preview.PreloadImageView.DrawableLoadListener;
import com.wgw.photo.preview.interfaces.IFindThumbnailView;
import com.wgw.photo.preview.interfaces.OnImageLongClickListener;
import com.wgw.photo.preview.interfaces.OnLongClickListener;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* 整个预览库都需要共享的数据
*
* @author Created by wanggaowan on 11/24/20 8:46 PM
*/
class ShareData {
@NonNull
final Config config = new Config();
/**
* 打开预览时的缩略图
*/
@Nullable
View thumbnailView;
/**
* 获取指定位置的缩略图
*/
@Nullable
IFindThumbnailView findThumbnailView;
/**
* 图片长按监听
*/
@Nullable
OnImageLongClickListener onLongClickListener;
/**
* 预览退出监听
*/
@Nullable
OnExitListener onExitListener;
/**
* 预览打开监听
*/
@Nullable
OnOpenListener onOpenListener;
/**
* 是否需要执行进入动画
*/
boolean showNeedAnim;
/**
* 预览界面是否第一次创建
*/
boolean isFirstCreate = true;
/**
* 预览动画延迟执行时间
*/
long openAnimDelayTime;
/**
* 预加载图片,加载内容为默认打开数据
*/
Drawable preLoadDrawable;
/**
* 预加载图片监听
*/
DrawableLoadListener preDrawableLoadListener;
void applyConfig(Config config) {
this.config.apply(config);
}
void release() {
config.release();
thumbnailView = null;
findThumbnailView = null;
onLongClickListener = null;
onExitListener = null;
onOpenListener = null;
showNeedAnim = false;
isFirstCreate = true;
openAnimDelayTime = 0;
preLoadDrawable = null;
preDrawableLoadListener = null;
}
}

View File

@@ -0,0 +1,18 @@
package com.wgw.photo.preview.interfaces;
import android.view.View;
/**
* 查找预览图指定下标对应的缩略图控件
*
* @author Created by wanggaowan on 11/20/20 9:16 PM
*/
public interface IFindThumbnailView {
/**
* 查找指定位置缩略图
*
* @param position 预览位置
* @return 预览图对应的缩略图控件
*/
View findView(int position);
}

View File

@@ -0,0 +1,22 @@
package com.wgw.photo.preview.interfaces;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* load image
*
* @author Created by wanggaowan on 2019/2/27 0027 10:32
*/
public interface ImageLoader {
/**
* 加载图片
*
* @param position 图片位置
* @param source 图片数据
* @param imageView 展示图片的控件
*/
void onLoadImage(int position, @Nullable Object source, @NonNull ImageView imageView);
}

View File

@@ -0,0 +1,12 @@
package com.wgw.photo.preview.interfaces;
import com.wgw.photo.preview.PhotoPreview;
/**
* call when {@link PhotoPreview} View close
*
* @author Created by wanggaowan on 2019/3/6 0006 17:33
*/
public interface OnDismissListener {
void onDismiss();
}

View File

@@ -0,0 +1,20 @@
package com.wgw.photo.preview.interfaces;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
/**
* 图片被长按监听
*
* @author Created by wanggaowan on 2019/3/6 0006 17:19
*/
public interface OnImageLongClickListener {
/**
* 长按,可添加自定义处理选项,比如保存图片、分享等
*
* @param position 被点击图片位置
* @param imageView 展示被点击图片的ImageView
*/
boolean onLongClick(int position, ImageView imageView);
}

View File

@@ -0,0 +1,21 @@
package com.wgw.photo.preview.interfaces;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
/**
* call when preview view long press
*
* @author Created by wanggaowan on 2019/3/6 0006 17:19
*/
public interface OnLongClickListener {
/**
* 长按,可添加自定义处理选项,比如保存图片、分享等
*
* @param position 被点击图片位置
* @param customViewRoot 自定义View根布局,全局唯一,可将自定义View加入到customViewRoot中。默认显示状态为{@link View#GONE}
* @param imageView 展示被点击图片的ImageView
*/
boolean onLongClick(int position, FrameLayout customViewRoot, ImageView imageView);
}

View File

@@ -0,0 +1,20 @@
package com.wgw.photo.preview.util;
import android.graphics.Matrix;
/**
* @author Created by wanggaowan on 11/19/20 10:27 PM
*/
public class MatrixUtils {
private static final float[] mMatrixValues = new float[9];
public static float getValue(Matrix matrix, int whichValue) {
matrix.getValues(mMatrixValues);
return mMatrixValues[whichValue];
}
public static float getScale(Matrix matrix) {
return (float) Math.sqrt((float) Math.pow(getValue(matrix, Matrix.MSCALE_X), 2)
+ (float) Math.pow(getValue(matrix, Matrix.MSKEW_Y), 2));
}
}

View File

@@ -0,0 +1,453 @@
package com.wgw.photo.preview.util;
import android.graphics.Typeface;
import android.text.Spannable;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.text.method.MovementMethod;
import android.text.style.ClickableSpan;
import android.view.View;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
/**
* 构建富文本
*
* @author Created by wanggaowan on 2020/10/26 08:32
*/
public class SpannableString extends android.text.SpannableString {
public SpannableString(CharSequence source) {
super(source);
}
public static class AppendBuilder {
private List<AppendSpan> mSpans;
public AppendBuilder() {
mSpans = new ArrayList<>();
}
/**
* 添加一个Span
*
* @param spanStr 需要处理的文本根据addSpan顺序拼接到整个文本内容中
* @return 如果数据无效则返回一个无效的Span节点仅仅为了流式调用实际执行时跳过
*/
public AppendSpan addSpan(String spanStr) {
AppendSpan span;
if (!TextUtils.isEmpty(spanStr)) {
span = new AppendSpan(this, spanStr);
mSpans.add(span);
} else {
span = new AppendSpan(this, null);
}
return span;
}
/**
* 应用SpannableString至目标
*
* @param textView 展示内容的TextView或子对象如果设置的span中包含点击项
* 则将调用{@link TextView#setMovementMethod(MovementMethod)},参数为LinkMovementMethod实例
*/
public <T extends TextView> void apply(T textView) {
if (textView == null) {
return;
}
if (mSpans.size() == 0) {
textView.setText(null);
return;
}
StringBuilder builder = new StringBuilder();
for (AppendSpan span : mSpans) {
builder.append(span.spanStr);
}
SpannableString ss = new SpannableString(builder.toString());
boolean needClick = false;
int start = 0;
for (AppendSpan span : mSpans) {
if (span.couldClick) {
needClick = true;
}
int end = start + span.spanStr.length();
ss.setSpan(new SpanImpl(span), start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
start = end;
}
if (needClick) {
textView.setMovementMethod(LinkMovementMethod.getInstance());
}
textView.setText(ss);
mSpans.clear();
mSpans = null;
}
/**
* 构建SpannableString如果构建对象中有可点击Span请确保应用该文本的{@link TextView}或子类设置了可点击的MovementMethod。
* 否则推荐使用{@link #apply(TextView)}方法
*/
public SpannableString create() {
if (mSpans.size() == 0) {
return new SpannableString("");
}
StringBuilder builder = new StringBuilder();
for (AppendSpan span : mSpans) {
builder.append(span.spanStr);
}
SpannableString ss = new SpannableString(builder.toString());
int start = 0;
for (AppendSpan span : mSpans) {
int end = start + span.spanStr.length();
ss.setSpan(new SpanImpl(span), start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
start = end;
}
mSpans.clear();
mSpans = null;
return ss;
}
}
/**
* 追加模式所有Span以拼接方式最终展示到目标View上
*/
public static class AppendSpan {
public static final int INVALID_VALUE = -1;
protected final AppendBuilder builder;
protected String spanStr;
protected int size = INVALID_VALUE;
protected int color = INVALID_VALUE;
protected boolean couldClick;
protected OnSpanClickListener clickListener;
protected boolean underLine;
protected Typeface typeface;
AppendSpan(AppendBuilder builder, String spanStr) {
this.builder = builder;
this.spanStr = spanStr;
}
/**
* 文本字体大小,不指定不调用此方法或传{@link AppendSpan#INVALID_VALUE}
*/
public AppendSpan size(int size) {
this.size = size;
return this;
}
public AppendSpan typeface(Typeface typeface) {
this.typeface = typeface;
return this;
}
/**
* 文本字体颜色,不指定不调用此方法或传{@link AppendSpan#INVALID_VALUE}
*/
public AppendSpan color(@ColorInt int color) {
this.color = color;
return this;
}
/**
* 文本是否可点击
*/
public AppendSpan couldClick(boolean cloudClick) {
this.couldClick = cloudClick;
return this;
}
/**
* 文本是否需要展示下划线
*/
public AppendSpan underLine(boolean underLine) {
this.underLine = underLine;
return this;
}
/**
* 文本点检监听,需要{@link #couldClick(boolean)}设置为true
*/
public AppendSpan clickListener(OnSpanClickListener listener) {
clickListener = listener;
return this;
}
/**
* 添加一个Span
*
* @param spanStr 需要处理的文本根据addSpan顺序拼接到整个文本内容中
* @return 如果数据无效则返回一个无效的Span节点仅仅为了流式调用实际执行时跳过
*/
public AppendSpan addSpan(String spanStr) {
return builder.addSpan(spanStr);
}
/**
* 应用SpannableString至目标
*
* @param textView 展示内容的TextView或子对象如果设置的span中包含点击项
* 则将调用{@link TextView#setMovementMethod(MovementMethod)},参数为LinkMovementMethod实例
*/
public <T extends TextView> void apply(@NonNull T textView) {
builder.apply(textView);
}
/**
* 构建SpannableString如果构建对象中有可点击Span请确保应用该文本的{@link TextView}或子类设置了可点击的MovementMethod。
* 否则推荐使用{@link #apply(TextView)}方法
*/
public SpannableString create() {
return builder.create();
}
}
/**
* SpannableString构建器
*/
public static class Builder extends AppendBuilder {
private String source;
private List<Span> mSpans;
private Builder() {
}
/**
* @param source 构建的SpannableString需要展示的原始文本还未进行处理过.如果为空串"",则不处理
*/
public Builder(@NonNull String source, Object... args) {
mSpans = new ArrayList<>();
this.source = source;
if (isSourceValid() && args != null && args.length > 0) {
this.source = String.format(this.source, args);
}
}
private boolean isSourceValid() {
return !TextUtils.isEmpty(source);
}
/**
* 以追加模式进行构建
*/
public static AppendBuilder appendMode() {
return new AppendBuilder();
}
/**
* @param source 构建的SpannableString需要展示的原始文本还未进行处理过.如果为空串"",则不处理
*/
public static Builder string(@NonNull String source) {
return new Builder(source);
}
/**
* @param source 构建的SpannableString需要展示的原始文本还未进行处理过.如果为空串"",则不处理
* @param args source格式化参数
*/
public static Builder string(@NonNull String source, Object... args) {
return new Builder(source, args);
}
/**
* 添加一个Span描述{@link #string(String)} 或 {@link #Builder(String, Object...)}参数source中某一段文本应该怎么显示
*
* @param start 文本在source中的开始位置如果 < 0 || > end || source.length()) 则不处理
* @param end 文本在source中的结束位置如果 < 0 || > (start || source.length()) 则不处理
* @return 如果数据无效则返回一个无效的Span节点仅仅为了流式调用实际执行时跳过
*/
public Span addSpan(int start, int end) {
Span span;
if (isSourceValid() && start >= 0 && start <= end && start <= source.length()) {
String spanStr = source.substring(start, end);
span = new Span(this, spanStr, start, end);
mSpans.add(span);
} else {
span = new Span(this, null, Span.INVALID_VALUE, Span.INVALID_VALUE);
}
return span;
}
/**
* 添加一个Span描述{@link #string(String)} 或 {@link #Builder(String, Object...)}参数source中某一段文本应该怎么显示
*
* @param spanStr source文本中指定需要处理的片段文本内容如果source中不存在spanStr则不处理。
* 请确保source不会多次出现spanStr文本否则请使用{@link #addSpan(int, int)}明确指明区间
* @return 如果数据无效则返回一个无效的Span节点仅仅为了流式调用实际执行时跳过
*/
@Override
public Span addSpan(String spanStr) {
Span span;
if (isSourceValid() && !TextUtils.isEmpty(spanStr) && source.contains(spanStr)) {
int index = source.indexOf(spanStr);
span = new Span(this, spanStr, index, index + spanStr.length());
span.spanStr = spanStr;
mSpans.add(span);
} else {
span = new Span(this, null, Span.INVALID_VALUE, Span.INVALID_VALUE);
}
return span;
}
/**
* 应用SpannableString至目标
*
* @param textView 展示内容的TextView或子对象如果设置的span中包含点击项
* 则将调用{@link TextView#setMovementMethod(MovementMethod)},参数为LinkMovementMethod实例
*/
public <T extends TextView> void apply(T textView) {
if (textView == null) {
return;
}
if (!isSourceValid() || mSpans.size() == 0) {
textView.setText(null);
return;
}
SpannableString ss = new SpannableString(source);
boolean needClick = false;
for (Span span : mSpans) {
if (span.couldClick) {
needClick = true;
}
ss.setSpan(new SpanImpl(span), span.start, span.end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
}
if (needClick) {
textView.setMovementMethod(LinkMovementMethod.getInstance());
}
textView.setText(ss);
mSpans.clear();
mSpans = null;
source = null;
}
/**
* 构建SpannableString如果构建对象中有可点击Span请确保应用该文本的{@link TextView}或子类设置了可点击的MovementMethod。
* 否则推荐使用{@link #apply(TextView)}方法
*/
public SpannableString create() {
if (!isSourceValid()) {
return new SpannableString("");
}
SpannableString ss = new SpannableString(source);
for (Span span : mSpans) {
ss.setSpan(new SpanImpl(span), span.start, span.end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
}
mSpans.clear();
mSpans = null;
source = null;
return ss;
}
}
public static class Span extends AppendSpan {
final Builder builder;
final int start;
final int end;
Span(Builder builder, String spanStr, int start, int end) {
super(builder, spanStr);
this.builder = builder;
this.start = start;
this.end = end;
}
/**
* 添加一个Span描述{@link Builder#string(String)} 或 {@link Builder#Builder(String, Object...)}参数source中某一段文本应该怎么显示
*
* @param start 文本在source中的开始位置如果 < 0 || > end || source.length()) 则不处理
* @param end 文本在source中的结束位置如果 < 0 || > (start || source.length()) 则不处理
* @return 如果数据无效则返回一个无效的Span节点仅仅为了流式调用实际执行时跳过
*/
public Span addSpan(int start, int end) {
return builder.addSpan(start, end);
}
/**
* 添加一个Span描述{@link Builder#string(String)} 或 {@link Builder#Builder(String, Object...)}参数source中某一段文本应该怎么显示
*
* @param spanStr source文本中指定需要处理的片段文本内容如果source中不存在spanStr则不处理.
* 请确保source不会多次出现spanStr文本否则请使用{@link #addSpan(int, int)}明确指明区间
* @return 如果数据无效则返回一个无效的Span节点仅仅为了流式调用实际执行时跳过
*/
@Override
public Span addSpan(String spanStr) {
return builder.addSpan(spanStr);
}
}
/**
* 用于实现{@link AppendSpan}中设置的内容
*/
static class SpanImpl extends ClickableSpan {
private final AppendSpan span;
SpanImpl(AppendSpan span) {
this.span = span;
}
@Override
public void onClick(@NonNull View widget) {
if (span.couldClick && span.clickListener != null) {
span.clickListener.onClick(widget, span.spanStr);
}
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
if (span.size != AppendSpan.INVALID_VALUE) {
ds.setTextSize(span.size);
}
if (span.typeface != null) {
ds.setTypeface(span.typeface);
}
if (span.color != AppendSpan.INVALID_VALUE) {
ds.setColor(span.color);
}
ds.setUnderlineText(span.underLine);
}
}
/**
* Span点击监听
*/
public interface OnSpanClickListener {
/**
* @param view 被点击文本当前应用的View对象
* @param spanStr 点检区域文本
*/
void onClick(@NonNull View view, @NonNull String spanStr);
}
}

View File

@@ -0,0 +1,49 @@
package com.wgw.photo.preview.util;
import android.content.Context;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.util.TypedValue;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
/**
* @author Created by wanggaowan on 2019/2/28 0028 13:59
*/
public class Utils {
public static int dp2px(Context context, int dipValue) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
dipValue, context.getResources().getDisplayMetrics());
}
public static int getStatusBarHeight(Context context) {
int result = 0;
int resourceId = context.getResources()
.getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
result = context.getResources().getDimensionPixelSize(resourceId);
}
return result;
}
/**
* 是否是沉浸式状态栏或无状态栏,此种情况都无需处理状态栏导致的偏移值
*/
public static boolean isImmersionBar(Window window) {
if ((window.getAttributes().flags & LayoutParams.FLAG_FULLSCREEN) == LayoutParams.FLAG_FULLSCREEN) {
return true;
}
View decorView = window.getDecorView();
if ((decorView.getSystemUiVisibility() & View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) == View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) {
return true;
} else if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
return (decorView.getSystemUiVisibility() & View.SYSTEM_UI_FLAG_LAYOUT_STABLE) == View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
}
return false;
}
}

View File

@@ -0,0 +1,39 @@
package com.wgw.photo.preview.util.notch;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import androidx.annotation.IntDef;
/**
* 异形屏全屏适配方案
*
* @author Created by 汪高皖 on 2019/3/12 0012 09:55
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.SOURCE)
@IntDef({CutOutMode.DEFAULT, CutOutMode.SHORT_EDGES, CutOutMode.NEVER, CutOutMode.ALWAYS})
public @interface CutOutMode {
/**
* 默认模式,在全屏状态下,效果与{@link #NEVER}一致,在非全屏状态下,竖屏时绘制到耳朵区域,横屏时禁用耳朵区
*/
int DEFAULT = 0;
/**
* 耳朵区域绘制模式在androidP(包含)以上机型,横竖屏都绘制到耳朵区域。
* 此版本之下,小米手机只竖屏绘制到耳朵区域
*/
int SHORT_EDGES = 1;
/**
* 耳朵区域不绘制模式,此时全屏时,状态栏呈现黑条
*/
int NEVER = 2;
/**
* 横竖屏都绘制到耳朵区域,如果可以单独设置的情况下
*/
int ALWAYS = 3;
}

View File

@@ -0,0 +1,268 @@
package com.wgw.photo.preview.util.notch;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
import android.view.DisplayCutout;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import androidx.annotation.RequiresApi;
/**
* 全屏刘海屏适配
* <ul>
* <li><a href='https://dev.mi.com/console/doc/detail?pId=1293'>小米适配文档</a></li>
* <li><a href='https://dev.vivo.com.cn/documentCenter/doc/103'>VIVO适配文档</a></li>
* <li><a href='https://open.oppomobile.com/wiki/doc#id=10159'>OPPO适配文档</a></li>
* <li><a href='https://developer.huawei.com/consumer/cn/devservice/doc/50114'>华为适配文档</a></li>
* </ul>
*
* @author Created by 汪高皖 on 2019/3/12 0012 09:39
*/
public class NotchAdapterUtils {
public static void adapter(Window window, @CutOutMode int cutOutMode) {
if (window == null) {
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// OPPO 通过windowInsets.getDisplayCutout() 拿不到 DisplayCutout始终返回null
// 因此只要 androidP以上就适配不管是否是异形屏
adapterP(window, cutOutMode);
return;
}
if (!isNotch(window)) {
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
adapterO(window, cutOutMode);
}
}
/**
* 适配android P及以上系统
*/
@RequiresApi(api = Build.VERSION_CODES.P)
private static void adapterP(Window window, @CutOutMode int cutOutMode) {
if (window == null) {
return;
}
WindowManager.LayoutParams lp = window.getAttributes();
if (cutOutMode == CutOutMode.DEFAULT) {
lp.layoutInDisplayCutoutMode = LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
} else if (cutOutMode == CutOutMode.SHORT_EDGES || cutOutMode == CutOutMode.ALWAYS) {
lp.layoutInDisplayCutoutMode = LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
} else {
lp.layoutInDisplayCutoutMode = LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
}
window.setAttributes(lp);
}
/**
* 适配android O系统
*/
@RequiresApi(api = Build.VERSION_CODES.O)
private static void adapterO(Window window, @CutOutMode int cutOutMode) {
if (window == null) {
return;
}
if (OSUtils.isMIUI()) {
adapterOWithMIUI(window, cutOutMode);
} else if (OSUtils.isEMUI()) {
adapterOWithEMUI(window, cutOutMode);
}
}
/**
* 适配 MIUI android O系统
*/
@RequiresApi(api = Build.VERSION_CODES.O)
private static void adapterOWithMIUI(Window window, @CutOutMode int cutOutMode) {
if (window == null) {
return;
}
/*
0x00000100 开启配置
0x00000200 竖屏配置
0x00000400 横屏配置
0x00000100 | 0x00000200 竖屏绘制到耳朵区
0x00000100 | 0x00000400 横屏绘制到耳朵区
0x00000100 | 0x00000200 | 0x00000400 横竖屏都绘制到耳朵区
*/
int flag;
if (cutOutMode == CutOutMode.ALWAYS) {
flag = 0x00000100 | 0x00000200 | 0x00000400;
} else {
flag = 0x00000100 | 0x00000200;
}
String methodName;
if (cutOutMode == CutOutMode.NEVER) {
methodName = "clearExtraFlags";
} else if (cutOutMode == CutOutMode.DEFAULT) {
WindowManager.LayoutParams attributes = window.getAttributes();
if ((attributes.flags & WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) > 0) {
methodName = "addExtraFlags";
} else {
methodName = "clearExtraFlags";
}
} else {
methodName = "addExtraFlags";
}
try {
Method method = Window.class.getMethod(methodName, int.class);
method.invoke(window, flag);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 适配 EMUI android O系统
*/
@SuppressWarnings({"unchecked"})
@RequiresApi(api = Build.VERSION_CODES.O)
private static void adapterOWithEMUI(Window window, @CutOutMode int cutOutMode) {
if (window == null) {
return;
}
int FLAG_NOTCH_SUPPORT = 0x00010000;
String methodName;
if (cutOutMode == CutOutMode.NEVER) {
methodName = "clearHwFlags";
} else if (cutOutMode == CutOutMode.DEFAULT) {
WindowManager.LayoutParams attributes = window.getAttributes();
if ((attributes.flags & WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) > 0) {
methodName = "addHwFlags";
} else {
methodName = "clearHwFlags";
}
} else {
methodName = "addHwFlags";
}
WindowManager.LayoutParams layoutParams = window.getAttributes();
try {
//noinspection rawtypes
Class layoutParamsExCls = Class.forName("com.huawei.android.view.LayoutParamsEx");
//noinspection rawtypes
Constructor con = layoutParamsExCls.getConstructor(WindowManager.LayoutParams.class);
Object layoutParamsExObj = con.newInstance(layoutParams);
Method method = layoutParamsExCls.getMethod(methodName, int.class);
method.invoke(layoutParamsExObj, FLAG_NOTCH_SUPPORT);
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InstantiationException
| InvocationTargetException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 是否是异形屏
*/
public static boolean isNotch(Window window) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
boolean isNotchScreen = false;
WindowInsets windowInsets = window.getDecorView().getRootWindowInsets();
if (windowInsets != null) {
DisplayCutout displayCutout = windowInsets.getDisplayCutout();
if (displayCutout != null) {
isNotchScreen = true;
}
}
return isNotchScreen;
} else if (OSUtils.isMIUI()) {
return isNotchOnMIUI();
} else if (OSUtils.isEMUI()) {
return isNotchOnEMUI(window.getContext());
} else if (OSUtils.isVIVO()) {
return isNotchOnVIVO(window.getContext());
} else if (OSUtils.isOPPO()) {
return isNotchOnOPPO(window.getContext());
} else {
return false;
}
}
public static boolean isNotchOnMIUI() {
return "1".equals(OSUtils.getProp("ro.miui.notch"));
}
@SuppressWarnings("unchecked")
public static boolean isNotchOnEMUI(Context context) {
if (context == null) {
return false;
}
boolean isNotch = false;
try {
ClassLoader cl = context.getClassLoader();
//noinspection rawtypes
Class HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil");
Method get = HwNotchSizeUtil.getMethod("hasNotchOnHuawei");
isNotch = (boolean) get.invoke(HwNotchSizeUtil);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return isNotch;
}
@SuppressWarnings("unchecked")
public static boolean isNotchOnVIVO(Context context) {
if (context == null) {
return false;
}
// 是否有刘海
int VIVO_NOTCH = 0x00000020;
// 是否有圆角
// int VIVO_FILLET = 0x00000008;
boolean isNotch = false;
try {
ClassLoader classLoader = context.getClassLoader();
//noinspection rawtypes
@SuppressLint("PrivateApi")
Class FtFeature = classLoader.loadClass("android.util.FtFeature");
Method method = FtFeature.getMethod("isFeatureSupport", int.class);
isNotch = (boolean) method.invoke(FtFeature, VIVO_NOTCH);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return isNotch;
}
public static boolean isNotchOnOPPO(Context context) {
if (context == null) {
return false;
}
return context.getPackageManager()
.hasSystemFeature("com.oppo.feature.screen.heteromorphism");
}
}

View File

@@ -0,0 +1,118 @@
package com.wgw.photo.preview.util.notch;
import android.os.Build;
import android.text.TextUtils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class OSUtils {
public static final String ROM_MIUI = "MIUI";
public static final String ROM_EMUI = "EMUI";
public static final String ROM_FLYME = "FLYME";
public static final String ROM_OPPO = "OPPO";
public static final String ROM_SMARTISAN = "SMARTISAN";
public static final String ROM_VIVO = "VIVO";
private static final String KEY_VERSION_MIUI = "ro.miui.ui.version.name";
private static final String KEY_VERSION_EMUI = "ro.build.version.emui";
private static final String KEY_VERSION_OPPO = "ro.build.version.opporom";
private static final String KEY_VERSION_SMARTISAN = "ro.smartisan.version";
private static final String KEY_VERSION_VIVO = "ro.vivo.os.version";
private static String sName;
private static String sVersion;
public static boolean isEMUI() {
return check(ROM_EMUI);
}
public static boolean isMIUI() {
return check(ROM_MIUI);
}
public static boolean isMI6() {
boolean check = check(ROM_MIUI);
if (check) {
return "MI 6".equals(Build.MODEL);
}
return false;
}
public static boolean isVIVO() {
return check(ROM_VIVO);
}
public static boolean isOPPO() {
return check(ROM_OPPO);
}
public static String getName() {
if (sName == null) {
check("");
}
return sName;
}
public static String getVersion() {
if (sVersion == null) {
check("");
}
return sVersion;
}
public static boolean check(String rom) {
if (sName != null) {
return sName.equals(rom);
}
if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_MIUI))) {
sName = ROM_MIUI;
} else if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_EMUI))) {
sName = ROM_EMUI;
} else if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_OPPO))) {
sName = ROM_OPPO;
} else if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_VIVO))) {
sName = ROM_VIVO;
} else if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_SMARTISAN))) {
sName = ROM_SMARTISAN;
} else {
sVersion = Build.DISPLAY;
if (sVersion.toUpperCase().contains(ROM_FLYME)) {
sName = ROM_FLYME;
} else {
sVersion = Build.UNKNOWN;
sName = Build.MANUFACTURER.toUpperCase();
}
}
return sName.equals(rom);
}
public static String getProp(String name) {
String line;
BufferedReader input = null;
try {
Process p = Runtime.getRuntime().exec("getprop " + name);
input = new BufferedReader(new InputStreamReader(p.getInputStream()), 1024);
line = input.readLine();
input.close();
} catch (IOException ex) {
return null;
} finally {
if (input != null) {
try {
input.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return line;
}
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<size
android:width="7dp"
android:height="7dp" />
<solid android:color="@android:color/darker_gray" />
</shape>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<size
android:width="7dp"
android:height="7dp" />
<solid android:color="@android:color/white" />
</shape>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.wgw.photo.preview.PhotoView
android:id="@+id/photoView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="invisible" />
<ProgressBar
android:id="@+id/loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
tools:visibility="visible" />
</FrameLayout>

View File

@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/rl_root_photo_preview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/transparent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--仅用于低版本切圆角-->
<androidx.cardview.widget.CardView
android:id="@+id/fl_parent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/transparent"
android:visibility="invisible"
app:cardBackgroundColor="@color/transparent"
app:cardElevation="0dp">
<ImageView
android:id="@+id/iv_anim"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="ContentDescription" />
</androidx.cardview.widget.CardView>
</FrameLayout>
<com.wgw.photo.preview.NoTouchExceptionViewPager
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="never" />
<LinearLayout
android:id="@+id/ll_dot_indicator_photo_preview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_marginBottom="44dp"
android:gravity="center"
android:orientation="horizontal"
android:visibility="gone"
tools:visibility="visible" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_select_dot_photo_preview"
android:layout_width="7dp"
android:layout_height="7dp"
android:layout_gravity="bottom"
android:layout_marginBottom="44dp"
android:src="@drawable/selected_dot"
android:visibility="gone"
tools:ignore="ContentDescription"
tools:visibility="visible" />
<TextView
android:id="@+id/tv_text_indicator_photo_preview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_marginBottom="40dp"
android:gravity="center"
android:textColor="@android:color/darker_gray"
android:textSize="14sp"
android:visibility="gone"
tools:text="1 / 9"
tools:visibility="visible" />
<FrameLayout
android:id="@+id/fl_custom"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
</FrameLayout>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="transparent">#0000</color>
<color name="black">#000</color>
<color name="white">#fff</color>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="view_holder" type="id" />
<item name="loading" type="id" />
<item name="view_pager_id" type="id" />
<item name="view_pager_id_next" type="id" />
</resources>

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">preview</string>
</resources>