callback, int errCode, String errMsg, O data) {
+ if (callback != null) {
+ callback.onError(errCode, errMsg, data);
+ }
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/Compat.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/Compat.java
new file mode 100644
index 00000000..d0909d04
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/Compat.java
@@ -0,0 +1,40 @@
+/*
+ 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.tencent.qcloud.tuikit.timcommon.component.photoview;
+
+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/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/CustomGestureDetector.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/CustomGestureDetector.java
new file mode 100644
index 00000000..8e5a8468
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/CustomGestureDetector.java
@@ -0,0 +1,221 @@
+/*
+ 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.tencent.qcloud.tuikit.timcommon.component.photoview;
+
+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 OnGestureListener mListener;
+
+ 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() {
+ private float lastFocusX = 0;
+ private float lastFocusY = 0;
+
+ @Override
+ public boolean onScale(ScaleGestureDetector detector) {
+ float scaleFactor = detector.getScaleFactor();
+
+ if (Float.isNaN(scaleFactor) || Float.isInfinite(scaleFactor)) {
+ return false;
+ }
+
+ if (scaleFactor >= 0) {
+ mListener.onScale(scaleFactor,
+ detector.getFocusX(),
+ detector.getFocusY(),
+ detector.getFocusX() - lastFocusX,
+ detector.getFocusY() - lastFocusY
+ );
+ lastFocusX = detector.getFocusX();
+ lastFocusY = detector.getFocusY();
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onScaleBegin(ScaleGestureDetector detector) {
+ lastFocusX = detector.getFocusX();
+ lastFocusY = detector.getFocusY();
+ return true;
+ }
+
+ @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();
+ }
+
+ 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;
+ break;
+ case MotionEvent.ACTION_MOVE:
+ final float x = getActiveX(ev);
+ final float y = getActiveY(ev);
+ final float dx = x - mLastTouchX;
+ final float dy = y - mLastTouchY;
+
+ if (!mIsDragging) {
+ // 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();
+ final float 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;
+ default:
+ break;
+ }
+
+ mActivePointerIndex = ev
+ .findPointerIndex(mActivePointerId != INVALID_POINTER_ID ? mActivePointerId
+ : 0);
+ return true;
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnGestureListener.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnGestureListener.java
new file mode 100644
index 00000000..f3190ba8
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnGestureListener.java
@@ -0,0 +1,29 @@
+/*
+ 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.tencent.qcloud.tuikit.timcommon.component.photoview;
+
+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);
+
+ void onScale(float scaleFactor, float focusX, float focusY, float dx, float dy);
+}
\ No newline at end of file
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnMatrixChangedListener.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnMatrixChangedListener.java
new file mode 100644
index 00000000..97528d75
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnMatrixChangedListener.java
@@ -0,0 +1,18 @@
+package com.tencent.qcloud.tuikit.timcommon.component.photoview;
+
+import android.graphics.RectF;
+
+/**
+ * Interface definition for a callback to be invoked when the internal Matrix has changed for
+ * this View.
+ */
+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/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnOutsidePhotoTapListener.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnOutsidePhotoTapListener.java
new file mode 100644
index 00000000..3c67b769
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnOutsidePhotoTapListener.java
@@ -0,0 +1,14 @@
+package com.tencent.qcloud.tuikit.timcommon.component.photoview;
+
+import android.widget.ImageView;
+
+/**
+ * Callback when the user tapped outside of the photo
+ */
+public interface OnOutsidePhotoTapListener {
+
+ /**
+ * The outside of the photo has been tapped
+ */
+ void onOutsidePhotoTap(ImageView imageView);
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnPhotoTapListener.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnPhotoTapListener.java
new file mode 100644
index 00000000..a03a5645
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnPhotoTapListener.java
@@ -0,0 +1,22 @@
+package com.tencent.qcloud.tuikit.timcommon.component.photoview;
+
+import android.widget.ImageView;
+
+/**
+ * A callback to be invoked when the Photo is tapped with a single
+ * tap.
+ */
+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/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnScaleChangedListener.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnScaleChangedListener.java
new file mode 100644
index 00000000..3c82f204
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnScaleChangedListener.java
@@ -0,0 +1,17 @@
+package com.tencent.qcloud.tuikit.timcommon.component.photoview;
+
+
+/**
+ * Interface definition for callback to be invoked when attached ImageView scale changes
+ */
+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/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnSingleFlingListener.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnSingleFlingListener.java
new file mode 100644
index 00000000..46c93109
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnSingleFlingListener.java
@@ -0,0 +1,21 @@
+package com.tencent.qcloud.tuikit.timcommon.component.photoview;
+
+import android.view.MotionEvent;
+
+/**
+ * A callback to be invoked when the ImageView is flung with a single
+ * touch
+ */
+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/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnViewDragListener.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnViewDragListener.java
new file mode 100644
index 00000000..5e5d2c30
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnViewDragListener.java
@@ -0,0 +1,16 @@
+package com.tencent.qcloud.tuikit.timcommon.component.photoview;
+
+/**
+ * Interface definition for a callback to be invoked when the photo is experiencing a drag event
+ */
+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
+ */
+ void onDrag(float dx, float dy);
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnViewTapListener.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnViewTapListener.java
new file mode 100644
index 00000000..00e5eb6b
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnViewTapListener.java
@@ -0,0 +1,16 @@
+package com.tencent.qcloud.tuikit.timcommon.component.photoview;
+
+import android.view.View;
+
+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/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/PhotoView.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/PhotoView.java
new file mode 100644
index 00000000..493d84fe
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/PhotoView.java
@@ -0,0 +1,257 @@
+/*
+ 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.tencent.qcloud.tuikit.timcommon.component.photoview;
+
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.util.AttributeSet;
+import android.view.GestureDetector;
+
+import androidx.appcompat.widget.AppCompatImageView;
+
+/**
+ * A zoomable ImageView. See {@link PhotoViewAttacher} for most of the details on how the zooming
+ * is accomplished
+ */
+@SuppressWarnings("unused")
+public class PhotoView extends AppCompatImageView {
+
+ private PhotoViewAttacher attacher;
+ private ScaleType pendingScaleType;
+
+ public PhotoView(Context context) {
+ this(context, null);
+ }
+
+ public PhotoView(Context context, AttributeSet attr) {
+ this(context, attr, 0);
+ }
+
+ public PhotoView(Context context, AttributeSet attr, int defStyle) {
+ super(context, attr, defStyle);
+ init();
+ }
+
+ private void init() {
+ attacher = new PhotoViewAttacher(this);
+ //We always pose as a Matrix scale type, though we can change to another scale type
+ //via the attacher
+ super.setScaleType(ScaleType.MATRIX);
+ //apply the previously applied scale type
+ if (pendingScaleType != null) {
+ setScaleType(pendingScaleType);
+ pendingScaleType = null;
+ }
+ }
+
+ /**
+ * Get the current {@link PhotoViewAttacher} for this view. Be wary of holding on to references
+ * to this attacher, as it has a reference to this view, which, if a reference is held in the
+ * wrong place, can cause memory leaks.
+ *
+ * @return the attacher.
+ */
+ public PhotoViewAttacher getAttacher() {
+ return attacher;
+ }
+
+ @Override
+ public ScaleType getScaleType() {
+ return attacher.getScaleType();
+ }
+
+ @Override
+ public Matrix getImageMatrix() {
+ return attacher.getImageMatrix();
+ }
+
+ @Override
+ public void setOnLongClickListener(OnLongClickListener l) {
+ attacher.setOnLongClickListener(l);
+ }
+
+ @Override
+ public void setOnClickListener(OnClickListener l) {
+ attacher.setOnClickListener(l);
+ }
+
+ @Override
+ public void setScaleType(ScaleType scaleType) {
+ if (attacher == null) {
+ pendingScaleType = scaleType;
+ } else {
+ attacher.setScaleType(scaleType);
+ }
+ }
+
+ @Override
+ public void setImageDrawable(Drawable drawable) {
+ super.setImageDrawable(drawable);
+ // setImageBitmap calls through to this method
+ if (attacher != null) {
+ attacher.update();
+ }
+ }
+
+ @Override
+ public void setImageResource(int resId) {
+ super.setImageResource(resId);
+ if (attacher != null) {
+ attacher.update();
+ }
+ }
+
+ @Override
+ public void setImageURI(Uri uri) {
+ super.setImageURI(uri);
+ if (attacher != null) {
+ attacher.update();
+ }
+ }
+
+ @Override
+ protected boolean setFrame(int l, int t, int r, int b) {
+ boolean changed = super.setFrame(l, t, r, b);
+ if (changed) {
+ attacher.update();
+ }
+ return changed;
+ }
+
+ public void setRotationTo(float rotationDegree) {
+ attacher.setRotationTo(rotationDegree);
+ }
+
+ public void setRotationBy(float rotationDegree) {
+ attacher.setRotationBy(rotationDegree);
+ }
+
+ public boolean isZoomable() {
+ return attacher.isZoomable();
+ }
+
+ public void setZoomable(boolean zoomable) {
+ attacher.setZoomable(zoomable);
+ }
+
+ public RectF getDisplayRect() {
+ return attacher.getDisplayRect();
+ }
+
+ public void getDisplayMatrix(Matrix matrix) {
+ attacher.getDisplayMatrix(matrix);
+ }
+
+ @SuppressWarnings("UnusedReturnValue") public boolean setDisplayMatrix(Matrix finalRectangle) {
+ return attacher.setDisplayMatrix(finalRectangle);
+ }
+
+ public void getSuppMatrix(Matrix matrix) {
+ attacher.getSuppMatrix(matrix);
+ }
+
+ public boolean setSuppMatrix(Matrix matrix) {
+ return attacher.setDisplayMatrix(matrix);
+ }
+
+ public float getMinimumScale() {
+ return attacher.getMinimumScale();
+ }
+
+ public float getMediumScale() {
+ return attacher.getMediumScale();
+ }
+
+ public float getMaximumScale() {
+ return attacher.getMaximumScale();
+ }
+
+ public float getScale() {
+ return attacher.getScale();
+ }
+
+ public void setAllowParentInterceptOnEdge(boolean allow) {
+ attacher.setAllowParentInterceptOnEdge(allow);
+ }
+
+ public void setMinimumScale(float minimumScale) {
+ attacher.setMinimumScale(minimumScale);
+ }
+
+ public void setMediumScale(float mediumScale) {
+ attacher.setMediumScale(mediumScale);
+ }
+
+ public void setMaximumScale(float maximumScale) {
+ attacher.setMaximumScale(maximumScale);
+ }
+
+ public void setScaleLevels(float minimumScale, float mediumScale, float maximumScale) {
+ attacher.setScaleLevels(minimumScale, mediumScale, maximumScale);
+ }
+
+ public void setOnMatrixChangeListener(OnMatrixChangedListener listener) {
+ attacher.setOnMatrixChangeListener(listener);
+ }
+
+ public void setOnPhotoTapListener(OnPhotoTapListener listener) {
+ attacher.setOnPhotoTapListener(listener);
+ }
+
+ public void setOnOutsidePhotoTapListener(OnOutsidePhotoTapListener listener) {
+ attacher.setOnOutsidePhotoTapListener(listener);
+ }
+
+ public void setOnViewTapListener(OnViewTapListener listener) {
+ attacher.setOnViewTapListener(listener);
+ }
+
+ public void setOnViewDragListener(OnViewDragListener listener) {
+ attacher.setOnViewDragListener(listener);
+ }
+
+ public void setScale(float scale) {
+ attacher.setScale(scale);
+ }
+
+ public void setScale(float scale, boolean animate) {
+ attacher.setScale(scale, animate);
+ }
+
+ public void setScale(float scale, float focalX, float focalY, boolean animate) {
+ attacher.setScale(scale, focalX, focalY, animate);
+ }
+
+ public void setZoomTransitionDuration(int milliseconds) {
+ attacher.setZoomTransitionDuration(milliseconds);
+ }
+
+ public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener onDoubleTapListener) {
+ attacher.setOnDoubleTapListener(onDoubleTapListener);
+ }
+
+ public void setOnScaleChangeListener(OnScaleChangedListener onScaleChangedListener) {
+ attacher.setOnScaleChangeListener(onScaleChangedListener);
+ }
+
+ public void setOnSingleFlingListener(OnSingleFlingListener onSingleFlingListener) {
+ attacher.setOnSingleFlingListener(onSingleFlingListener);
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/PhotoViewAttacher.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/PhotoViewAttacher.java
new file mode 100644
index 00000000..1dc0e99b
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/PhotoViewAttacher.java
@@ -0,0 +1,807 @@
+/*
+ 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.tencent.qcloud.tuikit.timcommon.component.photoview;
+
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.Matrix.ScaleToFit;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnLongClickListener;
+import android.view.ViewParent;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+import android.widget.OverScroller;
+
+/**
+ * The component of {@link PhotoView} which does the work allowing for zooming, scaling, panning, etc.
+ * It is made public in case you need to subclass something other than AppCompatImageView and still
+ * gain the functionality that {@link PhotoView} offers
+ */
+public class PhotoViewAttacher implements View.OnTouchListener, View.OnLayoutChangeListener {
+ private static float DEFAULT_MAX_SCALE = 3.0f;
+ private static float DEFAULT_MID_SCALE = 1.75f;
+ private static float DEFAULT_MIN_SCALE = 1.0f;
+ private static int DEFAULT_ZOOM_DURATION = 200;
+
+ private static final int HORIZONTAL_EDGE_NONE = -1;
+ private static final int HORIZONTAL_EDGE_LEFT = 0;
+ private static final int HORIZONTAL_EDGE_RIGHT = 1;
+ private static final int HORIZONTAL_EDGE_BOTH = 2;
+ private static final int VERTICAL_EDGE_NONE = -1;
+ private static final int VERTICAL_EDGE_TOP = 0;
+ private static final int VERTICAL_EDGE_BOTTOM = 1;
+ private static final int VERTICAL_EDGE_BOTH = 2;
+ private static int SINGLE_TOUCH = 1;
+
+ private Interpolator mInterpolator = new AccelerateDecelerateInterpolator();
+ private int mZoomDuration = DEFAULT_ZOOM_DURATION;
+ private float mMinScale = DEFAULT_MIN_SCALE;
+ private float mMidScale = DEFAULT_MID_SCALE;
+ private float mMaxScale = DEFAULT_MAX_SCALE;
+
+ private boolean mAllowParentInterceptOnEdge = true;
+ private boolean mBlockParentIntercept = false;
+
+ private ImageView mImageView;
+
+ // Gesture Detectors
+ private GestureDetector mGestureDetector;
+ private CustomGestureDetector mScaleDragDetector;
+
+ // These are set so we don't keep allocating them on the heap
+ private final Matrix mBaseMatrix = new Matrix();
+ private final Matrix mDrawMatrix = new Matrix();
+ private final Matrix mSuppMatrix = new Matrix();
+ private final RectF mDisplayRect = new RectF();
+ private final float[] mMatrixValues = new float[9];
+
+ // Listeners
+ private OnMatrixChangedListener mMatrixChangeListener;
+ private OnPhotoTapListener mPhotoTapListener;
+ private OnOutsidePhotoTapListener mOutsidePhotoTapListener;
+ private OnViewTapListener mViewTapListener;
+ private View.OnClickListener mOnClickListener;
+ private OnLongClickListener mLongClickListener;
+ private OnScaleChangedListener mScaleChangeListener;
+ private OnSingleFlingListener mSingleFlingListener;
+ private OnViewDragListener mOnViewDragListener;
+
+ private FlingRunnable mCurrentFlingRunnable;
+ private int mHorizontalScrollEdge = HORIZONTAL_EDGE_BOTH;
+ private int mVerticalScrollEdge = VERTICAL_EDGE_BOTH;
+ private float mBaseRotation;
+
+ private boolean mZoomEnabled = true;
+ private ScaleType mScaleType = ScaleType.FIT_CENTER;
+
+ private OnGestureListener onGestureListener = new OnGestureListener() {
+ @Override
+ public void onDrag(float dx, float dy) {
+ if (mScaleDragDetector.isScaling()) {
+ return; // Do not drag if we are already scaling
+ }
+ if (mOnViewDragListener != null) {
+ mOnViewDragListener.onDrag(dx, dy);
+ }
+ mSuppMatrix.postTranslate(dx, dy);
+ checkAndDisplayMatrix();
+
+ /*
+ * Here we decide whether to let the ImageView's parent to start taking
+ * over the touch event.
+ *
+ * First we check whether this function is enabled. We never want the
+ * parent to take over if we're scaling. We then check the edge we're
+ * on, and the direction of the scroll (i.e. if we're pulling against
+ * the edge, aka 'overscrolling', let the parent take over).
+ */
+ ViewParent parent = mImageView.getParent();
+ if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isScaling() && !mBlockParentIntercept) {
+ if (mHorizontalScrollEdge == HORIZONTAL_EDGE_BOTH || (mHorizontalScrollEdge == HORIZONTAL_EDGE_LEFT && dx >= 1f)
+ || (mHorizontalScrollEdge == HORIZONTAL_EDGE_RIGHT && dx <= -1f) || (mVerticalScrollEdge == VERTICAL_EDGE_TOP && dy >= 1f)
+ || (mVerticalScrollEdge == VERTICAL_EDGE_BOTTOM && dy <= -1f)) {
+ if (parent != null) {
+ parent.requestDisallowInterceptTouchEvent(false);
+ }
+ }
+ } else {
+ if (parent != null) {
+ parent.requestDisallowInterceptTouchEvent(true);
+ }
+ }
+ }
+
+ @Override
+ public void onFling(float startX, float startY, float velocityX, float velocityY) {
+ mCurrentFlingRunnable = new FlingRunnable(mImageView.getContext());
+ mCurrentFlingRunnable.fling(getImageViewWidth(mImageView), getImageViewHeight(mImageView), (int) velocityX, (int) velocityY);
+ mImageView.post(mCurrentFlingRunnable);
+ }
+
+ @Override
+ public void onScale(float scaleFactor, float focusX, float focusY) {
+ onScale(scaleFactor, focusX, focusY, 0, 0);
+ }
+
+ @Override
+ public void onScale(float scaleFactor, float focusX, float focusY, float dx, float dy) {
+ if (getScale() < mMaxScale || scaleFactor < 1f) {
+ if (mScaleChangeListener != null) {
+ mScaleChangeListener.onScaleChange(scaleFactor, focusX, focusY);
+ }
+ mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);
+ mSuppMatrix.postTranslate(dx, dy);
+ checkAndDisplayMatrix();
+ }
+ }
+ };
+
+ public PhotoViewAttacher(ImageView imageView) {
+ mImageView = imageView;
+ imageView.setOnTouchListener(this);
+ imageView.addOnLayoutChangeListener(this);
+ if (imageView.isInEditMode()) {
+ return;
+ }
+ mBaseRotation = 0.0f;
+ // Create Gesture Detectors...
+ mScaleDragDetector = new CustomGestureDetector(imageView.getContext(), onGestureListener);
+ mGestureDetector = new GestureDetector(imageView.getContext(), new GestureDetector.SimpleOnGestureListener() {
+ // forward long click listener
+ @Override
+ public void onLongPress(MotionEvent e) {
+ if (mLongClickListener != null) {
+ mLongClickListener.onLongClick(mImageView);
+ }
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ if (mSingleFlingListener != null) {
+ if (getScale() > DEFAULT_MIN_SCALE) {
+ return false;
+ }
+ if (e1.getPointerCount() > SINGLE_TOUCH || e2.getPointerCount() > SINGLE_TOUCH) {
+ return false;
+ }
+ return mSingleFlingListener.onFling(e1, e2, velocityX, velocityY);
+ }
+ return false;
+ }
+ });
+ mGestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() {
+ @Override
+ public boolean onSingleTapConfirmed(MotionEvent e) {
+ if (mOnClickListener != null) {
+ mOnClickListener.onClick(mImageView);
+ }
+ final RectF displayRect = getDisplayRect();
+ final float x = e.getX();
+ final float 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;
+ }
+
+
+ /**
+ * 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;
+ }
+
+ 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;
+ default:
+ break;
+ }
+ // Try the Scale/Drag detector
+ if (mScaleDragDetector != null) {
+ boolean wasScaling = mScaleDragDetector.isScaling();
+ boolean wasDragging = mScaleDragDetector.isDragging();
+ handled = mScaleDragDetector.onTouchEvent(ev);
+ boolean didntScale = !wasScaling && !mScaleDragDetector.isScaling();
+ boolean didntDrag = !wasDragging && !mScaleDragDetector.isDragging();
+ mBlockParentIntercept = didntScale && didntDrag;
+ }
+ // Check to see if the user double tapped
+ if (mGestureDetector != null && mGestureDetector.onTouchEvent(ev)) {
+ handled = true;
+ }
+ }
+ return handled;
+ }
+
+ public void setAllowParentInterceptOnEdge(boolean allow) {
+ mAllowParentInterceptOnEdge = allow;
+ }
+
+ public void setMinimumScale(float minimumScale) {
+ Util.checkZoomLevels(minimumScale, mMidScale, mMaxScale);
+ mMinScale = minimumScale;
+ }
+
+ public void setMediumScale(float mediumScale) {
+ Util.checkZoomLevels(mMinScale, mediumScale, mMaxScale);
+ mMidScale = mediumScale;
+ }
+
+ public void setMaximumScale(float maximumScale) {
+ Util.checkZoomLevels(mMinScale, mMidScale, maximumScale);
+ mMaxScale = maximumScale;
+ }
+
+ public void setScaleLevels(float minimumScale, float mediumScale, float maximumScale) {
+ Util.checkZoomLevels(minimumScale, mediumScale, maximumScale);
+ mMinScale = minimumScale;
+ mMidScale = mediumScale;
+ mMaxScale = maximumScale;
+ }
+
+ public void setOnLongClickListener(OnLongClickListener listener) {
+ mLongClickListener = listener;
+ }
+
+ public void setOnClickListener(View.OnClickListener listener) {
+ mOnClickListener = listener;
+ }
+
+ public void setOnMatrixChangeListener(OnMatrixChangedListener listener) {
+ mMatrixChangeListener = listener;
+ }
+
+ public void setOnPhotoTapListener(OnPhotoTapListener listener) {
+ mPhotoTapListener = listener;
+ }
+
+ public void setOnOutsidePhotoTapListener(OnOutsidePhotoTapListener mOutsidePhotoTapListener) {
+ this.mOutsidePhotoTapListener = mOutsidePhotoTapListener;
+ }
+
+ public void setOnViewTapListener(OnViewTapListener listener) {
+ mViewTapListener = listener;
+ }
+
+ public void setOnViewDragListener(OnViewDragListener listener) {
+ mOnViewDragListener = listener;
+ }
+
+ public void setScale(float scale) {
+ setScale(scale, false);
+ }
+
+ public void setScale(float scale, boolean animate) {
+ setScale(scale, (mImageView.getRight()) / 2, (mImageView.getBottom()) / 2, animate);
+ }
+
+ public void setScale(float scale, float focalX, float focalY, boolean animate) {
+ // Check to see if the scale is within bounds
+ if (scale < mMinScale || scale > mMaxScale) {
+ throw new IllegalArgumentException("Scale must be within the range of minScale and maxScale");
+ }
+ if (animate) {
+ mImageView.post(new AnimatedZoomRunnable(getScale(), scale, focalX, focalY));
+ } else {
+ mSuppMatrix.setScale(scale, scale, focalX, focalY);
+ checkAndDisplayMatrix();
+ }
+ }
+
+ /**
+ * Set the zoom interpolator
+ *
+ * @param interpolator the zoom interpolator
+ */
+ public void setZoomInterpolator(Interpolator interpolator) {
+ mInterpolator = interpolator;
+ }
+
+ public void setScaleType(ScaleType scaleType) {
+ if (Util.isSupportedScaleType(scaleType) && scaleType != mScaleType) {
+ mScaleType = scaleType;
+ update();
+ }
+ }
+
+ public boolean isZoomable() {
+ return mZoomEnabled;
+ }
+
+ public void setZoomable(boolean zoomable) {
+ mZoomEnabled = zoomable;
+ update();
+ }
+
+ public void update() {
+ if (mZoomEnabled) {
+ // Update the base matrix using the current drawable
+ updateBaseMatrix(mImageView.getDrawable());
+ } else {
+ // Reset the Matrix...
+ resetMatrix();
+ }
+ }
+
+ /**
+ * Get the display matrix
+ *
+ * @param matrix target matrix to copy to
+ */
+ public void getDisplayMatrix(Matrix matrix) {
+ matrix.set(getDrawMatrix());
+ }
+
+ /**
+ * Get the current support matrix
+ */
+ public void getSuppMatrix(Matrix matrix) {
+ matrix.set(mSuppMatrix);
+ }
+
+ private Matrix getDrawMatrix() {
+ mDrawMatrix.set(mBaseMatrix);
+ mDrawMatrix.postConcat(mSuppMatrix);
+ return mDrawMatrix;
+ }
+
+ public Matrix getImageMatrix() {
+ return mDrawMatrix;
+ }
+
+ public void setZoomTransitionDuration(int milliseconds) {
+ this.mZoomDuration = milliseconds;
+ }
+
+ /**
+ * Helper method that 'unpacks' a Matrix and returns the required value
+ *
+ * @param matrix Matrix to unpack
+ * @param whichValue Which value from Matrix.M* to return
+ * @return returned value
+ */
+ private float getValue(Matrix matrix, int whichValue) {
+ matrix.getValues(mMatrixValues);
+ return mMatrixValues[whichValue];
+ }
+
+ /**
+ * Resets the Matrix back to FIT_CENTER, and then displays its contents
+ */
+ private void resetMatrix() {
+ mSuppMatrix.reset();
+ setRotationBy(mBaseRotation);
+ setImageViewMatrix(getDrawMatrix());
+ checkMatrixBounds();
+ }
+
+ private void setImageViewMatrix(Matrix matrix) {
+ mImageView.setImageMatrix(matrix);
+ // Call MatrixChangedListener if needed
+ if (mMatrixChangeListener != null) {
+ RectF displayRect = getDisplayRect(matrix);
+ if (displayRect != null) {
+ mMatrixChangeListener.onMatrixChanged(displayRect);
+ }
+ }
+ }
+
+ /**
+ * Helper method that simply checks the Matrix, and then displays the result
+ */
+ private void checkAndDisplayMatrix() {
+ if (checkMatrixBounds()) {
+ setImageViewMatrix(getDrawMatrix());
+ }
+ }
+
+ /**
+ * 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();
+ final float width = rect.width();
+ float deltaX = 0;
+ float deltaY = 0;
+ final int viewHeight = getImageViewHeight(mImageView);
+ if (height <= viewHeight) {
+ switch (mScaleType) {
+ case FIT_START:
+ deltaY = -rect.top;
+ break;
+ case FIT_END:
+ deltaY = viewHeight - height - rect.top;
+ break;
+ default:
+ deltaY = (viewHeight - height) / 2 - rect.top;
+ break;
+ }
+ mVerticalScrollEdge = VERTICAL_EDGE_BOTH;
+ } else if (rect.top > 0) {
+ mVerticalScrollEdge = VERTICAL_EDGE_TOP;
+ deltaY = -rect.top;
+ } else if (rect.bottom < viewHeight) {
+ mVerticalScrollEdge = VERTICAL_EDGE_BOTTOM;
+ deltaY = viewHeight - rect.bottom;
+ } else {
+ mVerticalScrollEdge = VERTICAL_EDGE_NONE;
+ }
+ final int viewWidth = getImageViewWidth(mImageView);
+ if (width <= viewWidth) {
+ switch (mScaleType) {
+ case FIT_START:
+ deltaX = -rect.left;
+ break;
+ case FIT_END:
+ deltaX = viewWidth - width - rect.left;
+ break;
+ default:
+ deltaX = (viewWidth - width) / 2 - rect.left;
+ break;
+ }
+ mHorizontalScrollEdge = HORIZONTAL_EDGE_BOTH;
+ } else if (rect.left > 0) {
+ mHorizontalScrollEdge = HORIZONTAL_EDGE_LEFT;
+ deltaX = -rect.left;
+ } else if (rect.right < viewWidth) {
+ deltaX = viewWidth - rect.right;
+ mHorizontalScrollEdge = HORIZONTAL_EDGE_RIGHT;
+ } else {
+ mHorizontalScrollEdge = HORIZONTAL_EDGE_NONE;
+ }
+ // Finally actually translate the matrix
+ mSuppMatrix.postTranslate(deltaX, deltaY);
+ return true;
+ }
+
+ private int getImageViewWidth(ImageView imageView) {
+ return imageView.getWidth() - imageView.getPaddingLeft() - imageView.getPaddingRight();
+ }
+
+ private int getImageViewHeight(ImageView imageView) {
+ return imageView.getHeight() - imageView.getPaddingTop() - imageView.getPaddingBottom();
+ }
+
+ private void cancelFling() {
+ if (mCurrentFlingRunnable != null) {
+ mCurrentFlingRunnable.cancelFling();
+ mCurrentFlingRunnable = null;
+ }
+ }
+
+ private class AnimatedZoomRunnable implements Runnable {
+ private final float mFocalX;
+ private final float mFocalY;
+ private final long mStartTime;
+ private final float mZoomStart;
+ private final float 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;
+ private int 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;
+ final int maxX;
+ final int minY;
+ final int 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/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/Util.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/Util.java
new file mode 100644
index 00000000..3b5ce765
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/Util.java
@@ -0,0 +1,39 @@
+package com.tencent.qcloud.tuikit.timcommon.component.photoview;
+
+import android.view.MotionEvent;
+import android.widget.ImageView;
+
+class Util {
+
+ static void checkZoomLevels(float minZoom, float midZoom,
+ float maxZoom) {
+ if (minZoom >= midZoom) {
+ throw new IllegalArgumentException(
+ "Minimum zoom has to be less than Medium zoom. Call setMinimumZoom() with a more appropriate value");
+ } else if (midZoom >= maxZoom) {
+ throw new IllegalArgumentException(
+ "Medium zoom has to be less than Maximum zoom. Call setMaximumZoom() with a more appropriate value");
+ }
+ }
+
+ static boolean hasDrawable(ImageView imageView) {
+ return imageView.getDrawable() != null;
+ }
+
+ static boolean isSupportedScaleType(final ImageView.ScaleType scaleType) {
+ if (scaleType == null) {
+ return false;
+ }
+ switch (scaleType) {
+ case MATRIX:
+ throw new IllegalStateException("Matrix scale type is not supported");
+ default:
+ break;
+ }
+ return true;
+ }
+
+ static int getPointerIndex(int action) {
+ return (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/scroller/CenteredSmoothScroller.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/scroller/CenteredSmoothScroller.java
new file mode 100644
index 00000000..2003da35
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/scroller/CenteredSmoothScroller.java
@@ -0,0 +1,40 @@
+package com.tencent.qcloud.tuikit.timcommon.component.scroller;
+
+import android.content.Context;
+import android.view.View;
+
+import androidx.recyclerview.widget.LinearSmoothScroller;
+import androidx.recyclerview.widget.OrientationHelper;
+import androidx.recyclerview.widget.RecyclerView;
+
+public class CenteredSmoothScroller extends LinearSmoothScroller {
+
+ public CenteredSmoothScroller(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
+ RecyclerView.LayoutManager layoutManager = getLayoutManager();
+ if (layoutManager == null) {
+ return;
+ }
+ int distance = calculateDistanceToCenter(targetView, layoutManager);
+ int time = calculateTimeForDeceleration(distance);
+ if (time > 0) {
+ action.update(0, distance, time, mDecelerateInterpolator);
+ }
+ }
+
+ private int calculateDistanceToCenter(View targetView, RecyclerView.LayoutManager layoutManager) {
+ OrientationHelper helper = OrientationHelper.createVerticalHelper(layoutManager);
+ int childCenter = helper.getDecoratedStart(targetView) + helper.getDecoratedMeasurement(targetView) / 2;
+ int containerCenter;
+ if (layoutManager.getClipToPadding()) {
+ containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
+ } else {
+ containerCenter = helper.getEnd() / 2;
+ }
+ return childCenter - containerCenter;
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/Attributes.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/Attributes.java
new file mode 100644
index 00000000..a3ffdaa4
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/Attributes.java
@@ -0,0 +1,5 @@
+package com.tencent.qcloud.tuikit.timcommon.component.swipe;
+
+public class Attributes {
+ public enum Mode { Single, Multiple }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/RecyclerSwipeAdapter.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/RecyclerSwipeAdapter.java
new file mode 100644
index 00000000..95b2c0d9
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/RecyclerSwipeAdapter.java
@@ -0,0 +1,85 @@
+package com.tencent.qcloud.tuikit.timcommon.component.swipe;
+
+import android.view.ViewGroup;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.List;
+
+public abstract class RecyclerSwipeAdapter
+ extends RecyclerView.Adapter implements SwipeItemMangerInterface, SwipeAdapterInterface {
+ public SwipeItemMangerImpl mItemManger = new SwipeItemMangerImpl(this);
+
+ @Override public abstract VH onCreateViewHolder(ViewGroup parent, int viewType);
+
+ @Override public abstract void onBindViewHolder(VH viewHolder, final int position);
+
+ @Override
+ public void notifyDatasetChanged() {
+ super.notifyDataSetChanged();
+ }
+
+ @Override
+ public void notifySwipeItemChanged(int position) {
+ super.notifyItemChanged(position);
+ }
+
+ @Override
+ public void openItem(int position) {
+ mItemManger.openItem(position);
+ }
+
+ @Override
+ public void closeItem(int position) {
+ mItemManger.closeItem(position);
+ }
+
+ @Override
+ public void closeAllExcept(SwipeLayout layout) {
+ mItemManger.closeAllExcept(layout);
+ }
+
+ @Override
+ public void closeAllSwipeItems() {
+ mItemManger.closeAllSwipeItems();
+ }
+
+ @Override
+ public List getOpenItems() {
+ return mItemManger.getOpenItems();
+ }
+
+ @Override
+ public List getOpenLayouts() {
+ return mItemManger.getOpenLayouts();
+ }
+
+ @Override
+ public void removeShownLayouts(SwipeLayout layout) {
+ mItemManger.removeShownLayouts(layout);
+ }
+
+ @Override
+ public boolean isOpen(int position) {
+ return mItemManger.isOpen(position);
+ }
+
+ @Override
+ public Attributes.Mode getMode() {
+ return mItemManger.getMode();
+ }
+
+ @Override
+ public void setMode(Attributes.Mode mode) {
+ mItemManger.setMode(mode);
+ }
+
+ @Override
+ public void switchAllSwipeEnable(boolean enable) {
+ mItemManger.switchAllSwipeEnable(enable);
+ }
+
+ public void setSwipeEnabled(boolean enabled) {
+ mItemManger.setSwipeEnabled(enabled);
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SimpleSwipeListener.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SimpleSwipeListener.java
new file mode 100644
index 00000000..286c6075
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SimpleSwipeListener.java
@@ -0,0 +1,21 @@
+package com.tencent.qcloud.tuikit.timcommon.component.swipe;
+
+public class SimpleSwipeListener implements SwipeLayout.SwipeListener {
+ @Override
+ public void onStartOpen(SwipeLayout layout) {}
+
+ @Override
+ public void onOpen(SwipeLayout layout) {}
+
+ @Override
+ public void onStartClose(SwipeLayout layout) {}
+
+ @Override
+ public void onClose(SwipeLayout layout) {}
+
+ @Override
+ public void onUpdate(SwipeLayout layout, int leftOffset, int topOffset) {}
+
+ @Override
+ public void onHandRelease(SwipeLayout layout, float xvel, float yvel) {}
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SwipeAdapterInterface.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SwipeAdapterInterface.java
new file mode 100644
index 00000000..35bd2966
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SwipeAdapterInterface.java
@@ -0,0 +1,9 @@
+package com.tencent.qcloud.tuikit.timcommon.component.swipe;
+
+public interface SwipeAdapterInterface {
+ int getSwipeLayoutResourceId(int position);
+
+ void notifyDatasetChanged();
+
+ void notifySwipeItemChanged(int position);
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SwipeItemMangerImpl.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SwipeItemMangerImpl.java
new file mode 100644
index 00000000..2fd1ff5c
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SwipeItemMangerImpl.java
@@ -0,0 +1,219 @@
+package com.tencent.qcloud.tuikit.timcommon.component.swipe;
+
+import android.view.View;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class SwipeItemMangerImpl implements SwipeItemMangerInterface {
+ private Attributes.Mode mode = Attributes.Mode.Single;
+ public static final int INVALID_POSITION = -1;
+
+ protected int mOpenPosition = INVALID_POSITION;
+
+ protected Set mOpenPositions = new HashSet();
+ protected Set mShownLayouts = new HashSet();
+
+ protected boolean isSwipeEnabled = true;
+ protected SwipeAdapterInterface swipeAdapterInterface;
+
+ public SwipeItemMangerImpl(SwipeAdapterInterface swipeAdapterInterface) {
+ if (swipeAdapterInterface == null) {
+ throw new IllegalArgumentException("SwipeAdapterInterface can not be null");
+ }
+
+ this.swipeAdapterInterface = swipeAdapterInterface;
+ }
+
+ public Attributes.Mode getMode() {
+ return mode;
+ }
+
+ public void setMode(Attributes.Mode mode) {
+ this.mode = mode;
+ mOpenPositions.clear();
+ mShownLayouts.clear();
+ mOpenPosition = INVALID_POSITION;
+ }
+
+ public void bind(View view, int position) {
+ int resId = swipeAdapterInterface.getSwipeLayoutResourceId(position);
+ SwipeLayout swipeLayout = (SwipeLayout) view.findViewById(resId);
+ if (swipeLayout == null) {
+ throw new IllegalStateException("can not find SwipeLayout in target view");
+ }
+
+ swipeLayout.setSwipeEnabled(isSwipeEnabled);
+ if (swipeLayout.getTag(resId) == null) {
+ OnLayoutListener onLayoutListener = new OnLayoutListener(position);
+ SwipeMemory swipeMemory = new SwipeMemory(position);
+ swipeLayout.addSwipeListener(swipeMemory);
+ swipeLayout.addOnLayoutListener(onLayoutListener);
+ swipeLayout.setTag(resId, new ValueBox(position, swipeMemory, onLayoutListener));
+ mShownLayouts.add(swipeLayout);
+ } else {
+ ValueBox valueBox = (ValueBox) swipeLayout.getTag(resId);
+ valueBox.swipeMemory.setPosition(position);
+ valueBox.onLayoutListener.setPosition(position);
+ valueBox.position = position;
+ }
+ }
+
+ @Override
+ public void openItem(int position) {
+ if (mode == Attributes.Mode.Multiple) {
+ if (!mOpenPositions.contains(position)) {
+ mOpenPositions.add(position);
+ }
+ } else {
+ mOpenPosition = position;
+ }
+ swipeAdapterInterface.notifySwipeItemChanged(position);
+ }
+
+ @Override
+ public void closeItem(int position) {
+ if (mode == Attributes.Mode.Multiple) {
+ mOpenPositions.remove(position);
+ } else {
+ if (mOpenPosition == position) {
+ mOpenPosition = INVALID_POSITION;
+ }
+ }
+ swipeAdapterInterface.notifySwipeItemChanged(position);
+ }
+
+ @Override
+ public void closeAllExcept(SwipeLayout layout) {
+ for (SwipeLayout s : mShownLayouts) {
+ if (s != layout) {
+ s.close();
+ }
+ }
+ }
+
+ @Override
+ public void closeAllSwipeItems() {
+ if (mode == Attributes.Mode.Multiple) {
+ mOpenPositions.clear();
+ } else {
+ mOpenPosition = INVALID_POSITION;
+ }
+ for (SwipeLayout s : mShownLayouts) {
+ s.close();
+ }
+ }
+
+ @Override
+ public void switchAllSwipeEnable(boolean enable) {
+ for (SwipeLayout s : mShownLayouts) {
+ s.setSwipeEnabled(enable);
+ }
+ }
+
+ @Override
+ public void removeShownLayouts(SwipeLayout layout) {
+ mShownLayouts.remove(layout);
+ }
+
+ @Override
+ public List getOpenItems() {
+ if (mode == Attributes.Mode.Multiple) {
+ return new ArrayList(mOpenPositions);
+ } else {
+ return Collections.singletonList(mOpenPosition);
+ }
+ }
+
+ @Override
+ public List getOpenLayouts() {
+ return new ArrayList(mShownLayouts);
+ }
+
+ @Override
+ public boolean isOpen(int position) {
+ if (mode == Attributes.Mode.Multiple) {
+ return mOpenPositions.contains(position);
+ } else {
+ return mOpenPosition == position;
+ }
+ }
+
+ public void setSwipeEnabled(boolean swipeEnabled) {
+ isSwipeEnabled = swipeEnabled;
+ }
+
+ class ValueBox {
+ OnLayoutListener onLayoutListener;
+ SwipeMemory swipeMemory;
+ int position;
+
+ ValueBox(int position, SwipeMemory swipeMemory, OnLayoutListener onLayoutListener) {
+ this.swipeMemory = swipeMemory;
+ this.onLayoutListener = onLayoutListener;
+ this.position = position;
+ }
+ }
+
+ class OnLayoutListener implements SwipeLayout.OnLayout {
+ private int position;
+
+ OnLayoutListener(int position) {
+ this.position = position;
+ }
+
+ public void setPosition(int position) {
+ this.position = position;
+ }
+
+ @Override
+ public void onLayout(SwipeLayout v) {
+ if (isOpen(position)) {
+ v.open(false, false);
+ } else {
+ v.close(false, false);
+ }
+ }
+ }
+
+ class SwipeMemory extends SimpleSwipeListener {
+ private int position;
+
+ SwipeMemory(int position) {
+ this.position = position;
+ }
+
+ @Override
+ public void onClose(SwipeLayout layout) {
+ if (mode == Attributes.Mode.Multiple) {
+ mOpenPositions.remove(position);
+ } else {
+ mOpenPosition = INVALID_POSITION;
+ }
+ }
+
+ @Override
+ public void onStartOpen(SwipeLayout layout) {
+ if (mode == Attributes.Mode.Single) {
+ closeAllExcept(layout);
+ }
+ }
+
+ @Override
+ public void onOpen(SwipeLayout layout) {
+ if (mode == Attributes.Mode.Multiple) {
+ mOpenPositions.add(position);
+ } else {
+ closeAllExcept(layout);
+ mOpenPosition = position;
+ }
+ }
+
+ public void setPosition(int position) {
+ this.position = position;
+ }
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SwipeItemMangerInterface.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SwipeItemMangerInterface.java
new file mode 100644
index 00000000..137441ed
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SwipeItemMangerInterface.java
@@ -0,0 +1,27 @@
+package com.tencent.qcloud.tuikit.timcommon.component.swipe;
+
+import java.util.List;
+
+public interface SwipeItemMangerInterface {
+ void openItem(int position);
+
+ void closeItem(int position);
+
+ void closeAllExcept(SwipeLayout layout);
+
+ void closeAllSwipeItems();
+
+ List getOpenItems();
+
+ List getOpenLayouts();
+
+ void removeShownLayouts(SwipeLayout layout);
+
+ boolean isOpen(int position);
+
+ Attributes.Mode getMode();
+
+ void setMode(Attributes.Mode mode);
+
+ void switchAllSwipeEnable(boolean enable);
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SwipeLayout.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SwipeLayout.java
new file mode 100644
index 00000000..72a757f2
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SwipeLayout.java
@@ -0,0 +1,1806 @@
+package com.tencent.qcloud.tuikit.timcommon.component.swipe;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.GestureDetector;
+import android.view.Gravity;
+import android.view.HapticFeedbackConstants;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.FrameLayout;
+
+import androidx.core.view.GravityCompat;
+import androidx.core.view.ViewCompat;
+import androidx.customview.widget.ViewDragHelper;
+
+import com.tencent.qcloud.tuikit.timcommon.R;
+import com.tencent.qcloud.tuikit.timcommon.util.LayoutUtil;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class SwipeLayout extends FrameLayout {
+ @Deprecated public static final int EMPTY_LAYOUT = -1;
+ private static final int DRAG_LEFT = 1;
+ private static final int DRAG_RIGHT = 2;
+ private static final int DRAG_TOP = 4;
+ private static final int DRAG_BOTTOM = 8;
+ private static final DragEdge DefaultDragEdge = DragEdge.Right;
+
+ private int mTouchSlop;
+
+ private DragEdge mCurrentDragEdge = DefaultDragEdge;
+ private ViewDragHelper mDragHelper;
+
+ private int mDragDistance = 0;
+ private LinkedHashMap mDragEdges = new LinkedHashMap<>();
+ private ShowMode mShowMode;
+
+ private float[] mEdgeSwipesOffset = new float[4];
+
+ private List mSwipeListeners = new ArrayList<>();
+ private List mSwipeDeniers = new ArrayList<>();
+ private Map> mRevealListeners = new HashMap<>();
+ private Map mShowEntirely = new HashMap<>();
+ private Map mViewBoundCache = new HashMap<>(); // save all children's bound, restore in onLayout
+
+ private DoubleClickListener mDoubleClickListener;
+
+ private boolean mSwipeEnabled = true;
+ private boolean[] mSwipesEnabled = new boolean[] {true, true, true, true};
+ private boolean mClickToClose = false;
+ private float mWillOpenPercentAfterOpen = 0.75f;
+ private float mWillOpenPercentAfterClose = 0.25f;
+
+ public enum DragEdge { Left, Top, Right, Bottom }
+
+ public enum ShowMode { LayDown, PullOut }
+
+ public SwipeLayout(Context context) {
+ this(context, null);
+ }
+
+ public SwipeLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public SwipeLayout(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ mDragHelper = ViewDragHelper.create(this, mDragHelperCallback);
+ mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SwipeLayout);
+ mEdgeSwipesOffset[DragEdge.Left.ordinal()] = a.getDimension(R.styleable.SwipeLayout_leftEdgeSwipeOffset, 0);
+ mEdgeSwipesOffset[DragEdge.Right.ordinal()] = a.getDimension(R.styleable.SwipeLayout_rightEdgeSwipeOffset, 0);
+ mEdgeSwipesOffset[DragEdge.Top.ordinal()] = a.getDimension(R.styleable.SwipeLayout_topEdgeSwipeOffset, 0);
+ mEdgeSwipesOffset[DragEdge.Bottom.ordinal()] = a.getDimension(R.styleable.SwipeLayout_bottomEdgeSwipeOffset, 0);
+ setClickToClose(a.getBoolean(R.styleable.SwipeLayout_clickToClose, mClickToClose));
+ int defaultDragEdge = DRAG_RIGHT;
+ if (LayoutUtil.isRTL()) {
+ defaultDragEdge = DRAG_LEFT;
+ }
+ int dragEdgeChoices = a.getInt(R.styleable.SwipeLayout_drag_edge, defaultDragEdge);
+ if ((dragEdgeChoices & DRAG_LEFT) == DRAG_LEFT) {
+ mDragEdges.put(DragEdge.Left, null);
+ }
+ if ((dragEdgeChoices & DRAG_TOP) == DRAG_TOP) {
+ mDragEdges.put(DragEdge.Top, null);
+ }
+ if ((dragEdgeChoices & DRAG_RIGHT) == DRAG_RIGHT) {
+ mDragEdges.put(DragEdge.Right, null);
+ }
+ if ((dragEdgeChoices & DRAG_BOTTOM) == DRAG_BOTTOM) {
+ mDragEdges.put(DragEdge.Bottom, null);
+ }
+ int ordinal = a.getInt(R.styleable.SwipeLayout_show_mode, ShowMode.PullOut.ordinal());
+ mShowMode = ShowMode.values()[ordinal];
+ a.recycle();
+ }
+
+ public interface SwipeListener {
+ void onStartOpen(SwipeLayout layout);
+
+ void onOpen(SwipeLayout layout);
+
+ void onStartClose(SwipeLayout layout);
+
+ void onClose(SwipeLayout layout);
+
+ void onUpdate(SwipeLayout layout, int leftOffset, int topOffset);
+
+ void onHandRelease(SwipeLayout layout, float xvel, float yvel);
+ }
+
+ public void addSwipeListener(SwipeListener l) {
+ mSwipeListeners.add(l);
+ }
+
+ public void removeSwipeListener(SwipeListener l) {
+ mSwipeListeners.remove(l);
+ }
+
+ public void removeAllSwipeListener() {
+ mSwipeListeners.clear();
+ }
+
+ public interface SwipeDenier {
+ boolean shouldDenySwipe(MotionEvent ev);
+ }
+
+ public void addSwipeDenier(SwipeDenier denier) {
+ mSwipeDeniers.add(denier);
+ }
+
+ public void removeSwipeDenier(SwipeDenier denier) {
+ mSwipeDeniers.remove(denier);
+ }
+
+ public void removeAllSwipeDeniers() {
+ mSwipeDeniers.clear();
+ }
+
+ public interface OnRevealListener {
+ void onReveal(View child, DragEdge edge, float fraction, int distance);
+ }
+
+ /**
+ * bind a view with a specific
+ * {@link com.tencent.qcloud.tuikit.timcommon.component.swipe.SwipeLayout.OnRevealListener}
+ *
+ * @param childId the view id.
+ * @param l the target
+ * {@link com.tencent.qcloud.tuikit.timcommon.component.swipe.SwipeLayout.OnRevealListener}
+ */
+ public void addRevealListener(int childId, OnRevealListener l) {
+ View child = findViewById(childId);
+ if (child == null) {
+ throw new IllegalArgumentException("Child does not belong to SwipeListener.");
+ }
+
+ if (!mShowEntirely.containsKey(child)) {
+ mShowEntirely.put(child, false);
+ }
+ if (mRevealListeners.get(child) == null) {
+ mRevealListeners.put(child, new ArrayList());
+ }
+
+ mRevealListeners.get(child).add(l);
+ }
+
+ /**
+ * bind multiple views with an
+ * {@link com.tencent.qcloud.tuikit.timcommon.component.swipe.SwipeLayout.OnRevealListener}.
+ *
+ * @param childIds the view id.
+ * @param l the {@link com.tencent.qcloud.tuikit.timcommon.component.swipe.SwipeLayout.OnRevealListener}
+ */
+ public void addRevealListener(int[] childIds, OnRevealListener l) {
+ for (int i : childIds) {
+ addRevealListener(i, l);
+ }
+ }
+
+ public void removeRevealListener(int childId, OnRevealListener l) {
+ View child = findViewById(childId);
+
+ if (child == null) {
+ return;
+ }
+
+ mShowEntirely.remove(child);
+ if (mRevealListeners.containsKey(child)) {
+ mRevealListeners.get(child).remove(l);
+ }
+ }
+
+ public void removeAllRevealListeners(int childId) {
+ View child = findViewById(childId);
+ if (child != null) {
+ mRevealListeners.remove(child);
+ mShowEntirely.remove(child);
+ }
+ }
+
+ private ViewDragHelper.Callback mDragHelperCallback = new ViewDragHelper.Callback() {
+ @Override
+ public int clampViewPositionHorizontal(View child, int left, int dx) {
+ return handleClampHorizontal(child, left);
+ }
+
+ @Override
+ public int clampViewPositionVertical(View child, int top, int dy) {
+ return handleClampVertical(child, top, dy);
+ }
+
+ @Override
+ public boolean tryCaptureView(View child, int pointerId) {
+ boolean result = child == getSurfaceView() || getBottomViews().contains(child);
+ if (result) {
+ isCloseBeforeDrag = getOpenStatus() == Status.Close;
+ }
+ return result;
+ }
+
+ @Override
+ public int getViewHorizontalDragRange(View child) {
+ return mDragDistance;
+ }
+
+ @Override
+ public int getViewVerticalDragRange(View child) {
+ return mDragDistance;
+ }
+
+ boolean isCloseBeforeDrag = true;
+
+ @Override
+ public void onViewReleased(View releasedChild, float xvel, float yvel) {
+ super.onViewReleased(releasedChild, xvel, yvel);
+ processHandRelease(xvel, yvel, isCloseBeforeDrag);
+ for (SwipeListener l : mSwipeListeners) {
+ l.onHandRelease(SwipeLayout.this, xvel, yvel);
+ }
+
+ invalidate();
+ }
+
+ @Override
+ public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
+ View surfaceView = getSurfaceView();
+ if (surfaceView == null) {
+ return;
+ }
+ View currentBottomView = getCurrentBottomView();
+ int evLeft = surfaceView.getLeft();
+ int evRight = surfaceView.getRight();
+ int evTop = surfaceView.getTop();
+ int evBottom = surfaceView.getBottom();
+ if (changedView == surfaceView) {
+ if (mShowMode == ShowMode.PullOut && currentBottomView != null) {
+ if (mCurrentDragEdge == DragEdge.Left || mCurrentDragEdge == DragEdge.Right) {
+ currentBottomView.offsetLeftAndRight(dx);
+ } else {
+ currentBottomView.offsetTopAndBottom(dy);
+ }
+ }
+
+ } else if (getBottomViews().contains(changedView)) {
+ if (mShowMode == ShowMode.PullOut) {
+ surfaceView.offsetLeftAndRight(dx);
+ surfaceView.offsetTopAndBottom(dy);
+ } else {
+ Rect rect = computeBottomLayDown(mCurrentDragEdge);
+ if (currentBottomView != null) {
+ currentBottomView.layout(rect.left, rect.top, rect.right, rect.bottom);
+ }
+
+ int newLeft = surfaceView.getLeft() + dx;
+ int newTop = surfaceView.getTop() + dy;
+
+ if (mCurrentDragEdge == DragEdge.Left && newLeft < getPaddingLeft()) {
+ newLeft = getPaddingLeft();
+ } else if (mCurrentDragEdge == DragEdge.Right && newLeft > getPaddingLeft()) {
+ newLeft = getPaddingLeft();
+ } else if (mCurrentDragEdge == DragEdge.Top && newTop < getPaddingTop()) {
+ newTop = getPaddingTop();
+ } else if (mCurrentDragEdge == DragEdge.Bottom && newTop > getPaddingTop()) {
+ newTop = getPaddingTop();
+ }
+
+ surfaceView.layout(newLeft, newTop, newLeft + getMeasuredWidth(), newTop + getMeasuredHeight());
+ }
+ }
+
+ dispatchRevealEvent(evLeft, evTop, evRight, evBottom);
+
+ dispatchSwipeEvent(evLeft, evTop, dx, dy);
+
+ invalidate();
+
+ captureChildrenBound();
+ }
+ };
+
+ private int handleClampVertical(View child, int top, int dy) {
+ if (child == getSurfaceView()) {
+ switch (mCurrentDragEdge) {
+ case Left:
+ return getPaddingTop();
+ case Right:
+ return getPaddingTop();
+ case Top:
+ if (top < getPaddingTop()) {
+ return getPaddingTop();
+ }
+ if (top > getPaddingTop() + mDragDistance) {
+ return getPaddingTop() + mDragDistance;
+ }
+ break;
+ case Bottom:
+ if (top < getPaddingTop() - mDragDistance) {
+ return getPaddingTop() - mDragDistance;
+ }
+ if (top > getPaddingTop()) {
+ return getPaddingTop();
+ }
+ break;
+ default:
+ break;
+ }
+ } else {
+ View surfaceView = getSurfaceView();
+ int surfaceViewTop = surfaceView == null ? 0 : surfaceView.getTop();
+ switch (mCurrentDragEdge) {
+ case Left:
+ return getPaddingTop();
+ case Right:
+ return getPaddingTop();
+ case Top:
+ if (mShowMode == ShowMode.PullOut) {
+ if (top > getPaddingTop()) {
+ return getPaddingTop();
+ }
+ } else {
+ if (surfaceViewTop + dy < getPaddingTop()) {
+ return getPaddingTop();
+ }
+ if (surfaceViewTop + dy > getPaddingTop() + mDragDistance) {
+ return getPaddingTop() + mDragDistance;
+ }
+ }
+ break;
+ case Bottom:
+ if (mShowMode == ShowMode.PullOut) {
+ if (top < getMeasuredHeight() - mDragDistance) {
+ return getMeasuredHeight() - mDragDistance;
+ }
+ } else {
+ if (surfaceViewTop + dy >= getPaddingTop()) {
+ return getPaddingTop();
+ }
+ if (surfaceViewTop + dy <= getPaddingTop() - mDragDistance) {
+ return getPaddingTop() - mDragDistance;
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ return top;
+ }
+
+ private int handleClampHorizontal(View child, int left) {
+ if (child == getSurfaceView()) {
+ switch (mCurrentDragEdge) {
+ case Top:
+ return getPaddingLeft();
+ case Bottom:
+ return getPaddingLeft();
+ case Left:
+ if (left < getPaddingLeft()) {
+ return getPaddingLeft();
+ }
+ if (left > getPaddingLeft() + mDragDistance) {
+ return getPaddingLeft() + mDragDistance;
+ }
+ break;
+ case Right:
+ if (left > getPaddingLeft()) {
+ return getPaddingLeft();
+ }
+ if (left < getPaddingLeft() - mDragDistance) {
+ return getPaddingLeft() - mDragDistance;
+ }
+ break;
+ default:
+ break;
+ }
+ } else if (getCurrentBottomView() == child) {
+ switch (mCurrentDragEdge) {
+ case Top:
+ return getPaddingLeft();
+ case Bottom:
+ return getPaddingLeft();
+ case Left:
+ if (mShowMode == ShowMode.PullOut) {
+ if (left > getPaddingLeft()) {
+ return getPaddingLeft();
+ }
+ }
+ break;
+ case Right:
+ if (mShowMode == ShowMode.PullOut) {
+ if (left < getMeasuredWidth() - mDragDistance) {
+ return getMeasuredWidth() - mDragDistance;
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ return left;
+ }
+
+ private void captureChildrenBound() {
+ View currentBottomView = getCurrentBottomView();
+ if (getOpenStatus() == Status.Close) {
+ mViewBoundCache.remove(currentBottomView);
+ return;
+ }
+
+ View[] views = new View[] {getSurfaceView(), currentBottomView};
+ for (View child : views) {
+ Rect rect = mViewBoundCache.get(child);
+ if (rect == null) {
+ rect = new Rect();
+ mViewBoundCache.put(child, rect);
+ }
+ rect.left = child.getLeft();
+ rect.top = child.getTop();
+ rect.right = child.getRight();
+ rect.bottom = child.getBottom();
+ }
+ }
+
+ /**
+ * the dispatchRevealEvent method may not always get accurate position, it
+ * makes the view may not always get the event when the view is totally
+ * show( fraction = 1), so , we need to calculate every time.
+ */
+ protected boolean isViewTotallyFirstShowed(
+ View child, Rect relativePosition, DragEdge edge, int surfaceLeft, int surfaceTop, int surfaceRight, int surfaceBottom) {
+ if (mShowEntirely.get(child)) {
+ return false;
+ }
+ int childLeft = relativePosition.left;
+ int childRight = relativePosition.right;
+ int childTop = relativePosition.top;
+ int childBottom = relativePosition.bottom;
+ boolean r = false;
+ if (getShowMode() == ShowMode.LayDown) {
+ if ((edge == DragEdge.Right && surfaceRight <= childLeft) || (edge == DragEdge.Left && surfaceLeft >= childRight)
+ || (edge == DragEdge.Top && surfaceTop >= childBottom) || (edge == DragEdge.Bottom && surfaceBottom <= childTop)) {
+ r = true;
+ }
+ } else if (getShowMode() == ShowMode.PullOut) {
+ if ((edge == DragEdge.Right && childRight <= getWidth()) || (edge == DragEdge.Left && childLeft >= getPaddingLeft())
+ || (edge == DragEdge.Top && childTop >= getPaddingTop()) || (edge == DragEdge.Bottom && childBottom <= getHeight())) {
+ r = true;
+ }
+ }
+ return r;
+ }
+
+ protected boolean isViewShowing(
+ View child, Rect relativePosition, DragEdge availableEdge, int surfaceLeft, int surfaceTop, int surfaceRight, int surfaceBottom) {
+ int childLeft = relativePosition.left;
+ int childRight = relativePosition.right;
+ int childTop = relativePosition.top;
+ int childBottom = relativePosition.bottom;
+ if (getShowMode() == ShowMode.LayDown) {
+ switch (availableEdge) {
+ case Right:
+ if (surfaceRight > childLeft && surfaceRight <= childRight) {
+ return true;
+ }
+ break;
+ case Left:
+ if (surfaceLeft < childRight && surfaceLeft >= childLeft) {
+ return true;
+ }
+ break;
+ case Top:
+ if (surfaceTop >= childTop && surfaceTop < childBottom) {
+ return true;
+ }
+ break;
+ case Bottom:
+ if (surfaceBottom > childTop && surfaceBottom <= childBottom) {
+ return true;
+ }
+ break;
+ default:
+ break;
+ }
+ } else if (getShowMode() == ShowMode.PullOut) {
+ switch (availableEdge) {
+ case Right:
+ if (childLeft <= getWidth() && childRight > getWidth()) {
+ return true;
+ }
+ break;
+ case Left:
+ if (childRight >= getPaddingLeft() && childLeft < getPaddingLeft()) {
+ return true;
+ }
+ break;
+ case Top:
+ if (childTop < getPaddingTop() && childBottom >= getPaddingTop()) {
+ return true;
+ }
+ break;
+ case Bottom:
+ if (childTop < getHeight() && childTop >= getPaddingTop()) {
+ return true;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ return false;
+ }
+
+ protected Rect getRelativePosition(View child) {
+ View t = child;
+ Rect r = new Rect(t.getLeft(), t.getTop(), 0, 0);
+ while (t.getParent() != null && t != getRootView()) {
+ t = (View) t.getParent();
+ if (t == this) {
+ break;
+ }
+ r.left += t.getLeft();
+ r.top += t.getTop();
+ }
+ r.right = r.left + child.getMeasuredWidth();
+ r.bottom = r.top + child.getMeasuredHeight();
+ return r;
+ }
+
+ private int mEventCounter = 0;
+
+ protected void dispatchSwipeEvent(int surfaceLeft, int surfaceTop, int dx, int dy) {
+ DragEdge edge = getDragEdge();
+ boolean open = true;
+ if (edge == DragEdge.Left) {
+ if (dx < 0) {
+ open = false;
+ }
+ } else if (edge == DragEdge.Right) {
+ if (dx > 0) {
+ open = false;
+ }
+ } else if (edge == DragEdge.Top) {
+ if (dy < 0) {
+ open = false;
+ }
+ } else if (edge == DragEdge.Bottom) {
+ if (dy > 0) {
+ open = false;
+ }
+ }
+
+ dispatchSwipeEvent(surfaceLeft, surfaceTop, open);
+ }
+
+ protected void dispatchSwipeEvent(int surfaceLeft, int surfaceTop, boolean open) {
+ safeBottomView();
+ Status status = getOpenStatus();
+
+ if (!mSwipeListeners.isEmpty()) {
+ mEventCounter++;
+ for (SwipeListener l : mSwipeListeners) {
+ if (mEventCounter == 1) {
+ if (open) {
+ l.onStartOpen(this);
+ } else {
+ l.onStartClose(this);
+ }
+ }
+ l.onUpdate(SwipeLayout.this, surfaceLeft - getPaddingLeft(), surfaceTop - getPaddingTop());
+ }
+
+ if (status == Status.Close) {
+ for (SwipeListener l : mSwipeListeners) {
+ l.onClose(SwipeLayout.this);
+ }
+ mEventCounter = 0;
+ mClickToClose = false;
+ }
+
+ if (status == Status.Open) {
+ View currentBottomView = getCurrentBottomView();
+ if (currentBottomView != null) {
+ currentBottomView.setEnabled(true);
+ }
+ for (SwipeListener l : mSwipeListeners) {
+ l.onOpen(SwipeLayout.this);
+ }
+ mEventCounter = 0;
+ mClickToClose = true;
+ }
+ }
+ }
+
+ /**
+ * prevent bottom view get any touch event. Especially in LayDown mode.
+ */
+ private void safeBottomView() {
+ Status status = getOpenStatus();
+ List bottoms = getBottomViews();
+
+ if (status == Status.Close) {
+ for (View bottom : bottoms) {
+ if (bottom != null && bottom.getVisibility() != INVISIBLE) {
+ bottom.setVisibility(INVISIBLE);
+ }
+ }
+ } else {
+ View currentBottomView = getCurrentBottomView();
+ if (currentBottomView != null && currentBottomView.getVisibility() != VISIBLE) {
+ currentBottomView.setVisibility(VISIBLE);
+ }
+ }
+ }
+
+ protected void dispatchRevealEvent(final int surfaceLeft, final int surfaceTop, final int surfaceRight, final int surfaceBottom) {
+ if (mRevealListeners.isEmpty()) {
+ return;
+ }
+ for (Map.Entry> entry : mRevealListeners.entrySet()) {
+ View child = entry.getKey();
+ Rect rect = getRelativePosition(child);
+ if (isViewShowing(child, rect, mCurrentDragEdge, surfaceLeft, surfaceTop, surfaceRight, surfaceBottom)) {
+ mShowEntirely.put(child, false);
+ int distance = 0;
+ float fraction = 0f;
+ if (getShowMode() == ShowMode.LayDown) {
+ switch (mCurrentDragEdge) {
+ case Left:
+ distance = rect.left - surfaceLeft;
+ fraction = distance / (float) child.getWidth();
+ break;
+ case Right:
+ distance = rect.right - surfaceRight;
+ fraction = distance / (float) child.getWidth();
+ break;
+ case Top:
+ distance = rect.top - surfaceTop;
+ fraction = distance / (float) child.getHeight();
+ break;
+ case Bottom:
+ distance = rect.bottom - surfaceBottom;
+ fraction = distance / (float) child.getHeight();
+ break;
+ default:
+ break;
+ }
+ } else if (getShowMode() == ShowMode.PullOut) {
+ switch (mCurrentDragEdge) {
+ case Left:
+ distance = rect.right - getPaddingLeft();
+ fraction = distance / (float) child.getWidth();
+ break;
+ case Right:
+ distance = rect.left - getWidth();
+ fraction = distance / (float) child.getWidth();
+ break;
+ case Top:
+ distance = rect.bottom - getPaddingTop();
+ fraction = distance / (float) child.getHeight();
+ break;
+ case Bottom:
+ distance = rect.top - getHeight();
+ fraction = distance / (float) child.getHeight();
+ break;
+ default:
+ break;
+ }
+ }
+
+ for (OnRevealListener l : entry.getValue()) {
+ l.onReveal(child, mCurrentDragEdge, Math.abs(fraction), distance);
+ if (Math.abs(fraction) == 1) {
+ mShowEntirely.put(child, true);
+ }
+ }
+ }
+
+ if (isViewTotallyFirstShowed(child, rect, mCurrentDragEdge, surfaceLeft, surfaceTop, surfaceRight, surfaceBottom)) {
+ mShowEntirely.put(child, true);
+ for (OnRevealListener l : entry.getValue()) {
+ if (mCurrentDragEdge == DragEdge.Left || mCurrentDragEdge == DragEdge.Right) {
+ l.onReveal(child, mCurrentDragEdge, 1, child.getWidth());
+ } else {
+ l.onReveal(child, mCurrentDragEdge, 1, child.getHeight());
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void computeScroll() {
+ super.computeScroll();
+ if (mDragHelper.continueSettling(true)) {
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ }
+
+ /**
+ * {@link android.view.View.OnLayoutChangeListener} added in API 11. I need
+ * to support it from API 8.
+ */
+ public interface OnLayout {
+ void onLayout(SwipeLayout v);
+ }
+
+ private List mOnLayoutListeners;
+
+ public void addOnLayoutListener(OnLayout l) {
+ if (mOnLayoutListeners == null) {
+ mOnLayoutListeners = new ArrayList();
+ }
+ mOnLayoutListeners.add(l);
+ }
+
+ public void removeOnLayoutListener(OnLayout l) {
+ if (mOnLayoutListeners != null) {
+ mOnLayoutListeners.remove(l);
+ }
+ }
+
+ public void clearDragEdge() {
+ mDragEdges.clear();
+ }
+
+ public void setDrag(DragEdge dragEdge, int childId) {
+ clearDragEdge();
+ addDrag(dragEdge, childId);
+ }
+
+ public void setDrag(DragEdge dragEdge, View child) {
+ clearDragEdge();
+ addDrag(dragEdge, child);
+ }
+
+ public void addDrag(DragEdge dragEdge, int childId) {
+ addDrag(dragEdge, findViewById(childId), null);
+ }
+
+ public void addDrag(DragEdge dragEdge, View child) {
+ addDrag(dragEdge, child, null);
+ }
+
+ public void addDrag(DragEdge dragEdge, View child, ViewGroup.LayoutParams params) {
+ if (child == null) {
+ return;
+ }
+
+ if (params == null) {
+ params = generateDefaultLayoutParams();
+ }
+ if (!checkLayoutParams(params)) {
+ params = generateLayoutParams(params);
+ }
+ int gravity = -1;
+ switch (dragEdge) {
+ case Left:
+ gravity = Gravity.LEFT;
+ break;
+ case Right:
+ gravity = Gravity.RIGHT;
+ break;
+ case Top:
+ gravity = Gravity.TOP;
+ break;
+ case Bottom:
+ gravity = Gravity.BOTTOM;
+ break;
+ default:
+ break;
+ }
+ if (params instanceof LayoutParams) {
+ ((LayoutParams) params).gravity = gravity;
+ }
+ addView(child, 0, params);
+ }
+
+ @Override
+ public void addView(View child, int index, ViewGroup.LayoutParams params) {
+ if (child == null) {
+ return;
+ }
+ int gravity = Gravity.NO_GRAVITY;
+ try {
+ gravity = (Integer) params.getClass().getField("gravity").get(params);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ if (gravity > 0) {
+ gravity = GravityCompat.getAbsoluteGravity(gravity, ViewCompat.getLayoutDirection(this));
+
+ if ((gravity & Gravity.LEFT) == Gravity.LEFT) {
+ mDragEdges.put(DragEdge.Left, child);
+ }
+ if ((gravity & Gravity.RIGHT) == Gravity.RIGHT) {
+ mDragEdges.put(DragEdge.Right, child);
+ }
+ if ((gravity & Gravity.TOP) == Gravity.TOP) {
+ mDragEdges.put(DragEdge.Top, child);
+ }
+ if ((gravity & Gravity.BOTTOM) == Gravity.BOTTOM) {
+ mDragEdges.put(DragEdge.Bottom, child);
+ }
+ } else {
+ for (Map.Entry entry : mDragEdges.entrySet()) {
+ if (entry.getValue() == null) {
+ // means used the drag_edge attr, the no gravity child should be use set
+ mDragEdges.put(entry.getKey(), child);
+ break;
+ }
+ }
+ }
+ if (child.getParent() == this) {
+ return;
+ }
+ super.addView(child, index, params);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+
+ updateBottomViews();
+
+ if (mOnLayoutListeners != null) {
+ for (int i = 0; i < mOnLayoutListeners.size(); i++) {
+ mOnLayoutListeners.get(i).onLayout(this);
+ }
+ }
+ }
+
+ void layoutPullOut() {
+ View surfaceView = getSurfaceView();
+ Rect surfaceRect = mViewBoundCache.get(surfaceView);
+ if (surfaceRect == null) {
+ surfaceRect = computeSurfaceLayoutArea(false);
+ }
+ if (surfaceView != null) {
+ surfaceView.layout(surfaceRect.left, surfaceRect.top, surfaceRect.right, surfaceRect.bottom);
+ bringChildToFront(surfaceView);
+ }
+ View currentBottomView = getCurrentBottomView();
+ Rect bottomViewRect = mViewBoundCache.get(currentBottomView);
+ if (bottomViewRect == null) {
+ bottomViewRect = computeBottomLayoutAreaViaSurface(ShowMode.PullOut, surfaceRect);
+ }
+ if (currentBottomView != null) {
+ currentBottomView.layout(bottomViewRect.left, bottomViewRect.top, bottomViewRect.right, bottomViewRect.bottom);
+ }
+ }
+
+ void layoutLayDown() {
+ View surfaceView = getSurfaceView();
+ Rect surfaceRect = mViewBoundCache.get(surfaceView);
+ if (surfaceRect == null) {
+ surfaceRect = computeSurfaceLayoutArea(false);
+ }
+ if (surfaceView != null) {
+ surfaceView.layout(surfaceRect.left, surfaceRect.top, surfaceRect.right, surfaceRect.bottom);
+ bringChildToFront(surfaceView);
+ }
+ View currentBottomView = getCurrentBottomView();
+ Rect bottomViewRect = mViewBoundCache.get(currentBottomView);
+ if (bottomViewRect == null) {
+ bottomViewRect = computeBottomLayoutAreaViaSurface(ShowMode.LayDown, surfaceRect);
+ }
+ if (currentBottomView != null) {
+ currentBottomView.layout(bottomViewRect.left, bottomViewRect.top, bottomViewRect.right, bottomViewRect.bottom);
+ }
+ }
+
+ private boolean mIsBeingDragged;
+
+ private void checkCanDrag(MotionEvent ev) {
+ if (mIsBeingDragged) {
+ return;
+ }
+ if (getOpenStatus() == Status.Middle) {
+ mIsBeingDragged = true;
+ return;
+ }
+ Status status = getOpenStatus();
+ float distanceX = ev.getRawX() - sX;
+ float distanceY = ev.getRawY() - sY;
+ float angle = Math.abs(distanceY / distanceX);
+ angle = (float) Math.toDegrees(Math.atan(angle));
+ if (getOpenStatus() == Status.Close) {
+ DragEdge dragEdge;
+ if (angle < 45) {
+ if (distanceX > 0 && isLeftSwipeEnabled()) {
+ dragEdge = DragEdge.Left;
+ } else if (distanceX < 0 && isRightSwipeEnabled()) {
+ dragEdge = DragEdge.Right;
+ } else {
+ return;
+ }
+
+ } else {
+ if (distanceY > 0 && isTopSwipeEnabled()) {
+ dragEdge = DragEdge.Top;
+ } else if (distanceY < 0 && isBottomSwipeEnabled()) {
+ dragEdge = DragEdge.Bottom;
+ } else {
+ return;
+ }
+ }
+ setCurrentDragEdge(dragEdge);
+ }
+
+ boolean doNothing = isDoNothing(status, distanceX, distanceY, angle);
+ mIsBeingDragged = !doNothing;
+ }
+
+ private boolean isDoNothing(Status status, float distanceX, float distanceY, float angle) {
+ boolean doNothing = false;
+ if (mCurrentDragEdge == DragEdge.Right) {
+ boolean suitable = (status == Status.Open && distanceX > mTouchSlop) || (status == Status.Close && distanceX < -mTouchSlop);
+ suitable = suitable || (status == Status.Middle);
+
+ if (angle > 30 || !suitable) {
+ doNothing = true;
+ }
+ }
+
+ if (mCurrentDragEdge == DragEdge.Left) {
+ boolean suitable = (status == Status.Open && distanceX < -mTouchSlop) || (status == Status.Close && distanceX > mTouchSlop);
+ suitable = suitable || status == Status.Middle;
+
+ if (angle > 30 || !suitable) {
+ doNothing = true;
+ }
+ }
+
+ if (mCurrentDragEdge == DragEdge.Top) {
+ boolean suitable = (status == Status.Open && distanceY < -mTouchSlop) || (status == Status.Close && distanceY > mTouchSlop);
+ suitable = suitable || status == Status.Middle;
+
+ if (angle < 60 || !suitable) {
+ doNothing = true;
+ }
+ }
+
+ if (mCurrentDragEdge == DragEdge.Bottom) {
+ boolean suitable = (status == Status.Open && distanceY > mTouchSlop) || (status == Status.Close && distanceY < -mTouchSlop);
+ suitable = suitable || status == Status.Middle;
+
+ if (angle < 60 || !suitable) {
+ doNothing = true;
+ }
+ }
+ return doNothing;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (!isSwipeEnabled()) {
+ return false;
+ }
+ if (mClickToClose && getOpenStatus() == Status.Open && isTouchOnSurface(ev)) {
+ return true;
+ }
+ for (SwipeDenier denier : mSwipeDeniers) {
+ if (denier != null && denier.shouldDenySwipe(ev)) {
+ return false;
+ }
+ }
+
+ switch (ev.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mDragHelper.processTouchEvent(ev);
+ mIsBeingDragged = false;
+ sX = ev.getRawX();
+ sY = ev.getRawY();
+ // if the swipe is in middle state(scrolling), should intercept the touch
+ if (getOpenStatus() == Status.Middle) {
+ mIsBeingDragged = true;
+ }
+ break;
+ case MotionEvent.ACTION_MOVE:
+ boolean beforeCheck = mIsBeingDragged;
+ checkCanDrag(ev);
+ if (mIsBeingDragged) {
+ ViewParent parent = getParent();
+ if (parent != null) {
+ parent.requestDisallowInterceptTouchEvent(true);
+ }
+ }
+ if (!beforeCheck && mIsBeingDragged) {
+ // let children has one chance to catch the touch, and request the swipe not intercept
+ // useful when swipeLayout wrap a swipeLayout or other gestural layout
+ return false;
+ }
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ mIsBeingDragged = false;
+ mDragHelper.processTouchEvent(ev);
+ break;
+ case MotionEvent.ACTION_UP:
+ mIsBeingDragged = false;
+ mDragHelper.processTouchEvent(ev);
+ break;
+ default: // handle other action, such as ACTION_POINTER_DOWN/UP
+ mDragHelper.processTouchEvent(ev);
+ }
+ return mIsBeingDragged;
+ }
+
+ private float sX = -1;
+ private float sY = -1;
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (!isSwipeEnabled()) {
+ return super.onTouchEvent(event);
+ }
+
+ int action = event.getActionMasked();
+ gestureDetector.onTouchEvent(event);
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ mDragHelper.processTouchEvent(event);
+ sX = event.getRawX();
+ sY = event.getRawY();
+ checkCanDrag(event);
+ if (mIsBeingDragged) {
+ getParent().requestDisallowInterceptTouchEvent(true);
+ mDragHelper.processTouchEvent(event);
+ }
+ break;
+ case MotionEvent.ACTION_MOVE: {
+ // the drag state and the direction are already judged at onInterceptTouchEvent
+ checkCanDrag(event);
+ if (mIsBeingDragged) {
+ getParent().requestDisallowInterceptTouchEvent(true);
+ mDragHelper.processTouchEvent(event);
+ }
+ break;
+ }
+ case MotionEvent.ACTION_UP:
+ mIsBeingDragged = false;
+ mDragHelper.processTouchEvent(event);
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ mIsBeingDragged = false;
+ mDragHelper.processTouchEvent(event);
+ break;
+
+ default: // handle other action, such as ACTION_POINTER_DOWN/UP
+ mDragHelper.processTouchEvent(event);
+ }
+
+ return super.onTouchEvent(event) || mIsBeingDragged || action == MotionEvent.ACTION_DOWN;
+ }
+
+ public boolean isClickToClose() {
+ return mClickToClose;
+ }
+
+ public void setClickToClose(boolean mClickToClose) {
+ this.mClickToClose = mClickToClose;
+ }
+
+ public void setSwipeEnabled(boolean enabled) {
+ mSwipeEnabled = enabled;
+ }
+
+ public boolean isSwipeEnabled() {
+ return mSwipeEnabled;
+ }
+
+ public boolean isLeftSwipeEnabled() {
+ View bottomView = mDragEdges.get(DragEdge.Left);
+ return bottomView != null && bottomView.getParent() == this && bottomView != getSurfaceView() && mSwipesEnabled[DragEdge.Left.ordinal()];
+ }
+
+ public void setLeftSwipeEnabled(boolean leftSwipeEnabled) {
+ this.mSwipesEnabled[DragEdge.Left.ordinal()] = leftSwipeEnabled;
+ }
+
+ public boolean isRightSwipeEnabled() {
+ View bottomView = mDragEdges.get(DragEdge.Right);
+ return bottomView != null && bottomView.getParent() == this && bottomView != getSurfaceView() && mSwipesEnabled[DragEdge.Right.ordinal()];
+ }
+
+ public void setRightSwipeEnabled(boolean rightSwipeEnabled) {
+ this.mSwipesEnabled[DragEdge.Right.ordinal()] = rightSwipeEnabled;
+ }
+
+ public boolean isTopSwipeEnabled() {
+ View bottomView = mDragEdges.get(DragEdge.Top);
+ return bottomView != null && bottomView.getParent() == this && bottomView != getSurfaceView() && mSwipesEnabled[DragEdge.Top.ordinal()];
+ }
+
+ public void setTopSwipeEnabled(boolean topSwipeEnabled) {
+ this.mSwipesEnabled[DragEdge.Top.ordinal()] = topSwipeEnabled;
+ }
+
+ public boolean isBottomSwipeEnabled() {
+ View bottomView = mDragEdges.get(DragEdge.Bottom);
+ return bottomView != null && bottomView.getParent() == this && bottomView != getSurfaceView() && mSwipesEnabled[DragEdge.Bottom.ordinal()];
+ }
+
+ public void setBottomSwipeEnabled(boolean bottomSwipeEnabled) {
+ this.mSwipesEnabled[DragEdge.Bottom.ordinal()] = bottomSwipeEnabled;
+ }
+
+ /***
+ * Returns the percentage of revealing at which the view below should the view finish opening
+ * if it was already open before dragging
+ *
+ * @returns The percentage of view revealed to trigger, default value is 0.25
+ */
+ public float getWillOpenPercentAfterOpen() {
+ return mWillOpenPercentAfterOpen;
+ }
+
+ /***
+ * Allows to stablish at what percentage of revealing the view below should the view finish opening
+ * if it was already open before dragging
+ *
+ * @param willOpenPercentAfterOpen The percentage of view revealed to trigger, default value is 0.25
+ */
+ public void setWillOpenPercentAfterOpen(float willOpenPercentAfterOpen) {
+ this.mWillOpenPercentAfterOpen = willOpenPercentAfterOpen;
+ }
+
+ /***
+ * Returns the percentage of revealing at which the view below should the view finish opening
+ * if it was already closed before dragging
+ *
+ * @returns The percentage of view revealed to trigger, default value is 0.25
+ */
+ public float getWillOpenPercentAfterClose() {
+ return mWillOpenPercentAfterClose;
+ }
+
+ /***
+ * Allows to stablish at what percentage of revealing the view below should the view finish opening
+ * if it was already closed before dragging
+ *
+ * @param willOpenPercentAfterClose The percentage of view revealed to trigger, default value is 0.75
+ */
+ public void setWillOpenPercentAfterClose(float willOpenPercentAfterClose) {
+ this.mWillOpenPercentAfterClose = willOpenPercentAfterClose;
+ }
+
+ private boolean insideAdapterView() {
+ return getAdapterView() != null;
+ }
+
+ private AdapterView getAdapterView() {
+ ViewParent t = getParent();
+ if (t instanceof AdapterView) {
+ return (AdapterView) t;
+ }
+ return null;
+ }
+
+ public void performAdapterViewItemClick() {
+ if (getOpenStatus() != Status.Close) {
+ return;
+ }
+ ViewParent t = getParent();
+ if (t instanceof AdapterView) {
+ AdapterView view = (AdapterView) t;
+ int p = view.getPositionForView(SwipeLayout.this);
+ if (p != AdapterView.INVALID_POSITION) {
+ view.performItemClick(view.getChildAt(p - view.getFirstVisiblePosition()), p, view.getAdapter().getItemId(p));
+ }
+ }
+ }
+
+ private boolean performAdapterViewItemLongClick() {
+ if (getOpenStatus() != Status.Close) {
+ return false;
+ }
+ ViewParent t = getParent();
+ if (t instanceof AdapterView) {
+ AdapterView view = (AdapterView) t;
+ int p = view.getPositionForView(SwipeLayout.this);
+ if (p == AdapterView.INVALID_POSITION) {
+ return false;
+ }
+ long vId = view.getItemIdAtPosition(p);
+ boolean handled = false;
+ try {
+ Method m = AbsListView.class.getDeclaredMethod("performLongPress", View.class, int.class, long.class);
+ m.setAccessible(true);
+ handled = (boolean) m.invoke(view, SwipeLayout.this, p, vId);
+
+ } catch (Exception e) {
+ e.printStackTrace();
+
+ if (view.getOnItemLongClickListener() != null) {
+ handled = view.getOnItemLongClickListener().onItemLongClick(view, SwipeLayout.this, p, vId);
+ }
+ if (handled) {
+ view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ }
+ }
+ return handled;
+ }
+ return false;
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if (insideAdapterView()) {
+ if (clickListener == null) {
+ setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ performAdapterViewItemClick();
+ }
+ });
+ }
+ if (longClickListener == null) {
+ setOnLongClickListener(new OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View v) {
+ performAdapterViewItemLongClick();
+ return true;
+ }
+ });
+ }
+ }
+ }
+
+ OnClickListener clickListener;
+
+ @Override
+ public void setOnClickListener(OnClickListener l) {
+ super.setOnClickListener(l);
+ clickListener = l;
+ }
+
+ OnLongClickListener longClickListener;
+
+ @Override
+ public void setOnLongClickListener(OnLongClickListener l) {
+ super.setOnLongClickListener(l);
+ longClickListener = l;
+ }
+
+ private Rect hitSurfaceRect;
+
+ private boolean isTouchOnSurface(MotionEvent ev) {
+ View surfaceView = getSurfaceView();
+ if (surfaceView == null) {
+ return false;
+ }
+ if (hitSurfaceRect == null) {
+ hitSurfaceRect = new Rect();
+ }
+ surfaceView.getHitRect(hitSurfaceRect);
+ return hitSurfaceRect.contains((int) ev.getX(), (int) ev.getY());
+ }
+
+ private GestureDetector gestureDetector = new GestureDetector(getContext(), new SwipeDetector());
+
+ class SwipeDetector extends GestureDetector.SimpleOnGestureListener {
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ if (isTouchOnSurface(e)) {
+ if (mClickToClose && getOpenStatus() != Status.Close) {
+ close();
+ } else {
+ if (mDoubleClickListener != null) {
+ mDoubleClickListener.onClick();
+ }
+ }
+ }
+ return super.onSingleTapUp(e);
+ }
+
+ @Override
+ public boolean onDoubleTap(MotionEvent e) {
+ if (mDoubleClickListener != null) {
+ View target;
+ View bottom = getCurrentBottomView();
+ View surface = getSurfaceView();
+ if (bottom != null && e.getX() > bottom.getLeft() && e.getX() < bottom.getRight() && e.getY() > bottom.getTop()
+ && e.getY() < bottom.getBottom()) {
+ target = bottom;
+ } else {
+ target = surface;
+ }
+ mDoubleClickListener.onDoubleClick(SwipeLayout.this, target == surface);
+ }
+ return true;
+ }
+ }
+
+ /**
+ * set the drag distance, it will force set the bottom view's width or
+ * height via this value.
+ *
+ * @param max max distance in dp unit
+ */
+ public void setDragDistance(int max) {
+ if (max < 0) {
+ max = 0;
+ }
+ mDragDistance = dp2px(max);
+ requestLayout();
+ }
+
+ /**
+ * There are 2 diffirent show mode.
+ * {@link com.tencent.qcloud.tuikit.timcommon.component.swipe.SwipeLayout.ShowMode}.PullOut and
+ * {@link com.tencent.qcloud.tuikit.timcommon.component.swipe.SwipeLayout.ShowMode}.LayDown.
+ *
+ * @param mode
+ */
+ public void setShowMode(ShowMode mode) {
+ mShowMode = mode;
+ requestLayout();
+ }
+
+ public DragEdge getDragEdge() {
+ return mCurrentDragEdge;
+ }
+
+ public int getDragDistance() {
+ return mDragDistance;
+ }
+
+ public ShowMode getShowMode() {
+ return mShowMode;
+ }
+
+ /**
+ * return null if there is no surface view(no children)
+ */
+ public View getSurfaceView() {
+ if (getChildCount() == 0) {
+ return null;
+ }
+ return getChildAt(getChildCount() - 1);
+ }
+
+ /**
+ * return null if there is no bottom view
+ */
+ public View getCurrentBottomView() {
+ List bottoms = getBottomViews();
+ if (mCurrentDragEdge.ordinal() < bottoms.size()) {
+ return bottoms.get(mCurrentDragEdge.ordinal());
+ }
+ return null;
+ }
+
+ /**
+ * @return all bottomViews: left, top, right, bottom (may null if the edge is not set)
+ */
+ public List getBottomViews() {
+ ArrayList bottoms = new ArrayList();
+ for (DragEdge dragEdge : DragEdge.values()) {
+ bottoms.add(mDragEdges.get(dragEdge));
+ }
+ return bottoms;
+ }
+
+ public enum Status { Middle, Open, Close }
+
+ /**
+ * get the open status.
+ *
+ * @return {@link com.tencent.qcloud.tuikit.timcommon.component.swipe.SwipeLayout.Status} Open , Close or
+ * Middle.
+ */
+ public Status getOpenStatus() {
+ View surfaceView = getSurfaceView();
+ if (surfaceView == null) {
+ return Status.Close;
+ }
+ int surfaceLeft = surfaceView.getLeft();
+ int surfaceTop = surfaceView.getTop();
+ if (surfaceLeft == getPaddingLeft() && surfaceTop == getPaddingTop()) {
+ return Status.Close;
+ }
+
+ if (surfaceLeft == (getPaddingLeft() - mDragDistance) || surfaceLeft == (getPaddingLeft() + mDragDistance)
+ || surfaceTop == (getPaddingTop() - mDragDistance) || surfaceTop == (getPaddingTop() + mDragDistance)) {
+ return Status.Open;
+ }
+
+ return Status.Middle;
+ }
+
+ /**
+ * Process the surface release event.
+ *
+ * @param xvel xVelocity
+ * @param yvel yVelocity
+ * @param isCloseBeforeDragged the open state before drag
+ */
+ protected void processHandRelease(float xvel, float yvel, boolean isCloseBeforeDragged) {
+ float minVelocity = mDragHelper.getMinVelocity();
+ View surfaceView = getSurfaceView();
+ DragEdge currentDragEdge = mCurrentDragEdge;
+ if (currentDragEdge == null || surfaceView == null) {
+ return;
+ }
+ float willOpenPercent = (isCloseBeforeDragged ? mWillOpenPercentAfterClose : mWillOpenPercentAfterOpen);
+ if (currentDragEdge == DragEdge.Left) {
+ if (xvel > minVelocity) {
+ open();
+ } else if (xvel < -minVelocity) {
+ close();
+ } else {
+ float openPercent = 1f * getSurfaceView().getLeft() / mDragDistance;
+ if (openPercent > willOpenPercent) {
+ open();
+ } else {
+ close();
+ }
+ }
+ } else if (currentDragEdge == DragEdge.Right) {
+ if (xvel > minVelocity) {
+ close();
+ } else if (xvel < -minVelocity) {
+ open();
+ } else {
+ float openPercent = 1f * (-getSurfaceView().getLeft()) / mDragDistance;
+ if (openPercent > willOpenPercent) {
+ open();
+ } else {
+ close();
+ }
+ }
+ } else if (currentDragEdge == DragEdge.Top) {
+ if (yvel > minVelocity) {
+ open();
+ } else if (yvel < -minVelocity) {
+ close();
+ } else {
+ float openPercent = 1f * getSurfaceView().getTop() / mDragDistance;
+ if (openPercent > willOpenPercent) {
+ open();
+ } else {
+ close();
+ }
+ }
+ } else if (currentDragEdge == DragEdge.Bottom) {
+ if (yvel > minVelocity) {
+ close();
+ } else if (yvel < -minVelocity) {
+ open();
+ } else {
+ float openPercent = 1f * (-getSurfaceView().getTop()) / mDragDistance;
+ if (openPercent > willOpenPercent) {
+ open();
+ } else {
+ close();
+ }
+ }
+ }
+ }
+
+ /**
+ * smoothly open surface.
+ */
+ public void open() {
+ open(true, true);
+ }
+
+ public void open(boolean smooth) {
+ open(smooth, true);
+ }
+
+ public void open(boolean smooth, boolean notify) {
+ View surface = getSurfaceView();
+ View bottom = getCurrentBottomView();
+ if (surface == null) {
+ return;
+ }
+ int dx;
+ int dy;
+ Rect rect = computeSurfaceLayoutArea(true);
+ if (smooth) {
+ mDragHelper.smoothSlideViewTo(surface, rect.left, rect.top);
+ } else {
+ dx = rect.left - surface.getLeft();
+ dy = rect.top - surface.getTop();
+ surface.layout(rect.left, rect.top, rect.right, rect.bottom);
+ if (getShowMode() == ShowMode.PullOut) {
+ Rect bRect = computeBottomLayoutAreaViaSurface(ShowMode.PullOut, rect);
+ if (bottom != null) {
+ bottom.layout(bRect.left, bRect.top, bRect.right, bRect.bottom);
+ }
+ }
+ if (notify) {
+ dispatchRevealEvent(rect.left, rect.top, rect.right, rect.bottom);
+ dispatchSwipeEvent(rect.left, rect.top, dx, dy);
+ } else {
+ safeBottomView();
+ }
+ }
+ invalidate();
+ }
+
+ public void open(DragEdge edge) {
+ setCurrentDragEdge(edge);
+ open(true, true);
+ }
+
+ public void open(boolean smooth, DragEdge edge) {
+ setCurrentDragEdge(edge);
+ open(smooth, true);
+ }
+
+ public void open(boolean smooth, boolean notify, DragEdge edge) {
+ setCurrentDragEdge(edge);
+ open(smooth, notify);
+ }
+
+ /**
+ * smoothly close surface.
+ */
+ public void close() {
+ close(true, true);
+ }
+
+ public void close(boolean smooth) {
+ close(smooth, true);
+ }
+
+ /**
+ * close surface
+ *
+ * @param smooth smoothly or not.
+ * @param notify if notify all the listeners.
+ */
+ public void close(boolean smooth, boolean notify) {
+ View surface = getSurfaceView();
+ if (surface == null) {
+ return;
+ }
+ int dx;
+ int dy;
+ if (smooth) {
+ mDragHelper.smoothSlideViewTo(getSurfaceView(), getPaddingLeft(), getPaddingTop());
+ } else {
+ Rect rect = computeSurfaceLayoutArea(false);
+ dx = rect.left - surface.getLeft();
+ dy = rect.top - surface.getTop();
+ surface.layout(rect.left, rect.top, rect.right, rect.bottom);
+ if (notify) {
+ dispatchRevealEvent(rect.left, rect.top, rect.right, rect.bottom);
+ dispatchSwipeEvent(rect.left, rect.top, dx, dy);
+ } else {
+ safeBottomView();
+ }
+ }
+ invalidate();
+ }
+
+ public void toggle() {
+ toggle(true);
+ }
+
+ public void toggle(boolean smooth) {
+ if (getOpenStatus() == Status.Open) {
+ close(smooth);
+ } else if (getOpenStatus() == Status.Close) {
+ open(smooth);
+ }
+ }
+
+ /**
+ * a helper function to compute the Rect area that surface will hold in.
+ *
+ * @param open open status or close status.
+ */
+ private Rect computeSurfaceLayoutArea(boolean open) {
+ int l = getPaddingLeft();
+ int t = getPaddingTop();
+ if (open) {
+ if (mCurrentDragEdge == DragEdge.Left) {
+ l = getPaddingLeft() + mDragDistance;
+ } else if (mCurrentDragEdge == DragEdge.Right) {
+ l = getPaddingLeft() - mDragDistance;
+ } else if (mCurrentDragEdge == DragEdge.Top) {
+ t = getPaddingTop() + mDragDistance;
+ } else {
+ t = getPaddingTop() - mDragDistance;
+ }
+ }
+ return new Rect(l, t, l + getMeasuredWidth(), t + getMeasuredHeight());
+ }
+
+ private Rect computeBottomLayoutAreaViaSurface(ShowMode mode, Rect surfaceArea) {
+ Rect rect = surfaceArea;
+ View bottomView = getCurrentBottomView();
+
+ int bl = rect.left;
+ int bt = rect.top;
+ int br = rect.right;
+ int bb = rect.bottom;
+ if (mode == ShowMode.PullOut) {
+ if (mCurrentDragEdge == DragEdge.Left) {
+ bl = rect.left - mDragDistance;
+ } else if (mCurrentDragEdge == DragEdge.Right) {
+ bl = rect.right;
+ } else if (mCurrentDragEdge == DragEdge.Top) {
+ bt = rect.top - mDragDistance;
+ } else {
+ bt = rect.bottom;
+ }
+
+ if (mCurrentDragEdge == DragEdge.Left || mCurrentDragEdge == DragEdge.Right) {
+ bb = rect.bottom;
+ br = bl + (bottomView == null ? 0 : bottomView.getMeasuredWidth());
+ } else {
+ bb = bt + (bottomView == null ? 0 : bottomView.getMeasuredHeight());
+ br = rect.right;
+ }
+ } else if (mode == ShowMode.LayDown) {
+ if (mCurrentDragEdge == DragEdge.Left) {
+ br = bl + mDragDistance;
+ } else if (mCurrentDragEdge == DragEdge.Right) {
+ bl = br - mDragDistance;
+ } else if (mCurrentDragEdge == DragEdge.Top) {
+ bb = bt + mDragDistance;
+ } else {
+ bt = bb - mDragDistance;
+ }
+ }
+ return new Rect(bl, bt, br, bb);
+ }
+
+ private Rect computeBottomLayDown(DragEdge dragEdge) {
+ int bl = getPaddingLeft();
+ int bt = getPaddingTop();
+ int br;
+ int bb;
+ if (dragEdge == DragEdge.Right) {
+ bl = getMeasuredWidth() - mDragDistance;
+ } else if (dragEdge == DragEdge.Bottom) {
+ bt = getMeasuredHeight() - mDragDistance;
+ }
+ if (dragEdge == DragEdge.Left || dragEdge == DragEdge.Right) {
+ br = bl + mDragDistance;
+ bb = bt + getMeasuredHeight();
+ } else {
+ br = bl + getMeasuredWidth();
+ bb = bt + mDragDistance;
+ }
+ return new Rect(bl, bt, br, bb);
+ }
+
+ public void setOnDoubleClickListener(DoubleClickListener doubleClickListener) {
+ mDoubleClickListener = doubleClickListener;
+ }
+
+ public interface DoubleClickListener {
+ void onDoubleClick(SwipeLayout layout, boolean surface);
+
+ void onClick();
+ }
+
+ private int dp2px(float dp) {
+ return (int) (dp * getContext().getResources().getDisplayMetrics().density + 0.5f);
+ }
+
+ /**
+ * Deprecated, use {@link #setDrag(com.tencent.qcloud.tuikit.timcommon.component.swipe.SwipeLayout.DragEdge, android.view.View)}
+ */
+ @Deprecated
+ public void setDragEdge(DragEdge dragEdge) {
+ clearDragEdge();
+ if (getChildCount() >= 2) {
+ mDragEdges.put(dragEdge, getChildAt(getChildCount() - 2));
+ }
+ setCurrentDragEdge(dragEdge);
+ }
+
+ public void onViewRemoved(View child) {
+ for (Map.Entry entry : new HashMap(mDragEdges).entrySet()) {
+ if (entry.getValue() == child) {
+ mDragEdges.remove(entry.getKey());
+ }
+ }
+ }
+
+ public Map getDragEdgeMap() {
+ return mDragEdges;
+ }
+
+ /**
+ * Deprecated, use {@link #getDragEdgeMap()}
+ */
+ @Deprecated
+ public List getDragEdges() {
+ return new ArrayList(mDragEdges.keySet());
+ }
+
+ /**
+ * Deprecated, use {@link #setDrag(com.tencent.qcloud.tuikit.timcommon.component.swipe.SwipeLayout.DragEdge, android.view.View)}
+ */
+ @Deprecated
+ public void setDragEdges(List dragEdges) {
+ clearDragEdge();
+ for (int i = 0, size = Math.min(dragEdges.size(), getChildCount() - 1); i < size; i++) {
+ DragEdge dragEdge = dragEdges.get(i);
+ mDragEdges.put(dragEdge, getChildAt(i));
+ }
+ if (dragEdges.size() == 0 || dragEdges.contains(DefaultDragEdge)) {
+ setCurrentDragEdge(DefaultDragEdge);
+ } else {
+ setCurrentDragEdge(dragEdges.get(0));
+ }
+ }
+
+ /**
+ * Deprecated, use {@link #addDrag(com.tencent.qcloud.tuikit.timcommon.component.swipe.SwipeLayout.DragEdge, android.view.View)}
+ */
+ @Deprecated
+ public void setDragEdges(DragEdge... mDragEdges) {
+ clearDragEdge();
+ setDragEdges(Arrays.asList(mDragEdges));
+ }
+
+ /**
+ * Deprecated, use {@link #addDrag(com.tencent.qcloud.tuikit.timcommon.component.swipe.SwipeLayout.DragEdge, android.view.View)}
+ * When using multiple drag edges it's a good idea to pass the ids of the views that
+ * you're using for the left, right, top bottom views (-1 if you're not using a particular view)
+ */
+ @Deprecated
+ public void setBottomViewIds(int leftId, int rightId, int topId, int bottomId) {
+ addDrag(DragEdge.Left, findViewById(leftId));
+ addDrag(DragEdge.Right, findViewById(rightId));
+ addDrag(DragEdge.Top, findViewById(topId));
+ addDrag(DragEdge.Bottom, findViewById(bottomId));
+ }
+
+ private float getCurrentOffset() {
+ if (mCurrentDragEdge == null) {
+ return 0;
+ }
+ return mEdgeSwipesOffset[mCurrentDragEdge.ordinal()];
+ }
+
+ private void setCurrentDragEdge(DragEdge dragEdge) {
+ mCurrentDragEdge = dragEdge;
+ updateBottomViews();
+ }
+
+ private void updateBottomViews() {
+ View currentBottomView = getCurrentBottomView();
+ if (currentBottomView != null) {
+ if (mCurrentDragEdge == DragEdge.Left || mCurrentDragEdge == DragEdge.Right) {
+ mDragDistance = currentBottomView.getMeasuredWidth() - dp2px(getCurrentOffset());
+ } else {
+ mDragDistance = currentBottomView.getMeasuredHeight() - dp2px(getCurrentOffset());
+ }
+ }
+
+ if (mShowMode == ShowMode.PullOut) {
+ layoutPullOut();
+ } else if (mShowMode == ShowMode.LayDown) {
+ layoutLayDown();
+ }
+
+ safeBottomView();
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/IPlayer.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/IPlayer.java
new file mode 100644
index 00000000..54307945
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/IPlayer.java
@@ -0,0 +1,74 @@
+package com.tencent.qcloud.tuikit.timcommon.component.videoview;
+
+import android.content.Context;
+import android.net.Uri;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+
+import java.io.IOException;
+
+public interface IPlayer {
+ void setOnPreparedListener(final OnPreparedListener l);
+
+ void setOnErrorListener(final OnErrorListener l);
+
+ void setOnCompletionListener(final OnCompletionListener l);
+
+ void setOnVideoSizeChangedListener(final OnVideoSizeChangedListener l);
+
+ void setOnSeekCompleteListener(final OnSeekCompleteListener l);
+
+ void setOnInfoListener(final OnInfoListener l);
+
+ void setDisplay(SurfaceHolder sh);
+
+ void setSurface(Surface sh);
+
+ void setDataSource(Context context, Uri uri) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException;
+
+ void prepareAsync();
+
+ void release();
+
+ void start();
+
+ void stop();
+
+ void pause();
+
+ boolean isPlaying();
+
+ int getVideoWidth();
+
+ int getVideoHeight();
+
+ void seekTo(int progress);
+
+ int getCurrentPosition();
+
+ int getDuration();
+
+ interface OnPreparedListener {
+ void onPrepared(IPlayer mp);
+ }
+
+ interface OnErrorListener {
+ boolean onError(IPlayer mp, int what, int extra);
+ }
+
+ interface OnCompletionListener {
+ void onCompletion(IPlayer mp);
+ }
+
+ interface OnVideoSizeChangedListener {
+ void onVideoSizeChanged(IPlayer mp, int width, int height);
+ }
+
+ interface OnInfoListener {
+ void onInfo(IPlayer mp, int what, int extra);
+ }
+
+ interface OnSeekCompleteListener {
+ void onSeekComplete(IPlayer mp);
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/MediaPlayerProxy.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/MediaPlayerProxy.java
new file mode 100644
index 00000000..27913482
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/MediaPlayerProxy.java
@@ -0,0 +1,120 @@
+package com.tencent.qcloud.tuikit.timcommon.component.videoview;
+
+import android.content.Context;
+import android.net.Uri;
+import android.util.Log;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+
+import java.io.IOException;
+
+public class MediaPlayerProxy implements IPlayer {
+ private static final String TAG = MediaPlayerProxy.class.getSimpleName();
+
+ private IPlayer mMediaPlayer;
+
+ public MediaPlayerProxy() {
+ mMediaPlayer = new SystemMediaPlayerWrapper();
+ Log.i(TAG, "use mMediaPlayer: " + mMediaPlayer);
+ }
+
+ @Override
+ public void setOnPreparedListener(final OnPreparedListener l) {
+ mMediaPlayer.setOnPreparedListener(l);
+ }
+
+ @Override
+ public void setOnErrorListener(final OnErrorListener l) {
+ mMediaPlayer.setOnErrorListener(l);
+ }
+
+ @Override
+ public void setOnCompletionListener(final OnCompletionListener l) {
+ mMediaPlayer.setOnCompletionListener(l);
+ }
+
+ @Override
+ public void setOnVideoSizeChangedListener(final OnVideoSizeChangedListener l) {
+ mMediaPlayer.setOnVideoSizeChangedListener(l);
+ }
+
+ @Override
+ public void setOnSeekCompleteListener(OnSeekCompleteListener l) {
+ mMediaPlayer.setOnSeekCompleteListener(l);
+ }
+
+ @Override
+ public void setOnInfoListener(final OnInfoListener l) {
+ mMediaPlayer.setOnInfoListener(l);
+ }
+
+ @Override
+ public void setDisplay(SurfaceHolder sh) {
+ mMediaPlayer.setDisplay(sh);
+ }
+
+ @Override
+ public void setSurface(Surface sh) {
+ mMediaPlayer.setSurface(sh);
+ }
+
+ @Override
+ public void setDataSource(Context context, Uri uri) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {
+ mMediaPlayer.setDataSource(context, uri);
+ }
+
+ @Override
+ public void prepareAsync() {
+ mMediaPlayer.prepareAsync();
+ }
+
+ @Override
+ public void release() {
+ mMediaPlayer.release();
+ }
+
+ @Override
+ public void start() {
+ mMediaPlayer.start();
+ }
+
+ @Override
+ public void stop() {
+ mMediaPlayer.stop();
+ }
+
+ @Override
+ public void pause() {
+ mMediaPlayer.pause();
+ }
+
+ @Override
+ public boolean isPlaying() {
+ return mMediaPlayer.isPlaying();
+ }
+
+ @Override
+ public int getVideoWidth() {
+ return mMediaPlayer.getVideoWidth();
+ }
+
+ @Override
+ public int getVideoHeight() {
+ return mMediaPlayer.getVideoHeight();
+ }
+
+ @Override
+ public void seekTo(int progress) {
+ mMediaPlayer.seekTo(progress);
+ }
+
+ @Override
+ public int getCurrentPosition() {
+ return mMediaPlayer.getCurrentPosition();
+ }
+
+ @Override
+ public int getDuration() {
+ return mMediaPlayer.getDuration();
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/SystemMediaPlayerWrapper.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/SystemMediaPlayerWrapper.java
new file mode 100644
index 00000000..936b4d55
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/SystemMediaPlayerWrapper.java
@@ -0,0 +1,153 @@
+package com.tencent.qcloud.tuikit.timcommon.component.videoview;
+
+import android.content.Context;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.Build;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+
+import java.io.IOException;
+
+public class SystemMediaPlayerWrapper implements IPlayer {
+ private MediaPlayer mMediaPlayer;
+
+ public SystemMediaPlayerWrapper() {
+ mMediaPlayer = new MediaPlayer();
+ }
+
+ @Override
+ public void setOnPreparedListener(final OnPreparedListener l) {
+ mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
+ @Override
+ public void onPrepared(MediaPlayer mp) {
+ l.onPrepared(SystemMediaPlayerWrapper.this);
+ }
+ });
+ }
+
+ @Override
+ public void setOnErrorListener(final OnErrorListener l) {
+ mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
+ @Override
+ public boolean onError(MediaPlayer mp, int what, int extra) {
+ return l.onError(SystemMediaPlayerWrapper.this, what, extra);
+ }
+ });
+ }
+
+ @Override
+ public void setOnCompletionListener(final OnCompletionListener l) {
+ mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
+ @Override
+ public void onCompletion(MediaPlayer mp) {
+ l.onCompletion(SystemMediaPlayerWrapper.this);
+ }
+ });
+ }
+
+ @Override
+ public void setOnSeekCompleteListener(final OnSeekCompleteListener l) {
+ mMediaPlayer.setOnSeekCompleteListener(new MediaPlayer.OnSeekCompleteListener() {
+ @Override
+ public void onSeekComplete(MediaPlayer mediaPlayer) {
+ l.onSeekComplete(SystemMediaPlayerWrapper.this);
+ }
+ });
+ }
+
+ @Override
+ public void setOnVideoSizeChangedListener(final OnVideoSizeChangedListener l) {
+ mMediaPlayer.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() {
+ @Override
+ public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
+ l.onVideoSizeChanged(SystemMediaPlayerWrapper.this, width, height);
+ }
+ });
+ }
+
+ @Override
+ public void setOnInfoListener(final OnInfoListener l) {
+ mMediaPlayer.setOnInfoListener(new MediaPlayer.OnInfoListener() {
+ @Override
+ public boolean onInfo(MediaPlayer mp, int what, int extra) {
+ l.onInfo(SystemMediaPlayerWrapper.this, what, extra);
+ return false;
+ }
+ });
+ }
+
+ @Override
+ public void setDisplay(SurfaceHolder sh) {
+ mMediaPlayer.setDisplay(sh);
+ }
+
+ @Override
+ public void setSurface(Surface sh) {
+ mMediaPlayer.setSurface(sh);
+ }
+
+ @Override
+ public void setDataSource(Context context, Uri uri) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {
+ mMediaPlayer.setDataSource(context, uri);
+ }
+
+ @Override
+ public void prepareAsync() {
+ mMediaPlayer.prepareAsync();
+ }
+
+ @Override
+ public void release() {
+ mMediaPlayer.release();
+ }
+
+ @Override
+ public void start() {
+ mMediaPlayer.start();
+ }
+
+ @Override
+ public void stop() {
+ mMediaPlayer.stop();
+ }
+
+ @Override
+ public void pause() {
+ mMediaPlayer.pause();
+ }
+
+ @Override
+ public boolean isPlaying() {
+ return mMediaPlayer.isPlaying();
+ }
+
+ @Override
+ public int getVideoWidth() {
+ return mMediaPlayer.getVideoWidth();
+ }
+
+ @Override
+ public int getVideoHeight() {
+ return mMediaPlayer.getVideoHeight();
+ }
+
+ @Override
+ public void seekTo(int progress) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ mMediaPlayer.seekTo(progress, MediaPlayer.SEEK_CLOSEST);
+ } else {
+ mMediaPlayer.seekTo(progress);
+ }
+ }
+
+ @Override
+ public int getCurrentPosition() {
+ return mMediaPlayer.getCurrentPosition();
+ }
+
+ @Override
+ public int getDuration() {
+ return mMediaPlayer.getDuration();
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/VideoGestureScaleAttacher.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/VideoGestureScaleAttacher.java
new file mode 100644
index 00000000..429d25b3
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/VideoGestureScaleAttacher.java
@@ -0,0 +1,441 @@
+package com.tencent.qcloud.tuikit.timcommon.component.videoview;
+
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.TextureView;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewParent;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.widget.ImageView;
+import android.widget.OverScroller;
+import com.tencent.qcloud.tuikit.timcommon.util.ThreadUtils;
+
+public class VideoGestureScaleAttacher {
+ private static final float EDGE_DRAG_EVENT_INTERCEPT_THRESHOLD = 50f;
+ private static final int EDGE_NONE = -1;
+ private static final int EDGE_LEFT = 0;
+ private static final int EDGE_RIGHT = 1;
+ private static final int EDGE_BOTH = 2;
+ private static final float DEFAULT_MAX_SCALE = 3.0f;
+ private static final float DEFAULT_MID_SCALE = 1.75f;
+ private static final float DEFAULT_MIN_SCALE = 1.0f;
+ private static final int DEFAULT_ZOOM_DURATION = 200;
+ private ScaleGestureDetector scaleGestureDetector;
+ private float mTouchSlop;
+ private float mMinimumVelocity;
+ private VelocityTracker mVelocityTracker;
+ private boolean mIsDragging;
+ private float mLastTouchX;
+ private float mLastTouchY;
+ private OnScaleListener internalScaleListener;
+ private VideoView view;
+ private float minScale = DEFAULT_MIN_SCALE;
+ private float middleScale = DEFAULT_MID_SCALE;
+ private float maxScale = DEFAULT_MAX_SCALE;
+ private int scrollEdge;
+ private final Matrix transferMatrix = new Matrix();
+ private final RectF rectF = new RectF();
+ private final float[] mMatrixValues = new float[9];
+ private ImageView.ScaleType scaleType = ImageView.ScaleType.FIT_CENTER;
+ private final Interpolator interpolator = new AccelerateDecelerateInterpolator();
+
+ private int zoomDuration = DEFAULT_ZOOM_DURATION;
+
+ private FlingRunnable currentFlingRunnable;
+
+ private VideoGestureScaleAttacher() {}
+
+ public static void attach(VideoView view) {
+ VideoGestureScaleAttacher attacher = new VideoGestureScaleAttacher();
+ attacher.view = view;
+ if (view == null || view.getContext() == null) {
+ return;
+ }
+ Context context = view.getContext();
+ final ViewConfiguration configuration = ViewConfiguration.get(context);
+ attacher.mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
+ attacher.mTouchSlop = configuration.getScaledTouchSlop();
+ attacher.internalScaleListener = new OnScaleListener() {
+ @Override
+ public boolean onScale(ScaleGestureDetector detector) {
+ float scaleFactor = detector.getScaleFactor();
+ if (Float.isNaN(scaleFactor) || Float.isInfinite(scaleFactor)) {
+ return false;
+ }
+ float focusX = detector.getFocusX();
+ float focusY = detector.getFocusY();
+ return attacher.scale(scaleFactor, focusX, focusY);
+ }
+
+ @Override
+ public void onFling(float startX, float startY, float velocityX, float velocityY) {
+ if (attacher.scaleGestureDetector.isInProgress()) {
+ return;
+ }
+ attacher.currentFlingRunnable = attacher.new FlingRunnable(attacher.view);
+ attacher.currentFlingRunnable.fling(attacher.getViewWidth(), attacher.getViewHeight(), (int) velocityX, (int) velocityY);
+ ThreadUtils.runOnUiThread(attacher.currentFlingRunnable);
+ }
+
+ @Override
+ public void onDrag(float dx, float dy) {
+ if (attacher.scaleGestureDetector.isInProgress()) {
+ return;
+ }
+ attacher.transferMatrix.postTranslate(dx, dy);
+ attacher.invalidateView();
+ attacher.checkMatrixBounds();
+ if (attacher.scrollEdge == EDGE_BOTH && Math.abs(dx) >= EDGE_DRAG_EVENT_INTERCEPT_THRESHOLD
+ || (attacher.scrollEdge == EDGE_LEFT && dx >= EDGE_DRAG_EVENT_INTERCEPT_THRESHOLD)
+ || (attacher.scrollEdge == EDGE_RIGHT && dx <= -EDGE_DRAG_EVENT_INTERCEPT_THRESHOLD)) {
+ ViewParent viewParent = view.getParent();
+ if (viewParent != null) {
+ viewParent.requestDisallowInterceptTouchEvent(false);
+ }
+ } else {
+ ViewParent viewParent = view.getParent();
+ if (viewParent != null) {
+ viewParent.requestDisallowInterceptTouchEvent(true);
+ }
+ }
+ }
+ };
+ attacher.scaleGestureDetector = new ScaleGestureDetector(context, attacher.internalScaleListener);
+
+ view.setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ attacher.scaleGestureDetector.onTouchEvent(event);
+ attacher.processTouchEvent(event);
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN: {
+ attacher.cancelFling();
+ ViewParent viewParent = view.getParent();
+ if (viewParent != null) {
+ viewParent.requestDisallowInterceptTouchEvent(true);
+ }
+ break;
+ }
+ case MotionEvent.ACTION_CANCEL:
+ onActionCancel();
+ break;
+ case MotionEvent.ACTION_UP: {
+ onActionCancel();
+ break;
+ }
+ default:
+ break;
+ }
+ return true;
+ }
+
+ private void onActionCancel() {
+ if (attacher.getScale() < attacher.minScale) {
+ RectF rect = attacher.getDisplayRect();
+ view.post(attacher.new AnimatedZoomRunnable(attacher.getScale(), attacher.minScale, rect.centerX(), rect.centerY()));
+ } else if (attacher.getScale() > attacher.maxScale) {
+ RectF rect = attacher.getDisplayRect();
+ view.post(attacher.new AnimatedZoomRunnable(attacher.getScale(), attacher.maxScale, rect.centerX(), rect.centerY()));
+ }
+ }
+ });
+ }
+
+ public void cancelFling() {
+ if (currentFlingRunnable != null) {
+ currentFlingRunnable.cancelFling();
+ currentFlingRunnable = null;
+ }
+ }
+
+ private boolean scale(float scaleFactor, float focusX, float focusY) {
+ if ((getScale() <= maxScale || scaleFactor < 1f) && (getScale() >= minScale || scaleFactor > 1f)) {
+ transferMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);
+ invalidateView();
+ checkMatrixBounds();
+ return true;
+ }
+ return false;
+ }
+
+ private void invalidateView() {
+ Matrix baseMatrix = view.getBaseMatrix();
+ Matrix matrix = new Matrix();
+ matrix.setConcat(baseMatrix, transferMatrix);
+ view.setTransform(matrix);
+ view.invalidate();
+ }
+
+ private boolean processTouchEvent(MotionEvent ev) {
+ final int action = ev.getAction();
+ switch (action & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_DOWN:
+
+ mVelocityTracker = VelocityTracker.obtain();
+ if (null != mVelocityTracker) {
+ mVelocityTracker.addMovement(ev);
+ }
+
+ mLastTouchX = ev.getX();
+ mLastTouchY = ev.getY();
+ mIsDragging = false;
+
+ break;
+ case MotionEvent.ACTION_MOVE:
+ final float x = ev.getX();
+ final float y = ev.getY();
+ final float dx = x - mLastTouchX;
+ final float dy = y - mLastTouchY;
+
+ if (!mIsDragging) {
+ // Use Pythagoras to see if drag length is larger than
+ // touch slop
+ mIsDragging = Math.sqrt((dx * dx) + (dy * dy)) >= mTouchSlop;
+ }
+
+ if (mIsDragging) {
+ internalScaleListener.onDrag(dx, dy);
+ mLastTouchX = x;
+ mLastTouchY = y;
+
+ if (null != mVelocityTracker) {
+ mVelocityTracker.addMovement(ev);
+ }
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ // Recycle Velocity Tracker
+ if (null != mVelocityTracker) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ if (mIsDragging) {
+ if (null != mVelocityTracker) {
+ mLastTouchX = ev.getX();
+ mLastTouchY = ev.getY();
+
+ // Compute velocity within the last 1000ms
+ mVelocityTracker.addMovement(ev);
+ mVelocityTracker.computeCurrentVelocity(1000);
+
+ final float vX = mVelocityTracker.getXVelocity();
+ final float vY = mVelocityTracker.getYVelocity();
+
+ // If the velocity is greater than minVelocity, call
+ // listener
+ if (Math.max(Math.abs(vX), Math.abs(vY)) >= mMinimumVelocity) {
+ internalScaleListener.onFling(mLastTouchX, mLastTouchY, -vX, -vY);
+ }
+ }
+ }
+
+ // Recycle Velocity Tracker
+ if (null != mVelocityTracker) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ break;
+ default:
+ break;
+ }
+ return true;
+ }
+
+ private RectF getDisplayRect() {
+ rectF.set(0, 0, view.getWidth(), view.getHeight());
+ Matrix matrix = new Matrix();
+ view.getTransform(matrix);
+ matrix.mapRect(rectF);
+ return rectF;
+ }
+
+ public float getScale() {
+ return (float) Math.sqrt((Math.pow(getValue(transferMatrix, Matrix.MSCALE_X), 2) + Math.pow(getValue(transferMatrix, Matrix.MSCALE_Y), 2)) / 2);
+ }
+
+ private void checkMatrixBounds() {
+ final RectF rect = getDisplayRect();
+ final float height = rect.height();
+ final float width = rect.width();
+ float deltaX = 0;
+ float deltaY = 0;
+ final int viewHeight = getViewHeight();
+ if (height <= viewHeight) {
+ switch (scaleType) {
+ case FIT_START:
+ deltaY = -rect.top;
+ break;
+ case FIT_END:
+ deltaY = viewHeight - height - rect.top;
+ break;
+ default:
+ deltaY = (viewHeight - height) / 2 - rect.top;
+ break;
+ }
+ } else if (rect.top > 0) {
+ deltaY = -rect.top;
+ } else if (rect.bottom < viewHeight) {
+ deltaY = viewHeight - rect.bottom;
+ }
+ final int viewWidth = getViewWidth();
+ if (width <= viewWidth) {
+ switch (scaleType) {
+ case FIT_START:
+ deltaX = -rect.left;
+ break;
+ case FIT_END:
+ deltaX = viewWidth - width - rect.left;
+ break;
+ default:
+ deltaX = (viewWidth - width) / 2 - rect.left;
+ break;
+ }
+ scrollEdge = EDGE_BOTH;
+ } else if (rect.left > 0) {
+ scrollEdge = EDGE_LEFT;
+ deltaX = -rect.left;
+ } else if (rect.right < viewWidth) {
+ deltaX = viewWidth - rect.right;
+ scrollEdge = EDGE_RIGHT;
+ } else {
+ scrollEdge = EDGE_NONE;
+ }
+ // Finally actually translate the matrix
+ transferMatrix.postTranslate(deltaX, deltaY);
+ invalidateView();
+ }
+
+ private int getViewWidth() {
+ return view.getWidth() - view.getPaddingLeft() - view.getPaddingRight();
+ }
+
+ private int getViewHeight() {
+ return view.getHeight() - view.getPaddingTop() - view.getPaddingBottom();
+ }
+
+ /**
+ * 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];
+ }
+
+ private class AnimatedZoomRunnable implements Runnable {
+ private final float focalX;
+ private final float focalY;
+ private final long startTime;
+ private final float zoomStart;
+ private final float zoomEnd;
+
+ public AnimatedZoomRunnable(final float currentZoom, final float targetZoom, final float focalX, final float focalY) {
+ this.focalX = focalX;
+ this.focalY = focalY;
+ startTime = System.currentTimeMillis();
+ zoomStart = currentZoom;
+ zoomEnd = targetZoom;
+ }
+
+ @Override
+ public void run() {
+ float t = interpolate();
+ float scale = zoomStart + t * (zoomEnd - zoomStart);
+ float deltaScale = scale / getScale();
+ scale(deltaScale, focalX, focalY);
+ // We haven't hit our target scale yet, so post ourselves again
+ if (t < 1f) {
+ view.postOnAnimation(this);
+ }
+ }
+
+ private float interpolate() {
+ float t = 1f * (System.currentTimeMillis() - startTime) / zoomDuration;
+ t = Math.min(1f, t);
+ t = interpolator.getInterpolation(t);
+ return t;
+ }
+ }
+
+ public class FlingRunnable implements Runnable {
+ private final OverScroller mScroller;
+ private int mCurrentX;
+ private int mCurrentY;
+ private TextureView view;
+
+ public FlingRunnable(TextureView view) {
+ mScroller = new OverScroller(view.getContext());
+ this.view = view;
+ }
+
+ 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;
+ final int maxX;
+ final int minY;
+ final int 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();
+ transferMatrix.postTranslate(mCurrentX - newX, mCurrentY - newY);
+ invalidateView();
+ checkMatrixBounds();
+ mCurrentX = newX;
+ mCurrentY = newY;
+ // Post On animation
+ view.postOnAnimation(this);
+ }
+ }
+ }
+
+ public static class OnScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
+ public void onFling(float startX, float startY, float velocityX, float velocityY) {}
+
+ public void onDrag(float dx, float dy) {}
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/VideoView.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/VideoView.java
new file mode 100644
index 00000000..3c1eed8f
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/VideoView.java
@@ -0,0 +1,324 @@
+package com.tencent.qcloud.tuikit.timcommon.component.videoview;
+
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.SurfaceTexture;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Surface;
+import android.view.TextureView;
+
+import androidx.annotation.Nullable;
+
+public class VideoView extends TextureView {
+ private static final String TAG = VideoView.class.getSimpleName();
+
+ public static final int STATE_ERROR = -1;
+ public static final int STATE_IDLE = 0;
+ public static final int STATE_PREPARING = 1;
+ public static final int STATE_PREPARED = 2;
+ public static final int STATE_PLAYING = 3;
+ public static final int STATE_PAUSED = 4;
+ public static final int STATE_PLAYBACK_COMPLETED = 5;
+ public static final int STATE_STOPPED = 6;
+
+ private int mCurrentState = STATE_IDLE;
+
+ private Context mContext;
+ private Surface mSurface;
+ private MediaPlayerProxy mMediaPlayer;
+
+ private Uri mUri;
+ private int mVideoRotationDegree;
+ private Matrix baseMatrix = new Matrix();
+ private IPlayer.OnPreparedListener mOutOnPreparedListener;
+ private IPlayer.OnErrorListener mOutOnErrorListener;
+ private IPlayer.OnCompletionListener mOutOnCompletionListener;
+ private IPlayer.OnSeekCompleteListener mOnSeekCompleteListener;
+ private IPlayer.OnPreparedListener mOnPreparedListener = new IPlayer.OnPreparedListener() {
+ public void onPrepared(IPlayer mp) {
+ mCurrentState = STATE_PREPARED;
+ // Video fit center
+ float videoHeight = mp.getVideoHeight();
+ float videoWidth = mp.getVideoWidth();
+ float viewHeight = getHeight() - getPaddingBottom() - getPaddingTop();
+ float viewWidth = getWidth() - getPaddingLeft() - getPaddingRight();
+
+ float finalVideoHeight = viewHeight;
+ float finalVideoWidth = viewHeight * videoWidth / videoHeight;
+
+ if (finalVideoWidth > viewWidth) {
+ finalVideoWidth = viewWidth;
+ finalVideoHeight = viewWidth * videoHeight / videoWidth;
+ }
+
+ float scaleX = finalVideoWidth / viewWidth;
+ float scaleY = finalVideoHeight / viewHeight;
+ float dx = (viewWidth - finalVideoWidth) / 2;
+ float dy = (viewHeight - finalVideoHeight) / 2;
+ Matrix matrix = new Matrix();
+ matrix.postScale(scaleX, scaleY);
+ matrix.postTranslate(dx, dy);
+ baseMatrix.set(matrix);
+ setTransform(matrix);
+ invalidate();
+
+ Log.i(TAG, "onPrepared mVideoWidth: " + videoWidth + " mVideoHeight: " + videoHeight + " mVideoRotationDegree: " + mVideoRotationDegree);
+ if (mOutOnPreparedListener != null) {
+ mOutOnPreparedListener.onPrepared(mp);
+ }
+ }
+ };
+
+ private IPlayer.OnErrorListener mOnErrorListener = new IPlayer.OnErrorListener() {
+ public boolean onError(IPlayer mp, int what, int extra) {
+ Log.w(TAG, "onError: what/extra: " + what + "/" + extra);
+ mCurrentState = STATE_ERROR;
+ stopMedia();
+ if (mOutOnErrorListener != null) {
+ mOutOnErrorListener.onError(mp, what, extra);
+ }
+ return true;
+ }
+ };
+ private IPlayer.OnInfoListener mOnInfoListener = new IPlayer.OnInfoListener() {
+ public void onInfo(IPlayer mp, int what, int extra) {
+ Log.w(TAG, "onInfo: what/extra: " + what + "/" + extra);
+ if (what == 10001) { // IJK: MEDIA_INFO_VIDEO_ROTATION_CHANGED
+ mVideoRotationDegree = extra;
+ setRotation(mVideoRotationDegree);
+ requestLayout();
+ }
+ }
+ };
+ private IPlayer.OnCompletionListener mOnCompletionListener = new IPlayer.OnCompletionListener() {
+ public void onCompletion(IPlayer mp) {
+ Log.i(TAG, "onCompletion");
+ mCurrentState = STATE_PLAYBACK_COMPLETED;
+ if (mOutOnCompletionListener != null) {
+ mOutOnCompletionListener.onCompletion(mp);
+ }
+ }
+ };
+ private IPlayer.OnVideoSizeChangedListener mOnVideoSizeChangedListener = new IPlayer.OnVideoSizeChangedListener() {
+ @Override
+ public void onVideoSizeChanged(IPlayer mp, int width, int height) {
+ // TUIChatLog.i(TAG, "onVideoSizeChanged width: " + width + " height: " + height);
+ }
+ };
+
+ private IPlayer.OnSeekCompleteListener onSeekCompleteListener = new IPlayer.OnSeekCompleteListener() {
+ @Override
+ public void onSeekComplete(IPlayer mp) {
+ if (mOnSeekCompleteListener != null) {
+ mOnSeekCompleteListener.onSeekComplete(mp);
+ }
+ }
+ };
+ private SurfaceTextureListener mSurfaceTextureListener = new SurfaceTextureListener() {
+ @Override
+ public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
+ Log.i(TAG, "onSurfaceTextureAvailable");
+ mSurface = new Surface(surface);
+ openVideo();
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
+ Log.i(TAG, "onSurfaceTextureSizeChanged");
+ }
+
+ @Override
+ public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
+ Log.i(TAG, "onSurfaceTextureDestroyed");
+ return true;
+ }
+
+ @Override
+ public void onSurfaceTextureUpdated(SurfaceTexture surface) {
+ // TUIChatLog.i(TAG,"onSurfaceTextureUpdated");
+ }
+ };
+
+ public VideoView(Context context) {
+ super(context);
+ initVideoView(context);
+ }
+
+ public VideoView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initVideoView(context);
+ }
+
+ public VideoView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initVideoView(context);
+ }
+
+ private void initVideoView(Context context) {
+ Log.i(TAG, "initVideoView");
+ mContext = context;
+ setSurfaceTextureListener(mSurfaceTextureListener);
+ mCurrentState = STATE_IDLE;
+
+ VideoGestureScaleAttacher.attach(this);
+ }
+
+ public Matrix getBaseMatrix() {
+ return baseMatrix;
+ }
+
+ public void setOnPreparedListener(IPlayer.OnPreparedListener l) {
+ mOutOnPreparedListener = l;
+ }
+
+ public void setOnSeekCompleteListener(IPlayer.OnSeekCompleteListener l) {
+ mOnSeekCompleteListener = l;
+ }
+
+ public void setOnErrorListener(IPlayer.OnErrorListener l) {
+ mOutOnErrorListener = l;
+ }
+
+ public void setOnCompletionListener(IPlayer.OnCompletionListener l) {
+ mOutOnCompletionListener = l;
+ }
+
+ public void setVideoURI(Uri uri) {
+ mUri = uri;
+ openVideo();
+ }
+
+ public void resetVideo() {
+ openVideo();
+ }
+
+ private void openVideo() {
+ if (mUri == null) {
+ Log.e(TAG, "openVideo: mUri is null ");
+ return;
+ }
+ Log.i(TAG, "openVideo: mUri: " + mUri.getPath() + " mSurface: " + mSurface);
+ if (mSurface == null) {
+ Log.e(TAG, "openVideo: mSurface is null ");
+ return;
+ }
+
+ stopMedia();
+ try {
+ mMediaPlayer = new MediaPlayerProxy();
+ mMediaPlayer.setOnPreparedListener(mOnPreparedListener);
+ mMediaPlayer.setOnCompletionListener(mOnCompletionListener);
+ mMediaPlayer.setOnErrorListener(mOnErrorListener);
+ mMediaPlayer.setOnInfoListener(mOnInfoListener);
+ mMediaPlayer.setOnVideoSizeChangedListener(mOnVideoSizeChangedListener);
+ mMediaPlayer.setOnSeekCompleteListener(onSeekCompleteListener);
+ mMediaPlayer.setSurface(mSurface);
+ mMediaPlayer.setDataSource(getContext(), mUri);
+ mMediaPlayer.prepareAsync();
+ mCurrentState = STATE_PREPARING;
+ } catch (Exception ex) {
+ Log.w(TAG, "ex = " + ex.getMessage());
+ mCurrentState = STATE_ERROR;
+ }
+ }
+
+ public boolean start() {
+ Log.i(TAG, "start mCurrentState:" + mCurrentState);
+ if (mMediaPlayer != null) {
+ mMediaPlayer.start();
+ mCurrentState = STATE_PLAYING;
+ }
+ return true;
+ }
+
+ public boolean stop() {
+ Log.i(TAG, "stop mCurrentState:" + mCurrentState);
+ stopMedia();
+ return true;
+ }
+
+ public boolean pause() {
+ Log.i(TAG, "pause mCurrentState:" + mCurrentState);
+ if (mMediaPlayer != null) {
+ mMediaPlayer.pause();
+ mCurrentState = STATE_PAUSED;
+ }
+ return true;
+ }
+
+ public void stopMedia() {
+ if (mMediaPlayer != null) {
+ mMediaPlayer.stop();
+ mMediaPlayer.release();
+ mMediaPlayer = null;
+ mCurrentState = STATE_IDLE;
+ }
+ }
+
+ public int getCurrentState() {
+ return mCurrentState;
+ }
+
+ public boolean isPlaying() {
+ if (mMediaPlayer != null) {
+ return mMediaPlayer.isPlaying();
+ }
+ return false;
+ }
+
+ public void seekTo(int progress) {
+ if (mMediaPlayer != null) {
+ mMediaPlayer.seekTo(progress);
+ }
+ }
+
+ public boolean isPrepared() {
+ if (mUri == null) {
+ Log.e(TAG, "isPrepared: mUri is null ");
+ return false;
+ }
+ Log.i(TAG, "isPrepared: mUri: " + mUri.getPath() + " mSurface: " + mSurface);
+ if (mSurface == null) {
+ Log.e(TAG, "isPrepared: mSurface is null ");
+ return false;
+ }
+
+ return true;
+ }
+
+ public int getCurrentPosition() {
+ if (mMediaPlayer != null) {
+ return mMediaPlayer.getCurrentPosition();
+ }
+ return 0;
+ }
+
+ public int getDuration() {
+ if (mMediaPlayer != null) {
+ return mMediaPlayer.getDuration();
+ }
+ return 0;
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ stopMedia();
+ }
+
+ @Override
+ public void setBackgroundDrawable(Drawable background) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N && background != null) {
+ super.setBackgroundDrawable(background);
+ }
+ }
+
+ @Override
+ public void setOnClickListener(@Nullable OnClickListener l) {
+ super.setOnClickListener(l);
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/config/classicui/TUIConfigClassic.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/config/classicui/TUIConfigClassic.java
new file mode 100644
index 00000000..5c683253
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/config/classicui/TUIConfigClassic.java
@@ -0,0 +1,336 @@
+package com.tencent.qcloud.tuikit.timcommon.config.classicui;
+
+import static com.tencent.qcloud.tuikit.timcommon.util.TUIUtil.newDrawable;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+import com.tencent.qcloud.tuicore.TUIConfig;
+import com.tencent.qcloud.tuicore.TUIThemeManager;
+import com.tencent.qcloud.tuicore.util.ToastUtil;
+import com.tencent.qcloud.tuikit.timcommon.component.highlight.HighlightPresenter;
+
+public class TUIConfigClassic {
+ private TUIConfigClassic() {}
+
+ private static final class TUIConfigClassicHolder {
+ private static final TUIConfigClassic INSTANCE = new TUIConfigClassic();
+ }
+
+ private static TUIConfigClassic getInstance() {
+ return TUIConfigClassicHolder.INSTANCE;
+ }
+
+ public static final int UNDEFINED = -1;
+ // message bubble
+ private boolean enableMessageBubbleStyle = true;
+ private Drawable sendBubbleBackground;
+ private Drawable receiveBubbleBackground;
+ private Drawable sendErrorBubbleBackground;
+ private Drawable receiveErrorBubbleBackground;
+
+ // message style
+ private Drawable chatTimeBubble;
+ private int chatTimeFontSize = UNDEFINED;
+ private int chatTimeFontColor = UNDEFINED;
+ private Drawable defaultAvatarImage;
+ private int avatarRadius = UNDEFINED;
+ private int avatarSize = UNDEFINED;
+ private int receiveNickNameVisibility = UNDEFINED;
+ private int receiveNickNameFontSize = UNDEFINED;
+ private int receiveNickNameColor = UNDEFINED;
+
+ /**
+ * Set the default app directory.The default dir is "file".
+ * @param appDir
+ */
+ public static void setDefaultAppDir(String appDir) {
+ TUIConfig.setDefaultAppDir(appDir);
+ TUIConfig.initPath();
+ }
+
+ /**
+ * Set whether show the toast prompt built into TUIKit.The default value is true.
+ * @param enableToast
+ */
+ public static void enableToast(boolean enableToast) {
+ ToastUtil.setEnableToast(enableToast);
+ }
+
+ /**
+ * Set whether to enable language switching.The default value is false.
+ * @param enableLanguageSwitch
+ */
+ public static void enableLanguageSwitch(boolean enableLanguageSwitch) {
+ TUIThemeManager.setEnableLanguageSwitch(enableLanguageSwitch);
+ }
+
+ /**
+ * Switch the language of TUIKit.
+ * The currently supported languages are "en", "zh", and "ar".
+ * @param context
+ * @param targetLanguage
+ */
+ public static void switchLanguageToTarget(Context context, String targetLanguage) {
+ TUIThemeManager.getInstance().changeLanguage(context, targetLanguage);
+ }
+
+ /**
+ * Switch theme to target.
+ * The currently supported themes are THEME_LIGHT, THEME_LIVELY, and THEME_SERIOUS.
+ * @param context
+ * @param themeID
+ */
+ public static void switchThemeToTarget(Context context, int themeID) {
+ TUIThemeManager.getInstance().changeTheme(context, themeID);
+ }
+
+ /**
+ * Set whether to enable message bubble style.The default value is true.
+ * @param enable
+ */
+ public static void setEnableMessageBubbleStyle(boolean enable) {
+ getInstance().enableMessageBubbleStyle = enable;
+ }
+
+ /**
+ * Get whether to enable message bubble style.
+ * @return true is enable, false is not
+ */
+ public static boolean isEnableMessageBubbleStyle() {
+ return getInstance().enableMessageBubbleStyle;
+ }
+
+ /**
+ * Set the background of the send message bubble.
+ * @param drawable
+ */
+ public static void setSendBubbleBackground(Drawable drawable) {
+ getInstance().sendBubbleBackground = drawable;
+ }
+
+ /**
+ * Get the background of the send message bubble.
+ * @return the background
+ */
+ public static Drawable getSendBubbleBackground() {
+ return newDrawable(getInstance().sendBubbleBackground);
+ }
+
+ /**
+ * Set the background of the receive message bubble.
+ * @param drawable
+ */
+ public static void setReceiveBubbleBackground(Drawable drawable) {
+ getInstance().receiveBubbleBackground = drawable;
+ }
+
+ /**
+ * Get the background of the receive message bubble.
+ * @return the background
+ */
+ public static Drawable getReceiveBubbleBackground() {
+ return newDrawable(getInstance().receiveBubbleBackground);
+ }
+
+ /**
+ * Set the background of the receive error message bubble.
+ * @param receiveErrorBubbleBackground
+ */
+ public static void setReceiveErrorBubbleBackground(Drawable receiveErrorBubbleBackground) {
+ getInstance().receiveErrorBubbleBackground = receiveErrorBubbleBackground;
+ }
+
+ /**
+ * Get the background of the receive error message bubble.
+ * @return the background
+ */
+ public static Drawable getReceiveErrorBubbleBackground() {
+ return newDrawable(getInstance().receiveErrorBubbleBackground);
+ }
+
+ /**
+ * Set the background of the send error message bubble.
+ * @param sendErrorBubbleBackground
+ */
+ public static void setSendErrorBubbleBackground(Drawable sendErrorBubbleBackground) {
+ getInstance().sendErrorBubbleBackground = sendErrorBubbleBackground;
+ }
+
+ /**
+ * Get the background of the send error message bubble.
+ * @return the background
+ */
+ public static Drawable getSendErrorBubbleBackground() {
+ return newDrawable(getInstance().sendErrorBubbleBackground);
+ }
+
+ /**
+ * Set the light color of the message bubble in highlight status..
+ * @param color
+ */
+ public static void setBubbleHighlightLightColor(int color) {
+ HighlightPresenter.setHighlightLightColor(color);
+ }
+
+ /**
+ * Set the dark color of the message bubble in highlight status..
+ * @param color
+ */
+ public static void setBubbleHighlightDarkColor(int color) {
+ HighlightPresenter.setHighlightDarkColor(color);
+ }
+
+ /**
+ * Set the chat time bubble.
+ * @param drawable
+ */
+ public static void setChatTimeBubble(Drawable drawable) {
+ getInstance().chatTimeBubble = drawable;
+ }
+
+ /**
+ * Get the chat time bubble.
+ * @return
+ */
+ public static Drawable getChatTimeBubble() {
+ return newDrawable(getInstance().chatTimeBubble);
+ }
+
+ /**
+ * Set the font size of the chat time text.
+ * @param size
+ */
+ public static void setChatTimeFontSize(int size) {
+ getInstance().chatTimeFontSize = size;
+ }
+
+ /**
+ * Get the font size of the chat time text.
+ * @return
+ */
+ public static int getChatTimeFontSize() {
+ return getInstance().chatTimeFontSize;
+ }
+
+ /**
+ * Set the font color of the chat time text.
+ * @param color
+ */
+ public static void setChatTimeFontColor(int color) {
+ getInstance().chatTimeFontColor = color;
+ }
+
+ /**
+ * Get the font color of the chat time text.
+ * @return
+ */
+ public static int getChatTimeFontColor() {
+ return getInstance().chatTimeFontColor;
+ }
+
+ /**
+ * Set the default avatar image.
+ * @param drawable
+ */
+ public static void setDefaultAvatarImage(Drawable drawable) {
+ getInstance().defaultAvatarImage = drawable;
+ }
+
+ /**
+ * Get the default avatar image.
+ * @return the default avatar image
+ */
+ public static Drawable getDefaultAvatarImage() {
+ return newDrawable(getInstance().defaultAvatarImage);
+ }
+
+ /**
+ * Set the radius of the avatar in the message list.
+ * @param radius
+ */
+ public static void setMessageListAvatarRadius(int radius) {
+ getInstance().avatarRadius = radius;
+ }
+
+ /**
+ * Get the radius of the avatar in the message list.
+ * @return
+ */
+ public static int getMessageListAvatarRadius() {
+ return getInstance().avatarRadius;
+ }
+
+ /**
+ * Set whether to enable the grid avatar with the group chat.The default value is true.
+ * @param enableGroupGridAvatar
+ */
+ public static void setEnableGroupGridAvatar(boolean enableGroupGridAvatar) {
+ TUIConfig.setEnableGroupGridAvatar(enableGroupGridAvatar);
+ }
+
+ /**
+ * Set the avatar size in the message list.
+ * @param size
+ */
+ public static void setMessageListAvatarSize(int size) {
+ getInstance().avatarSize = size;
+ }
+
+ /**
+ * Get the avatar size in the message list.
+ * @return
+ */
+ public static int getMessageListAvatarSize() {
+ return getInstance().avatarSize;
+ }
+
+ /**
+ * Set whether to hide the nickname of the received message.
+ * @param hideReceiveNickName
+ */
+ public static void setHideReceiveNickName(boolean hideReceiveNickName) {
+ getInstance().receiveNickNameVisibility = hideReceiveNickName ? View.GONE : View.VISIBLE;
+ }
+
+ /**
+ * Get the visibility of the nickname of the received message.
+ * @return
+ */
+ public static int getReceiveNickNameVisibility() {
+ return getInstance().receiveNickNameVisibility;
+ }
+
+ /**
+ * Set the font size of the nickname of the received message.
+ * @param size
+ */
+ public static void setReceiveNickNameFontSize(int size) {
+ getInstance().receiveNickNameFontSize = size;
+ }
+
+ /**
+ * Get the font size of the nickname of the received message.
+ * @return
+ */
+ public static int getReceiveNickNameFontSize() {
+ return getInstance().receiveNickNameFontSize;
+ }
+
+ /**
+ * Set the font color of the nickname of the received message.
+ * @param color
+ */
+ public static void setReceiveNickNameColor(int color) {
+ getInstance().receiveNickNameColor = color;
+ }
+
+ /**
+ * Get the font color of the nickname of the received message.
+ * @return
+ */
+ public static int getReceiveNickNameColor() {
+ return getInstance().receiveNickNameColor;
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/config/minimalistui/TUIConfigMinimalist.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/config/minimalistui/TUIConfigMinimalist.java
new file mode 100644
index 00000000..d47dc8ff
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/config/minimalistui/TUIConfigMinimalist.java
@@ -0,0 +1,273 @@
+package com.tencent.qcloud.tuikit.timcommon.config.minimalistui;
+
+import static com.tencent.qcloud.tuikit.timcommon.util.TUIUtil.newDrawable;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import com.tencent.qcloud.tuicore.TUIConfig;
+import com.tencent.qcloud.tuicore.TUIThemeManager;
+import com.tencent.qcloud.tuicore.util.ToastUtil;
+import com.tencent.qcloud.tuikit.timcommon.component.highlight.HighlightPresenter;
+
+public class TUIConfigMinimalist {
+ private TUIConfigMinimalist() {}
+
+ private static final class TUIConfigMinimalistHolder {
+ private static final TUIConfigMinimalist INSTANCE = new TUIConfigMinimalist();
+ }
+
+ private static TUIConfigMinimalist getInstance() {
+ return TUIConfigMinimalistHolder.INSTANCE;
+ }
+
+ public static final int UNDEFINED = -1;
+ // message bubble
+ private boolean enableMessageBubbleStyle = true;
+ private Drawable sendBubbleBackground;
+ private Drawable sendLastBubbleBackground;
+ private Drawable receiveBubbleBackground;
+ private Drawable receiveLastBubbleBackground;
+
+ // message style
+ private Drawable chatTimeBubble;
+ private int chatTimeFontSize = UNDEFINED;
+ private int chatTimeFontColor = UNDEFINED;
+ private Drawable defaultAvatarImage;
+ private int avatarRadius = UNDEFINED;
+ private int avatarSize = UNDEFINED;
+
+ /**
+ * Set the default app directory.The default dir is "file".
+ * @param appDir
+ */
+ public static void setDefaultAppDir(String appDir) {
+ TUIConfig.setDefaultAppDir(appDir);
+ TUIConfig.initPath();
+ }
+
+ /**
+ * Set whether show the toast prompt built into TUIKit.The default value is true.
+ * @param enableToast
+ */
+ public static void enableToast(boolean enableToast) {
+ ToastUtil.setEnableToast(enableToast);
+ }
+
+ /**
+ * Set whether to enable language switching.The default value is false.
+ * @param enableLanguageSwitch
+ */
+ public static void enableLanguageSwitch(boolean enableLanguageSwitch) {
+ TUIThemeManager.setEnableLanguageSwitch(enableLanguageSwitch);
+ }
+
+ /**
+ * Switch the language of TUIKit.
+ * The currently supported languages are "en", "zh", and "ar".
+ * @param context
+ * @param targetLanguage
+ */
+ public static void switchLanguageToTarget(Context context, String targetLanguage) {
+ TUIThemeManager.getInstance().changeLanguage(context, targetLanguage);
+ }
+
+ /**
+ * Set whether to enable message bubble style.The default value is true.
+ * @param enable
+ */
+ public static void setEnableMessageBubbleStyle(boolean enable) {
+ getInstance().enableMessageBubbleStyle = enable;
+ }
+
+ /**
+ * Get whether to enable message bubble style.
+ * @return true is enable, false is not
+ */
+ public static boolean isEnableMessageBubbleStyle() {
+ return getInstance().enableMessageBubbleStyle;
+ }
+
+ /**
+ * Set the background of the send message's bubble in consecutive messages.
+ * @param drawable
+ */
+ public static void setSendBubbleBackground(Drawable drawable) {
+ getInstance().sendBubbleBackground = drawable;
+ }
+
+ /**
+ * Get the background of the send message's bubble in consecutive messages.
+ * @return the background
+ */
+ public static Drawable getSendBubbleBackground() {
+ return newDrawable(getInstance().sendBubbleBackground);
+ }
+
+ /**
+ * Set the background of the receive message's bubble in consecutive messages.
+ * @param drawable
+ */
+ public static void setReceiveBubbleBackground(Drawable drawable) {
+ getInstance().receiveBubbleBackground = drawable;
+ }
+
+ /**
+ * Get the background of the receive message's bubble in consecutive messages.
+ * @return the background
+ */
+ public static Drawable getReceiveBubbleBackground() {
+ return newDrawable(getInstance().receiveBubbleBackground);
+ }
+
+ /**
+ * Set the background image of the last sent message's bubble in consecutive messages.
+ * @param drawable
+ */
+ public static void setSendLastBubbleBackground(Drawable drawable) {
+ getInstance().sendLastBubbleBackground = drawable;
+ }
+
+ /**
+ * Get the background image of the last sent message's bubble in consecutive messages.
+ * @return drawable
+ */
+ public static Drawable getSendLastBubbleBackground() {
+ return newDrawable(getInstance().sendLastBubbleBackground);
+ }
+
+ /**
+ * Set the background image of the last received message's bubble in consecutive messages.
+ * @param drawable
+ */
+ public static void setReceiveLastBubbleBackground(Drawable drawable) {
+ getInstance().receiveLastBubbleBackground = drawable;
+ }
+
+ /**
+ * Get the background image of the last received message's bubble in consecutive messages.
+ * @return drawable
+ */
+ public static Drawable getReceiveLastBubbleBackground() {
+ return newDrawable(getInstance().receiveLastBubbleBackground);
+ }
+
+ /**
+ * Set the light color of the message bubble in highlight status..
+ * @param color
+ */
+ public static void setBubbleHighlightLightColor(int color) {
+ HighlightPresenter.setHighlightLightColor(color);
+ }
+
+ /**
+ * Set the dark color of the message bubble in highlight status..
+ * @param color
+ */
+ public static void setBubbleHighlightDarkColor(int color) {
+ HighlightPresenter.setHighlightDarkColor(color);
+ }
+
+ /**
+ * Set the chat time bubble.
+ * @param drawable
+ */
+ public static void setChatTimeBubble(Drawable drawable) {
+ getInstance().chatTimeBubble = drawable;
+ }
+
+ /**
+ * Get the chat time bubble.
+ * @return
+ */
+ public static Drawable getChatTimeBubble() {
+ return newDrawable(getInstance().chatTimeBubble);
+ }
+
+ /**
+ * Set the font size of the chat time text.
+ * @param size
+ */
+ public static void setChatTimeFontSize(int size) {
+ getInstance().chatTimeFontSize = size;
+ }
+
+ /**
+ * Get the font size of the chat time text.
+ * @return
+ */
+ public static int getChatTimeFontSize() {
+ return getInstance().chatTimeFontSize;
+ }
+
+ /**
+ * Set the font color of the chat time text.
+ * @param color
+ */
+ public static void setChatTimeFontColor(int color) {
+ getInstance().chatTimeFontColor = color;
+ }
+
+ /**
+ * Get the font color of the chat time text.
+ * @return
+ */
+ public static int getChatTimeFontColor() {
+ return getInstance().chatTimeFontColor;
+ }
+
+ /**
+ * Set the default avatar image.
+ * @param drawable
+ */
+ public static void setDefaultAvatarImage(Drawable drawable) {
+ getInstance().defaultAvatarImage = drawable;
+ }
+
+ /**
+ * Get the default avatar image.
+ * @return the default avatar image
+ */
+ public static Drawable getDefaultAvatarImage() {
+ return newDrawable(getInstance().defaultAvatarImage);
+ }
+
+ /**
+ * Set the radius of the avatar in the message list.
+ * @param radius
+ */
+ public static void setMessageListAvatarRadius(int radius) {
+ getInstance().avatarRadius = radius;
+ }
+
+ /**
+ * Get the radius of the avatar in the message list.
+ * @return
+ */
+ public static int getMessageListAvatarRadius() {
+ return getInstance().avatarRadius;
+ }
+
+ /**
+ * Set whether to enable the grid avatar with the group chat.The default value is true.
+ * @param enableGroupGridAvatar
+ */
+ public static void setEnableGroupGridAvatar(boolean enableGroupGridAvatar) {
+ TUIConfig.setEnableGroupGridAvatar(enableGroupGridAvatar);
+ }
+
+ /**
+ * Set the avatar size in the message list.
+ * @param size
+ */
+ public static void setMessageListAvatarSize(int size) {
+ getInstance().avatarSize = size;
+ }
+
+ /**
+ * Get the avatar size in the message list.
+ * @return
+ */
+ public static int getMessageListAvatarSize() {
+ return getInstance().avatarSize;
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/ChatInputMoreListener.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/ChatInputMoreListener.java
new file mode 100644
index 00000000..46ad8ff7
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/ChatInputMoreListener.java
@@ -0,0 +1,10 @@
+package com.tencent.qcloud.tuikit.timcommon.interfaces;
+
+import com.tencent.qcloud.tuikit.timcommon.bean.TUIMessageBean;
+import com.tencent.qcloud.tuikit.timcommon.component.interfaces.IUIKitCallback;
+
+public abstract class ChatInputMoreListener {
+ public String sendMessage(TUIMessageBean msg, IUIKitCallback callback) {
+ return null;
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/HighlightListener.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/HighlightListener.java
new file mode 100644
index 00000000..938b36d1
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/HighlightListener.java
@@ -0,0 +1,10 @@
+package com.tencent.qcloud.tuikit.timcommon.interfaces;
+
+public interface HighlightListener {
+
+ void onHighlightStart();
+
+ void onHighlightEnd();
+
+ void onHighlightUpdate(int color);
+}
\ No newline at end of file
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/ICommonMessageAdapter.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/ICommonMessageAdapter.java
new file mode 100644
index 00000000..afee786b
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/ICommonMessageAdapter.java
@@ -0,0 +1,15 @@
+package com.tencent.qcloud.tuikit.timcommon.interfaces;
+
+import com.tencent.qcloud.tuikit.timcommon.bean.TUIMessageBean;
+
+public interface ICommonMessageAdapter {
+ TUIMessageBean getItem(int position);
+
+ TUIMessageBean getFirstMessageBean();
+
+ TUIMessageBean getLastMessageBean();
+
+ void onItemRefresh(TUIMessageBean messageBean);
+
+ UserFaceUrlCache getUserFaceUrlCache();
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/IMessageProperties.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/IMessageProperties.java
new file mode 100644
index 00000000..ff839420
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/IMessageProperties.java
@@ -0,0 +1,295 @@
+package com.tencent.qcloud.tuikit.timcommon.interfaces;
+
+import android.graphics.drawable.Drawable;
+
+public interface IMessageProperties {
+
+ /**
+ *
+ * Get default avatar
+ *
+ * @return
+ */
+ int getAvatar();
+
+ /**
+ *
+ * Set the default avatar, the default is the same as the avatar on the left and right
+ *
+ * @param resId
+ */
+ void setAvatar(int resId);
+
+ /**
+ *
+ * Get avatar rounded corners
+ *
+ * @return
+ */
+ int getAvatarRadius();
+
+ /**
+ *
+ * Set avatar rounded corners
+ *
+ * @param radius
+ */
+ void setAvatarRadius(int radius);
+
+ /**
+ *
+ * Get avatar size
+ *
+ * @return
+ */
+ int[] getAvatarSize();
+
+ /**
+ *
+ * Set avatar size
+ *
+ * @param size
+ */
+ void setAvatarSize(int[] size);
+
+ /**
+ *
+ *
+ * Get nickname text size
+ *
+ * @return
+ */
+ int getNameFontSize();
+
+ /**
+ *
+ * Set nickname text size
+ *
+ * @param size
+ */
+ void setNameFontSize(int size);
+
+ /**
+ *
+ * Get nickname text color
+ *
+ * @return
+ */
+ int getNameFontColor();
+
+ /**
+ *
+ * Set nickname text color
+ *
+ * @param color
+ */
+ void setNameFontColor(int color);
+
+ /**
+ *
+ * Get the display status of the nickname on the left
+ *
+ * @return
+ */
+ int getLeftNameVisibility();
+
+ /**
+ *
+ * Set the display status of the nickname on the left
+ *
+ * @param visibility
+ */
+ void setLeftNameVisibility(int visibility);
+
+ /**
+ *
+ * Get the display status of the nickname on the right
+ *
+ * @return
+ */
+ int getRightNameVisibility();
+
+ /**
+ *
+ * Set the display status of the nickname on the right
+ *
+ * @param visibility
+ */
+ void setRightNameVisibility(int visibility);
+
+ /**
+ *
+ * Get the background of the chat bubble on the right
+ *
+ * @return
+ */
+ Drawable getRightBubble();
+
+ /**
+ *
+ * Set the background of the chat bubble on the right
+ *
+ * @param drawable
+ */
+ void setRightBubble(Drawable drawable);
+
+ /**
+ *
+ * Get the background of the left chat bubble
+ *
+ * @return
+ */
+ Drawable getLeftBubble();
+
+ /**
+ *
+ * Set the background of the left chat bubble
+ *
+ * @param drawable
+ */
+ void setLeftBubble(Drawable drawable);
+
+ /**
+ *
+ * Get chat content font size
+ *
+ * @return
+ */
+ int getChatContextFontSize();
+
+ /**
+ *
+ * Set chat content font size
+ *
+ * @param size
+ */
+ void setChatContextFontSize(int size);
+
+ /**
+ *
+ * Get the font color of the chat content on the right
+ *
+ * @return
+ */
+ int getRightChatContentFontColor();
+
+ /**
+ *
+ * Set the font color of the chat content on the right
+ *
+ * @param color
+ */
+ void setRightChatContentFontColor(int color);
+
+ /**
+ *
+ * Get the font color of the chat content on the left
+ *
+ * @return
+ */
+ int getLeftChatContentFontColor();
+
+ /**
+ *
+ * Set the font color of the chat content on the left
+ *
+ * @param color
+ */
+ void setLeftChatContentFontColor(int color);
+
+ /**
+ *
+ * Get the context of the chat time
+ *
+ * @return
+ */
+ Drawable getChatTimeBubble();
+
+ /**
+ *
+ * Set the context of the chat time
+ *
+ * @param drawable
+ */
+ void setChatTimeBubble(Drawable drawable);
+
+ /**
+ *
+ * Get the text size of the chat time
+ *
+ * @return
+ */
+ int getChatTimeFontSize();
+
+ /**
+ *
+ * Set the text size of the chat time
+ *
+ * @param size
+ */
+ void setChatTimeFontSize(int size);
+
+ /**
+ *
+ * Get the font color of chat time
+ *
+ * @return
+ */
+ int getChatTimeFontColor();
+
+ /**
+ *
+ * Set the font color of chat time
+ *
+ * @param color
+ */
+ void setChatTimeFontColor(int color);
+
+ /**
+ *
+ * Get context for chat alerts
+ *
+ * @return
+ */
+ Drawable getTipsMessageBubble();
+
+ /**
+ *
+ * Set context for chat alerts
+ *
+ * @param drawable
+ */
+ void setTipsMessageBubble(Drawable drawable);
+
+ /**
+ *
+ * Get the text size of the chat prompt message
+ *
+ * @return
+ */
+ int getTipsMessageFontSize();
+
+ /**
+ *
+ * Set the text size of the chat prompt message
+ *
+ * @param size
+ */
+ void setTipsMessageFontSize(int size);
+
+ /**
+ *
+ * Get the text color of the chat prompt message
+ *
+ * @return
+ */
+ int getTipsMessageFontColor();
+
+ /**
+ *
+ * Set the text color of the chat prompt message
+ *
+ * @param color
+ */
+ void setTipsMessageFontColor(int color);
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/OnChatPopActionClickListener.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/OnChatPopActionClickListener.java
new file mode 100644
index 00000000..868c7169
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/OnChatPopActionClickListener.java
@@ -0,0 +1,25 @@
+package com.tencent.qcloud.tuikit.timcommon.interfaces;
+
+import com.tencent.qcloud.tuikit.timcommon.bean.TUIMessageBean;
+
+public abstract class OnChatPopActionClickListener {
+ public void onCopyClick(TUIMessageBean msg) {}
+
+ public void onSendMessageClick(TUIMessageBean msg, boolean retry) {}
+
+ public void onDeleteMessageClick(TUIMessageBean msg) {}
+
+ public void onRevokeMessageClick(TUIMessageBean msg) {}
+
+ public void onMultiSelectMessageClick(TUIMessageBean msg) {}
+
+ public void onForwardMessageClick(TUIMessageBean msg) {}
+
+ public void onReplyMessageClick(TUIMessageBean msg) {}
+
+ public void onQuoteMessageClick(TUIMessageBean msg) {}
+
+ public void onInfoMessageClick(TUIMessageBean msg) {}
+
+ public void onSpeakerModeSwitchClick(TUIMessageBean msg) {}
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/OnFaceInputListener.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/OnFaceInputListener.java
new file mode 100644
index 00000000..a9072547
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/OnFaceInputListener.java
@@ -0,0 +1,14 @@
+package com.tencent.qcloud.tuikit.timcommon.interfaces;
+
+import com.tencent.qcloud.tuikit.timcommon.bean.ChatFace;
+
+public interface OnFaceInputListener {
+
+ void onEmojiClicked(String emojiKey);
+
+ void onDeleteClicked();
+
+ void onSendClicked();
+
+ void onFaceClicked(ChatFace chatFace);
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/OnItemClickListener.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/OnItemClickListener.java
new file mode 100644
index 00000000..74a207ed
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/OnItemClickListener.java
@@ -0,0 +1,29 @@
+package com.tencent.qcloud.tuikit.timcommon.interfaces;
+
+import android.view.View;
+
+import com.tencent.qcloud.tuikit.timcommon.bean.TUIMessageBean;
+
+public abstract class OnItemClickListener {
+ public void onMessageLongClick(View view, TUIMessageBean messageBean) {}
+
+ public void onMessageClick(View view, TUIMessageBean messageBean) {}
+
+ public void onUserIconClick(View view, TUIMessageBean messageBean) {}
+
+ public void onUserIconLongClick(View view, TUIMessageBean messageBean) {}
+
+ public void onReEditRevokeMessage(View view, TUIMessageBean messageBean) {}
+
+ public void onRecallClick(View view, TUIMessageBean messageBean) {}
+
+ public void onReplyMessageClick(View view, TUIMessageBean messageBean) {}
+
+ public void onReplyDetailClick(TUIMessageBean messageBean) {}
+
+ public void onSendFailBtnClick(View view, TUIMessageBean messageBean) {}
+
+ public void onTextSelected(View view, int position, TUIMessageBean messageBean) {}
+
+ public void onMessageReadStatusClick(View view, TUIMessageBean messageBean) {}
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/UserFaceUrlCache.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/UserFaceUrlCache.java
new file mode 100644
index 00000000..568c54f9
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/UserFaceUrlCache.java
@@ -0,0 +1,8 @@
+package com.tencent.qcloud.tuikit.timcommon.interfaces;
+
+public interface UserFaceUrlCache {
+
+ String getCachedFaceUrl(String userID);
+
+ void pushFaceUrl(String userID, String faceUrl);
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/MessageBaseHolder.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/MessageBaseHolder.java
new file mode 100644
index 00000000..0fcd3816
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/MessageBaseHolder.java
@@ -0,0 +1,240 @@
+package com.tencent.qcloud.tuikit.timcommon.minimalistui.widget.message;
+
+import android.content.Context;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import androidx.recyclerview.widget.RecyclerView;
+import com.tencent.qcloud.tuicore.TUIConfig;
+import com.tencent.qcloud.tuicore.TUIThemeManager;
+import com.tencent.qcloud.tuikit.timcommon.R;
+import com.tencent.qcloud.tuikit.timcommon.bean.TUIMessageBean;
+import com.tencent.qcloud.tuikit.timcommon.component.highlight.HighlightPresenter;
+import com.tencent.qcloud.tuikit.timcommon.config.minimalistui.TUIConfigMinimalist;
+import com.tencent.qcloud.tuikit.timcommon.interfaces.HighlightListener;
+import com.tencent.qcloud.tuikit.timcommon.interfaces.ICommonMessageAdapter;
+import com.tencent.qcloud.tuikit.timcommon.interfaces.OnItemClickListener;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+
+public abstract class MessageBaseHolder extends RecyclerView.ViewHolder {
+ public ICommonMessageAdapter mAdapter;
+ protected OnItemClickListener onItemClickListener;
+
+ public FrameLayout msgContentFrame;
+ public LinearLayout msgArea;
+ public LinearLayout msgAreaAndReply;
+ public FrameLayout reactionArea;
+ public CheckBox mMutiSelectCheckBox;
+ public View mContentLayout;
+
+ public TextView chatTimeText;
+
+ protected boolean floatMode = false;
+
+ protected boolean isLayoutOnStart = true;
+ private HighlightListener highlightListener;
+ protected TUIMessageBean currentMessageBean;
+
+ public MessageBaseHolder(View itemView) {
+ super(itemView);
+ msgContentFrame = itemView.findViewById(R.id.msg_content_fl);
+ reactionArea = itemView.findViewById(R.id.message_reaction_area);
+ msgArea = itemView.findViewById(R.id.msg_area);
+ msgAreaAndReply = itemView.findViewById(R.id.msg_area_and_reply);
+ mMutiSelectCheckBox = itemView.findViewById(R.id.select_checkbox);
+ chatTimeText = itemView.findViewById(R.id.message_top_time_tv);
+ mContentLayout = itemView.findViewById(R.id.message_content_layout);
+ initVariableLayout();
+ }
+
+ public abstract int getVariableLayout();
+
+ private void setVariableLayout(int resId) {
+ if (msgContentFrame.getChildCount() == 0) {
+ View.inflate(itemView.getContext(), resId, msgContentFrame);
+ }
+ }
+
+ private void initVariableLayout() {
+ if (getVariableLayout() != 0) {
+ setVariableLayout(getVariableLayout());
+ }
+ }
+
+ public void setAdapter(ICommonMessageAdapter adapter) {
+ mAdapter = adapter;
+ }
+
+ public void setOnItemClickListener(OnItemClickListener listener) {
+ this.onItemClickListener = listener;
+ }
+
+ public OnItemClickListener getOnItemClickListener() {
+ return this.onItemClickListener;
+ }
+
+ public void layoutViews(final TUIMessageBean msg, final int position) {
+ currentMessageBean = msg;
+ registerHighlightListener(msg.getId());
+
+ setChatTimeStyle();
+
+ if (position > 1) {
+ TUIMessageBean last = mAdapter.getItem(position - 1);
+ if (last != null) {
+ if (msg.getMessageTime() - last.getMessageTime() >= 5 * 60) {
+ chatTimeText.setVisibility(View.VISIBLE);
+ chatTimeText.setText(getTimeFormatText(new Date(msg.getMessageTime() * 1000)));
+ } else {
+ chatTimeText.setVisibility(View.GONE);
+ }
+ }
+ } else {
+ chatTimeText.setVisibility(View.VISIBLE);
+ chatTimeText.setText(getTimeFormatText(new Date(msg.getMessageTime() * 1000)));
+ }
+ }
+
+ private void setChatTimeStyle() {
+ Drawable chatTimeBubble = TUIConfigMinimalist.getChatTimeBubble();
+ if (chatTimeBubble != null) {
+ chatTimeText.setBackground(chatTimeBubble);
+ }
+ int chatTimeFontColor = TUIConfigMinimalist.getChatTimeFontColor();
+ if (chatTimeFontColor != TUIConfigMinimalist.UNDEFINED) {
+ chatTimeText.setTextColor(chatTimeFontColor);
+ }
+ int chatTimeFontSize = TUIConfigMinimalist.getChatTimeFontSize();
+ if (chatTimeFontSize != TUIConfigMinimalist.UNDEFINED) {
+ chatTimeText.setTextSize(chatTimeFontSize);
+ }
+ }
+
+ private void registerHighlightListener(String msgID) {
+ if (highlightListener == null) {
+ highlightListener = new HighlightListener() {
+ @Override
+ public void onHighlightStart() {}
+
+ @Override
+ public void onHighlightEnd() {
+ clearHighLightBackground();
+ }
+
+ @Override
+ public void onHighlightUpdate(int color) {
+ setHighLightBackground(color);
+ }
+ };
+ }
+ HighlightPresenter.registerHighlightListener(msgID, highlightListener);
+ }
+
+ public void onRecycled() {
+ if (currentMessageBean != null) {
+ HighlightPresenter.unregisterHighlightListener(currentMessageBean.getId());
+ }
+ }
+
+ public static String getTimeFormatText(Date date) {
+ if (date == null) {
+ return "";
+ }
+ Context context = TUIConfig.getAppContext();
+ Locale locale;
+ if (context == null) {
+ locale = Locale.getDefault();
+ } else {
+ locale = TUIThemeManager.getInstance().getLocale(context);
+ }
+ String timeText;
+ Calendar dayCalendar = Calendar.getInstance();
+ dayCalendar.set(Calendar.HOUR_OF_DAY, 0);
+ dayCalendar.set(Calendar.MINUTE, 0);
+ dayCalendar.set(Calendar.SECOND, 0);
+ dayCalendar.set(Calendar.MILLISECOND, 0);
+ Calendar weekCalendar = Calendar.getInstance();
+ weekCalendar.setFirstDayOfWeek(Calendar.SUNDAY);
+ weekCalendar.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY);
+ weekCalendar.set(Calendar.HOUR_OF_DAY, 0);
+ weekCalendar.set(Calendar.MINUTE, 0);
+ weekCalendar.set(Calendar.SECOND, 0);
+ weekCalendar.set(Calendar.MILLISECOND, 0);
+ Calendar yearCalendar = Calendar.getInstance();
+ yearCalendar.set(Calendar.DAY_OF_YEAR, 1);
+ yearCalendar.set(Calendar.HOUR_OF_DAY, 0);
+ yearCalendar.set(Calendar.MINUTE, 0);
+ yearCalendar.set(Calendar.SECOND, 0);
+ yearCalendar.set(Calendar.MILLISECOND, 0);
+ long dayStartTimeInMillis = dayCalendar.getTimeInMillis();
+ long weekStartTimeInMillis = weekCalendar.getTimeInMillis();
+ long yearStartTimeInMillis = yearCalendar.getTimeInMillis();
+ long outTimeMillis = date.getTime();
+ if (outTimeMillis < yearStartTimeInMillis) {
+ timeText = String.format(Locale.US, "%1$tY/%1$tm/%1$td", date);
+ } else if (outTimeMillis < weekStartTimeInMillis) {
+ timeText = String.format(Locale.US, "%1$tm/%1$td", date);
+ } else if (outTimeMillis < dayStartTimeInMillis) {
+ timeText = String.format(locale, "%tA", date);
+ } else {
+ timeText = context.getResources().getString(R.string.chat_time_today);
+ }
+ return timeText;
+ }
+
+ public void setFloatMode(boolean floatMode) {
+ this.floatMode = floatMode;
+ }
+
+ protected boolean isShowAvatar(TUIMessageBean messageBean) {
+ return false;
+ }
+
+ public void setMessageBubbleZeroPadding() {
+ if (msgArea == null) {
+ return;
+ }
+ msgArea.setPaddingRelative(0, 0, 0, 0);
+ }
+
+ public void setMessageBubbleBackground(int resID) {
+ if (msgArea == null) {
+ return;
+ }
+ msgArea.setBackgroundResource(resID);
+ }
+
+ public void setMessageBubbleBackground(Drawable drawable) {
+ if (msgArea == null) {
+ return;
+ }
+ msgArea.setBackground(drawable);
+ }
+
+ public Drawable getMessageBubbleBackground() {
+ if (msgArea == null) {
+ return null;
+ }
+ return msgArea.getBackground();
+ }
+
+ public void setHighLightBackground(int color) {
+ Drawable drawable = getMessageBubbleBackground();
+ if (drawable != null) {
+ drawable.setColorFilter(color, PorterDuff.Mode.SRC_IN);
+ }
+ }
+
+ public void clearHighLightBackground() {
+ Drawable drawable = getMessageBubbleBackground();
+ if (drawable != null) {
+ drawable.setColorFilter(null);
+ }
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/MessageContentHolder.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/MessageContentHolder.java
new file mode 100644
index 00000000..72f3d41e
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/MessageContentHolder.java
@@ -0,0 +1,714 @@
+package com.tencent.qcloud.tuikit.timcommon.minimalistui.widget.message;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.RecyclerView;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.RequestBuilder;
+import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
+import com.tencent.imsdk.v2.V2TIMManager;
+import com.tencent.imsdk.v2.V2TIMMessage;
+import com.tencent.imsdk.v2.V2TIMUserFullInfo;
+import com.tencent.imsdk.v2.V2TIMValueCallback;
+import com.tencent.qcloud.tuicore.TUIConstants;
+import com.tencent.qcloud.tuicore.TUICore;
+import com.tencent.qcloud.tuicore.TUIThemeManager;
+import com.tencent.qcloud.tuikit.timcommon.R;
+import com.tencent.qcloud.tuikit.timcommon.bean.MessageRepliesBean;
+import com.tencent.qcloud.tuikit.timcommon.bean.TUIMessageBean;
+import com.tencent.qcloud.tuikit.timcommon.component.UnreadCountTextView;
+import com.tencent.qcloud.tuikit.timcommon.config.minimalistui.TUIConfigMinimalist;
+import com.tencent.qcloud.tuikit.timcommon.util.DateTimeUtil;
+import com.tencent.qcloud.tuikit.timcommon.util.ScreenUtil;
+import com.tencent.qcloud.tuikit.timcommon.util.TUIUtil;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public abstract class MessageContentHolder extends MessageBaseHolder {
+ protected static final int READ_STATUS_UNREAD = 1;
+ protected static final int READ_STATUS_PART_READ = 2;
+ protected static final int READ_STATUS_ALL_READ = 3;
+ protected static final int READ_STATUS_HIDE = 4;
+ protected static final int READ_STATUS_SENDING = 5;
+
+ public ImageView leftUserIcon;
+ public ImageView rightUserIcon;
+ public TextView usernameText;
+ public LinearLayout msgContentLinear;
+ public ImageView messageStatusImage;
+ public ImageView fileStatusImage;
+ public UnreadCountTextView unreadAudioText;
+ public LinearLayout extraInfoArea;
+ protected FrameLayout bottomContentFrameLayout;
+
+ public boolean isForwardMode = false;
+ public boolean isMessageDetailMode = false;
+ public boolean isMultiSelectMode = false;
+ public boolean isOptimize = true;
+ public boolean isShowSelfAvatar = false;
+ protected boolean isShowAvatar = true;
+
+ protected TimeInLineTextLayout timeInLineTextLayout;
+ protected MinimalistMessageLayout rootLayout;
+ protected ReplyPreviewView replyPreviewView;
+
+ private List mDataSource = new ArrayList<>();
+
+ // Whether to display the bottom content. The merged-forwarded message details activity does not display the bottom content.
+ protected boolean isNeedShowBottom = true;
+ protected boolean isShowRead = false;
+ private Fragment fragment;
+ private RecyclerView recyclerView;
+
+ public MessageContentHolder(View itemView) {
+ super(itemView);
+ rootLayout = (MinimalistMessageLayout) itemView;
+ leftUserIcon = itemView.findViewById(R.id.left_user_icon_view);
+ rightUserIcon = itemView.findViewById(R.id.right_user_icon_view);
+ usernameText = itemView.findViewById(R.id.user_name_tv);
+ msgContentLinear = itemView.findViewById(R.id.msg_content_ll);
+ messageStatusImage = itemView.findViewById(R.id.message_status_iv);
+ fileStatusImage = itemView.findViewById(R.id.file_status_iv);
+ unreadAudioText = itemView.findViewById(R.id.unread_audio_text);
+ replyPreviewView = itemView.findViewById(R.id.msg_reply_preview);
+ extraInfoArea = itemView.findViewById(R.id.extra_info_area);
+ bottomContentFrameLayout = itemView.findViewById(R.id.bottom_content_fl);
+ }
+
+ public void setFragment(Fragment fragment) {
+ this.fragment = fragment;
+ }
+
+ public void setRecyclerView(RecyclerView recyclerView) {
+ this.recyclerView = recyclerView;
+ }
+
+ public void setDataSource(List dataSource) {
+ if (dataSource == null || dataSource.isEmpty()) {
+ mDataSource = null;
+ }
+
+ List mediaSource = new ArrayList<>();
+ for (TUIMessageBean messageBean : dataSource) {
+ int type = messageBean.getMsgType();
+ if (type == V2TIMMessage.V2TIM_ELEM_TYPE_IMAGE || type == V2TIMMessage.V2TIM_ELEM_TYPE_VIDEO) {
+ mediaSource.add(messageBean);
+ }
+ }
+ mDataSource = mediaSource;
+ }
+
+ public List getDataSource() {
+ return mDataSource;
+ }
+
+ @Override
+ public void layoutViews(final TUIMessageBean msg, final int position) {
+ Context context = itemView.getContext();
+ if (TUIUtil.isActivityDestroyed(context)) {
+ return;
+ }
+
+ super.layoutViews(msg, position);
+ setLayoutAlignment(msg);
+ setIsShowAvatar(msg, position);
+ setMessageGravity();
+ setUserNameText(msg);
+ setMessageBubbleBackground();
+ setMessageStatusImage(msg);
+ setMessageTimeVisibility();
+ setAvatarVisibility();
+ setEventListener(msg);
+
+ msgContentLinear.setVisibility(View.VISIBLE);
+
+ if (!isForwardMode && !isMessageDetailMode) {
+ setTimeInLineStatus(msg);
+ setShowReadStatusClickListener(msg);
+ }
+ if (timeInLineTextLayout != null && timeInLineTextLayout.getTextView() != null) {
+ if (isMultiSelectMode) {
+ timeInLineTextLayout.getTextView().setActivated(false);
+ } else {
+ timeInLineTextLayout.getTextView().setActivated(true);
+ }
+ }
+
+ if (timeInLineTextLayout != null) {
+ timeInLineTextLayout.setTimeText(DateTimeUtil.getHMTimeString(new Date(msg.getMessageTime() * 1000)));
+ }
+
+ extraInfoArea.setVisibility(View.GONE);
+ setReplyContent(msg);
+ setReactContent(msg);
+ if (isNeedShowBottom) {
+ setBottomContent(msg);
+ }
+ setMessageBubbleDefaultPadding();
+ if (floatMode) {
+ itemView.setPaddingRelative(0, 0, 0, 0);
+ leftUserIcon.setVisibility(View.GONE);
+ rightUserIcon.setVisibility(View.GONE);
+ usernameText.setVisibility(View.GONE);
+ replyPreviewView.setVisibility(View.GONE);
+ reactionArea.setVisibility(View.GONE);
+ }
+ if (isMessageDetailMode) {
+ replyPreviewView.setVisibility(View.GONE);
+ }
+
+ optimizePadding(position, msg);
+ loadAvatar(msg);
+ layoutVariableViews(msg, position);
+ }
+
+ private void setEventListener(TUIMessageBean msg) {
+ if (!isForwardMode && !isMessageDetailMode) {
+ if (onItemClickListener != null) {
+ msgContentFrame.setOnLongClickListener(new View.OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View v) {
+ onItemClickListener.onMessageLongClick(msgArea, msg);
+ return true;
+ }
+ });
+
+ msgArea.setOnLongClickListener(new View.OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View v) {
+ onItemClickListener.onMessageLongClick(msgArea, msg);
+ return true;
+ }
+ });
+
+ leftUserIcon.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ onItemClickListener.onUserIconClick(view, msg);
+ }
+ });
+ leftUserIcon.setOnLongClickListener(new View.OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View view) {
+ onItemClickListener.onUserIconLongClick(view, msg);
+ return true;
+ }
+ });
+ rightUserIcon.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ onItemClickListener.onUserIconClick(view, msg);
+ }
+ });
+ }
+
+ if (msg.getStatus() != TUIMessageBean.MSG_STATUS_SEND_FAIL) {
+ msgContentFrame.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (onItemClickListener != null) {
+ onItemClickListener.onMessageClick(msgContentFrame, msg);
+ }
+ }
+ });
+ }
+ }
+ }
+
+ private void setMessageTimeVisibility() {
+ if (isForwardMode || floatMode) {
+ chatTimeText.setVisibility(View.GONE);
+ }
+ }
+
+ private void setLayoutAlignment(TUIMessageBean msg) {
+ if (isForwardMode || isMessageDetailMode) {
+ isLayoutOnStart = true;
+ } else {
+ if (msg.isSelf()) {
+ isLayoutOnStart = false;
+ } else {
+ isLayoutOnStart = true;
+ }
+ }
+
+ if (isForwardMode || isMessageDetailMode) {
+ rootLayout.setIsStart(true);
+ msgContentLinear.removeView(msgAreaAndReply);
+ msgContentLinear.addView(msgAreaAndReply);
+ } else {
+ if (msg.isSelf()) {
+ msgContentLinear.removeView(msgAreaAndReply);
+ msgContentLinear.addView(msgAreaAndReply);
+ } else {
+ msgContentLinear.removeView(msgAreaAndReply);
+ msgContentLinear.addView(msgAreaAndReply, 0);
+ }
+ }
+ setGravity(isLayoutOnStart);
+ rootLayout.setIsStart(isLayoutOnStart);
+ }
+
+ private void setUserNameText(TUIMessageBean msg) {
+ if (isForwardMode || isMessageDetailMode) {
+ usernameText.setVisibility(View.GONE);
+ } else {
+ if (msg.isSelf()) {
+ usernameText.setVisibility(View.GONE);
+ } else {
+ if (msg.isGroup()) {
+ usernameText.setVisibility(View.GONE);
+ } else {
+ usernameText.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ usernameText.setText(msg.getUserDisplayName());
+ }
+
+ public void setIsShowAvatar(TUIMessageBean msg, int position) {
+ isShowAvatar = true;
+ if (mAdapter == null) {
+ return;
+ }
+ if (isMessageDetailMode || !isOptimize) {
+ return;
+ }
+ TUIMessageBean nextMessage = mAdapter.getItem(position + 1);
+ if (nextMessage != null) {
+ if (TextUtils.equals(msg.getSender(), nextMessage.getSender())) {
+ boolean longPeriod = nextMessage.getMessageTime() - msg.getMessageTime() >= 5 * 60;
+ if (!isShowAvatar(nextMessage) && nextMessage.getStatus() != TUIMessageBean.MSG_STATUS_REVOKE && !longPeriod) {
+ isShowAvatar = false;
+ }
+ }
+ }
+ }
+
+ public void setMessageBubbleBackground() {
+ if (!TUIConfigMinimalist.isEnableMessageBubbleStyle()) {
+ setMessageBubbleBackground(null);
+ return;
+ }
+ Drawable sendBubble = TUIConfigMinimalist.getSendBubbleBackground();
+ Drawable receiveBubble = TUIConfigMinimalist.getReceiveBubbleBackground();
+ Drawable sendLastBubble = TUIConfigMinimalist.getSendLastBubbleBackground();
+ Drawable receiveLastBubble = TUIConfigMinimalist.getReceiveLastBubbleBackground();
+ if (isShowAvatar) {
+ if (isLayoutOnStart) {
+ if (receiveLastBubble != null) {
+ setMessageBubbleBackground(receiveLastBubble);
+ } else {
+ setMessageBubbleBackground(R.drawable.chat_message_popup_stroke_border_left);
+ }
+ } else {
+ if (sendLastBubble != null) {
+ setMessageBubbleBackground(sendLastBubble);
+ } else {
+ setMessageBubbleBackground(R.drawable.chat_message_popup_fill_border_right);
+ }
+ }
+ } else {
+ if (isLayoutOnStart) {
+ if (receiveBubble != null) {
+ setMessageBubbleBackground(receiveBubble);
+ } else {
+ setMessageBubbleBackground(R.drawable.chat_message_popup_stroke_border);
+ }
+ } else {
+ if (sendBubble != null) {
+ setMessageBubbleBackground(sendBubble);
+ } else {
+ setMessageBubbleBackground(R.drawable.chat_message_popup_fill_border);
+ }
+ }
+ }
+ }
+
+ public void setMessageStatusImage(TUIMessageBean msg) {
+ if (isForwardMode || isMessageDetailMode || floatMode) {
+ messageStatusImage.setVisibility(View.GONE);
+ } else {
+ if (msg.hasRiskContent()) {
+ messageStatusImage.setVisibility(View.VISIBLE);
+ } else if (msg.getStatus() == TUIMessageBean.MSG_STATUS_SEND_FAIL) {
+ messageStatusImage.setVisibility(View.VISIBLE);
+ messageStatusImage.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (onItemClickListener != null) {
+ onItemClickListener.onSendFailBtnClick(messageStatusImage, msg);
+ }
+ }
+ });
+ } else {
+ messageStatusImage.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ public void setBottomContent(TUIMessageBean msg) {
+ HashMap param = new HashMap<>();
+ param.put(TUIConstants.TUIChat.MESSAGE_BEAN, msg);
+ param.put(TUIConstants.TUIChat.CHAT_RECYCLER_VIEW, recyclerView);
+ param.put(TUIConstants.TUIChat.FRAGMENT, fragment);
+
+ TUICore.raiseExtension(TUIConstants.TUIChat.Extension.MessageBottom.MINIMALIST_EXTENSION_ID, bottomContentFrameLayout, param);
+ }
+
+ private void loadAvatar(TUIMessageBean msg) {
+ Drawable drawable = TUIConfigMinimalist.getDefaultAvatarImage();
+ if (drawable != null) {
+ setupAvatar(drawable);
+ return;
+ }
+
+ if (msg.isUseMsgReceiverAvatar() && mAdapter != null) {
+ String cachedFaceUrl = mAdapter.getUserFaceUrlCache().getCachedFaceUrl(msg.getSender());
+ if (cachedFaceUrl == null) {
+ List idList = new ArrayList<>();
+ idList.add(msg.getSender());
+ V2TIMManager.getInstance().getUsersInfo(idList, new V2TIMValueCallback>() {
+ @Override
+ public void onSuccess(List v2TIMUserFullInfos) {
+ if (v2TIMUserFullInfos == null || v2TIMUserFullInfos.isEmpty()) {
+ return;
+ }
+ V2TIMUserFullInfo userInfo = v2TIMUserFullInfos.get(0);
+ String faceUrl = userInfo.getFaceUrl();
+ if (TextUtils.isEmpty(userInfo.getFaceUrl())) {
+ faceUrl = "";
+ }
+ mAdapter.getUserFaceUrlCache().pushFaceUrl(userInfo.getUserID(), faceUrl);
+ mAdapter.onItemRefresh(msg);
+ }
+
+ @Override
+ public void onError(int code, String desc) {
+ setupAvatar("");
+ }
+ });
+ } else {
+ setupAvatar(cachedFaceUrl);
+ }
+ } else {
+ setupAvatar(msg.getFaceUrl());
+ }
+ }
+
+ private void setupAvatar(Object faceUrl) {
+ int avatarSize = TUIConfigMinimalist.getMessageListAvatarSize();
+ if (avatarSize == TUIConfigMinimalist.UNDEFINED) {
+ avatarSize = ScreenUtil.dip2px(32);
+ }
+ ViewGroup.LayoutParams params = leftUserIcon.getLayoutParams();
+ params.width = avatarSize;
+ if (leftUserIcon.getVisibility() == View.INVISIBLE) {
+ params.height = 1;
+ } else {
+ params.height = avatarSize;
+ }
+ leftUserIcon.setLayoutParams(params);
+
+ params = rightUserIcon.getLayoutParams();
+ params.width = avatarSize;
+ if (rightUserIcon.getVisibility() == View.INVISIBLE) {
+ params.height = 1;
+ } else {
+ params.height = avatarSize;
+ }
+ rightUserIcon.setLayoutParams(params);
+
+ int radius = ScreenUtil.dip2px(100);
+ if (TUIConfigMinimalist.getMessageListAvatarRadius() != TUIConfigMinimalist.UNDEFINED) {
+ radius = TUIConfigMinimalist.getMessageListAvatarRadius();
+ }
+ ImageView renderedView;
+ if (isLayoutOnStart) {
+ renderedView = leftUserIcon;
+ } else {
+ renderedView = rightUserIcon;
+ }
+
+ RequestBuilder errorRequestBuilder = Glide.with(itemView.getContext())
+ .load(TUIThemeManager.getAttrResId(leftUserIcon.getContext(), com.tencent.qcloud.tuikit.timcommon.R.attr.core_default_user_icon))
+ .placeholder(TUIThemeManager.getAttrResId(leftUserIcon.getContext(), com.tencent.qcloud.tuikit.timcommon.R.attr.core_default_user_icon))
+ .transform(new RoundedCorners(radius));
+
+ Glide.with(itemView.getContext())
+ .load(faceUrl)
+ .transform(new RoundedCorners(radius))
+ .error(errorRequestBuilder)
+ .into(renderedView);
+ }
+
+ private void setAvatarVisibility() {
+ if (isShowAvatar) {
+ if (isLayoutOnStart) {
+ leftUserIcon.setVisibility(View.VISIBLE);
+ rightUserIcon.setVisibility(View.GONE);
+ } else {
+ leftUserIcon.setVisibility(View.GONE);
+ if (isShowSelfAvatar) {
+ rightUserIcon.setVisibility(View.VISIBLE);
+ } else {
+ rightUserIcon.setVisibility(View.GONE);
+ }
+ }
+ } else {
+ leftUserIcon.setVisibility(View.INVISIBLE);
+ if (isShowSelfAvatar) {
+ rightUserIcon.setVisibility(View.INVISIBLE);
+ } else {
+ rightUserIcon.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ private void optimizePadding(int position, TUIMessageBean messageBean) {
+ if (mAdapter == null) {
+ return;
+ }
+ if (isMessageDetailMode || !isOptimize) {
+ return;
+ }
+
+ TUIMessageBean nextMessage = mAdapter.getItem(position + 1);
+ int horizontalPadding = ScreenUtil.dip2px(16);
+ int verticalPadding = ScreenUtil.dip2px(25f);
+ if (!isShowAvatar) {
+ horizontalPadding = ScreenUtil.dip2px(16);
+ verticalPadding = ScreenUtil.dip2px(2);
+ }
+ if (nextMessage != null) {
+ rootLayout.setPaddingRelative(horizontalPadding, 0, horizontalPadding, verticalPadding);
+ } else {
+ rootLayout.setPaddingRelative(horizontalPadding, 0, horizontalPadding, ScreenUtil.dip2px(5));
+ }
+ optimizeMessageContent(isShowAvatar);
+ }
+
+ protected void optimizeMessageContent(boolean isShowAvatar) {}
+
+ private void setMessageGravity() {
+ if (isLayoutOnStart) {
+ msgContentLinear.setGravity(Gravity.START | Gravity.BOTTOM);
+ extraInfoArea.setGravity(Gravity.START);
+ } else {
+ msgContentLinear.setGravity(Gravity.END | Gravity.BOTTOM);
+ extraInfoArea.setGravity(Gravity.END);
+ }
+ }
+
+ private void setTimeInLineStatus(TUIMessageBean msg) {
+ if (isShowRead) {
+ if (msg.isSelf()) {
+ if (TUIMessageBean.MSG_STATUS_SEND_SUCCESS == msg.getStatus()) {
+ if (!msg.isNeedReadReceipt()) {
+ setReadStatus(READ_STATUS_HIDE);
+ } else {
+ processReadStatus(msg);
+ }
+ } else if (TUIMessageBean.MSG_STATUS_SENDING == msg.getStatus()) {
+ setReadStatus(READ_STATUS_SENDING);
+ } else {
+ setReadStatus(READ_STATUS_HIDE);
+ }
+ } else {
+ setReadStatus(READ_STATUS_HIDE);
+ }
+ }
+ }
+
+ protected void setOnTimeInLineTextClickListener(TUIMessageBean messageBean) {
+ timeInLineTextLayout.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (onItemClickListener != null) {
+ onItemClickListener.onMessageClick(msgArea, messageBean);
+ }
+ }
+ });
+ timeInLineTextLayout.getTextView().setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (onItemClickListener != null) {
+ onItemClickListener.onMessageClick(msgArea, messageBean);
+ }
+ }
+ });
+ timeInLineTextLayout.setOnLongClickListener(new View.OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View v) {
+ if (onItemClickListener != null) {
+ onItemClickListener.onMessageLongClick(msgArea, messageBean);
+ }
+ return true;
+ }
+ });
+ timeInLineTextLayout.getTextView().setOnLongClickListener(new View.OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View v) {
+ if (onItemClickListener != null) {
+ onItemClickListener.onMessageLongClick(msgArea, messageBean);
+ }
+ return true;
+ }
+ });
+ }
+
+ protected void setReadStatus(int readStatus) {
+ if (timeInLineTextLayout != null) {
+ int statusIconResID = 0;
+ switch (readStatus) {
+ case READ_STATUS_UNREAD: {
+ statusIconResID = R.drawable.chat_minimalist_message_status_send_no_read;
+ break;
+ }
+ case READ_STATUS_PART_READ: {
+ statusIconResID = R.drawable.chat_minimalist_message_status_send_part_read;
+ break;
+ }
+ case READ_STATUS_ALL_READ: {
+ statusIconResID = R.drawable.chat_minimalist_message_status_send_all_read;
+ break;
+ }
+ case READ_STATUS_SENDING: {
+ statusIconResID = R.drawable.chat_minimalist_status_loading_anim;
+ break;
+ }
+ default: {
+ }
+ }
+ timeInLineTextLayout.setStatusIcon(statusIconResID);
+ }
+ }
+
+ protected void setMessageBubbleDefaultPadding() {
+ // after setting background, the padding will be reset
+ int paddingHorizontal = itemView.getResources().getDimensionPixelSize(R.dimen.chat_minimalist_message_area_padding_left_right);
+ int paddingVertical = itemView.getResources().getDimensionPixelSize(R.dimen.chat_minimalist_message_area_padding_top_bottom);
+ msgArea.setPaddingRelative(paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical);
+ }
+
+ protected void setGravity(boolean isStart) {
+ int gravity = isStart ? Gravity.START : Gravity.END;
+ msgAreaAndReply.setGravity(gravity);
+ ViewGroup.LayoutParams layoutParams = msgContentFrame.getLayoutParams();
+ if (layoutParams instanceof FrameLayout.LayoutParams) {
+ ((FrameLayout.LayoutParams) layoutParams).gravity = gravity;
+ } else if (layoutParams instanceof LinearLayout.LayoutParams) {
+ ((LinearLayout.LayoutParams) layoutParams).gravity = gravity;
+ }
+ msgContentFrame.setLayoutParams(layoutParams);
+ }
+
+ private void setReplyContent(TUIMessageBean messageBean) {
+ MessageRepliesBean messageRepliesBean = messageBean.getMessageRepliesBean();
+ if (messageRepliesBean != null && messageRepliesBean.getRepliesSize() > 0) {
+ extraInfoArea.setVisibility(View.VISIBLE);
+ replyPreviewView.setVisibility(View.VISIBLE);
+ replyPreviewView.setMessageRepliesBean(messageRepliesBean);
+ replyPreviewView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (onItemClickListener != null) {
+ onItemClickListener.onReplyDetailClick(messageBean);
+ }
+ }
+ });
+ } else {
+ replyPreviewView.setVisibility(View.GONE);
+ }
+ }
+
+ private void setReactContent(TUIMessageBean messageBean) {
+ Map param = new HashMap<>();
+ param.put(TUIConstants.TUIChat.Extension.MessageReactPreviewExtension.MESSAGE, messageBean);
+ param.put(TUIConstants.TUIChat.Extension.MessageReactPreviewExtension.VIEW_TYPE,
+ TUIConstants.TUIChat.Extension.MessageReactPreviewExtension.VIEW_TYPE_MINIMALIST);
+ TUICore.raiseExtension(TUIConstants.TUIChat.Extension.MessageReactPreviewExtension.EXTENSION_ID, reactionArea, param);
+ }
+
+ private void processReadStatus(TUIMessageBean msg) {
+ if (msg.isGroup()) {
+ if (msg.isAllRead()) {
+ setReadStatus(READ_STATUS_ALL_READ);
+ } else if (msg.isUnread()) {
+ setReadStatus(READ_STATUS_UNREAD);
+ } else {
+ long readCount = msg.getReadCount();
+ if (readCount > 0) {
+ setReadStatus(READ_STATUS_PART_READ);
+ }
+ }
+ } else {
+ if (msg.isPeerRead()) {
+ setReadStatus(READ_STATUS_ALL_READ);
+ } else {
+ setReadStatus(READ_STATUS_UNREAD);
+ }
+ }
+ }
+
+ private void setShowReadStatusClickListener(TUIMessageBean messageBean) {
+ if (timeInLineTextLayout != null) {
+ timeInLineTextLayout.setOnStatusAreaClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (onItemClickListener != null) {
+ onItemClickListener.onMessageReadStatusClick(v, messageBean);
+ }
+ }
+ });
+ timeInLineTextLayout.setOnStatusAreaLongClickListener(new View.OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View v) {
+ if (onItemClickListener != null) {
+ onItemClickListener.onMessageLongClick(msgArea, messageBean);
+ }
+ return true;
+ }
+ });
+
+ timeInLineTextLayout.setOnLongClickListener(new View.OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View v) {
+ if (onItemClickListener != null) {
+ onItemClickListener.onMessageLongClick(msgArea, messageBean);
+ }
+ return true;
+ }
+ });
+ }
+ }
+
+ public abstract void layoutVariableViews(final TUIMessageBean msg, final int position);
+
+ public void onRecycled() {
+ super.onRecycled();
+ }
+
+ public void setNeedShowBottom(boolean needShowBottom) {
+ isNeedShowBottom = needShowBottom;
+ }
+
+ public void setShowRead(boolean showRead) {
+ isShowRead = showRead;
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/MessageStatusTimeView.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/MessageStatusTimeView.java
new file mode 100644
index 00000000..dcbb310b
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/MessageStatusTimeView.java
@@ -0,0 +1,62 @@
+package com.tencent.qcloud.tuikit.timcommon.minimalistui.widget.message;
+
+import android.content.Context;
+import android.graphics.drawable.Animatable;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.tencent.qcloud.tuikit.timcommon.R;
+
+public class MessageStatusTimeView extends FrameLayout {
+ private TextView timeView;
+ private ImageView statusIconView;
+
+ public MessageStatusTimeView(@NonNull Context context) {
+ super(context);
+ init(context, null);
+ }
+
+ public MessageStatusTimeView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ init(context, attrs);
+ }
+
+ public MessageStatusTimeView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context, attrs);
+ }
+
+ private void init(Context context, AttributeSet attrs) {
+ LayoutInflater.from(context).inflate(R.layout.chat_minimalist_text_status_layout, this);
+ statusIconView = findViewById(R.id.status_icon);
+ timeView = findViewById(R.id.time);
+ }
+
+ public void setStatusIcon(int resID) {
+ if (resID == 0) {
+ statusIconView.setVisibility(GONE);
+ } else {
+ statusIconView.setBackgroundResource(resID);
+ Drawable drawable = statusIconView.getBackground();
+ if (drawable instanceof Animatable) {
+ ((Animatable) drawable).start();
+ }
+ statusIconView.setVisibility(VISIBLE);
+ }
+ }
+
+ public void setTimeText(CharSequence charSequence) {
+ timeView.setText(charSequence);
+ }
+
+ public void setTimeColor(int color) {
+ timeView.setTextColor(color);
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/MinimalistMessageLayout.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/MinimalistMessageLayout.java
new file mode 100644
index 00000000..0ad18b3b
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/MinimalistMessageLayout.java
@@ -0,0 +1,219 @@
+package com.tencent.qcloud.tuikit.timcommon.minimalistui.widget.message;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.constraintlayout.widget.ConstraintLayout;
+
+import com.tencent.qcloud.tuikit.timcommon.R;
+import com.tencent.qcloud.tuikit.timcommon.util.LayoutUtil;
+
+public class MinimalistMessageLayout extends ConstraintLayout {
+ private View msgArea;
+ private View quoteArea;
+ private View bottomArea;
+ private View replyArea;
+
+ private boolean isStart = false;
+ private Paint paint;
+ private Path quotePath;
+ private Path bottomPath;
+ private Path replyPath;
+
+ private Rect msgAreaRect;
+ private Rect bottomRect;
+ private Rect quoteRect;
+ private Rect replyRect;
+ private float strokeWidth;
+
+ private boolean isRTL = false;
+
+ public MinimalistMessageLayout(Context context) {
+ super(context);
+ init();
+ }
+
+ public MinimalistMessageLayout(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public MinimalistMessageLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init();
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+ public MinimalistMessageLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ init();
+ }
+
+ private void init() {
+ setWillNotDraw(false);
+ isRTL = LayoutUtil.isRTL();
+ quotePath = new Path();
+ bottomPath = new Path();
+ replyPath = new Path();
+ msgAreaRect = new Rect();
+ bottomRect = new Rect();
+ quoteRect = new Rect();
+ replyRect = new Rect();
+ paint = new Paint();
+ strokeWidth = getResources().getDimension(R.dimen.chat_minimalist_message_quato_line_width);
+ paint.setStrokeWidth(strokeWidth);
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setAntiAlias(true);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ msgArea = findViewById(R.id.msg_area);
+ bottomArea = findViewById(R.id.bottom_content_fl);
+ quoteArea = findViewById(R.id.quote_content_fl);
+ replyArea = findViewById(R.id.msg_reply_preview);
+ drawLine(canvas);
+ }
+
+ public void setIsStart(boolean isStart) {
+ this.isStart = isStart;
+ }
+
+ private Rect getChildRectInParent(View child) {
+ int[] location = new int[2];
+ getLocationInWindow(location);
+
+ Rect rect = new Rect();
+ int[] childLocation = new int[2];
+ child.getLocationInWindow(childLocation);
+ int left = childLocation[0] - location[0];
+ int top = childLocation[1] - location[1];
+ int right = left + child.getWidth();
+ int bottom = top + child.getHeight();
+ rect.set(left, top, right, bottom);
+ return rect;
+ }
+
+ private void drawLine(Canvas canvas) {
+ canvas.drawColor(0x00FFFFFF);
+ if (msgArea.getVisibility() == VISIBLE) {
+ msgAreaRect = getChildRectInParent(msgArea);
+ float msgX;
+ if (isStart) {
+ if (isRTL) {
+ paint.setColor(getResources().getColor(R.color.chat_minimalist_right_message_bubble_color));
+ msgX = msgAreaRect.right - strokeWidth / 2;
+ } else {
+ paint.setColor(getResources().getColor(R.color.chat_minimalist_left_message_bubble_color));
+ msgX = msgAreaRect.left + strokeWidth / 2;
+ }
+ } else {
+ if (isRTL) {
+ paint.setColor(getResources().getColor(R.color.chat_minimalist_left_message_bubble_color));
+ msgX = msgAreaRect.left + strokeWidth / 2;
+ } else {
+ paint.setColor(getResources().getColor(R.color.chat_minimalist_right_message_bubble_color));
+ msgX = msgAreaRect.right - strokeWidth / 2;
+ }
+ }
+ float msgCenterY;
+ msgCenterY = msgAreaRect.top + msgAreaRect.height() * 1.0f / 2;
+ drawBottomArea(canvas, msgX, msgCenterY);
+ drawQuoteArea(canvas, msgX, msgCenterY);
+ drawReplyArea(canvas, msgX, msgCenterY);
+ }
+ }
+
+ private void drawReplyArea(Canvas canvas, float msgX, float msgCenterY) {
+ if (replyArea.getVisibility() == VISIBLE) {
+ float replyX;
+ float replyCenterY;
+ replyRect = getChildRectInParent(replyArea);
+ if (isStart) {
+ if (isRTL) {
+ replyX = replyRect.right;
+ } else {
+ replyX = replyRect.left;
+ }
+ } else {
+ if (isRTL) {
+ replyX = replyRect.left;
+ } else {
+ replyX = replyRect.right;
+ }
+ }
+ replyCenterY = replyRect.top + replyRect.height() * 1.0f / 2;
+ int replyControlRadius = (int) Math.abs(replyX - msgX);
+ replyPath.reset();
+ replyPath.moveTo(msgX, msgCenterY);
+ replyPath.quadTo(msgX, replyCenterY - replyControlRadius, msgX, replyCenterY - replyControlRadius);
+ replyPath.quadTo(msgX, replyCenterY, replyX, replyCenterY);
+ canvas.drawPath(replyPath, paint);
+ }
+ }
+
+ private void drawQuoteArea(Canvas canvas, float msgX, float msgCenterY) {
+ if (quoteArea.getVisibility() == VISIBLE) {
+ float quoteX;
+ float quoteCenterY;
+ quoteRect = getChildRectInParent(quoteArea);
+ if (isStart) {
+ if (isRTL) {
+ quoteX = quoteRect.right;
+ } else {
+ quoteX = quoteRect.left;
+ }
+ } else {
+ if (isRTL) {
+ quoteX = quoteRect.left;
+ } else {
+ quoteX = quoteRect.right;
+ }
+ }
+ quoteCenterY = quoteRect.top + quoteRect.height() * 1.0f / 2;
+ int quoteControlRadius = (int) Math.abs(quoteX - msgX);
+ quotePath.reset();
+ quotePath.moveTo(msgX, msgCenterY);
+ quotePath.quadTo(msgX, quoteCenterY - quoteControlRadius, msgX, quoteCenterY - quoteControlRadius);
+ quotePath.quadTo(msgX, quoteCenterY, quoteX, quoteCenterY);
+ canvas.drawPath(quotePath, paint);
+ }
+ }
+
+ private void drawBottomArea(Canvas canvas, float msgX, float msgCenterY) {
+ if (bottomArea.getVisibility() == VISIBLE) {
+ float bottomX;
+ float bottomCenterY;
+ bottomRect = getChildRectInParent(bottomArea);
+ if (isStart) {
+ if (isRTL) {
+ bottomX = bottomRect.right;
+ } else {
+ bottomX = bottomRect.left;
+ }
+ } else {
+ if (isRTL) {
+ bottomX = bottomRect.left;
+ } else {
+ bottomX = bottomRect.right;
+ }
+ }
+ bottomCenterY = bottomRect.top + bottomRect.height() * 1.0f / 2;
+ int bottomControlRadius = (int) Math.abs(bottomX - msgX);
+ bottomPath.reset();
+ bottomPath.moveTo(msgX, msgCenterY);
+ bottomPath.quadTo(msgX, bottomCenterY - bottomControlRadius, msgX, bottomCenterY - bottomControlRadius);
+ bottomPath.quadTo(msgX, bottomCenterY, bottomX, bottomCenterY);
+ canvas.drawPath(bottomPath, paint);
+ }
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/ReplyPreviewView.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/ReplyPreviewView.java
new file mode 100644
index 00000000..9896e659
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/ReplyPreviewView.java
@@ -0,0 +1,168 @@
+package com.tencent.qcloud.tuikit.timcommon.minimalistui.widget.message;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.RequestBuilder;
+import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
+import com.bumptech.glide.load.resource.bitmap.CircleCrop;
+import com.tencent.qcloud.tuikit.timcommon.R;
+import com.tencent.qcloud.tuikit.timcommon.bean.MessageRepliesBean;
+import com.tencent.qcloud.tuikit.timcommon.util.ScreenUtil;
+import com.tencent.qcloud.tuikit.timcommon.util.TUIUtil;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+public class ReplyPreviewView extends FrameLayout {
+ private ImageView firstImg;
+ private ImageView secondImg;
+ private ImageView thirdImg;
+ private TextView replyText;
+
+ private MessageRepliesBean messageRepliesBean;
+
+ public ReplyPreviewView(@NonNull Context context) {
+ super(context);
+ init(context);
+ }
+
+ public ReplyPreviewView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ public ReplyPreviewView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context);
+ }
+
+ public void setMessageRepliesBean(MessageRepliesBean messageRepliesBean) {
+ this.messageRepliesBean = messageRepliesBean;
+ setView();
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+ public ReplyPreviewView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ init(context);
+ }
+
+ private void init(Context context) {
+ LayoutInflater.from(context).inflate(R.layout.chat_minimalist_reply_preview_layout, this);
+ firstImg = findViewById(R.id.first_avatar);
+ secondImg = findViewById(R.id.second_avatar);
+ thirdImg = findViewById(R.id.third_avatar);
+ replyText = findViewById(R.id.reply_text);
+ }
+
+ private void setView() {
+ if (messageRepliesBean != null && messageRepliesBean.getRepliesSize() > 0) {
+ setVisibility(VISIBLE);
+ firstImg.setVisibility(GONE);
+ secondImg.setVisibility(GONE);
+ thirdImg.setVisibility(GONE);
+ replyText.setText(String.format(Locale.US, getResources().getString(R.string.chat_reply_num), messageRepliesBean.getRepliesSize()));
+ List repliesBeanList = messageRepliesBean.getReplies();
+ List iconList = getReplyUserIconLt(repliesBeanList);
+ if (iconList.isEmpty()) {
+ return;
+ }
+ String secondIconUrl = null;
+ String thirdIconUrl = null;
+
+ if (iconList.size() > 1) {
+ secondIconUrl = iconList.get(1);
+ }
+ if (iconList.size() > 2) {
+ thirdIconUrl = iconList.get(2);
+ }
+ String firstIconUrl = iconList.get(0);
+ firstImg.setVisibility(VISIBLE);
+ loadAvatar(firstImg, firstIconUrl);
+ if (secondIconUrl != null) {
+ secondImg.setVisibility(VISIBLE);
+ loadAvatar(secondImg, secondIconUrl);
+ }
+
+ if (iconList.size() == 3 && thirdIconUrl != null) {
+ thirdImg.setVisibility(VISIBLE);
+ loadAvatar(thirdImg, thirdIconUrl);
+ } else if (iconList.size() > 3) {
+ thirdImg.setVisibility(VISIBLE);
+ loadAvatar(thirdImg, R.drawable.chat_reply_more_icon);
+ }
+ } else {
+ setVisibility(GONE);
+ }
+ }
+
+ private List getReplyUserIconLt(List repliesBeanList) {
+ Set iconUrlSet = new LinkedHashSet<>();
+ for (MessageRepliesBean.ReplyBean replyBean : repliesBeanList) {
+ iconUrlSet.add(replyBean.getSenderFaceUrl());
+ if (iconUrlSet.size() >= 3) {
+ break;
+ }
+ }
+ return new ArrayList<>(iconUrlSet);
+ }
+
+ private void loadAvatar(ImageView imageView, Object url) {
+ if (TUIUtil.isActivityDestroyed(getContext())) {
+ return;
+ }
+
+ RequestBuilder errorRequestBuilder = Glide.with(getContext())
+ .load(com.tencent.qcloud.tuikit.timcommon.R.drawable.core_default_user_icon_light)
+ .transform(new ReplyRingCircleCrop());
+
+ Glide.with(getContext())
+ .load(url)
+ .centerCrop()
+ .transform(new ReplyRingCircleCrop())
+ .error(errorRequestBuilder)
+ .into(imageView);
+ }
+
+ static class ReplyRingCircleCrop extends CircleCrop {
+ @Override
+ protected Bitmap transform(@NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) {
+ Bitmap outBitmap = pool.get(outWidth, outHeight, Bitmap.Config.ARGB_8888);
+ Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
+ paint.setColor(Color.WHITE);
+ Canvas canvas = new Canvas(outBitmap);
+ int borderWidth = ScreenUtil.dip2px(1);
+ int innerWidth = outWidth - 2 * borderWidth;
+ canvas.drawCircle(outWidth / 2, outHeight / 2, innerWidth / 2, paint);
+ paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
+ Rect rect = new Rect(borderWidth, borderWidth, outWidth - borderWidth, outHeight - borderWidth);
+ int innerHeight = outHeight - 2 * borderWidth;
+ Bitmap bitmap = super.transform(pool, toTransform, innerWidth, innerHeight);
+ canvas.drawBitmap(bitmap, null, rect, paint);
+ paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OVER));
+ canvas.drawCircle(outWidth / 2, outHeight / 2, outWidth / 2, paint);
+ return outBitmap;
+ }
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/TUIReplyQuoteView.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/TUIReplyQuoteView.java
new file mode 100644
index 00000000..bcb215a6
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/TUIReplyQuoteView.java
@@ -0,0 +1,31 @@
+package com.tencent.qcloud.tuikit.timcommon.minimalistui.widget.message;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.widget.FrameLayout;
+
+import com.tencent.qcloud.tuikit.timcommon.bean.TUIReplyQuoteBean;
+
+public abstract class TUIReplyQuoteView extends FrameLayout {
+
+ public abstract int getLayoutResourceId();
+
+ public TUIReplyQuoteView(Context context) {
+ super(context);
+ int resId = getLayoutResourceId();
+ if (resId != 0) {
+ LayoutInflater.from(context).inflate(resId, this, true);
+ }
+ }
+
+ public abstract void onDrawReplyQuote(TUIReplyQuoteBean quoteBean);
+
+ /**
+ *
+ * Whether the original message sender is himself, used for different UI displays
+ *
+ * @param isSelf
+ */
+ public void setSelf(boolean isSelf) {}
+
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/TimeInLineTextLayout.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/TimeInLineTextLayout.java
new file mode 100644
index 00000000..81f8c42a
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/TimeInLineTextLayout.java
@@ -0,0 +1,160 @@
+package com.tencent.qcloud.tuikit.timcommon.minimalistui.widget.message;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.text.Layout;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.tencent.qcloud.tuikit.timcommon.R;
+import com.tencent.qcloud.tuikit.timcommon.util.LayoutUtil;
+
+public class TimeInLineTextLayout extends FrameLayout {
+ private TextView textView;
+ private MessageStatusTimeView statusArea;
+ private int lineCount;
+ private boolean isRTL = false;
+ private int lastLineWidth = 0;
+ private boolean lastLineRunRTL = true;
+
+ public TimeInLineTextLayout(@NonNull Context context) {
+ super(context);
+ init(context, null);
+ }
+
+ public TimeInLineTextLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ init(context, attrs);
+ }
+
+ public TimeInLineTextLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context, attrs);
+ }
+
+ private void init(Context context, AttributeSet attrs) {
+ isRTL = LayoutUtil.isRTL();
+ textView = new TextView(context, null, R.style.ChatMinimalistMessageTextStyle);
+ textView.setTextColor(Color.BLACK);
+ textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textView.getResources().getDimension(R.dimen.chat_minimalist_message_text_size));
+ LayoutParams textViewParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ addView(textView, textViewParams);
+ statusArea = new MessageStatusTimeView(context);
+ LayoutParams statusAreaParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ addView(statusArea, statusAreaParams);
+ }
+
+ public void setText(CharSequence charSequence) {
+ textView.setText(charSequence);
+ }
+
+ public void setText(int resID) {
+ textView.setText(resID);
+ }
+
+ public void setStatusIcon(int resID) {
+ statusArea.setStatusIcon(resID);
+ }
+
+ public void setTimeText(CharSequence charSequence) {
+ statusArea.setTimeText(charSequence);
+ }
+
+ public void setTimeColor(int color) {
+ statusArea.setTimeColor(color);
+ }
+
+ public void setTextColor(int color) {
+ textView.setTextColor(color);
+ }
+
+ public void setTextSize(int size) {
+ textView.setTextSize(size);
+ }
+
+ public TextView getTextView() {
+ return textView;
+ }
+
+ public void setOnStatusAreaClickListener(OnClickListener listener) {
+ statusArea.setOnClickListener(listener);
+ }
+
+ public void setOnStatusAreaLongClickListener(OnLongClickListener listener) {
+ statusArea.setOnLongClickListener(listener);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ int textViewWidth = textView.getMeasuredWidth();
+ int textViewHeight = textView.getMeasuredHeight();
+ int statusAreaWidth = statusArea.getMeasuredWidth();
+ int statusAreaHeight = statusArea.getMeasuredHeight();
+ if (isRTL) {
+ if (lineCount <= 1) {
+ textView.layout(statusAreaWidth, 0, statusAreaWidth + textViewWidth, textViewHeight);
+ } else {
+ textView.layout(0, 0, textViewWidth, textViewHeight);
+ }
+ } else {
+ textView.layout(0, 0, textViewWidth, textViewHeight);
+ }
+
+ if (isRTL) {
+ if (lineCount <= 1) {
+ statusArea.layout(0, bottom - top - statusAreaHeight, statusAreaWidth, bottom - top);
+ } else {
+ if (lastLineRunRTL) {
+ statusArea.layout(0, bottom - top - statusAreaHeight, statusAreaWidth, bottom - top);
+ } else {
+ statusArea.layout(right - left - statusAreaWidth, bottom - top - statusAreaHeight, right - left, bottom - top);
+ }
+ }
+ } else {
+ statusArea.layout(right - left - statusAreaWidth, bottom - top - statusAreaHeight, right - left, bottom - top);
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int maxWidth;
+ int maxHeight;
+ // measure text view
+ measureChildren(widthMeasureSpec, heightMeasureSpec);
+
+ int textWidth = textView.getMeasuredWidth();
+ int textHeight = textView.getMeasuredHeight();
+ lineCount = textView.getLineCount();
+
+ // get last line's width
+ Layout layout = textView.getLayout();
+ if (layout != null) {
+ lastLineWidth = (int) layout.getLineWidth(lineCount - 1);
+ int direction = layout.getParagraphDirection(lineCount - 1);
+ lastLineRunRTL = direction == Layout.DIR_RIGHT_TO_LEFT;
+ }
+
+ int statusAreaWidth = statusArea.getMeasuredWidth();
+ int statusAreaHeight = statusArea.getMeasuredHeight();
+ MarginLayoutParams lp = (MarginLayoutParams) statusArea.getLayoutParams();
+ statusAreaWidth += lp.leftMargin + lp.rightMargin;
+
+ int layoutWidth = MeasureSpec.getSize(widthMeasureSpec);
+ // switch a new line
+ if (lastLineWidth + statusAreaWidth > layoutWidth) {
+ maxHeight = textHeight + statusAreaHeight;
+ lineCount++;
+ maxWidth = Math.max(textWidth, statusAreaWidth);
+ } else {
+ maxHeight = Math.max(textHeight, statusAreaHeight);
+ maxWidth = Math.max(textWidth, lastLineWidth + statusAreaWidth);
+ }
+
+ setMeasuredDimension(maxWidth, maxHeight);
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/ActivityResultResolver.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/ActivityResultResolver.java
new file mode 100644
index 00000000..d3d480f6
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/ActivityResultResolver.java
@@ -0,0 +1,281 @@
+package com.tencent.qcloud.tuikit.timcommon.util;
+
+import android.app.Activity;
+import android.content.ClipData;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+import android.util.Pair;
+import androidx.activity.result.ActivityResult;
+import androidx.activity.result.ActivityResultCallback;
+import androidx.activity.result.ActivityResultCaller;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContract;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.FragmentActivity;
+import com.tencent.qcloud.tuicore.TUICore;
+import com.tencent.qcloud.tuicore.interfaces.TUIValueCallback;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+
+public class ActivityResultResolver {
+ public static final String CONTENT_TYPE_ALL = "*/*";
+ public static final String CONTENT_TYPE_IMAGE = "image/*";
+ public static final String CONTENT_TYPE_VIDEO = "video/*";
+
+ private static final String CONTENT_KEY_TYPE = "ContentType";
+ private static final String URI = "Uri";
+ private static final String METHOD = "Method";
+ private static final String KEY_DATA = "Data";
+
+ private static final String METHOD_GET_SINGLE_CONTENT = "MethodGetSingleContent";
+ private static final String METHOD_GET_MULTIPLE_CONTENT = "MethodGetMultipleContent";
+ private static final String METHOD_TAKE_PICTURE = "MethodTakePicture";
+ private static final String METHOD_TAKE_VIDEO = "MethodTakeVideo";
+
+ private ActivityResultResolver() {}
+
+ public static void getSingleContent(ActivityResultCaller activityResultCaller, @NonNull String type, TUIValueCallback callback) {
+ getContent(activityResultCaller, new String[] {type}, false, new TUIValueCallback>() {
+ @Override
+ public void onSuccess(List list) {
+ if (list != null && !list.isEmpty()) {
+ TUIValueCallback.onSuccess(callback, list.get(0));
+ } else {
+ TUIValueCallback.onError(callback, -1, "getSingleContent result list is empty");
+ }
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMessage) {
+ TUIValueCallback.onError(callback, errorCode, errorMessage);
+ }
+ });
+ }
+
+ public static void getSingleContent(ActivityResultCaller activityResultCaller, @NonNull String[] type, TUIValueCallback callback) {
+ getContent(activityResultCaller, type, false, new TUIValueCallback>() {
+ @Override
+ public void onSuccess(List list) {
+ if (list != null && !list.isEmpty()) {
+ TUIValueCallback.onSuccess(callback, list.get(0));
+ } else {
+ TUIValueCallback.onError(callback, -1, "getSingleContent result list is empty");
+ }
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMessage) {
+ TUIValueCallback.onError(callback, errorCode, errorMessage);
+ }
+ });
+ }
+
+ public static void getMultipleContent(ActivityResultCaller activityResultCaller, @NonNull String type, TUIValueCallback> callback) {
+ getContent(activityResultCaller, new String[] {type}, true, callback);
+ }
+
+ public static void getMultipleContent(ActivityResultCaller activityResultCaller, @NonNull String[] type, TUIValueCallback> callback) {
+ getContent(activityResultCaller, type, true, callback);
+ }
+
+ private static void getContent(
+ ActivityResultCaller activityResultCaller, @NonNull String[] types, boolean isMultiContent, TUIValueCallback> callback) {
+ Bundle bundle = new Bundle();
+ bundle.putStringArray(CONTENT_KEY_TYPE, types);
+ bundle.putString(METHOD, isMultiContent ? METHOD_GET_MULTIPLE_CONTENT : METHOD_GET_SINGLE_CONTENT);
+ TUICore.startActivityForResult(activityResultCaller, ActivityResultProxyActivity.class, bundle, new ActivityResultCallback() {
+ @Override
+ public void onActivityResult(ActivityResult result) {
+ if (result.getData() != null) {
+ TUIValueCallback.onSuccess(callback, (List) result.getData().getSerializableExtra(KEY_DATA));
+ }
+ }
+ });
+ }
+
+ public static void takePicture(ActivityResultCaller activityResultCaller, @NonNull Uri uri, TUIValueCallback callback) {
+ takePictureVideo(activityResultCaller, uri, true, callback);
+ }
+
+ public static void takeVideo(ActivityResultCaller activityResultCaller, @NonNull Uri uri, TUIValueCallback callback) {
+ takePictureVideo(activityResultCaller, uri, false, callback);
+ }
+
+ private static void takePictureVideo(ActivityResultCaller activityResultCaller, @NonNull Uri uri, boolean isPicture, TUIValueCallback callback) {
+ Bundle bundle = new Bundle();
+ if (isPicture) {
+ bundle.putString(METHOD, METHOD_TAKE_PICTURE);
+ } else {
+ bundle.putString(METHOD, METHOD_TAKE_VIDEO);
+ }
+ bundle.putParcelable(URI, uri);
+ TUICore.startActivityForResult(activityResultCaller, ActivityResultProxyActivity.class, bundle, new ActivityResultCallback() {
+ @Override
+ public void onActivityResult(ActivityResult result) {
+ if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
+ TUIValueCallback.onSuccess(callback, result.getData().getBooleanExtra(KEY_DATA, false));
+ }
+ }
+ });
+ }
+
+ private static class GetContentsContract extends ActivityResultContract, List> {
+ @NonNull
+ @Override
+ public Intent createIntent(@NonNull Context context, @NonNull Pair input) {
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ String[] type = input.first;
+ boolean allowMultiple = input.second;
+ if (type.length == 1) {
+ intent.setType(type[0]);
+ } else if (type.length > 1) {
+ intent.setType(type[0]);
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, type);
+ }
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple);
+ return intent;
+ }
+
+ @NonNull
+ @Override
+ public final List parseResult(int resultCode, @Nullable Intent intent) {
+ if (intent == null || resultCode != Activity.RESULT_OK) {
+ return Collections.emptyList();
+ } else {
+ return getClipDataUris(intent);
+ }
+ }
+
+ @NonNull
+ static List getClipDataUris(@NonNull Intent intent) {
+ // Use a LinkedHashSet to maintain any ordering that may be
+ // present in the ClipData
+ LinkedHashSet resultSet = new LinkedHashSet<>();
+ if (intent.getData() != null) {
+ resultSet.add(intent.getData());
+ }
+ ClipData clipData = intent.getClipData();
+ if (clipData == null && resultSet.isEmpty()) {
+ return Collections.emptyList();
+ } else if (clipData != null) {
+ for (int i = 0; i < clipData.getItemCount(); i++) {
+ Uri uri = clipData.getItemAt(i).getUri();
+ if (uri != null) {
+ resultSet.add(uri);
+ }
+ }
+ }
+ return new ArrayList<>(resultSet);
+ }
+ }
+
+ private static class TakePictureVideoContract extends ActivityResultContract, Boolean> {
+ private boolean isTakePicture;
+
+ @NonNull
+ @Override
+ public Intent createIntent(@NonNull Context context, @NonNull Pair input) {
+ isTakePicture = input.second;
+ if (isTakePicture) {
+ return new Intent(MediaStore.ACTION_IMAGE_CAPTURE)
+ .putExtra(MediaStore.EXTRA_OUTPUT, input.first)
+ .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ } else {
+ return new Intent(MediaStore.ACTION_VIDEO_CAPTURE)
+ .putExtra(MediaStore.EXTRA_OUTPUT, input.first)
+ .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ }
+ }
+
+ @NonNull
+ @Override
+ public final Boolean parseResult(int resultCode, @Nullable Intent intent) {
+ if (isTakePicture) {
+ return resultCode == Activity.RESULT_OK;
+ } else {
+ return intent != null && resultCode == Activity.RESULT_OK;
+ }
+ }
+ }
+
+ public static class ActivityResultProxyActivity extends FragmentActivity {
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Intent intent = getIntent();
+ String method = intent.getStringExtra(METHOD);
+ if (TextUtils.equals(method, METHOD_GET_SINGLE_CONTENT)) {
+ getSingleContent(intent);
+ } else if (TextUtils.equals(method, METHOD_GET_MULTIPLE_CONTENT)) {
+ getMultipleContent(intent);
+ } else if (TextUtils.equals(method, METHOD_TAKE_PICTURE)) {
+ takePicture(intent);
+ } else if (TextUtils.equals(method, METHOD_TAKE_VIDEO)) {
+ takeVideo(intent);
+ }
+ }
+
+ private void getSingleContent(Intent intent) {
+ String[] types = intent.getStringArrayExtra(CONTENT_KEY_TYPE);
+ getContent(types, false);
+ }
+
+ private void getMultipleContent(Intent intent) {
+ String[] types = intent.getStringArrayExtra(CONTENT_KEY_TYPE);
+ getContent(types, true);
+ }
+
+ private void getContent(String[] types, boolean isMultiple) {
+ ActivityResultLauncher> launcher =
+ this.registerForActivityResult(new GetContentsContract(), new ActivityResultCallback>() {
+ @Override
+ public void onActivityResult(List result) {
+ Intent dataIntent = new Intent();
+ dataIntent.putExtra(KEY_DATA, new ArrayList<>(result));
+ setResult(Activity.RESULT_OK, dataIntent);
+ finish();
+ }
+ });
+ try {
+ launcher.launch(Pair.create(types, isMultiple));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void takePicture(Intent intent) {
+ takePictureVideo(intent, true);
+ }
+
+ private void takeVideo(Intent intent) {
+ takePictureVideo(intent, false);
+ }
+
+ private void takePictureVideo(Intent intent, boolean isPicture) {
+ Uri uri = intent.getParcelableExtra(URI);
+ ActivityResultLauncher> launcher =
+ this.registerForActivityResult(new TakePictureVideoContract(), new ActivityResultCallback() {
+ @Override
+ public void onActivityResult(Boolean result) {
+ Intent dataIntent = new Intent();
+ dataIntent.putExtra(KEY_DATA, result);
+ setResult(Activity.RESULT_OK, dataIntent);
+ finish();
+ }
+ });
+ try {
+ launcher.launch(Pair.create(uri, isPicture));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/DateTimeUtil.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/DateTimeUtil.java
new file mode 100644
index 00000000..d65dbe45
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/DateTimeUtil.java
@@ -0,0 +1,150 @@
+package com.tencent.qcloud.tuikit.timcommon.util;
+
+import android.content.Context;
+
+import com.tencent.qcloud.tuicore.TUIConfig;
+import com.tencent.qcloud.tuicore.TUIThemeManager;
+import com.tencent.qcloud.tuikit.timcommon.R;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+
+public class DateTimeUtil {
+ private static final long minute = 60 * 1000;
+ private static final long hour = 60 * minute;
+ private static final long day = 24 * hour;
+ private static final long week = 7 * day;
+ private static final long month = 31 * day;
+ private static final long year = 12 * month;
+
+ /**
+ * return format text for time
+ * you can see https://docs.oracle.com/javase/8/docs/api/java/util/Formatter.html
+ * today:HH:MM
+ * this week:Sunday, Friday ..
+ * this year:MM/DD
+ * before this year:YYYY/MM/DD
+ * @param date current time
+ * @return format text
+ */
+ public static String getTimeFormatText(Date date) {
+ if (date == null) {
+ return "";
+ }
+ Context context = TUIConfig.getAppContext();
+ Locale locale;
+ if (context == null) {
+ locale = Locale.getDefault();
+ } else {
+ locale = TUIThemeManager.getInstance().getLocale(context);
+ }
+ String timeText;
+ Calendar dayStartCalendar = Calendar.getInstance();
+ dayStartCalendar.set(Calendar.HOUR_OF_DAY, 0);
+ dayStartCalendar.set(Calendar.MINUTE, 0);
+ dayStartCalendar.set(Calendar.SECOND, 0);
+ dayStartCalendar.set(Calendar.MILLISECOND, 0);
+ Calendar weekStartCalendar = Calendar.getInstance();
+ weekStartCalendar.setFirstDayOfWeek(Calendar.SUNDAY);
+ weekStartCalendar.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY);
+ weekStartCalendar.set(Calendar.HOUR_OF_DAY, 0);
+ weekStartCalendar.set(Calendar.MINUTE, 0);
+ weekStartCalendar.set(Calendar.SECOND, 0);
+ weekStartCalendar.set(Calendar.MILLISECOND, 0);
+ Calendar yearStartCalendar = Calendar.getInstance();
+ yearStartCalendar.set(Calendar.DAY_OF_YEAR, 1);
+ yearStartCalendar.set(Calendar.HOUR_OF_DAY, 0);
+ yearStartCalendar.set(Calendar.MINUTE, 0);
+ yearStartCalendar.set(Calendar.SECOND, 0);
+ yearStartCalendar.set(Calendar.MILLISECOND, 0);
+ long dayStartTimeInMillis = dayStartCalendar.getTimeInMillis();
+ long weekStartTimeInMillis = weekStartCalendar.getTimeInMillis();
+ long yearStartTimeInMillis = yearStartCalendar.getTimeInMillis();
+ long outTimeMillis = date.getTime();
+ if (outTimeMillis < yearStartTimeInMillis) {
+ timeText = String.format(Locale.US, "%tD", date);
+ } else if (outTimeMillis < weekStartTimeInMillis) {
+ timeText = String.format(Locale.US, "%1$tm/%1$td", date);
+ } else if (outTimeMillis < dayStartTimeInMillis) {
+ timeText = String.format(locale, "%tA", date);
+ } else {
+ timeText = String.format(Locale.US, "%tR", date);
+ }
+ return timeText;
+ }
+
+ /**
+ * HH:MM
+ * @param date current time
+ * @return format text e.g. "12:12"
+ */
+ public static String getHMTimeString(Date date) {
+ if (date == null) {
+ return "";
+ }
+ return String.format(Locale.US, "%tR", date);
+ }
+
+ public static String formatSeconds(long seconds) {
+ Context context = TUIConfig.getAppContext();
+ String timeStr = seconds + context.getString(R.string.date_second_short);
+ if (seconds > 60) {
+ long second = seconds % 60;
+ long min = seconds / 60;
+ timeStr = min + context.getString(R.string.date_minute_short) + second + context.getString(R.string.date_second_short);
+ if (min > 60) {
+ min = (seconds / 60) % 60;
+ long hour = (seconds / 60) / 60;
+ timeStr = hour + context.getString(R.string.date_hour_short) + min + context.getString(R.string.date_minute_short) + second
+ + context.getString(R.string.date_second_short);
+ if (hour % 24 == 0) {
+ long day = (((seconds / 60) / 60) / 24);
+ timeStr = day + context.getString(R.string.date_day_short);
+ } else if (hour > 24) {
+ hour = ((seconds / 60) / 60) % 24;
+ long day = (((seconds / 60) / 60) / 24);
+ timeStr = day + context.getString(R.string.date_day_short) + hour + context.getString(R.string.date_hour_short) + min
+ + context.getString(R.string.date_minute_short) + second + context.getString(R.string.date_second_short);
+ }
+ }
+ }
+ return timeStr;
+ }
+
+ public static String formatSecondsTo00(int timeSeconds) {
+ int second = timeSeconds % 60;
+ int minuteTemp = timeSeconds / 60;
+ if (minuteTemp > 0) {
+ int minute = minuteTemp % 60;
+ int hour = minuteTemp / 60;
+ if (hour > 0) {
+ return (hour >= 10 ? (hour + "") : ("0" + hour)) + ":" + (minute >= 10 ? (minute + "") : ("0" + minute)) + ":"
+ + (second >= 10 ? (second + "") : ("0" + second));
+ } else {
+ return (minute >= 10 ? (minute + "") : ("0" + minute)) + ":" + (second >= 10 ? (second + "") : ("0" + second));
+ }
+ } else {
+ return "00:" + (second >= 10 ? (second + "") : ("0" + second));
+ }
+ }
+
+ public static long getStringToDate(String dateString, String pattern) {
+ SimpleDateFormat dateFormat = new SimpleDateFormat(pattern, Locale.US);
+ Date date = new Date();
+ try {
+ date = dateFormat.parse(dateString);
+ } catch (ParseException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ return date.getTime();
+ }
+
+ public static String getTimeStringFromDate(Date date, String pattern) {
+ SimpleDateFormat simpleDateFormat = new SimpleDateFormat(pattern, Locale.US);
+ return simpleDateFormat.format(date);
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/FileProvider.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/FileProvider.java
new file mode 100644
index 00000000..9e5021ca
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/FileProvider.java
@@ -0,0 +1,3 @@
+package com.tencent.qcloud.tuikit.timcommon.util;
+
+public class FileProvider extends androidx.core.content.FileProvider {}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/FileUtil.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/FileUtil.java
new file mode 100644
index 00000000..2bbc9229
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/FileUtil.java
@@ -0,0 +1,553 @@
+package com.tencent.qcloud.tuikit.timcommon.util;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.provider.DocumentsContract;
+import android.provider.MediaStore;
+import android.provider.OpenableColumns;
+import android.text.TextUtils;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.tencent.qcloud.tuicore.ServiceInitializer;
+import com.tencent.qcloud.tuicore.TUIConfig;
+import com.tencent.qcloud.tuicore.TUILogin;
+import com.tencent.qcloud.tuikit.timcommon.R;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.util.Locale;
+import java.util.Random;
+
+public class FileUtil {
+ public static final String DOCUMENTS_DIR = "documents";
+
+ public static final String FILE_PROVIDER_AUTH = ".timcommon.fileprovider";
+
+ public static final int SIZETYPE_B = 1;
+ public static final int SIZETYPE_KB = 2;
+ public static final int SIZETYPE_MB = 3;
+ public static final int SIZETYPE_GB = 4;
+
+ public static boolean deleteFile(String path) {
+ if (TextUtils.isEmpty(path)) {
+ return false;
+ }
+ boolean result = false;
+ File file = new File(path);
+ if (file.exists()) {
+ result = file.delete();
+ }
+ return result;
+ }
+
+ public static String getPathFromUri(Uri uri) {
+ String path = "";
+ try {
+ int sdkVersion = Build.VERSION.SDK_INT;
+ if (sdkVersion >= 19) {
+ path = getPathByCopyFile(TUILogin.getAppContext(), uri);
+ } else {
+ path = getRealFilePath(uri);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ if (path == null) {
+ path = "";
+ }
+ return path;
+ }
+
+ public static String getRealFilePath(Uri uri) {
+ if (null == uri) {
+ return null;
+ }
+ final String scheme = uri.getScheme();
+ String data = null;
+ if (scheme == null) {
+ data = uri.getPath();
+ } else if (ContentResolver.SCHEME_FILE.equals(scheme)) {
+ data = uri.getPath();
+ } else if (ContentResolver.SCHEME_CONTENT.equals(scheme)) {
+ Cursor cursor = TUILogin.getAppContext().getContentResolver().query(uri, new String[] {MediaStore.Images.ImageColumns.DATA}, null, null, null);
+ if (null != cursor) {
+ if (cursor.moveToFirst()) {
+ int index = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
+ if (index > -1) {
+ data = cursor.getString(index);
+ }
+ }
+ cursor.close();
+ }
+ }
+ return data;
+ }
+
+ public static Uri getUriFromPath(String path) {
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ return FileProvider.getUriForFile(
+ TUIConfig.getAppContext(), TUIConfig.getAppContext().getApplicationInfo().packageName + FILE_PROVIDER_AUTH, new File(path));
+ } else {
+ return Uri.fromFile(new File(path));
+ }
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ /**
+ *
+ * Get file path from Uri specially designed for Android4.4 and above
+ */
+ public static String getPath(final Context context, final Uri uri) {
+ final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
+
+ // DocumentProvider
+ if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
+ // ExternalStorageProvider
+ if (isExternalStorageDocument(uri)) {
+ final String docId = DocumentsContract.getDocumentId(uri);
+ final String[] split = docId.split(":");
+ final String type = split[0];
+
+ if ("primary".equalsIgnoreCase(type)) {
+ return Environment.getExternalStorageDirectory() + "/" + split[1];
+ } else {
+ return getPathByCopyFile(context, uri);
+ }
+ }
+ // DownloadsProvider
+ else if (isDownloadsDocument(uri)) {
+ final String id = DocumentsContract.getDocumentId(uri);
+ if (id.startsWith("raw:")) {
+ final String path = id.replaceFirst("raw:", "");
+ return path;
+ }
+ String[] contentUriPrefixesToTry =
+ new String[] {"content://downloads/public_downloads", "content://downloads/my_downloads", "content://downloads/all_downloads"};
+
+ for (String contentUriPrefix : contentUriPrefixesToTry) {
+ Uri contentUri = ContentUris.withAppendedId(Uri.parse(contentUriPrefix), Long.parseLong(id));
+ try {
+ String path = getDataColumn(context, contentUri, null, null);
+ if (path != null && Build.VERSION.SDK_INT < 29) {
+ return path;
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+
+ // On some android8+ mobile phones, the path cannot be obtained, so the new file name is obtained by copying, and then the file is sent out
+ return getPathByCopyFile(context, uri);
+ }
+ // MediaProvider
+ else if (isMediaDocument(uri)) {
+ final String docId = DocumentsContract.getDocumentId(uri);
+ final String[] split = docId.split(":");
+ final String type = split[0];
+
+ Uri contentUri = null;
+ if ("image".equals(type)) {
+ contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+ } else if ("video".equals(type)) {
+ contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
+ } else if ("audio".equals(type)) {
+ contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
+ }
+
+ final String selection = "_id=?";
+ final String[] selectionArgs = new String[] {split[1]};
+
+ String path = getDataColumn(context, contentUri, selection, selectionArgs);
+ if (TextUtils.isEmpty(path) || Build.VERSION.SDK_INT >= 29) {
+ path = getPathByCopyFile(context, uri);
+ }
+ return path;
+ }
+ }
+ // MediaStore (and general)
+ else if ("content".equalsIgnoreCase(uri.getScheme())) {
+ String path = getDataColumn(context, uri, null, null);
+ if (TextUtils.isEmpty(path) || Build.VERSION.SDK_INT >= 29) {
+
+ path = getPathByCopyFile(context, uri);
+ }
+ return path;
+ }
+ // File
+ else if ("file".equalsIgnoreCase(uri.getScheme())) {
+ return uri.getPath();
+ }
+
+ return null;
+ }
+
+ private static String getPathByCopyFile(Context context, Uri uri) {
+ String fileName = getFileName(context, uri);
+ File cacheDir = getDocumentCacheDir(context);
+ File file = generateFileName(fileName, cacheDir);
+ String destinationPath = null;
+ if (file != null) {
+ destinationPath = file.getAbsolutePath();
+ boolean saveSuccess = saveFileFromUri(context, uri, destinationPath);
+ if (!saveSuccess) {
+ file.delete();
+ return null;
+ }
+ }
+
+ return destinationPath;
+ }
+
+ @Nullable
+ public static File generateFileName(@Nullable String name, File directory) {
+ if (name == null) {
+ return null;
+ }
+
+ File file = new File(directory, name);
+
+ if (file.exists()) {
+ String fileName = name;
+ String extension = "";
+ int dotIndex = name.lastIndexOf('.');
+ if (dotIndex > 0) {
+ fileName = name.substring(0, dotIndex);
+ extension = name.substring(dotIndex);
+ }
+
+ int index = 0;
+
+ while (file.exists()) {
+ index++;
+ name = fileName + '(' + index + ')' + extension;
+ file = new File(directory, name);
+ }
+ }
+
+ try {
+ if (!file.createNewFile()) {
+ return null;
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+
+ return file;
+ }
+
+ public static String getFileName(@NonNull Context context, Uri uri) {
+ String mimeType = context.getContentResolver().getType(uri);
+ String filename = null;
+
+ if (mimeType == null) {
+ filename = getName(uri.toString());
+ } else {
+ Cursor returnCursor = context.getContentResolver().query(uri, null, null, null, null);
+ if (returnCursor != null) {
+ int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
+ returnCursor.moveToFirst();
+ filename = returnCursor.getString(nameIndex);
+ returnCursor.close();
+ }
+ }
+
+ return filename;
+ }
+
+ public static String getName(String filePath) {
+ if (filePath == null) {
+ return null;
+ }
+ int index = filePath.lastIndexOf('/');
+ return filePath.substring(index + 1);
+ }
+
+ public static File getDocumentCacheDir(@NonNull Context context) {
+ File dir = new File(context.getCacheDir(), DOCUMENTS_DIR);
+ if (!dir.exists()) {
+ dir.mkdirs();
+ }
+
+ return dir;
+ }
+
+ private static boolean saveFileFromUri(Context context, Uri uri, String destinationPath) {
+ InputStream is = null;
+ BufferedOutputStream bos = null;
+ try {
+ is = context.getContentResolver().openInputStream(uri);
+ bos = new BufferedOutputStream(new FileOutputStream(destinationPath, false));
+ byte[] buf = new byte[1024];
+
+ int actualBytes;
+ while ((actualBytes = is.read(buf)) != -1) {
+ bos.write(buf, 0, actualBytes);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ return false;
+ } finally {
+ try {
+ if (is != null) {
+ is.close();
+ }
+ if (bos != null) {
+ bos.close();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Get the value of the data column for this Uri. This is useful for
+ * MediaStore Uris, and other file-based ContentProviders.
+ *
+ * @param context The context.
+ * @param uri The Uri to query.
+ * @param selection (Optional) Filter used in the query.
+ * @param selectionArgs (Optional) Selection arguments used in the query.
+ * @return The value of the _data column, which is typically a file path.
+ */
+ public static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) {
+ Cursor cursor = null;
+ final String column = "_data";
+ final String[] projection = {column};
+
+ try {
+ cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null);
+ if (cursor != null && cursor.moveToFirst()) {
+ final int column_index = cursor.getColumnIndexOrThrow(column);
+ return cursor.getString(column_index);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @param uri The Uri to check.
+ * @return Whether the Uri authority is ExternalStorageProvider.
+ */
+ public static boolean isExternalStorageDocument(Uri uri) {
+ return "com.android.externalstorage.documents".equals(uri.getAuthority());
+ }
+
+ /**
+ * @param uri The Uri to check.
+ * @return Whether the Uri authority is DownloadsProvider.
+ */
+ public static boolean isDownloadsDocument(Uri uri) {
+ return "com.android.providers.downloads.documents".equals(uri.getAuthority());
+ }
+
+ /**
+ * @param uri The Uri to check.
+ * @return Whether the Uri authority is MediaProvider.
+ */
+ public static boolean isMediaDocument(Uri uri) {
+ return "com.android.providers.media.documents".equals(uri.getAuthority());
+ }
+
+ /**
+ *
+ * Convert file size to string
+ *
+ * @param fileS
+ * @return
+ */
+ public static String formatFileSize(long fileS) {
+ DecimalFormatSymbols symbols = new DecimalFormatSymbols(Locale.US);
+ DecimalFormat df = new DecimalFormat("#.00", symbols);
+ String fileSizeString = "";
+ String wrongSize = "0B";
+ if (fileS == 0) {
+ return wrongSize;
+ }
+ if (fileS < 1024) {
+ fileSizeString = df.format((double) fileS) + "B";
+ } else if (fileS < 1048576) {
+ fileSizeString = df.format((double) fileS / 1024) + "KB";
+ } else if (fileS < 1073741824) {
+ fileSizeString = df.format((double) fileS / 1048576) + "MB";
+ } else {
+ fileSizeString = df.format((double) fileS / 1073741824) + "GB";
+ }
+ return fileSizeString;
+ }
+
+
+ // fix the problem that getFileExtensionFromUrl does not support Chinese
+ public static String getFileExtensionFromUrl(String url) {
+ if (!TextUtils.isEmpty(url)) {
+ int fragment = url.lastIndexOf('#');
+ if (fragment > 0) {
+ url = url.substring(0, fragment);
+ }
+
+ int query = url.lastIndexOf('?');
+ if (query > 0) {
+ url = url.substring(0, query);
+ }
+
+ int filenamePos = url.lastIndexOf('/');
+ String filename = 0 <= filenamePos ? url.substring(filenamePos + 1) : url;
+
+ // if the filename contains special characters, we don't
+ // consider it valid for our matching purposes:
+
+ // if (!filename.isEmpty() && Pattern.matches("[a-zA-Z_0-9\\.\\-\\(\\)\\%]+", filename))
+ if (!filename.isEmpty()) {
+ int dotPos = filename.lastIndexOf('.');
+ if (0 <= dotPos) {
+ return filename.substring(dotPos + 1).toLowerCase();
+ }
+ }
+ }
+
+ return "";
+ }
+
+ public static void openFile(String path, String fileName) {
+ Uri uri = FileProvider.getUriForFile(
+ TUIConfig.getAppContext(), TUIConfig.getAppContext().getApplicationInfo().packageName + FILE_PROVIDER_AUTH, new File(path));
+ if (uri == null) {
+ Log.e("FileUtil", "openFile failed , uri is null");
+ return;
+ }
+ String fileExtension;
+ if (TextUtils.isEmpty(fileName)) {
+ fileExtension = FileUtil.getFileExtensionFromUrl(path);
+ } else {
+ fileExtension = FileUtil.getFileExtensionFromUrl(fileName);
+ }
+ String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension);
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ intent.setDataAndType(uri, mimeType);
+ try {
+ Intent chooserIntent = Intent.createChooser(intent, TUIConfig.getAppContext().getString(R.string.open_file_tips));
+ chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ TUIConfig.getAppContext().startActivity(chooserIntent);
+ } catch (Exception e) {
+ Log.e("FileUtil", "openFile failed , " + e.getMessage());
+ }
+ }
+
+ public static long getFileSize(String path) {
+ File file = new File(path);
+ if (file.exists()) {
+ return file.length();
+ }
+ return 0;
+ }
+
+ public static String generateImageFilePath() {
+ String name = System.nanoTime() + "_" + Math.abs(new Random().nextInt());
+ return TUIConfig.getImageBaseDir() + name + ".jpg";
+ }
+
+ public static String generateExternalStorageImageFilePath() {
+ File dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + File.separatorChar + TUIConfig.getAppContext().getPackageName()
+ + TUIConfig.IMAGE_BASE_DIR_SUFFIX);
+ if (!dir.exists()) {
+ dir.mkdirs();
+ }
+ return dir.getAbsolutePath() + File.separatorChar + System.nanoTime() + "_" + Math.abs(new Random().nextInt()) + ".jpg";
+ }
+
+ public static String generateVideoFilePath() {
+ String name = System.nanoTime() + "_" + Math.abs(new Random().nextInt());
+ return TUIConfig.getVideoBaseDir() + name + ".mp4";
+ }
+
+ public static String generateExternalStorageVideoFilePath() {
+ File dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + File.separatorChar + TUIConfig.getAppContext().getPackageName()
+ + TUIConfig.VIDEO_BASE_DIR_SUFFIX);
+ if (!dir.exists()) {
+ dir.mkdirs();
+ }
+ return dir.getAbsolutePath() + File.separatorChar + System.nanoTime() + "_" + Math.abs(new Random().nextInt()) + ".mp4";
+ }
+
+ public static boolean saveBitmap(String path, Bitmap b) {
+ try {
+ FileOutputStream fout = new FileOutputStream(path);
+ BufferedOutputStream bos = new BufferedOutputStream(fout);
+ b.compress(Bitmap.CompressFormat.JPEG, 100, bos);
+ bos.flush();
+ bos.close();
+ return true;
+ } catch (IOException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ public static boolean isFileExists(String path) {
+ try {
+ File file = new File(path);
+ return file.exists() && file.isFile();
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ public static boolean isDirExists(String path) {
+ try {
+ File file = new File(path);
+ return file.exists() && file.isDirectory();
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ public static boolean isFileSizeExceedsLimit(Uri data, int maxSize) {
+ try {
+ Cursor returnCursor = ServiceInitializer.getAppContext().getContentResolver().query(data, null, null, null, null);
+ if (returnCursor != null) {
+ int sizeIndex = returnCursor.getColumnIndex(OpenableColumns.SIZE);
+ returnCursor.moveToFirst();
+ int size = returnCursor.getInt(sizeIndex);
+ if (size > maxSize) {
+ return true;
+ }
+ returnCursor.close();
+ return false;
+ }
+ return false;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/ImageUtil.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/ImageUtil.java
new file mode 100644
index 00000000..dd2c3758
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/ImageUtil.java
@@ -0,0 +1,300 @@
+package com.tencent.qcloud.tuikit.timcommon.util;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.media.ExifInterface;
+import android.text.TextUtils;
+
+import com.tencent.imsdk.v2.V2TIMImageElem;
+import com.tencent.qcloud.tuicore.TUIConfig;
+import com.tencent.qcloud.tuicore.TUILogin;
+import com.tencent.qcloud.tuicore.util.SPUtils;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+public class ImageUtil {
+ public static final String SP_IMAGE = "_conversation_group_face";
+
+ /**
+ * @param outFile
+ * @param bitmap
+ * @return
+ */
+ public static File storeBitmap(File outFile, Bitmap bitmap) {
+ if (!outFile.exists() || outFile.isDirectory()) {
+ outFile.getParentFile().mkdirs();
+ }
+ FileOutputStream fOut = null;
+ try {
+ outFile.deleteOnExit();
+ outFile.createNewFile();
+ fOut = new FileOutputStream(outFile);
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut);
+ fOut.flush();
+ } catch (IOException e1) {
+ outFile.deleteOnExit();
+ } finally {
+ if (null != fOut) {
+ try {
+ fOut.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ outFile.deleteOnExit();
+ }
+ }
+ }
+ return outFile;
+ }
+
+ /**
+ *
+ * Read the rotation angle of the image
+ */
+ public static int getBitmapDegree(String fileName) {
+ int degree = 0;
+ try {
+ ExifInterface exifInterface = new ExifInterface(fileName);
+ int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
+ switch (orientation) {
+ case ExifInterface.ORIENTATION_ROTATE_90:
+ degree = 90;
+ break;
+ case ExifInterface.ORIENTATION_ROTATE_180:
+ degree = 180;
+ break;
+ case ExifInterface.ORIENTATION_ROTATE_270:
+ degree = 270;
+ break;
+ default:
+ break;
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return degree;
+ }
+
+ /**
+ *
+ *
+ * Rotate the image by an angle
+ *
+ * @param bm image to be rotated
+ * @param degree Rotation angle
+ * @return rotated image
+ */
+ public static Bitmap rotateBitmapByDegree(Bitmap bm, int degree) {
+ Bitmap returnBm = null;
+
+ Matrix matrix = new Matrix();
+ matrix.postRotate(degree);
+ try {
+ returnBm = Bitmap.createBitmap(bm, 0, 0, bm.getWidth(), bm.getHeight(), matrix, true);
+ } catch (OutOfMemoryError e) {
+ e.printStackTrace();
+ }
+ if (returnBm == null) {
+ returnBm = bm;
+ }
+ if (bm != returnBm) {
+ bm.recycle();
+ }
+ return returnBm;
+ }
+
+ public static int[] getImageSize(String path) {
+ int[] size = new int[2];
+ try {
+ BitmapFactory.Options onlyBoundsOptions = new BitmapFactory.Options();
+ onlyBoundsOptions.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(path, onlyBoundsOptions);
+ int originalWidth = onlyBoundsOptions.outWidth;
+ int originalHeight = onlyBoundsOptions.outHeight;
+ // size[0] = originalWidth;
+ // size[1] = originalHeight;
+
+ int degree = getBitmapDegree(path);
+ if (degree == 0) {
+ size[0] = originalWidth;
+ size[1] = originalHeight;
+ } else {
+ float hh = 800f;
+ float ww = 480f;
+ if (degree == 90 || degree == 270) {
+ hh = 480;
+ ww = 800;
+ }
+ int be = 1;
+ if (originalWidth > originalHeight && originalWidth > ww) {
+ be = (int) (originalWidth / ww);
+ } else if (originalWidth < originalHeight && originalHeight > hh) {
+ be = (int) (originalHeight / hh);
+ }
+ if (be <= 0) {
+ be = 1;
+ }
+ BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
+ bitmapOptions.inSampleSize = be;
+ bitmapOptions.inDither = true;
+ bitmapOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ Bitmap bitmap = BitmapFactory.decodeFile(path, bitmapOptions);
+ bitmap = rotateBitmapByDegree(bitmap, degree);
+ size[0] = bitmap.getWidth();
+ size[1] = bitmap.getHeight();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return size;
+ }
+
+
+ // The image file is rotated locally, and the path of the image file after rotation is returned.
+ public static String getImagePathAfterRotate(final String imagePath) {
+ try {
+ Bitmap originBitmap = BitmapFactory.decodeFile(imagePath, null);
+ int degree = ImageUtil.getBitmapDegree(imagePath);
+ if (degree == 0) {
+ return imagePath;
+ } else {
+ Bitmap newBitmap = ImageUtil.rotateBitmapByDegree(originBitmap, degree);
+ String oldName = FileUtil.getName(imagePath);
+ File newImageFile = FileUtil.generateFileName(oldName, FileUtil.getDocumentCacheDir(TUIConfig.getAppContext()));
+ if (newImageFile == null) {
+ return imagePath;
+ }
+ ImageUtil.storeBitmap(newImageFile, newBitmap);
+ newBitmap.recycle();
+ return newImageFile.getAbsolutePath();
+ }
+ } catch (Exception e) {
+ return imagePath;
+ }
+ }
+
+ /**
+ *
+ * Convert image to circle
+ *
+ * @param bitmap Pass in a Bitmap object
+ * @return
+ */
+ public static Bitmap toRoundBitmap(Bitmap bitmap) {
+ int width = bitmap.getWidth();
+ int height = bitmap.getHeight();
+ float roundPx;
+ float left;
+ float top;
+ float right;
+ float bottom;
+ float dstLeft;
+ float dstTop;
+ float dstRight;
+ float dstBottom;
+ if (width <= height) {
+ roundPx = width / 2;
+ left = 0;
+ top = 0;
+ right = width;
+ bottom = width;
+ height = width;
+ dstLeft = 0;
+ dstTop = 0;
+ dstRight = width;
+ dstBottom = width;
+ } else {
+ roundPx = height / 2;
+ float clip = (width - height) / 2;
+ left = clip;
+ right = width - clip;
+ top = 0;
+ bottom = height;
+ width = height;
+ dstLeft = 0;
+ dstTop = 0;
+ dstRight = height;
+ dstBottom = height;
+ }
+
+ Bitmap output = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(output);
+
+ final int color = 0xff424242;
+ final Paint paint = new Paint();
+ final Rect src = new Rect((int) left, (int) top, (int) right, (int) bottom);
+ final Rect dst = new Rect((int) dstLeft, (int) dstTop, (int) dstRight, (int) dstBottom);
+ final RectF rectF = new RectF(dst);
+
+ paint.setAntiAlias(true);
+
+ canvas.drawARGB(0, 0, 0, 0);
+ paint.setColor(color);
+
+ // canvas.drawRoundRect(rectF, roundPx, roundPx, paint);
+ canvas.drawCircle(roundPx, roundPx, roundPx, paint);
+
+ paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
+ canvas.drawBitmap(bitmap, src, dst, paint);
+
+ return output;
+ }
+
+ public static Bitmap zoomImg(Bitmap bm, int targetWidth, int targetHeight) {
+ int srcWidth = bm.getWidth();
+ int srcHeight = bm.getHeight();
+ float widthScale = targetWidth * 1.0f / srcWidth;
+ float heightScale = targetHeight * 1.0f / srcHeight;
+ Matrix matrix = new Matrix();
+ matrix.postScale(widthScale, heightScale, 0, 0);
+ Bitmap bmpRet = Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.RGB_565);
+ Canvas canvas = new Canvas(bmpRet);
+ Paint paint = new Paint();
+ canvas.drawBitmap(bm, matrix, paint);
+ return bmpRet;
+ }
+
+ /**
+ *
+ * Get the image file path based on the image UUID and type
+ * @param uuid
+ * @param imageType V2TIMImageElem.V2TIM_IMAGE_TYPE_THUMB , V2TIMImageElem.V2TIM_IMAGE_TYPE_ORIGIN ,
+ * V2TIMImageElem.V2TIM_IMAGE_TYPE_LARGE
+ * @return path
+ */
+ public static String generateImagePath(String uuid, int imageType) {
+ String imageTypePreStr;
+ if (imageType == V2TIMImageElem.V2TIM_IMAGE_TYPE_THUMB) {
+ imageTypePreStr = "thumb_";
+ } else if (imageType == V2TIMImageElem.V2TIM_IMAGE_TYPE_ORIGIN) {
+ imageTypePreStr = "origin_";
+ } else if (imageType == V2TIMImageElem.V2TIM_IMAGE_TYPE_LARGE) {
+ imageTypePreStr = "large_";
+ } else {
+ imageTypePreStr = "other_";
+ }
+ return TUIConfig.getImageDownloadDir() + imageTypePreStr + uuid;
+ }
+
+ public static String getGroupConversationAvatar(String conversationID) {
+ SPUtils spUtils = SPUtils.getInstance(TUILogin.getSdkAppId() + SP_IMAGE);
+ final String savedIcon = spUtils.getString(conversationID, "");
+ if (!TextUtils.isEmpty(savedIcon) && new File(savedIcon).isFile() && new File(savedIcon).exists()) {
+ return savedIcon;
+ }
+ return "";
+ }
+
+ public static void setGroupConversationAvatar(String conversationId, String url) {
+ SPUtils spUtils = SPUtils.getInstance(TUILogin.getSdkAppId() + SP_IMAGE);
+ spUtils.put(conversationId, url);
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/LayoutUtil.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/LayoutUtil.java
new file mode 100644
index 00000000..2e4ff0d5
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/LayoutUtil.java
@@ -0,0 +1,17 @@
+package com.tencent.qcloud.tuikit.timcommon.util;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.view.View;
+
+import com.tencent.qcloud.tuicore.ServiceInitializer;
+
+public class LayoutUtil {
+
+ public static boolean isRTL() {
+ Context context = ServiceInitializer.getAppContext();
+ Configuration configuration = context.getResources().getConfiguration();
+ int layoutDirection = configuration.getLayoutDirection();
+ return layoutDirection == View.LAYOUT_DIRECTION_RTL;
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/MessageBuilder.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/MessageBuilder.java
new file mode 100644
index 00000000..266cfd5e
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/MessageBuilder.java
@@ -0,0 +1,34 @@
+package com.tencent.qcloud.tuikit.timcommon.util;
+
+import android.text.TextUtils;
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+import com.tencent.qcloud.tuikit.timcommon.bean.TUIMessageBean;
+import java.util.HashMap;
+
+public class MessageBuilder {
+ private static final String TAG = MessageBuilder.class.getSimpleName();
+
+ public static void mergeCloudCustomData(TUIMessageBean messageBean, String key, Object data) {
+ if (messageBean == null || messageBean.getV2TIMMessage() == null) {
+ return;
+ }
+ String cloudCustomData = messageBean.getV2TIMMessage().getCloudCustomData();
+ Gson gson = new Gson();
+ HashMap hashMap = null;
+ if (TextUtils.isEmpty(cloudCustomData)) {
+ hashMap = new HashMap();
+ } else {
+ try {
+ hashMap = gson.fromJson(cloudCustomData, HashMap.class);
+ } catch (JsonSyntaxException e) {
+ TIMCommonLog.e(TAG, " mergeCloudCustomData error " + e.getMessage());
+ }
+ }
+ if (hashMap != null) {
+ hashMap.put(key, data);
+ cloudCustomData = gson.toJson(hashMap);
+ }
+ messageBean.getV2TIMMessage().setCloudCustomData(cloudCustomData);
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/MessageParser.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/MessageParser.java
new file mode 100644
index 00000000..3ca8b3bb
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/MessageParser.java
@@ -0,0 +1,69 @@
+package com.tencent.qcloud.tuikit.timcommon.util;
+
+import android.text.TextUtils;
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+import com.tencent.imsdk.v2.V2TIMMessage;
+import com.tencent.qcloud.tuikit.timcommon.bean.MessageFeature;
+import com.tencent.qcloud.tuikit.timcommon.bean.MessageRepliesBean;
+import com.tencent.qcloud.tuikit.timcommon.bean.TUIMessageBean;
+import java.util.HashMap;
+import java.util.Map;
+
+public class MessageParser {
+ private static final String TAG = MessageParser.class.getSimpleName();
+
+ public static MessageRepliesBean parseMessageReplies(TUIMessageBean messageBean) {
+ V2TIMMessage message = messageBean.getV2TIMMessage();
+ String cloudCustomData = message.getCloudCustomData();
+
+ try {
+ Gson gson = new Gson();
+ HashMap hashMap = gson.fromJson(cloudCustomData, HashMap.class);
+ if (hashMap != null) {
+ Object repliesContentObj = hashMap.get(TIMCommonConstants.MESSAGE_REPLIES_KEY);
+ MessageRepliesBean repliesBean = null;
+ if (repliesContentObj instanceof Map) {
+ repliesBean = gson.fromJson(gson.toJson(repliesContentObj), MessageRepliesBean.class);
+ }
+ if (repliesBean != null) {
+ if (repliesBean.getVersion() > MessageRepliesBean.VERSION) {
+ return null;
+ }
+ return repliesBean;
+ }
+ }
+ } catch (JsonSyntaxException e) {
+ TIMCommonLog.e(TAG, " getCustomJsonMap error ");
+ }
+ return null;
+ }
+
+ public static MessageFeature isSupportTyping(TUIMessageBean messageBean) {
+ String cloudCustomData = messageBean.getV2TIMMessage().getCloudCustomData();
+ if (TextUtils.isEmpty(cloudCustomData)) {
+ return null;
+ }
+ try {
+ Gson gson = new Gson();
+ HashMap featureHashMap = gson.fromJson(cloudCustomData, HashMap.class);
+ if (featureHashMap != null) {
+ Object featureContentObj = featureHashMap.get(TIMCommonConstants.MESSAGE_FEATURE_KEY);
+ MessageFeature messageFeature = null;
+ if (featureContentObj instanceof Map) {
+ messageFeature = gson.fromJson(gson.toJson(featureContentObj), MessageFeature.class);
+ }
+ if (messageFeature != null) {
+ if (messageFeature.getVersion() > MessageFeature.VERSION) {
+ return null;
+ }
+
+ return messageFeature;
+ }
+ }
+ } catch (JsonSyntaxException e) {
+ TIMCommonLog.e(TAG, " isSupportTyping exception e = " + e);
+ }
+ return null;
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/PopWindowUtil.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/PopWindowUtil.java
new file mode 100644
index 00000000..4f517a55
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/PopWindowUtil.java
@@ -0,0 +1,19 @@
+package com.tencent.qcloud.tuikit.timcommon.util;
+
+import android.graphics.drawable.ColorDrawable;
+import android.view.Gravity;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.PopupWindow;
+
+public class PopWindowUtil {
+
+ public static PopupWindow popupWindow(View windowView, View parent, int x, int y) {
+ PopupWindow popup = new PopupWindow(windowView, WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT);
+ popup.setOutsideTouchable(true);
+ popup.setFocusable(true);
+ popup.setBackgroundDrawable(new ColorDrawable(0xAEEEEE00));
+ popup.showAtLocation(windowView, Gravity.CENTER | Gravity.TOP, x, y);
+ return popup;
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/ScreenUtil.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/ScreenUtil.java
new file mode 100644
index 00000000..08567d9c
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/ScreenUtil.java
@@ -0,0 +1,39 @@
+package com.tencent.qcloud.tuikit.timcommon.util;
+
+import android.content.Context;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+import android.view.WindowManager;
+import com.tencent.qcloud.tuicore.TUIConfig;
+
+public class ScreenUtil {
+ private static final String TAG = ScreenUtil.class.getSimpleName();
+
+ public static int getScreenHeight(Context context) {
+ DisplayMetrics metric = new DisplayMetrics();
+ WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ wm.getDefaultDisplay().getMetrics(metric);
+ return metric.heightPixels;
+ }
+
+ public static int getScreenWidth(Context context) {
+ DisplayMetrics metric = new DisplayMetrics();
+ WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ wm.getDefaultDisplay().getMetrics(metric);
+ return metric.widthPixels;
+ }
+
+ public static int getPxByDp(float dp) {
+ float scale = TUIConfig.getAppContext().getResources().getDisplayMetrics().density;
+ return (int) (dp * scale + 0.5f);
+ }
+
+ public static int dip2px(float dpValue) {
+ final float scale = TUIConfig.getAppContext().getResources().getDisplayMetrics().density;
+ return (int) (dpValue * scale + 0.5f);
+ }
+
+ public static float dp2px(float dpValue, DisplayMetrics displayMetrics) {
+ return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, displayMetrics);
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/SoftKeyBoardUtil.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/SoftKeyBoardUtil.java
new file mode 100644
index 00000000..d41cd87d
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/SoftKeyBoardUtil.java
@@ -0,0 +1,59 @@
+package com.tencent.qcloud.tuikit.timcommon.util;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.util.DisplayMetrics;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.inputmethod.InputMethodManager;
+import com.tencent.qcloud.tuicore.TUIConfig;
+
+public class SoftKeyBoardUtil {
+ public static void hideKeyBoard(IBinder token) {
+ InputMethodManager imm = (InputMethodManager) TUIConfig.getAppContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (imm != null) {
+ imm.hideSoftInputFromWindow(token, 0);
+ }
+ }
+
+ public static void hideKeyBoard(Window window) {
+ InputMethodManager imm = (InputMethodManager) TUIConfig.getAppContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (imm != null) {
+ if (isSoftInputShown(window)) {
+ imm.toggleSoftInput(0, 0);
+ }
+ }
+ }
+
+ public static void showKeyBoard(Window window) {
+ InputMethodManager imm = (InputMethodManager) TUIConfig.getAppContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (imm != null) {
+ if (!isSoftInputShown(window)) {
+ imm.toggleSoftInput(0, 0);
+ }
+ }
+ }
+
+ private static boolean isSoftInputShown(Window window) {
+ View decorView = window.getDecorView();
+ int screenHeight = decorView.getHeight();
+ Rect rect = new Rect();
+ decorView.getWindowVisibleDisplayFrame(rect);
+ return screenHeight - rect.bottom - getNavigateBarHeight(window.getWindowManager()) >= 0;
+ }
+
+ private static int getNavigateBarHeight(WindowManager windowManager) {
+ DisplayMetrics metrics = new DisplayMetrics();
+ windowManager.getDefaultDisplay().getMetrics(metrics);
+ int usableHeight = metrics.heightPixels;
+ windowManager.getDefaultDisplay().getRealMetrics(metrics);
+ int realHeight = metrics.heightPixels;
+ if (realHeight > usableHeight) {
+ return realHeight - usableHeight;
+ } else {
+ return 0;
+ }
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TIMCommonConstants.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TIMCommonConstants.java
new file mode 100644
index 00000000..937a2de4
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TIMCommonConstants.java
@@ -0,0 +1,11 @@
+package com.tencent.qcloud.tuikit.timcommon.util;
+
+public class TIMCommonConstants {
+ public static final String MESSAGE_REPLY_KEY = "messageReply";
+ public static final String MESSAGE_REPLIES_KEY = "messageReplies";
+ public static final String MESSAGE_REACT_KEY = "messageReact";
+ public static final String MESSAGE_FEATURE_KEY = "messageFeature";
+ public static final String CHAT_SETTINGS_SP_NAME = "chat_settings_sp";
+ public static final String CHAT_REPLY_GUIDE_SHOW_SP_KEY = "chat_reply_guide_show";
+
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TIMCommonLog.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TIMCommonLog.java
new file mode 100644
index 00000000..3b642aa6
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TIMCommonLog.java
@@ -0,0 +1,77 @@
+package com.tencent.qcloud.tuikit.timcommon.util;
+
+import com.tencent.imsdk.common.IMLog;
+
+public class TIMCommonLog extends IMLog {
+ private static final String PRE = "TIMCommon-";
+
+ private static String mixTag(String tag) {
+ return PRE + tag;
+ }
+
+ /**
+ *
+ * print INFO level log
+ *
+ * @param strTag TAG
+ * @param strInfo
+ */
+ public static void v(String strTag, String strInfo) {
+ IMLog.v(mixTag(strTag), strInfo);
+ }
+
+ /**
+ *
+ * print DEBUG level log
+ *
+ * @param strTag TAG
+ * @param strInfo
+ */
+ public static void d(String strTag, String strInfo) {
+ IMLog.d(mixTag(strTag), strInfo);
+ }
+
+ /**
+ *
+ * print INFO level log
+ *
+ * @param strTag TAG
+ * @param strInfo
+ */
+ public static void i(String strTag, String strInfo) {
+ IMLog.i(mixTag(strTag), strInfo);
+ }
+
+ /**
+ *
+ * print WARN level log
+ *
+ * @param strTag TAG
+ * @param strInfo
+ */
+ public static void w(String strTag, String strInfo) {
+ IMLog.w(mixTag(strTag), strInfo);
+ }
+
+ /**
+ *
+ * print WARN level log
+ *
+ * @param strTag TAG
+ * @param strInfo
+ */
+ public static void w(String strTag, String strInfo, Throwable e) {
+ IMLog.w(mixTag(strTag), strInfo + e.getMessage());
+ }
+
+ /**
+ *
+ * print ERROR level log
+ *
+ * @param strTag TAG
+ * @param strInfo
+ */
+ public static void e(String strTag, String strInfo) {
+ IMLog.e(mixTag(strTag), strInfo);
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TIMCommonUtil.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TIMCommonUtil.java
new file mode 100644
index 00000000..dfa25303
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TIMCommonUtil.java
@@ -0,0 +1,12 @@
+package com.tencent.qcloud.tuikit.timcommon.util;
+
+import static com.tencent.qcloud.tuicore.TUIConstants.TUIConversation.CONVERSATION_C2C_PREFIX;
+import static com.tencent.qcloud.tuicore.TUIConstants.TUIConversation.CONVERSATION_GROUP_PREFIX;
+
+public class TIMCommonUtil {
+
+ public static String getConversationIdByID(String chatID, boolean isGroup) {
+ String conversationIdPrefix = isGroup ? CONVERSATION_GROUP_PREFIX : CONVERSATION_C2C_PREFIX;
+ return conversationIdPrefix + chatID;
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TUIUtil.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TUIUtil.java
new file mode 100644
index 00000000..d4b680c4
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TUIUtil.java
@@ -0,0 +1,82 @@
+package com.tencent.qcloud.tuikit.timcommon.util;
+
+import android.app.Activity;
+import android.app.Application;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.text.TextUtils;
+import com.tencent.imsdk.v2.V2TIMManager;
+import com.tencent.qcloud.tuicore.TUIThemeManager;
+import com.tencent.qcloud.tuikit.timcommon.R;
+import java.lang.reflect.Method;
+
+public class TUIUtil {
+ private static String currentProcessName = "";
+
+ public static String getProcessName() {
+ if (!TextUtils.isEmpty(currentProcessName)) {
+ return currentProcessName;
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ currentProcessName = Application.getProcessName();
+ return currentProcessName;
+ }
+
+ try {
+ final Method declaredMethod = Class.forName("android.app.ActivityThread", false, Application.class.getClassLoader())
+ .getDeclaredMethod("currentProcessName", (Class>[]) new Class[0]);
+ declaredMethod.setAccessible(true);
+ final Object invoke = declaredMethod.invoke(null, new Object[0]);
+ if (invoke instanceof String) {
+ currentProcessName = (String) invoke;
+ }
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+
+ return currentProcessName;
+ }
+
+ public static int getDefaultGroupIconResIDByGroupType(Context context, String groupType) {
+ if (context == null || TextUtils.isEmpty(groupType)) {
+ return R.drawable.core_default_group_icon_community;
+ }
+ if (TextUtils.equals(groupType, V2TIMManager.GROUP_TYPE_WORK)) {
+ return TUIThemeManager.getAttrResId(context, R.attr.core_default_group_icon_work);
+ } else if (TextUtils.equals(groupType, V2TIMManager.GROUP_TYPE_MEETING)) {
+ return TUIThemeManager.getAttrResId(context, R.attr.core_default_group_icon_meeting);
+ } else if (TextUtils.equals(groupType, V2TIMManager.GROUP_TYPE_PUBLIC)) {
+ return TUIThemeManager.getAttrResId(context, R.attr.core_default_group_icon_public);
+ } else if (TextUtils.equals(groupType, V2TIMManager.GROUP_TYPE_COMMUNITY)) {
+ return TUIThemeManager.getAttrResId(context, R.attr.core_default_group_icon_community);
+ }
+ return R.drawable.core_default_group_icon_community;
+ }
+
+ public static Drawable newDrawable(Drawable drawable) {
+ if (drawable == null) {
+ return null;
+ }
+ Drawable.ConstantState state = drawable.getConstantState();
+ if (state != null) {
+ return state.newDrawable().mutate();
+ }
+ return drawable.mutate();
+ }
+
+ public static String identityHashCode(Object object) {
+ return System.identityHashCode(object) + "";
+ }
+
+
+ public static boolean isActivityDestroyed(Context context) {
+ if (context instanceof Activity) {
+ if (((Activity) context).isFinishing() || ((Activity) context).isDestroyed()) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TextUtil.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TextUtil.java
new file mode 100644
index 00000000..8b7763e1
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TextUtil.java
@@ -0,0 +1,155 @@
+package com.tencent.qcloud.tuikit.timcommon.util;
+
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.text.Layout;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.TextPaint;
+import android.text.method.LinkMovementMethod;
+import android.text.style.ClickableSpan;
+import android.text.style.URLSpan;
+import android.text.util.Linkify;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+
+import com.tencent.qcloud.tuicore.TUIThemeManager;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+public class TextUtil {
+
+ public static final Pattern PHONE_NUMBER_PATTERN =
+ Pattern.compile("(\\+?(\\d{1,4}[-\\s]?)?)?(\\(?\\d+\\)?[-\\s]?)?[\\d\\s-]{5,14}");
+
+ public static void linkifyUrls(TextView textView) {
+ Linkify.addLinks(textView, Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES);
+ Linkify.addLinks(textView, PHONE_NUMBER_PATTERN, "tel:");
+ SpannableString spannableString = new SpannableString(textView.getText());
+
+ URLSpan[] urlSpans = spannableString.getSpans(0, spannableString.length(), URLSpan.class);
+ int urlColor = 0xFF6495ED;
+ if (TUIThemeManager.getInstance().getCurrentTheme() != TUIThemeManager.THEME_LIGHT) {
+ urlColor = 0xFF87CEFA;
+ }
+ if (urlSpans != null) {
+ for (URLSpan span : urlSpans) {
+ int start = spannableString.getSpanStart(span);
+ int end = spannableString.getSpanEnd(span);
+ spannableString.removeSpan(span);
+ spannableString.setSpan(new TextLinkSpan(span.getURL(), urlColor), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+ GestureDetector gestureDetector = new GestureDetector(textView.getContext(), new GestureDetector.SimpleOnGestureListener() {
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ if (!textView.isActivated()) {
+ return false;
+ }
+ ClickableSpan[] spans = findSpansByLocation(textView, Math.round(e.getX()), Math.round(e.getY()));
+ if (spans != null && spans.length > 0) {
+ ClickableSpan span = spans[0];
+ span.onClick(textView);
+ }
+ return false;
+ }
+ });
+ textView.setText(spannableString);
+ textView.setMovementMethod(new LinkMovementMethod() {
+ @Override
+ public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
+ gestureDetector.onTouchEvent(event);
+ return false;
+ }
+ });
+ }
+
+ public static ClickableSpan[] findSpansByLocation(TextView textView, int x, int y) {
+ if (textView == null || !(textView.getText() instanceof Spannable)) {
+ return null;
+ }
+ Spannable spannable = (Spannable) textView.getText();
+ Layout layout = textView.getLayout();
+ int offset = getPreciseOffset(textView, x, y);
+ ClickableSpan[] spans = spannable.getSpans(offset, offset, ClickableSpan.class);
+ List result = new ArrayList<>();
+ for (ClickableSpan span : spans) {
+ int spanStart = spannable.getSpanStart(span);
+ int spanEnd = spannable.getSpanEnd(span);
+ Path path = new Path();
+ layout.getSelectionPath(spanStart, spanEnd, path);
+ RectF rect = new RectF();
+ path.computeBounds(rect, true);
+ Region region = new Region();
+ Region pathClipRegion = new Region((int) rect.left, (int) rect.top, (int) rect.right, (int) rect.bottom);
+ region.setPath(path, pathClipRegion);
+ if (region.contains(x, y)) {
+ result.add(span);
+ }
+ }
+ return result.toArray(new ClickableSpan[] {});
+ }
+
+ private static int getPreciseOffset(TextView textView, int x, int y) {
+ Layout layout = textView.getLayout();
+ if (layout != null) {
+ int topVisibleLine = layout.getLineForVertical(y);
+ int offset = layout.getOffsetForHorizontal(topVisibleLine, x);
+
+ int offsetX = (int) layout.getPrimaryHorizontal(offset);
+
+ if (offsetX > x) {
+ return layout.getOffsetToLeftOf(offset);
+ } else {
+ return offset;
+ }
+ } else {
+ return -1;
+ }
+ }
+
+ public static class TextLinkSpan extends URLSpan {
+ private final int color;
+
+ public TextLinkSpan(String url, int color) {
+ super(url);
+ this.color = color;
+ }
+
+ @Override
+ public void updateDrawState(@NonNull TextPaint ds) {
+ ds.setColor(color);
+ }
+ }
+
+ public static class ForegroundColorClickableSpan extends ClickableSpan {
+ private final int color;
+ private final View.OnClickListener listener;
+
+ public ForegroundColorClickableSpan(int color, View.OnClickListener listener) {
+ super();
+ this.color = color;
+ this.listener = listener;
+ }
+
+ @Override
+ public void updateDrawState(@NonNull TextPaint ds) {
+ ds.setColor(color);
+ }
+
+ @Override
+ public void onClick(@NonNull View widget) {
+ if (listener != null) {
+ listener.onClick(widget);
+ }
+ }
+ }
+}
diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/ThreadUtils.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/ThreadUtils.java
new file mode 100644
index 00000000..4782eb95
--- /dev/null
+++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/ThreadUtils.java
@@ -0,0 +1,38 @@
+package com.tencent.qcloud.tuikit.timcommon.util;
+
+import android.os.Handler;
+import android.os.Looper;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class ThreadUtils {
+ private static final Handler handler = new Handler(Looper.getMainLooper());
+ private static final ExecutorService executors = Executors.newCachedThreadPool();
+
+ private ThreadUtils() {}
+
+ public static void execute(Runnable runnable) {
+ executors.execute(runnable);
+ }
+
+ public static boolean isOnMainThread() {
+ return Thread.currentThread() == Looper.getMainLooper().getThread();
+ }
+
+ public static void runOnUiThread(Runnable runnable) {
+ if (isOnMainThread()) {
+ runnable.run();
+ } else {
+ postOnUiThread(runnable);
+ }
+ }
+
+ public static boolean postOnUiThread(Runnable runnable) {
+ return handler.post(runnable);
+ }
+
+ public static boolean postOnUiThreadDelayed(Runnable runnable, long delayMillis) {
+ return handler.postDelayed(runnable, delayMillis);
+ }
+}
diff --git a/timcommon/src/main/res-light/drawable-ldrtl/chat_bubble_other_bg_light.xml b/timcommon/src/main/res-light/drawable-ldrtl/chat_bubble_other_bg_light.xml
new file mode 100644
index 00000000..e68d78c9
--- /dev/null
+++ b/timcommon/src/main/res-light/drawable-ldrtl/chat_bubble_other_bg_light.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/timcommon/src/main/res-light/drawable-ldrtl/chat_bubble_self_bg_light.xml b/timcommon/src/main/res-light/drawable-ldrtl/chat_bubble_self_bg_light.xml
new file mode 100644
index 00000000..4803d45a
--- /dev/null
+++ b/timcommon/src/main/res-light/drawable-ldrtl/chat_bubble_self_bg_light.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/timcommon/src/main/res-light/drawable-xxhdpi/core_default_group_icon_community_light.png b/timcommon/src/main/res-light/drawable-xxhdpi/core_default_group_icon_community_light.png
new file mode 100644
index 0000000000000000000000000000000000000000..67d36c20fa2caccdd4b69b041c7aee0bebaf0946
GIT binary patch
literal 5871
zcmd^D_d6S2)K4OcnoVsjDPkA3i%M)&n;NxSqb2sxZ}B>=Q*Ey&vVYX=bQusJx$u{?AHMR0PRC9H6zlB`)^QQBh5w&
zND9&d^fA&@0lXi%wFLk$S3OiyHVL%Z&8L3*c*^fv1aSn1XEn6f_<1-Xq?8y{F2gND
zTijc1_iU!C_GDshw2_IX+sOHc
ze2DeNbxUXC&J_FiFY4-=rrdO`kSii9uW))}^FArg*BN?$y7*dHRS1pg|#a%|Lp
z>K1vfALRz_6Nngg2)TuqwDZ(J31Dp!5gzIYEvS6ww0ZB8z9eEzo1~(wm$*RJW$c}&4ZWa$@h)}-Uckvq^DoCWAOp4)lLkDAI^1qchf7M
zh^eEk7}6{TwW$wh7TkO7>(W2!{2W#u9{#6jG-R|cmVG(x{|kZpsOo05*=yMJWoV(8@N-pOV`RQ-Mcs`7Rf53GLY=o`-$13!?aNHpLf4K)2E1jE
zjQJpyQ=dafOC*;ozDD~s6ugj>w(4Rm*{>9{3Oc^JIeGEJ1S={&LsRh7R@XwKl34O2
z9SY9la0nWn)N8D%wU>n-
zpoWWe$!@7A2*Qthj8XsyS-kiW2t#cnif1v*x$%XD^pjS-6x+iAUdwEwfQUZ$nsKTN1d
zVqL4)L4f&Uw!j#~NLH4#&B0;)ap(a>*Cv_WuS?I(J#h!X(SGh7L=6w(;+pvpP$ckX
zR$UxzvN*cFiKZ?;%S-h4pQ9WQg8_&Uz`VBI-1?=QPwuJ5TTiJZF1l!n-~45Dw9Hpj
zv2!8vTN~SF0G!qN*wKKK5HIiB4k#VW51pp2n76?y4}wmu?n?Y)uWkKw{x5%7X%n
zSN3b&?&Vsd$yAA^+bWzUAm5lQfgoe*Nt5oIH&@Y5WY;a&eC9mac4JLcqN=cQRn&F{
z@ci3CYOi<}YQUS6ie6q{b#ub$Lc?N+9fWxvK*6D*ZIh-IdZN60f9Yj@CfKVx?@<8+
z$|7W>XnKAr8P{F=Do+6-AJNG_TYMGWK9tVmsUD0cfA3?b7fK@d}r9=#87%Hz6zIIO2SLg>y+cnkG%rULWVu
zMj_2%fUMoYnM>(~pkP7={NU~qd2n0Tjg=C=5!ghsw>An&`HLI3%tg_b|Dy?PewxFB
z3!(j9p9+B*Y}Am?MgnqIxKdtP>Tg+qLh)5RxPzAO`gak~iy1%EF6vkqcT=;Aeq^VB
zX~n9^0P?Eyvcd9(O-KJOe4G_ZNihj(rjJ^0p7EmSI)Bs=a5fR~l0M4TYEJ|;q|+}$
zvv|82Oc{mqpFp=q?}X4q!L9c7T>QQ%p?5s+;J{7Cx8l?;e(X{X*c$TZsqY0k+o6Y|
z(T2nx>=m|V=_sb-x3l=O8#L9RIi6B829o>l+8-Mi&4iJc=l^Cd!ni6A317X_6+aL)
zZ@TBBBBHfiWAIl*ci4fp{>L{UN3PvT0sbqKyRb@t+e`f3Xt%dKlFsBIz9yI*LNa+#
z-wVUOklt<>PlvOr)xRQEs_+P6K>-bQ)hXMYHp&WGK>)Y3KTFfsjkzWnsu;xD{B
zPbMKpABWhJ|ImcY|iS1<#
zox(9-|FMg!#o|*J;6aSVG?-m7r_eEaAx7+DAhfPi9n!`BRhJ99Kx3vzlZpi@xbnp-
zhQM0qCrRF4mWI#>e;9qaTaO#&Z!c|C8n*wub))IewRpdvb=CH8&d#Gq(ZKZydf=WR
z=ofYMa>n5)vz!yW-Crv7*e<(&Sl|9kt=-bhRDu{7%6C7a5b1T9dgYbF^_7r)m*pMb
zgXMzwVu_6;Fg!N@tV2pb
ze$nE&*jIln1kEaN5idH8Xnklxh_>(OwhzIDtLBN>8m^A83?_))Uu+44)jrubsx>?I
zs_68c6uWj|UT6qXi!_e1`x~K_4y!9Fl}yKa93M-vmB#XhNp6ik2=bjwjS5AM3IW%m
zBowTJ-wUNbxho-B`s_^!IG#)oUQI;Yw=7>C$`Xw(ElXQNDd@3jqg2K+$E>Zm43ShZ
zJ?rU$MyG%G^#5J{@O@FrXu@8}C@XMywI!61a~z|BRo+jfCE3g!gR}8UqdP{K*4wcU
z*$<1PJ?&_BT3!if2)&$@gmsUHoc)>UPHe&V@_z`97H;GR9-xE*>X9Meb+=-V)Q|)r
zASP-&^`sc@t%qRr|7wFT3$tE-i(Qy;^!OtbRxHpC9@wvoTo!v7yWjOn*zS~_b#{il
z-&^noW;@+Hq8_|SGtz8iX}-M4T$93a-7F=lgy5q+f2mzhQ_&JQ?=kMBoU$`sy!rY-
z%?PU0$ZB)-^>$BYtaLtS^5cGmo8_e7n(u2d$tOD+vDPKNr6%{Z-mT{I+F?Z-LhEl5
z$iLU^n3!w{&Uj=R?dYX50_yzNP@~Q#F~tt%kAk2de6`7OO#g#_uo6-s2P&De(vy+4wX^a9|nB
zT0k%7kx?&=vjbz-%yAbkKya<^!%0)t?-b$QGI`-|=SK`GBWwZq{H1OB0j=b?;f(|Z|5a7|V3`Y5VozhnMkYYwZ1DW^
zB)H>XR(vD8&GPSE^ok7z)tZT1q+wB{9PRw+|883YGh3O41ofFXcWb=$<_;zNI+28z
zIw$|ybci%A+x-c;Q}q?>Jc4R7b;TUbPQv?hR?qw3PL;xOcd4zH2q|PMJFF_dPqXWy
zmOe#Z{efDutj_yBWfTI+~aHC0&kqf7=+LL@LVkUfDe-$
zD-JtOeLi!Sw3jMV5!CUVvM+}dy
zF0lkVO;}Azr*uwAU*^761$5~#I-bpWPN)ABDmKGm^Vdzoo9?3an>6`Q$PP+nfSAu-
z4U~5?}OvQ!<5tk7%<4{udZxp1nCX=-0t6hnzifDc?msL0#m*Kb{1=Kz|T5~
zOa=@6>cT5o{B~1uHZ5j?_rVOY*)h!$S7MQ#d6CaeK#U#I#TS$oeb+kVG|k{|75`qd
zIx^_Ir!qiQNaxx#`gt?bD)wn}520lf{V<7L<#B6slscIFyrX(ZqV4AM@H)^hX8
z2OT2+cDT}ix-BF1?;U7a8xj=;PGrq5*4?|=X{P02nme>``I~f=bIkfwK)U>GNW!U<1Z+vP
z1>ogtaOWNhOL)Jr8mDRBhg)(6I!Gvc7o;dG)KwE7k)+Cyrx|K9<0z6g9sp9s{5HVB
zCDgncj}A&d-@W(PiR(24iaO}M)5u8WIij!Zq#MiwY+ak9&(ihh|MSZT^tGrC=#a?`
z9J#C-Buvy$rAgP6gDKJNUI`-Vpm_4lU2t0T*46R6^++5ik;Ep4^gYsSBNSInuZ1;-<=PEcG77Et5!jvMkU&t_AmsMpT>KALr}~v
z6oxPl<#}WVQj7aw4Ae$_IMK*@500lPfZu0c^OG^-jf
z)=7W$k+QW}ZoDGF=FH5Sa)qeE=n+=(r&8{XfqW=}i6(QbK01YE#_o~NmaFT_i+jt4E0UxS1CXh$T#YC__-L(hI#T`lWw3|RGV>W{!+W<~(AD?-B;{}kDh4u7
zp<+c+y_=~CcM(ohLr@v;dY+jFLD67=)bL4D#sm}ut+jI0aDX5|oEMdK0{A0yac#@yyWDke8HLb4N~m8z3V=K}RL6#ag7qUmaAD`i6o89c*g4si%{Q|3t7JmoPmE
zS|3aOk;qLddKszmluS8E&_D@S3zIrGi4`IjR{MXvP^1))H%C2B&^z&*IZWrqra!p0
zDjx}N_@E$Km;ou8Cn9WT$tX+x7QYCQ+AgA95>`~-$rjP|JA&W^I}eMeaXPI&v`M3-
zBQ)vabrtMY`;Wo$Du{(k#;hC_R1NJp>+^KQio_i5Gyr%U{>T#Kc}eRE)$PBI_ZX|y!)wcx0KJ*cYv0RownN5m+Tao
zUTv-uilC7zkM)5#h2c*r0JAw4h4`LQ)@t`O7i~74m4!l?C5a2U>dzxc@jOT2x2{iI
z-CMKM*OdIC2il1(FWig2d4_kJNLu_huRnUJU(yVi!tk;@z+RGv2#PxO7;H?8?}E$n?LBJOy6k5{!OjQZqMD
zHyve`i5`Uo83ty#x(<1uQ?E2t8?v=lpWAz_QL+Hf2ztKYd&Q!iRAx)G}
z;82pu^4YG)iQRnzmjyOziJxe6v{kKyKa&$AOJu2KE~05J9eTH-845ROo0BPcUiIsT
zUt7-l$4u-qP@l&(GEsh_mEhNUm35ia<1HLV)-+AUTb-31G^1r*Dlt(B@kkN-CK7Qr
zwzPi`y3ATWEsKXv=r+x)TkUSCylB%+pPrs`nePk2owB9?`~|&GM~AOtalv*CZqM|p
zx#;`2&4MSn;@(7d@(5r!TY4a!Dd|0p&1v%UZT{5VKUU12vCcWMn%d8Gv%5$e=w?OtzJu@Hz&~xBl&{TA`l`GshH`P2Yh}=Z9>aoJD-A=h4B6Iv$WZ*no%X
LdTQ@g?4te$6A%k+
literal 0
HcmV?d00001
diff --git a/timcommon/src/main/res-light/drawable-xxhdpi/core_default_group_icon_meeting_light.png b/timcommon/src/main/res-light/drawable-xxhdpi/core_default_group_icon_meeting_light.png
new file mode 100644
index 0000000000000000000000000000000000000000..f32ec4721c14159fef1698db571130ab2f048a75
GIT binary patch
literal 3318
zcmb`K*FPJK7sg}NDDl=PC80HHhL)(k_lUiB?NNKL64Vw|uN5m+tlDap8bzxWDWM3p
zw-_~QRQ>w?55J4&d@r7Jb3W(hd6M;Y)oG|$r~m)}ji!c*;a`UTCyG0Nr>}+j`d<(Q
z7^*7)8Ze+80D!tlQ$^7@)Rs`>`GRqjarCw|;^w2?lRUtP*fT%4KAk#QuAv-MA`(@@
z@=meM;GPLC(Ks88)zdIAQC0FV(N|%tF=1phc&Ex`QgKI4UrWFsg~sFs#lS#}UwT~g
zz%uzrM#SNPvFkaamzin5H{#d9kD|=ibZZY>Fg_Fc`J!82pXfjeJpmHTYpFuTby%7;
zMgYySIVB3DfZ+_n(M~>s`vH3G-N<|gv1cJYW{m{mEouOXBf8h}ED*e@X
z)uwCfzAR{#=!lM}n7?jSosw&|iRca}?dSH7;_md2*Yt5`D|V>bKj_mlO2B8e
zC5sMkaeAxj&f`ZhmmWdj$nN%gpNb~IoMrM8{21{A;?cg3yXmGgwN409H`NCCb8)Ll
z|4m*Je3^nP&<3wT>IQt7X3_=?CN{7A8mtzZvw3|7&SvC2o)-Ut9VY>Qyj$zkNy@NI
zO)i*Nr67QPl&PSRPy@Au1CvRWM06^@Q3Yyn{qyEIf4V73nFIcs+Q_|)7)(bQ@06!i
z&ba+r<003OF-yGyxfL-7K1R0C=OM;%qf{^g$_ftn5hgQBd&u1t!bQ1B}x<89~?^r;6+CsRgwR3wAVx2L~DB0=Ca$4;M*!&RA$?w@9$
zdyc3^j(+YY6dxm86!CmWp)f#j3^R~RmD1eTL{U;=6u-M*U0T%2=R`I?I&xG(t}GD{
zO>xV@rV+VPKFJOIoir7%*g3H
z>_b}hUb?EELlb-S&f(C9Ls||Xf@54uofChrf{qsp4Uv07-JB9$+C;i!*F?dZnlcR1
zd3E2A)C_qthkiNMW-yn~MoxN$!#;c&(ZkAfNSl-#g=mPIAy(vr17OP<7jX&j?%%}k
zn?w2ftd04d1h@1kzrzOfzMsn{Tpe$Yv%0Z8P-|{T&ETgv={QuJ`hC$4eN9HwQ(_s*
zE@&Pv1EU}0{>T5-{^RAHc@9GMW)u#3T!3Gk4cRmLXzzePrv}BAq&6zjum1@Jy$cJw
z2y#w0cM78k<(HB6wQReoK4F+Pa=E60x*sftf|mFM
zMy;~N92&LQYUjMOxLlsARb91RR@C9@CkEzoEE;CMOE<3kit5eVYL&!`qc#RTwLi;%
ztWBkxk-=1RkR(wr^@gsUwRyi@n$bH4S03=qyVwfK@`e7EeVIu8Yw9r$3wm-IQO35L
zUlp!WWE8yjshFvbl5SphTv$Of#As4={qW`1h;Kq
zNk5ITS@anyJd75(Y$;-(%s>CJXJzFZlos!0lc^?eCF%;)Z43okq*box$jtp3S$XuL
zcb;GO>pF8$+GxF$J2`~OPTtBm*+$PKsh2n3uSzwx?*Uj{8Lz&IRc=@JVsev0n#v|}
z$iAV&;(R0%ZaTV;P7ZYK1Tm9TNS*T}cY70CImC2>S|6GR5=nVwDwqPx6}ErKRssO8
zS?>q2n$48z>-|}}ie@k0CrS6x#;aPhfbThKZz*aZ6_pDg-PX8}J!8yHfoQe}!KAmD
zgzWm48L;c2?pjaXt^n)iy3}WD)1DlS3{9
zrS$5V;YUcPPHszTv$|QbQF=Al!ePS)Zml+*PTf;A--7G^T-(d{O&8nN>)v%k79_%y
zRpQ^@p7K}iHMs9xYL*kgk(XoK_nMVb2LiuNyAyd`wm3@YTf`BZ@7m-S+_-fUETgI$
z$)%+mOQEm>g9*i{LREamss<^K$9H?b^Pv^_&$S&(&~X1ACRJtp`YLvB%5y?7>)BM!
z7Hhzlqn=vJ@3O`(Nnq>^r-M-)*wUtI7$sZXz0sG^q1
zufvbRT_Y?JBC_O==b*+Lkm=?|qE@+;x_VO=fin@axc*p7PjM>8)@WaA1HCOw~B>G!mxJgA;qQ@p}n54YKJ2}O$PscB#*=>7%JpN5#tCki{
zg;WaSGdiA_7C3S^UTA|C6Lzv8mGj3)gV;a#W!cEq8~V)S9p{9^4%C3}QrcQvRms3r
ztiyz&cE-Zzc`>E(?c0)y8d6D~OmRY93#@#Cns&8PfW2HlHFWm`NaNOHY8bna1)w*+
z0*f40hUa`@s3|a74y}49CIv!G{e52F#@xE50kff=U`mOgb*!DSU;t>Bfqx2iy&VV{
z*+;pSjZf-)$$O^tjT@_5Zm;x?ISWWH+-|nm%xl{=oF*v?@n;QmYd5o%(s|a_p2VyF
zV1Cc(DL3p_29wZ$u+5XjQvbY)NivVb%U*$mGZ{`zv_EAD7x6EpX=2
zuF7VTFi5sLT$%s|K)QK72W#eX`U1M=V%{tkgvStMOovnr(l7DAV5
zD1QZ!APT%RTEX4Ec61K>Bo~YE>n>JyJ^qwcL-+fA26SOshyvd?UfLc3ZbtqUm%6HN
zY+8I5-U@GR>GRCZy}QZ~6TmJT&c7`{y>ZvB9G|bqO;NdUTJAJ4;~v!G)a+<%_q}T>
zLa45Tpfii{xzk)EpVGT`Tacp~-w1nE3xLfEnxGpTbp=}J-vapSHy4b0aU>-;y^hAD
z$jJ{|GfQ;>9AFJYo2y*~H__@dkm1iJ5~BG&4I|n@$v8G%tWwW(
z)+>YtIq)<-SpN9lq|Ex)pFa-$FyG{!)v`SbI_0bEoYX01w9U!lVG}KtZoNx4K}DPD
znnZJK{w3(_t#1ln*X;Y7cjo@~DquN=;_giuw-l%4xP
z607-P94ZpCLw5O9@WEXvQ9nucY+{0SFub-#ft)J$1jCQTI5SlvsRB{Hl80~q4;J0_
z({869Vf3{w0InyxC}|==80a%kfTAs6gtc2}>i
z9yK4n9I;uRtx|C1@)n?d{WrJ;9$UX+6PF`>VwZoI77H0a|1aeUZ?2DRwqEPF9F&Q_
zCsGmd=v)D(+1L1aMY}F`FA@h%52JsY7nV3HaLayE>3}4f$vt@LKk%BQ$l_jmgJV#X
z@K7eFRJ*rWEpoyu&2z-hcb$J0p5VpDKV<$9Cmw>UmsL%xCm0h455GctJ-K`Yu%Hyx
zMb@`l4=n2jh$y0K#P6hg9a