diff --git a/PhotoPreview/.gitignore b/PhotoPreview/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/PhotoPreview/.gitignore @@ -0,0 +1 @@ +/build diff --git a/PhotoPreview/build.gradle b/PhotoPreview/build.gradle new file mode 100644 index 0000000..c513c91 --- /dev/null +++ b/PhotoPreview/build.gradle @@ -0,0 +1,34 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 29 + defaultConfig { + minSdkVersion 24 + targetSdkVersion 29 + versionCode 27 + versionName "2.4.3" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + // 设置JDK1.8 + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.recyclerview:recyclerview:1.1.0' + implementation 'androidx.transition:transition:1.4.1' + implementation 'androidx.cardview:cardview:1.0.0' + // 为了兼容大部分用户仅依赖低版本,本库仅使用GifDrawable类型判断 + implementation 'com.github.bumptech.glide:glide:3.3.0' +} diff --git a/PhotoPreview/proguard-rules.pro b/PhotoPreview/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/PhotoPreview/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/PhotoPreview/src/main/AndroidManifest.xml b/PhotoPreview/src/main/AndroidManifest.xml new file mode 100644 index 0000000..00e99bd --- /dev/null +++ b/PhotoPreview/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/Compat.java b/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/Compat.java new file mode 100644 index 0000000..2439d67 --- /dev/null +++ b/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/Compat.java @@ -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); + } +} diff --git a/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/CustomGestureDetector.java b/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/CustomGestureDetector.java new file mode 100644 index 0000000..ca27351 --- /dev/null +++ b/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/CustomGestureDetector.java @@ -0,0 +1,211 @@ +/* + 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.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; + } +} diff --git a/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/OnGestureListener.java b/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/OnGestureListener.java new file mode 100644 index 0000000..eda81f4 --- /dev/null +++ b/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/OnGestureListener.java @@ -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); + +} \ No newline at end of file diff --git a/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/OnMatrixChangedListener.java b/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/OnMatrixChangedListener.java new file mode 100644 index 0000000..b3b2792 --- /dev/null +++ b/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/OnMatrixChangedListener.java @@ -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); +} diff --git a/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/OnOutsidePhotoTapListener.java b/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/OnOutsidePhotoTapListener.java new file mode 100644 index 0000000..8616085 --- /dev/null +++ b/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/OnOutsidePhotoTapListener.java @@ -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); +} diff --git a/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/OnPhotoTapListener.java b/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/OnPhotoTapListener.java new file mode 100644 index 0000000..2e05c0e --- /dev/null +++ b/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/OnPhotoTapListener.java @@ -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); +} diff --git a/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/OnScaleChangedListener.java b/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/OnScaleChangedListener.java new file mode 100644 index 0000000..d022baf --- /dev/null +++ b/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/OnScaleChangedListener.java @@ -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); +} diff --git a/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/OnSingleFlingListener.java b/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/OnSingleFlingListener.java new file mode 100644 index 0000000..bd5b170 --- /dev/null +++ b/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/OnSingleFlingListener.java @@ -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); +} diff --git a/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/OnViewDragListener.java b/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/OnViewDragListener.java new file mode 100644 index 0000000..df7cb85 --- /dev/null +++ b/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/OnViewDragListener.java @@ -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); +} diff --git a/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/OnViewTapListener.java b/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/OnViewTapListener.java new file mode 100644 index 0000000..268ffa7 --- /dev/null +++ b/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/OnViewTapListener.java @@ -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); +} diff --git a/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/PhotoView.java b/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/PhotoView.java new file mode 100644 index 0000000..d772050 --- /dev/null +++ b/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/PhotoView.java @@ -0,0 +1,264 @@ +/* + 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.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); + } +} diff --git a/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/PhotoViewAttacher.java b/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/PhotoViewAttacher.java new file mode 100644 index 0000000..c77800c --- /dev/null +++ b/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/PhotoViewAttacher.java @@ -0,0 +1,874 @@ +/* + 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.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); + } + } + } +} diff --git a/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/Util.java b/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/Util.java new file mode 100644 index 0000000..c8e8e5c --- /dev/null +++ b/PhotoPreview/src/main/java/com/github/chrisbanes/photoview/custom/Util.java @@ -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; + } +} diff --git a/PhotoPreview/src/main/java/com/wgw/photo/preview/ChangeShape.java b/PhotoPreview/src/main/java/com/wgw/photo/preview/ChangeShape.java new file mode 100644 index 0000000..a0f79b7 --- /dev/null +++ b/PhotoPreview/src/main/java/com/wgw/photo/preview/ChangeShape.java @@ -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 { + + 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; + } +} diff --git a/PhotoPreview/src/main/java/com/wgw/photo/preview/Config.java b/PhotoPreview/src/main/java/com/wgw/photo/preview/Config.java new file mode 100644 index 0000000..8ce0620 --- /dev/null +++ b/PhotoPreview/src/main/java/com/wgw/photo/preview/Config.java @@ -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; + } +} diff --git a/PhotoPreview/src/main/java/com/wgw/photo/preview/ImagePagerAdapter.java b/PhotoPreview/src/main/java/com/wgw/photo/preview/ImagePagerAdapter.java new file mode 100644 index 0000000..6e7d21d --- /dev/null +++ b/PhotoPreview/src/main/java/com/wgw/photo/preview/ImagePagerAdapter.java @@ -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; + } + } +} diff --git a/PhotoPreview/src/main/java/com/wgw/photo/preview/IndicatorType.java b/PhotoPreview/src/main/java/com/wgw/photo/preview/IndicatorType.java new file mode 100644 index 0000000..569b11d --- /dev/null +++ b/PhotoPreview/src/main/java/com/wgw/photo/preview/IndicatorType.java @@ -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; +} diff --git a/PhotoPreview/src/main/java/com/wgw/photo/preview/NoTouchExceptionViewPager.java b/PhotoPreview/src/main/java/com/wgw/photo/preview/NoTouchExceptionViewPager.java new file mode 100644 index 0000000..4fc1798 --- /dev/null +++ b/PhotoPreview/src/main/java/com/wgw/photo/preview/NoTouchExceptionViewPager.java @@ -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; + } +} diff --git a/PhotoPreview/src/main/java/com/wgw/photo/preview/PhotoPreview.java b/PhotoPreview/src/main/java/com/wgw/photo/preview/PhotoPreview.java new file mode 100644 index 0000000..b604d7f --- /dev/null +++ b/PhotoPreview/src/main/java/com/wgw/photo/preview/PhotoPreview.java @@ -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> 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 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 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

+ */ + 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 + */ + 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 + */ + 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 + */ + 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); + } + } +} diff --git a/PhotoPreview/src/main/java/com/wgw/photo/preview/PhotoPreviewHelper.java b/PhotoPreview/src/main/java/com/wgw/photo/preview/PhotoPreviewHelper.java new file mode 100644 index 0000000..adbd302 --- /dev/null +++ b/PhotoPreview/src/main/java/com/wgw/photo/preview/PhotoPreviewHelper.java @@ -0,0 +1,1236 @@ +package com.wgw.photo.preview; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ArgbEvaluator; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.view.ViewParent; +import android.view.animation.Interpolator; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.ImageView.ScaleType; + +import com.wgw.photo.preview.ImagePagerAdapter.ViewHolder; +import com.wgw.photo.preview.util.MatrixUtils; + +import java.util.ArrayList; +import java.util.List; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; +import androidx.interpolator.view.animation.FastOutSlowInInterpolator; +import androidx.lifecycle.Lifecycle.State; +import androidx.transition.ChangeBounds; +import androidx.transition.ChangeImageTransform; +import androidx.transition.ChangeTransform; +import androidx.transition.Transition; +import androidx.transition.TransitionListenerAdapter; +import androidx.transition.TransitionManager; +import androidx.transition.TransitionSet; + +/** + * 图片预览辅助类,主要处理预览打开和关闭时过渡动画 + * + * @author Created by wanggaowan on 2019/2/27 0027 11:24 + */ +@RestrictTo(Scope.LIBRARY) +class PhotoPreviewHelper { + + /* + 此预览库动画采用androidx.transition.Transition库实现,使用此库有以下几个点需要注意 + 说明:适配不同缩放类型的逻辑都是基于Glide图片加载库 + srcView 指定缩略图,且是ImageView类型 + helperView 指定实现从缩略图到预览图过度动画辅助类 + photoView 指定预览大图 + + 1. 如果srcView 的缩放类型为ScaleType.CENTER_CROP,那么helperView 设置的drawable 必须为photoView drawable, + 否则过度动画不能无缝衔接。比如以下情况: + + // srcView并非加载的原图,缩放类型为ScaleType.CENTER_CROP + Glide.with(mContext) + .load(item) + // .override(Target.SIZE_ORIGINAL) + .into(srcView); + + // 此时不管photoView是否加载原图,如果helperView设置为srcView的drawable,那么过度动画不能无缝衔接 + Glide.with(mContext) + .load(item) + // .override(Target.SIZE_ORIGINAL) + .into(photoView); + + 2. 如果srcView 的缩放类型为非ScaleType.CENTER_CROP,那么helperView 设置的drawable 必须为srcView drawable, + 否则过度动画不能无缝衔接。比如以下情况: + + // srcView缩放类型非ScaleType.CENTER_CROP + Glide.with(mContext) + .load(item) + // 无论是否加载原图 + // .override(Target.SIZE_ORIGINAL) + .into(srcView); + + // 此时不管photoView是否加载原图,如果helperView设置为photoView的drawable,那么过度动画不能无缝衔接 + Glide.with(mContext) + .load(item) + // .override(Target.SIZE_ORIGINAL) + .into(photoView); + + 3. 由于存在srcView实际显示大小并非布局或代码指定的固定大小,因此在helperView外部包裹一层父布局,用于裁剪 + */ + + private static final long OPEN_AND_EXIT_ANIM_DURATION = 200; + private static final long OPEN_AND_EXIT_ANIM_DURATION_FOR_IMAGE = 350; + + private static final ArgbEvaluator ARGB_EVALUATOR = new ArgbEvaluator(); + private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator(); + + private static final int ANIM_START_PRE = 0; + private static final int ANIM_START = 1; + private static final int ANIM_END = 2; + + final PreviewDialogFragment mFragment; + // 占位图,辅助执行过度动画 + // 为什么不直接使用mPhotoView进行过度动画,主要有一下几个问题 + // 1. 使用mPhotoView,某些情况下预览打开后图片某一部分自动被放大。导致这个的原因可能是使用Glide加载库,设置了placeholder导致 + // 2. 预览动画开始时,需要对图片进行位移,裁减等操作,而预览大图加载时,不能调整其大小和缩放类型,否则预览大图显示将出现问题 + private final ImageView mHelperView; + private final FrameLayout mHelperViewParent; + final FrameLayout mRootViewBgMask; + private final ShareData mShareData; + + // 当前界面显示预览图位置 + private int mPosition; + + private View mThumbnailView; + private int mThumbnailViewVisibility = View.VISIBLE; + private ScaleType mThumbnailViewScaleType; + // 当前界面是否需要执行动画 + private boolean mNeedInAnim; + // 根据动画时间可决定整个预览是否需要执行动画 + private long mAnimDuration; + + private final int[] mIntTemp = new int[2]; + // 缩略图设定大小 + private final int[] mSrcViewSize = new int[2]; + // 缩略图实际可显示大小 + private final int[] mSrcViewParentSize = new int[2]; + private final int[] mSrcImageLocation = new int[2]; + private final float[] mFloatTemp = new float[2]; + // 辅助图是否可接收新图片 + private boolean mHelpViewCanSetImage = true; + + // 预览打开动画执行结束 + private boolean mOpenAnimEnd = false; + private List mOpenListenerList; + private List mExitListenerList; + // 上一次缩放倍率是否小于1 + private boolean oldScaleLessOne = false; + + public PhotoPreviewHelper(PreviewDialogFragment fragment, int position) { + mFragment = fragment; + mShareData = mFragment.mShareData; + mPosition = position; + + mFragment.mRootView.setFocusableInTouchMode(true); + mFragment.mRootView.requestFocus(); + mRootViewBgMask = mFragment.mRootView; + mHelperView = mFragment.mRootView.findViewById(R.id.iv_anim); + mHelperViewParent = mFragment.mRootView.findViewById(R.id.fl_parent); + + mRootViewBgMask.setBackgroundColor(Color.TRANSPARENT); + + mHelperViewParent.setVisibility(View.INVISIBLE); + mHelperViewParent.setTranslationX(0); + mHelperViewParent.setTranslationY(0); + mHelperView.setScaleX(1f); + mHelperView.setScaleY(1f); + mHelperView.setImageDrawable(null); + if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + mHelperView.setOutlineProvider(null); + } + setViewSize(mHelperViewParent, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + setViewSize(mHelperView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + + initEvent(); + initData(); + } + + private void initEvent() { + mFragment.mRootView.setOnKeyListener((v, keyCode, event) -> { + if (keyCode == KeyEvent.KEYCODE_BACK + && (event == null || event.getAction() == KeyEvent.ACTION_UP) + && mOpenAnimEnd) { + exit(); + return true; + } + + return false; + }); + + mFragment.mRootView.setOnClickListener(v -> { + if (!mOpenAnimEnd) { + return; + } + + exit(); + }); + } + + private void initData() { + View thumbnailView = getThumbnailView(mShareData); + if (thumbnailView != mThumbnailView) { + mThumbnailView = thumbnailView; + if (mThumbnailView != null) { + mThumbnailViewVisibility = mThumbnailView.getVisibility(); + } + mAnimDuration = getOpenAndExitAnimDuration(mThumbnailView, mShareData); + initThumbnailViewScaleType(); + } + + mNeedInAnim = mAnimDuration > 0 && mShareData.showNeedAnim; + doPreviewAnim(); + } + + /** + * 初始化缩略图的缩放类型 + */ + private void initThumbnailViewScaleType() { + if (mThumbnailView instanceof ImageView) { + mThumbnailViewScaleType = ((ImageView) mThumbnailView).getScaleType(); + if (mThumbnailViewScaleType == ScaleType.CENTER || mThumbnailViewScaleType == ScaleType.CENTER_INSIDE) { + Drawable drawable = ((ImageView) mThumbnailView).getDrawable(); + if (drawable != null && drawable.getIntrinsicWidth() >= mThumbnailView.getWidth() + && drawable.getIntrinsicHeight() >= mThumbnailView.getHeight()) { + // ScaleType.CENTER:不缩放图片,如果图片大于图片控件,效果与ScaleType.CENTER_CROP一致 + if (mThumbnailViewScaleType == ScaleType.CENTER) { + mThumbnailViewScaleType = ScaleType.CENTER_CROP; + } + } else if (mAnimDuration > 0) { + // 当图片小于控件大小时,对于缩放类型ScaleType.CENTER 和 ScaleType.CENTER_INSIDE,需要以动画的方式打开 + // 除非整个预览不需要动画,否则再推出时,可能不会无缝衔接 + mNeedInAnim = true; + } + } + } else { + mThumbnailViewScaleType = null; + } + } + + /** + * 执行预览动画 + */ + private void doPreviewAnim() { + if (!mNeedInAnim) { + initNoAnim(); + // 整个预览无需执行动画,因此第一个执行的预览界面执行一次预览打开回调 + callOnOpen(ANIM_START_PRE, ANIM_START, ANIM_END); + mShareData.showNeedAnim = false; + return; + } + + // 处理进入时的动画 + mNeedInAnim = false; + mShareData.showNeedAnim = false; + mHelpViewCanSetImage = true; + mShareData.preDrawableLoadListener = drawable -> { + if (mHelpViewCanSetImage) { + mHelperView.setImageDrawable(drawable); + } + }; + mHelperView.setImageDrawable(mShareData.preLoadDrawable); + mShareData.preLoadDrawable = null; + + if (mThumbnailView == null) { + enterAnimByScale(); + return; + } + + enterAnimByTransition(mThumbnailView); + } + + /** + * 无动画进入 + */ + private void initNoAnim() { + mRootViewBgMask.setBackgroundColor(Color.BLACK); + mHelperViewParent.setVisibility(View.INVISIBLE); + } + + /** + * 仅缩放动画 + */ + private void enterAnimByScale() { + ObjectAnimator scaleOx = ObjectAnimator.ofFloat(mHelperView, "scaleX", 0, 1f); + ObjectAnimator scaleOy = ObjectAnimator.ofFloat(mHelperView, "scaleY", 0, 1f); + + callOnOpen(ANIM_START_PRE); + AnimatorSet set = new AnimatorSet(); + set.setDuration(mAnimDuration); + set.setInterpolator(INTERPOLATOR); + set.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + callOnOpen(ANIM_START); + mHelperViewParent.setVisibility(View.VISIBLE); + } + + @Override + public void onAnimationEnd(Animator animation) { + mHelperViewParent.setVisibility(View.INVISIBLE); + callOnOpen(ANIM_END); + } + }); + + set.playTogether(scaleOx, scaleOy, getViewBgAnim(Color.BLACK, mAnimDuration, null)); + set.start(); + } + + /** + * 使用Transition库实现过度动画 + */ + private void enterAnimByTransition(final View thumbnailView) { + long delay = mShareData.openAnimDelayTime; + callOnOpen(ANIM_START_PRE); + if (delay > 0) { + viewPostDelayed(this::doEnterAnimByTransition, delay, mHelperView); + initEnterAnimByTransition(thumbnailView); + } else { + initEnterAnimByTransition(thumbnailView); + viewPost(this::doEnterAnimByTransition, mHelperView); + } + } + + /** + * 初始化Transition所需内容 + */ + private void initEnterAnimByTransition(final View thumbnailView) { + getSrcViewSize(thumbnailView); + getSrcViewLocation(thumbnailView); + + mHelperViewParent.setTranslationX(mSrcImageLocation[0]); + mHelperViewParent.setTranslationY(mSrcImageLocation[1]); + setViewSize(mHelperViewParent, mSrcViewParentSize[0], mSrcViewParentSize[1]); + + setHelperViewDataByThumbnail(); + } + + /** + * 展示/隐藏缩略图蒙层数据 + */ + void showThumbnailViewMask(boolean show) { + if (!mShareData.config.showThumbnailViewMask) { + return; + } + + mThumbnailView.setVisibility(show ? View.INVISIBLE : mThumbnailViewVisibility); + } + + /** + * 根据缩列图数据设置预览占位View大小、位置和缩放模式 + */ + private void setHelperViewDataByThumbnail() { + if (mThumbnailViewScaleType != null) { + mHelperView.setScaleType(mThumbnailViewScaleType); + } else { + mHelperView.setScaleType(ScaleType.FIT_CENTER); + } + + mHelperView.setTranslationX(0); + mHelperView.setTranslationY(0); + if (mShareData == null || mShareData.config.shapeTransformType == null || mThumbnailViewScaleType == null) { + setViewSize(mHelperView, mSrcViewSize[0], mSrcViewSize[1]); + return; + } + + // 需要进行图片裁剪,部分缩放类型不能使图片充满控件,而裁剪基于控件,因此设置控件大小与图片大小一致 + Drawable drawable = ((ImageView) mThumbnailView).getDrawable(); + if (drawable == null) { + setViewSize(mHelperView, mSrcViewSize[0], mSrcViewSize[1]); + return; + } + + int intrinsicWidth = drawable.getIntrinsicWidth(); + int intrinsicHeight = drawable.getIntrinsicHeight(); + if (intrinsicWidth <= 0 || intrinsicHeight <= 0) { + setViewSize(mHelperView, mSrcViewSize[0], mSrcViewSize[1]); + return; + } + + int width = mSrcViewSize[0]; + int height = mSrcViewSize[1]; + if (mShareData.config.shapeTransformType == ShapeTransformType.CIRCLE) { + int minSize = Math.min(width, height); + switch (mThumbnailViewScaleType) { + case FIT_START: + case MATRIX: + width = minSize; + height = minSize; + break; + + case FIT_END: + mHelperView.setTranslationX(width - minSize); + mHelperView.setTranslationY(height - minSize); + width = minSize; + height = minSize; + break; + + case FIT_CENTER: + mHelperView.setTranslationX((width - minSize) / 2f); + mHelperView.setTranslationY((height - minSize) / 2f); + width = minSize; + height = minSize; + break; + + case CENTER_INSIDE: + if (intrinsicWidth < width && intrinsicHeight < height) { + minSize = Math.min(intrinsicWidth, intrinsicHeight); + } + + mHelperView.setTranslationX((width - minSize) / 2f); + mHelperView.setTranslationY((height - minSize) / 2f); + width = minSize; + height = minSize; + break; + + case CENTER: + int w = Math.min(intrinsicWidth, width); + int h = Math.min(intrinsicHeight, height); + minSize = Math.min(w, h); + + mHelperView.setTranslationX((width - minSize) / 2f); + mHelperView.setTranslationY((height - minSize) / 2f); + width = minSize; + height = minSize; + break; + } + + // 需要重新设置缩放类型为CENTER_CROP,否则动画不会无缝衔接 + // 重新调整缩放类型,全部都是为了适配 Transition动画ChangeImageTransform过渡 + mHelperView.setScaleType(ScaleType.CENTER_CROP); + setViewSize(mHelperView, width, height); + return; + } + + switch (mThumbnailViewScaleType) { + case FIT_START: + case FIT_END: + case FIT_CENTER: + case CENTER_INSIDE: + if (intrinsicWidth < width && intrinsicHeight < height) { + if (mThumbnailViewScaleType == ScaleType.CENTER_INSIDE) { + mHelperView.setTranslationX((width - intrinsicWidth) / 2f); + mHelperView.setTranslationY((height - intrinsicHeight) / 2f); + width = intrinsicWidth; + height = intrinsicHeight; + } else { + float widthScale = width * 1f / intrinsicWidth; + float heightScale = height * 1f / intrinsicHeight; + if (widthScale < heightScale) { + // 根据宽度缩放值缩放高度,放大 + intrinsicHeight = (int) (widthScale * intrinsicHeight); + if (mThumbnailViewScaleType == ScaleType.FIT_END) { + mHelperView.setTranslationY(height - intrinsicHeight); + } else if (mThumbnailViewScaleType == ScaleType.FIT_CENTER) { + mHelperView.setTranslationY((height - intrinsicHeight) / 2f); + } + height = intrinsicHeight; + } else if (widthScale > heightScale) { + // 根据高度缩放值缩放宽度,放大 + intrinsicWidth = (int) (heightScale * intrinsicWidth); + if (mThumbnailViewScaleType == ScaleType.FIT_END) { + mHelperView.setTranslationX(width - intrinsicWidth); + } else if (mThumbnailViewScaleType == ScaleType.FIT_CENTER) { + mHelperView.setTranslationX((width - intrinsicWidth) / 2f); + } + width = intrinsicWidth; + } + } + } else if (intrinsicWidth > width && intrinsicHeight > height) { + float widthScale = intrinsicWidth * 1f / width; + float heightScale = intrinsicHeight * 1f / height; + + if (widthScale > heightScale) { + // 根据宽度缩放值缩放高度,缩小 + intrinsicHeight = (int) (intrinsicHeight / widthScale); + if (mThumbnailViewScaleType == ScaleType.FIT_END) { + mHelperView.setTranslationY(height - intrinsicHeight); + } else if (mThumbnailViewScaleType == ScaleType.FIT_CENTER + || mThumbnailViewScaleType == ScaleType.CENTER_INSIDE) { + mHelperView.setTranslationY((height - intrinsicHeight) / 2f); + } + height = intrinsicHeight; + } else if (widthScale < heightScale) { + // 根据高度缩放值缩放宽度,缩小 + intrinsicWidth = (int) (intrinsicWidth / heightScale); + if (mThumbnailViewScaleType == ScaleType.FIT_END) { + mHelperView.setTranslationX(width - intrinsicWidth); + } else if (mThumbnailViewScaleType == ScaleType.FIT_CENTER + || mThumbnailViewScaleType == ScaleType.CENTER_INSIDE) { + mHelperView.setTranslationX((width - intrinsicWidth) / 2f); + } + width = intrinsicWidth; + } + } else if (intrinsicWidth < width) { + if (intrinsicHeight > height) { + // 根据高度缩放值缩放宽度,缩小 + float heightScale = intrinsicHeight * 1f / height; + intrinsicWidth = (int) (intrinsicWidth / heightScale); + } + + if (mThumbnailViewScaleType == ScaleType.FIT_END) { + mHelperView.setTranslationX(width - intrinsicWidth); + } else if (mThumbnailViewScaleType == ScaleType.FIT_CENTER + || mThumbnailViewScaleType == ScaleType.CENTER_INSIDE) { + mHelperView.setTranslationX((width - intrinsicWidth) / 2f); + } + width = intrinsicWidth; + } else { + if (intrinsicWidth > width) { + // 根据宽度缩放值缩放高度,缩小 + float widthScale = intrinsicWidth * 1f / width; + intrinsicHeight = (int) (intrinsicHeight / widthScale); + } + + if (mThumbnailViewScaleType == ScaleType.FIT_END) { + mHelperView.setTranslationY(height - intrinsicHeight); + } else if (mThumbnailViewScaleType == ScaleType.FIT_CENTER + || mThumbnailViewScaleType == ScaleType.CENTER_INSIDE) { + mHelperView.setTranslationY((height - intrinsicHeight) / 2f); + } + height = intrinsicHeight; + } + + if (mThumbnailViewScaleType == ScaleType.FIT_CENTER + || mThumbnailViewScaleType == ScaleType.CENTER_INSIDE) { + // 需要重新设置缩放类型为CENTER_CROP,否则动画不会无缝衔接 + // 重新调整缩放类型,全部都是为了适配 Transition动画ChangeImageTransform过渡 + mHelperView.setScaleType(ScaleType.CENTER_CROP); + } + break; + + case CENTER: + if (width > intrinsicWidth) { + mHelperView.setTranslationX((width - intrinsicWidth) / 2f); + } + + if (height > intrinsicHeight) { + mHelperView.setTranslationY((height - intrinsicHeight) / 2f); + } + + width = Math.min(intrinsicWidth, width); + height = Math.min(intrinsicHeight, height); + // 需要重新设置缩放类型为CENTER_CROP,否则动画不会无缝衔接 + // 重新调整缩放类型,全部都是为了适配 Transition动画ChangeImageTransform过渡 + mHelperView.setScaleType(ScaleType.CENTER_CROP); + break; + + case MATRIX: + width = Math.min(intrinsicWidth, width); + height = Math.min(intrinsicHeight, height); + mHelperView.setScaleType(ScaleType.FIT_START); + break; + } + + setViewSize(mHelperView, width, height); + } + + /** + * 执行预览打开过渡动画 + */ + private void doEnterAnimByTransition() { + TransitionSet transitionSet = new TransitionSet() + .setDuration(mAnimDuration) + .addTransition(new ChangeBounds()) + .addTransition(new ChangeTransform()) + .setInterpolator(INTERPOLATOR) + .addListener(new TransitionListenerAdapter() { + @Override + public void onTransitionStart(@NonNull Transition transition) { + callOnOpen(ANIM_START); + mHelpViewCanSetImage = false; + doViewBgAnim(Color.BLACK, mAnimDuration, null); + mHelperViewParent.setVisibility(View.VISIBLE); + // 不延迟会有闪屏 + viewPostDelayed(() -> showThumbnailViewMask(true), mAnimDuration / 10, mThumbnailView, mHelperView); + } + + @Override + public void onTransitionEnd(@NonNull Transition transition) { + showThumbnailViewMask(false); + mHelpViewCanSetImage = true; + mHelperViewParent.setVisibility(View.INVISIBLE); + callOnOpen(ANIM_END); + } + }); + + if (mShareData.config.shapeTransformType != null) { + if (mShareData.config.shapeTransformType == ShapeTransformType.CIRCLE) { + transitionSet.addTransition( + new ChangeShape(Math.min(mSrcViewSize[0], mSrcViewSize[1]) / 2f, 0) + .addTarget(mHelperView)); + } else { + transitionSet.addTransition( + new ChangeShape(mShareData.config.shapeCornerRadius, 0) + .addTarget(mHelperView)); + } + } + + if (mHelperView.getDrawable() != null) { + mHelpViewCanSetImage = false; + // ChangeImageTransform执行MATRIX变换,因此一定需要最终加载完成图片 + transitionSet.addTransition(new ChangeImageTransform().addTarget(mHelperView)); + } + + TransitionManager.beginDelayedTransition((ViewGroup) mHelperViewParent.getParent(), transitionSet); + + mHelperViewParent.setTranslationX(0); + mHelperViewParent.setTranslationY(0); + setViewSize(mHelperViewParent, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + + mHelperView.setTranslationX(0); + mHelperView.setTranslationY(0); + mHelperView.setScaleType(ScaleType.FIT_CENTER); + setViewSize(mHelperView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + } + + /** + * 设置View的大小 + */ + private void setViewSize(View target, int width, int height) { + LayoutParams params = target.getLayoutParams(); + params.width = width; + params.height = height; + target.setLayoutParams(params); + } + + /** + * 退出预览 + * + * @return {@code false}:未执行退出逻辑,可能当前界面已关闭或还未创建完成 + */ + public boolean exit() { + if (!mFragment.getLifecycle().getCurrentState().isAtLeast(State.STARTED)) { + return false; + } + + if (mAnimDuration <= 0) { + callOnExit(ANIM_START_PRE, ANIM_START, ANIM_END); + return true; + } + + View view = mFragment.mViewPager.findViewWithTag(mFragment.mViewPager.getCurrentItem()); + if (view == null) { + callOnExit(ANIM_START_PRE, ANIM_START, ANIM_END); + return true; + } + + Object tag = view.getTag(R.id.view_holder); + if (!(tag instanceof ViewHolder)) { + callOnExit(ANIM_START_PRE, ANIM_START, ANIM_END); + return true; + } + + ViewHolder viewHolder = (ViewHolder) tag; + PhotoView photoView = viewHolder.getPhotoView(); + viewHolder.getLoading().setVisibility(View.GONE); + + if (photoView.getDrawable() == null) { + callOnExit(ANIM_START_PRE, ANIM_START); + doViewBgAnim(Color.TRANSPARENT, mAnimDuration, new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + callOnExit(ANIM_END); + } + }); + return true; + } + + // 记录预览打开时,缩略图对象 + View openThumbnailView = mThumbnailView; + mThumbnailView = getThumbnailView(mShareData); + if (openThumbnailView != mThumbnailView) { + if (mThumbnailView != null) { + mThumbnailViewVisibility = mThumbnailView.getVisibility(); + } + mAnimDuration = getOpenAndExitAnimDuration(mThumbnailView, mShareData); + initThumbnailViewScaleType(); + } + + // 关闭时,最小缩放比设置为0f,否则手指放开,预览图又会回到1倍图大小,导致计算不准确 + photoView.setMinimumScale(0f); + resetHelpViewSize(viewHolder); + + if (mThumbnailView == null) { + callOnOpen(ANIM_START_PRE); + ObjectAnimator scaleOx = ObjectAnimator.ofFloat(mHelperView, "scaleX", 1f, 0f); + ObjectAnimator scaleOy = ObjectAnimator.ofFloat(mHelperView, "scaleY", 1f, 0f); + AnimatorSet set = new AnimatorSet(); + set.setDuration(mAnimDuration); + set.setInterpolator(INTERPOLATOR); + set.playTogether(scaleOx, scaleOy, getViewBgAnim(Color.TRANSPARENT, mAnimDuration, null)); + + set.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + callOnExit(ANIM_START); + mHelperViewParent.setVisibility(View.VISIBLE); + } + + @Override + public void onAnimationEnd(Animator animation) { + callOnExit(ANIM_END); + } + }); + + set.start(); + return true; + } + + exitAnimByTransition(mThumbnailView, photoView, openThumbnailView); + return true; + } + + /** + * 重置辅助View的大小 + */ + private void resetHelpViewSize(ViewHolder viewHolder) { + PhotoView photoView = viewHolder.getPhotoView(); + float[] noScaleImageActualSize = viewHolder.getNoScaleImageActualSize(); + FrameLayout rootView = mFragment.mRootView; + + if (mThumbnailViewScaleType == ScaleType.MATRIX || photoView.getScale() != 1) { + // thumbnailView是ScaleType.MATRIX需要设置ImageView与drawable大小一致,且如果是下拉后缩小后关闭预览,则肯能无法与缩略图无缝衔接 + // 得到关闭时预览图片真实绘制大小 + float[] imageActualSize = mFloatTemp; + getImageActualSize(photoView, imageActualSize); + if (noScaleImageActualSize[0] == 0 && noScaleImageActualSize[1] == 0) { + // 此时只是降低偏移值,但是这是不准确的 + getImageActualSize(mHelperView, noScaleImageActualSize); + } + + if (photoView.getScale() < 1 || (mThumbnailViewScaleType == ScaleType.MATRIX && photoView.getScale() == 1)) { + // 计算关闭时预览图片真实X,Y坐标 + // mPhotoView向下移动后缩放比例误差值,该值是手动调出来的,不清楚为什么超出了这么多 + // 只有mPhotoView.getScale() < 1时才会出现此误差 + float errorRatio = photoView.getScale() < 1 ? 0.066f : 0; + // 此处取mRoot.getHeight()而不是mIvAnim.getHeight()是因为如果当前界面非默认预览的界面, + // 那么mIvAnim.getHeight()获取的并不是可显示区域高度,只是图片实际绘制的高度,因此使用mRoot.getHeight() + float y = rootView.getHeight() / 2f - noScaleImageActualSize[1] / 2 // 预览图片未移动未缩放时实际绘制drawable左上角Y轴值 + - photoView.getScrollY() // 向下移动的距离,向上移动不会触发关闭 + + imageActualSize[1] * (1 - photoView.getScale() - errorRatio); // 由于在向下移动时,伴随图片缩小,因此需要加上缩小高度 + float x = rootView.getWidth() / 2f - noScaleImageActualSize[0] / 2 // 预览图片未移动未缩放时实际绘制drawable左上角X轴值 + - photoView.getScrollX() // 向左或向右移动的距离 + + imageActualSize[0] * (1 - photoView.getScale() - errorRatio); // 由于在向下移动时,伴随图片缩小,因此需要加上缩小宽度 + + mHelperViewParent.setTranslationX(x); + mHelperViewParent.setTranslationY(y); + } else if (photoView.getScale() > 1) { + Matrix imageMatrix = photoView.getImageMatrix(); + float scrollX = MatrixUtils.getValue(imageMatrix, Matrix.MTRANS_X); + float scrollY = MatrixUtils.getValue(imageMatrix, Matrix.MTRANS_Y); + float y = imageActualSize[1] > rootView.getHeight() ? scrollY : rootView.getHeight() / 2f - imageActualSize[1] / 2f; + float x = imageActualSize[0] > rootView.getWidth() ? scrollX : rootView.getWidth() / 2f - imageActualSize[0] / 2f; + + mHelperViewParent.setTranslationX(x); + mHelperViewParent.setTranslationY(y); + } + setViewSize(mHelperViewParent, ((int) imageActualSize[0]), ((int) imageActualSize[1])); + setViewSize(mHelperView, ((int) imageActualSize[0]), ((int) imageActualSize[1])); + } + } + + /** + * 使用Transition库实现过度动画 + */ + private void exitAnimByTransition(final View thumbnailView, final PhotoView photoView, View openThumbnailView) { + callOnExit(ANIM_START_PRE); + + mHelperView.setScaleType(ScaleType.FIT_CENTER); + mHelperView.setImageDrawable(photoView.getDrawable()); + + viewPostDelayed(() -> { + // 延迟100毫秒后计算缩略图位置,因为关闭时存在全屏->非全屏或非全屏->全屏的转换,此时缩略图位置可能发生了改变 + if (thumbnailView == openThumbnailView) { + // 进入和退出缩略图位置不变 + // 获取退出时缩略图位置 + getSrcViewLocation(thumbnailView); + } else { + getSrcViewLocation(thumbnailView); + getSrcViewSize(thumbnailView); + } + + viewPost(() -> { + TransitionSet transitionSet = new TransitionSet() + .setDuration(mAnimDuration) + .addTransition(new ChangeBounds()) + .addTransition(new ChangeTransform()) + .addTransition(new ChangeImageTransform().addTarget(mHelperView)) + .setInterpolator(INTERPOLATOR) + .addListener(new TransitionListenerAdapter() { + @Override + public void onTransitionEnd(@NonNull Transition transition) { + showThumbnailViewMask(false); + if (VERSION.SDK_INT >= 33) { + // android13需要先将预览图隐藏再关闭预览的dialog,否则出现预览图发生位移闪屏 + mHelperViewParent.setVisibility(View.INVISIBLE); + viewPost(() -> callOnExit(ANIM_END), mHelperViewParent); + } else { + callOnExit(ANIM_END); + } + } + + @Override + public void onTransitionStart(@NonNull Transition transition) { + callOnExit(ANIM_START); + if (photoView.getScale() >= 1) { + showThumbnailViewMask(true); + } + mHelperViewParent.setVisibility(View.VISIBLE); + doViewBgAnim(Color.TRANSPARENT, mAnimDuration, null); + } + }); + + if (mShareData != null && mShareData.config.shapeTransformType != null) { + if (mShareData.config.shapeTransformType == ShapeTransformType.CIRCLE) { + transitionSet.addTransition( + new ChangeShape(0, Math.min(mSrcViewSize[0], mSrcViewSize[1]) / 2f) + .addTarget(mHelperView)); + } else { + transitionSet.addTransition( + new ChangeShape(0, mShareData.config.shapeCornerRadius) + .addTarget(mHelperView)); + } + } + + TransitionManager.beginDelayedTransition((ViewGroup) mHelperViewParent.getParent(), transitionSet); + + mHelperViewParent.setTranslationX(mSrcImageLocation[0]); + mHelperViewParent.setTranslationY(mSrcImageLocation[1]); + setViewSize(mHelperViewParent, mSrcViewParentSize[0], mSrcViewParentSize[1]); + + setHelperViewDataByThumbnail(); + }, mHelperView); + }, 100, mThumbnailView, mHelperView); + } + + private void viewPostDelayed(Runnable runnable, long delayMillis, View... views) { + for (View view : views) { + if (view.postDelayed(runnable, delayMillis)) { + return; + } + } + runnable.run(); + } + + private void viewPost(Runnable runnable, View... views) { + for (View view : views) { + if (view.post(runnable)) { + return; + } + } + runnable.run(); + } + + /** + * 执行背景过渡动画 + */ + void doViewBgAnim(final int endColor, long duration, AnimatorListenerAdapter listenerAdapter) { + getViewBgAnim(endColor, duration, listenerAdapter).start(); + } + + /** + * 返回背景过渡动画 + */ + Animator getViewBgAnim(final int endColor, long duration, AnimatorListenerAdapter listenerAdapter) { + final int start = ((ColorDrawable) mRootViewBgMask.getBackground()).getColor(); + ValueAnimator animator = ValueAnimator.ofFloat(0, 1f); + animator.addUpdateListener(animation -> + mRootViewBgMask.setBackgroundColor((int) ARGB_EVALUATOR.evaluate(animation.getAnimatedFraction(), start, endColor))); + animator.setDuration(duration); + animator.setInterpolator(INTERPOLATOR); + if (listenerAdapter != null) { + animator.addListener(listenerAdapter); + } + return animator; + } + + /** + * 获取动画时间 + */ + private long getOpenAndExitAnimDuration(View thumbnailView, ShareData shareData) { + if (shareData.config.animDuration != null) { + return shareData.config.animDuration; + } + + if (thumbnailView instanceof ImageView) { + return OPEN_AND_EXIT_ANIM_DURATION_FOR_IMAGE; + } + return OPEN_AND_EXIT_ANIM_DURATION; + } + + /** + * 获取ImageView实际绘制的图片大小,如果没有设置图片,则返回数据为0。 + * 该方法调用时机不同,返回值有很大差别,如果刚设置imageView drawable, + * 则可能返回的是drawable原图大小,而不是在imageView中实际绘制出来的大小 + */ + private void getImageActualSize(ImageView imageView, float[] size) { + size[0] = 0; + size[1] = 0; + if (imageView == null || imageView.getDrawable() == null) { + return; + } + + Drawable drawable = imageView.getDrawable(); + // 获得ImageView中Image的真实宽高, + int dw = drawable.getBounds().width(); + int dh = drawable.getBounds().height(); + + // 获得ImageView中Image的变换矩阵 + Matrix m = imageView.getImageMatrix(); + // Image在绘制过程中的变换矩阵,从中获得x和y方向的缩放系数 + float sx = MatrixUtils.getValue(m, Matrix.MSCALE_X); + float sy = MatrixUtils.getValue(m, Matrix.MSCALE_Y); + + // 计算Image在屏幕上实际绘制的宽高 + size[0] = dw * sx; + size[1] = dh * sy; + } + + /** + * 获取当前预览图对应的缩略图View,如果未找到则查找默认位置View + */ + @Nullable + private View getThumbnailView(ShareData shareData) { + View view = getThumbnailViewNotDefault(shareData, mPosition); + if (view == null) { + if (mPosition != shareData.config.defaultShowPosition) { + return getThumbnailViewNotDefault(shareData, shareData.config.defaultShowPosition); + } + } + + return view; + } + + /** + * 获取指定位置的缩略图 + */ + @Nullable + private View getThumbnailViewNotDefault(ShareData shareData, int position) { + if (shareData.thumbnailView != null) { + return shareData.thumbnailView; + } else if (shareData.findThumbnailView != null) { + return shareData.findThumbnailView.findView(position); + } + + return null; + } + + /** + * 获取指定位置的略图的大小. + * 结果存储在{@link #mSrcViewSize} + */ + private void getSrcViewSize(View view) { + mSrcViewSize[0] = 0; + mSrcViewSize[1] = 0; + mSrcViewParentSize[0] = 0; + mSrcViewParentSize[1] = 0; + + if (view == null) { + return; + } + + mSrcViewSize[0] = view.getWidth(); + mSrcViewSize[1] = view.getHeight(); + + mSrcViewParentSize[0] = mSrcViewSize[0]; + mSrcViewParentSize[1] = mSrcViewSize[1]; + // 暂时不处理缩略图父类比自己小的情况,主要原因是无法确定缩略图在父类布局方式 + // getSrcViewParentSize(view.getParent()); + } + + /** + * 获取父类最小宽高,可能View设置了固定宽高,但是父类被裁剪,因此实际绘制区域可能没有设定宽高那么大 + */ + private void getSrcViewParentSize(ViewParent parent) { + if (parent instanceof View) { + int width = ((View) parent).getWidth(); + if (width < mSrcViewParentSize[0]) { + mSrcViewParentSize[0] = width; + } + + int height = ((View) parent).getHeight(); + if (height < mSrcViewParentSize[1]) { + mSrcViewParentSize[1] = height; + } + + if (width > mSrcViewParentSize[0] && height > mSrcViewParentSize[1]) { + return; + } + + getSrcViewParentSize(parent.getParent()); + } + } + + /** + * 获取指定位置缩略图的位置 + * 结果存储在{@link #mSrcImageLocation} + */ + private void getSrcViewLocation(View view) { + mSrcImageLocation[0] = 0; + mSrcImageLocation[1] = 0; + + if (view == null) { + return; + } + + view.getLocationOnScreen(mSrcImageLocation); + // 预览界面采用沉浸式全屏显示模式,如果手机系统支持,横竖屏都绘制到耳朵区域 + // 以下逻辑防止部分手机横屏时,耳朵区域不显示内容,此时设置的预览坐标不能采用OnScreen坐标 + mFragment.mRootView.getLocationOnScreen(mIntTemp); + mSrcImageLocation[0] -= mIntTemp[0]; + mSrcImageLocation[1] -= mIntTemp[1]; + } + + /** + * 获取指定位置的略图的背景颜色 + */ + private Drawable getSrcViewBg(View view) { + ColorDrawable drawable = getSrcParentBg(view); + if (drawable != null) { + ColorDrawable colorDrawable = new ColorDrawable(drawable.getColor()); + if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + colorDrawable.setColorFilter(drawable.getColorFilter()); + } + colorDrawable.setAlpha(drawable.getAlpha()); + colorDrawable.setState(drawable.getState()); + return colorDrawable; + } + + return null; + } + + /** + * 获取指定位置的略图的背景颜色 + */ + private ColorDrawable getSrcParentBg(View view) { + Drawable background = view.getBackground(); + if (background instanceof ColorDrawable) { + return (ColorDrawable) background; + } + + ViewParent parent = view.getParent(); + if (!(parent instanceof View)) { + return null; + } + + return getSrcParentBg((View) parent); + } + + /** + * 预览界面打开时执行回调 + */ + private void callOnOpen(int... animTypes) { + List list = new ArrayList<>(); + if (mShareData != null && mShareData.onOpenListener != null) { + list.add(mShareData.onOpenListener); + } + + if (mOpenListenerList != null) { + list.addAll(mOpenListenerList); + } + + for (int type : animTypes) { + if (type == ANIM_END) { + mOpenAnimEnd = true; + break; + } + } + + for (int type : animTypes) { + for (OnOpenListener onOpenListener : list) { + if (type == ANIM_START_PRE) { + onOpenListener.onStartPre(); + } else if (type == ANIM_START) { + onOpenListener.onStart(); + } else if (type == ANIM_END) { + onOpenListener.onEnd(); + } + } + } + } + + /** + * 预览图拖拽导致缩放倍率改变 + */ + void dragScaleChange(float scale) { + if (scale < 1) { + if (!oldScaleLessOne) { + mFragment.initFullScreen(false); + } + oldScaleLessOne = true; + } else { + if (oldScaleLessOne) { + mFragment.initFullScreen(true); + } + oldScaleLessOne = false; + } + } + + /** + * 预览界面关闭时执行回调 + */ + private void callOnExit(int... animTypes) { + List list = new ArrayList<>(); + if (mShareData != null && mShareData.onExitListener != null) { + list.add(mShareData.onExitListener); + } + + if (mExitListenerList != null) { + list.addAll(mExitListenerList); + } + + boolean isAnimEnd = false; + for (int type : animTypes) { + if (type == ANIM_END) { + isAnimEnd = true; + } + + for (OnExitListener onExitListener : list) { + if (type == ANIM_START_PRE) { + onExitListener.onStartPre(); + } else if (type == ANIM_START) { + onExitListener.onStart(); + } else if (type == ANIM_END) { + onExitListener.onExit(); + } + } + } + + if (isAnimEnd) { + if (mOpenListenerList != null) { + mOpenListenerList.clear(); + } + + if (mExitListenerList != null) { + mExitListenerList.clear(); + } + } + } + + /** + * 预览打开动画执行结束 + */ + boolean isOpenAnimEnd() { + return mOpenAnimEnd; + } + + /** + * 设置当前预览图片的位置 + */ + public void setPosition(int position) { + mPosition = position; + } + + /** + * 增加预览动画打开监听 + */ + void addOnOpenListener(OnOpenListener openListener) { + if (openListener == null) { + return; + } + + if (mOpenListenerList == null) { + mOpenListenerList = new ArrayList<>(); + } + mOpenListenerList.add(openListener); + } + + /** + * 移除预览动画打开监听 + */ + void removeOnOpenListener(OnOpenListener openListener) { + if (openListener == null || mOpenListenerList == null) { + return; + } + + mOpenListenerList.remove(openListener); + } + + /** + * 增加预览动画关闭监听 + */ + void addOnExitListener(OnExitListener onExitListener) { + if (onExitListener == null) { + return; + } + + if (mExitListenerList == null) { + mExitListenerList = new ArrayList<>(); + } + mExitListenerList.add(onExitListener); + } + + /** + * 移除预览动画关闭监听 + */ + void removeOnExitListener(OnExitListener onExitListener) { + if (onExitListener == null || mExitListenerList == null) { + return; + } + + mExitListenerList.remove(onExitListener); + } + + /** + * 预览退出监听 + */ + public interface OnExitListener { + /** + * 动画开始之前数据准备阶段 + */ + void onStartPre(); + + /** + * 退出动作开始执行 + */ + void onStart(); + + /** + * 完全退出 + */ + void onExit(); + } + + /** + * 预览打开监听 + */ + public interface OnOpenListener { + /** + * 动画开始之前数据准备阶段 + */ + void onStartPre(); + + /** + * 进入动画开始执行 + */ + void onStart(); + + /** + * 进入动画开始执行结束 + */ + void onEnd(); + } +} diff --git a/PhotoPreview/src/main/java/com/wgw/photo/preview/PhotoView.java b/PhotoPreview/src/main/java/com/wgw/photo/preview/PhotoView.java new file mode 100644 index 0000000..a48d3f9 --- /dev/null +++ b/PhotoPreview/src/main/java/com/wgw/photo/preview/PhotoView.java @@ -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.
+ * + * @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); + } +} diff --git a/PhotoPreview/src/main/java/com/wgw/photo/preview/PreloadImageView.java b/PhotoPreview/src/main/java/com/wgw/photo/preview/PreloadImageView.java new file mode 100644 index 0000000..752fa74 --- /dev/null +++ b/PhotoPreview/src/main/java/com/wgw/photo/preview/PreloadImageView.java @@ -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); + } +} diff --git a/PhotoPreview/src/main/java/com/wgw/photo/preview/PreviewDialogFragment.java b/PhotoPreview/src/main/java/com/wgw/photo/preview/PreviewDialogFragment.java new file mode 100644 index 0000000..6756ed7 --- /dev/null +++ b/PhotoPreview/src/main/java/com/wgw/photo/preview/PreviewDialogFragment.java @@ -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); + } +} diff --git a/PhotoPreview/src/main/java/com/wgw/photo/preview/ShapeTransformType.java b/PhotoPreview/src/main/java/com/wgw/photo/preview/ShapeTransformType.java new file mode 100644 index 0000000..9ead350 --- /dev/null +++ b/PhotoPreview/src/main/java/com/wgw/photo/preview/ShapeTransformType.java @@ -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; +} diff --git a/PhotoPreview/src/main/java/com/wgw/photo/preview/ShareData.java b/PhotoPreview/src/main/java/com/wgw/photo/preview/ShareData.java new file mode 100644 index 0000000..08f23fa --- /dev/null +++ b/PhotoPreview/src/main/java/com/wgw/photo/preview/ShareData.java @@ -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; + } +} diff --git a/PhotoPreview/src/main/java/com/wgw/photo/preview/interfaces/IFindThumbnailView.java b/PhotoPreview/src/main/java/com/wgw/photo/preview/interfaces/IFindThumbnailView.java new file mode 100644 index 0000000..e40412e --- /dev/null +++ b/PhotoPreview/src/main/java/com/wgw/photo/preview/interfaces/IFindThumbnailView.java @@ -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); +} diff --git a/PhotoPreview/src/main/java/com/wgw/photo/preview/interfaces/ImageLoader.java b/PhotoPreview/src/main/java/com/wgw/photo/preview/interfaces/ImageLoader.java new file mode 100644 index 0000000..4c3b2db --- /dev/null +++ b/PhotoPreview/src/main/java/com/wgw/photo/preview/interfaces/ImageLoader.java @@ -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); +} diff --git a/PhotoPreview/src/main/java/com/wgw/photo/preview/interfaces/OnDismissListener.java b/PhotoPreview/src/main/java/com/wgw/photo/preview/interfaces/OnDismissListener.java new file mode 100644 index 0000000..4d928ea --- /dev/null +++ b/PhotoPreview/src/main/java/com/wgw/photo/preview/interfaces/OnDismissListener.java @@ -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(); +} diff --git a/PhotoPreview/src/main/java/com/wgw/photo/preview/interfaces/OnImageLongClickListener.java b/PhotoPreview/src/main/java/com/wgw/photo/preview/interfaces/OnImageLongClickListener.java new file mode 100644 index 0000000..44f9b0a --- /dev/null +++ b/PhotoPreview/src/main/java/com/wgw/photo/preview/interfaces/OnImageLongClickListener.java @@ -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); +} diff --git a/PhotoPreview/src/main/java/com/wgw/photo/preview/interfaces/OnLongClickListener.java b/PhotoPreview/src/main/java/com/wgw/photo/preview/interfaces/OnLongClickListener.java new file mode 100644 index 0000000..5ed78a3 --- /dev/null +++ b/PhotoPreview/src/main/java/com/wgw/photo/preview/interfaces/OnLongClickListener.java @@ -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); +} diff --git a/PhotoPreview/src/main/java/com/wgw/photo/preview/util/MatrixUtils.java b/PhotoPreview/src/main/java/com/wgw/photo/preview/util/MatrixUtils.java new file mode 100644 index 0000000..2ba09d9 --- /dev/null +++ b/PhotoPreview/src/main/java/com/wgw/photo/preview/util/MatrixUtils.java @@ -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)); + } +} diff --git a/PhotoPreview/src/main/java/com/wgw/photo/preview/util/SpannableString.java b/PhotoPreview/src/main/java/com/wgw/photo/preview/util/SpannableString.java new file mode 100644 index 0000000..1f7e7c1 --- /dev/null +++ b/PhotoPreview/src/main/java/com/wgw/photo/preview/util/SpannableString.java @@ -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 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 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 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 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 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); + } +} diff --git a/PhotoPreview/src/main/java/com/wgw/photo/preview/util/Utils.java b/PhotoPreview/src/main/java/com/wgw/photo/preview/util/Utils.java new file mode 100644 index 0000000..656df93 --- /dev/null +++ b/PhotoPreview/src/main/java/com/wgw/photo/preview/util/Utils.java @@ -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; + } +} diff --git a/PhotoPreview/src/main/java/com/wgw/photo/preview/util/notch/CutOutMode.java b/PhotoPreview/src/main/java/com/wgw/photo/preview/util/notch/CutOutMode.java new file mode 100644 index 0000000..f999b14 --- /dev/null +++ b/PhotoPreview/src/main/java/com/wgw/photo/preview/util/notch/CutOutMode.java @@ -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; +} diff --git a/PhotoPreview/src/main/java/com/wgw/photo/preview/util/notch/NotchAdapterUtils.java b/PhotoPreview/src/main/java/com/wgw/photo/preview/util/notch/NotchAdapterUtils.java new file mode 100644 index 0000000..b170a31 --- /dev/null +++ b/PhotoPreview/src/main/java/com/wgw/photo/preview/util/notch/NotchAdapterUtils.java @@ -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; + +/** + * 全屏刘海屏适配 + * + * + * @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"); + } +} diff --git a/PhotoPreview/src/main/java/com/wgw/photo/preview/util/notch/OSUtils.java b/PhotoPreview/src/main/java/com/wgw/photo/preview/util/notch/OSUtils.java new file mode 100644 index 0000000..d5a9371 --- /dev/null +++ b/PhotoPreview/src/main/java/com/wgw/photo/preview/util/notch/OSUtils.java @@ -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; + } +} + + diff --git a/PhotoPreview/src/main/res/drawable/no_selected_dot.xml b/PhotoPreview/src/main/res/drawable/no_selected_dot.xml new file mode 100644 index 0000000..881d6ab --- /dev/null +++ b/PhotoPreview/src/main/res/drawable/no_selected_dot.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/PhotoPreview/src/main/res/drawable/selected_dot.xml b/PhotoPreview/src/main/res/drawable/selected_dot.xml new file mode 100644 index 0000000..59991ed --- /dev/null +++ b/PhotoPreview/src/main/res/drawable/selected_dot.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/PhotoPreview/src/main/res/layout/fragment_preview.xml b/PhotoPreview/src/main/res/layout/fragment_preview.xml new file mode 100644 index 0000000..037ef35 --- /dev/null +++ b/PhotoPreview/src/main/res/layout/fragment_preview.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/PhotoPreview/src/main/res/layout/view_preview_root.xml b/PhotoPreview/src/main/res/layout/view_preview_root.xml new file mode 100644 index 0000000..d9f2a9f --- /dev/null +++ b/PhotoPreview/src/main/res/layout/view_preview_root.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PhotoPreview/src/main/res/values/colors.xml b/PhotoPreview/src/main/res/values/colors.xml new file mode 100644 index 0000000..dad53d2 --- /dev/null +++ b/PhotoPreview/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #0000 + #000 + #fff + \ No newline at end of file diff --git a/PhotoPreview/src/main/res/values/ids.xml b/PhotoPreview/src/main/res/values/ids.xml new file mode 100644 index 0000000..856c573 --- /dev/null +++ b/PhotoPreview/src/main/res/values/ids.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/PhotoPreview/src/main/res/values/strings.xml b/PhotoPreview/src/main/res/values/strings.xml new file mode 100644 index 0000000..7bfcb1b --- /dev/null +++ b/PhotoPreview/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + preview + diff --git a/app/build.gradle b/app/build.gradle index cd4927d..e3eb19a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,8 +16,8 @@ android { minSdkVersion 24 targetSdkVersion 29 - versionCode 83 - versionName "1.9.1" + versionCode 85 + versionName "1.9.4" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true @@ -153,6 +153,7 @@ dependencies { implementation project(path: ':niceimageview') implementation project(path: ':FlycoTabLayoutZ_Lib') implementation project(path: ':verification-view') + implementation project(path: ':PhotoPreview') //保持1.3.1 更新会报错 @@ -235,6 +236,8 @@ dependencies { implementation 'com.github.getActivity:Toaster:12.6' //图片选择 implementation 'io.github.lucksiege:pictureselector:v3.11.1' +// implementation 'com.github.wanglu1209:PhotoViewer:0.50' +// implementation 'com.github.wanggaowan:PhotoPreview:2.5.5' } preBuild { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b7add84..0f5f422 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -117,6 +117,11 @@ android:excludeFromRecents="true" android:screenOrientation="userLandscape" android:theme="@style/DialogCloseOnTouchOutside" /> + - + android:launchMode="singleTask" /> + params = new HashMap<>(); params.put("sn", RemoteManager.getInstance().getSerial()); params.put(mTypeName, mViewDataBinding.editText.getText().toString()); diff --git a/app/src/main/java/com/uiui/zyos/activity/edit/EditViewModel.java b/app/src/main/java/com/uiui/zyos/activity/edit/EditViewModel.java index 499b7a5..888c3ac 100644 --- a/app/src/main/java/com/uiui/zyos/activity/edit/EditViewModel.java +++ b/app/src/main/java/com/uiui/zyos/activity/edit/EditViewModel.java @@ -34,7 +34,7 @@ public class EditViewModel extends BaseViewModel mSuccessfulData = new MutableLiveData<>(); public void updateInfo(Map params) { - if (!ActivationUtil.isActivation(getCtx())) { + if (!ActivationUtil.isActivation()) { return; } NetInterfaceManager.getInstance().getUpdateInfoObservable(params) diff --git a/app/src/main/java/com/uiui/zyos/activity/homework/HomeworkActivity.java b/app/src/main/java/com/uiui/zyos/activity/homework/HomeworkActivity.java index 4d22fc7..69890ca 100644 --- a/app/src/main/java/com/uiui/zyos/activity/homework/HomeworkActivity.java +++ b/app/src/main/java/com/uiui/zyos/activity/homework/HomeworkActivity.java @@ -9,12 +9,12 @@ import androidx.fragment.app.FragmentManager; import androidx.lifecycle.Observer; import androidx.viewpager.widget.ViewPager; -import com.flyco.tablayout.SlidingTabLayout; import com.uiui.zyos.R; import com.uiui.zyos.base.mvvm.BaseMvvmActivity; import com.uiui.zyos.bean.HomeworkBean; import com.uiui.zyos.databinding.ActivityHomeworkBinding; import com.uiui.zyos.fragment.content.ContentFragment; +import com.uiui.zyos.utils.TimeUtils; import com.uiui.zyos.view.ScaleCircleNavigator; import com.uiui.zyos.view.viewpager.BaseFragmentPagerAdapter; @@ -22,12 +22,13 @@ import net.lucode.hackware.magicindicator.ViewPagerHelper; import java.util.ArrayList; import java.util.List; +import java.util.function.Predicate; import java.util.stream.Collectors; public class HomeworkActivity extends BaseMvvmActivity { private static final String TAG = "HomeworkActivity"; - private String[] title = new String[]{"语文", "数学", "英语", "物理", "化学", "地理", "生物", "政治", "历史"}; + private String[] title = new String[]{"全部", "语文", "数学", "英语", "其他",}; private int defaultCurrent = 0; @@ -37,15 +38,12 @@ public class HomeworkActivity extends BaseMvvmActivity(); - + mAllFragment = ContentFragment.newInstance("全部"); + if (!mAllFragment.isAdded()) mFragments.add(mAllFragment); mChineseFragment = ContentFragment.newInstance("语文"); if (!mChineseFragment.isAdded()) mFragments.add(mChineseFragment); mMathFragment = ContentFragment.newInstance("数学"); if (!mMathFragment.isAdded()) mFragments.add(mMathFragment); mEnglishFragment = ContentFragment.newInstance("英语"); if (!mEnglishFragment.isAdded()) mFragments.add(mEnglishFragment); - mPhysicsFragment = ContentFragment.newInstance("物理"); - if (!mPhysicsFragment.isAdded()) mFragments.add(mPhysicsFragment); - mChemicalFragment = ContentFragment.newInstance("化学"); - if (!mChemicalFragment.isAdded()) mFragments.add(mChemicalFragment); - mGeographyFragment = ContentFragment.newInstance("地理"); - if (!mGeographyFragment.isAdded()) mFragments.add(mGeographyFragment); - mBiologyFragment = ContentFragment.newInstance("生物"); - if (!mBiologyFragment.isAdded()) mFragments.add(mBiologyFragment); - mPoliticsFragment = ContentFragment.newInstance("政治"); - if (!mPoliticsFragment.isAdded()) mFragments.add(mPoliticsFragment); - mHistoryFragment = ContentFragment.newInstance("历史"); - if (!mHistoryFragment.isAdded()) mFragments.add(mHistoryFragment); + mOtherFragment = ContentFragment.newInstance("其他"); + if (!mOtherFragment.isAdded()) mFragments.add(mOtherFragment); + mBaseFragmentPagerAdapter = new BaseFragmentPagerAdapter(mFragmentManager, mFragments); mViewDataBinding.viewPager.setAdapter(mBaseFragmentPagerAdapter); @@ -133,6 +123,7 @@ public class HomeworkActivity extends BaseMvvmActivity>() { + mViewModel.mHomeworkBeanListData.observe(this, new Observer>() { @Override - public void onChanged(List homeWorkList) { + public void onChanged(ArrayList homeWorkList) { if (homeWorkList != null && homeWorkList.size() != 0) { - List chineseHomeWorkList = homeWorkList.stream().filter(homeWork -> "语文".equals(homeWork.getSubject().getName())).collect(Collectors.toList()); + mAllFragment.setHomeWorkList(homeWorkList); + ArrayList chineseHomeWorkList = (ArrayList) homeWorkList.stream().filter(homeWork -> "语文".equals(homeWork.getSubject().getName())).collect(Collectors.toList()); mChineseFragment.setHomeWorkList(chineseHomeWorkList); - List mathHomeWorkList = homeWorkList.stream().filter(homeWork -> "数学".equals(homeWork.getSubject().getName())).collect(Collectors.toList()); + ArrayList mathHomeWorkList = (ArrayList) homeWorkList.stream().filter(homeWork -> "数学".equals(homeWork.getSubject().getName())).collect(Collectors.toList()); mMathFragment.setHomeWorkList(mathHomeWorkList); - List englishHomeWorkList = homeWorkList.stream().filter(homeWork -> "英语".equals(homeWork.getSubject().getName())).collect(Collectors.toList()); + ArrayList englishHomeWorkList = (ArrayList) homeWorkList.stream().filter(homeWork -> "英语".equals(homeWork.getSubject().getName())).collect(Collectors.toList()); mEnglishFragment.setHomeWorkList(englishHomeWorkList); - List physicsHomeWorkList = homeWorkList.stream().filter(homeWork -> "物理".equals(homeWork.getSubject().getName())).collect(Collectors.toList()); - mPhysicsFragment.setHomeWorkList(physicsHomeWorkList); - List chemicalHomeWorkList = homeWorkList.stream().filter(homeWork -> "化学".equals(homeWork.getSubject().getName())).collect(Collectors.toList()); - mChemicalFragment.setHomeWorkList(chemicalHomeWorkList); - List geographyHomeWorkList = homeWorkList.stream().filter(homeWork -> "地理".equals(homeWork.getSubject().getName())).collect(Collectors.toList()); - mGeographyFragment.setHomeWorkList(geographyHomeWorkList); - List biologyHomeWorkList = homeWorkList.stream().filter(homeWork -> "生物".equals(homeWork.getSubject().getName())).collect(Collectors.toList()); - mBiologyFragment.setHomeWorkList(biologyHomeWorkList); - List politicsHomeWorkList = homeWorkList.stream().filter(homeWork -> "政治".equals(homeWork.getSubject().getName())).collect(Collectors.toList()); - mPoliticsFragment.setHomeWorkList(politicsHomeWorkList); - List historyHomeWorkList = homeWorkList.stream().filter(homeWork -> "历史".equals(homeWork.getSubject().getName())).collect(Collectors.toList()); - mHistoryFragment.setHomeWorkList(historyHomeWorkList); - + ArrayList physicsHomeWorkList = (ArrayList) homeWorkList.stream().filter(new Predicate() { + @Override + public boolean test(HomeworkBean homeworkBean) { + return "物理".equals(homeworkBean.getSubject().getName()) + || "化学".equals(homeworkBean.getSubject().getName()) + || "地理".equals(homeworkBean.getSubject().getName()) + || "生物".equals(homeworkBean.getSubject().getName()) + || "政治".equals(homeworkBean.getSubject().getName()) + || "历史".equals(homeworkBean.getSubject().getName()) + || "其他".equals(homeworkBean.getSubject().getName()) + ; + } + }).collect(Collectors.toList()); + mOtherFragment.setHomeWorkList(physicsHomeWorkList); } } }); diff --git a/app/src/main/java/com/uiui/zyos/activity/homework/HomeworkDetailsActivity.java b/app/src/main/java/com/uiui/zyos/activity/homework/HomeworkDetailsActivity.java new file mode 100644 index 0000000..9d5fcd7 --- /dev/null +++ b/app/src/main/java/com/uiui/zyos/activity/homework/HomeworkDetailsActivity.java @@ -0,0 +1,117 @@ +package com.uiui.zyos.activity.homework; + +import android.content.Intent; +import android.util.Log; +import android.view.View; + +import androidx.lifecycle.Observer; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.arialyy.annotations.Download; +import com.arialyy.aria.core.Aria; +import com.arialyy.aria.core.task.DownloadTask; +import com.uiui.zyos.R; +import com.uiui.zyos.adapter.HomeworkDetailPicAdapter; +import com.uiui.zyos.base.mvvm.BaseMvvmActivity; +import com.uiui.zyos.bean.HomeworkBean; +import com.uiui.zyos.databinding.ActivityHomeworkDetailsBinding; +import com.uiui.zyos.utils.TimeUtils; + +public class HomeworkDetailsActivity extends BaseMvvmActivity { + private static final String TAG = "HomeworkDetailsActivity"; + + private HomeworkDetailPicAdapter mHomeworkPicAdapter; + private int mId = -1; + + @Download.onTaskComplete + void taskComplete(DownloadTask task) { + //在这里处理任务完成的状态 + Log.e(TAG, "taskComplete: " + task.getFilePath()); + Log.e(TAG, "taskComplete: " + task.getDownloadEntity().getUrl()); + mHomeworkPicAdapter.notifyDataSetChanged(); + } + + @Override + protected int getLayoutId() { + return R.layout.activity_homework_details; + } + + @Override + protected void initDataBinding() { + mViewModel.setCtx(this); + mViewModel.setVDBinding(mViewDataBinding); + mViewModel.setLifecycle(getLifecycleSubject()); + mViewDataBinding.setClick(new BtnClick()); + } + + @Override + protected void initView() { + Aria.download(this).register(); + + Intent intent = getIntent(); + if (intent == null) { + finish(); + return; + } + mId = intent.getIntExtra("HomeworkBeanID", -1); + if (mId == -1) { + finish(); + return; + } + + mHomeworkPicAdapter = new HomeworkDetailPicAdapter(); + LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this); + linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL); + mViewDataBinding.rvPic.setLayoutManager(linearLayoutManager); + mViewDataBinding.rvPic.setAdapter(mHomeworkPicAdapter); + + mViewModel.mHomeworkBeanData.observe(this, new Observer() { + @Override + public void onChanged(HomeworkBean homeworkBean) { + mViewDataBinding.setHomeworkBean(homeworkBean); + mViewDataBinding.tvTitle.setText(TimeUtils.getHomeworkTime3(homeworkBean.getCreated_at()) + homeworkBean.getSubject().getName() + "作业"); + switch (homeworkBean.getStatus()) { + default: + case 0: + mViewDataBinding.tvStatu.setBackground(getDrawable(R.drawable.homework_unfinished_background)); + mViewDataBinding.tvStatu.setTextColor(getColor(R.color.black)); + mViewDataBinding.tvStatu.setText("未完成"); + break; + case 1: + mViewDataBinding.tvStatu.setBackground(getDrawable(R.drawable.homework_finish_background)); + mViewDataBinding.tvStatu.setTextColor(getColor(R.color.white)); + mViewDataBinding.tvStatu.setText("已完成"); + mViewDataBinding.tvStatu.setEnabled(false); + break; + } + mHomeworkPicAdapter.setUrls(homeworkBean.getFile_url()); + } + }); + + mViewModel.getHomeworkDetail(mId); + + } + + @Override + protected void initData() { + + } + + + public class BtnClick { + public void exit(View v) { + finish(); + } + + public void empty(View v) { + + } + + public void updateHomework(View v) { +// mViewModel.updateHomework(mId); + Intent intent = new Intent(HomeworkDetailsActivity.this, UpdateActivity.class); + intent.putExtra("HomeworkBeanID", mId); + startActivity(intent); + } + } +} diff --git a/app/src/main/java/com/uiui/zyos/activity/homework/HomeworkDetailsViewModel.java b/app/src/main/java/com/uiui/zyos/activity/homework/HomeworkDetailsViewModel.java new file mode 100644 index 0000000..a9ed159 --- /dev/null +++ b/app/src/main/java/com/uiui/zyos/activity/homework/HomeworkDetailsViewModel.java @@ -0,0 +1,66 @@ +package com.uiui.zyos.activity.homework; + + +import android.util.Log; + +import androidx.lifecycle.MutableLiveData; + +import com.trello.rxlifecycle4.RxLifecycle; +import com.trello.rxlifecycle4.android.ActivityEvent; +import com.uiui.zyos.base.mvvm.BaseViewModel; +import com.uiui.zyos.bean.BaseResponse; +import com.uiui.zyos.bean.HomeworkBean; +import com.uiui.zyos.databinding.ActivityHomeworkDetailsBinding; +import com.uiui.zyos.network.NetInterfaceManager; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; + +public class HomeworkDetailsViewModel extends BaseViewModel { + private static final String TAG = "HomeworkDetailsViewModel"; + + @Override + public ActivityHomeworkDetailsBinding getVDBinding() { + return binding; + } + + @Override + public void onDestroy() { + + } + + public MutableLiveData mHomeworkBeanData = new MutableLiveData<>(); + + public void getHomeworkDetail(int id) { + NetInterfaceManager.getInstance() + .getHomeworkDetailObservable(id) + .compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY)) + .subscribe(new Observer>() { + @Override + public void onSubscribe(@NonNull Disposable d) { + Log.e("getHomeworkDetail", "onSubscribe: "); + } + + @Override + public void onNext(@NonNull BaseResponse homeworkBeanBaseResponse) { + Log.e("getHomeworkDetail", "onNext: " + homeworkBeanBaseResponse); + if (homeworkBeanBaseResponse.code == 200) { + mHomeworkBeanData.setValue(homeworkBeanBaseResponse.data); + } + } + + @Override + public void onError(@NonNull Throwable e) { + Log.e("getHomeworkDetail", "onError: " + e.getMessage()); + } + + @Override + public void onComplete() { + Log.e("getHomeworkDetail", "onComplete: "); + } + }); + } + + +} diff --git a/app/src/main/java/com/uiui/zyos/activity/homework/HomeworkViewModel.java b/app/src/main/java/com/uiui/zyos/activity/homework/HomeworkViewModel.java index eb97028..4f1de80 100644 --- a/app/src/main/java/com/uiui/zyos/activity/homework/HomeworkViewModel.java +++ b/app/src/main/java/com/uiui/zyos/activity/homework/HomeworkViewModel.java @@ -13,8 +13,12 @@ import com.uiui.zyos.bean.HomeworkBean; import com.uiui.zyos.databinding.ActivityHomeworkBinding; import com.uiui.zyos.network.NetInterfaceManager; import com.uiui.zyos.utils.ActivationUtil; +import com.uiui.zyos.utils.TimeUtils; +import java.util.ArrayList; import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; import io.reactivex.rxjava3.annotations.NonNull; import io.reactivex.rxjava3.core.Observer; @@ -32,26 +36,33 @@ public class HomeworkViewModel extends BaseViewModel> mHomeworkBeanListData = new MutableLiveData<>(); + public MutableLiveData> mHomeworkBeanListData = new MutableLiveData<>(); - public void getHomeWork(){ - boolean activation = ActivationUtil.isActivation(getCtx()); + public void getHomeWork() { + boolean activation = ActivationUtil.isActivation(); if (!activation) { return; } NetInterfaceManager.getInstance().getHomeworkObservable() .compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY)) - .subscribe(new Observer>>() { + .subscribe(new Observer>>() { @Override public void onSubscribe(@NonNull Disposable d) { Log.e("getHomework", "onSubscribe: "); } @Override - public void onNext(@NonNull BaseResponse> listBaseResponse) { + public void onNext(@NonNull BaseResponse> listBaseResponse) { Log.e("getHomework", "onNext: " + listBaseResponse); - List homeworkBeans = listBaseResponse.data; - mHomeworkBeanListData.setValue(homeworkBeans); + ArrayList homeworkBeans = listBaseResponse.data; + + ArrayList filter = (ArrayList) homeworkBeans.stream().filter(new Predicate() { + @Override + public boolean test(HomeworkBean homeworkBean) { + return TimeUtils.isHomeworkToday(homeworkBean.getCreated_at()); + } + }).collect(Collectors.toList()); + mHomeworkBeanListData.setValue(filter); } @Override diff --git a/app/src/main/java/com/uiui/zyos/activity/homework/UpdateActivity.java b/app/src/main/java/com/uiui/zyos/activity/homework/UpdateActivity.java new file mode 100644 index 0000000..b1a84fa --- /dev/null +++ b/app/src/main/java/com/uiui/zyos/activity/homework/UpdateActivity.java @@ -0,0 +1,70 @@ +package com.uiui.zyos.activity.homework; + +import android.content.Intent; +import android.view.View; + +import androidx.lifecycle.Observer; + +import com.hjq.toast.Toaster; +import com.uiui.zyos.R; +import com.uiui.zyos.base.mvvm.BaseMvvmActivity; +import com.uiui.zyos.databinding.ActivityHomeworkUpdateBinding; + +public class UpdateActivity extends BaseMvvmActivity { + + private int mId = -1; + + @Override + protected int getLayoutId() { + return R.layout.activity_homework_update; + } + + @Override + protected void initDataBinding() { + mViewModel.setCtx(this); + mViewModel.setVDBinding(mViewDataBinding); + mViewModel.setLifecycle(getLifecycleSubject()); + mViewDataBinding.setClick(new BtnClick()); + } + + @Override + protected void initView() { + + } + + @Override + protected void initData() { + Intent intent = getIntent(); + if (intent == null) { + finish(); + return; + } + mId = intent.getIntExtra("HomeworkBeanID", -1); + if (mId == -1) { + finish(); + return; + } + mViewModel.mBooleanMutableLiveData.observe(this, new Observer() { + @Override + public void onChanged(Boolean aBoolean) { + if (aBoolean) { + Toaster.show("已完成作业"); + finish(); + } + } + }); + + } + + public class BtnClick { + public void exit(View view) { + finish(); + } + + public void updateHomework(View v) { + mViewModel.updateHomework(mId); + } + } + + +} diff --git a/app/src/main/java/com/uiui/zyos/activity/homework/UpdateViewModel.java b/app/src/main/java/com/uiui/zyos/activity/homework/UpdateViewModel.java new file mode 100644 index 0000000..546e7d4 --- /dev/null +++ b/app/src/main/java/com/uiui/zyos/activity/homework/UpdateViewModel.java @@ -0,0 +1,66 @@ +package com.uiui.zyos.activity.homework; + +import android.util.Log; + +import androidx.lifecycle.MutableLiveData; + +import com.hjq.toast.Toaster; +import com.trello.rxlifecycle4.RxLifecycle; +import com.trello.rxlifecycle4.android.ActivityEvent; +import com.uiui.zyos.base.mvvm.BaseViewModel; +import com.uiui.zyos.bean.BaseResponse; +import com.uiui.zyos.databinding.ActivityHomeworkUpdateBinding; +import com.uiui.zyos.network.NetInterfaceManager; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; + +public class UpdateViewModel extends BaseViewModel { + + @Override + public ActivityHomeworkUpdateBinding getVDBinding() { + return binding; + } + + @Override + public void onDestroy() { + + } + + public MutableLiveData mBooleanMutableLiveData = new MutableLiveData<>(); + + public void updateHomework(int id) { + NetInterfaceManager.getInstance().getHomeworkUpdateObservable(id) + .compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY)) + .subscribe(new Observer() { + @Override + public void onSubscribe(@NonNull Disposable d) { + Log.e("updateHomework", "onSubscribe: "); + } + + @Override + public void onNext(@NonNull BaseResponse baseResponse) { + Log.e("updateHomework", "onNext: " + baseResponse); + if (baseResponse.code == 200) { + mBooleanMutableLiveData.setValue(true); + } else { + Toaster.show(baseResponse.msg); + } + + } + + @Override + public void onError(@NonNull Throwable e) { + Log.e("updateHomework", "onError: " + e.getMessage()); + } + + @Override + public void onComplete() { + Log.e("updateHomework", "onComplete: "); + } + }); + + } + +} diff --git a/app/src/main/java/com/uiui/zyos/activity/main/MainViewModel.java b/app/src/main/java/com/uiui/zyos/activity/main/MainViewModel.java index ac253db..7b8558b 100644 --- a/app/src/main/java/com/uiui/zyos/activity/main/MainViewModel.java +++ b/app/src/main/java/com/uiui/zyos/activity/main/MainViewModel.java @@ -81,7 +81,7 @@ public class MainViewModel extends BaseViewModel params = new HashMap<>(); params.put("sn", RemoteManager.getInstance().getSerial()); params.put("sn_grade", grade); diff --git a/app/src/main/java/com/uiui/zyos/activity/selectegrade/SelecteGradeViewModel.java b/app/src/main/java/com/uiui/zyos/activity/selectegrade/SelecteGradeViewModel.java index e3f95e2..b3661d5 100644 --- a/app/src/main/java/com/uiui/zyos/activity/selectegrade/SelecteGradeViewModel.java +++ b/app/src/main/java/com/uiui/zyos/activity/selectegrade/SelecteGradeViewModel.java @@ -34,7 +34,7 @@ public class SelecteGradeViewModel extends BaseViewModel mSuccessfulData = new MutableLiveData<>(); public void updateInfo(Map params) { - if (!ActivationUtil.isActivation(getCtx())) { + if (!ActivationUtil.isActivation()) { return; } diff --git a/app/src/main/java/com/uiui/zyos/activity/user/UserActivity.java b/app/src/main/java/com/uiui/zyos/activity/user/UserActivity.java index 82064a8..94aa3c4 100644 --- a/app/src/main/java/com/uiui/zyos/activity/user/UserActivity.java +++ b/app/src/main/java/com/uiui/zyos/activity/user/UserActivity.java @@ -148,7 +148,7 @@ public class UserActivity extends BaseMvvmActivity mSuccessfulData = new MutableLiveData<>(); public void updateInfo(Map params, MultipartBody.Part multipartBody) { - if (!ActivationUtil.isActivation(getCtx())) { + if (!ActivationUtil.isActivation()) { return; } diff --git a/app/src/main/java/com/uiui/zyos/adapter/AppAdapter.java b/app/src/main/java/com/uiui/zyos/adapter/AppAdapter.java index 92e6627..af302b5 100644 --- a/app/src/main/java/com/uiui/zyos/adapter/AppAdapter.java +++ b/app/src/main/java/com/uiui/zyos/adapter/AppAdapter.java @@ -99,9 +99,7 @@ public class AppAdapter extends RecyclerView.Adapter { break; default: int settingOtherAppInstaller = Settings.Global.getInt(mContext.getContentResolver(), CommonConfig.SETTING_OTHER_APPINSTALLER_KEY, 1); - if (settingOtherAppInstaller == 0 - && !ApkUtils.isSystemApp(mContext, desktopIcon.getPackageName() - )) { + if (settingOtherAppInstaller == 0 && !ApkUtils.isSystemApp(mContext, desktopIcon.getPackageName())) { Toaster.show("已禁止应用打开"); } else { OpenApkUtils.getInstance().openApp(desktopIcon.getPackageName(), desktopIcon.getClassName()); diff --git a/app/src/main/java/com/uiui/zyos/adapter/FileAdapter.java b/app/src/main/java/com/uiui/zyos/adapter/FileAdapter.java index 5aaf472..dd9097f 100644 --- a/app/src/main/java/com/uiui/zyos/adapter/FileAdapter.java +++ b/app/src/main/java/com/uiui/zyos/adapter/FileAdapter.java @@ -94,18 +94,18 @@ public class FileAdapter extends RecyclerView.Adapter { case AppManager.SERVICE_NAME: mContext.startActivity(new Intent(mContext, ServiceActivity.class)); break; + case "com.uiui.zyappstore": + OpenApkUtils.getInstance().openApp(desktopIcon.getPackageName(), desktopIcon.getClassName()); + break; default: int settingOtherAppInstaller = Settings.Global.getInt(mContext.getContentResolver(), CommonConfig.SETTING_OTHER_APPINSTALLER_KEY, 1); - if (settingOtherAppInstaller == 0 - && !ApkUtils.isSystemApp(mContext, desktopIcon.getPackageName() - )) { + if (settingOtherAppInstaller == 0 && !ApkUtils.isSystemApp(mContext, desktopIcon.getPackageName())) { Toaster.show("已禁止应用打开"); } else { OpenApkUtils.getInstance().openApp(desktopIcon.getPackageName(), desktopIcon.getClassName()); } break; } - } }); } diff --git a/app/src/main/java/com/uiui/zyos/adapter/HomeworkAdapter.java b/app/src/main/java/com/uiui/zyos/adapter/HomeworkAdapter.java new file mode 100644 index 0000000..f8fa3cb --- /dev/null +++ b/app/src/main/java/com/uiui/zyos/adapter/HomeworkAdapter.java @@ -0,0 +1,121 @@ +package com.uiui.zyos.adapter; + +import android.content.Context; +import android.content.Intent; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.uiui.zyos.R; +import com.uiui.zyos.activity.homework.HomeworkDetailsActivity; +import com.uiui.zyos.bean.HomeworkBean; +import com.uiui.zyos.utils.ScreenUtils; +import com.uiui.zyos.utils.TimeUtils; +import com.uiui.zyos.view.HorizontalItemDecoration; + +import java.util.List; + +public class HomeworkAdapter extends RecyclerView.Adapter { + private static final String TAG = "HomeworkAdapter"; + + private Context mContext; + private List mHomeworkList; + + public void setHomeworkList(List homeworkList) { + mHomeworkList = homeworkList; + notifyDataSetChanged(); + } + + @NonNull + @Override + public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + mContext = parent.getContext(); + return new Holder(LayoutInflater.from(mContext).inflate(R.layout.item_homework, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull Holder holder, int position) { + HomeworkBean homeworkBean = mHomeworkList.get(position); + holder.tv_title.setText(homeworkBean.getSubject().getName() + "作业"); +// switch (homeworkBean.getFile_type()) { +// case 2: +// holder.tv_type.setText("图片"); +// break; +// case 3: +// holder.tv_type.setText("视频"); +// break; +// case 4: +// holder.tv_type.setText("文档"); +// break; +// default: +// holder.tv_type.setText("文字"); +// } + switch (homeworkBean.getStatus()) { + default: + case 0: + holder.iv_statu.setImageDrawable(mContext.getDrawable(R.drawable.icon_hw_statu_unfinished)); + break; + case 1: + holder.iv_statu.setImageDrawable(mContext.getDrawable(R.drawable.icon_hw_statu_finish)); + break; + } + holder.tv_type.setText(homeworkBean.getContent()); + holder.tv_time.setText(TimeUtils.getHomeworkTime(homeworkBean.getCreated_at())); + + List urls = homeworkBean.getFile_url(); + if (urls != null && !urls.isEmpty()) { + urls.removeIf(TextUtils::isEmpty); + HomeworkPicAdapter homeworkPicAdapter = new HomeworkPicAdapter(); + homeworkPicAdapter.setUrls(urls); + homeworkPicAdapter.setId(homeworkBean.getId()); + holder.rv_pic.setVisibility(View.VISIBLE); + holder.rv_pic.setAdapter(homeworkPicAdapter); + } else { + holder.rv_pic.setVisibility(View.INVISIBLE); + } + + holder.root.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Intent intent = new Intent(mContext, HomeworkDetailsActivity.class); + intent.putExtra("HomeworkBeanID", homeworkBean.getId()); + mContext.startActivity(intent); + } + }); + + } + + @Override + public int getItemCount() { + return mHomeworkList == null ? 0 : mHomeworkList.size(); + } + + static class Holder extends RecyclerView.ViewHolder { + ConstraintLayout root; + RecyclerView rv_pic; + TextView tv_title, tv_type, tv_time; + ImageView iv_statu; + + Holder(@NonNull View itemView) { + super(itemView); + root = itemView.findViewById(R.id.root); + rv_pic = itemView.findViewById(R.id.rv_pic); + tv_title = itemView.findViewById(R.id.tv_title); + tv_type = itemView.findViewById(R.id.tv_type); + tv_time = itemView.findViewById(R.id.tv_time); + iv_statu = itemView.findViewById(R.id.iv_statu); + LinearLayoutManager linearLayoutManager = new LinearLayoutManager(root.getContext()); + linearLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL); + rv_pic.setLayoutManager(linearLayoutManager); + rv_pic.addItemDecoration(new HorizontalItemDecoration(ScreenUtils.dp2px(root.getContext().getResources(), 8), root.getContext())); + } + } +} diff --git a/app/src/main/java/com/uiui/zyos/adapter/HomeworkDetailPicAdapter.java b/app/src/main/java/com/uiui/zyos/adapter/HomeworkDetailPicAdapter.java new file mode 100644 index 0000000..ee18a4a --- /dev/null +++ b/app/src/main/java/com/uiui/zyos/adapter/HomeworkDetailPicAdapter.java @@ -0,0 +1,114 @@ +package com.uiui.zyos.adapter; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentActivity; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.Priority; +import com.bumptech.glide.load.DecodeFormat; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.request.RequestOptions; +import com.bumptech.glide.request.target.Target; +import com.uiui.zyos.R; +import com.uiui.zyos.bean.HomeworkBean; +import com.uiui.zyos.utils.FileUtil; +import com.uiui.zyos.utils.GlideLoadUtils; +import com.uiui.zyos.utils.OpenFileUtil; +import com.wgw.photo.preview.PhotoPreview; +import com.wgw.photo.preview.interfaces.ImageLoader; + +import java.io.File; +import java.util.List; + +public class HomeworkDetailPicAdapter extends RecyclerView.Adapter { + private static final String TAG = "HomeworkPicAdapter"; + + private Context mContext; + private List mUrls; + + public void setUrls(List urls) { + mUrls = urls; + notifyDataSetChanged(); + } + + @NonNull + @Override + public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + mContext = parent.getContext(); + return new Holder(LayoutInflater.from(mContext).inflate(R.layout.item_homework_detail_pic, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull Holder holder, int position) { + String url = mUrls.get(position); + GlideLoadUtils.getInstance().glideLoadSetSize(mContext, url, holder.nv_pic, R.mipmap.ic_launcher); + File file = new File(FileUtil.getDownLoadPath(mContext) + FileUtil.getFileNamefromURL(url)); + if (file.exists()) { + holder.iv_download.setVisibility(View.GONE); + holder.nv_pic.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { +// OpenFileUtil.openFile(mContext, file); + PhotoPreview.with((FragmentActivity) mContext) + .sources(mUrls) + .defaultShowPosition(position) + .imageLoader(new ImageLoader() { + @Override + public void onLoadImage(int position, @Nullable Object source, @NonNull ImageView imageView) { + RequestOptions requestOptions = new RequestOptions() + .diskCacheStrategy(DiskCacheStrategy.ALL) + .override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) + .priority(Priority.HIGH) + .format(DecodeFormat.PREFER_RGB_565); + Glide.with(imageView.getContext()) + .setDefaultRequestOptions(requestOptions) + .load((String) source) + .into(imageView); +// GlideLoadUtils.getInstance().glideLoad(imageView.getContext(), (String)source, imageView, R.drawable.icon_no_homework); + } + }) + .build() + .show(holder.nv_pic); // 指定缩略图 + } + }); + } else { + + holder.iv_download.setVisibility(View.VISIBLE); + holder.iv_download.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + FileUtil.ariaDownloadUrl(mContext, url); + notifyDataSetChanged(); + } + }); + } + + } + + @Override + public int getItemCount() { + return mUrls == null ? 0 : mUrls.size(); + } + + static class Holder extends RecyclerView.ViewHolder { + + ImageView nv_pic; + ImageView iv_download; + + Holder(@NonNull View itemView) { + super(itemView); + nv_pic = itemView.findViewById(R.id.nv_pic); + iv_download = itemView.findViewById(R.id.iv_download); + + } + } + +} diff --git a/app/src/main/java/com/uiui/zyos/adapter/HomeworkDetailPicAdapter2.java b/app/src/main/java/com/uiui/zyos/adapter/HomeworkDetailPicAdapter2.java new file mode 100644 index 0000000..f0bf8d6 --- /dev/null +++ b/app/src/main/java/com/uiui/zyos/adapter/HomeworkDetailPicAdapter2.java @@ -0,0 +1,128 @@ +package com.uiui.zyos.adapter; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentActivity; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.Priority; +import com.bumptech.glide.load.DecodeFormat; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.request.RequestOptions; +import com.bumptech.glide.request.target.Target; +import com.uiui.zyos.R; +import com.uiui.zyos.bean.HomeworkBean; +import com.uiui.zyos.utils.FileUtil; +import com.uiui.zyos.utils.GlideLoadUtils; +import com.uiui.zyos.utils.OpenFileUtil; +import com.wgw.photo.preview.PhotoPreview; +import com.wgw.photo.preview.interfaces.ImageLoader; + +import java.io.File; +import java.util.List; + +public class HomeworkDetailPicAdapter2 extends RecyclerView.Adapter { + private static final String TAG = "HomeworkPicAdapter"; + + private Context mContext; + private HomeworkBean mHomeworkBean; + + public void setHomeworkBean(HomeworkBean homeworkBean) { + mHomeworkBean = homeworkBean; + notifyDataSetChanged(); + } + + @NonNull + @Override + public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + mContext = parent.getContext(); + return new Holder(LayoutInflater.from(mContext).inflate(R.layout.item_homework_detail_pic, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull Holder holder, int position) { + List urls = mHomeworkBean.getFile_url(); + String url = urls.get(position); + File file = new File(FileUtil.getDownLoadPath(mContext) + FileUtil.getFileNamefromURL(url)); + if (file.exists()) { + GlideLoadUtils.getInstance().glideLoadSetSize(mContext, file, holder.nv_pic, R.mipmap.ic_launcher); + holder.iv_download.setVisibility(View.GONE); + if (mHomeworkBean.getFile_type() == 3) { + holder.iv_play.setVisibility(View.VISIBLE); + } else { + holder.iv_play.setVisibility(View.GONE); + } + + holder.nv_pic.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (mHomeworkBean.getFile_type() == 3) { + OpenFileUtil.openFile(mContext, file); + } else { + PhotoPreview.with((FragmentActivity) mContext) + .sources(urls) + .imageLoader(new ImageLoader() { + @Override + public void onLoadImage(int position, @Nullable Object source, @NonNull ImageView imageView) { + RequestOptions requestOptions = new RequestOptions() + .diskCacheStrategy(DiskCacheStrategy.ALL) + .override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) + .priority(Priority.HIGH) + .format(DecodeFormat.PREFER_RGB_565); + Glide.with(imageView.getContext()) + .setDefaultRequestOptions(requestOptions) + .load((String) source) + .into(imageView); + //GlideLoadUtils.getInstance().glideLoad(imageView.getContext(), (String)source, imageView, R.drawable.icon_no_homework); + } + }) + .build() + .show(holder.nv_pic); // 指定缩略图 + } + } + }); + } else { + GlideLoadUtils.getInstance().glideLoadSetSize(mContext, url, holder.nv_pic, R.mipmap.ic_launcher); + if (mHomeworkBean.getFile_type() == 3) { + holder.iv_download.setVisibility(View.VISIBLE); + holder.iv_download.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + FileUtil.ariaDownloadUrl(mContext, url); + } + }); + } else { + FileUtil.ariaDownloadUrl(mContext, url); + } + } + + } + + @Override + public int getItemCount() { + return mHomeworkBean == null ? 0 : mHomeworkBean.getFile_url().size(); + } + + static class Holder extends RecyclerView.ViewHolder { + + ImageView nv_pic; + ImageView iv_download; + ImageView iv_play; + + Holder(@NonNull View itemView) { + super(itemView); + nv_pic = itemView.findViewById(R.id.nv_pic); + iv_download = itemView.findViewById(R.id.iv_download); + iv_play = itemView.findViewById(R.id.iv_play); + + } + } + +} diff --git a/app/src/main/java/com/uiui/zyos/adapter/HomeworkPicAdapter.java b/app/src/main/java/com/uiui/zyos/adapter/HomeworkPicAdapter.java new file mode 100644 index 0000000..9862b46 --- /dev/null +++ b/app/src/main/java/com/uiui/zyos/adapter/HomeworkPicAdapter.java @@ -0,0 +1,87 @@ +package com.uiui.zyos.adapter; + +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import androidx.annotation.NonNull; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.recyclerview.widget.RecyclerView; + +import com.arialyy.annotations.Download; +import com.arialyy.aria.core.Aria; +import com.arialyy.aria.core.task.DownloadTask; +import com.shehuan.niv.NiceImageView; +import com.uiui.zyos.R; +import com.uiui.zyos.activity.homework.HomeworkDetailsActivity; +import com.uiui.zyos.utils.FileUtil; +import com.uiui.zyos.utils.GlideLoadUtils; +import com.uiui.zyos.utils.OpenFileUtil; + +import java.io.File; +import java.util.List; + +public class HomeworkPicAdapter extends RecyclerView.Adapter { + private static final String TAG = "HomeworkPicAdapter"; + + private Context mContext; + private List mUrls; + private int mId = -1; + + public void setUrls(List urls) { + mUrls = urls; + notifyDataSetChanged(); + } + + public void setId(int id) { + mId = id; + } + + @NonNull + @Override + public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + mContext = parent.getContext(); + return new Holder(LayoutInflater.from(mContext).inflate(R.layout.item_homework_pic, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull Holder holder, int position) { + String url = mUrls.get(position); + File file = new File(FileUtil.getDownLoadPath(mContext) + FileUtil.getFileNamefromURL(url)); + if (file.exists()) { + GlideLoadUtils.getInstance().glideLoad(mContext, file, holder.nv_pic, R.drawable.icon_no_homework); + } else { + GlideLoadUtils.getInstance().glideLoad(mContext, url, holder.nv_pic, R.drawable.icon_no_homework); + } + holder.cl_root.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Intent intent = new Intent(mContext, HomeworkDetailsActivity.class); + intent.putExtra("HomeworkBeanID", mId); + mContext.startActivity(intent); + } + }); + } + + @Override + public int getItemCount() { + return mUrls == null ? 0 : mUrls.size(); + } + + static class Holder extends RecyclerView.ViewHolder { + ConstraintLayout cl_root; + NiceImageView nv_pic; + + Holder(@NonNull View itemView) { + super(itemView); + cl_root = itemView.findViewById(R.id.cl_root); + nv_pic = itemView.findViewById(R.id.nv_pic); + + } + } + +} diff --git a/app/src/main/java/com/uiui/zyos/adapter/NewHomeworkAdapter.java b/app/src/main/java/com/uiui/zyos/adapter/NewHomeworkAdapter.java deleted file mode 100644 index 8b85a91..0000000 --- a/app/src/main/java/com/uiui/zyos/adapter/NewHomeworkAdapter.java +++ /dev/null @@ -1,124 +0,0 @@ -package com.uiui.zyos.adapter; - -import android.content.Context; -import android.content.Intent; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.recyclerview.widget.RecyclerView; - -import com.arialyy.annotations.Download; -import com.arialyy.aria.core.Aria; -import com.arialyy.aria.core.task.DownloadTask; -import com.shehuan.niv.NiceImageView; -import com.uiui.zyos.R; -import com.uiui.zyos.bean.HomeworkBean; -import com.uiui.zyos.utils.FileUtil; -import com.uiui.zyos.utils.GlideLoadUtils; -import com.uiui.zyos.utils.OpenFileUtil; -import com.uiui.zyos.utils.TimeUtils; - -import java.io.File; -import java.util.List; - -public class NewHomeworkAdapter extends RecyclerView.Adapter { - private static final String TAG = "NewHomeworkAdapter"; - - private Context mContext; - private List mHomeworkList; - - public void setHomeworkList(List homeworkList) { - mHomeworkList = homeworkList; - notifyDataSetChanged(); - } - - @NonNull - @Override - public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - mContext = parent.getContext(); - return new Holder(LayoutInflater.from(mContext).inflate(R.layout.item_homework_new, parent, false)); - } - - @Override - public void onBindViewHolder(@NonNull Holder holder, int position) { - Aria.download(this).register(); - - HomeworkBean homeworkBean = mHomeworkList.get(position); - GlideLoadUtils.getInstance().glideLoad(mContext, homeworkBean.getFile_url(), holder.iv_cover, R.drawable.icon_homework_sample); - holder.tv_title.setText(homeworkBean.getTitle()); - holder.tv_content.setText(homeworkBean.getContent()); - switch (homeworkBean.getFile_type()) { - case 2: - holder.tv_type.setText("图片"); - break; - case 3: - holder.tv_type.setText("视频"); - break; - case 4: - holder.tv_type.setText("文档"); - break; - default: - holder.tv_type.setText("文字"); - } - holder.tv_time.setText(TimeUtils.getHomeworkTime(homeworkBean.getCreated_at())); - - String mFileUrl = homeworkBean.getFile_url(); - File file = new File(FileUtil.getDownLoadPath(mContext) + FileUtil.getFileNamefromURL(mFileUrl)); - if (file.exists()) { - holder.iv_download.setVisibility(View.GONE); - holder.root.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - OpenFileUtil.openFile(mContext, file); - } - }); - } else { - holder.iv_download.setVisibility(View.VISIBLE); - holder.iv_download.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - FileUtil.ariaDownloadTestPaper(mContext, mFileUrl, homeworkBean); - notifyDataSetChanged(); - } - }); - } - - } - - @Override - public int getItemCount() { - return mHomeworkList == null ? 0 : mHomeworkList.size(); - } - - class Holder extends RecyclerView.ViewHolder { - ConstraintLayout root; - NiceImageView iv_cover; - ImageView iv_download; - TextView tv_title, tv_content, tv_type, tv_time; - - public Holder(@NonNull View itemView) { - super(itemView); - root = itemView.findViewById(R.id.root); - iv_cover = itemView.findViewById(R.id.iv_cover); - iv_download = itemView.findViewById(R.id.iv_download); - tv_title = itemView.findViewById(R.id.tv_title); - tv_content = itemView.findViewById(R.id.tv_content); - tv_type = itemView.findViewById(R.id.tv_type); - tv_time = itemView.findViewById(R.id.tv_time); - } - } - - @Download.onTaskComplete - void taskComplete(DownloadTask task) { - //在这里处理任务完成的状态 - Log.e(TAG, "taskComplete: " + task.getFilePath()); - Log.e(TAG, "taskComplete: " + task.getDownloadEntity().getUrl()); - notifyDataSetChanged(); - } -} diff --git a/app/src/main/java/com/uiui/zyos/base/BaseApplication.java b/app/src/main/java/com/uiui/zyos/base/BaseApplication.java index ba66cb3..59ad42b 100644 --- a/app/src/main/java/com/uiui/zyos/base/BaseApplication.java +++ b/app/src/main/java/com/uiui/zyos/base/BaseApplication.java @@ -1,6 +1,8 @@ package com.uiui.zyos.base; +import android.annotation.SuppressLint; import android.app.Application; +import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Debug; @@ -30,10 +32,22 @@ import com.uiui.zyos.utils.SystemUtils; public class BaseApplication extends Application { private static final String TAG = "BaseApplication"; + + /** + * ViewModel中因为经常旋转导致弱引用为空 + */ + @SuppressLint("StaticFieldLeak") + private static Context context; + + public static Context getContext() { + return context; + } + @Override public void onCreate() { super.onCreate(); Log.e(TAG, "onCreate: "); + context = getApplicationContext(); if (!BuildConfig.DEBUG) { // catchException(); } diff --git a/app/src/main/java/com/uiui/zyos/bean/HomeworkBean.java b/app/src/main/java/com/uiui/zyos/bean/HomeworkBean.java index f20b1bf..03c39f8 100644 --- a/app/src/main/java/com/uiui/zyos/bean/HomeworkBean.java +++ b/app/src/main/java/com/uiui/zyos/bean/HomeworkBean.java @@ -1,8 +1,12 @@ package com.uiui.zyos.bean; -import java.io.Serializable; +import android.os.Parcel; +import android.os.Parcelable; -public class HomeworkBean implements Serializable { +import java.io.Serializable; +import java.util.List; + +public class HomeworkBean implements Serializable, Parcelable { private static final long serialVersionUID = 4974017718666880157L; int id; @@ -11,7 +15,7 @@ public class HomeworkBean implements Serializable { String sn; String title; String content; - String file_url; + List file_url; String file_name; int file_type; int work_type; @@ -20,6 +24,34 @@ public class HomeworkBean implements Serializable { String updated_at; HomeworkSubject subject; + protected HomeworkBean(Parcel in) { + id = in.readInt(); + subject_id = in.readInt(); + status = in.readInt(); + sn = in.readStringNoHelper(); + title = in.readStringNoHelper(); + content = in.readStringNoHelper(); + file_url = in.createStringArrayList(); + file_name = in.readStringNoHelper(); + file_type = in.readInt(); + work_type = in.readInt(); + end_time = in.readStringNoHelper(); + created_at = in.readStringNoHelper(); + updated_at = in.readStringNoHelper(); + } + + public static final Creator CREATOR = new Creator() { + @Override + public HomeworkBean createFromParcel(Parcel in) { + return new HomeworkBean(in); + } + + @Override + public HomeworkBean[] newArray(int size) { + return new HomeworkBean[size]; + } + }; + public int getId() { return id; } @@ -68,11 +100,11 @@ public class HomeworkBean implements Serializable { this.content = content; } - public String getFile_url() { + public List getFile_url() { return file_url; } - public void setFile_url(String file_url) { + public void setFile_url(List file_url) { this.file_url = file_url; } @@ -131,4 +163,26 @@ public class HomeworkBean implements Serializable { public void setSubject(HomeworkSubject subject) { this.subject = subject; } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int i) { + parcel.writeInt(id); + parcel.writeInt(subject_id); + parcel.writeInt(status); + parcel.writeString(sn); + parcel.writeString(title); + parcel.writeString(content); + parcel.writeStringList(file_url); + parcel.writeString(file_name); + parcel.writeInt(file_type); + parcel.writeInt(work_type); + parcel.writeString(end_time); + parcel.writeString(created_at); + parcel.writeString(updated_at); + } } diff --git a/app/src/main/java/com/uiui/zyos/fragment/app/AppViewModel.java b/app/src/main/java/com/uiui/zyos/fragment/app/AppViewModel.java index dd596b0..27d73a0 100644 --- a/app/src/main/java/com/uiui/zyos/fragment/app/AppViewModel.java +++ b/app/src/main/java/com/uiui/zyos/fragment/app/AppViewModel.java @@ -8,6 +8,7 @@ import com.uiui.zyos.base.mvvm.BaseViewModel; import com.uiui.zyos.bean.DesktopIcon; import com.uiui.zyos.databinding.FragmentAppBinding; import com.uiui.zyos.manager.AppManager; +import com.uiui.zyos.utils.ApkUtils; import java.util.ArrayList; @@ -29,10 +30,10 @@ public class AppViewModel extends BaseViewModel desktopIcons = AppManager.getInstance().getFilterAppList(); DesktopIcon appstoreDesktopIcon = new DesktopIcon(); - appstoreDesktopIcon.setLable("应用市场"); + appstoreDesktopIcon.setLable(ApkUtils.getAppName(getCtx(), "com.uiui.zyappstore")); appstoreDesktopIcon.setPackageName(AppManager.APPSTORE_PACKAGE_NAME); appstoreDesktopIcon.setClassName(AppManager.APPSTORE_CLASS_NAME); - appstoreDesktopIcon.setIcon(getCtx().getDrawable(R.drawable.com_android_appstore)); + appstoreDesktopIcon.setIcon(ApkUtils.getAppDrawable(getCtx(), "com.uiui.zyappstore")); desktopIcons.add(desktopIcons.size(), appstoreDesktopIcon); DesktopIcon updateDesktopIcon = new DesktopIcon(); diff --git a/app/src/main/java/com/uiui/zyos/fragment/content/ContentFragment.java b/app/src/main/java/com/uiui/zyos/fragment/content/ContentFragment.java index fcca8c1..3002588 100644 --- a/app/src/main/java/com/uiui/zyos/fragment/content/ContentFragment.java +++ b/app/src/main/java/com/uiui/zyos/fragment/content/ContentFragment.java @@ -1,6 +1,7 @@ package com.uiui.zyos.fragment.content; import android.content.Context; +import android.content.res.Configuration; import android.os.Bundle; import android.util.DisplayMetrics; import android.util.Log; @@ -11,17 +12,22 @@ import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.recyclerview.widget.GridLayoutManager; +import com.arialyy.annotations.Download; +import com.arialyy.aria.core.Aria; +import com.arialyy.aria.core.task.DownloadTask; import com.tencent.mmkv.MMKV; import com.uiui.zyos.R; -import com.uiui.zyos.adapter.NewHomeworkAdapter; +import com.uiui.zyos.adapter.HomeworkAdapter; import com.uiui.zyos.base.mvvm.fragment.BaseMvvmFragment; import com.uiui.zyos.bean.HomeworkBean; import com.uiui.zyos.config.CommonConfig; import com.uiui.zyos.databinding.FragmentContentBinding; +import com.uiui.zyos.utils.ScreenUtils; +import com.uiui.zyos.view.EquallyDividedItemDecoration; import com.uiui.zyos.view.RecyclerViewSpacesItemDecoration; +import java.util.ArrayList; import java.util.HashMap; -import java.util.List; /** @@ -36,7 +42,21 @@ public class ContentFragment extends BaseMvvmFragment mHomeworkBeanList; + + private static final String HOME_WORK_LIST_NAME = "home_work_list_name"; + + public void setHomeWorkList(ArrayList homeWorkList) { + Log.e(TAG, "setHomeWorkList: "); + Bundle args = getArguments(); + args.putSerializable(HOME_WORK_LIST_NAME, homeWorkList); + setArguments(args); + mHomeworkBeanList = homeWorkList; + setAdapterData(); + + } // TODO: Rename parameter arguments, choose names that match @@ -59,6 +79,7 @@ public class ContentFragment extends BaseMvvmFragment stringIntegerHashMap = new HashMap<>(); - WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); - DisplayMetrics dm = new DisplayMetrics(); - wm.getDefaultDisplay().getRealMetrics(dm); - float density = dm.density; // 屏幕密度(0.75 / 1.0 / 1.5) - stringIntegerHashMap.put(RecyclerViewSpacesItemDecoration.TOP_DECORATION, (int) (density * 10));//top间距 - stringIntegerHashMap.put(RecyclerViewSpacesItemDecoration.BOTTOM_DECORATION, (int) (density * 10));//底部间距 - stringIntegerHashMap.put(RecyclerViewSpacesItemDecoration.LEFT_DECORATION, (int) (density * 10));//左间距 - stringIntegerHashMap.put(RecyclerViewSpacesItemDecoration.RIGHT_DECORATION, (int) (density * 10));//右间距 - mViewDataBinding.recyclerView.addItemDecoration(new RecyclerViewSpacesItemDecoration(stringIntegerHashMap)); - mViewDataBinding.recyclerView.setLayoutManager(new GridLayoutManager(mContext, 2)); + Aria.download(this).register(); + mHomeworkAdapter = new HomeworkAdapter(); + int orientation = mContext.getResources().getConfiguration().orientation; + if (orientation == Configuration.ORIENTATION_PORTRAIT) { + mViewDataBinding.recyclerView.addItemDecoration(new EquallyDividedItemDecoration(1, ScreenUtils.dp2px(getResources(), 4))); + mViewDataBinding.recyclerView.setLayoutManager(new GridLayoutManager(mContext, 1)); + } else if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + mViewDataBinding.recyclerView.addItemDecoration(new EquallyDividedItemDecoration(2, ScreenUtils.dp2px(getResources(), 4))); + mViewDataBinding.recyclerView.setLayoutManager(new GridLayoutManager(mContext, 2)); + } mViewDataBinding.recyclerView.setAdapter(mHomeworkAdapter); + + } @Override protected void initData(Bundle savedInstanceState) { - + Log.e(TAG, "initData: "); + if (getArguments() != null) { + Bundle args = getArguments(); + mHomeworkBeanList = args.getParcelableArrayList(HOME_WORK_LIST_NAME); + setAdapterData(); + } } - - @Override - public void fetchData() { - - } - - - public void setHomeWorkList(List homeWorkList) { - if (homeWorkList == null || homeWorkList.size() == 0) { + private void setAdapterData() { + Log.e(TAG, "setAdapterData: "); + if (mViewDataBinding == null) { + return; + } + if (mHomeworkBeanList == null || mHomeworkBeanList.isEmpty()) { mViewDataBinding.ivNodata.setVisibility(View.VISIBLE); mViewDataBinding.recyclerView.setVisibility(View.GONE); } else { mViewDataBinding.ivNodata.setVisibility(View.GONE); mViewDataBinding.recyclerView.setVisibility(View.VISIBLE); - mHomeworkAdapter.setHomeworkList(homeWorkList); } + if (mHomeworkAdapter != null) { + mHomeworkAdapter.setHomeworkList(mHomeworkBeanList); + } + } + + @Override + public void fetchData() { + Log.e(TAG, "fetchData: "); } @Override @@ -139,6 +183,7 @@ public class ContentFragment extends BaseMvvmFragment> mSnInfoData = new MutableLiveData<>(); public void getSnInfo() { - boolean activation = ActivationUtil.isActivation(getCtx()); + boolean activation = ActivationUtil.isActivation(); if (!activation) { return; } @@ -96,7 +97,7 @@ public class UserViewModel extends BaseViewModel mStudyStatBeanData = new MutableLiveData<>(); public void getStudyStat() { - boolean activation = ActivationUtil.isActivation(getCtx()); + boolean activation = ActivationUtil.isActivation(); if (!activation) { return; } @@ -147,10 +148,10 @@ public class UserViewModel extends BaseViewModel desktopIcons = AppManager.getInstance().getFilterAppList(); DesktopIcon appstoreDesktopIcon = new DesktopIcon(); - appstoreDesktopIcon.setLable("应用市场"); + appstoreDesktopIcon.setLable(ApkUtils.getAppName(getCtx(), "com.uiui.zyappstore")); appstoreDesktopIcon.setPackageName(AppManager.APPSTORE_PACKAGE_NAME); - appstoreDesktopIcon.setPackageName(AppManager.APPSTORE_CLASS_NAME); - appstoreDesktopIcon.setIcon(getCtx().getDrawable(R.drawable.com_android_appstore)); + appstoreDesktopIcon.setClassName(AppManager.APPSTORE_CLASS_NAME); + appstoreDesktopIcon.setIcon(ApkUtils.getAppDrawable(getCtx(), "com.uiui.zyappstore")); desktopIcons.add(desktopIcons.size(), appstoreDesktopIcon); DesktopIcon updateDesktopIcon = new DesktopIcon(); @@ -200,20 +201,20 @@ public class UserViewModel extends BaseViewModel> mHomeworkBeanListData = new MutableLiveData<>(); public void getHomework() { - boolean activation = ActivationUtil.isActivation(getCtx()); + boolean activation = ActivationUtil.isActivation(); if (!activation) { return; } NetInterfaceManager.getInstance().getHomeworkObservable() .compose(RxLifecycle.bindUntilEvent(getLifecycle(), FragmentEvent.DESTROY)) - .subscribe(new Observer>>() { + .subscribe(new Observer>>() { @Override public void onSubscribe(@NonNull Disposable d) { Log.e("getHomework", "onSubscribe: "); } @Override - public void onNext(@NonNull BaseResponse> listBaseResponse) { + public void onNext(@NonNull BaseResponse> listBaseResponse) { Log.e("getHomework", "onNext: " + listBaseResponse); List homeworkBeans = listBaseResponse.data; mHomeworkBeanListData.setValue(homeworkBeans); @@ -234,7 +235,7 @@ public class UserViewModel extends BaseViewModel mPhraseData = new MutableLiveData<>(); public void getPhrase() { - boolean activation = ActivationUtil.isActivation(getCtx()); + boolean activation = ActivationUtil.isActivation(); if (!activation) { return; } diff --git a/app/src/main/java/com/uiui/zyos/fragment/usercenter/info/InfoFragment.java b/app/src/main/java/com/uiui/zyos/fragment/usercenter/info/InfoFragment.java index bc45daf..369a893 100644 --- a/app/src/main/java/com/uiui/zyos/fragment/usercenter/info/InfoFragment.java +++ b/app/src/main/java/com/uiui/zyos/fragment/usercenter/info/InfoFragment.java @@ -60,7 +60,7 @@ public class InfoFragment extends BaseMvvmFragment params = new HashMap<>(); params.put("sn", RemoteManager.getInstance().getSerial()); params.put("sex", "1"); @@ -75,7 +75,7 @@ public class InfoFragment extends BaseMvvmFragment params = new HashMap<>(); params.put("sn", RemoteManager.getInstance().getSerial()); params.put("sex", "2"); @@ -123,7 +123,7 @@ public class InfoFragment extends BaseMvvmFragment mSnInfoData = new MutableLiveData<>(); public void getSnInfo() { - if (!ActivationUtil.isActivation(getCtx())) { + if (!ActivationUtil.isActivation()) { return; } @@ -72,7 +72,7 @@ public class InfoViewModel extends BaseViewModel mSuccessfulData = new MutableLiveData<>(); public void updateInfo(Map params) { - if (!ActivationUtil.isActivation(getCtx())) { + if (!ActivationUtil.isActivation()) { return; } diff --git a/app/src/main/java/com/uiui/zyos/manager/AmapManager.java b/app/src/main/java/com/uiui/zyos/manager/AmapManager.java index 01caffb..74590bc 100644 --- a/app/src/main/java/com/uiui/zyos/manager/AmapManager.java +++ b/app/src/main/java/com/uiui/zyos/manager/AmapManager.java @@ -104,7 +104,7 @@ public class AmapManager { } public void startLocation() { - boolean activation = ActivationUtil.isActivation(mContext); + boolean activation = ActivationUtil.isActivation(); if (!activation) { return; } @@ -139,7 +139,7 @@ public class AmapManager { }; private void updateAddress(AMapLocation aMapLocation) { - boolean activation = ActivationUtil.isActivation(mContext); + boolean activation = ActivationUtil.isActivation(); if (!activation) { return; } diff --git a/app/src/main/java/com/uiui/zyos/network/NetInterfaceManager.java b/app/src/main/java/com/uiui/zyos/network/NetInterfaceManager.java index d397d47..5612241 100644 --- a/app/src/main/java/com/uiui/zyos/network/NetInterfaceManager.java +++ b/app/src/main/java/com/uiui/zyos/network/NetInterfaceManager.java @@ -36,6 +36,7 @@ import com.uiui.zyos.network.api.CloudLessonAppApi; import com.uiui.zyos.network.api.GetFilesApi; import com.uiui.zyos.network.api.GetHomeworkApi; import com.uiui.zyos.network.api.HomeworkDetailApi; +import com.uiui.zyos.network.api.HomeworkUpdateApi; import com.uiui.zyos.network.api.PhraseApi; import com.uiui.zyos.network.api.RunNewApp; import com.uiui.zyos.network.api.SNInfoApi; @@ -50,6 +51,7 @@ import com.uiui.zyos.utils.OpenApkUtils; import java.io.File; import java.lang.reflect.Type; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -333,7 +335,7 @@ public class NetInterfaceManager { .observeOn(AndroidSchedulers.mainThread()); } - public Observable>> getHomeworkObservable() { + public Observable>> getHomeworkObservable() { return mRetrofit.create(GetHomeworkApi.class) .getHomeworks(RemoteManager.getInstance().getSerial()) .subscribeOn(Schedulers.io()) @@ -347,6 +349,13 @@ public class NetInterfaceManager { .observeOn(AndroidSchedulers.mainThread()); } + public Observable getHomeworkUpdateObservable(int id) { + return mRetrofit.create(HomeworkUpdateApi.class) + .updateHomework(RemoteManager.getInstance().getSerial(), id) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } + public Observable> getPhraseObservable() { return mRetrofit.create(PhraseApi.class) .getPhrase(RemoteManager.getInstance().getSerial()) @@ -354,6 +363,8 @@ public class NetInterfaceManager { .observeOn(AndroidSchedulers.mainThread()); } + + /* * * execution diff --git a/app/src/main/java/com/uiui/zyos/network/UrlAddress.java b/app/src/main/java/com/uiui/zyos/network/UrlAddress.java index d4cb995..5c95de1 100644 --- a/app/src/main/java/com/uiui/zyos/network/UrlAddress.java +++ b/app/src/main/java/com/uiui/zyos/network/UrlAddress.java @@ -31,6 +31,9 @@ public class UrlAddress { public static final String GET_HOMEWORK = "homework/index"; /*家庭作业详情*/ public static final String GET_HOMEWORK_DETAIL = "homework/show"; + /*家庭作业详标记完成*/ + public static final String GET_HOMEWORK_UPDATE = "homework/update"; + /*获取桌面短语*/ public static final String GET_PHRASES = "other/phrase"; diff --git a/app/src/main/java/com/uiui/zyos/network/api/GetHomeworkApi.java b/app/src/main/java/com/uiui/zyos/network/api/GetHomeworkApi.java index 3a0896a..3ba5810 100644 --- a/app/src/main/java/com/uiui/zyos/network/api/GetHomeworkApi.java +++ b/app/src/main/java/com/uiui/zyos/network/api/GetHomeworkApi.java @@ -4,7 +4,7 @@ import com.uiui.zyos.bean.BaseResponse; import com.uiui.zyos.bean.HomeworkBean; import com.uiui.zyos.network.UrlAddress; -import java.util.List; +import java.util.ArrayList; import io.reactivex.rxjava3.core.Observable; import retrofit2.http.GET; @@ -12,7 +12,7 @@ import retrofit2.http.Query; public interface GetHomeworkApi { @GET(UrlAddress.GET_HOMEWORK) - Observable>> getHomeworks( + Observable>> getHomeworks( @Query("sn") String sn ); } diff --git a/app/src/main/java/com/uiui/zyos/network/api/HomeworkUpdateApi.java b/app/src/main/java/com/uiui/zyos/network/api/HomeworkUpdateApi.java new file mode 100644 index 0000000..79e3477 --- /dev/null +++ b/app/src/main/java/com/uiui/zyos/network/api/HomeworkUpdateApi.java @@ -0,0 +1,19 @@ +package com.uiui.zyos.network.api; + +import com.uiui.zyos.bean.BaseResponse; +import com.uiui.zyos.network.UrlAddress; + +import io.reactivex.rxjava3.core.Observable; +import retrofit2.http.Field; +import retrofit2.http.FormUrlEncoded; +import retrofit2.http.POST; + +public interface HomeworkUpdateApi { + @FormUrlEncoded + @POST(UrlAddress.GET_HOMEWORK_UPDATE) + Observable updateHomework( + @Field("sn") String sn, + @Field("id") int id + ); + +} diff --git a/app/src/main/java/com/uiui/zyos/service/SocketService.java b/app/src/main/java/com/uiui/zyos/service/SocketService.java index 712e7ed..417151c 100644 --- a/app/src/main/java/com/uiui/zyos/service/SocketService.java +++ b/app/src/main/java/com/uiui/zyos/service/SocketService.java @@ -113,7 +113,7 @@ public class SocketService extends Service implements NetworkUtils.OnNetworkStat NetworkUtils.registerNetworkStatusChangedListener(this); registerScreenLockReceiver(); - if (ActivationUtil.isActivation(SocketService.this)) { + if (ActivationUtil.isActivation()) { //初始化websocket initSocketClient(); startLoop(); @@ -258,7 +258,7 @@ public class SocketService extends Service implements NetworkUtils.OnNetworkStat public void accept(Long s) throws Exception { Log.d(TAG, "startLoop accept: " + s); Log.i(TAG, "心跳包检测websocket连接状态"); - if (!ActivationUtil.isActivation(SocketService.this)) { + if (!ActivationUtil.isActivation()) { dispose(); } //每隔一定的时间,对长连接进行一次心跳检测 diff --git a/app/src/main/java/com/uiui/zyos/utils/ActivationUtil.java b/app/src/main/java/com/uiui/zyos/utils/ActivationUtil.java index be53869..35e78a4 100644 --- a/app/src/main/java/com/uiui/zyos/utils/ActivationUtil.java +++ b/app/src/main/java/com/uiui/zyos/utils/ActivationUtil.java @@ -1,8 +1,8 @@ package com.uiui.zyos.utils; -import android.content.Context; import android.provider.Settings; +import com.uiui.zyos.base.BaseApplication; import com.uiui.zyos.config.CommonConfig; public class ActivationUtil { @@ -19,72 +19,65 @@ public class ActivationUtil { /** * 获取激活状态 * - * @param context * @return */ - public static int getActivationStateCode(Context context) { - int activation = Settings.Global.getInt(context.getContentResolver(), CommonConfig.UIUI_ACTIVATION_KEY, INACTIVATED_KEY); + public static int getActivationStateCode() { + int activation = Settings.Global.getInt(BaseApplication.getContext().getContentResolver(), CommonConfig.UIUI_ACTIVATION_KEY, INACTIVATED_KEY); return activation; } /** * 是否激活 * - * @param context * @return */ - public static boolean isActivation(Context context) { - return getActivationStateCode(context) == ACTIVATED_KEY; + public static boolean isActivation() { + return getActivationStateCode() == ACTIVATED_KEY; } /** * 设置激活状态 * - * @param context * @param code */ - public static void setActivation(Context context, int code) { - Settings.Global.putInt(context.getContentResolver(), CommonConfig.UIUI_ACTIVATION_KEY, code); + public static void setActivation(int code) { + Settings.Global.putInt(BaseApplication.getContext().getContentResolver(), CommonConfig.UIUI_ACTIVATION_KEY, code); } /** * 设置激活码类型 * - * @param context * @param code */ - public static void setActivationCodeType(Context context, int code) { - Settings.Global.putInt(context.getContentResolver(), CommonConfig.UIUI_CODE_TYPE_KEY, code); + public static void setActivationCodeType(int code) { + Settings.Global.putInt(BaseApplication.getContext().getContentResolver(), CommonConfig.UIUI_CODE_TYPE_KEY, code); } /** * 获取激活码类型 * - * @param context * @return */ - public static int getActivationCodeType(Context context) { - return Settings.Global.getInt(context.getContentResolver(), CommonConfig.UIUI_CODE_TYPE_KEY, DEFAULT_CODE_TYPE); + public static int getActivationCodeType() { + return Settings.Global.getInt(BaseApplication.getContext().getContentResolver(), CommonConfig.UIUI_CODE_TYPE_KEY, DEFAULT_CODE_TYPE); } /** * 设置过期时间 * - * @param context * @param expireTime */ - public static void setActivationExpireTime(Context context, long expireTime) { - Settings.Global.putLong(context.getContentResolver(), CommonConfig.UIUI_EXPIRE_TIME_KEY, expireTime); + public static void setActivationExpireTime(long expireTime) { + Settings.Global.putLong(BaseApplication.getContext().getContentResolver(), CommonConfig.UIUI_EXPIRE_TIME_KEY, expireTime); } /** * 获取过期时间 * - * @param context * @return */ - public static long getActivationExpireTime(Context context) { - return Settings.Global.getLong(context.getContentResolver(), CommonConfig.UIUI_EXPIRE_TIME_KEY, DEFAULT_EXPIRE_TIME); + public static long getActivationExpireTime() { + return Settings.Global.getLong(BaseApplication.getContext().getContentResolver(), CommonConfig.UIUI_EXPIRE_TIME_KEY, DEFAULT_EXPIRE_TIME); } } \ No newline at end of file diff --git a/app/src/main/java/com/uiui/zyos/utils/ApkUtils.java b/app/src/main/java/com/uiui/zyos/utils/ApkUtils.java index b2a1bba..bc9fdb7 100644 --- a/app/src/main/java/com/uiui/zyos/utils/ApkUtils.java +++ b/app/src/main/java/com/uiui/zyos/utils/ApkUtils.java @@ -53,7 +53,6 @@ public class ApkUtils { this.add("com.uiui.zy"); this.add("com.uiui.zyos"); this.add("com.uiui.zybrowser"); - this.add("com.uiui.zyappstore"); this.add("org.chromium.browser"); this.add("com.sprd.sprdnote"); this.add("com.android.deskclock"); @@ -122,6 +121,7 @@ public class ApkUtils { // this.add("com.android.calendar"); this.add("com.mediatek.camera"); this.add("com.uiui.zybrowser"); + this.add("com.uiui.zyappstore"); }}; private static HashSet allHintPackage = new HashSet() {{ this.add("com.android.uiuios"); diff --git a/app/src/main/java/com/uiui/zyos/utils/AppUsedTimeUtils.java b/app/src/main/java/com/uiui/zyos/utils/AppUsedTimeUtils.java index 03e108c..f91ed50 100644 --- a/app/src/main/java/com/uiui/zyos/utils/AppUsedTimeUtils.java +++ b/app/src/main/java/com/uiui/zyos/utils/AppUsedTimeUtils.java @@ -179,7 +179,7 @@ public class AppUsedTimeUtils { } public void sendRunningApp(RunningAppCallback runningAppCallback) { - boolean activation = ActivationUtil.isActivation(mContext); + boolean activation = ActivationUtil.isActivation(); if (!activation) { return; } diff --git a/app/src/main/java/com/uiui/zyos/utils/FileUtil.java b/app/src/main/java/com/uiui/zyos/utils/FileUtil.java index 2402b1a..6a51305 100644 --- a/app/src/main/java/com/uiui/zyos/utils/FileUtil.java +++ b/app/src/main/java/com/uiui/zyos/utils/FileUtil.java @@ -281,4 +281,18 @@ public class FileUtil { .create(); //启动下载} } } + + public static void ariaDownloadUrl(Context context, String url) { + String fileName = getFileNamefromURL(url); + File file = new File(getDownLoadPath(context) + fileName); + if (file.exists() && !file.isDirectory()) { + Log.e(TAG, "ariaDownload: " + "file exists"); + } else { + Aria.download(context) + .load(url) //读取下载地址 + .setFilePath(getDownLoadPath(context) + fileName) + .ignoreFilePathOccupy() + .create(); //启动下载} + } + } } diff --git a/app/src/main/java/com/uiui/zyos/utils/GlideLoadUtils.java b/app/src/main/java/com/uiui/zyos/utils/GlideLoadUtils.java index cb2c64f..805e092 100644 --- a/app/src/main/java/com/uiui/zyos/utils/GlideLoadUtils.java +++ b/app/src/main/java/com/uiui/zyos/utils/GlideLoadUtils.java @@ -3,13 +3,23 @@ package com.uiui.zyos.utils; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; import android.os.Build; import android.util.Log; +import android.view.ViewGroup; import android.widget.ImageView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import com.bumptech.glide.Glide; +import com.bumptech.glide.request.target.SimpleTarget; +import com.bumptech.glide.request.transition.Transition; + +import java.io.File; /** * Glide 加载 简单判空封装 防止异步加载数据时调用Glide 抛出异常 @@ -51,6 +61,70 @@ public class GlideLoadUtils { } } + public void glideLoad(Context context, File file, ImageView imageView, int default_image) { + if (context != null) { + Glide.with(context).load(file).centerCrop().error(default_image).into(imageView); + } else { + Log.i(TAG, "Picture loading failed,context is null"); + } + } + + public void glideLoadSetSize(Context context, String url, ImageView imageView, int default_image) { + if (context != null) { + Glide.with(context).load(url).centerCrop().error(default_image).into(new SimpleTarget() { + @Override + public void onResourceReady(@NonNull Drawable resource, @Nullable Transition transition) { + imageView.setImageDrawable(resource); + // 获取图片的原始尺寸 + BitmapDrawable drawable = (BitmapDrawable) resource; + Bitmap bitmap = drawable.getBitmap(); + int height = bitmap.getHeight(); + int width = bitmap.getWidth(); + int imageViewWidth = imageView.getWidth(); + int imageViewHeight = (int) ((double)height/width*imageViewWidth); + // 设置ImageView的高度 + ViewGroup.LayoutParams params = imageView.getLayoutParams(); + params.height = imageViewHeight; + imageView.setLayoutParams(params); + } + }); + } else { + Log.i(TAG, "Picture loading failed,context is null"); + } + } + + public void glideLoadSetSize(Context context, File file, ImageView imageView, int default_image) { + if (context != null) { + Glide.with(context).load(file).centerCrop().error(default_image).into(new SimpleTarget() { + @Override + public void onResourceReady(@NonNull Drawable resource, @Nullable Transition transition) { + imageView.setImageDrawable(resource); + // 获取图片的原始尺寸 + BitmapDrawable drawable = (BitmapDrawable) resource; + Bitmap bitmap = drawable.getBitmap(); + int height = bitmap.getHeight(); + int width = bitmap.getWidth(); + int imageViewWidth = imageView.getWidth(); + int imageViewHeight = (int) ((double)height/width*imageViewWidth); + // 设置ImageView的高度 + ViewGroup.LayoutParams params = imageView.getLayoutParams(); + params.height = imageViewHeight; + imageView.setLayoutParams(params); + } + }); + } else { + Log.i(TAG, "Picture loading failed,context is null"); + } + } + + public void glideLoadCenterInside(Context context, String url, ImageView imageView, int default_image) { + if (context != null) { + Glide.with(context).load(url).centerInside().error(default_image).into(imageView); + } else { + Log.i(TAG, "Picture loading failed,context is null"); + } + } + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) public void glideLoad(Activity activity, String url, ImageView imageView, int default_image) { if (!activity.isDestroyed()) { diff --git a/app/src/main/java/com/uiui/zyos/utils/OpenApkUtils.java b/app/src/main/java/com/uiui/zyos/utils/OpenApkUtils.java index 265c9d5..f6166eb 100644 --- a/app/src/main/java/com/uiui/zyos/utils/OpenApkUtils.java +++ b/app/src/main/java/com/uiui/zyos/utils/OpenApkUtils.java @@ -7,7 +7,6 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; -import android.os.Build; import android.provider.Settings; import android.text.TextUtils; import android.util.Log; @@ -216,7 +215,7 @@ public class OpenApkUtils { }}; private void sendRuningApp(String packageName) { - boolean activation = ActivationUtil.isActivation(mContext); + boolean activation = ActivationUtil.isActivation(); if (!activation) { return; } diff --git a/app/src/main/java/com/uiui/zyos/utils/OpenFileUtil.java b/app/src/main/java/com/uiui/zyos/utils/OpenFileUtil.java index daa48e0..c993139 100644 --- a/app/src/main/java/com/uiui/zyos/utils/OpenFileUtil.java +++ b/app/src/main/java/com/uiui/zyos/utils/OpenFileUtil.java @@ -16,11 +16,17 @@ import androidx.core.content.FileProvider; import com.uiui.zyos.BuildConfig; +import java.io.BufferedInputStream; import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URLConnection; import java.util.HashMap; import java.util.Locale; public class OpenFileUtil { + private static final String TAG = "OpenFileUtil"; public static void openFile(Context context, String filePath) { File file = new File(filePath); @@ -63,6 +69,20 @@ public class OpenFileUtil { type = map.get(end); } } + Log.e(TAG, "getMimeTypeFromFile: " + type); + Log.e(TAG, "getMimeType: " + getMimeType(file.getAbsolutePath())); + return type; + } + + private static String getMimeType(String filePath) { + String type = "*/*"; + try { + FileInputStream inputFile = new FileInputStream(filePath); + type = URLConnection.guessContentTypeFromStream(new BufferedInputStream(inputFile)); + return type; + } catch (IOException e) { + e.printStackTrace(); + } return type; } @@ -75,6 +95,7 @@ public class OpenFileUtil { */ public static HashMap getMimeMap() { if (mapSimple.size() == 0) { + mapSimple.put("", "*/*"); mapSimple.put(".3gp", "video/3gpp"); mapSimple.put(".apk", "application/vnd.android.package-archive"); mapSimple.put(".asf", "video/x-ms-asf"); @@ -142,7 +163,6 @@ public class OpenFileUtil { mapSimple.put(".xlsx", "application/vnd.ms-excel"); mapSimple.put(".z", "application/x-compress"); mapSimple.put(".zip", "application/zip"); - mapSimple.put("", "*/*"); } return mapSimple; } diff --git a/app/src/main/java/com/uiui/zyos/utils/TimeUtils.java b/app/src/main/java/com/uiui/zyos/utils/TimeUtils.java index 3f4ec5a..a9d85d9 100644 --- a/app/src/main/java/com/uiui/zyos/utils/TimeUtils.java +++ b/app/src/main/java/com/uiui/zyos/utils/TimeUtils.java @@ -67,6 +67,60 @@ public class TimeUtils { return sdf.format(date); } + public static String getHomeworkTime2(String timeString) { + SimpleDateFormat old = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date date = new Date(); + try { + date = old.parse(timeString); + } catch (ParseException e) { + e.printStackTrace(); + } + SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm"); + return sdf.format(date); + } + + public static String getHomeworkTime3(String timeString) { + SimpleDateFormat old = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date date = new Date(); + try { + date = old.parse(timeString); + } catch (ParseException e) { + e.printStackTrace(); + } + SimpleDateFormat sdf = new SimpleDateFormat("MM月dd日"); + return sdf.format(date); + } + + public static String getHomeworkNowTime() { + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + Log.e(TAG, "getHomeworkNowTime: " + calendar.getTimeInMillis()); + SimpleDateFormat sdf = new SimpleDateFormat("MM月dd日"); + return sdf.format(new Date(calendar.getTimeInMillis())); + } + + public static boolean isHomeworkToday(String timeString) { + if (TextUtils.isEmpty(timeString)) { + return true; + } + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + Log.e(TAG, "getHomeworkNowTime: " + calendar.getTimeInMillis()); + SimpleDateFormat old = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date date = new Date(); + try { + date = old.parse(timeString); + return date.getTime() >= calendar.getTimeInMillis(); + } catch (ParseException e) { + e.printStackTrace(); + } + return false; + } + public static String formatTime(Long s) { if (s == 0) { return "0分钟"; @@ -101,6 +155,12 @@ public class TimeUtils { return sb.toString(); } + public static String secondsToHourMin(long seconds) { + long hour = seconds / 3600; // 1 hour = 3600 seconds + long remainingSeconds = seconds % 3600; // remaining seconds not including hours + long min = (long) Math.ceil((double) remainingSeconds / 60); // 1 min = 60 seconds + return String.format("%d小时%d分", hour, min); + } /** * 是否在管控时间内 diff --git a/app/src/main/java/com/uiui/zyos/utils/Utils.java b/app/src/main/java/com/uiui/zyos/utils/Utils.java index 183628b..95f583e 100644 --- a/app/src/main/java/com/uiui/zyos/utils/Utils.java +++ b/app/src/main/java/com/uiui/zyos/utils/Utils.java @@ -298,7 +298,6 @@ public class Utils { } - /** * 获取手机的MAC地址 * diff --git a/app/src/main/java/com/uiui/zyos/view/EquallyDividedItemDecoration.java b/app/src/main/java/com/uiui/zyos/view/EquallyDividedItemDecoration.java new file mode 100644 index 0000000..1914f44 --- /dev/null +++ b/app/src/main/java/com/uiui/zyos/view/EquallyDividedItemDecoration.java @@ -0,0 +1,74 @@ +package com.uiui.zyos.view; + +import android.graphics.Rect; +import android.util.Log; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +public class EquallyDividedItemDecoration extends RecyclerView.ItemDecoration { + private static final String TAG = EquallyDividedItemDecoration.class.getSimpleName(); + + private int mSpanCount;// 横条目数量 + private int mHalfRowSpacing;// 行间距的一半 + private int mHalfColumnSpacing;// 列间距的一半 + + public EquallyDividedItemDecoration(int spanCount, int halfRowSpacing) { + mSpanCount = spanCount; + mHalfRowSpacing = halfRowSpacing; + mHalfColumnSpacing = halfRowSpacing; + } + + public EquallyDividedItemDecoration(int spanCount, int halfRowSpacing, int halfColumnSpacing) { + mSpanCount = spanCount; + mHalfRowSpacing = halfRowSpacing; + mHalfColumnSpacing = halfColumnSpacing; + } + + @Override + public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, + @NonNull RecyclerView.State state) { + super.getItemOffsets(outRect, view, parent, state); + int position = parent.getChildAdapterPosition(view); // 获取view 在adapter中的位置。 + Log.d(TAG, "getItemOffsets: position = " + position); + + int itemCount = parent.getAdapter().getItemCount();// item全部数量 + Log.d(TAG, "getItemOffsets: itemCount = " + itemCount); + + int column = position % mSpanCount; // view 所在的列 + Log.d(TAG, "getItemOffsets: column = " + column); + + if (column == 0) { + outRect.left = 2 * mHalfRowSpacing; + outRect.right = mHalfRowSpacing; + } else if (column == mSpanCount - 1) { + outRect.left = mHalfRowSpacing; + outRect.right = 2 * mHalfRowSpacing; + } else { + outRect.left = mHalfRowSpacing; + outRect.right = mHalfRowSpacing; + } + + int row = (position / mSpanCount);// 所在行 + Log.d(TAG, "getItemOffsets: row = " + row); + int maxRow = (int) Math.ceil((double) itemCount / mSpanCount);// 一共多少行 + Log.d(TAG, "getItemOffsets: maxRow = " + maxRow); + + if (row == 0) { + outRect.top = 2 * mHalfColumnSpacing; + outRect.bottom = mHalfColumnSpacing; + } else if (row == maxRow - 1) { + outRect.top = mHalfColumnSpacing; + outRect.bottom = 2 * mHalfColumnSpacing; + } else { + outRect.top = mHalfColumnSpacing; + outRect.bottom = mHalfColumnSpacing; + } + + Log.d(TAG, "getItemOffsets: outRect.left = " + outRect.left); + Log.d(TAG, "getItemOffsets: outRect.right = " + outRect.right); + Log.d(TAG, "getItemOffsets: outRect.top = " + outRect.top); + Log.d(TAG, "getItemOffsets: outRect.bottom = " + outRect.bottom); + } +} diff --git a/app/src/main/res/drawable-hdpi/com_android_appstore.png b/app/src/main/res/drawable-hdpi/com_android_appstore.png index b240f56..a5f54a7 100644 Binary files a/app/src/main/res/drawable-hdpi/com_android_appstore.png and b/app/src/main/res/drawable-hdpi/com_android_appstore.png differ diff --git a/app/src/main/res/drawable-hdpi/com_android_calculator2.png b/app/src/main/res/drawable-hdpi/com_android_calculator2.png new file mode 100644 index 0000000..dfb3348 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/com_android_calculator2.png differ diff --git a/app/src/main/res/drawable-hdpi/com_android_dialer.png b/app/src/main/res/drawable-hdpi/com_android_dialer.png new file mode 100644 index 0000000..78cb04f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/com_android_dialer.png differ diff --git a/app/src/main/res/drawable-hdpi/com_android_mms_ui.png b/app/src/main/res/drawable-hdpi/com_android_mms_ui.png new file mode 100644 index 0000000..6f8d6e4 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/com_android_mms_ui.png differ diff --git a/app/src/main/res/drawable-hdpi/default_avatar.png b/app/src/main/res/drawable-hdpi/default_avatar.png index 1c02c71..a9e50b9 100644 Binary files a/app/src/main/res/drawable-hdpi/default_avatar.png and b/app/src/main/res/drawable-hdpi/default_avatar.png differ diff --git a/app/src/main/res/drawable-hdpi/exit_icon.png b/app/src/main/res/drawable-hdpi/exit_icon.png index 6905d5a..264678f 100644 Binary files a/app/src/main/res/drawable-hdpi/exit_icon.png and b/app/src/main/res/drawable-hdpi/exit_icon.png differ diff --git a/app/src/main/res/drawable-hdpi/icon_activition_bind.png b/app/src/main/res/drawable-hdpi/icon_activition_bind.png index 8cef580..4d52d62 100644 Binary files a/app/src/main/res/drawable-hdpi/icon_activition_bind.png and b/app/src/main/res/drawable-hdpi/icon_activition_bind.png differ diff --git a/app/src/main/res/drawable-hdpi/icon_homework_detail_back.png b/app/src/main/res/drawable-hdpi/icon_homework_detail_back.png new file mode 100644 index 0000000..cbc2581 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_homework_detail_back.png differ diff --git a/app/src/main/res/drawable-hdpi/icon_homework_pull.png b/app/src/main/res/drawable-hdpi/icon_homework_pull.png new file mode 100644 index 0000000..c39ce7b Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_homework_pull.png differ diff --git a/app/src/main/res/drawable-hdpi/icon_homework_refresh.png b/app/src/main/res/drawable-hdpi/icon_homework_refresh.png new file mode 100644 index 0000000..71e4222 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_homework_refresh.png differ diff --git a/app/src/main/res/drawable-hdpi/icon_hw_statu_finish.png b/app/src/main/res/drawable-hdpi/icon_hw_statu_finish.png new file mode 100644 index 0000000..880aef5 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_hw_statu_finish.png differ diff --git a/app/src/main/res/drawable-hdpi/icon_hw_statu_unfinished.png b/app/src/main/res/drawable-hdpi/icon_hw_statu_unfinished.png new file mode 100644 index 0000000..0662267 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_hw_statu_unfinished.png differ diff --git a/app/src/main/res/drawable-hdpi/icon_no_homework.png b/app/src/main/res/drawable-hdpi/icon_no_homework.png new file mode 100644 index 0000000..07b377f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_no_homework.png differ diff --git a/app/src/main/res/drawable-hdpi/icon_update.png b/app/src/main/res/drawable-hdpi/icon_update.png index df52354..6872dfd 100644 Binary files a/app/src/main/res/drawable-hdpi/icon_update.png and b/app/src/main/res/drawable-hdpi/icon_update.png differ diff --git a/app/src/main/res/drawable-hdpi/service_icon.png b/app/src/main/res/drawable-hdpi/service_icon.png index e099d76..5c45820 100644 Binary files a/app/src/main/res/drawable-hdpi/service_icon.png and b/app/src/main/res/drawable-hdpi/service_icon.png differ diff --git a/app/src/main/res/drawable-hdpi/status_download.png b/app/src/main/res/drawable-hdpi/status_download.png index 2081bff..d838532 100644 Binary files a/app/src/main/res/drawable-hdpi/status_download.png and b/app/src/main/res/drawable-hdpi/status_download.png differ diff --git a/app/src/main/res/drawable-hdpi/user_edit_avatar.png b/app/src/main/res/drawable-hdpi/user_edit_avatar.png deleted file mode 100644 index 2ce7e4b..0000000 Binary files a/app/src/main/res/drawable-hdpi/user_edit_avatar.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/main_background.png b/app/src/main/res/drawable-xhdpi/main_background.png index abcb601..3e806f0 100644 Binary files a/app/src/main/res/drawable-xhdpi/main_background.png and b/app/src/main/res/drawable-xhdpi/main_background.png differ diff --git a/app/src/main/res/drawable/finish_text_selector.xml b/app/src/main/res/drawable/finish_text_selector.xml new file mode 100644 index 0000000..0ba084a --- /dev/null +++ b/app/src/main/res/drawable/finish_text_selector.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/homework_details_background.xml b/app/src/main/res/drawable/homework_details_background.xml new file mode 100644 index 0000000..54d0993 --- /dev/null +++ b/app/src/main/res/drawable/homework_details_background.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/homework_finish_background.xml b/app/src/main/res/drawable/homework_finish_background.xml new file mode 100644 index 0000000..656db72 --- /dev/null +++ b/app/src/main/res/drawable/homework_finish_background.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/homework_unfinished_background.xml b/app/src/main/res/drawable/homework_unfinished_background.xml new file mode 100644 index 0000000..85b3c94 --- /dev/null +++ b/app/src/main/res/drawable/homework_unfinished_background.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/update_finish_background.xml b/app/src/main/res/drawable/update_finish_background.xml new file mode 100644 index 0000000..d0a6417 --- /dev/null +++ b/app/src/main/res/drawable/update_finish_background.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/user_box_bg.xml b/app/src/main/res/drawable/user_box_bg.xml index a407dae..bda9504 100644 --- a/app/src/main/res/drawable/user_box_bg.xml +++ b/app/src/main/res/drawable/user_box_bg.xml @@ -13,9 +13,9 @@ android:topLeftRadius="@dimen/dp_8" android:topRightRadius="@dimen/dp_8" /> - - - - - + \ No newline at end of file diff --git a/app/src/main/res/layout-land/activity_exit.xml b/app/src/main/res/layout-land/activity_exit.xml index 86de838..ba7274f 100644 --- a/app/src/main/res/layout-land/activity_exit.xml +++ b/app/src/main/res/layout-land/activity_exit.xml @@ -383,7 +383,7 @@ android:layout_height="@dimen/dp_30" android:background="@drawable/bt_dialer_selector" android:gravity="center" - android:padding="@dimen/dp_4" + android:padding="@dimen/dp_8" android:src="@drawable/icon_dialer_cancel" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -402,7 +402,7 @@ android:layout_height="@dimen/dp_30" android:background="@drawable/bt_dialer_selector" android:gravity="center" - android:padding="@dimen/dp_4" + android:padding="@dimen/dp_8" android:src="@drawable/icon_dialer_confirm" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout-land/activity_homework.xml b/app/src/main/res/layout-land/activity_homework.xml new file mode 100644 index 0000000..ae64d70 --- /dev/null +++ b/app/src/main/res/layout-land/activity_homework.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/activity_homework_details.xml b/app/src/main/res/layout-land/activity_homework_details.xml new file mode 100644 index 0000000..39db965 --- /dev/null +++ b/app/src/main/res/layout-land/activity_homework_details.xml @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/activity_passwd.xml b/app/src/main/res/layout-land/activity_passwd.xml index 83c52bd..d87e1a3 100644 --- a/app/src/main/res/layout-land/activity_passwd.xml +++ b/app/src/main/res/layout-land/activity_passwd.xml @@ -348,7 +348,7 @@ android:layout_height="@dimen/dp_30" android:background="@drawable/bt_dialer_selector" android:gravity="center" - android:padding="@dimen/dp_4" + android:padding="@dimen/dp_8" android:src="@drawable/icon_dialer_cancel" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -367,7 +367,7 @@ android:layout_height="@dimen/dp_30" android:background="@drawable/bt_dialer_selector" android:gravity="center" - android:padding="@dimen/dp_4" + android:padding="@dimen/dp_8" android:src="@drawable/icon_dialer_confirm" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout-land/fragment_user.xml b/app/src/main/res/layout-land/fragment_user.xml index 05e6e46..7f965f3 100644 --- a/app/src/main/res/layout-land/fragment_user.xml +++ b/app/src/main/res/layout-land/fragment_user.xml @@ -194,7 +194,7 @@ android:id="@+id/cl_study_time" android:layout_width="0dp" android:layout_height="wrap_content" - android:visibility="visible" + android:visibility="gone" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/imageView17" @@ -206,8 +206,7 @@ android:layout_height="wrap_content" android:maxLines="1" android:textColor="@color/percent_color" - android:textSize="@dimen/sp_20" - android:text="100%" + android:textSize="@dimen/sp_12" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" @@ -219,7 +218,7 @@ android:layout_height="wrap_content" android:layout_marginTop="@dimen/dp_8" android:maxLines="1" - android:text="今日学习时间" + android:text="今日学习时长" android:textColor="@color/white" android:textSize="@dimen/sp_8" app:layout_constraintEnd_toEndOf="@+id/tv_percent" @@ -452,14 +451,13 @@ android:id="@+id/cl_homework" android:layout_width="match_parent" android:layout_height="match_parent" + android:onClick="@{click::openHomework}" android:visibility="visible"> - - - - - + app:layout_constraintTop_toTopOf="parent" /> + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/item_homework_pic.xml b/app/src/main/res/layout-land/item_homework_pic.xml new file mode 100644 index 0000000..75f4da4 --- /dev/null +++ b/app/src/main/res/layout-land/item_homework_pic.xml @@ -0,0 +1,29 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw800dp/activity_exit.xml b/app/src/main/res/layout-sw800dp/activity_exit.xml index cabe922..67383b8 100644 --- a/app/src/main/res/layout-sw800dp/activity_exit.xml +++ b/app/src/main/res/layout-sw800dp/activity_exit.xml @@ -382,7 +382,7 @@ android:layout_height="@dimen/dp_30" android:background="@drawable/bt_dialer_selector" android:gravity="center" - android:padding="@dimen/dp_4" + android:padding="@dimen/dp_8" android:src="@drawable/icon_dialer_cancel" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -401,7 +401,7 @@ android:layout_height="@dimen/dp_30" android:background="@drawable/bt_dialer_selector" android:gravity="center" - android:padding="@dimen/dp_4" + android:padding="@dimen/dp_8" android:src="@drawable/icon_dialer_confirm" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout-sw800dp/activity_passwd.xml b/app/src/main/res/layout-sw800dp/activity_passwd.xml index 83c52bd..d87e1a3 100644 --- a/app/src/main/res/layout-sw800dp/activity_passwd.xml +++ b/app/src/main/res/layout-sw800dp/activity_passwd.xml @@ -348,7 +348,7 @@ android:layout_height="@dimen/dp_30" android:background="@drawable/bt_dialer_selector" android:gravity="center" - android:padding="@dimen/dp_4" + android:padding="@dimen/dp_8" android:src="@drawable/icon_dialer_cancel" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -367,7 +367,7 @@ android:layout_height="@dimen/dp_30" android:background="@drawable/bt_dialer_selector" android:gravity="center" - android:padding="@dimen/dp_4" + android:padding="@dimen/dp_8" android:src="@drawable/icon_dialer_confirm" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout-sw800dp/fragment_user.xml b/app/src/main/res/layout-sw800dp/fragment_user.xml index a41a0e2..0783176 100644 --- a/app/src/main/res/layout-sw800dp/fragment_user.xml +++ b/app/src/main/res/layout-sw800dp/fragment_user.xml @@ -190,9 +190,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:maxLines="1" - android:text="100%" android:textColor="@color/percent_color" - android:textSize="@dimen/sp_20" + android:textSize="@dimen/sp_12" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" @@ -437,6 +436,7 @@ @@ -444,8 +444,6 @@ android:id="@+id/tv_date3" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="@dimen/dp_8" - android:layout_marginTop="@dimen/dp_8" android:maxLines="1" android:text="2月14日 星期二" android:textColor="@color/white" @@ -453,34 +451,21 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> - - - - - + app:layout_constraintTop_toTopOf="parent" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_homework_update.xml b/app/src/main/res/layout/activity_homework_update.xml new file mode 100644 index 0000000..35045ff --- /dev/null +++ b/app/src/main/res/layout/activity_homework_update.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_passwd.xml b/app/src/main/res/layout/activity_passwd.xml index a65dff1..2c243ab 100644 --- a/app/src/main/res/layout/activity_passwd.xml +++ b/app/src/main/res/layout/activity_passwd.xml @@ -348,7 +348,7 @@ android:layout_height="@dimen/dp_30" android:background="@drawable/bt_dialer_selector" android:gravity="center" - android:padding="@dimen/dp_4" + android:padding="@dimen/dp_8" android:src="@drawable/icon_dialer_cancel" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -367,7 +367,7 @@ android:layout_height="@dimen/dp_30" android:background="@drawable/bt_dialer_selector" android:gravity="center" - android:padding="@dimen/dp_4" + android:padding="@dimen/dp_8" android:src="@drawable/icon_dialer_confirm" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/activity_user.xml b/app/src/main/res/layout/activity_user.xml index aa18bd7..62a0d0e 100644 --- a/app/src/main/res/layout/activity_user.xml +++ b/app/src/main/res/layout/activity_user.xml @@ -108,7 +108,7 @@ android:adjustViewBounds="true" android:onClick="@{click::edit}" android:scaleType="centerCrop" - android:src="@drawable/user_edit_avatar" + android:src="@drawable/icon_user_edit" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/app/src/main/res/layout/dialog_privacy_policy.xml b/app/src/main/res/layout/dialog_privacy_policy.xml index ead0a47..19d5a65 100644 --- a/app/src/main/res/layout/dialog_privacy_policy.xml +++ b/app/src/main/res/layout/dialog_privacy_policy.xml @@ -5,7 +5,7 @@ android:layout_height="match_parent"> + android:visibility="visible"> - - - - - + app:layout_constraintTop_toTopOf="parent" /> + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_homework_detail_pic.xml b/app/src/main/res/layout/item_homework_detail_pic.xml new file mode 100644 index 0000000..76ee26f --- /dev/null +++ b/app/src/main/res/layout/item_homework_detail_pic.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_homework_new.xml b/app/src/main/res/layout/item_homework_new.xml deleted file mode 100644 index 76a621c..0000000 --- a/app/src/main/res/layout/item_homework_new.xml +++ /dev/null @@ -1,106 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_homework_pic.xml b/app/src/main/res/layout/item_homework_pic.xml new file mode 100644 index 0000000..4b31e5d --- /dev/null +++ b/app/src/main/res/layout/item_homework_pic.xml @@ -0,0 +1,30 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index e27d08a..bf9c68c 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -28,6 +28,7 @@ #f4bea3 #858585 + #969696 #1AFFFFFF @@ -44,4 +45,5 @@ #EFEFEF + #80000000 diff --git a/settings.gradle b/settings.gradle index d9a5ed0..358a99e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ -include ':app', ':niceimageview', ':FlycoTabLayoutZ_Lib', ':verification-view' +include ':app', ':niceimageview', ':FlycoTabLayoutZ_Lib', ':verification-view', ':PhotoPreview' rootProject.name='科大讯飞智慧课堂' \ No newline at end of file