Procházet zdrojové kódy

feat: 集成腾讯云IM,增加IM模块

DoggyZhang před 4 měsíci
rodič
revize
07a9d28b18
100 změnil soubory, kde provedl 13713 přidání a 53 odebrání
  1. 6 7
      app/build.gradle
  2. 2 15
      app/src/main/java/com/adealink/weparty/App.kt
  3. 10 0
      app/src/main/java/com/adealink/weparty/module/im/IIMService.kt
  4. 21 0
      app/src/main/java/com/adealink/weparty/module/im/IMConfig.kt
  5. 52 0
      app/src/main/java/com/adealink/weparty/module/im/IMModule.kt
  6. 17 0
      app/src/main/java/com/adealink/weparty/module/im/Router.kt
  7. 11 6
      app/src/main/java/com/adealink/weparty/ui/home/HomeFragment.kt
  8. 7 23
      app/src/main/res/layout/fragment_home.xml
  9. 1 1
      app/src/main/res/values/strings.xml
  10. 2 1
      app/src/main/resources/META-INF/services/com.adealink.frame.router.IRouterInit
  11. 1 0
      build.gradle
  12. 61 0
      frame/tuikit/TIMCommon/timcommon/build.gradle
  13. 38 0
      frame/tuikit/TIMCommon/timcommon/src/main/AndroidManifest.xml
  14. 61 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/TIMCommonConfig.java
  15. 26 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/TIMCommonService.java
  16. 69 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/ChatFace.java
  17. 15 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/CustomFace.java
  18. 15 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/Emoji.java
  19. 98 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/FaceGroup.java
  20. 15 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/FriendProfileBean.java
  21. 13 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/GroupMemberBean.java
  22. 136 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/GroupProfileBean.java
  23. 30 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/MessageFeature.java
  24. 55 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/MessageReceiptInfo.java
  25. 117 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/MessageRepliesBean.java
  26. 477 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/TUIMessageBean.java
  27. 42 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/TUIReplyQuoteBean.java
  28. 82 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/UserBean.java
  29. 181 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/classicui/widget/message/MessageBaseHolder.java
  30. 708 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/classicui/widget/message/MessageContentHolder.java
  31. 512 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/classicui/widget/message/SelectionHelper.java
  32. 31 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/classicui/widget/message/TUIReplyQuoteView.java
  33. 93 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/BottomSelectSheet.java
  34. 34 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/CustomLinearLayoutManager.java
  35. 44 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/GifSpan.java
  36. 126 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/IndicatorView.java
  37. 144 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/LineControllerView.java
  38. 44 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/MaxWidthFrameLayout.java
  39. 44 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/MaxWidthLinearLayout.java
  40. 159 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/MinimalistLineControllerView.java
  41. 39 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/MinimalistTitleBar.java
  42. 278 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/PopupInputCard.java
  43. 126 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/RoundCornerImageView.java
  44. 127 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/RoundFrameLayout.java
  45. 59 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/SwitchCustomWidth.java
  46. 186 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/TitleBarLayout.java
  47. 84 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/UnreadCountTextView.java
  48. 5 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/action/PopActionClickListener.java
  49. 63 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/action/PopDialogAdapter.java
  50. 51 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/action/PopMenuAction.java
  51. 81 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/action/PopMenuAdapter.java
  52. 48 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/activities/BaseLightActivity.java
  53. 44 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/activities/BaseMinimalistLightActivity.java
  54. 450 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/activities/ImageSelectActivity.java
  55. 483 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/activities/ImageSelectMinimalistActivity.java
  56. 241 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/activities/SelectionActivity.java
  57. 241 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/activities/SelectionMinimalistActivity.java
  58. 199 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/dialog/MinimalistToast.java
  59. 329 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/dialog/TUIKitDialog.java
  60. 51 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/face/CenterImageSpan.java
  61. 546 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/face/FaceManager.java
  62. 87 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/face/RecentEmojiManager.java
  63. 99 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/gatherimage/MultiImageData.java
  64. 92 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/gatherimage/ShadeImageView.java
  65. 87 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/gatherimage/SynthesizedImageView.java
  66. 12 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/gatherimage/Synthesizer.java
  67. 334 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/gatherimage/TeamHeadSynthesizer.java
  68. 65 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/gatherimage/UserIconView.java
  69. 136 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/highlight/HighlightPresenter.java
  70. 120 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/impl/GlideEngine.java
  71. 19 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/interfaces/ILayout.java
  72. 131 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/interfaces/ITitleBarLayout.java
  73. 23 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/interfaces/IUIKitCallback.java
  74. 40 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/Compat.java
  75. 221 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/CustomGestureDetector.java
  76. 29 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnGestureListener.java
  77. 18 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnMatrixChangedListener.java
  78. 14 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnOutsidePhotoTapListener.java
  79. 22 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnPhotoTapListener.java
  80. 17 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnScaleChangedListener.java
  81. 21 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnSingleFlingListener.java
  82. 16 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnViewDragListener.java
  83. 16 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnViewTapListener.java
  84. 257 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/PhotoView.java
  85. 807 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/PhotoViewAttacher.java
  86. 39 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/Util.java
  87. 40 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/scroller/CenteredSmoothScroller.java
  88. 5 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/Attributes.java
  89. 85 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/RecyclerSwipeAdapter.java
  90. 21 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SimpleSwipeListener.java
  91. 9 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SwipeAdapterInterface.java
  92. 219 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SwipeItemMangerImpl.java
  93. 27 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SwipeItemMangerInterface.java
  94. 1806 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SwipeLayout.java
  95. 74 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/IPlayer.java
  96. 120 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/MediaPlayerProxy.java
  97. 153 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/SystemMediaPlayerWrapper.java
  98. 441 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/VideoGestureScaleAttacher.java
  99. 324 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/VideoView.java
  100. 336 0
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/config/classicui/TUIConfigClassic.java

+ 6 - 7
app/build.gradle

@@ -115,12 +115,12 @@ android {
 
 //        buildConfigField("String", "AGORA_APP_ID", '"a5233b5e42534b77bbabf2ca8ac95215"')
 
-//        //trtc
-//        buildConfigField("Integer", "TRTC_APP_ID", "1721001739")
-//        buildConfigField("String", "TRTC_SECRET_KEY", '"ea5b233540129b2700b059d4fbb0c55a21d8e8bd69599b1ee9ccc9ed3b9142bb"')
-//        //trtc debug
-//        buildConfigField("Integer", "TRTC_APP_ID_DEBUG", "1600073604")
-//        buildConfigField("String", "TRTC_SECRET_KEY_DEBUG", '"f7c992cc3b8fc0273716da5ad59745cf188594c695c9191806d80532914f2aa1"')
+        //trtc
+        buildConfigField("Integer", "TRTC_APP_ID", "20030346")
+        buildConfigField("String", "TRTC_SECRET_KEY", '"a063a1a88b743dacba7f969b70e0b3aec161be06f1326a3913032c5632ed28f9"')
+        //trtc debug
+        buildConfigField("Integer", "TRTC_APP_ID_DEBUG", "20030346")
+        buildConfigField("String", "TRTC_SECRET_KEY_DEBUG", '"a063a1a88b743dacba7f969b70e0b3aec161be06f1326a3913032c5632ed28f9"')
 
         //sign key
         buildConfigField("String", "SIGN_KEY", '"abc|abc|edg|9527|1234"')
@@ -435,7 +435,6 @@ dependencies {
     api libs.frame.debug
 
     api project(":frame:room")
-//    api project(":frame:imkit")
     api libs.frame.startup
 
     api libs.frame.locale

+ 2 - 15
app/src/main/java/com/adealink/weparty/App.kt

@@ -76,6 +76,7 @@ import com.adealink.weparty.log.LogConfig
 import com.adealink.weparty.media.IMediaManager
 import com.adealink.weparty.media.MediaConfig
 import com.adealink.weparty.media.MediaManager
+import com.adealink.weparty.module.im.IMModule
 import com.adealink.weparty.network.INetworkManager
 import com.adealink.weparty.network.NetworkConfig
 import com.adealink.weparty.network.NetworkManager
@@ -351,21 +352,7 @@ class App : SplitCompatApplication(), ActivityLifecycleCallbacksExt {
 
     inner class InitIM : SubWaitStartUpTask() {
         override fun run() {
-//            imService.init(
-//                this@App,
-//                InitOption.Builder()
-//                    .setAreaCode(
-//                        when (AppBase.isProdEnv) {
-//                            true -> InitOption.AreaCode.SG
-//                            else -> InitOption.AreaCode.BJ
-//                        }
-//                    )
-//                    .setMainProcess(true)
-//                    .enablePush(false)
-//                    .enableSyncEmptyTopConversation(true)
-//                    .build()
-//            )
-//            MessageModule.appOnCreateMainTask(this@App)
+            IMModule.appOnCreateMainTask(this@App)
         }
     }
 

+ 10 - 0
app/src/main/java/com/adealink/weparty/module/im/IIMService.kt

@@ -0,0 +1,10 @@
+package com.adealink.weparty.module.im
+
+import com.adealink.frame.aab.IService
+import com.adealink.frame.startup.IAppStartUpTask
+
+interface IIMService : IService<IIMService>, IAppStartUpTask{
+
+
+
+}

+ 21 - 0
app/src/main/java/com/adealink/weparty/module/im/IMConfig.kt

@@ -0,0 +1,21 @@
+package com.adealink.weparty.module.im
+
+import com.adealink.frame.base.AppBase
+import com.adealink.weparty.BuildConfig
+
+object IMConfig {
+
+    val APP_ID: Int
+        get() = if (AppBase.isProdEnv) {
+            BuildConfig.TRTC_APP_ID
+        } else {
+            BuildConfig.TRTC_APP_ID_DEBUG
+        }
+
+    val SECRET_KEY: String
+        get() = if (AppBase.isProdEnv) {
+            BuildConfig.TRTC_SECRET_KEY
+        } else {
+            BuildConfig.TRTC_SECRET_KEY_DEBUG
+        }
+}

+ 52 - 0
app/src/main/java/com/adealink/weparty/module/im/IMModule.kt

@@ -0,0 +1,52 @@
+package com.adealink.weparty.module.im
+
+import android.app.Application
+import com.adealink.frame.aab.BaseDynamicModule
+import com.adealink.weparty.R
+
+object IMModule : BaseDynamicModule<IIMService>(IIMService::class), IIMService {
+
+    override val moduleNameResId: Int
+        get() = R.string.module_im
+
+    override val featureName: String
+        get() = "im"
+
+    override fun emptyService(): IIMService {
+        return object : IIMService {
+            override fun getService(): IIMService? {
+                return null
+            }
+
+            override fun activityOnCreateMainTask() {
+            }
+
+            override fun activityOnCreateSubTask() {
+            }
+
+            override fun appOnCreateMainTask(application: Application) {
+            }
+
+            override fun appOnCreateSubTask(application: Application) {
+            }
+        }
+    }
+
+    override fun activityOnCreateMainTask() {
+        getService().activityOnCreateMainTask()
+    }
+
+    override fun activityOnCreateSubTask() {
+        getService().activityOnCreateSubTask()
+    }
+
+    override fun appOnCreateMainTask(application: Application) {
+        getService().appOnCreateMainTask(application)
+    }
+
+    override fun appOnCreateSubTask(application: Application) {
+        getService().appOnCreateSubTask(application)
+    }
+
+
+}

+ 17 - 0
app/src/main/java/com/adealink/weparty/module/im/Router.kt

@@ -0,0 +1,17 @@
+package com.adealink.weparty.module.im
+
+interface IM {
+
+    interface Common {
+        companion object {
+            const val PATH = "/im"
+        }
+    }
+
+    interface SessionList {
+        companion object {
+            const val PATH = "${Common.PATH}/session_list"
+        }
+    }
+
+}

+ 11 - 6
app/src/main/java/com/adealink/weparty/ui/home/HomeFragment.kt

@@ -4,17 +4,21 @@ import android.annotation.SuppressLint
 import android.os.Bundle
 import android.view.View
 import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.view.updatePadding
 import com.adealink.frame.log.Log
 import com.adealink.frame.mvvm.view.viewBinding
+import com.adealink.frame.router.Router
 import com.adealink.frame.startup.DistributedLoadManager
 import com.adealink.weparty.AppModule
 import com.adealink.weparty.R
 import com.adealink.weparty.commonui.BaseFragment
 import com.adealink.weparty.commonui.ext.dp
+import com.adealink.weparty.commonui.toast.util.ROUTER_TOAST_URI
 import com.adealink.weparty.commonui.util.isNavigationBarVisible
 import com.adealink.weparty.constant.TAG_TIME_APP_START
 import com.adealink.weparty.constant.logTime
 import com.adealink.weparty.databinding.FragmentHomeBinding
+import com.adealink.weparty.module.im.IM
 import java.util.LinkedList
 import java.util.Queue
 
@@ -61,13 +65,14 @@ class HomeFragment : BaseFragment(R.layout.fragment_home) {
 
     override fun initViews() {
         super.initViews()
-        if (context?.let { isNavigationBarVisible(it) } == false) {
-            binding.tlTab.apply {
-                if (layoutParams is ConstraintLayout.LayoutParams) {
-                    (layoutParams as ConstraintLayout.LayoutParams).bottomMargin = 12.dp()
-                }
-            }
+        binding.btnImList.setOnClickListener {
+            testIMList()
         }
     }
 
+    private fun testIMList(){
+        val act = activity?:return
+        Router.build(act, IM.SessionList.PATH).start()
+    }
+
 }

+ 7 - 23
app/src/main/res/layout/fragment_home.xml

@@ -6,29 +6,13 @@
     android:layout_height="match_parent"
     tools:context=".ui.home.HomeFragment">
 
-    <androidx.viewpager2.widget.ViewPager2
-        android:id="@+id/vp_content"
-        android:layout_width="match_parent"
-        android:layout_height="0dp"
-        app:layout_constraintBottom_toTopOf="@+id/tl_tab"
-        app:layout_constraintTop_toTopOf="parent" />
-
-    <View
-        android:layout_width="0dp"
-        android:layout_height="0.5dp"
-        android:background="@color/color_border"
-        app:layout_constraintBottom_toTopOf="@id/tl_tab"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent" />
-
-    <com.google.android.material.tabs.TabLayout
-        android:id="@+id/tl_tab"
-        android:layout_width="match_parent"
-        android:layout_height="54dp"
+    <Button
+        android:id="@+id/btn_im_list"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="IM会话列表"
         app:layout_constraintBottom_toBottomOf="parent"
-        app:tabBackground="@null"
-        app:tabIndicatorHeight="0dp"
-        app:tabMode="fixed"
-        app:tabRippleColor="@null" />
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
 
 </androidx.constraintlayout.widget.ConstraintLayout>

+ 1 - 1
app/src/main/res/values/strings.xml

@@ -322,7 +322,7 @@
     <string name="common_gift_id">Gifted ID</string>
     <string name="common_your_custom_id">Your Custom ID</string>
     <string name="module_betting_pk" translatable="false">bettingpk</string>
-    <string name="module_call" translatable="false">Call</string>
+    <string name="module_im" translatable="false">IM</string>
     <string name="module_couple" translatable="false">Couple</string>
     <string name="couple_invite_self">can\'t invite yourself</string>
     <string name="couple_over">already have CP</string>

+ 2 - 1
app/src/main/resources/META-INF/services/com.adealink.frame.router.IRouterInit

@@ -1,3 +1,4 @@
 com.adealink.frame.router.RouterInit_app
 com.adealink.frame.router.RouterInit_module_account
-com.adealink.frame.router.RouterInit_module_profile
+com.adealink.frame.router.RouterInit_module_profile
+com.adealink.frame.router.RouterInit_module_im

+ 1 - 0
build.gradle

@@ -5,6 +5,7 @@ buildscript {
         google()
         mavenCentral()
         gradlePluginPortal()
+        maven { url "https://mirrors.tencent.com/nexus/repository/maven-public/" }
         maven {
             url'http://8.134.139.102:8085/repository/wenext-android/'
             credentials {

+ 61 - 0
frame/tuikit/TIMCommon/timcommon/build.gradle

@@ -0,0 +1,61 @@
+plugins {
+    id 'com.android.library'
+}
+
+android {
+    namespace "com.tencent.qcloud.tuikit.timcommon"
+    compileSdk libs.versions.compileSdk.get().toInteger()
+
+    defaultConfig {
+        minSdk libs.versions.minSdk.get().toInteger()
+        targetSdk libs.versions.targetSdk.get().toInteger()
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        consumerProguardFiles "consumer-rules.pro"
+        ndk {
+            abiFilters "armeabi-v7a"
+            abiFilters "arm64-v8a"
+        }
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_17
+        targetCompatibility JavaVersion.VERSION_17
+    }
+
+    sourceSets {
+        main {
+            res.srcDirs += "src/main/res-light"
+            res.srcDirs += "src/main/res-lively"
+            res.srcDirs += "src/main/res-serious"
+        }
+    }
+}
+
+dependencies {
+    /*plugin-build-Begin
+
+    compileOnly fileTree(include: ['*.jar','*.aar'], dir: '../../../../tuikit/android/libs')
+
+    plugin-build-End*/
+
+    def projects = this.rootProject.getAllprojects().stream().map { project -> project.name }.collect()
+    api projects.contains("tuicore") ? project(':tuicore') : "com.tencent.liteav.tuikit:tuicore:8.7.7201"
+
+    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+    implementation 'com.google.android.material:material:1.4.0'
+    implementation 'com.google.code.gson:gson:2.9.1'
+    implementation 'androidx.appcompat:appcompat:1.3.1'
+    implementation 'com.github.bumptech.glide:glide:4.12.0'
+    implementation 'androidx.recyclerview:recyclerview:1.2.1'
+    implementation 'androidx.viewpager2:viewpager2:1.0.0'
+    annotationProcessor 'com.google.auto.service:auto-service:1.1.1'
+
+}
+

+ 38 - 0
frame/tuikit/TIMCommon/timcommon/src/main/AndroidManifest.xml

@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <application >
+
+        <activity
+            android:name="com.tencent.qcloud.tuikit.timcommon.component.activities.SelectionActivity"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name="com.tencent.qcloud.tuikit.timcommon.component.activities.ImageSelectActivity"
+            android:screenOrientation="portrait" />
+
+        <activity
+            android:name="com.tencent.qcloud.tuikit.timcommon.component.activities.SelectionMinimalistActivity"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name="com.tencent.qcloud.tuikit.timcommon.component.activities.ImageSelectMinimalistActivity"
+            android:screenOrientation="portrait" />
+
+        <activity android:name="com.tencent.qcloud.tuikit.timcommon.util.ActivityResultResolver$ActivityResultProxyActivity"
+            android:configChanges="orientation|keyboardHidden|screenSize"
+            android:multiprocess="true"
+            android:launchMode="singleTask"
+            android:theme="@style/CoreActivityTranslucent"
+            android:windowSoftInputMode="stateHidden|stateAlwaysHidden"/>
+        
+        <provider
+            android:name="com.tencent.qcloud.tuikit.timcommon.util.FileProvider"
+            android:authorities="${applicationId}.timcommon.fileprovider"
+            android:exported="false"
+            android:grantUriPermissions="true">
+            <meta-data
+                android:name="android.support.FILE_PROVIDER_PATHS"
+                android:resource="@xml/file_paths_public"/>
+        </provider>
+
+    </application>
+</manifest>

+ 61 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/TIMCommonConfig.java

@@ -0,0 +1,61 @@
+package com.tencent.qcloud.tuikit.timcommon;
+
+public class TIMCommonConfig {
+    private static boolean enableGroupGridAvatar = true;
+    private static int defaultAvatarImage;
+    private static int defaultGroupAvatarImage;
+
+    /**
+     * Gets whether to display the avatar in the nine-square grid style in the group conversation, the default is true
+     */
+    public static boolean isEnableGroupGridAvatar() {
+        return enableGroupGridAvatar;
+    }
+
+    /**
+     * Set whether to display the avatar in the nine-square grid style in group conversations
+     */
+    public static void setEnableGroupGridAvatar(boolean enableGroupGridAvatar) {
+        TIMCommonConfig.enableGroupGridAvatar = enableGroupGridAvatar;
+    }
+
+    /**
+     *
+     * Get the default avatar for c2c conversation
+     *
+     * @return
+     */
+    public static int getDefaultAvatarImage() {
+        return defaultAvatarImage;
+    }
+
+    /**
+     *
+     *Set the default avatar for c2c conversation
+     *
+     * @return
+     */
+    public static void setDefaultAvatarImage(int defaultAvatarImage) {
+        TIMCommonConfig.defaultAvatarImage = defaultAvatarImage;
+    }
+
+    /**
+     *
+     * Get the default avatar for group conversation
+     *
+     * @return
+     */
+    public static int getDefaultGroupAvatarImage() {
+        return defaultGroupAvatarImage;
+    }
+
+    /**
+     *
+     *Set the default avatar for group conversation
+     *
+     * @return
+     */
+    public static void setDefaultGroupAvatarImage(int defaultGroupAvatarImage) {
+        TIMCommonConfig.defaultGroupAvatarImage = defaultGroupAvatarImage;
+    }
+}

+ 26 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/TIMCommonService.java

@@ -0,0 +1,26 @@
+package com.tencent.qcloud.tuikit.timcommon;
+
+import android.content.Context;
+import android.text.TextUtils;
+
+import com.google.auto.service.AutoService;
+import com.tencent.qcloud.tuicore.ServiceInitializer;
+import com.tencent.qcloud.tuicore.TUIThemeManager;
+import com.tencent.qcloud.tuicore.annotations.TUIInitializerID;
+import com.tencent.qcloud.tuicore.interfaces.TUIInitializer;
+
+@AutoService(TUIInitializer.class)
+@TUIInitializerID("TIMCommon")
+public class TIMCommonService implements TUIInitializer {
+
+    @Override
+    public void init(Context context) {
+        TUIThemeManager.addLightTheme(R.style.TIMCommonLightTheme);
+        TUIThemeManager.addLivelyTheme(R.style.TIMCommonLivelyTheme);
+        TUIThemeManager.addSeriousTheme(R.style.TIMCommonSeriousTheme);
+    }
+
+    public static Context getAppContext() {
+        return ServiceInitializer.getAppContext();
+    }
+}

+ 69 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/ChatFace.java

@@ -0,0 +1,69 @@
+package com.tencent.qcloud.tuikit.timcommon.bean;
+
+import java.io.Serializable;
+
+public class ChatFace implements Serializable {
+    private int width;
+    private int height;
+    protected String faceUrl;
+    private FaceGroup<? extends ChatFace> faceGroup;
+    private String faceKey;
+    private String faceName;
+    private boolean autoMirrored = false;
+
+    public void setFaceKey(String faceKey) {
+        this.faceKey = faceKey;
+    }
+
+    public String getFaceKey() {
+        return faceKey;
+    }
+
+    public void setFaceName(String faceName) {
+        this.faceName = faceName;
+    }
+
+    public String getFaceName() {
+        return faceName;
+    }
+
+    public void setFaceGroup(FaceGroup<? extends ChatFace> faceGroup) {
+        this.faceGroup = faceGroup;
+    }
+
+    public FaceGroup<? extends ChatFace> getFaceGroup() {
+        return faceGroup;
+    }
+
+    public void setWidth(int width) {
+        this.width = width;
+    }
+
+    public void setHeight(int height) {
+        this.height = height;
+    }
+
+    public void setFaceUrl(String faceUrl) {
+        this.faceUrl = faceUrl;
+    }
+
+    public String getFaceUrl() {
+        return faceUrl;
+    }
+
+    public int getHeight() {
+        return height;
+    }
+
+    public int getWidth() {
+        return width;
+    }
+
+    public void setAutoMirrored(boolean autoMirrored) {
+        this.autoMirrored = autoMirrored;
+    }
+
+    public boolean isAutoMirrored() {
+        return autoMirrored;
+    }
+}

+ 15 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/CustomFace.java

@@ -0,0 +1,15 @@
+package com.tencent.qcloud.tuikit.timcommon.bean;
+
+/**
+ *
+ * Custom expression attribute class
+ */
+public class CustomFace extends ChatFace {
+    /**
+     *
+     * @param assetPath
+     */
+    public void setAssetPath(String assetPath) {
+        this.faceUrl = "file:///android_asset/" + assetPath;
+    }
+}

+ 15 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/Emoji.java

@@ -0,0 +1,15 @@
+package com.tencent.qcloud.tuikit.timcommon.bean;
+
+import android.graphics.Bitmap;
+
+public class Emoji extends ChatFace {
+    private Bitmap icon;
+
+    public Bitmap getIcon() {
+        return icon;
+    }
+
+    public void setIcon(Bitmap icon) {
+        this.icon = icon;
+    }
+}

+ 98 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/FaceGroup.java

@@ -0,0 +1,98 @@
+package com.tencent.qcloud.tuikit.timcommon.bean;
+
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class FaceGroup<T extends ChatFace> {
+    private int groupID;
+    private String groupName;
+    private String desc;
+    private Object faceGroupIconUrl;
+    private int pageRowCount;
+    private int pageColumnCount;
+    private boolean isEmoji = false;
+    private final Map<String, T> faces = new LinkedHashMap<>();
+
+    public int getGroupID() {
+        return groupID;
+    }
+
+    public void setGroupID(int groupID) {
+        this.groupID = groupID;
+    }
+
+    public void setGroupName(String groupName) {
+        this.groupName = groupName;
+    }
+
+    public String getGroupName() {
+        return groupName;
+    }
+
+    public boolean isEmojiGroup() {
+        return isEmoji;
+    }
+
+    public String getDesc() {
+        return desc;
+    }
+
+    public void setDesc(String desc) {
+        this.desc = desc;
+    }
+
+    public void setFaceGroupIconUrl(Object faceGroupIconUrl) {
+        this.faceGroupIconUrl = faceGroupIconUrl;
+    }
+
+    public Object getFaceGroupIconUrl() {
+        return faceGroupIconUrl;
+    }
+
+    public int getPageRowCount() {
+        return pageRowCount;
+    }
+
+    public void setPageRowCount(int pageRowCount) {
+        this.pageRowCount = pageRowCount;
+    }
+
+    public int getPageColumnCount() {
+        return pageColumnCount;
+    }
+
+    public void setPageColumnCount(int pageColumnCount) {
+        this.pageColumnCount = pageColumnCount;
+    }
+
+    public ArrayList<T> getFaces() {
+        return new ArrayList<>(faces.values());
+    }
+
+    public void addFace(String faceKey, T face) {
+        if (face instanceof Emoji) {
+            isEmoji = true;
+        }
+        face.setFaceGroup(this);
+        faces.put(faceKey, face);
+    }
+
+    public T getFace(String faceKey) {
+        if (TextUtils.isEmpty(faceKey)) {
+            return null;
+        }
+        T face = faces.get(faceKey);
+        if (face == null) {
+            int index = faceKey.lastIndexOf("@2x");
+            if (index == -1) {
+                return null;
+            }
+            String oldFaceKey = faceKey.substring(0, index);
+            face = faces.get(oldFaceKey);
+        }
+        return face;
+    }
+}

+ 15 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/FriendProfileBean.java

@@ -0,0 +1,15 @@
+package com.tencent.qcloud.tuikit.timcommon.bean;
+
+import java.io.Serializable;
+
+public class FriendProfileBean extends UserBean implements Serializable {
+    private int allowType;
+
+    public int getAllowType() {
+        return allowType;
+    }
+
+    public void setAllowType(int allowType) {
+        this.allowType = allowType;
+    }
+}

+ 13 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/GroupMemberBean.java

@@ -0,0 +1,13 @@
+package com.tencent.qcloud.tuikit.timcommon.bean;
+
+public class GroupMemberBean extends UserBean {
+    private int role;
+
+    public int getRole() {
+        return role;
+    }
+
+    public void setRole(int role) {
+        this.role = role;
+    }
+}

+ 136 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/GroupProfileBean.java

@@ -0,0 +1,136 @@
+package com.tencent.qcloud.tuikit.timcommon.bean;
+
+import com.tencent.imsdk.group.GroupMemberInfo;
+
+import java.io.Serializable;
+
+public class GroupProfileBean implements Serializable {
+    private String groupName;
+    private String groupID;
+    private String groupType;
+    private String notification;
+    private String groupIntroduction;
+    private String groupFaceUrl;
+    private int memberCount;
+    private int recvOpt;
+    private int approveOpt;
+    private int addOpt;
+    private boolean isAllMuted = false;
+    private GroupMemberBean selfInfo;
+
+    public String getGroupName() {
+        return groupName;
+    }
+
+    public void setGroupName(String groupName) {
+        this.groupName = groupName;
+    }
+
+    public String getGroupID() {
+        return groupID;
+    }
+
+    public void setGroupID(String groupID) {
+        this.groupID = groupID;
+    }
+
+    public String getGroupType() {
+        return groupType;
+    }
+
+    public void setGroupType(String groupType) {
+        this.groupType = groupType;
+    }
+
+    public String getNotification() {
+        return notification;
+    }
+
+    public void setNotification(String notification) {
+        this.notification = notification;
+    }
+
+    public String getGroupIntroduction() {
+        return groupIntroduction;
+    }
+
+    public void setGroupIntroduction(String groupIntroduction) {
+        this.groupIntroduction = groupIntroduction;
+    }
+
+    public String getGroupFaceUrl() {
+        return groupFaceUrl;
+    }
+
+    public void setGroupFaceUrl(String groupFaceUrl) {
+        this.groupFaceUrl = groupFaceUrl;
+    }
+
+    public int getMemberCount() {
+        return memberCount;
+    }
+
+    public void setMemberCount(int memberCount) {
+        this.memberCount = memberCount;
+    }
+
+    public void setSelfInfo(GroupMemberBean selfInfo) {
+        this.selfInfo = selfInfo;
+    }
+
+    public GroupMemberBean getSelfInfo() {
+        return selfInfo;
+    }
+
+    public int getRoleInGroup() {
+        return selfInfo.getRole();
+    }
+
+    public void setRoleInGroup(int role) {
+        selfInfo.setRole(role);
+    }
+
+    public boolean canManage() {
+        return selfInfo.getRole() == GroupMemberInfo.MEMBER_ROLE_OWNER || selfInfo.getRole() == GroupMemberInfo.MEMBER_ROLE_ADMINISTRATOR;
+    }
+
+    public boolean isOwner() {
+        return selfInfo.getRole() == GroupMemberInfo.MEMBER_ROLE_OWNER;
+    }
+
+    public boolean isAdmin() {
+        return selfInfo.getRole() == GroupMemberInfo.MEMBER_ROLE_ADMINISTRATOR;
+    }
+
+    public void setAllMuted(boolean allMuted) {
+        isAllMuted = allMuted;
+    }
+
+    public boolean isAllMuted() {
+        return isAllMuted;
+    }
+
+    public void setApproveOpt(int approveOpt) {
+        this.approveOpt = approveOpt;
+    }
+
+    public int getApproveOpt() {
+        return approveOpt;
+    }
+
+    public int getAddOpt() {
+        return addOpt;
+    }
+
+    public void setAddOpt(int addOpt) {
+        this.addOpt = addOpt;
+    }
+
+    public void setRecvOpt(int recvOpt) {
+        this.recvOpt = recvOpt;
+    }
+
+    public int getRecvOpt() {
+        return recvOpt;
+    }
+}

+ 30 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/MessageFeature.java

@@ -0,0 +1,30 @@
+package com.tencent.qcloud.tuikit.timcommon.bean;
+
+import java.io.Serializable;
+
+/*
+ *  Carrying function macros through messages,Mainly used to be compatible with old and new versions,Use the cloudCustomData field.
+ *  Such as Typing function.
+ */
+public class MessageFeature implements Serializable {
+    public static final int VERSION = 1;
+
+    private int needTyping = 1; // message typing feature ...
+    private int version = VERSION;
+
+    public int getVersion() {
+        return version;
+    }
+
+    public void setVersion(int version) {
+        this.version = version;
+    }
+
+    public int getNeedTyping() {
+        return needTyping;
+    }
+
+    public void setNeedTyping(int needTyping) {
+        this.needTyping = needTyping;
+    }
+}

+ 55 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/MessageReceiptInfo.java

@@ -0,0 +1,55 @@
+package com.tencent.qcloud.tuikit.timcommon.bean;
+
+import com.tencent.imsdk.v2.V2TIMMessageReceipt;
+
+import java.io.Serializable;
+
+public class MessageReceiptInfo implements Serializable {
+    private V2TIMMessageReceipt messageReceipt;
+
+    public void setMessageReceipt(V2TIMMessageReceipt messageReceipt) {
+        this.messageReceipt = messageReceipt;
+    }
+
+    public String getUserID() {
+        if (messageReceipt != null) {
+            return messageReceipt.getUserID();
+        }
+        return null;
+    }
+
+    public boolean isPeerRead() {
+        if (messageReceipt != null) {
+            return messageReceipt.isPeerRead();
+        }
+        return false;
+    }
+
+    public String getGroupID() {
+        if (messageReceipt != null) {
+            return messageReceipt.getGroupID();
+        }
+        return null;
+    }
+
+    public long getReadCount() {
+        if (messageReceipt != null) {
+            return messageReceipt.getReadCount();
+        }
+        return 0;
+    }
+
+    public long getUnreadCount() {
+        if (messageReceipt != null) {
+            return messageReceipt.getUnreadCount();
+        }
+        return 0;
+    }
+
+    public String getMsgID() {
+        if (messageReceipt != null) {
+            return messageReceipt.getMsgID();
+        }
+        return null;
+    }
+}

+ 117 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/MessageRepliesBean.java

@@ -0,0 +1,117 @@
+package com.tencent.qcloud.tuikit.timcommon.bean;
+
+import android.text.TextUtils;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+public class MessageRepliesBean implements Serializable {
+    public static final int VERSION = 1;
+    private List<ReplyBean> replies;
+    private int version = VERSION;
+
+    public void addReplyMessage(String messageId, String messageAbstract, String sender) {
+        if (replies == null) {
+            replies = new ArrayList<>();
+        }
+        for (ReplyBean replyBean : replies) {
+            if (TextUtils.equals(replyBean.messageID, messageId)) {
+                return;
+            }
+        }
+        ReplyBean replyBean = new ReplyBean();
+        replyBean.messageID = messageId;
+        replyBean.messageAbstract = messageAbstract;
+        replyBean.messageSender = sender;
+        replies.add(replyBean);
+    }
+
+    public void removeReplyMessage(String messageID) {
+        if (replies == null) {
+            return;
+        }
+        for (ReplyBean replyBean : replies) {
+            if (TextUtils.equals(replyBean.messageID, messageID)) {
+                replies.remove(replyBean);
+                return;
+            }
+        }
+    }
+
+    public void setVersion(int version) {
+        this.version = version;
+    }
+
+    public int getVersion() {
+        return version;
+    }
+
+    public List<ReplyBean> getReplies() {
+        return replies;
+    }
+
+    public void setReplies(List<ReplyBean> replies) {
+        this.replies = replies;
+    }
+
+    public int getRepliesSize() {
+        if (replies != null) {
+            return replies.size();
+        }
+        return 0;
+    }
+
+
+    public static class ReplyBean implements Serializable {
+        private String messageID;
+        private String messageAbstract;
+        private String messageSender;
+        private transient String senderFaceUrl;
+        private transient String senderShowName;
+
+        public String getMessageID() {
+            return messageID;
+        }
+
+        public void setMessageID(String messageID) {
+            this.messageID = messageID;
+        }
+
+        public String getMessageAbstract() {
+            return messageAbstract;
+        }
+
+        public void setMessageAbstract(String messageAbstract) {
+            this.messageAbstract = messageAbstract;
+        }
+
+        public String getMessageSender() {
+            return messageSender;
+        }
+
+        public void setMessageSender(String messageSender) {
+            this.messageSender = messageSender;
+        }
+
+        public void setSenderFaceUrl(String senderFaceUrl) {
+            this.senderFaceUrl = senderFaceUrl;
+        }
+
+        public String getSenderFaceUrl() {
+            return senderFaceUrl;
+        }
+
+        public void setSenderShowName(String senderShowName) {
+            this.senderShowName = senderShowName;
+        }
+
+        public String getSenderShowName() {
+            if (TextUtils.isEmpty(senderShowName)) {
+                return messageSender;
+            }
+            return senderShowName;
+        }
+    }
+
+}

+ 477 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/TUIMessageBean.java

@@ -0,0 +1,477 @@
+package com.tencent.qcloud.tuikit.timcommon.bean;
+
+import android.text.TextUtils;
+import com.tencent.imsdk.v2.V2TIMManager;
+import com.tencent.imsdk.v2.V2TIMMessage;
+import com.tencent.imsdk.v2.V2TIMUserFullInfo;
+import com.tencent.qcloud.tuikit.timcommon.R;
+import com.tencent.qcloud.tuikit.timcommon.TIMCommonService;
+import com.tencent.qcloud.tuikit.timcommon.util.MessageBuilder;
+import com.tencent.qcloud.tuikit.timcommon.util.MessageParser;
+import com.tencent.qcloud.tuikit.timcommon.util.TIMCommonConstants;
+
+import java.io.Serializable;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public abstract class TUIMessageBean implements Serializable {
+
+    public static final int MSG_STATUS_SENDING = V2TIMMessage.V2TIM_MSG_STATUS_SENDING;
+    public static final int MSG_STATUS_SEND_SUCCESS = V2TIMMessage.V2TIM_MSG_STATUS_SEND_SUCC;
+    public static final int MSG_STATUS_SEND_FAIL = V2TIMMessage.V2TIM_MSG_STATUS_SEND_FAIL;
+    public static final int MSG_STATUS_REVOKE = V2TIMMessage.V2TIM_MSG_STATUS_LOCAL_REVOKED;
+
+    public static final int MSG_SOURCE_UNKNOWN = 0;
+
+    public static final int MSG_SOURCE_ONLINE_PUSH = 1;
+
+    public static final int MSG_SOURCE_GET_HISTORY = 2;
+
+    private V2TIMMessage v2TIMMessage;
+    private long msgTime;
+    private String extra;
+    private String id;
+    private boolean isGroup;
+    private boolean isSelf = true;
+    private int status;
+    private String selectText;
+    private boolean excludeFromHistory;
+    private boolean isUseMsgReceiverAvatar = false;
+    private boolean isEnableForward = true;
+    private UserBean revoker;
+    private boolean hasRiskContent = false;
+    private int messageSource = 0;
+    private MessageReceiptInfo messageReceiptInfo;
+    private MessageRepliesBean messageRepliesBean;
+    private boolean hasReaction = false;
+    private Map<String, UserBean> userBeanMap = new LinkedHashMap<>();
+    private boolean isSending = false;
+    private boolean isProcessing = false;
+    private Object processingThumbnail;
+    private String userId = "";
+    private String groupId = "";
+
+    public void setExcludeFromHistory(boolean excludeFromHistory) {
+        this.excludeFromHistory = excludeFromHistory;
+    }
+
+    public boolean isExcludeFromHistory() {
+        return excludeFromHistory;
+    }
+
+    public void setUseMsgReceiverAvatar(boolean useMsgReceiverAvatar) {
+        isUseMsgReceiverAvatar = useMsgReceiverAvatar;
+    }
+
+    public boolean isUseMsgReceiverAvatar() {
+        return isUseMsgReceiverAvatar;
+    }
+
+    public boolean isEnableForward() {
+        return isEnableForward;
+    }
+
+    public void setEnableForward(boolean enableForward) {
+        isEnableForward = enableForward;
+    }
+
+    public MessageRepliesBean getMessageRepliesBean() {
+        return messageRepliesBean;
+    }
+
+    public void setMessageRepliesBean(MessageRepliesBean messageRepliesBean) {
+        this.messageRepliesBean = messageRepliesBean;
+        MessageBuilder.mergeCloudCustomData(this, TIMCommonConstants.MESSAGE_REPLIES_KEY, messageRepliesBean);
+    }
+
+    public void setMessageReceiptInfo(MessageReceiptInfo messageReceiptInfo) {
+        this.messageReceiptInfo = messageReceiptInfo;
+    }
+
+    public long getReadCount() {
+        if (messageReceiptInfo != null) {
+            return messageReceiptInfo.getReadCount();
+        }
+        return 0;
+    }
+
+    public long getUnreadCount() {
+        if (messageReceiptInfo != null) {
+            return messageReceiptInfo.getUnreadCount();
+        }
+        return 0;
+    }
+
+    public void setCommonAttribute(V2TIMMessage v2TIMMessage) {
+        msgTime = System.currentTimeMillis() / 1000;
+        this.v2TIMMessage = v2TIMMessage;
+
+        if (v2TIMMessage == null) {
+            return;
+        }
+
+        id = v2TIMMessage.getMsgID();
+        isGroup = !TextUtils.isEmpty(v2TIMMessage.getGroupID());
+        hasRiskContent = v2TIMMessage.hasRiskContent();
+        if (v2TIMMessage.getStatus() == V2TIMMessage.V2TIM_MSG_STATUS_LOCAL_REVOKED) {
+            status = MSG_STATUS_REVOKE;
+            if (isSelf()) {
+                extra = TIMCommonService.getAppContext().getString(R.string.revoke_tips_you);
+            } else if (isGroup) {
+                extra = "\"" + getSender() + "\"" + TIMCommonService.getAppContext().getString(R.string.revoke_tips);
+            } else {
+                extra = TIMCommonService.getAppContext().getString(R.string.revoke_tips_other);
+            }
+        } else {
+            if (isSelf()) {
+                if (v2TIMMessage.getStatus() == V2TIMMessage.V2TIM_MSG_STATUS_SEND_FAIL) {
+                    status = MSG_STATUS_SEND_FAIL;
+                } else if (v2TIMMessage.getStatus() == V2TIMMessage.V2TIM_MSG_STATUS_SEND_SUCC) {
+                    status = MSG_STATUS_SEND_SUCCESS;
+                } else if (v2TIMMessage.getStatus() == V2TIMMessage.V2TIM_MSG_STATUS_SENDING) {
+                    status = MSG_STATUS_SENDING;
+                }
+            }
+        }
+
+        messageRepliesBean = MessageParser.parseMessageReplies(this);
+    }
+
+    public boolean isPeerRead() {
+        if (messageReceiptInfo != null) {
+            return messageReceiptInfo.isPeerRead();
+        }
+        return false;
+    }
+
+    public boolean hasRiskContent() {
+        return hasRiskContent;
+    }
+
+    public boolean isAllRead() {
+        return getUnreadCount() == 0 && getReadCount() > 0;
+    }
+
+    public boolean isUnread() {
+        return getReadCount() == 0;
+    }
+
+    /**
+     *
+     * Get a summary of messages to display in the conversation list
+     * @return
+     */
+    public String onGetDisplayString() {
+        return getExtra();
+    }
+
+    public abstract void onProcessMessage(V2TIMMessage v2TIMMessage);
+
+    public final long getMessageTime() {
+        if (v2TIMMessage != null) {
+            long timestamp = v2TIMMessage.getTimestamp();
+            if (timestamp != 0) {
+                return timestamp;
+            }
+        }
+        return msgTime;
+    }
+
+    public long getMsgSeq() {
+        if (v2TIMMessage != null) {
+            return v2TIMMessage.getSeq();
+        }
+        return 0;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public String getUserId() {
+        if (v2TIMMessage != null) {
+            return v2TIMMessage.getUserID();
+        }
+        return userId;
+    }
+
+    public void setUserId(String userId) {
+        this.userId = userId;
+    }
+
+    public void setGroupId(String groupId) {
+        this.groupId = groupId;
+    }
+
+    public boolean isSelf() {
+        if (v2TIMMessage != null) {
+            return v2TIMMessage.isSelf();
+        }
+        return isSelf;
+    }
+
+    public void setSelf(boolean self) {
+        isSelf = self;
+    }
+
+    public String getSender() {
+        String sender = null;
+        if (v2TIMMessage != null) {
+            sender = v2TIMMessage.getSender();
+        }
+        if (TextUtils.isEmpty(sender)) {
+            sender = V2TIMManager.getInstance().getLoginUser();
+        }
+        return sender;
+    }
+
+    public V2TIMMessage getV2TIMMessage() {
+        return v2TIMMessage;
+    }
+
+    public boolean isGroup() {
+        return isGroup;
+    }
+
+    public void setGroup(boolean group) {
+        isGroup = group;
+    }
+
+    public String getGroupId() {
+        if (v2TIMMessage != null) {
+            return v2TIMMessage.getGroupID();
+        }
+        return groupId;
+    }
+
+    public String getNameCard() {
+        if (v2TIMMessage != null) {
+            return v2TIMMessage.getNameCard();
+        }
+        return "";
+    }
+
+    public String getNickName() {
+        if (v2TIMMessage != null) {
+            return v2TIMMessage.getNickName();
+        }
+        return "";
+    }
+
+    public String getFriendRemark() {
+        if (v2TIMMessage != null) {
+            return v2TIMMessage.getFriendRemark();
+        }
+        return "";
+    }
+
+    public String getUserDisplayName() {
+        String displayName;
+        if (!TextUtils.isEmpty(getNameCard())) {
+            displayName = getNameCard();
+        } else if (!TextUtils.isEmpty(getFriendRemark())) {
+            displayName = getFriendRemark();
+        } else if (!TextUtils.isEmpty(getNickName())) {
+            displayName = getNickName();
+        } else {
+            displayName = getSender();
+        }
+        return displayName;
+    }
+
+    public String getFaceUrl() {
+        if (v2TIMMessage != null) {
+            return v2TIMMessage.getFaceUrl();
+        }
+        return "";
+    }
+
+    public void setStatus(int status) {
+        this.status = status;
+    }
+
+    public int getStatus() {
+        return status;
+    }
+
+    public void setExtra(String extra) {
+        this.extra = extra;
+    }
+
+    public String getExtra() {
+        return extra;
+    }
+
+    public int getMsgType() {
+        if (v2TIMMessage != null) {
+            return v2TIMMessage.getElemType();
+        } else {
+            return V2TIMMessage.V2TIM_ELEM_TYPE_NONE;
+        }
+    }
+
+    public boolean isNeedReadReceipt() {
+        if (v2TIMMessage != null) {
+            return v2TIMMessage.isNeedReadReceipt();
+        }
+        return false;
+    }
+
+    public void setNeedReadReceipt(boolean isNeedReceipt) {
+        if (v2TIMMessage != null) {
+            v2TIMMessage.setNeedReadReceipt(isNeedReceipt);
+        }
+    }
+
+    public void setV2TIMMessage(V2TIMMessage v2TIMMessage) {
+        this.v2TIMMessage = v2TIMMessage;
+        setCommonAttribute(v2TIMMessage);
+        onProcessMessage(v2TIMMessage);
+    }
+
+    public void update(TUIMessageBean messageBean) {
+        setV2TIMMessage(messageBean.getV2TIMMessage());
+    }
+
+    public String getSelectText() {
+        return selectText;
+    }
+
+    public void setSelectText(String text) {
+        this.selectText = text;
+    }
+
+    public MessageFeature isSupportTyping() {
+        return MessageParser.isSupportTyping(this);
+    }
+
+    public void setMessageTypingFeature(MessageFeature messageFeature) {
+        MessageBuilder.mergeCloudCustomData(this, TIMCommonConstants.MESSAGE_FEATURE_KEY, messageFeature);
+    }
+
+    public UserBean getRevoker() {
+        if (revoker != null) {
+            return revoker;
+        }
+        if (v2TIMMessage != null) {
+            V2TIMUserFullInfo fullInfo = v2TIMMessage.getRevokerInfo();
+            if (fullInfo != null) {
+                revoker = new UserBean();
+                revoker.setUserId(fullInfo.getUserID());
+                revoker.setNickName(fullInfo.getNickName());
+                revoker.setFaceUrl(fullInfo.getFaceUrl());
+                return revoker;
+            }
+        }
+        return null;
+    }
+
+    public void setRevoker(UserBean revoker) {
+        this.revoker = revoker;
+    }
+
+    public String getRevokeReason() {
+        if (v2TIMMessage != null) {
+            return v2TIMMessage.getRevokeReason();
+        }
+        return null;
+    }
+
+    public void setHasRiskContent(boolean hasRiskContent) {
+        this.hasRiskContent = hasRiskContent;
+    }
+
+    public int getMessageSource() {
+        return messageSource;
+    }
+
+    public void setMessageSource(int messageSource) {
+        this.messageSource = messageSource;
+    }
+
+    public boolean customReloadWithNewMsg(V2TIMMessage v2TIMMessage) {
+        return false;
+    }
+
+    public boolean isHasReaction() {
+        return hasReaction;
+    }
+
+    public void setHasReaction(boolean hasReaction) {
+        this.hasReaction = hasReaction;
+    }
+
+    public boolean isRevoked() {
+        return getStatus() == TUIMessageBean.MSG_STATUS_REVOKE;
+    }
+
+    public void setUserBean(String userID, UserBean userBean) {
+        userBeanMap.put(userID, userBean);
+        if (messageRepliesBean != null) {
+            List<MessageRepliesBean.ReplyBean> replyBeanList = messageRepliesBean.getReplies();
+            if (replyBeanList != null && !replyBeanList.isEmpty()) {
+                for (MessageRepliesBean.ReplyBean replyBean : replyBeanList) {
+                    if (userBean != null && TextUtils.equals(replyBean.getMessageSender(), userID)) {
+                        replyBean.setSenderFaceUrl(userBean.getFaceUrl());
+                        replyBean.setSenderShowName(userBean.getDisplayName());
+                    }
+                }
+            }
+        }
+    }
+
+    public UserBean getUserBean(String userID) {
+        return userBeanMap.get(userID);
+    }
+
+    public boolean isSending() {
+        return isSending;
+    }
+
+    public void setSending(boolean sending) {
+        this.isSending = sending;
+    }
+
+    public Set<String> getAdditionalUserIDList() {
+        Set<String> userIdSet = new HashSet<>();
+        MessageRepliesBean messageRepliesBean = getMessageRepliesBean();
+        if (messageRepliesBean != null && messageRepliesBean.getRepliesSize() > 0) {
+            List<MessageRepliesBean.ReplyBean> replyBeanList = messageRepliesBean.getReplies();
+            for (MessageRepliesBean.ReplyBean replyBean : replyBeanList) {
+                userIdSet.add(replyBean.getMessageSender());
+            }
+        }
+        return userIdSet;
+    }
+
+    public void setProcessing(boolean processing) {
+        isProcessing = processing;
+    }
+
+    public boolean isProcessing() {
+        return isProcessing;
+    }
+
+    public Object getProcessingThumbnail() {
+        return processingThumbnail;
+    }
+
+    public void setProcessingThumbnail(Object processingThumbnail) {
+        this.processingThumbnail = processingThumbnail;
+    }
+
+    public boolean needAsyncGetDisplayString() {
+        return false;
+    }
+
+    public Class<? extends TUIReplyQuoteBean> getReplyQuoteBeanClass() {
+        return null;
+    }
+}

+ 42 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/TUIReplyQuoteBean.java

@@ -0,0 +1,42 @@
+package com.tencent.qcloud.tuikit.timcommon.bean;
+
+import java.io.Serializable;
+
+public abstract class TUIReplyQuoteBean<T extends TUIMessageBean> implements Serializable {
+    private T messageBean;
+    protected String defaultAbstract;
+    protected int messageType;
+
+    public abstract void onProcessReplyQuoteBean(T messageBean);
+
+    public void setMessageBean(T messageBean) {
+        this.messageBean = messageBean;
+    }
+
+    public void setDefaultAbstract(String defaultAbstract) {
+        this.defaultAbstract = defaultAbstract;
+    }
+
+    public void setMessageType(int messageType) {
+        this.messageType = messageType;
+    }
+
+    public int getMessageType() {
+        return messageType;
+    }
+
+    public T getMessageBean() {
+        return messageBean;
+    }
+
+    public boolean hasRiskContent() {
+        if (messageBean != null) {
+            return messageBean.hasRiskContent();
+        }
+        return false;
+    }
+
+    public String getDefaultAbstract() {
+        return defaultAbstract;
+    }
+}

+ 82 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/UserBean.java

@@ -0,0 +1,82 @@
+package com.tencent.qcloud.tuikit.timcommon.bean;
+
+import android.text.TextUtils;
+import java.io.Serializable;
+
+public class UserBean implements Serializable {
+    protected String userId;
+    protected String nickName;
+    protected String nameCard;
+    protected String friendRemark;
+    protected String faceUrl;
+    protected String signature;
+    protected long birthday;
+
+    public String getUserId() {
+        return userId;
+    }
+
+    public void setUserId(String userId) {
+        this.userId = userId;
+    }
+
+    public String getNickName() {
+        return nickName;
+    }
+
+    public void setNickName(String nickName) {
+        this.nickName = nickName;
+    }
+
+    public String getFriendRemark() {
+        return friendRemark;
+    }
+
+    public void setFriendRemark(String friendRemark) {
+        this.friendRemark = friendRemark;
+    }
+
+    public void setNameCard(String nameCard) {
+        this.nameCard = nameCard;
+    }
+
+    public String getNameCard() {
+        return nameCard;
+    }
+
+    public String getDisplayName() {
+        if (!TextUtils.isEmpty(nameCard)) {
+            return nameCard;
+        } else if (!TextUtils.isEmpty(friendRemark)) {
+            return friendRemark;
+        } else if (!TextUtils.isEmpty(nickName)) {
+            return nickName;
+        } else {
+            return userId;
+        }
+    }
+
+    public String getFaceUrl() {
+        return faceUrl;
+    }
+
+    public void setFaceUrl(String faceUrl) {
+        this.faceUrl = faceUrl;
+    }
+
+    public String getSignature() {
+        return signature;
+    }
+
+    public long getBirthday() {
+        return birthday;
+    }
+
+    public void setSignature(String signature) {
+        this.signature = signature;
+    }
+
+    public void setBirthday(long birthday) {
+        this.birthday = birthday;
+    }
+}

+ 181 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/classicui/widget/message/MessageBaseHolder.java

@@ -0,0 +1,181 @@
+package com.tencent.qcloud.tuikit.timcommon.classicui.widget.message;
+
+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.RelativeLayout;
+import android.widget.TextView;
+import androidx.recyclerview.widget.RecyclerView;
+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.classicui.TUIConfigClassic;
+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 com.tencent.qcloud.tuikit.timcommon.util.DateTimeUtil;
+import java.util.Date;
+
+public abstract class MessageBaseHolder<T extends TUIMessageBean> extends RecyclerView.ViewHolder {
+    public ICommonMessageAdapter mAdapter;
+    protected OnItemClickListener onItemClickListener;
+
+    public TextView chatTimeText;
+    public FrameLayout msgContentFrame;
+    public LinearLayout msgReplyDetailLayout;
+    public LinearLayout msgArea;
+    public LinearLayout msgAreaAndReply;
+    public FrameLayout reactionArea;
+    public CheckBox mMutiSelectCheckBox;
+    public RelativeLayout rightGroupLayout;
+    public RelativeLayout mContentLayout;
+    private HighlightListener highlightListener;
+    protected T currentMessageBean;
+
+    public MessageBaseHolder(View itemView) {
+        super(itemView);
+        chatTimeText = itemView.findViewById(R.id.message_top_time_tv);
+        msgContentFrame = itemView.findViewById(R.id.msg_content_fl);
+        msgReplyDetailLayout = itemView.findViewById(R.id.msg_reply_detail_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);
+        rightGroupLayout = itemView.findViewById(R.id.right_group_layout);
+        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 T 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(DateTimeUtil.getTimeFormatText(new Date(msg.getMessageTime() * 1000)));
+                } else {
+                    chatTimeText.setVisibility(View.GONE);
+                }
+            }
+        } else {
+            chatTimeText.setVisibility(View.VISIBLE);
+            chatTimeText.setText(DateTimeUtil.getTimeFormatText(new Date(msg.getMessageTime() * 1000)));
+        }
+    }
+
+    private void setChatTimeStyle() {
+        Drawable chatTimeBubble = TUIConfigClassic.getChatTimeBubble();
+        if (chatTimeBubble != null) {
+            chatTimeText.setBackground(chatTimeBubble);
+        }
+        int chatTimeFontColor = TUIConfigClassic.getChatTimeFontColor();
+        if (chatTimeFontColor != TUIConfigClassic.UNDEFINED) {
+            chatTimeText.setTextColor(chatTimeFontColor);
+        }
+        int chatTimeFontSize = TUIConfigClassic.getChatTimeFontSize();
+        if (chatTimeFontSize != TUIConfigClassic.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 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);
+        }
+    }
+}

+ 708 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/classicui/widget/message/MessageContentHolder.java

@@ -0,0 +1,708 @@
+package com.tencent.qcloud.tuikit.timcommon.classicui.widget.message;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.drawable.Animatable;
+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.ProgressBar;
+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.engine.DiskCacheStrategy;
+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.config.classicui.TUIConfigClassic;
+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.Locale;
+import java.util.Map;
+
+public abstract class MessageContentHolder<T extends TUIMessageBean> extends MessageBaseHolder<T> {
+    public ImageView leftUserIcon;
+    public ImageView rightUserIcon;
+    public TextView leftUserNameText;
+    public LinearLayout msgContentLinear;
+    public View riskContentLine;
+    public TextView riskContentText;
+    public ProgressBar sendingProgress;
+    public ImageView statusImage;
+    public TextView isReadText;
+    public TextView unreadAudioText;
+    public TextView messageDetailsTimeTv;
+    private FrameLayout bottomContentFrameLayout;
+    private View bottomFailedIv;
+
+    public boolean isForwardMode = false;
+    public boolean isReplyDetailMode = false;
+    public boolean isMultiSelectMode = false;
+
+    private List<TUIMessageBean> mForwardDataSource = new ArrayList<>();
+    protected SelectionHelper selectionHelper;
+
+    // Whether to display the bottom content. The merged-forwarded message details activity does not display the bottom content.
+    protected boolean isNeedShowBottomLayout = true;
+    protected boolean isShowRead = false;
+    private Fragment fragment;
+    private RecyclerView recyclerView;
+    protected boolean hasRiskContent = false;
+    protected boolean isLayoutOnStart = true;
+
+    public MessageContentHolder(View itemView) {
+        super(itemView);
+        leftUserIcon = itemView.findViewById(R.id.left_user_icon_view);
+        rightUserIcon = itemView.findViewById(R.id.right_user_icon_view);
+        leftUserNameText = itemView.findViewById(R.id.left_user_name_tv);
+        msgContentLinear = itemView.findViewById(R.id.msg_content_ll);
+        riskContentLine = itemView.findViewById(R.id.risk_content_line);
+        riskContentText = itemView.findViewById(R.id.risk_content_text);
+        statusImage = itemView.findViewById(R.id.message_status_iv);
+        sendingProgress = itemView.findViewById(R.id.message_sending_pb);
+        sendingProgress.getIndeterminateDrawable().mutate();
+        isReadText = itemView.findViewById(R.id.is_read_tv);
+        unreadAudioText = itemView.findViewById(R.id.audio_unread);
+        messageDetailsTimeTv = itemView.findViewById(R.id.msg_detail_time_tv);
+        bottomContentFrameLayout = itemView.findViewById(R.id.bottom_content_fl);
+        bottomFailedIv = itemView.findViewById(R.id.bottom_failed_iv);
+    }
+
+    public void setFragment(Fragment fragment) {
+        this.fragment = fragment;
+    }
+
+    public void setRecyclerView(RecyclerView recyclerView) {
+        this.recyclerView = recyclerView;
+    }
+
+    public RecyclerView getRecyclerView() {
+        return this.recyclerView;
+    }
+
+    public void setForwardDataSource(List<TUIMessageBean> dataSource) {
+        if (dataSource == null || dataSource.isEmpty()) {
+            mForwardDataSource = null;
+        }
+
+        List<TUIMessageBean> 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);
+            }
+        }
+        mForwardDataSource = mediaSource;
+    }
+
+    public List<TUIMessageBean> getForwardDataSource() {
+        return mForwardDataSource;
+    }
+
+    @Override
+    public void layoutViews(final T msg, final int position) {
+        Context context = itemView.getContext();
+        if (TUIUtil.isActivityDestroyed(context)) {
+            return;
+        }
+
+        hasRiskContent = msg.hasRiskContent();
+        super.layoutViews(msg, position);
+        setLayoutAlignment(msg);
+        setUserIcon(msg);
+        setUserName(msg);
+        loadAvatar(msg);
+        setSendingProgress(msg);
+        setStatusImage(msg);
+        setMessageBubbleBackground();
+        setOnClickListener(msg, position);
+
+        if (rightGroupLayout != null) {
+            rightGroupLayout.setVisibility(View.VISIBLE);
+        }
+        msgContentLinear.setVisibility(View.VISIBLE);
+
+        setReadStatus(msg);
+
+        if (isReplyDetailMode) {
+            chatTimeText.setVisibility(View.GONE);
+        }
+
+        setReplyContent(msg);
+        setReactContent(msg);
+        if (isNeedShowBottomLayout) {
+            setBottomContent(msg);
+        }
+        bottomFailedIv.setVisibility(View.GONE);
+        if (hasRiskContent) {
+            bottomContentFrameLayout.setBackgroundResource(R.drawable.chat_message_bottom_area_risk_bg);
+            if (bottomContentFrameLayout.getVisibility() == View.VISIBLE) {
+                bottomFailedIv.setVisibility(View.VISIBLE);
+            }
+            riskContentLine.setVisibility(View.VISIBLE);
+        } else {
+            riskContentLine.setVisibility(View.GONE);
+            bottomContentFrameLayout.setBackgroundResource(R.drawable.chat_message_bottom_area_bg);
+        }
+
+        setMessageBubbleDefaultPadding();
+        layoutVariableViews(msg, position);
+    }
+
+    private void setReadStatus(T msg) {
+        // clear isReadText status
+        isReadText.setTextColor(isReadText.getResources().getColor(R.color.text_gray1));
+        isReadText.setOnClickListener(null);
+
+        if (isForwardMode || isReplyDetailMode) {
+            isReadText.setVisibility(View.GONE);
+            unreadAudioText.setVisibility(View.GONE);
+        } else {
+            if (isShowRead) {
+                if (msg.isSelf() && TUIMessageBean.MSG_STATUS_SEND_SUCCESS == msg.getStatus()) {
+                    if (!msg.isNeedReadReceipt()) {
+                        isReadText.setVisibility(View.GONE);
+                    } else {
+                        showReadText(msg);
+                    }
+                } else {
+                    isReadText.setVisibility(View.GONE);
+                }
+            }
+            unreadAudioText.setVisibility(View.GONE);
+        }
+    }
+
+    private void setLayoutAlignment(TUIMessageBean msg) {
+        if (isForwardMode || isReplyDetailMode) {
+            isLayoutOnStart = true;
+        } else {
+            if (msg.isSelf()) {
+                isLayoutOnStart = false;
+            } else {
+                isLayoutOnStart = true;
+            }
+        }
+        if (isForwardMode || isReplyDetailMode) {
+            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);
+    }
+
+    private void setMessageBubbleBackground() {
+        if (!TUIConfigClassic.isEnableMessageBubbleStyle()) {
+            setMessageBubbleBackground(null);
+            return;
+        }
+
+        Drawable sendBubble = TUIConfigClassic.getSendBubbleBackground();
+        Drawable receiveBubble = TUIConfigClassic.getReceiveBubbleBackground();
+        Drawable sendErrorBubble = TUIConfigClassic.getSendErrorBubbleBackground();
+        Drawable receiveErrorBubble = TUIConfigClassic.getReceiveErrorBubbleBackground();
+
+        if (hasRiskContent) {
+            if (!isLayoutOnStart) {
+                if (sendErrorBubble != null) {
+                    setMessageBubbleBackground(sendErrorBubble);
+                } else {
+                    setMessageBubbleBackground(R.drawable.chat_message_popup_risk_content_border_right);
+                }
+            } else {
+                if (receiveErrorBubble != null) {
+                    setMessageBubbleBackground(receiveErrorBubble);
+                } else {
+                    setMessageBubbleBackground(R.drawable.chat_message_popup_risk_content_border_left);
+                }
+            }
+        } else {
+            setRiskContent(null);
+            if (isLayoutOnStart) {
+                if (receiveBubble != null) {
+                    setMessageBubbleBackground(receiveBubble);
+                } else {
+                    setMessageBubbleBackground(TUIThemeManager.getAttrResId(itemView.getContext(), R.attr.chat_bubble_other_bg));
+                }
+            } else {
+                if (sendBubble != null) {
+                    setMessageBubbleBackground(sendBubble);
+                } else {
+                    setMessageBubbleBackground(TUIThemeManager.getAttrResId(itemView.getContext(), R.attr.chat_bubble_self_bg));
+                }
+            }
+        }
+    }
+
+    protected void setStatusImage(T msg) {
+        statusImage.setVisibility(View.GONE);
+        if (hasRiskContent) {
+            statusImage.setVisibility(View.VISIBLE);
+        } else {
+            if (msg.getStatus() == TUIMessageBean.MSG_STATUS_SEND_FAIL) {
+                statusImage.setVisibility(View.VISIBLE);
+                statusImage.setOnClickListener(new View.OnClickListener() {
+                    @Override
+                    public void onClick(View v) {
+                        if (onItemClickListener != null) {
+                            onItemClickListener.onSendFailBtnClick(statusImage, msg);
+                        }
+                    }
+                });
+            }
+        }
+    }
+
+    protected void setRiskContent(String riskContent) {
+        if (TextUtils.isEmpty(riskContent)) {
+            riskContentLine.setVisibility(View.GONE);
+            riskContentText.setVisibility(View.GONE);
+        } else {
+            riskContentLine.setVisibility(View.VISIBLE);
+            riskContentText.setVisibility(View.VISIBLE);
+            riskContentText.setText(riskContent);
+        }
+    }
+
+    private void setOnClickListener(T msg, int position) {
+        msgContentFrame.setOnLongClickListener(new View.OnLongClickListener() {
+            @Override
+            public boolean onLongClick(View v) {
+                if (onItemClickListener != null) {
+                    onItemClickListener.onMessageLongClick(v, msg);
+                }
+                return true;
+            }
+        });
+
+        msgArea.setOnLongClickListener(new View.OnLongClickListener() {
+            @Override
+            public boolean onLongClick(View v) {
+                if (onItemClickListener != null) {
+                    onItemClickListener.onMessageLongClick(msgArea, msg);
+                }
+                return true;
+            }
+        });
+
+        leftUserIcon.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                if (onItemClickListener != null) {
+                    onItemClickListener.onUserIconClick(view, msg);
+                }
+            }
+        });
+        leftUserIcon.setOnLongClickListener(new View.OnLongClickListener() {
+            @Override
+            public boolean onLongClick(View view) {
+                if (onItemClickListener != null) {
+                    onItemClickListener.onUserIconLongClick(view, msg);
+                }
+                return true;
+            }
+        });
+        rightUserIcon.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                if (onItemClickListener != null) {
+                    onItemClickListener.onUserIconClick(view, msg);
+                }
+            }
+        });
+
+        if (msg.getStatus() == TUIMessageBean.MSG_STATUS_SEND_FAIL) {
+            msgContentFrame.setOnClickListener(new View.OnClickListener() {
+                @Override
+                public void onClick(View view) {
+                    if (onItemClickListener != null) {
+                        onItemClickListener.onMessageLongClick(msgContentFrame, msg);
+                    }
+                }
+            });
+        } else {
+            msgContentFrame.setOnClickListener(new View.OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    if (onItemClickListener != null) {
+                        onItemClickListener.onMessageClick(msgContentFrame, msg);
+                    }
+                }
+            });
+        }
+    }
+
+    private void setSendingProgress(T msg) {
+        if (isForwardMode || isReplyDetailMode) {
+            hideSendingProgress();
+        } else {
+            if (msg.isSelf()) {
+                if (msg.isSending()) {
+                    showSendingProgress();
+                } else {
+                    hideSendingProgress();
+                }
+            } else {
+                hideSendingProgress();
+            }
+        }
+    }
+
+    protected void showSendingProgress() {
+        sendingProgress.setVisibility(View.VISIBLE);
+        Drawable drawable = sendingProgress.getIndeterminateDrawable();
+        if (drawable instanceof Animatable) {
+            ((Animatable) drawable).start();
+        }
+    }
+
+    protected void hideSendingProgress() {
+        sendingProgress.setVisibility(View.GONE);
+    }
+
+    @SuppressLint("WrongConstant")
+    private void setUserName(T msg) {
+        if (isForwardMode || isReplyDetailMode) {
+            leftUserNameText.setVisibility(View.VISIBLE);
+        } else {
+            if (isLayoutOnStart) {
+                if (TUIConfigClassic.getReceiveNickNameVisibility() != TUIConfigClassic.UNDEFINED) {
+                    leftUserNameText.setVisibility(TUIConfigClassic.getReceiveNickNameVisibility());
+                } else {
+                    if (msg.isGroup()) {
+                        leftUserNameText.setVisibility(View.VISIBLE);
+                    } else {
+                        leftUserNameText.setVisibility(View.GONE);
+                    }
+                }
+            } else {
+                leftUserNameText.setVisibility(View.GONE);
+            }
+        }
+        if (TUIConfigClassic.getReceiveNickNameColor() != TUIConfigClassic.UNDEFINED) {
+            leftUserNameText.setTextColor(TUIConfigClassic.getReceiveNickNameColor());
+        }
+
+        if (TUIConfigClassic.getReceiveNickNameFontSize() != TUIConfigClassic.UNDEFINED) {
+            leftUserNameText.setTextSize(TUIConfigClassic.getReceiveNickNameFontSize());
+        }
+
+        leftUserNameText.setText(msg.getUserDisplayName());
+    }
+
+    private void setUserIcon(T msg) {
+        if (isForwardMode || isReplyDetailMode) {
+            leftUserIcon.setVisibility(View.VISIBLE);
+            rightUserIcon.setVisibility(View.GONE);
+        } else {
+            if (msg.isSelf()) {
+                leftUserIcon.setVisibility(View.GONE);
+                rightUserIcon.setVisibility(View.VISIBLE);
+            } else {
+                leftUserIcon.setVisibility(View.VISIBLE);
+                rightUserIcon.setVisibility(View.GONE);
+            }
+        }
+    }
+
+    private void setBottomContent(TUIMessageBean msg) {
+        HashMap<String, Object> 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.CLASSIC_EXTENSION_ID, bottomContentFrameLayout, param);
+    }
+
+    private void loadAvatar(TUIMessageBean msg) {
+        Drawable drawable = TUIConfigClassic.getDefaultAvatarImage();
+        if (drawable != null) {
+            setupAvatar(drawable);
+            return;
+        }
+
+        if (msg.isUseMsgReceiverAvatar() && mAdapter != null) {
+            String cachedFaceUrl = mAdapter.getUserFaceUrlCache().getCachedFaceUrl(msg.getSender());
+            if (cachedFaceUrl == null) {
+                List<String> idList = new ArrayList<>();
+                idList.add(msg.getSender());
+                V2TIMManager.getInstance().getUsersInfo(idList, new V2TIMValueCallback<List<V2TIMUserFullInfo>>() {
+                    @Override
+                    public void onSuccess(List<V2TIMUserFullInfo> 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 = TUIConfigClassic.getMessageListAvatarSize();
+        if (avatarSize == TUIConfigClassic.UNDEFINED) {
+            avatarSize = ScreenUtil.dip2px(41);
+        }
+        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(4);
+        if (TUIConfigClassic.getMessageListAvatarRadius() != TUIConfigClassic.UNDEFINED) {
+            radius = TUIConfigClassic.getMessageListAvatarRadius();
+        }
+
+        ImageView renderedView;
+        if (isLayoutOnStart) {
+            renderedView = leftUserIcon;
+        } else {
+            renderedView = rightUserIcon;
+        }
+        Glide.with(itemView.getContext()).clear(renderedView);
+        int defaultIconResId = TUIThemeManager.getAttrResId(leftUserIcon.getContext(), com.tencent.qcloud.tuikit.timcommon.R.attr.core_default_user_icon);
+        RequestBuilder<Drawable> placeholderRequest = Glide.with(itemView.getContext())
+                .load(defaultIconResId)
+                .transform(new RoundedCorners(radius))
+                .diskCacheStrategy(DiskCacheStrategy.ALL);
+        RequestBuilder<Drawable> errorRequestBuilder = Glide.with(itemView.getContext())
+                .load(defaultIconResId)
+                .transform(new RoundedCorners(radius))
+                .diskCacheStrategy(DiskCacheStrategy.ALL);
+        Glide.with(itemView.getContext())
+                .load(faceUrl)
+                .thumbnail(placeholderRequest)
+                .transform(new RoundedCorners(radius))
+                .diskCacheStrategy(DiskCacheStrategy.ALL)
+                .skipMemoryCache(false)
+                .dontAnimate()
+                .error(errorRequestBuilder)
+                .into(renderedView);
+    }
+
+    protected void setMessageBubbleDefaultPadding() {
+        // after setting background, the padding will be reset
+        int paddingHorizontal = itemView.getResources().getDimensionPixelSize(R.dimen.chat_message_area_padding_left_right);
+        int paddingVertical = itemView.getResources().getDimensionPixelSize(R.dimen.chat_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;
+        }
+        msgArea.setGravity(gravity);
+        msgContentFrame.setLayoutParams(layoutParams);
+    }
+
+    private void setReplyContent(TUIMessageBean messageBean) {
+        MessageRepliesBean messageRepliesBean = messageBean.getMessageRepliesBean();
+        if (messageRepliesBean != null && messageRepliesBean.getRepliesSize() > 0) {
+            TextView replyNumText = msgReplyDetailLayout.findViewById(R.id.reply_num);
+            replyNumText.setText(String.format(Locale.US, replyNumText.getResources().getString(R.string.chat_reply_num), messageRepliesBean.getRepliesSize()));
+            msgReplyDetailLayout.setVisibility(View.VISIBLE);
+            msgReplyDetailLayout.setOnClickListener(new View.OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    if (onItemClickListener != null) {
+                        onItemClickListener.onReplyDetailClick(messageBean);
+                    }
+                }
+            });
+        } else {
+            msgReplyDetailLayout.setVisibility(View.GONE);
+            msgReplyDetailLayout.setOnClickListener(null);
+        }
+        if (!isReplyDetailMode) {
+            messageDetailsTimeTv.setVisibility(View.GONE);
+        } else {
+            messageDetailsTimeTv.setText(DateTimeUtil.getTimeFormatText(new Date(messageBean.getMessageTime() * 1000)));
+            messageDetailsTimeTv.setVisibility(View.VISIBLE);
+            msgReplyDetailLayout.setVisibility(View.GONE);
+        }
+    }
+
+    private void setReactContent(TUIMessageBean messageBean) {
+        Map<String, Object> 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_CLASSIC);
+        TUICore.raiseExtension(TUIConstants.TUIChat.Extension.MessageReactPreviewExtension.EXTENSION_ID, reactionArea, param);
+    }
+
+    private void showReadText(TUIMessageBean msg) {
+        if (hasRiskContent) {
+            isReadText.setVisibility(View.GONE);
+            return;
+        }
+        if (msg.isGroup()) {
+            isReadText.setVisibility(View.VISIBLE);
+            if (msg.isAllRead()) {
+                isReadText.setText(R.string.has_all_read);
+            } else if (msg.isUnread()) {
+                isReadText.setTextColor(
+                    isReadText.getResources().getColor(TUIThemeManager.getAttrResId(isReadText.getContext(), R.attr.chat_read_receipt_text_color)));
+                isReadText.setText(R.string.unread);
+                isReadText.setOnClickListener(new View.OnClickListener() {
+                    @Override
+                    public void onClick(View v) {
+                        onReadStatusClick(v, msg);
+                    }
+                });
+            } else {
+                long readCount = msg.getReadCount();
+                if (readCount > 0) {
+                    isReadText.setText(String.format(Locale.US, isReadText.getResources().getString(R.string.someone_has_read), readCount));
+                    isReadText.setTextColor(
+                        isReadText.getResources().getColor(TUIThemeManager.getAttrResId(isReadText.getContext(), R.attr.chat_read_receipt_text_color)));
+                    isReadText.setOnClickListener(new View.OnClickListener() {
+                        @Override
+                        public void onClick(View v) {
+                            onReadStatusClick(v, msg);
+                        }
+                    });
+                }
+            }
+        } else {
+            isReadText.setVisibility(View.VISIBLE);
+            if (msg.isPeerRead()) {
+                isReadText.setText(R.string.has_read);
+            } else {
+                isReadText.setText(R.string.unread);
+                isReadText.setTextColor(
+                    isReadText.getResources().getColor(TUIThemeManager.getAttrResId(isReadText.getContext(), R.attr.chat_read_receipt_text_color)));
+                isReadText.setOnClickListener(new View.OnClickListener() {
+                    @Override
+                    public void onClick(View v) {
+                        onReadStatusClick(v, msg);
+                    }
+                });
+            }
+        }
+    }
+
+    public abstract void layoutVariableViews(final T msg, final int position);
+
+    public void onRecycled() {
+        super.onRecycled();
+        if (selectionHelper != null) {
+            selectionHelper.destroy();
+        }
+    }
+
+    public void onReadStatusClick(View view, TUIMessageBean messageBean) {
+        if (onItemClickListener != null) {
+            onItemClickListener.onMessageReadStatusClick(view, messageBean);
+        }
+    }
+
+    protected void setSelectionHelper(TUIMessageBean msg, TextView textView, int position) {
+        if (selectionHelper == null) {
+            selectionHelper = new SelectionHelper();
+        }
+        selectionHelper.setTextView(textView);
+        if (isMultiSelectMode || isForwardMode) {
+            selectionHelper.setFrozen(true);
+        } else {
+            selectionHelper.setFrozen(false);
+        }
+        selectionHelper.setSelectListener(new SelectionHelper.OnSelectListener() {
+            @Override
+            public void onTextSelected(CharSequence content) {
+                String selectedText = "";
+                if (!TextUtils.isEmpty(content)) {
+                    selectedText = content.toString();
+                    msg.setSelectText(selectedText);
+                    SelectionHelper.setSelected(selectionHelper);
+                    if (onItemClickListener != null) {
+                        onItemClickListener.onTextSelected(msgArea, position, msg);
+                    }
+                }
+            }
+
+            @Override
+            public void onDismiss() {
+                msg.setSelectText(msg.getExtra());
+            }
+
+            @Override
+            public void onClickUrl(String url) {}
+
+            @Override
+            public void onShowPop() {}
+
+            @Override
+            public void onDismissPop() {}
+        });
+    }
+
+    public void setNeedShowBottomLayout(boolean needShowBottomLayout) {
+        isNeedShowBottomLayout = needShowBottomLayout;
+    }
+
+    public void setShowRead(boolean showRead) {
+        isShowRead = showRead;
+    }
+}

+ 512 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/classicui/widget/message/SelectionHelper.java

@@ -0,0 +1,512 @@
+package com.tencent.qcloud.tuikit.timcommon.classicui.widget.message;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.text.Layout;
+import android.text.Spannable;
+import android.text.Spanned;
+import android.text.method.LinkMovementMethod;
+import android.text.style.BackgroundColorSpan;
+import android.text.style.ClickableSpan;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewTreeObserver;
+import android.widget.PopupWindow;
+import android.widget.TextView;
+import com.tencent.qcloud.tuikit.timcommon.component.face.CenterImageSpan;
+import com.tencent.qcloud.tuikit.timcommon.util.ScreenUtil;
+import com.tencent.qcloud.tuikit.timcommon.util.TUIUtil;
+import com.tencent.qcloud.tuikit.timcommon.util.TextUtil;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+
+public class SelectionHelper {
+    private static final String TAG = "SelectionHelper";
+
+    private SelectionHandle startHandle;
+    private SelectionHandle endHandle;
+    private final SelectionInfo mSelectionInfo = new SelectionInfo();
+    private OnSelectListener mSelectListener;
+
+    private TextView textView;
+    private Spannable spannable;
+
+    private int mTextViewMarginStart = 0;
+    private GestureDetector gestureDetector;
+    private GestureDetector.SimpleOnGestureListener gestureListener;
+    private int selectionColor;
+    private int handleColor;
+    private int handleSize;
+    private BackgroundColorSpan bgSpan;
+    private boolean frozen = false;
+
+    private ViewTreeObserver.OnPreDrawListener mOnPreDrawListener;
+    private View.OnAttachStateChangeListener onAttachStateChangeListener;
+    private static WeakReference<SelectionHelper> selectedReference;
+
+    public static void setSelected(SelectionHelper selected) {
+        SelectionHelper oldSelected = getSelected();
+        if (oldSelected != null && selected != oldSelected) {
+            oldSelected.clearSelection();
+        }
+        selectedReference = new WeakReference<>(selected);
+    }
+
+    public static void resetSelected() {
+        SelectionHelper selectionHelper = getSelected();
+        if (selectionHelper != null) {
+            selectionHelper.clearSelection();
+        }
+    }
+
+    private static SelectionHelper getSelected() {
+        if (selectedReference != null) {
+            return selectedReference.get();
+        }
+        return null;
+    }
+
+    public void setFrozen(boolean frozen) {
+        this.frozen = frozen;
+    }
+
+    public interface OnSelectListener {
+        void onTextSelected(CharSequence content);
+
+        void onDismiss();
+
+        void onClickUrl(String url);
+
+        void onShowPop();
+
+        void onDismissPop();
+    }
+
+    public SelectionHelper() {
+        selectionColor = 0x3f1470ff;
+        handleColor = 0xff1470ff;
+        handleSize = ScreenUtil.dip2px(16);
+        gestureListener = new GestureDetector.SimpleOnGestureListener() {
+            @Override
+            public void onShowPress(MotionEvent e) {
+                if (frozen) {
+                    return;
+                }
+                initHandler();
+                ClickableSpan[] spans = TextUtil.findSpansByLocation(textView, Math.round(e.getX()), Math.round(e.getY()));
+                if (spans != null && spans.length > 0) {
+                    ClickableSpan span = spans[0];
+                    int spanStart = spannable.getSpanStart(span);
+                    int spanEnd = spannable.getSpanEnd(span);
+                    setSelection(spanStart, spanEnd);
+                } else {
+                    selectAll();
+                }
+            }
+
+            @Override
+            public boolean onSingleTapUp(MotionEvent e) {
+                if (frozen) {
+                    return super.onSingleTapUp(e);
+                }
+                ClickableSpan[] spans = TextUtil.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;
+            }
+        };
+        gestureDetector = new GestureDetector(gestureListener);
+    }
+
+    public void setSelectListener(OnSelectListener selectListener) {
+        mSelectListener = selectListener;
+    }
+
+    public void destroy() {
+        if (textView == null) {
+            return;
+        }
+        textView.removeOnAttachStateChangeListener(onAttachStateChangeListener);
+        textView.getViewTreeObserver().removeOnPreDrawListener(mOnPreDrawListener);
+        clearSelection();
+    }
+
+    public void selectAll() {
+        initHandler();
+        if (textView.getText() instanceof Spannable) {
+            spannable = (Spannable) textView.getText();
+        }
+        if (spannable == null) {
+            return;
+        }
+        setSelection(0, textView.getText().length());
+    }
+
+    public void setTextView(TextView textView) {
+        this.textView = textView;
+        if (textView.getText() instanceof Spannable) {
+            this.spannable = (Spannable) textView.getText();
+        }
+        textView.removeOnAttachStateChangeListener(onAttachStateChangeListener);
+        onAttachStateChangeListener = new View.OnAttachStateChangeListener() {
+            @Override
+            public void onViewAttachedToWindow(View v) {}
+
+            @Override
+            public void onViewDetachedFromWindow(View v) {
+                destroy();
+            }
+        };
+        textView.addOnAttachStateChangeListener(onAttachStateChangeListener);
+        textView.getViewTreeObserver().removeOnPreDrawListener(mOnPreDrawListener);
+        mOnPreDrawListener = new ViewTreeObserver.OnPreDrawListener() {
+            @Override
+            public boolean onPreDraw() {
+                int[] location = new int[2];
+                textView.getLocationInWindow(location);
+                mTextViewMarginStart = location[0];
+                return true;
+            }
+        };
+        textView.getViewTreeObserver().addOnPreDrawListener(mOnPreDrawListener);
+        textView.setMovementMethod(new LinkMovementMethodInterceptor());
+    }
+
+    private void initHandler() {
+        if (startHandle == null) {
+            startHandle = new SelectionHandle(true);
+        }
+        if (endHandle == null) {
+            endHandle = new SelectionHandle(false);
+        }
+    }
+
+    private void showSelectionHandle(SelectionHandle selectionHandle) {
+        Layout layout = textView.getLayout();
+        if (layout == null) {
+            return;
+        }
+        int offset = selectionHandle.isLeft ? mSelectionInfo.start : mSelectionInfo.end;
+        selectionHandle.show((int) layout.getPrimaryHorizontal(offset), layout.getLineBottom(layout.getLineForOffset(offset)));
+    }
+
+    private void setSelection(int startPos, int endPos) {
+        initHandler();
+
+        if (startPos != -1) {
+            mSelectionInfo.start = startPos;
+        }
+        if (endPos != -1) {
+            mSelectionInfo.end = endPos;
+        }
+        if (mSelectionInfo.start > mSelectionInfo.end) {
+            int temp = mSelectionInfo.start;
+            mSelectionInfo.start = mSelectionInfo.end;
+            mSelectionInfo.end = temp;
+        }
+
+        mSelectionInfo.selectionContent = spannable.subSequence(mSelectionInfo.start, mSelectionInfo.end).toString();
+        setSelectionBg(spannable, mSelectionInfo.start, mSelectionInfo.end);
+        showSelectionHandle(startHandle);
+        showSelectionHandle(endHandle);
+        if (mSelectListener != null) {
+            mSelectListener.onTextSelected(mSelectionInfo.selectionContent);
+        }
+    }
+
+    private void setSelectionBg(Spannable text, int start, int end) {
+        if (bgSpan == null) {
+            bgSpan = new BackgroundColorSpan(selectionColor);
+        }
+        text.setSpan(bgSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+        CenterImageSpan[] allImageSpans = text.getSpans(0, text.length(), CenterImageSpan.class);
+        CenterImageSpan[] imageSpans = text.getSpans(start, end, CenterImageSpan.class);
+        if (allImageSpans != null) {
+            for (CenterImageSpan imageSpan : allImageSpans) {
+                imageSpan.setBgColor(-1);
+            }
+        }
+        if (imageSpans != null) {
+            for (CenterImageSpan imageSpan : imageSpans) {
+                imageSpan.setBgColor(selectionColor);
+            }
+        }
+    }
+
+    private void clearSelection() {
+        mSelectionInfo.selectionContent = null;
+        clearSelectionBg();
+    }
+
+    private void clearSelectionBg() {
+        if (spannable == null) {
+            return;
+        }
+        if (bgSpan != null) {
+            spannable.removeSpan(bgSpan);
+        }
+        CenterImageSpan[] imageSpans = spannable.getSpans(0, spannable.length(), CenterImageSpan.class);
+        if (imageSpans != null) {
+            for (CenterImageSpan imageSpan : imageSpans) {
+                imageSpan.setBgColor(-1);
+            }
+        }
+        if (startHandle != null) {
+            startHandle.dismiss();
+        }
+        if (endHandle != null) {
+            endHandle.dismiss();
+        }
+    }
+
+    private class SelectionHandle extends View {
+        private PopupWindow mPopupWindow;
+        private Paint mPaint;
+
+        private int mCircleRadius = handleSize / 2;
+        private int mWidth = handleSize;
+        private int mHeight = handleSize;
+        private int mPadding = 32;
+        private boolean isLeft;
+
+        public SelectionHandle(boolean isLeft) {
+            super(textView.getContext());
+            this.isLeft = isLeft;
+            mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+            mPaint.setColor(handleColor);
+
+            mPopupWindow = new PopupWindow(this);
+            mPopupWindow.setClippingEnabled(false);
+            mPopupWindow.setWidth(mWidth + mPadding * 2);
+            mPopupWindow.setHeight(mHeight + mPadding / 2);
+            invalidate();
+        }
+
+        @Override
+        protected void onDraw(Canvas canvas) {
+            canvas.drawCircle(mCircleRadius + mPadding, mCircleRadius, mCircleRadius, mPaint);
+            if (isLeft) {
+                canvas.drawRect(mCircleRadius + mPadding, 0, mCircleRadius * 2 + mPadding, mCircleRadius, mPaint);
+            } else {
+                canvas.drawRect(mPadding, 0, mCircleRadius + mPadding, mCircleRadius, mPaint);
+            }
+        }
+
+        private int mAdjustX;
+        private int mAdjustY;
+
+        private int mBeforeDragStart;
+        private int mBeforeDragEnd;
+
+        @Override
+        public boolean onTouchEvent(MotionEvent event) {
+            switch (event.getAction()) {
+                case MotionEvent.ACTION_DOWN:
+                    mBeforeDragStart = mSelectionInfo.start;
+                    mBeforeDragEnd = mSelectionInfo.end;
+                    mAdjustX = (int) event.getX();
+                    mAdjustY = (int) event.getY();
+                    break;
+                case MotionEvent.ACTION_UP:
+                case MotionEvent.ACTION_CANCEL:
+                    break;
+                case MotionEvent.ACTION_MOVE:
+                    if (null != mSelectListener) {
+                        mSelectListener.onDismissPop();
+                    }
+                    int rawX = (int) event.getRawX();
+                    int rawY = (int) event.getRawY();
+
+                    update(rawX + mAdjustX - mWidth - mTextViewMarginStart, rawY + mAdjustY - mHeight - (int) textView.getTextSize());
+                    break;
+                default:
+                    break;
+            }
+            return true;
+        }
+
+        private void changeDirection() {
+            isLeft = !isLeft;
+            invalidate();
+        }
+
+        public void dismiss() {
+            Log.e(TAG, "handler dismiss");
+            mPopupWindow.dismiss();
+        }
+
+        private int[] mTempCoors = new int[2];
+
+        public void update(int x, int y) {
+            textView.getLocationInWindow(mTempCoors);
+            int oldOffset;
+            if (isLeft) {
+                oldOffset = mSelectionInfo.start;
+            } else {
+                oldOffset = mSelectionInfo.end;
+            }
+
+            y -= mTempCoors[1];
+
+            int offset = getHysteresisOffset(textView, x, y, oldOffset);
+
+            if (offset != oldOffset) {
+                mSelectionInfo.selectionContent = null;
+                if (isLeft) {
+                    if (offset > mBeforeDragEnd) {
+                        SelectionHandle handle = getSelectionHandle(false);
+                        changeDirection();
+                        handle.changeDirection();
+                        mBeforeDragStart = mBeforeDragEnd;
+                        setSelection(mBeforeDragEnd, offset);
+                        handle.updateSelectionHandle();
+                    } else {
+                        setSelection(offset, -1);
+                    }
+                    updateSelectionHandle();
+                } else {
+                    if (offset < mBeforeDragStart) {
+                        SelectionHandle handle = getSelectionHandle(true);
+                        handle.changeDirection();
+                        changeDirection();
+                        mBeforeDragEnd = mBeforeDragStart;
+                        setSelection(offset, mBeforeDragStart);
+                        handle.updateSelectionHandle();
+                    } else {
+                        setSelection(mBeforeDragStart, offset);
+                    }
+                    updateSelectionHandle();
+                }
+            }
+        }
+
+        private void updateSelectionHandle() {
+            textView.getLocationInWindow(mTempCoors);
+            Layout layout = textView.getLayout();
+            if (isLeft) {
+                mPopupWindow.update((int) layout.getPrimaryHorizontal(mSelectionInfo.start) - mWidth + getExtraX(),
+                    layout.getLineBottom(layout.getLineForOffset(mSelectionInfo.start)) + getExtraY(), -1, -1);
+            } else {
+                mPopupWindow.update((int) layout.getPrimaryHorizontal(mSelectionInfo.end) + getExtraX(),
+                    layout.getLineBottom(layout.getLineForOffset(mSelectionInfo.end)) + getExtraY(), -1, -1);
+            }
+        }
+
+        public void show(int x, int y) {
+            textView.getLocationInWindow(mTempCoors);
+            int offset = isLeft ? mWidth : 0;
+            mPopupWindow.showAtLocation(textView, Gravity.NO_GRAVITY, x - offset + getExtraX(), y + getExtraY());
+        }
+
+        public int getExtraX() {
+            return mTempCoors[0] - mPadding + textView.getPaddingLeft();
+        }
+
+        public int getExtraY() {
+            return mTempCoors[1] + textView.getPaddingTop();
+        }
+    }
+
+    private SelectionHandle getSelectionHandle(boolean isLeft) {
+        if (startHandle.isLeft == isLeft) {
+            return startHandle;
+        } else {
+            return endHandle;
+        }
+    }
+
+    private static class SelectionInfo {
+        public int start;
+        public int end;
+        public String selectionContent;
+    }
+
+    private class LinkMovementMethodInterceptor extends LinkMovementMethod {
+        @Override
+        public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
+            return gestureDetector.onTouchEvent(event);
+        }
+    }
+
+    public static int getHysteresisOffset(TextView textView, int x, int y, int previousOffset) {
+        final Layout layout = textView.getLayout();
+        if (layout == null) {
+            return -1;
+        }
+
+        int line = layout.getLineForVertical(y);
+
+        // The "HACK BLOCK"S in this function is required because of how Android Layout for
+        // TextView works - if 'offset' equals to the last character of a line, then
+        //
+        // * getLineForOffset(offset) will result the NEXT line
+        // * getPrimaryHorizontal(offset) will return 0 because the next insertion point is on the next line
+        // * getOffsetForHorizontal(line, x) will not return the last offset of a line no matter where x is
+        // These are highly undesired and is worked around with the HACK BLOCK
+        //
+        // @see Moon+ Reader/Color Note - see how it can't select the last character of a line unless you move
+        // the cursor to the beginning of the next line.
+        //
+        ////////////////////HACK BLOCK////////////////////////////////////////////////////
+
+        if (isEndOfLineOffset(layout, previousOffset)) {
+            // we have to minus one from the offset so that the code below to find
+            // the previous line can work correctly.
+            int left = (int) layout.getPrimaryHorizontal(previousOffset - 1);
+            int right = (int) layout.getLineRight(line);
+            int threshold = (right - left) / 2; // half the width of the last character
+            if (x > right - threshold) {
+                previousOffset -= 1;
+            }
+        }
+        ///////////////////////////////////////////////////////////////////////////////////
+
+        final int previousLine = layout.getLineForOffset(previousOffset);
+        final int previousLineTop = layout.getLineTop(previousLine);
+        final int previousLineBottom = layout.getLineBottom(previousLine);
+        final int hysteresisThreshold = (previousLineBottom - previousLineTop) / 2;
+
+        // If new line is just before or after previous line and y position is less than
+        // hysteresisThreshold away from previous line, keep cursor on previous line.
+        if (((line == previousLine + 1) && ((y - previousLineBottom) < hysteresisThreshold))
+            || ((line == previousLine - 1) && ((previousLineTop - y) < hysteresisThreshold))) {
+            line = previousLine;
+        }
+
+        int offset = layout.getOffsetForHorizontal(line, x);
+
+        // This allow the user to select the last character of a line without moving the
+        // cursor to the next line. (As Layout.getOffsetForHorizontal does not return the
+        // offset of the last character of the specified line)
+        //
+        // But this function will probably get called again immediately, must decrement the offset
+        // by 1 to compensate for the change made below. (see previous HACK BLOCK)
+        /////////////////////HACK BLOCK///////////////////////////////////////////////////
+        if (offset < textView.getText().length() - 1) {
+            if (isEndOfLineOffset(layout, offset + 1)) {
+                int left = (int) layout.getPrimaryHorizontal(offset);
+                int right = (int) layout.getLineRight(line);
+                int threshold = (right - left) / 2; // half the width of the last character
+                if (x > right - threshold) {
+                    offset += 1;
+                }
+            }
+        }
+        //////////////////////////////////////////////////////////////////////////////////
+
+        return offset;
+    }
+
+    private static boolean isEndOfLineOffset(Layout layout, int offset) {
+        return offset > 0 && layout.getLineForOffset(offset) == layout.getLineForOffset(offset - 1) + 1;
+    }
+}

+ 31 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/classicui/widget/message/TUIReplyQuoteView.java

@@ -0,0 +1,31 @@
+package com.tencent.qcloud.tuikit.timcommon.classicui.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<T extends TUIReplyQuoteBean<?>> 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(T quoteBean);
+
+    /**
+     *
+     * Whether the original message sender is himself, used for different UI displays
+     * 
+     * @param isSelf
+     */
+    public void setSelf(boolean isSelf) {}
+
+}

+ 93 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/BottomSelectSheet.java

@@ -0,0 +1,93 @@
+package com.tencent.qcloud.tuikit.timcommon.component;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.view.Display;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.tencent.qcloud.tuikit.timcommon.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class BottomSelectSheet {
+    private List<String> selectList = new ArrayList<>();
+    private Dialog dialog;
+    private ArrayAdapter<String> listAdapter;
+    private BottomSelectSheetOnClickListener onClickListener;
+
+    public BottomSelectSheet(Context context) {
+        View view = View.inflate(context, R.layout.common_bottom_select_sheet, null);
+        dialog = new Dialog(context, R.style.BottomSelectSheet);
+        dialog.setContentView(view);
+        dialog.setCancelable(true);
+        dialog.setCanceledOnTouchOutside(true);
+        Window window = dialog.getWindow();
+        window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
+        WindowManager m = window.getWindowManager();
+        Display d = m.getDefaultDisplay();
+        WindowManager.LayoutParams p = window.getAttributes();
+        p.width = d.getWidth();
+        p.height = ViewGroup.LayoutParams.WRAP_CONTENT;
+        window.setAttributes(p);
+        window.setGravity(Gravity.BOTTOM);
+        window.setWindowAnimations(R.style.BottomSelectSheet_Anim); 
+
+        final ListView listView = view.findViewById(R.id.item_list);
+        listAdapter = new ArrayAdapter<>(context, R.layout.common_bottom_sheet_item, R.id.sheet_item, selectList);
+        listView.setAdapter(listAdapter);
+        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+            @Override
+            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+                dismiss();
+                if (onClickListener != null) {
+                    onClickListener.onSheetClick(position);
+                }
+            }
+        });
+
+        TextView cancelButton = view.findViewById(R.id.cancel_button);
+        cancelButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                dismiss();
+            }
+        });
+    }
+
+    public void dismiss() {
+        if (dialog != null && dialog.isShowing()) {
+            dialog.dismiss();
+        }
+    }
+
+    public void show() {
+        if (dialog != null && !dialog.isShowing()) {
+            listAdapter.notifyDataSetChanged();
+            dialog.show();
+        }
+    }
+
+    public void setSelectList(List<String> selectList) {
+        this.selectList.clear();
+        this.selectList.addAll(selectList);
+    }
+
+    public void setOnClickListener(BottomSelectSheetOnClickListener onClickListener) {
+        this.onClickListener = onClickListener;
+    }
+
+    public interface BottomSelectSheetOnClickListener {
+        void onSheetClick(int index);
+    }
+}

+ 34 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/CustomLinearLayoutManager.java

@@ -0,0 +1,34 @@
+package com.tencent.qcloud.tuikit.timcommon.component;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * https://stackoverflow.com/questions/30458640/recyclerview-java-lang-indexoutofboundsexception-inconsistency-detected-inval
+ */
+public class CustomLinearLayoutManager extends LinearLayoutManager {
+    public CustomLinearLayoutManager(Context context) {
+        super(context);
+    }
+
+    public CustomLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
+        super(context, orientation, reverseLayout);
+    }
+
+    public CustomLinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+        try {
+            super.onLayoutChildren(recycler, state);
+        } catch (Throwable e) {
+            Log.w("CustomLinearLayoutManager", "" + e.getLocalizedMessage());
+        }
+    }
+}

+ 44 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/GifSpan.java

@@ -0,0 +1,44 @@
+package com.tencent.qcloud.tuikit.timcommon.component;
+
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.Animatable;
+import android.graphics.drawable.Drawable;
+import android.text.style.DynamicDrawableSpan;
+import androidx.annotation.NonNull;
+
+public class GifSpan extends DynamicDrawableSpan {
+    private static final String TAG = "EmojiGifSpan";
+    private Drawable mGifDrawable;
+
+    public GifSpan(Drawable drawable) {
+        mGifDrawable = drawable;
+        if (mGifDrawable instanceof Animatable) {
+            ((Animatable) mGifDrawable).start();
+        }
+    }
+
+    @Override
+    public Drawable getDrawable() {
+        return mGifDrawable;
+    }
+
+    @Override
+    public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
+        Drawable drawable = getDrawable();
+        Rect rect = drawable.getBounds();
+        
+        if (fm != null) {
+            Paint.FontMetricsInt paintFm = paint.getFontMetricsInt();
+            int drawableHeight = rect.height();
+            
+            int center = (paintFm.ascent + paintFm.descent) / 2;
+            fm.ascent = center - drawableHeight / 2;
+            fm.descent = center + drawableHeight / 2;
+            fm.top = fm.ascent;
+            fm.bottom = fm.descent;
+        }
+        
+        return rect.right;
+    }
+}

+ 126 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/IndicatorView.java

@@ -0,0 +1,126 @@
+package com.tencent.qcloud.tuikit.timcommon.component;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+
+import com.tencent.qcloud.tuikit.timcommon.R;
+import com.tencent.qcloud.tuikit.timcommon.util.ScreenUtil;
+
+import java.util.ArrayList;
+
+public class IndicatorView extends LinearLayout {
+    private Context mContext;
+    private ArrayList<ImageView> mImageViews;
+    private Bitmap bmpSelect;
+    private Bitmap bmpNormal;
+    private int mHeight = 6;
+    private int mMaxHeight;
+    private AnimatorSet mPlayByInAnimatorSet;
+    private AnimatorSet mPlayByOutAnimatorSet;
+
+    public IndicatorView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        this.mContext = context;
+        this.setOrientation(HORIZONTAL);
+        mMaxHeight = ScreenUtil.dip2px(mHeight);
+        bmpSelect = BitmapFactory.decodeResource(getResources(), R.drawable.indicator_point_select);
+        bmpNormal = BitmapFactory.decodeResource(getResources(), R.drawable.indicator_point_nomal);
+    }
+
+    public IndicatorView(Context context) {
+        this(context, null);
+    }
+
+    public void init(int count) {
+        mImageViews = new ArrayList<>();
+        this.removeAllViews();
+        for (int i = 0; i < count; i++) {
+            LayoutParams params = new LinearLayout.LayoutParams(mMaxHeight, mMaxHeight);
+            ImageView imageView = new ImageView(mContext);
+            params.setMarginStart(12);
+            params.setMarginEnd(12);
+            if (i == 0) {
+                imageView.setImageBitmap(bmpSelect);
+            } else {
+                imageView.setImageBitmap(bmpNormal);
+            }
+            this.addView(imageView, params);
+            mImageViews.add(imageView);
+        }
+    }
+
+    public void playBy(int startPosition, int nextPosition) {
+        final boolean isShowInAnimOnly = false;
+        if (startPosition < 0 || nextPosition < 0 || nextPosition == startPosition) {
+            startPosition = nextPosition = 0;
+        }
+        if (mImageViews == null || mImageViews.isEmpty()) {
+            return;
+        }
+        if (startPosition >= mImageViews.size() || nextPosition >= mImageViews.size()) {
+            return;
+        }
+
+        final ImageView imageViewStrat = mImageViews.get(startPosition);
+        final ImageView imageViewNext = mImageViews.get(nextPosition);
+
+        ObjectAnimator anim1 = ObjectAnimator.ofFloat(imageViewStrat, "scaleX", 1.0f, 0.25f);
+        ObjectAnimator anim2 = ObjectAnimator.ofFloat(imageViewStrat, "scaleY", 1.0f, 0.25f);
+
+        if (mPlayByOutAnimatorSet != null && mPlayByOutAnimatorSet.isRunning()) {
+            mPlayByOutAnimatorSet.cancel();
+            mPlayByOutAnimatorSet = null;
+        }
+        mPlayByOutAnimatorSet = new AnimatorSet();
+        mPlayByOutAnimatorSet.play(anim1).with(anim2);
+        mPlayByOutAnimatorSet.setDuration(100);
+
+        ObjectAnimator animIn1 = ObjectAnimator.ofFloat(imageViewNext, "scaleX", 0.25f, 1.0f);
+        ObjectAnimator animIn2 = ObjectAnimator.ofFloat(imageViewNext, "scaleY", 0.25f, 1.0f);
+
+        if (mPlayByInAnimatorSet != null && mPlayByInAnimatorSet.isRunning()) {
+            mPlayByInAnimatorSet.cancel();
+            mPlayByInAnimatorSet = null;
+        }
+        mPlayByInAnimatorSet = new AnimatorSet();
+        mPlayByInAnimatorSet.play(animIn1).with(animIn2);
+        mPlayByInAnimatorSet.setDuration(100);
+
+        if (isShowInAnimOnly) {
+            mPlayByInAnimatorSet.start();
+            return;
+        }
+
+        anim1.addListener(new Animator.AnimatorListener() {
+            @Override
+            public void onAnimationStart(Animator animation) {}
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                imageViewStrat.setImageBitmap(bmpNormal);
+                ObjectAnimator animFil1l = ObjectAnimator.ofFloat(imageViewStrat, "scaleX", 1.0f);
+                ObjectAnimator animFill2 = ObjectAnimator.ofFloat(imageViewStrat, "scaleY", 1.0f);
+                AnimatorSet mFillAnimatorSet = new AnimatorSet();
+                mFillAnimatorSet.play(animFil1l).with(animFill2);
+                mFillAnimatorSet.start();
+                imageViewNext.setImageBitmap(bmpSelect);
+                mPlayByInAnimatorSet.start();
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {}
+
+            @Override
+            public void onAnimationRepeat(Animator animation) {}
+        });
+        mPlayByOutAnimatorSet.start();
+    }
+}

+ 144 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/LineControllerView.java

@@ -0,0 +1,144 @@
+package com.tencent.qcloud.tuikit.timcommon.component;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.Switch;
+import android.widget.TextView;
+
+import androidx.core.graphics.drawable.DrawableCompat;
+
+import com.tencent.qcloud.tuikit.timcommon.R;
+
+/**
+ * Custom LineControllerView
+ */
+public class LineControllerView extends RelativeLayout {
+    private String mName;
+    private boolean mIsBottom;
+    private boolean mIsTop;
+    private String mContent;
+    private boolean mIsJump;
+    private boolean mIsSwitch;
+
+    protected TextView mNameText;
+    protected TextView mContentText;
+    private ImageView mNavArrowView;
+    protected Switch mSwitchView;
+    protected View bottomLine;
+    private View mMask;
+    private View container;
+
+    public LineControllerView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        LayoutInflater.from(context).inflate(R.layout.timcommon_line_controller_view, this);
+        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.LineControllerView, 0, 0);
+        try {
+            mName = ta.getString(R.styleable.LineControllerView_name);
+            mContent = ta.getString(R.styleable.LineControllerView_subject);
+            mIsBottom = ta.getBoolean(R.styleable.LineControllerView_isBottom, false);
+            mIsTop = ta.getBoolean(R.styleable.LineControllerView_isTop, false);
+            mIsJump = ta.getBoolean(R.styleable.LineControllerView_canNav, false);
+            mIsSwitch = ta.getBoolean(R.styleable.LineControllerView_isSwitch, false);
+            setUpView();
+        } finally {
+            ta.recycle();
+        }
+    }
+
+    private void setUpView() {
+        mNameText = findViewById(R.id.name);
+        mNameText.setText(mName);
+        mContentText = findViewById(R.id.content);
+        mContentText.setText(mContent);
+        bottomLine = findViewById(R.id.bottom_line);
+        View topLine = findViewById(R.id.top_line);
+        bottomLine.setVisibility(mIsBottom ? VISIBLE : GONE);
+        topLine.setVisibility(mIsTop ? VISIBLE : GONE);
+        mNavArrowView = findViewById(R.id.rightArrow);
+        Drawable arrowDrawable = mNavArrowView.getDrawable();
+        if (arrowDrawable != null) {
+            DrawableCompat.setAutoMirrored(arrowDrawable, true);
+        }
+        mNavArrowView.setVisibility(mIsJump ? VISIBLE : GONE);
+        ViewGroup contentLayout = findViewById(R.id.content_view);
+        contentLayout.setVisibility(mIsSwitch ? GONE : VISIBLE);
+        mSwitchView = findViewById(R.id.btnSwitch);
+        mSwitchView.setVisibility(mIsSwitch ? VISIBLE : GONE);
+        mMask = findViewById(R.id.disable_mask);
+        container = findViewById(R.id.view_container);
+    }
+
+    public void setBackground(Drawable drawable) {
+        super.setBackground(drawable);
+        container.setBackground(drawable);
+    }
+
+    public String getContent() {
+        return mContentText.getText().toString();
+    }
+
+    public void setContent(String content) {
+        this.mContent = content;
+        mContentText.setText(content);
+        mContentText.requestLayout();
+    }
+
+    public void setName(String name) {
+        mNameText.setText(name);
+    }
+
+    public void setSingleLine(boolean singleLine) {
+        mContentText.setSingleLine(singleLine);
+    }
+
+    /**
+     * Set whether to jump
+     *
+     * @param canNav
+     */
+    public void setCanNav(boolean canNav) {
+        this.mIsJump = canNav;
+        mNavArrowView.setVisibility(canNav ? VISIBLE : GONE);
+        if (canNav) {
+            mContentText.setTextIsSelectable(false);
+        } else {
+            mContentText.setTextIsSelectable(true);
+        }
+    }
+
+    public boolean isChecked() {
+        return mSwitchView.isChecked();
+    }
+
+    public void setChecked(boolean on) {
+        mSwitchView.setChecked(on);
+    }
+
+    public void setCheckListener(CompoundButton.OnCheckedChangeListener listener) {
+        mSwitchView.setOnCheckedChangeListener(listener);
+    }
+
+    public void setMask(boolean enableMask) {
+        if (enableMask) {
+            mNameText.setEnabled(false);
+            mContentText.setEnabled(false);
+            mNameText.setTextColor(getResources().getColor(R.color.text_color_gray));
+            mContentText.setTextColor(getResources().getColor(R.color.text_color_gray));
+            mSwitchView.setEnabled(false);
+        } else {
+            mNameText.setEnabled(true);
+            mContentText.setEnabled(true);
+            mNameText.setTextColor(getResources().getColor(R.color.core_line_controller_title_color));
+            mContentText.setTextColor(getResources().getColor(R.color.core_line_controller_content_color));
+            mSwitchView.setEnabled(true);
+        }
+    }
+}

+ 44 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/MaxWidthFrameLayout.java

@@ -0,0 +1,44 @@
+package com.tencent.qcloud.tuikit.timcommon.component;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.tencent.qcloud.tuikit.timcommon.R;
+
+public class MaxWidthFrameLayout extends FrameLayout {
+    int maxWidthPx;
+
+    public MaxWidthFrameLayout(@NonNull Context context) {
+        super(context);
+    }
+
+    public MaxWidthFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+        init(context, attrs);
+    }
+
+    public MaxWidthFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init(context, attrs);
+    }
+
+    private void init(Context context, AttributeSet attributeSet) {
+        TypedArray array = context.obtainStyledAttributes(attributeSet, R.styleable.max_width_style);
+        maxWidthPx = array.getDimensionPixelSize(R.styleable.max_width_style_maxWidth, 0);
+        array.recycle();
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
+        if (maxWidthPx > 0 && maxWidthPx < measuredWidth) {
+            widthMeasureSpec = MeasureSpec.makeMeasureSpec(maxWidthPx, MeasureSpec.AT_MOST);
+        }
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+    }
+}

+ 44 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/MaxWidthLinearLayout.java

@@ -0,0 +1,44 @@
+package com.tencent.qcloud.tuikit.timcommon.component;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.widget.LinearLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.tencent.qcloud.tuikit.timcommon.R;
+
+public class MaxWidthLinearLayout extends LinearLayout {
+    int maxWidthPx;
+
+    public MaxWidthLinearLayout(@NonNull Context context) {
+        super(context);
+    }
+
+    public MaxWidthLinearLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+        init(context, attrs);
+    }
+
+    public MaxWidthLinearLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init(context, attrs);
+    }
+
+    private void init(Context context, AttributeSet attributeSet) {
+        TypedArray array = context.obtainStyledAttributes(attributeSet, R.styleable.max_width_style);
+        maxWidthPx = array.getDimensionPixelSize(R.styleable.max_width_style_maxWidth, 0);
+        array.recycle();
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
+        if (maxWidthPx > 0 && maxWidthPx < measuredWidth) {
+            widthMeasureSpec = MeasureSpec.makeMeasureSpec(maxWidthPx, MeasureSpec.AT_MOST);
+        }
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+    }
+}

+ 159 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/MinimalistLineControllerView.java

@@ -0,0 +1,159 @@
+package com.tencent.qcloud.tuikit.timcommon.component;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import androidx.appcompat.widget.SwitchCompat;
+import androidx.core.graphics.drawable.DrawableCompat;
+
+import com.tencent.qcloud.tuikit.timcommon.R;
+
+/**
+ * Custom LineControllerView
+ */
+public class MinimalistLineControllerView extends RelativeLayout {
+    private String mName;
+    private boolean mIsBottom;
+    private boolean mIsTop;
+    private String mContent;
+    private boolean mIsJump;
+    private boolean mIsSwitch;
+
+    protected TextView mNameText;
+    protected TextView mContentText;
+    private ImageView mNavArrowView;
+    protected SwitchCompat mSwitchView;
+    protected View bottomLine;
+    private View mMask;
+    private View container;
+
+    public MinimalistLineControllerView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        LayoutInflater.from(context).inflate(R.layout.minimalist_line_controller_view, this);
+        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.LineControllerView, 0, 0);
+        try {
+            mName = ta.getString(R.styleable.LineControllerView_name);
+            mContent = ta.getString(R.styleable.LineControllerView_subject);
+            mIsBottom = ta.getBoolean(R.styleable.LineControllerView_isBottom, false);
+            mIsTop = ta.getBoolean(R.styleable.LineControllerView_isTop, false);
+            mIsJump = ta.getBoolean(R.styleable.LineControllerView_canNav, false);
+            mIsSwitch = ta.getBoolean(R.styleable.LineControllerView_isSwitch, false);
+            setUpView();
+        } finally {
+            ta.recycle();
+        }
+    }
+
+    private void setUpView() {
+        mNameText = findViewById(R.id.name);
+        mNameText.setText(mName);
+        mContentText = findViewById(R.id.content);
+        mContentText.setText(mContent);
+        mContentText.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                MinimalistLineControllerView.this.performClick();
+            }
+        });
+        bottomLine = findViewById(R.id.bottom_line);
+        View topLine = findViewById(R.id.top_line);
+        bottomLine.setVisibility(mIsBottom ? VISIBLE : GONE);
+        topLine.setVisibility(mIsTop ? VISIBLE : GONE);
+        mNavArrowView = findViewById(R.id.rightArrow);
+        Drawable arrowDrawable = mNavArrowView.getDrawable();
+        if (arrowDrawable != null) {
+            DrawableCompat.setAutoMirrored(arrowDrawable, true);
+        }
+        mNavArrowView.setVisibility(mIsJump ? VISIBLE : GONE);
+        ViewGroup contentLayout = findViewById(R.id.content_view);
+        contentLayout.setVisibility(mIsSwitch ? GONE : VISIBLE);
+        mSwitchView = findViewById(R.id.btnSwitch);
+        mSwitchView.setVisibility(mIsSwitch ? VISIBLE : GONE);
+        mMask = findViewById(R.id.disable_mask);
+        container = findViewById(R.id.view_container);
+    }
+
+    public void setBackground(Drawable drawable) {
+        super.setBackground(drawable);
+        if (container != null) {
+            container.setBackground(drawable);
+        }
+    }
+
+    public void setBackgroundColor(int color) {
+        super.setBackgroundColor(color);
+        if (container != null) {
+            container.setBackgroundColor(color);
+        }
+    }
+
+    public String getContent() {
+        return mContentText.getText().toString();
+    }
+
+    public void setContent(String content) {
+        this.mContent = content;
+        mContentText.setText(content);
+        mContentText.requestLayout();
+    }
+
+    public void setSingleLine(boolean singleLine) {
+        mContentText.setSingleLine(singleLine);
+    }
+
+    /**
+     * Set whether to jump
+     *
+     * @param canNav
+     */
+    public void setCanNav(boolean canNav) {
+        this.mIsJump = canNav;
+        mNavArrowView.setVisibility(canNav ? VISIBLE : GONE);
+    }
+
+    public boolean isChecked() {
+        return mSwitchView.isChecked();
+    }
+
+    public void setChecked(boolean on) {
+        mSwitchView.setChecked(on);
+    }
+
+    public void setCheckListener(CompoundButton.OnCheckedChangeListener listener) {
+        mSwitchView.setOnCheckedChangeListener(listener);
+    }
+
+    public void setMask(boolean enableMask) {
+        if (enableMask) {
+            mNameText.setEnabled(false);
+            mNameText.setTextColor(getResources().getColor(R.color.text_color_gray));
+            mContentText.setEnabled(false);
+            mContentText.setTextColor(getResources().getColor(R.color.text_color_gray));
+            mSwitchView.setEnabled(false);
+        } else {
+            mNameText.setEnabled(true);
+            mNameText.setTextColor(getResources().getColor(R.color.core_line_controller_title_color));
+            mContentText.setEnabled(true);
+            mContentText.setTextColor(getResources().getColor(R.color.core_line_controller_content_color));
+            mSwitchView.setEnabled(true);
+        }
+    }
+
+    public void setNameColor(int color) {
+        mNameText.setTextColor(color);
+    }
+
+    public void setName(String name) {
+        this.mName = name;
+        mNameText.setText(name);
+    }
+}

+ 39 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/MinimalistTitleBar.java

@@ -0,0 +1,39 @@
+package com.tencent.qcloud.tuikit.timcommon.component;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+
+import androidx.annotation.Nullable;
+
+import com.tencent.qcloud.tuikit.timcommon.R;
+
+public class MinimalistTitleBar extends TitleBarLayout {
+    public MinimalistTitleBar(Context context) {
+        super(context);
+        initView(context);
+    }
+
+    public MinimalistTitleBar(Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+        initView(context);
+    }
+
+    public MinimalistTitleBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        initView(context);
+    }
+
+    private void initView(Context context) {
+        setLeftReturnListener(context);
+        setBackgroundColor(Color.WHITE);
+        getLeftIcon().setBackgroundResource(R.drawable.core_minimalist_back_icon);
+        Drawable leftIconDrawable = getLeftIcon().getBackground();
+        if (leftIconDrawable != null) {
+            leftIconDrawable.setAutoMirrored(true);
+        }
+        getLeftTitle().setTextColor(0xFF0365F9);
+        getRightTitle().setTextColor(0xFF0365F9);
+    }
+}

+ 278 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/PopupInputCard.java

@@ -0,0 +1,278 @@
+package com.tencent.qcloud.tuikit.timcommon.component;
+
+import android.animation.ValueAnimator;
+import android.app.Activity;
+import android.graphics.drawable.ColorDrawable;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.animation.LinearInterpolator;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.PopupWindow;
+import android.widget.TextView;
+import com.tencent.qcloud.tuicore.util.ToastUtil;
+import com.tencent.qcloud.tuikit.timcommon.R;
+import com.tencent.qcloud.tuikit.timcommon.util.SoftKeyBoardUtil;
+import java.util.regex.Pattern;
+
+public class PopupInputCard {
+    private PopupWindow popupWindow;
+
+    private TextView titleTv;
+    private EditText editText;
+    private TextView descriptionTv;
+    private Button positiveBtn;
+    private View closeBtn;
+    private OnClickListener positiveOnClickListener;
+    private OnTextExceedListener textExceedListener;
+
+    private int minLimit = 0;
+    private int maxLimit = Integer.MAX_VALUE;
+    private String rule;
+    private String notMachRuleTip;
+    private ByteLengthFilter lengthFilter = new ByteLengthFilter();
+
+    public PopupInputCard(Activity activity) {
+        View popupView = LayoutInflater.from(activity).inflate(R.layout.timcommon_layout_popup_card, null);
+        titleTv = popupView.findViewById(R.id.popup_card_title);
+        editText = popupView.findViewById(R.id.popup_card_edit);
+        descriptionTv = popupView.findViewById(R.id.popup_card_description);
+        positiveBtn = popupView.findViewById(R.id.popup_card_positive_btn);
+        closeBtn = popupView.findViewById(R.id.close_btn);
+
+        popupWindow = new PopupWindow(popupView, WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT, true) {
+            @Override
+            public void showAtLocation(View anchor, int gravity, int x, int y) {
+                if (activity != null && !activity.isFinishing()) {
+                    Window dialogWindow = activity.getWindow();
+                    startAnimation(dialogWindow, true);
+                }
+                editText.requestFocus();
+                if (activity.getWindow() != null) {
+                    SoftKeyBoardUtil.showKeyBoard(activity.getWindow());
+                }
+                super.showAtLocation(anchor, gravity, x, y);
+            }
+
+            @Override
+            public void dismiss() {
+                if (activity != null && !activity.isFinishing()) {
+                    Window dialogWindow = activity.getWindow();
+                    startAnimation(dialogWindow, false);
+                }
+
+                super.dismiss();
+            }
+        };
+        popupWindow.setBackgroundDrawable(new ColorDrawable());
+        popupWindow.setTouchable(true);
+        popupWindow.setOutsideTouchable(false);
+        popupWindow.setAnimationStyle(R.style.PopupInputCardAnim);
+        popupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
+        popupWindow.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
+        popupWindow.setOnDismissListener(new PopupWindow.OnDismissListener() {
+            @Override
+            public void onDismiss() {
+                if (activity.getWindow() != null) {
+                    SoftKeyBoardUtil.hideKeyBoard(activity.getWindow());
+                }
+            }
+        });
+
+        positiveBtn.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                String result = editText.getText().toString();
+
+                if (result.length() < minLimit || result.length() > maxLimit) {
+                    ToastUtil.toastShortMessage(notMachRuleTip);
+                    return;
+                }
+
+                if (!TextUtils.isEmpty(rule) && !Pattern.matches(rule, result)) {
+                    ToastUtil.toastShortMessage(notMachRuleTip);
+                    return;
+                }
+
+                if (positiveOnClickListener != null) {
+                    positiveOnClickListener.onClick(editText.getText().toString());
+                }
+                popupWindow.dismiss();
+            }
+        });
+
+        closeBtn.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                popupWindow.dismiss();
+            }
+        });
+        editText.setFilters(new InputFilter[] {lengthFilter});
+        editText.addTextChangedListener(new TextWatcher() {
+            @Override
+            public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+            @Override
+            public void onTextChanged(CharSequence s, int start, int before, int count) {}
+
+            @Override
+            public void afterTextChanged(Editable s) {
+                if (!TextUtils.isEmpty(rule)) {
+                    if (!Pattern.matches(rule, s.toString())) {
+                        positiveBtn.setEnabled(false);
+                    } else {
+                        positiveBtn.setEnabled(true);
+                    }
+                }
+            }
+        });
+    }
+
+    private void startAnimation(Window window, boolean isShow) {
+        ValueAnimator animator;
+        if (isShow) {
+            animator = ValueAnimator.ofFloat(1.0f, 0.5f);
+        } else {
+            animator = ValueAnimator.ofFloat(0.5f, 1.0f);
+        }
+        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                WindowManager.LayoutParams lp = window.getAttributes();
+                lp.alpha = (float) animation.getAnimatedValue();
+                window.setAttributes(lp);
+            }
+        });
+        LinearInterpolator interpolator = new LinearInterpolator();
+        animator.setDuration(200);
+        animator.setInterpolator(interpolator);
+        animator.start();
+    }
+
+    public void show(View rootView, int gravity) {
+        if (popupWindow != null) {
+            popupWindow.showAtLocation(rootView, gravity, 0, 0);
+        }
+    }
+
+    public void setTitle(String title) {
+        titleTv.setText(title);
+    }
+
+    public void setDescription(String description) {
+        if (!TextUtils.isEmpty(description)) {
+            descriptionTv.setVisibility(View.VISIBLE);
+            descriptionTv.setText(description);
+        }
+    }
+
+    public void setContent(String content) {
+        editText.setText(content);
+    }
+
+    public void setOnPositive(OnClickListener clickListener) {
+        positiveOnClickListener = clickListener;
+    }
+
+    public void setTextExceedListener(OnTextExceedListener textExceedListener) {
+        this.textExceedListener = textExceedListener;
+    }
+
+    public void setSingleLine(boolean isSingleLine) {
+        editText.setSingleLine(isSingleLine);
+    }
+
+    public void setMaxLimit(int maxLimit) {
+        this.maxLimit = maxLimit;
+        lengthFilter.setLength(maxLimit);
+    }
+
+    public void setMinLimit(int minLimit) {
+        this.minLimit = minLimit;
+    }
+
+    public void setRule(String rule) {
+        if (TextUtils.isEmpty(rule)) {
+            this.rule = "";
+        } else {
+            this.rule = rule;
+        }
+    }
+
+    public void setNotMachRuleTip(String notMachRuleTip) {
+        this.notMachRuleTip = notMachRuleTip;
+    }
+
+    class ByteLengthFilter implements InputFilter {
+        private int length = Integer.MAX_VALUE;
+
+        public ByteLengthFilter() {}
+
+        public void setLength(int length) {
+            this.length = length;
+        }
+
+        @Override
+        public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
+            int destLength = 0;
+            int destReplaceLength = 0;
+            int sourceLength = 0;
+            if (!TextUtils.isEmpty(dest)) {
+                destLength = dest.toString().getBytes().length;
+                destReplaceLength = dest.subSequence(dstart, dend).toString().getBytes().length;
+            }
+            if (!TextUtils.isEmpty(source)) {
+                sourceLength = source.subSequence(start, end).toString().getBytes().length;
+            }
+            int keepBytesLength = length - (destLength - destReplaceLength);
+            if (keepBytesLength <= 0) {
+                if (textExceedListener != null) {
+                    textExceedListener.onTextExceedMax();
+                }
+                return "";
+            } else if (keepBytesLength >= sourceLength) {
+                return null;
+            } else {
+                if (textExceedListener != null) {
+                    textExceedListener.onTextExceedMax();
+                }
+                return getSource(source, start, keepBytesLength);
+            }
+        }
+
+        private CharSequence getSource(CharSequence sequence, int start, int keepLength) {
+            int sequenceLength = sequence.length();
+            int end = 0;
+            for (int i = 1; i <= sequenceLength; i++) {
+                if (sequence.subSequence(0, i).toString().getBytes().length <= keepLength) {
+                    end = i;
+                } else {
+                    break;
+                }
+            }
+            if (end > 0 && Character.isHighSurrogate(sequence.charAt(end - 1))) {
+                --end;
+                if (end == start) {
+                    return "";
+                }
+            }
+            return sequence.subSequence(start, end);
+        }
+    }
+
+    @FunctionalInterface
+    public interface OnClickListener {
+        void onClick(String result);
+    }
+
+    public interface OnTextExceedListener {
+        void onTextExceedMax();
+    }
+}

+ 126 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/RoundCornerImageView.java

@@ -0,0 +1,126 @@
+package com.tencent.qcloud.tuikit.timcommon.component;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PaintFlagsDrawFilter;
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatImageView;
+
+import com.tencent.qcloud.tuikit.timcommon.R;
+
+public class RoundCornerImageView extends AppCompatImageView {
+    private final Path path = new Path();
+    private final RectF rectF = new RectF();
+    private final PaintFlagsDrawFilter aliasFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
+    private int radius;
+    private int leftTopRadius;
+    private int rightTopRadius;
+    private int rightBottomRadius;
+    private int leftBottomRadius;
+
+    public RoundCornerImageView(@NonNull Context context) {
+        super(context);
+        init(context, null);
+    }
+
+    public RoundCornerImageView(@NonNull Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+        init(context, attrs);
+    }
+
+    public RoundCornerImageView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    private void init(Context context, AttributeSet attrs) {
+        setLayerType(View.LAYER_TYPE_HARDWARE, null);
+        int defaultRadius = 0;
+        if (attrs != null) {
+            TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.RoundCornerImageView);
+            radius = array.getDimensionPixelOffset(R.styleable.RoundCornerImageView_corner_radius, defaultRadius);
+            leftTopRadius = array.getDimensionPixelOffset(R.styleable.RoundCornerImageView_left_top_corner_radius, defaultRadius);
+            rightTopRadius = array.getDimensionPixelOffset(R.styleable.RoundCornerImageView_right_top_corner_radius, defaultRadius);
+            rightBottomRadius = array.getDimensionPixelOffset(R.styleable.RoundCornerImageView_right_bottom_corner_radius, defaultRadius);
+            leftBottomRadius = array.getDimensionPixelOffset(R.styleable.RoundCornerImageView_left_bottom_corner_radius, defaultRadius);
+            array.recycle();
+        }
+
+        if (defaultRadius == leftTopRadius) {
+            leftTopRadius = radius;
+        }
+        if (defaultRadius == rightTopRadius) {
+            rightTopRadius = radius;
+        }
+        if (defaultRadius == rightBottomRadius) {
+            rightBottomRadius = radius;
+        }
+        if (defaultRadius == leftBottomRadius) {
+            leftBottomRadius = radius;
+        }
+    }
+
+    public void setLeftBottomRadius(int leftBottomRadius) {
+        this.leftBottomRadius = leftBottomRadius;
+    }
+
+    public void setLeftTopRadius(int leftTopRadius) {
+        this.leftTopRadius = leftTopRadius;
+    }
+
+    public void setRadius(int radius) {
+        this.radius = radius;
+        leftBottomRadius = radius;
+        rightBottomRadius = radius;
+        rightTopRadius = radius;
+        leftTopRadius = radius;
+    }
+
+    public void setRightBottomRadius(int rightBottomRadius) {
+        this.rightBottomRadius = rightBottomRadius;
+    }
+
+    public void setRightTopRadius(int rightTopRadius) {
+        this.rightTopRadius = rightTopRadius;
+    }
+
+    public int getLeftBottomRadius() {
+        return leftBottomRadius;
+    }
+
+    public int getLeftTopRadius() {
+        return leftTopRadius;
+    }
+
+    public int getRadius() {
+        return radius;
+    }
+
+    public int getRightBottomRadius() {
+        return rightBottomRadius;
+    }
+
+    public int getRightTopRadius() {
+        return rightTopRadius;
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        path.reset();
+        canvas.setDrawFilter(aliasFilter);
+        rectF.set(0, 0, getMeasuredWidth(), getMeasuredHeight());
+        // left-top -> right-top -> right-bottom -> left-bottom
+        float[] radius = {
+            leftTopRadius, leftTopRadius, rightTopRadius, rightTopRadius, rightBottomRadius, rightBottomRadius, leftBottomRadius, leftBottomRadius};
+        path.addRoundRect(rectF, radius, Path.Direction.CW);
+        canvas.clipPath(path);
+        super.onDraw(canvas);
+    }
+}

+ 127 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/RoundFrameLayout.java

@@ -0,0 +1,127 @@
+package com.tencent.qcloud.tuikit.timcommon.component;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PaintFlagsDrawFilter;
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.tencent.qcloud.tuikit.timcommon.R;
+
+public class RoundFrameLayout extends FrameLayout {
+    private final Path path = new Path();
+    private final RectF rectF = new RectF();
+    private final PaintFlagsDrawFilter aliasFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
+    private int radius;
+    private int leftTopRadius;
+    private int rightTopRadius;
+    private int rightBottomRadius;
+    private int leftBottomRadius;
+
+    public RoundFrameLayout(@NonNull Context context) {
+        super(context);
+        init(context, null);
+    }
+
+    public RoundFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+        init(context, attrs);
+    }
+
+    public RoundFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    private void init(Context context, AttributeSet attrs) {
+        setLayerType(View.LAYER_TYPE_HARDWARE, null);
+        int defaultRadius = 0;
+        if (attrs != null) {
+            TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.RoundFrameLayout);
+            radius = array.getDimensionPixelOffset(R.styleable.RoundFrameLayout_corner_radius, defaultRadius);
+            leftTopRadius = array.getDimensionPixelOffset(R.styleable.RoundFrameLayout_left_top_corner_radius, defaultRadius);
+            rightTopRadius = array.getDimensionPixelOffset(R.styleable.RoundFrameLayout_right_top_corner_radius, defaultRadius);
+            rightBottomRadius = array.getDimensionPixelOffset(R.styleable.RoundFrameLayout_right_bottom_corner_radius, defaultRadius);
+            leftBottomRadius = array.getDimensionPixelOffset(R.styleable.RoundFrameLayout_left_bottom_corner_radius, defaultRadius);
+            array.recycle();
+        }
+
+        if (defaultRadius == leftTopRadius) {
+            leftTopRadius = radius;
+        }
+        if (defaultRadius == rightTopRadius) {
+            rightTopRadius = radius;
+        }
+        if (defaultRadius == rightBottomRadius) {
+            rightBottomRadius = radius;
+        }
+        if (defaultRadius == leftBottomRadius) {
+            leftBottomRadius = radius;
+        }
+    }
+
+
+    public void setLeftBottomRadius(int leftBottomRadius) {
+        this.leftBottomRadius = leftBottomRadius;
+    }
+
+    public void setLeftTopRadius(int leftTopRadius) {
+        this.leftTopRadius = leftTopRadius;
+    }
+
+    public void setRadius(int radius) {
+        this.radius = radius;
+        leftBottomRadius = radius;
+        rightBottomRadius = radius;
+        rightTopRadius = radius;
+        leftTopRadius = radius;
+    }
+
+    public void setRightBottomRadius(int rightBottomRadius) {
+        this.rightBottomRadius = rightBottomRadius;
+    }
+
+    public void setRightTopRadius(int rightTopRadius) {
+        this.rightTopRadius = rightTopRadius;
+    }
+
+    public int getLeftBottomRadius() {
+        return leftBottomRadius;
+    }
+
+    public int getLeftTopRadius() {
+        return leftTopRadius;
+    }
+
+    public int getRadius() {
+        return radius;
+    }
+
+    public int getRightBottomRadius() {
+        return rightBottomRadius;
+    }
+
+    public int getRightTopRadius() {
+        return rightTopRadius;
+    }
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        path.reset();
+        canvas.setDrawFilter(aliasFilter);
+        rectF.set(0, 0, getMeasuredWidth(), getMeasuredHeight());
+        // left-top -> right-top -> right-bottom -> left-bottom
+        float[] radius = {
+            leftTopRadius, leftTopRadius, rightTopRadius, rightTopRadius, rightBottomRadius, rightBottomRadius, leftBottomRadius, leftBottomRadius};
+        path.addRoundRect(rectF, radius, Path.Direction.CW);
+        canvas.clipPath(path);
+        super.dispatchDraw(canvas);
+    }
+}

+ 59 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/SwitchCustomWidth.java

@@ -0,0 +1,59 @@
+package com.tencent.qcloud.tuikit.timcommon.component;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.SwitchCompat;
+
+import com.tencent.qcloud.tuikit.timcommon.R;
+
+import java.lang.reflect.Field;
+
+public class SwitchCustomWidth extends SwitchCompat {
+    private static final String TAG = "SwitchCustomWidth";
+
+    private int customSwitchWidth;
+
+    public SwitchCustomWidth(@NonNull Context context) {
+        super(context);
+        initCustomAttr(context, null);
+    }
+
+    public SwitchCustomWidth(@NonNull Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+        initCustomAttr(context, attrs);
+    }
+
+    public SwitchCustomWidth(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        initCustomAttr(context, attrs);
+    }
+
+    public void initCustomAttr(Context context, AttributeSet attributeSet) {
+        if (attributeSet != null) {
+            TypedArray array = context.obtainStyledAttributes(attributeSet, R.styleable.SwitchCustomWidth);
+            customSwitchWidth = array.getDimensionPixelSize(R.styleable.SwitchCustomWidth_custom_width, 0);
+            array.recycle();
+        }
+    }
+
+    @Override
+    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        try {
+            if (customSwitchWidth == 0) {
+                return;
+            }
+            Class<SwitchCompat> clazz = SwitchCompat.class;
+            Field mSwitchWidthFiled = clazz.getDeclaredField("mSwitchWidth");
+            mSwitchWidthFiled.setAccessible(true);
+            mSwitchWidthFiled.set(this, customSwitchWidth);
+        } catch (Exception e) {
+            Log.w(TAG, e.getMessage());
+        }
+    }
+}

+ 186 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/TitleBarLayout.java

@@ -0,0 +1,186 @@
+package com.tencent.qcloud.tuikit.timcommon.component;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+
+import com.tencent.qcloud.tuicore.TUIThemeManager;
+import com.tencent.qcloud.tuikit.timcommon.R;
+import com.tencent.qcloud.tuikit.timcommon.component.interfaces.ITitleBarLayout;
+import com.tencent.qcloud.tuikit.timcommon.util.ScreenUtil;
+
+public class TitleBarLayout extends LinearLayout implements ITitleBarLayout {
+    private LinearLayout mLeftGroup;
+    private LinearLayout mRightGroup;
+    private TextView mLeftTitle;
+    private TextView mCenterTitle;
+    private TextView mRightTitle;
+    private ImageView mLeftIcon;
+    private ImageView mRightIcon;
+    private RelativeLayout mTitleLayout;
+    private UnreadCountTextView unreadCountTextView;
+
+    public TitleBarLayout(Context context) {
+        super(context);
+        init(context, null);
+    }
+
+    public TitleBarLayout(Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+        init(context, attrs);
+    }
+
+    public TitleBarLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init(context, attrs);
+    }
+
+    private void init(Context context, @Nullable AttributeSet attrs) {
+        String middleTitle = null;
+        boolean canReturn = false;
+        if (attrs != null) {
+            TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.TitleBarLayout);
+            middleTitle = array.getString(R.styleable.TitleBarLayout_title_bar_middle_title);
+            canReturn = array.getBoolean(R.styleable.TitleBarLayout_title_bar_can_return, false);
+            array.recycle();
+        }
+        inflate(context, R.layout.timcommon_title_bar_layout, this);
+        mTitleLayout = findViewById(R.id.page_title_layout);
+        mLeftGroup = findViewById(R.id.page_title_left_group);
+        mRightGroup = findViewById(R.id.page_title_right_group);
+        mLeftTitle = findViewById(R.id.page_title_left_text);
+        mRightTitle = findViewById(R.id.page_title_right_text);
+        mCenterTitle = findViewById(R.id.page_title);
+        mLeftIcon = findViewById(R.id.page_title_left_icon);
+        Drawable leftIconDrawable = mLeftIcon.getBackground();
+        if (leftIconDrawable != null) {
+            leftIconDrawable.setAutoMirrored(true);
+        }
+        mRightIcon = findViewById(R.id.page_title_right_icon);
+        unreadCountTextView = findViewById(R.id.new_message_total_unread);
+
+        LayoutParams params = (LayoutParams) mTitleLayout.getLayoutParams();
+        params.height = ScreenUtil.getPxByDp(50);
+        mTitleLayout.setLayoutParams(params);
+        setBackgroundResource(TUIThemeManager.getAttrResId(getContext(), R.attr.core_title_bar_bg));
+
+        int iconSize = ScreenUtil.dip2px(20);
+        ViewGroup.LayoutParams iconParams = mLeftIcon.getLayoutParams();
+        iconParams.width = iconSize;
+        iconParams.height = iconSize;
+        mLeftIcon.setLayoutParams(iconParams);
+        iconParams = mRightIcon.getLayoutParams();
+        iconParams.width = iconSize;
+        iconParams.height = iconSize;
+
+        mRightIcon.setLayoutParams(iconParams);
+
+        if (canReturn) {
+            setLeftReturnListener(context);
+        }
+        if (!TextUtils.isEmpty(middleTitle)) {
+            mCenterTitle.setText(middleTitle);
+        }
+    }
+
+    public void setLeftReturnListener(Context context) {
+        mLeftGroup.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (context instanceof Activity) {
+                    InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+                    imm.hideSoftInputFromWindow(TitleBarLayout.this.getWindowToken(), 0);
+                    ((Activity) context).finish();
+                }
+            }
+        });
+    }
+
+    @Override
+    public void setOnLeftClickListener(OnClickListener listener) {
+        mLeftGroup.setOnClickListener(listener);
+    }
+
+    @Override
+    public void setOnRightClickListener(OnClickListener listener) {
+        mRightGroup.setOnClickListener(listener);
+    }
+
+    @Override
+    public void setTitle(String title, Position position) {
+        switch (position) {
+            case LEFT:
+                mLeftTitle.setText(title);
+                break;
+            case RIGHT:
+                mRightTitle.setText(title);
+                break;
+            case MIDDLE:
+                mCenterTitle.setText(title);
+                break;
+            default:
+                break;
+        }
+    }
+
+    @Override
+    public LinearLayout getLeftGroup() {
+        return mLeftGroup;
+    }
+
+    @Override
+    public LinearLayout getRightGroup() {
+        return mRightGroup;
+    }
+
+    @Override
+    public ImageView getLeftIcon() {
+        return mLeftIcon;
+    }
+
+    @Override
+    public void setLeftIcon(int resId) {
+        mLeftIcon.setBackgroundResource(resId);
+    }
+
+    @Override
+    public ImageView getRightIcon() {
+        return mRightIcon;
+    }
+
+    @Override
+    public void setRightIcon(int resId) {
+        mRightIcon.setBackgroundResource(resId);
+    }
+
+    @Override
+    public TextView getLeftTitle() {
+        return mLeftTitle;
+    }
+
+    @Override
+    public TextView getMiddleTitle() {
+        return mCenterTitle;
+    }
+
+    @Override
+    public TextView getRightTitle() {
+        return mRightTitle;
+    }
+
+    public UnreadCountTextView getUnreadCountTextView() {
+        return unreadCountTextView;
+    }
+}

+ 84 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/UnreadCountTextView.java

@@ -0,0 +1,84 @@
+package com.tencent.qcloud.tuikit.timcommon.component;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+import android.view.View;
+
+import androidx.appcompat.widget.AppCompatTextView;
+
+import com.tencent.qcloud.tuikit.timcommon.R;
+
+public class UnreadCountTextView extends AppCompatTextView {
+    private int mNormalSize;
+    private Paint mPaint;
+
+    public UnreadCountTextView(Context context) {
+        super(context);
+        init(context, null);
+    }
+
+    public UnreadCountTextView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context, attrs);
+    }
+
+    public UnreadCountTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init(context, attrs);
+    }
+
+    private void init(Context context, AttributeSet attrs) {
+        setTextDirection(View.TEXT_DIRECTION_LTR);
+        mNormalSize = dp2px(18.4f);
+        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.UnreadCountTextView);
+        int paintColor = typedArray.getColor(R.styleable.UnreadCountTextView_paint_color, getResources().getColor(R.color.read_dot_bg));
+        typedArray.recycle();
+
+        mPaint = new Paint();
+        mPaint.setColor(paintColor);
+        mPaint.setAntiAlias(true);
+    }
+
+    public void setPaintColor(int color) {
+        if (mPaint != null) {
+            mPaint.setColor(color);
+        }
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        if (getText().length() == 0) {
+            int l = (getMeasuredWidth() - dp2px(6)) / 2;
+            int t = l;
+            int r = getMeasuredWidth() - l;
+            int b = r;
+            canvas.drawOval(new RectF(l, t, r, b), mPaint);
+        } else if (getText().length() == 1) {
+            canvas.drawOval(new RectF(0, 0, mNormalSize, mNormalSize), mPaint);
+        } else if (getText().length() > 1) {
+            canvas.drawRoundRect(new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight()), getMeasuredHeight() / 2, getMeasuredHeight() / 2, mPaint);
+        }
+        super.onDraw(canvas);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int width = mNormalSize;
+        int height = mNormalSize;
+        if (getText().length() > 1) {
+            width = mNormalSize + dp2px((getText().length() - 1) * 10);
+        }
+        setMeasuredDimension(width, height);
+    }
+
+    private int dp2px(float dp) {
+        DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
+        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, displayMetrics);
+    }
+}

+ 5 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/action/PopActionClickListener.java

@@ -0,0 +1,5 @@
+package com.tencent.qcloud.tuikit.timcommon.component.action;
+
+public interface PopActionClickListener {
+    void onActionClick(int index, Object data);
+}

+ 63 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/action/PopDialogAdapter.java

@@ -0,0 +1,63 @@
+package com.tencent.qcloud.tuikit.timcommon.component.action;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+
+import com.tencent.qcloud.tuicore.TUIConfig;
+import com.tencent.qcloud.tuikit.timcommon.R;
+import com.tencent.qcloud.tuikit.timcommon.util.ThreadUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PopDialogAdapter extends BaseAdapter {
+    private List<PopMenuAction> dataSource = new ArrayList<>();
+
+    public void setDataSource(final List datas) {
+        dataSource = datas;
+        ThreadUtils.postOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                notifyDataSetChanged();
+            }
+        });
+    }
+
+    @Override
+    public int getCount() {
+        return dataSource.size();
+    }
+
+    @Override
+    public Object getItem(int position) {
+        return dataSource.get(position);
+    }
+
+    @Override
+    public long getItemId(int position) {
+        return position;
+    }
+
+    @Override
+    public View getView(final int position, View convertView, ViewGroup parent) {
+        ViewHolder holder;
+        if (convertView == null) {
+            convertView = LayoutInflater.from(TUIConfig.getAppContext()).inflate(R.layout.pop_dialog_adapter, parent, false);
+            holder = new ViewHolder();
+            holder.text = convertView.findViewById(R.id.pop_dialog_text);
+            convertView.setTag(holder);
+        } else {
+            holder = (ViewHolder) convertView.getTag();
+        }
+        PopMenuAction action = (PopMenuAction) getItem(position);
+        holder.text.setText(action.getActionName());
+        return convertView;
+    }
+
+    static class ViewHolder {
+        TextView text;
+    }
+}

+ 51 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/action/PopMenuAction.java

@@ -0,0 +1,51 @@
+package com.tencent.qcloud.tuikit.timcommon.component.action;
+
+import android.graphics.Bitmap;
+
+public class PopMenuAction {
+    private String actionName;
+    private Bitmap icon;
+    private int iconResId;
+    private PopActionClickListener actionClickListener;
+    private int weight;
+
+    public String getActionName() {
+        return actionName;
+    }
+
+    public void setActionName(String actionName) {
+        this.actionName = actionName;
+    }
+
+    public Bitmap getIcon() {
+        return icon;
+    }
+
+    public void setIcon(Bitmap mIcon) {
+        this.icon = mIcon;
+    }
+
+    public int getIconResId() {
+        return iconResId;
+    }
+
+    public void setIconResId(int iconResId) {
+        this.iconResId = iconResId;
+    }
+
+    public PopActionClickListener getActionClickListener() {
+        return actionClickListener;
+    }
+
+    public void setActionClickListener(PopActionClickListener actionClickListener) {
+        this.actionClickListener = actionClickListener;
+    }
+
+    public int getWeight() {
+        return weight;
+    }
+
+    public void setWeight(int weight) {
+        this.weight = weight;
+    }
+}

+ 81 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/action/PopMenuAdapter.java

@@ -0,0 +1,81 @@
+package com.tencent.qcloud.tuikit.timcommon.component.action;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+import com.tencent.qcloud.tuicore.TUIConfig;
+import com.tencent.qcloud.tuikit.timcommon.R;
+import com.tencent.qcloud.tuikit.timcommon.util.ThreadUtils;
+import java.util.ArrayList;
+import java.util.List;
+
+public class PopMenuAdapter extends BaseAdapter {
+    private List<PopMenuAction> dataSource = new ArrayList<>();
+
+    public PopMenuAdapter() {}
+
+    public void setDataSource(final List datas) {
+        dataSource = datas;
+        ThreadUtils.postOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                notifyDataSetChanged();
+            }
+        });
+    }
+
+    @Override
+    public int getCount() {
+        return dataSource.size();
+    }
+
+    @Override
+    public Object getItem(int position) {
+        return dataSource.get(position);
+    }
+
+    @Override
+    public long getItemId(int position) {
+        return position;
+    }
+
+    @Override
+    public View getView(final int position, View convertView, ViewGroup parent) {
+        ViewHolder holder;
+        if (convertView == null) {
+            convertView = LayoutInflater.from(TUIConfig.getAppContext()).inflate(R.layout.pop_menu_adapter, parent, false);
+            holder = new ViewHolder();
+            holder.menuIcon = convertView.findViewById(R.id.pop_menu_icon);
+
+            int iconSize = convertView.getResources().getDimensionPixelSize(R.dimen.core_pop_menu_icon_size);
+            ViewGroup.LayoutParams params = holder.menuIcon.getLayoutParams();
+            params.width = iconSize;
+            params.height = iconSize;
+            holder.menuIcon.setLayoutParams(params);
+
+            holder.menuLable = convertView.findViewById(R.id.pop_menu_label);
+            convertView.setTag(holder);
+        } else {
+            holder = (ViewHolder) convertView.getTag();
+        }
+        PopMenuAction action = (PopMenuAction) getItem(position);
+        holder.menuIcon.setVisibility(View.VISIBLE);
+        if (action.getIcon() != null) {
+            holder.menuIcon.setImageBitmap(action.getIcon());
+        } else if (action.getIconResId() > 0) {
+            holder.menuIcon.setImageResource(action.getIconResId());
+        } else {
+            holder.menuIcon.setVisibility(View.GONE);
+        }
+        holder.menuLable.setText(action.getActionName());
+        return convertView;
+    }
+
+    static class ViewHolder {
+        TextView menuLable;
+        ImageView menuIcon;
+    }
+}

+ 48 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/activities/BaseLightActivity.java

@@ -0,0 +1,48 @@
+package com.tencent.qcloud.tuikit.timcommon.component.activities;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.inputmethod.InputMethodManager;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.tencent.qcloud.tuicore.TUIThemeManager;
+import com.tencent.qcloud.tuikit.timcommon.R;
+
+public class BaseLightActivity extends AppCompatActivity {
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
+            getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+            getWindow().setStatusBarColor(
+                getResources().getColor(TUIThemeManager.getAttrResId(this, com.tencent.qcloud.tuicore.R.attr.core_header_start_color)));
+            getWindow().setNavigationBarColor(getResources().getColor(R.color.navigation_bar_color));
+            int vis = getWindow().getDecorView().getSystemUiVisibility();
+            vis |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
+            vis |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
+            getWindow().getDecorView().setSystemUiVisibility(vis);
+        }
+    }
+
+    @Override
+    public void finish() {
+        hideSoftInput();
+        super.finish();
+    }
+
+    public void hideSoftInput() {
+        InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+        Window window = getWindow();
+        if (window != null) {
+            imm.hideSoftInputFromWindow(window.getDecorView().getWindowToken(), 0);
+        }
+    }
+}

+ 44 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/activities/BaseMinimalistLightActivity.java

@@ -0,0 +1,44 @@
+package com.tencent.qcloud.tuikit.timcommon.component.activities;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.inputmethod.InputMethodManager;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+public class BaseMinimalistLightActivity extends AppCompatActivity {
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
+            getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+            getWindow().setStatusBarColor(0xFFFFFFFF);
+            getWindow().setNavigationBarColor(0xFFFFFFFF);
+            int vis = getWindow().getDecorView().getSystemUiVisibility();
+            vis |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
+            vis |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
+            getWindow().getDecorView().setSystemUiVisibility(vis);
+        }
+    }
+
+    @Override
+    public void finish() {
+        hideSoftInput();
+        super.finish();
+    }
+
+    public void hideSoftInput() {
+        InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+        Window window = getWindow();
+        if (window != null) {
+            imm.hideSoftInputFromWindow(window.getDecorView().getWindowToken(), 0);
+        }
+    }
+}

+ 450 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/activities/ImageSelectActivity.java

@@ -0,0 +1,450 @@
+package com.tencent.qcloud.tuikit.timcommon.component.activities;
+
+import android.app.ProgressDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.load.DataSource;
+import com.bumptech.glide.load.engine.GlideException;
+import com.bumptech.glide.request.RequestListener;
+import com.bumptech.glide.request.RequestOptions;
+import com.bumptech.glide.request.target.Target;
+import com.tencent.qcloud.tuicore.TUIConstants;
+import com.tencent.qcloud.tuicore.TUIThemeManager;
+import com.tencent.qcloud.tuicore.util.ToastUtil;
+import com.tencent.qcloud.tuikit.timcommon.R;
+import com.tencent.qcloud.tuikit.timcommon.component.TitleBarLayout;
+import com.tencent.qcloud.tuikit.timcommon.component.gatherimage.SynthesizedImageView;
+import com.tencent.qcloud.tuikit.timcommon.component.interfaces.ITitleBarLayout;
+import com.tencent.qcloud.tuikit.timcommon.util.LayoutUtil;
+import com.tencent.qcloud.tuikit.timcommon.util.ScreenUtil;
+import java.io.File;
+import java.io.Serializable;
+import java.util.List;
+
+public class ImageSelectActivity extends BaseLightActivity {
+    private static final String TAG = ImageSelectActivity.class.getSimpleName();
+
+    public static final String CHAT_CONVERSATION_BACKGROUND_DEFAULT_URL = "chat/conversation/background/default/url";
+    public static final int RESULT_CODE_ERROR = -1;
+    public static final int RESULT_CODE_SUCCESS = 0;
+    public static final String TITLE = "title";
+    public static final String SPAN_COUNT = "spanCount";
+    public static final String DATA = "data";
+    public static final String ITEM_HEIGHT = "itemHeight";
+    public static final String ITEM_WIDTH = "itemWidth";
+    public static final String SELECTED = "selected";
+    public static final String PLACEHOLDER = "placeholder";
+    public static final String NEED_DOWNLOAD_LOCAL = "needDownload";
+
+    private int defaultSpacing;
+
+    private List<ImageBean> data;
+    private ImageBean selected;
+    private int placeHolder;
+    private int columnNum;
+    private RecyclerView imageGrid;
+    private GridLayoutManager gridLayoutManager;
+    private ImageGridAdapter gridAdapter;
+    private TitleBarLayout titleBarLayout;
+    private int itemHeight;
+    private int itemWidth;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        defaultSpacing = ScreenUtil.dip2px(12);
+        setContentView(R.layout.core_activity_image_select_layout);
+        Intent intent = getIntent();
+        String title = intent.getStringExtra(TITLE);
+        titleBarLayout = findViewById(R.id.image_select_title);
+        titleBarLayout.setTitle(title, ITitleBarLayout.Position.MIDDLE);
+        titleBarLayout.setTitle(getString(com.tencent.qcloud.tuicore.R.string.sure), ITitleBarLayout.Position.RIGHT);
+        titleBarLayout.getRightIcon().setVisibility(View.GONE);
+        titleBarLayout.getRightTitle().setTextColor(0xFF006EFF);
+        titleBarLayout.setOnLeftClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                setResult(RESULT_CODE_ERROR);
+                finish();
+            }
+        });
+        boolean needDownload = intent.getBooleanExtra(NEED_DOWNLOAD_LOCAL, false);
+        titleBarLayout.setOnRightClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (selected == null) {
+                    return;
+                }
+                if (needDownload) {
+                    downloadUrl();
+                } else {
+                    Intent resultIntent = new Intent();
+                    resultIntent.putExtra(DATA, (Serializable) selected);
+                    setResult(RESULT_CODE_SUCCESS, resultIntent);
+                    finish();
+                }
+            }
+        });
+
+        data = (List<ImageBean>) intent.getSerializableExtra(DATA);
+        selected = (ImageBean) intent.getSerializableExtra(SELECTED);
+        placeHolder = intent.getIntExtra(PLACEHOLDER, 0);
+        itemHeight = intent.getIntExtra(ITEM_HEIGHT, 0);
+        itemWidth = intent.getIntExtra(ITEM_WIDTH, 0);
+        columnNum = intent.getIntExtra(SPAN_COUNT, 2);
+        gridLayoutManager = new GridLayoutManager(this, columnNum);
+        imageGrid = findViewById(R.id.image_select_grid);
+        imageGrid.addItemDecoration(new GridDecoration(columnNum, defaultSpacing, defaultSpacing));
+        imageGrid.setLayoutManager(gridLayoutManager);
+        imageGrid.setItemAnimator(null);
+        gridAdapter = new ImageGridAdapter();
+        gridAdapter.setPlaceHolder(placeHolder);
+        gridAdapter.setSelected(selected);
+        gridAdapter.setOnItemClickListener(new OnItemClickListener() {
+            @Override
+            public void onClick(ImageBean obj) {
+                selected = obj;
+                setSelectedStatus();
+            }
+        });
+        gridAdapter.setItemWidth(itemWidth);
+        gridAdapter.setItemHeight(itemHeight);
+        imageGrid.setAdapter(gridAdapter);
+        gridAdapter.setData(data);
+        setSelectedStatus();
+        gridAdapter.notifyDataSetChanged();
+    }
+
+    private void downloadUrl() {
+        if (selected == null) {
+            return;
+        }
+
+        if (selected.isDefault()) {
+            selected.setLocalPath(CHAT_CONVERSATION_BACKGROUND_DEFAULT_URL);
+            setResult(selected);
+            ToastUtil.toastShortMessage(getResources().getString(R.string.setting_success));
+            finish();
+            return;
+        }
+
+        String url = selected.getImageUri();
+        if (TextUtils.isEmpty(url)) {
+            Log.d(TAG, "DownloadUrl is null");
+            return;
+        }
+
+        final ProgressDialog dialog = new ProgressDialog(this);
+        dialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
+        dialog.setCancelable(false);
+        dialog.setCanceledOnTouchOutside(false);
+        dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
+            @Override
+            public void onDismiss(DialogInterface dialog) {
+                // TODO Auto-generated method stub
+                finish();
+            }
+        });
+        dialog.setMessage(getResources().getString(R.string.setting));
+        dialog.show();
+
+        ImageBean finalBean = selected;
+        Glide.with(this)
+            .downloadOnly()
+            .load(url)
+            .listener(new RequestListener<File>() {
+                @Override
+                public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<File> target, boolean isFirstResource) {
+                    dialog.cancel();
+                    Log.e(TAG, "DownloadUrl onLoadFailed e = " + e);
+                    ToastUtil.toastShortMessage(getResources().getString(R.string.setting_fail));
+                    return false;
+                }
+
+                @Override
+                public boolean onResourceReady(File resource, Object model, Target<File> target, DataSource dataSource, boolean isFirstResource) {
+                    dialog.cancel();
+                    String path = resource.getAbsolutePath();
+                    Log.e(TAG, "DownloadUrl resource path = " + path);
+                    finalBean.setLocalPath(path);
+                    setResult(finalBean);
+                    ToastUtil.toastShortMessage(getResources().getString(R.string.setting_success));
+                    return false;
+                }
+            })
+            .preload();
+    }
+
+    private void setResult(ImageBean bean) {
+        Intent resultIntent = new Intent();
+        resultIntent.putExtra(DATA, (Serializable) bean);
+        setResult(RESULT_CODE_SUCCESS, resultIntent);
+        finish();
+    }
+
+    private void setSelectedStatus() {
+        if (selected != null && data != null && data.contains(selected)) {
+            titleBarLayout.getRightTitle().setEnabled(true);
+            titleBarLayout.getRightTitle().setTextColor(
+                getResources().getColor(TUIThemeManager.getAttrResId(this, com.tencent.qcloud.tuicore.R.attr.core_primary_color)));
+        } else {
+            titleBarLayout.getRightTitle().setEnabled(false);
+            titleBarLayout.getRightTitle().setTextColor(0xFF666666);
+        }
+        gridAdapter.setSelected(selected);
+    }
+
+    public static class ImageGridAdapter extends RecyclerView.Adapter<ImageGridAdapter.ImageViewHolder> {
+        private int itemWidth;
+        private int itemHeight;
+
+        private List<ImageBean> data;
+        private ImageBean selected;
+        private int placeHolder;
+        private OnItemClickListener onItemClickListener;
+
+        public void setData(List<ImageBean> data) {
+            this.data = data;
+        }
+
+        public void setSelected(ImageBean selected) {
+            if (data == null || data.isEmpty()) {
+                this.selected = selected;
+            } else {
+                this.selected = selected;
+                notifyDataSetChanged();
+            }
+        }
+
+        public void setPlaceHolder(int placeHolder) {
+            this.placeHolder = placeHolder;
+        }
+
+        public void setItemHeight(int itemHeight) {
+            this.itemHeight = itemHeight;
+        }
+
+        public void setItemWidth(int itemWidth) {
+            this.itemWidth = itemWidth;
+        }
+
+        public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
+            this.onItemClickListener = onItemClickListener;
+        }
+
+        @NonNull
+        @Override
+        public ImageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.core_select_image_item_layout, parent, false);
+            return new ImageViewHolder(view);
+        }
+
+        @Override
+        public void onBindViewHolder(@NonNull ImageViewHolder holder, int position) {
+            ImageView imageView = holder.imageView;
+            setItemLayoutParams(holder);
+            ImageBean imageBean = data.get(position);
+            if (selected != null && imageBean != null && TextUtils.equals(selected.getThumbnailUri(), imageBean.getThumbnailUri())) {
+                holder.selectBorderLayout.setVisibility(View.VISIBLE);
+            } else {
+                holder.selectBorderLayout.setVisibility(View.GONE);
+            }
+
+            if (imageBean.getGroupGridAvatar() != null) {
+                holder.defaultLayout.setVisibility(View.GONE);
+                if (imageView instanceof SynthesizedImageView) {
+                    SynthesizedImageView synthesizedImageView = ((SynthesizedImageView) (imageView));
+                    String imageId = imageBean.getImageId();
+                    synthesizedImageView.setImageId(imageId);
+                    synthesizedImageView.displayImage(imageBean.getGroupGridAvatar()).load(imageId);
+                }
+            } else if (imageBean.isDefault()) {
+                holder.defaultLayout.setVisibility(View.VISIBLE);
+                imageView.setImageResource(android.R.color.transparent);
+            } else {
+                holder.defaultLayout.setVisibility(View.GONE);
+                Glide.with(holder.itemView.getContext())
+                    .asBitmap()
+                    .load(imageBean.getThumbnailUri())
+                    .placeholder(placeHolder)
+                    .apply(new RequestOptions().error(placeHolder))
+                    .into(imageView);
+            }
+
+            holder.itemView.setOnClickListener(new View.OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    if (onItemClickListener != null) {
+                        onItemClickListener.onClick(imageBean);
+                    }
+                }
+            });
+        }
+
+        private void setItemLayoutParams(ImageViewHolder holder) {
+            if (itemHeight > 0 && itemWidth > 0) {
+                ViewGroup.LayoutParams itemViewLayoutParams = holder.itemView.getLayoutParams();
+                itemViewLayoutParams.width = itemWidth;
+                itemViewLayoutParams.height = itemHeight;
+                holder.itemView.setLayoutParams(itemViewLayoutParams);
+
+                ViewGroup.LayoutParams params = holder.imageView.getLayoutParams();
+                params.width = itemWidth;
+                params.height = itemHeight;
+                holder.imageView.setLayoutParams(params);
+
+                ViewGroup.LayoutParams borderLayoutParams = holder.selectBorderLayout.getLayoutParams();
+                borderLayoutParams.width = itemWidth;
+                borderLayoutParams.height = itemHeight;
+                holder.selectBorderLayout.setLayoutParams(borderLayoutParams);
+
+                ViewGroup.LayoutParams borderParams = holder.selectedBorder.getLayoutParams();
+                borderParams.width = itemWidth;
+                borderParams.height = itemHeight;
+                holder.selectedBorder.setLayoutParams(borderParams);
+            }
+        }
+
+        @Override
+        public int getItemCount() {
+            if (data == null || data.isEmpty()) {
+                return 0;
+            }
+            return data.size();
+        }
+
+        public static class ImageViewHolder extends RecyclerView.ViewHolder {
+            private final ImageView imageView;
+            private final ImageView selectedBorder;
+            private final RelativeLayout selectBorderLayout;
+            private final Button defaultLayout;
+
+            public ImageViewHolder(@NonNull View itemView) {
+                super(itemView);
+                imageView = itemView.findViewById(R.id.content_image);
+                selectedBorder = itemView.findViewById(R.id.select_border);
+                selectBorderLayout = itemView.findViewById(R.id.selected_border_area);
+                defaultLayout = itemView.findViewById(R.id.default_image_layout);
+            }
+        }
+    }
+
+    /**
+     * add spacing
+     */
+    public static class GridDecoration extends RecyclerView.ItemDecoration {
+        private final int columnNum; // span count
+        private final int leftRightSpace; // vertical spacing
+        private final int topBottomSpace; // horizontal spacing
+
+        public GridDecoration(int columnNum, int leftRightSpace, int topBottomSpace) {
+            this.columnNum = columnNum;
+            this.leftRightSpace = leftRightSpace;
+            this.topBottomSpace = topBottomSpace;
+        }
+
+        @Override
+        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+            int position = parent.getChildAdapterPosition(view);
+            int column = position % columnNum;
+
+            int left = column * leftRightSpace / columnNum;
+            int right = leftRightSpace * (columnNum - 1 - column) / columnNum;
+            if (LayoutUtil.isRTL()) {
+                outRect.left = right;
+                outRect.right = left;
+            } else {
+                outRect.left = left;
+                outRect.right = right;
+            }
+            // add top spacing
+            if (position >= columnNum) {
+                outRect.top = topBottomSpace;
+            }
+        }
+    }
+
+    public interface OnItemClickListener {
+        void onClick(ImageBean obj);
+    }
+
+    public static class ImageBean implements Serializable {
+        String thumbnailUri; // for display
+        String imageUri; // for download
+        String localPath; // for local path
+        boolean isDefault = false; // for default display
+        List<Object> groupGridAvatar = null; // for group grid avatar
+        String imageId;
+
+        public ImageBean() {}
+
+        public ImageBean(String thumbnailUri, String imageUri, boolean isDefault) {
+            this.thumbnailUri = thumbnailUri;
+            this.imageUri = imageUri;
+            this.isDefault = isDefault;
+        }
+
+        public String getImageUri() {
+            return imageUri;
+        }
+
+        public String getThumbnailUri() {
+            return thumbnailUri;
+        }
+
+        public void setImageUri(String imageUri) {
+            this.imageUri = imageUri;
+        }
+
+        public void setThumbnailUri(String thumbnailUri) {
+            this.thumbnailUri = thumbnailUri;
+        }
+
+        public String getLocalPath() {
+            return localPath;
+        }
+
+        public void setLocalPath(String localPath) {
+            this.localPath = localPath;
+        }
+
+        public boolean isDefault() {
+            return isDefault;
+        }
+
+        public void setDefault(boolean aDefault) {
+            isDefault = aDefault;
+        }
+
+        public List<Object> getGroupGridAvatar() {
+            return groupGridAvatar;
+        }
+
+        public void setGroupGridAvatar(List<Object> groupGridAvatar) {
+            this.groupGridAvatar = groupGridAvatar;
+        }
+
+        public String getImageId() {
+            return imageId;
+        }
+
+        public void setImageId(String imageId) {
+            this.imageId = imageId;
+        }
+    }
+}

+ 483 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/activities/ImageSelectMinimalistActivity.java

@@ -0,0 +1,483 @@
+package com.tencent.qcloud.tuikit.timcommon.component.activities;
+
+import android.app.ProgressDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.load.DataSource;
+import com.bumptech.glide.load.engine.GlideException;
+import com.bumptech.glide.request.RequestListener;
+import com.bumptech.glide.request.RequestOptions;
+import com.bumptech.glide.request.target.Target;
+import com.tencent.qcloud.tuicore.TUIConstants;
+import com.tencent.qcloud.tuicore.TUIThemeManager;
+import com.tencent.qcloud.tuicore.util.ToastUtil;
+import com.tencent.qcloud.tuikit.timcommon.R;
+import com.tencent.qcloud.tuikit.timcommon.component.TitleBarLayout;
+import com.tencent.qcloud.tuikit.timcommon.component.gatherimage.SynthesizedImageView;
+import com.tencent.qcloud.tuikit.timcommon.component.interfaces.ITitleBarLayout;
+import com.tencent.qcloud.tuikit.timcommon.util.LayoutUtil;
+import com.tencent.qcloud.tuikit.timcommon.util.ScreenUtil;
+import java.io.File;
+import java.io.Serializable;
+import java.util.List;
+
+public class ImageSelectMinimalistActivity extends BaseMinimalistLightActivity {
+    private static final String TAG = ImageSelectMinimalistActivity.class.getSimpleName();
+
+    public static final int RESULT_CODE_ERROR = -1;
+    public static final int RESULT_CODE_SUCCESS = 0;
+    public static final String TITLE = "title";
+    public static final String SPAN_COUNT = "spanCount";
+    public static final String DATA = "data";
+    public static final String ITEM_HEIGHT = "itemHeight";
+    public static final String ITEM_WIDTH = "itemWidth";
+    public static final String SELECTED = "selected";
+    public static final String PLACEHOLDER = "placeholder";
+    public static final String NEED_DOWLOAD_LOCAL = "needdowmload";
+    public static final String CHAT_CONVERSATION_BACKGROUND_DEFAULT_URL = "chat/conversation/background/default/url";
+
+    private int defaultSpacing;
+
+    private List<ImageBean> data;
+    private ImageBean selected;
+    private int placeHolder;
+    private int columnNum;
+    private RecyclerView imageGrid;
+    private GridLayoutManager gridLayoutManager;
+    private ImageGridAdapter gridAdapter;
+    private TitleBarLayout titleBarLayout;
+    private int itemHeight;
+    private int itemWidth;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        defaultSpacing = ScreenUtil.dip2px(12);
+        setContentView(R.layout.core_minimalist_activity_image_select_layout);
+        Intent intent = getIntent();
+        String title = intent.getStringExtra(TITLE);
+        titleBarLayout = findViewById(R.id.image_select_title);
+        titleBarLayout.setTitle(title, ITitleBarLayout.Position.MIDDLE);
+        titleBarLayout.setTitle(getString(com.tencent.qcloud.tuicore.R.string.sure), ITitleBarLayout.Position.RIGHT);
+        titleBarLayout.getRightIcon().setVisibility(View.GONE);
+        titleBarLayout.getRightTitle().setTextColor(0xFF006EFF);
+        titleBarLayout.setOnLeftClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                setResult(RESULT_CODE_ERROR);
+                finish();
+            }
+        });
+        boolean needDownload = intent.getBooleanExtra(NEED_DOWLOAD_LOCAL, false);
+        titleBarLayout.setOnRightClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (selected == null) {
+                    return;
+                }
+                if (needDownload) {
+                    downloadUrl();
+                } else {
+                    Intent resultIntent = new Intent();
+                    resultIntent.putExtra(DATA, (Serializable) selected);
+                    setResult(RESULT_CODE_SUCCESS, resultIntent);
+                    finish();
+                }
+            }
+        });
+
+        data = (List<ImageBean>) intent.getSerializableExtra(DATA);
+        selected = (ImageBean) intent.getSerializableExtra(SELECTED);
+        placeHolder = intent.getIntExtra(PLACEHOLDER, 0);
+        itemHeight = intent.getIntExtra(ITEM_HEIGHT, 0);
+        itemWidth = intent.getIntExtra(ITEM_WIDTH, 0);
+        columnNum = intent.getIntExtra(SPAN_COUNT, 2);
+        gridLayoutManager = new GridLayoutManager(this, columnNum);
+        imageGrid = findViewById(R.id.image_select_grid);
+        imageGrid.addItemDecoration(new GridDecoration(columnNum, defaultSpacing, defaultSpacing));
+        imageGrid.setLayoutManager(gridLayoutManager);
+        imageGrid.setItemAnimator(null);
+        gridAdapter = new ImageGridAdapter();
+        gridAdapter.setPlaceHolder(placeHolder);
+        gridAdapter.setSelected(selected);
+        gridAdapter.setOnItemClickListener(new OnItemClickListener() {
+            @Override
+            public void onClick(ImageBean obj) {
+                selected = obj;
+                setSelectedStatus();
+            }
+        });
+        gridAdapter.setItemWidth(itemWidth);
+        gridAdapter.setItemHeight(itemHeight);
+        gridAdapter.setData(data);
+        imageGrid.setAdapter(gridAdapter);
+        setSelectedStatus();
+    }
+
+    private void downloadUrl() {
+        if (selected == null) {
+            return;
+        }
+
+        if (selected.isDefault()) {
+            selected.setLocalPath(CHAT_CONVERSATION_BACKGROUND_DEFAULT_URL);
+            setResult(selected);
+            ToastUtil.toastShortMessage(getResources().getString(R.string.setting_success));
+            finish();
+            return;
+        }
+
+        String url = selected.getImageUri();
+        if (TextUtils.isEmpty(url)) {
+            Log.d(TAG, "DownloadUrl is null");
+            return;
+        }
+
+        final ProgressDialog dialog = new ProgressDialog(this);
+        dialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
+        dialog.setCancelable(false);
+        dialog.setCanceledOnTouchOutside(false);
+        dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
+            @Override
+            public void onDismiss(DialogInterface dialog) {
+                // TODO Auto-generated method stub
+                finish();
+            }
+        });
+        dialog.setMessage(getResources().getString(R.string.setting));
+        dialog.show();
+
+        ImageBean finalBean = selected;
+        Glide.with(this)
+            .downloadOnly()
+            .load(url)
+            .listener(new RequestListener<File>() {
+                @Override
+                public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<File> target, boolean isFirstResource) {
+                    dialog.cancel();
+                    Log.e(TAG, "DownloadUrl onLoadFailed e = " + e);
+                    ToastUtil.toastShortMessage(getResources().getString(R.string.setting_fail));
+                    return false;
+                }
+
+                @Override
+                public boolean onResourceReady(File resource, Object model, Target<File> target, DataSource dataSource, boolean isFirstResource) {
+                    dialog.cancel();
+                    String path = resource.getAbsolutePath();
+                    Log.e(TAG, "DownloadUrl resource path = " + path);
+                    finalBean.setLocalPath(path);
+                    setResult(finalBean);
+                    ToastUtil.toastShortMessage(getResources().getString(R.string.setting_success));
+                    return false;
+                }
+            })
+            .preload();
+    }
+
+    private void setResult(ImageBean bean) {
+        Intent resultIntent = new Intent();
+        resultIntent.putExtra(DATA, (Serializable) bean);
+        setResult(RESULT_CODE_SUCCESS, resultIntent);
+        finish();
+    }
+
+    private void setSelectedStatus() {
+        if (selected != null && data != null && data.contains(selected)) {
+            titleBarLayout.getRightTitle().setEnabled(true);
+            titleBarLayout.getRightTitle().setTextColor(
+                getResources().getColor(TUIThemeManager.getAttrResId(this, com.tencent.qcloud.tuicore.R.attr.core_primary_color)));
+        } else {
+            titleBarLayout.getRightTitle().setEnabled(false);
+            titleBarLayout.getRightTitle().setTextColor(0xFF666666);
+        }
+        gridAdapter.setSelected(selected);
+    }
+
+    public static class ImageGridAdapter extends RecyclerView.Adapter<ImageGridAdapter.ImageViewHolder> {
+        private int itemWidth;
+        private int itemHeight;
+
+        private List<ImageBean> data;
+        private ImageBean selected;
+        private int placeHolder;
+        private OnItemClickListener onItemClickListener;
+
+        public void setData(List<ImageBean> data) {
+            this.data = data;
+        }
+
+        public ImageBean getDataByPosition(int position) {
+            if (data != null) {
+                return data.get(position);
+            } else {
+                return null;
+            }
+        }
+
+        public void setSelected(ImageBean selected) {
+            if (data == null || data.isEmpty()) {
+                this.selected = selected;
+            } else {
+                int index = indexOf(selected);
+                int index2 = indexOf(this.selected);
+                this.selected = selected;
+                notifyItemChanged(index, this.selected);
+                notifyItemChanged(index2, this.selected);
+            }
+        }
+
+        private int indexOf(ImageBean imageBean) {
+            if (data == null || data.isEmpty()) {
+                return -1;
+            } else {
+                int originIndex = -1;
+                for (int i = 0; i < data.size(); i++) {
+                    ImageBean item = data.get(i);
+                    if (TextUtils.equals(item.thumbnailUri, imageBean.thumbnailUri)) {
+                        originIndex = i;
+                        break;
+                    }
+                }
+                return originIndex;
+            }
+        }
+
+        public void setPlaceHolder(int placeHolder) {
+            this.placeHolder = placeHolder;
+        }
+
+        public void setItemHeight(int itemHeight) {
+            this.itemHeight = itemHeight;
+        }
+
+        public void setItemWidth(int itemWidth) {
+            this.itemWidth = itemWidth;
+        }
+
+        public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
+            this.onItemClickListener = onItemClickListener;
+        }
+
+        @NonNull
+        @Override
+        public ImageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.core_select_image_item_layout, parent, false);
+            return new ImageViewHolder(view);
+        }
+
+        @Override
+        public void onBindViewHolder(@NonNull ImageViewHolder holder, int position) {}
+
+        @Override
+        public void onBindViewHolder(@NonNull ImageViewHolder holder, int position, @NonNull List<Object> payload) {
+            ImageView imageView = holder.imageView;
+            setItemLayoutParams(holder);
+            ImageBean imageBean = data.get(position);
+            if (selected != null && imageBean != null && TextUtils.equals(selected.getThumbnailUri(), imageBean.getThumbnailUri())) {
+                holder.selectBorderLayout.setVisibility(View.VISIBLE);
+            } else {
+                holder.selectBorderLayout.setVisibility(View.GONE);
+            }
+
+            if (imageBean.getGroupGridAvatar() != null) {
+                holder.defaultLayout.setVisibility(View.GONE);
+                if (!payload.isEmpty()) {
+                    return;
+                }
+                if (imageView instanceof SynthesizedImageView) {
+                    SynthesizedImageView synthesizedImageView = ((SynthesizedImageView) (imageView));
+                    String imageId = imageBean.getImageId();
+                    synthesizedImageView.setImageId(imageId);
+                    synthesizedImageView.displayImage(imageBean.getGroupGridAvatar()).load(imageId);
+                }
+            } else if (imageBean.isDefault()) {
+                holder.defaultLayout.setVisibility(View.VISIBLE);
+                imageView.setImageResource(android.R.color.transparent);
+            } else {
+                holder.defaultLayout.setVisibility(View.GONE);
+                Glide.with(holder.itemView.getContext())
+                    .asBitmap()
+                    .load(imageBean.getThumbnailUri())
+                    .placeholder(placeHolder)
+                    .apply(new RequestOptions().error(placeHolder))
+                    .into(imageView);
+            }
+
+            holder.itemView.setOnClickListener(new View.OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    if (onItemClickListener != null) {
+                        onItemClickListener.onClick(imageBean);
+                    }
+                }
+            });
+        }
+
+        private void setItemLayoutParams(ImageViewHolder holder) {
+            if (itemHeight > 0 && itemWidth > 0) {
+                ViewGroup.LayoutParams itemViewLayoutParams = holder.itemView.getLayoutParams();
+                itemViewLayoutParams.width = itemWidth;
+                itemViewLayoutParams.height = itemHeight;
+                holder.itemView.setLayoutParams(itemViewLayoutParams);
+
+                ViewGroup.LayoutParams params = holder.imageView.getLayoutParams();
+                params.width = itemWidth;
+                params.height = itemHeight;
+                holder.imageView.setLayoutParams(params);
+
+                ViewGroup.LayoutParams borderLayoutParams = holder.selectBorderLayout.getLayoutParams();
+                borderLayoutParams.width = itemWidth;
+                borderLayoutParams.height = itemHeight;
+                holder.selectBorderLayout.setLayoutParams(borderLayoutParams);
+
+                ViewGroup.LayoutParams borderParams = holder.selectedBorder.getLayoutParams();
+                borderParams.width = itemWidth;
+                borderParams.height = itemHeight;
+                holder.selectedBorder.setLayoutParams(borderParams);
+            }
+        }
+
+        @Override
+        public int getItemCount() {
+            if (data == null || data.isEmpty()) {
+                return 0;
+            }
+            return data.size();
+        }
+
+        public static class ImageViewHolder extends RecyclerView.ViewHolder {
+            private final ImageView imageView;
+            private final ImageView selectedBorder;
+            private final RelativeLayout selectBorderLayout;
+            private final Button defaultLayout;
+
+            public ImageViewHolder(@NonNull View itemView) {
+                super(itemView);
+                imageView = itemView.findViewById(R.id.content_image);
+                selectedBorder = itemView.findViewById(R.id.select_border);
+                selectBorderLayout = itemView.findViewById(R.id.selected_border_area);
+                defaultLayout = itemView.findViewById(R.id.default_image_layout);
+            }
+        }
+    }
+
+    /**
+     * add spacing
+     */
+    public static class GridDecoration extends RecyclerView.ItemDecoration {
+        private final int columnNum; // span count
+        private final int leftRightSpace; // vertical spacing
+        private final int topBottomSpace; // horizontal spacing
+
+        public GridDecoration(int columnNum, int leftRightSpace, int topBottomSpace) {
+            this.columnNum = columnNum;
+            this.leftRightSpace = leftRightSpace;
+            this.topBottomSpace = topBottomSpace;
+        }
+
+        @Override
+        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+            int position = parent.getChildAdapterPosition(view);
+            int column = position % columnNum;
+
+            int left = column * leftRightSpace / columnNum;
+            int right = leftRightSpace * (columnNum - 1 - column) / columnNum;
+            if (LayoutUtil.isRTL()) {
+                outRect.left = right;
+                outRect.right = left;
+            } else {
+                outRect.left = left;
+                outRect.right = right;
+            }
+
+            // add top spacing
+            if (position >= columnNum) {
+                outRect.top = topBottomSpace;
+            }
+        }
+    }
+
+    public interface OnItemClickListener {
+        void onClick(ImageBean obj);
+    }
+
+    public static class ImageBean implements Serializable {
+        String thumbnailUri; // for display
+        String imageUri; // for download
+        String localPath; // for local path
+        boolean isDefault = false; // for default display
+        List<Object> groupGridAvatar = null; // for group grid avatar
+        String imageId;
+
+        public ImageBean() {}
+
+        public ImageBean(String thumbnailUri, String imageUri, boolean isDefault) {
+            this.thumbnailUri = thumbnailUri;
+            this.imageUri = imageUri;
+            this.isDefault = isDefault;
+        }
+
+        public String getImageUri() {
+            return imageUri;
+        }
+
+        public String getThumbnailUri() {
+            return thumbnailUri;
+        }
+
+        public void setImageUri(String imageUri) {
+            this.imageUri = imageUri;
+        }
+
+        public void setThumbnailUri(String thumbnailUri) {
+            this.thumbnailUri = thumbnailUri;
+        }
+
+        public String getLocalPath() {
+            return localPath;
+        }
+
+        public void setLocalPath(String localPath) {
+            this.localPath = localPath;
+        }
+
+        public boolean isDefault() {
+            return isDefault;
+        }
+
+        public void setDefault(boolean aDefault) {
+            isDefault = aDefault;
+        }
+
+        public List<Object> getGroupGridAvatar() {
+            return groupGridAvatar;
+        }
+
+        public void setGroupGridAvatar(List<Object> groupGridAvatar) {
+            this.groupGridAvatar = groupGridAvatar;
+        }
+
+        public String getImageId() {
+            return imageId;
+        }
+
+        public void setImageId(String imageId) {
+            this.imageId = imageId;
+        }
+    }
+}

+ 241 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/activities/SelectionActivity.java

@@ -0,0 +1,241 @@
+package com.tencent.qcloud.tuikit.timcommon.component.activities;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.InputFilter;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.DividerItemDecoration;
+import androidx.recyclerview.widget.RecyclerView;
+import com.tencent.qcloud.tuikit.timcommon.R;
+import com.tencent.qcloud.tuikit.timcommon.component.CustomLinearLayoutManager;
+import com.tencent.qcloud.tuikit.timcommon.component.TitleBarLayout;
+import com.tencent.qcloud.tuikit.timcommon.component.interfaces.ITitleBarLayout;
+import java.util.ArrayList;
+
+public class SelectionActivity extends BaseLightActivity {
+    private static OnResultReturnListener sOnResultReturnListener;
+
+    private RecyclerView selectListView;
+    private SelectAdapter selectListAdapter;
+    private EditText input;
+    private int mSelectionType;
+    private ArrayList<String> selectList = new ArrayList<>();
+    private int selectedItem = -1;
+    private OnItemClickListener onItemClickListener;
+    private boolean needConfirm = true;
+    private boolean returnNow = true;
+
+    public static void startTextSelection(Context context, Bundle bundle, OnResultReturnListener listener) {
+        bundle.putInt(Selection.TYPE, Selection.TYPE_TEXT);
+        startSelection(context, bundle, listener);
+    }
+
+    public static void startListSelection(Context context, Bundle bundle, OnResultReturnListener listener) {
+        bundle.putInt(Selection.TYPE, Selection.TYPE_LIST);
+        startSelection(context, bundle, listener);
+    }
+
+    private static void startSelection(Context context, Bundle bundle, OnResultReturnListener listener) {
+        Intent intent = new Intent(context, SelectionActivity.class);
+        intent.putExtra(Selection.CONTENT, bundle);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        context.startActivity(intent);
+        sOnResultReturnListener = listener;
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.tuicore_selection_activity);
+        final TitleBarLayout titleBar = findViewById(R.id.edit_title_bar);
+        selectListView = findViewById(R.id.select_list);
+        selectListAdapter = new SelectAdapter();
+        selectListView.setAdapter(selectListAdapter);
+        selectListView.setLayoutManager(new CustomLinearLayoutManager(this));
+        DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(this, DividerItemDecoration.VERTICAL);
+        dividerItemDecoration.setDrawable(getResources().getDrawable(R.drawable.core_list_divider));
+        selectListView.addItemDecoration(dividerItemDecoration);
+        onItemClickListener = new OnItemClickListener() {
+            @Override
+            public void onClick(int position) {
+                selectedItem = position;
+                selectListAdapter.setSelectedItem(position);
+                selectListAdapter.notifyDataSetChanged();
+                if (!needConfirm) {
+                    echoClick();
+                }
+            }
+        };
+        input = findViewById(R.id.edit_content_et);
+
+        Bundle bundle = getIntent().getBundleExtra(Selection.CONTENT);
+        switch (bundle.getInt(Selection.TYPE)) {
+            case Selection.TYPE_TEXT:
+                selectListView.setVisibility(View.GONE);
+                String defaultString = bundle.getString(Selection.INIT_CONTENT);
+                int limit = bundle.getInt(Selection.LIMIT);
+                if (!TextUtils.isEmpty(defaultString)) {
+                    input.setText(defaultString);
+                    input.setSelection(defaultString.length());
+                }
+                if (limit > 0) {
+                    input.setFilters(new InputFilter[] {new InputFilter.LengthFilter(limit)});
+                }
+                break;
+            case Selection.TYPE_LIST:
+                input.setVisibility(View.GONE);
+                ArrayList<String> list = bundle.getStringArrayList(Selection.LIST);
+                selectedItem = bundle.getInt(Selection.DEFAULT_SELECT_ITEM_INDEX);
+                if (list == null || list.size() == 0) {
+                    return;
+                }
+                selectList.clear();
+                selectList.addAll(list);
+                selectListAdapter.setSelectedItem(selectedItem);
+                selectListAdapter.setData(selectList);
+                selectListAdapter.notifyDataSetChanged();
+
+                break;
+            default:
+                finish();
+                return;
+        }
+        mSelectionType = bundle.getInt(Selection.TYPE);
+
+        final String title = bundle.getString(Selection.TITLE);
+
+        needConfirm = bundle.getBoolean(Selection.NEED_CONFIRM, true);
+        returnNow = bundle.getBoolean(Selection.RETURN_NOW, true);
+
+        titleBar.setTitle(title, ITitleBarLayout.Position.MIDDLE);
+        titleBar.setOnLeftClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                finish();
+            }
+        });
+        titleBar.getRightIcon().setVisibility(View.GONE);
+        if (needConfirm) {
+            titleBar.getRightTitle().setText(getResources().getString(com.tencent.qcloud.tuicore.R.string.sure));
+            titleBar.setOnRightClickListener(new View.OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    echoClick();
+                }
+            });
+        } else {
+            titleBar.getRightGroup().setVisibility(View.GONE);
+        }
+    }
+
+    private void echoClick() {
+        switch (mSelectionType) {
+            case Selection.TYPE_TEXT:
+                if (sOnResultReturnListener != null) {
+                    sOnResultReturnListener.onReturn(input.getText().toString());
+                }
+                break;
+            case Selection.TYPE_LIST:
+                if (sOnResultReturnListener != null) {
+                    sOnResultReturnListener.onReturn(selectedItem);
+                }
+                break;
+            default:
+                break;
+        }
+        if (returnNow) {
+            finish();
+        }
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+        sOnResultReturnListener = null;
+    }
+
+    class SelectAdapter extends RecyclerView.Adapter<SelectAdapter.SelectViewHolder> {
+        int selectedItem = -1;
+        ArrayList<String> data = new ArrayList<>();
+
+        public void setData(ArrayList<String> data) {
+            this.data.clear();
+            this.data.addAll(data);
+        }
+
+        public void setSelectedItem(int selectedItem) {
+            this.selectedItem = selectedItem;
+        }
+
+        @NonNull
+        @Override
+        public SelectViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+            View view = LayoutInflater.from(SelectionActivity.this).inflate(R.layout.core_select_item_layout, parent, false);
+            return new SelectViewHolder(view);
+        }
+
+        @Override
+        public void onBindViewHolder(@NonNull SelectViewHolder holder, int position) {
+            String nameStr = data.get(position);
+            holder.name.setText(nameStr);
+            if (selectedItem == position) {
+                holder.selectedIcon.setVisibility(View.VISIBLE);
+            } else {
+                holder.selectedIcon.setVisibility(View.GONE);
+            }
+            holder.itemView.setOnClickListener(new View.OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    onItemClickListener.onClick(position);
+                }
+            });
+        }
+
+        @Override
+        public int getItemCount() {
+            return data.size();
+        }
+
+        class SelectViewHolder extends RecyclerView.ViewHolder {
+            TextView name;
+            ImageView selectedIcon;
+
+            public SelectViewHolder(@NonNull View itemView) {
+                super(itemView);
+                name = itemView.findViewById(R.id.name);
+                selectedIcon = itemView.findViewById(R.id.selected_icon);
+            }
+        }
+    }
+
+    public interface OnResultReturnListener {
+        void onReturn(Object res);
+    }
+
+    public interface OnItemClickListener {
+        void onClick(int position);
+    }
+
+    public static class Selection {
+        public static final String SELECT_ALL = "select_all";
+        public static final String CONTENT = "content";
+        public static final String TYPE = "type";
+        public static final String TITLE = "title";
+        public static final String INIT_CONTENT = "init_content";
+        public static final String DEFAULT_SELECT_ITEM_INDEX = "default_select_item_index";
+        public static final String LIST = "list";
+        public static final String LIMIT = "limit";
+        public static final String NEED_CONFIRM = "needConfirm";
+        public static final String RETURN_NOW = "returnNow";
+        public static final int TYPE_TEXT = 1;
+        public static final int TYPE_LIST = 2;
+    }
+}

+ 241 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/activities/SelectionMinimalistActivity.java

@@ -0,0 +1,241 @@
+package com.tencent.qcloud.tuikit.timcommon.component.activities;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.InputFilter;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.DividerItemDecoration;
+import androidx.recyclerview.widget.RecyclerView;
+import com.tencent.qcloud.tuikit.timcommon.R;
+import com.tencent.qcloud.tuikit.timcommon.component.CustomLinearLayoutManager;
+import com.tencent.qcloud.tuikit.timcommon.component.TitleBarLayout;
+import com.tencent.qcloud.tuikit.timcommon.component.interfaces.ITitleBarLayout;
+import java.util.ArrayList;
+
+public class SelectionMinimalistActivity extends BaseMinimalistLightActivity {
+    private static OnResultReturnListener sOnResultReturnListener;
+
+    private RecyclerView selectListView;
+    private SelectAdapter selectListAdapter;
+    private EditText input;
+    private int mSelectionType;
+    private ArrayList<String> selectList = new ArrayList<>();
+    private int selectedItem = -1;
+    private OnItemClickListener onItemClickListener;
+    private boolean needConfirm = true;
+    private boolean returnNow = true;
+
+    public static void startTextSelection(Context context, Bundle bundle, OnResultReturnListener listener) {
+        bundle.putInt(Selection.TYPE, Selection.TYPE_TEXT);
+        startSelection(context, bundle, listener);
+    }
+
+    public static void startListSelection(Context context, Bundle bundle, OnResultReturnListener listener) {
+        bundle.putInt(Selection.TYPE, Selection.TYPE_LIST);
+        startSelection(context, bundle, listener);
+    }
+
+    private static void startSelection(Context context, Bundle bundle, OnResultReturnListener listener) {
+        Intent intent = new Intent(context, SelectionMinimalistActivity.class);
+        intent.putExtra(Selection.CONTENT, bundle);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        context.startActivity(intent);
+        sOnResultReturnListener = listener;
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.core_minimalist_selection_activity);
+        final TitleBarLayout titleBar = findViewById(R.id.edit_title_bar);
+        selectListView = findViewById(R.id.select_list);
+        selectListAdapter = new SelectAdapter();
+        selectListView.setAdapter(selectListAdapter);
+        selectListView.setLayoutManager(new CustomLinearLayoutManager(this));
+        DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(this, DividerItemDecoration.VERTICAL);
+        dividerItemDecoration.setDrawable(getResources().getDrawable(R.drawable.core_list_divider));
+        selectListView.addItemDecoration(dividerItemDecoration);
+        onItemClickListener = new OnItemClickListener() {
+            @Override
+            public void onClick(int position) {
+                selectedItem = position;
+                selectListAdapter.setSelectedItem(position);
+                selectListAdapter.notifyDataSetChanged();
+                if (!needConfirm) {
+                    echoClick();
+                }
+            }
+        };
+        input = findViewById(R.id.edit_content_et);
+
+        Bundle bundle = getIntent().getBundleExtra(Selection.CONTENT);
+        switch (bundle.getInt(Selection.TYPE)) {
+            case Selection.TYPE_TEXT:
+                selectListView.setVisibility(View.GONE);
+                String defaultString = bundle.getString(Selection.INIT_CONTENT);
+                int limit = bundle.getInt(Selection.LIMIT);
+                if (!TextUtils.isEmpty(defaultString)) {
+                    input.setText(defaultString);
+                    input.setSelection(defaultString.length());
+                }
+                if (limit > 0) {
+                    input.setFilters(new InputFilter[] {new InputFilter.LengthFilter(limit)});
+                }
+                break;
+            case Selection.TYPE_LIST:
+                input.setVisibility(View.GONE);
+                ArrayList<String> list = bundle.getStringArrayList(Selection.LIST);
+                selectedItem = bundle.getInt(Selection.DEFAULT_SELECT_ITEM_INDEX);
+                if (list == null || list.size() == 0) {
+                    return;
+                }
+                selectList.clear();
+                selectList.addAll(list);
+                selectListAdapter.setSelectedItem(selectedItem);
+                selectListAdapter.setData(selectList);
+                selectListAdapter.notifyDataSetChanged();
+
+                break;
+            default:
+                finish();
+                return;
+        }
+        mSelectionType = bundle.getInt(Selection.TYPE);
+
+        final String title = bundle.getString(Selection.TITLE);
+
+        needConfirm = bundle.getBoolean(Selection.NEED_CONFIRM, true);
+        returnNow = bundle.getBoolean(Selection.RETURN_NOW, true);
+
+        titleBar.setTitle(title, ITitleBarLayout.Position.MIDDLE);
+        titleBar.setOnLeftClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                finish();
+            }
+        });
+        titleBar.getRightIcon().setVisibility(View.GONE);
+        if (needConfirm) {
+            titleBar.getRightTitle().setText(getResources().getString(com.tencent.qcloud.tuicore.R.string.sure));
+            titleBar.setOnRightClickListener(new View.OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    echoClick();
+                }
+            });
+        } else {
+            titleBar.getRightGroup().setVisibility(View.GONE);
+        }
+    }
+
+    private void echoClick() {
+        switch (mSelectionType) {
+            case Selection.TYPE_TEXT:
+                if (sOnResultReturnListener != null) {
+                    sOnResultReturnListener.onReturn(input.getText().toString());
+                }
+                break;
+            case Selection.TYPE_LIST:
+                if (sOnResultReturnListener != null) {
+                    sOnResultReturnListener.onReturn(selectedItem);
+                }
+                break;
+            default:
+                break;
+        }
+        if (returnNow) {
+            finish();
+        }
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+        sOnResultReturnListener = null;
+    }
+
+    class SelectAdapter extends RecyclerView.Adapter<SelectAdapter.SelectViewHolder> {
+        int selectedItem = -1;
+        ArrayList<String> data = new ArrayList<>();
+
+        public void setData(ArrayList<String> data) {
+            this.data.clear();
+            this.data.addAll(data);
+        }
+
+        public void setSelectedItem(int selectedItem) {
+            this.selectedItem = selectedItem;
+        }
+
+        @NonNull
+        @Override
+        public SelectViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+            View view = LayoutInflater.from(SelectionMinimalistActivity.this).inflate(R.layout.core_select_item_layout, parent, false);
+            return new SelectViewHolder(view);
+        }
+
+        @Override
+        public void onBindViewHolder(@NonNull SelectViewHolder holder, int position) {
+            String nameStr = data.get(position);
+            holder.name.setText(nameStr);
+            if (selectedItem == position) {
+                holder.selectedIcon.setVisibility(View.VISIBLE);
+            } else {
+                holder.selectedIcon.setVisibility(View.GONE);
+            }
+            holder.itemView.setOnClickListener(new View.OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    onItemClickListener.onClick(position);
+                }
+            });
+        }
+
+        @Override
+        public int getItemCount() {
+            return data.size();
+        }
+
+        class SelectViewHolder extends RecyclerView.ViewHolder {
+            TextView name;
+            ImageView selectedIcon;
+
+            public SelectViewHolder(@NonNull View itemView) {
+                super(itemView);
+                name = itemView.findViewById(R.id.name);
+                selectedIcon = itemView.findViewById(R.id.selected_icon);
+            }
+        }
+    }
+
+    public interface OnResultReturnListener {
+        void onReturn(Object res);
+    }
+
+    public interface OnItemClickListener {
+        void onClick(int position);
+    }
+
+    public static class Selection {
+        public static final String SELECT_ALL = "select_all";
+        public static final String CONTENT = "content";
+        public static final String TYPE = "type";
+        public static final String TITLE = "title";
+        public static final String INIT_CONTENT = "init_content";
+        public static final String DEFAULT_SELECT_ITEM_INDEX = "default_select_item_index";
+        public static final String LIST = "list";
+        public static final String LIMIT = "limit";
+        public static final String NEED_CONFIRM = "needConfirm";
+        public static final String RETURN_NOW = "returnNow";
+        public static final int TYPE_TEXT = 1;
+        public static final int TYPE_LIST = 2;
+    }
+}

+ 199 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/dialog/MinimalistToast.java

@@ -0,0 +1,199 @@
+package com.tencent.qcloud.tuikit.timcommon.component.dialog;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.GradientDrawable;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+public class MinimalistToast {
+    
+    public static final int TYPE_SUCCESS = 1;
+    public static final int TYPE_ERROR = 2;
+    public static final int TYPE_WARNING = 3;
+    public static final int TYPE_INFO = 4;
+    
+    private static final int DURATION_SHORT = 2000;
+    private static final int DURATION_LONG = 3500;
+    
+    private Context mContext;
+    private Dialog mDialog;
+    private Handler mHandler;
+    
+    private MinimalistToast(Context context) {
+        mContext = context;
+        mHandler = new Handler(Looper.getMainLooper());
+    }
+    
+    public static void show(Context context, String message) {
+        show(context, message, TYPE_INFO, DURATION_SHORT);
+    }
+    
+    public static void show(Context context, String message, int type) {
+        show(context, message, type, DURATION_SHORT);
+    }
+    
+    public static void show(Context context, String message, int type, int duration) {
+        new MinimalistToast(context).showToast(message, type, duration);
+    }
+    
+    public static void showSuccess(Context context, String message) {
+        show(context, message, TYPE_SUCCESS);
+    }
+    
+    public static void showError(Context context, String message) {
+        show(context, message, TYPE_ERROR);
+    }
+    
+    public static void showWarning(Context context, String message) {
+        show(context, message, TYPE_WARNING);
+    }
+    
+    public static void showInfo(Context context, String message) {
+        show(context, message, TYPE_INFO);
+    }
+    
+    private void showToast(String message, int type, int duration) {
+        if (mDialog != null && mDialog.isShowing()) {
+            mDialog.dismiss();
+        }
+        
+        View toastView = createToastView(message, type);
+        
+        mDialog = new Dialog(mContext);
+        mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
+        mDialog.setContentView(toastView);
+        mDialog.setCancelable(true);
+        mDialog.setCanceledOnTouchOutside(true);
+        
+        Window window = mDialog.getWindow();
+        if (window != null) {
+            window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
+            window.setGravity(Gravity.CENTER);
+            window.setDimAmount(0f);
+            window.setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, 
+                           WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
+            WindowManager.LayoutParams params = window.getAttributes();
+            params.width = WindowManager.LayoutParams.WRAP_CONTENT;
+            params.height = WindowManager.LayoutParams.WRAP_CONTENT;
+            window.setAttributes(params);
+        }
+        
+        mDialog.show();
+        
+        mHandler.postDelayed(() -> {
+            if (mDialog != null && mDialog.isShowing()) {
+                mDialog.dismiss();
+            }
+        }, duration);
+    }
+    
+    private View createToastView(String message, int type) {
+        FrameLayout container = new FrameLayout(mContext);
+        FrameLayout.LayoutParams containerParams = new FrameLayout.LayoutParams(
+            ViewGroup.LayoutParams.WRAP_CONTENT,
+            ViewGroup.LayoutParams.WRAP_CONTENT
+        );
+        container.setLayoutParams(containerParams);
+        
+        // Create multiple shadow layers for better effect
+        for (int i = 4; i >= 1; i--) {
+            FrameLayout shadowLayer = new FrameLayout(mContext);
+            FrameLayout.LayoutParams shadowParams = new FrameLayout.LayoutParams(
+                ViewGroup.LayoutParams.WRAP_CONTENT,
+                ViewGroup.LayoutParams.WRAP_CONTENT
+            );
+            shadowParams.setMargins(dp2px(i), dp2px(i), 0, 0);
+            shadowLayer.setLayoutParams(shadowParams);
+            
+            GradientDrawable shadowBg = new GradientDrawable();
+            shadowBg.setShape(GradientDrawable.RECTANGLE);
+            shadowBg.setCornerRadius(dp2px(6));
+            int alpha = (int) (255 * 0.3f / i); // Increased opacity for more visible shadow
+            shadowBg.setColor(Color.argb(alpha, 0, 0, 0));
+            shadowLayer.setBackground(shadowBg);
+            
+            container.addView(shadowLayer);
+        }
+        
+        FrameLayout contentLayout = new FrameLayout(mContext);
+        FrameLayout.LayoutParams contentParams = new FrameLayout.LayoutParams(
+            ViewGroup.LayoutParams.WRAP_CONTENT,
+            ViewGroup.LayoutParams.WRAP_CONTENT
+        );
+        contentLayout.setLayoutParams(contentParams);
+        
+        GradientDrawable background = new GradientDrawable();
+        background.setShape(GradientDrawable.RECTANGLE);
+        background.setCornerRadius(dp2px(6));
+        background.setColor(Color.parseColor("#EBF3FF"));
+        contentLayout.setBackground(background);
+        
+        if (hasIcon(type)) {
+            ImageView iconView = new ImageView(mContext);
+            FrameLayout.LayoutParams iconParams = new FrameLayout.LayoutParams(
+                dp2px(16), dp2px(16)
+            );
+            iconParams.gravity = Gravity.CENTER_VERTICAL | Gravity.START;
+            iconParams.setMargins(dp2px(16), 0, 0, 0);
+            iconView.setLayoutParams(iconParams);
+            iconView.setImageResource(getIconResource(type));
+            contentLayout.addView(iconView);
+        }
+        
+        TextView textView = new TextView(mContext);
+        FrameLayout.LayoutParams textParams = new FrameLayout.LayoutParams(
+            ViewGroup.LayoutParams.WRAP_CONTENT,
+            ViewGroup.LayoutParams.WRAP_CONTENT
+        );
+        textParams.gravity = Gravity.CENTER;
+        if (hasIcon(type)) {
+            textParams.setMargins(dp2px(48), dp2px(8), dp2px(16), dp2px(8));
+        } else {
+            textParams.setMargins(dp2px(16), dp2px(8), dp2px(16), dp2px(8));
+        }
+        textView.setLayoutParams(textParams);
+        textView.setText(message);
+        textView.setTextColor(Color.BLACK);
+        textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
+        textView.setMaxLines(2);
+        textView.setGravity(Gravity.CENTER);
+        contentLayout.addView(textView);
+        
+        container.addView(contentLayout);
+        return container;
+    }
+    
+    private boolean hasIcon(int type) {
+        return type != TYPE_INFO;
+    }
+    
+    private int getIconResource(int type) {
+        switch (type) {
+            case TYPE_SUCCESS:
+                return android.R.drawable.ic_menu_info_details;
+            case TYPE_ERROR:
+                return android.R.drawable.ic_dialog_alert;
+            case TYPE_WARNING:
+                return android.R.drawable.ic_dialog_info;
+            default:
+                return 0;
+        }
+    }
+    
+    private int dp2px(float dp) {
+        float density = mContext.getResources().getDisplayMetrics().density;
+        return (int) (dp * density + 0.5f);
+    }
+}

+ 329 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/dialog/TUIKitDialog.java

@@ -0,0 +1,329 @@
+package com.tencent.qcloud.tuikit.timcommon.component.dialog;
+
+import static com.tencent.qcloud.tuicore.TUIConfig.TUICORE_SETTINGS_SP_NAME;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.text.method.MovementMethod;
+import android.view.Display;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.LinearLayout.LayoutParams;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+
+import com.tencent.qcloud.tuicore.TUIConfig;
+import com.tencent.qcloud.tuicore.util.SPUtils;
+import com.tencent.qcloud.tuikit.timcommon.BuildConfig;
+import com.tencent.qcloud.tuikit.timcommon.R;
+
+import java.lang.ref.WeakReference;
+
+public class TUIKitDialog {
+    private Context mContext;
+    protected Dialog dialog;
+    private LinearLayout mBackgroundLayout;
+    private LinearLayout mMainLayout;
+    protected TextView mTitleTv;
+    private Button mCancelButton;
+    private Button mSureButton;
+    private ImageView mLineImg;
+    private Display mDisplay;
+
+    private boolean showTitle = false;
+    private boolean showPosBtn = false;
+    private boolean showNegBtn = false;
+
+    private float dialogWidth = 0.7f;
+
+    public TUIKitDialog(Context context) {
+        this.mContext = context;
+        WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+        mDisplay = windowManager.getDefaultDisplay();
+    }
+
+    public TUIKitDialog builder() {
+        View view = LayoutInflater.from(mContext).inflate(R.layout.common_dialog_view_layout, null);
+        mBackgroundLayout = view.findViewById(R.id.ll_background);
+        mMainLayout = view.findViewById(R.id.ll_alert);
+        mMainLayout.setVerticalGravity(View.GONE);
+        mTitleTv = view.findViewById(R.id.tv_title);
+        mTitleTv.setVisibility(View.GONE);
+        mCancelButton = view.findViewById(R.id.btn_neg);
+        mCancelButton.setVisibility(View.GONE);
+        mSureButton = view.findViewById(R.id.btn_pos);
+        mSureButton.setVisibility(View.GONE);
+        mLineImg = view.findViewById(R.id.img_line);
+        mLineImg.setVisibility(View.GONE);
+
+        dialog = new Dialog(mContext, R.style.TUIKit_AlertDialogStyle);
+        dialog.setContentView(view);
+
+        mBackgroundLayout.setLayoutParams(new FrameLayout.LayoutParams((int) (mDisplay.getWidth() * dialogWidth), LayoutParams.WRAP_CONTENT));
+        return this;
+    }
+
+    public TUIKitDialog setTitle(@NonNull CharSequence title) {
+        showTitle = true;
+        mTitleTv.setText(title);
+        return this;
+    }
+
+    /***
+     * Whether to click back to cancel
+     * @param cancel
+     * @return
+     */
+    public TUIKitDialog setCancelable(boolean cancel) {
+        dialog.setCancelable(cancel);
+        return this;
+    }
+
+    /**
+     * Whether the setting can be canceled
+     *
+     * @param isCancelOutside
+     * @return
+     */
+    public TUIKitDialog setCancelOutside(boolean isCancelOutside) {
+        dialog.setCanceledOnTouchOutside(isCancelOutside);
+        return this;
+    }
+
+    public TUIKitDialog setPositiveButton(final OnClickListener listener) {
+        setPositiveButton(TUIConfig.getAppContext().getString(com.tencent.qcloud.tuicore.R.string.sure), listener);
+        return this;
+    }
+
+    public TUIKitDialog setPositiveButton(CharSequence text, final OnClickListener listener) {
+        showPosBtn = true;
+        mSureButton.setText(text);
+        mSureButton.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                listener.onClick(v);
+                dialog.dismiss();
+            }
+        });
+        return this;
+    }
+
+    public void setTitleGravity(int gravity) {
+        mTitleTv.setGravity(gravity);
+    }
+
+    public TUIKitDialog setNegativeButton(CharSequence text, final OnClickListener listener) {
+        showNegBtn = true;
+        mCancelButton.setText(text);
+        mCancelButton.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                listener.onClick(v);
+                dialog.dismiss();
+            }
+        });
+        return this;
+    }
+
+    public TUIKitDialog setNegativeButton(final OnClickListener listener) {
+        setNegativeButton(TUIConfig.getAppContext().getString(com.tencent.qcloud.tuicore.R.string.cancel), listener);
+        return this;
+    }
+
+    private void setLayout() {
+        if (!showTitle) {
+            mTitleTv.setVisibility(View.GONE);
+        }
+
+        if (showTitle) {
+            mTitleTv.setVisibility(View.VISIBLE);
+        }
+
+        if (!showPosBtn && !showNegBtn) {
+            mSureButton.setVisibility(View.GONE);
+            mSureButton.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    dialog.dismiss();
+                }
+            });
+        }
+
+        if (showPosBtn && showNegBtn) {
+            mSureButton.setVisibility(View.VISIBLE);
+            mCancelButton.setVisibility(View.VISIBLE);
+            mLineImg.setVisibility(View.VISIBLE);
+        }
+
+        if (showPosBtn && !showNegBtn) {
+            mSureButton.setVisibility(View.VISIBLE);
+        }
+
+        if (!showPosBtn && showNegBtn) {
+            mCancelButton.setVisibility(View.VISIBLE);
+        }
+    }
+
+    public void show() {
+        setLayout();
+        dialog.show();
+    }
+
+    public void dismiss() {
+        if (dialog != null && dialog.isShowing()) {
+            dialog.dismiss();
+        }
+    }
+
+    public boolean isShowing() {
+        return dialog != null && dialog.isShowing();
+    }
+
+    /**
+     *
+     * @param dialogWidth
+     * @return
+     */
+    public TUIKitDialog setDialogWidth(float dialogWidth) {
+        if (mBackgroundLayout != null) {
+            mBackgroundLayout.setLayoutParams(new FrameLayout.LayoutParams((int) (mDisplay.getWidth() * dialogWidth), LayoutParams.WRAP_CONTENT));
+        }
+        this.dialogWidth = dialogWidth;
+        return this;
+    }
+
+    public static class TUIIMUpdateDialog {
+        private static final class TUIIMUpdateDialogHolder {
+            private static final TUIIMUpdateDialog instance = new TUIIMUpdateDialog();
+        }
+
+        public static final String KEY_NEVER_SHOW = "neverShow";
+
+        private boolean isNeverShow;
+        private boolean isShowOnlyDebug = false;
+        private String dialogFeatureName;
+
+        private WeakReference<TUIKitDialog> tuiKitDialog;
+
+        public static TUIIMUpdateDialog getInstance() {
+            return TUIIMUpdateDialogHolder.instance;
+        }
+
+        private TUIIMUpdateDialog() {
+            isNeverShow = SPUtils.getInstance(TUICORE_SETTINGS_SP_NAME).getBoolean(getDialogFeatureName(), false);
+        }
+
+        public TUIIMUpdateDialog createDialog(Context context) {
+            tuiKitDialog = new WeakReference<>(new TUIKitDialog(context));
+            tuiKitDialog.get().builder();
+            return this;
+        }
+
+        public void setNeverShow(boolean neverShowAlert) {
+            this.isNeverShow = neverShowAlert;
+            SPUtils.getInstance(TUICORE_SETTINGS_SP_NAME).put(getDialogFeatureName(), neverShowAlert);
+        }
+
+        public TUIIMUpdateDialog setShowOnlyDebug(boolean isShowOnlyDebug) {
+            this.isShowOnlyDebug = isShowOnlyDebug;
+            return this;
+        }
+
+        public TUIIMUpdateDialog setMovementMethod(MovementMethod movementMethod) {
+            if (tuiKitDialog != null && tuiKitDialog.get() != null) {
+                tuiKitDialog.get().mTitleTv.setMovementMethod(movementMethod);
+            }
+            return this;
+        }
+
+        public TUIIMUpdateDialog setHighlightColor(int color) {
+            if (tuiKitDialog != null && tuiKitDialog.get() != null) {
+                tuiKitDialog.get().mTitleTv.setHighlightColor(color);
+            }
+            return this;
+        }
+
+        public TUIIMUpdateDialog setCancelable(boolean cancelable) {
+            if (tuiKitDialog != null && tuiKitDialog.get() != null) {
+                tuiKitDialog.get().setCancelable(cancelable);
+            }
+            return this;
+        }
+
+        public TUIIMUpdateDialog setCancelOutside(boolean cancelOutside) {
+            if (tuiKitDialog != null && tuiKitDialog.get() != null) {
+                tuiKitDialog.get().setCancelOutside(cancelOutside);
+            }
+            return this;
+        }
+
+        public TUIIMUpdateDialog setTitle(CharSequence charSequence) {
+            if (tuiKitDialog != null && tuiKitDialog.get() != null) {
+                tuiKitDialog.get().setTitle(charSequence);
+            }
+            return this;
+        }
+
+        public TUIIMUpdateDialog setDialogWidth(float dialogWidth) {
+            if (tuiKitDialog != null && tuiKitDialog.get() != null) {
+                tuiKitDialog.get().setDialogWidth(dialogWidth);
+            }
+            return this;
+        }
+
+        public TUIIMUpdateDialog setPositiveButton(CharSequence text, OnClickListener clickListener) {
+            if (tuiKitDialog != null && tuiKitDialog.get() != null) {
+                tuiKitDialog.get().setPositiveButton(text, clickListener);
+            }
+            return this;
+        }
+
+        public TUIIMUpdateDialog setNegativeButton(CharSequence text, OnClickListener clickListener) {
+            if (tuiKitDialog != null && tuiKitDialog.get() != null) {
+                tuiKitDialog.get().setNegativeButton(text, clickListener);
+            }
+            return this;
+        }
+
+        public TUIIMUpdateDialog setDialogFeatureName(String featureName) {
+            this.dialogFeatureName = featureName;
+            return this;
+        }
+
+        private String getDialogFeatureName() {
+            return dialogFeatureName;
+        }
+
+        public void show() {
+            if (tuiKitDialog == null || tuiKitDialog.get() == null) {
+                return;
+            }
+            isNeverShow = SPUtils.getInstance(TUICORE_SETTINGS_SP_NAME).getBoolean(getDialogFeatureName(), false);
+            Dialog dialog = tuiKitDialog.get().dialog;
+            if (dialog == null || dialog.isShowing()) {
+                return;
+            }
+            if (isNeverShow) {
+                return;
+            }
+            if (isShowOnlyDebug && !BuildConfig.DEBUG) {
+                return;
+            }
+            tuiKitDialog.get().show();
+        }
+
+        public void dismiss() {
+            if (tuiKitDialog == null || tuiKitDialog.get() == null) {
+                return;
+            }
+            tuiKitDialog.get().dismiss();
+        }
+    }
+}

+ 51 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/face/CenterImageSpan.java

@@ -0,0 +1,51 @@
+package com.tencent.qcloud.tuikit.timcommon.component.face;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.text.style.ImageSpan;
+
+import androidx.annotation.NonNull;
+
+public class CenterImageSpan extends ImageSpan {
+
+    private int bgColor = -1;
+
+    public CenterImageSpan(@NonNull Drawable drawable) {
+        super(drawable);
+    }
+
+    public void setBgColor(int bgColor) {
+        this.bgColor = bgColor;
+    }
+
+    @Override
+    public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
+        Drawable drawable = getDrawable();
+        Rect rect = drawable.getBounds();
+        Paint.FontMetricsInt paintFm = paint.getFontMetricsInt();
+        int center = (paintFm.top + paintFm.bottom) / 2;
+        if (fm != null) {
+            fm.ascent = center - (rect.height() / 2);
+            fm.descent = center + (rect.height() / 2);
+            fm.top = fm.ascent;
+            fm.bottom = fm.descent;
+        }
+
+        return rect.right;
+    }
+
+    @Override
+    public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) {
+        if (bgColor == -1) {
+            super.draw(canvas, text, start, end, x, top, y, bottom, paint);
+        } else {
+            canvas.save();
+            paint.setColor(bgColor);
+            canvas.drawRect(x, top, getDrawable().getBounds().right + x, bottom, paint);
+            canvas.restore();
+            super.draw(canvas, text, start, end, x, top, y, bottom, paint);
+        }
+    }
+}

+ 546 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/face/FaceManager.java

@@ -0,0 +1,546 @@
+package com.tencent.qcloud.tuikit.timcommon.component.face;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.text.Editable;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+import android.text.style.ImageSpan;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.TextView;
+import androidx.annotation.Nullable;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.load.DataSource;
+import com.bumptech.glide.load.engine.GlideException;
+import com.bumptech.glide.request.RequestListener;
+import com.bumptech.glide.request.RequestOptions;
+import com.bumptech.glide.request.target.Target;
+import com.tencent.qcloud.tuikit.timcommon.R;
+import com.tencent.qcloud.tuikit.timcommon.TIMCommonService;
+import com.tencent.qcloud.tuikit.timcommon.bean.ChatFace;
+import com.tencent.qcloud.tuikit.timcommon.bean.Emoji;
+import com.tencent.qcloud.tuikit.timcommon.bean.FaceGroup;
+import com.tencent.qcloud.tuikit.timcommon.util.TIMCommonLog;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class FaceManager {
+    private static final String TAG = "FaceManager";
+
+    private static final class FaceManagerHolder {
+        @SuppressLint("StaticFieldLeak") private static final FaceManager instance = new FaceManager();
+    }
+
+    public static final int EMOJI_GROUP_ID = 0;
+    public static final int EMOJI_COLUMN_COUNT = 8;
+    public static final int EMOJI_ROW_COUNT = 3;
+
+    private final Map<String, Emoji> emojiMap = new LinkedHashMap<>();
+    private final Context context;
+
+    private final Map<Integer, FaceGroup<?>> faceGroupMap = new ConcurrentHashMap<>();
+
+    private FaceManager() {
+        context = TIMCommonService.getAppContext();
+    }
+
+    private static FaceManager getInstance() {
+        return FaceManagerHolder.instance;
+    }
+
+    public static ArrayList<Emoji> getEmojiList() {
+        return new ArrayList<>(getInstance().emojiMap.values());
+    }
+
+    public static Map<String, Emoji> getEmojiMap() {
+        return Collections.unmodifiableMap(getInstance().emojiMap);
+    }
+
+    public static int getEmojiCount() {
+        return getInstance().emojiMap.size();
+    }
+
+    /**
+     * add a new faceGroup
+     * @param groupID must >= 1
+     * @param faceGroup the faceGroup be added
+     */
+    public static synchronized <T extends ChatFace> void addFaceGroup(int groupID, FaceGroup<T> faceGroup) {
+        faceGroup.setGroupID(groupID);
+        getInstance().faceGroupMap.put(groupID, faceGroup);
+        if (faceGroup.isEmojiGroup()) {
+            List<T> faces = faceGroup.getFaces();
+            for (T face : faces) {
+                getInstance().emojiMap.put(face.getFaceKey(), (Emoji) face);
+            }
+        }
+    }
+
+    public static List<FaceGroup> getFaceGroupList() {
+        return new ArrayList<>(getInstance().faceGroupMap.values());
+    }
+
+    public static Emoji loadAssetEmoji(String emojiKey, String assetFilePath, int size) {
+        String realPath = "file:///android_asset/" + assetFilePath;
+        Bitmap bitmap = loadBitmap(realPath, size, size);
+        if (bitmap == null) {
+            TIMCommonLog.e(TAG, "load bitmap failed : " + realPath);
+            return null;
+        }
+        Emoji emoji = new Emoji();
+        emoji.setIcon(bitmap);
+        emoji.setFaceKey(emojiKey);
+        return emoji;
+    }
+
+    private static Bitmap loadBitmap(String resUrl, int width, int height) {
+        Bitmap bitmap = null;
+        try {
+            bitmap = Glide.with(TIMCommonService.getAppContext())
+                         .asBitmap()
+                         .load(resUrl)
+                         .apply(new RequestOptions().error(android.R.drawable.ic_menu_report_image))
+                         .submit(width, height)
+                         .get();
+        } catch (InterruptedException | ExecutionException e) {
+            TIMCommonLog.e(TAG, "load bitmap failed : " + e.getMessage());
+        }
+        return bitmap;
+    }
+
+    public static void loadFace(ChatFace chatFace, ImageView imageView) {
+        getInstance().internalLoadFace(chatFace, imageView, true);
+    }
+
+    public static void loadFace(int faceGroupID, String faceKey, ImageView view) {
+        getInstance().internalLoadFace(faceGroupID, faceKey, view);
+    }
+
+    private void internalLoadFace(int faceGroupID, String faceKey, ImageView imageView) {
+        if (imageView == null) {
+            return;
+        }
+        if (TextUtils.isEmpty(faceKey)) {
+            Glide.with(TIMCommonService.getAppContext()).load(android.R.drawable.ic_menu_report_image).centerInside().into(imageView);
+            return;
+        }
+        String faceUrl = "";
+        FaceGroup faceGroup = faceGroupMap.get(faceGroupID);
+        ChatFace face = null;
+        if (faceGroup != null) {
+            face = faceGroup.getFace(faceKey);
+            if (face != null) {
+                faceUrl = face.getFaceUrl();
+            }
+        }
+        final ChatFace finalFace = face;
+        Glide.with(TIMCommonService.getAppContext())
+            .load(faceUrl)
+            .centerInside()
+            .apply(new RequestOptions().error(android.R.drawable.ic_menu_report_image))
+            .addListener(new RequestListener<Drawable>() {
+                @Override
+                public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
+                    return false;
+                }
+
+                @Override
+                public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
+                    if (finalFace != null && finalFace.isAutoMirrored()) {
+                        resource.setAutoMirrored(true);
+                    }
+                    return false;
+                }
+            })
+            .into(imageView);
+    }
+
+    private void internalLoadFace(ChatFace chatFace, ImageView imageView, boolean isBitMap) {
+        if (imageView == null || chatFace == null) {
+            return;
+        }
+        if (chatFace instanceof Emoji) {
+            Glide.with(TIMCommonService.getAppContext())
+                .load(((Emoji) chatFace).getIcon())
+                .centerInside()
+                .apply(new RequestOptions().error(android.R.drawable.ic_menu_report_image))
+                .addListener(new RequestListener<Drawable>() {
+                    @Override
+                    public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
+                        return false;
+                    }
+
+                    @Override
+                    public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
+                        if (chatFace.isAutoMirrored()) {
+                            resource.setAutoMirrored(true);
+                        }
+                        return false;
+                    }
+                })
+                .into(imageView);
+            return;
+        }
+        String faceUrl = "";
+        FaceGroup faceGroup = chatFace.getFaceGroup();
+        ChatFace face = null;
+        if (faceGroup != null) {
+            face = faceGroup.getFace(chatFace.getFaceKey());
+            if (face != null) {
+                faceUrl = face.getFaceUrl();
+            }
+        }
+        final ChatFace finalFace = face;
+        if (isBitMap) {
+            Glide.with(TIMCommonService.getAppContext())
+                .asBitmap()
+                .load(faceUrl)
+                .centerInside()
+                .apply(new RequestOptions().error(android.R.drawable.ic_menu_report_image))
+                .addListener(new RequestListener<Bitmap>() {
+                    @Override
+                    public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Bitmap> target, boolean isFirstResource) {
+                        return false;
+                    }
+
+                    @Override
+                    public boolean onResourceReady(Bitmap resource, Object model, Target<Bitmap> target, DataSource dataSource, boolean isFirstResource) {
+                        if (finalFace.isAutoMirrored()) {
+                            imageView.setImageBitmap(resource);
+                            imageView.getDrawable().setAutoMirrored(true);
+                            return true;
+                        } else {
+                            return false;
+                        }
+                    }
+                })
+                .into(imageView);
+        } else {
+            Glide.with(TIMCommonService.getAppContext())
+                .load(faceUrl)
+                .centerInside()
+                .apply(new RequestOptions().error(android.R.drawable.ic_menu_report_image))
+                .addListener(new RequestListener<Drawable>() {
+                    @Override
+                    public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
+                        return false;
+                    }
+
+                    @Override
+                    public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
+                        if (finalFace != null && finalFace.isAutoMirrored()) {
+                            resource.setAutoMirrored(true);
+                        }
+                        return false;
+                    }
+                })
+                .into(imageView);
+        }
+    }
+
+    public static boolean isFaceChar(String faceChar) {
+        return getEmojiMap().get(faceChar) != null;
+    }
+
+    public static boolean handlerEmojiText(TextView comment, CharSequence content, boolean typing) {
+        if (comment == null) {
+            return false;
+        }
+        if (content == null) {
+            comment.setText(null);
+            return false;
+        }
+
+        Spannable spannable;
+        if (comment instanceof EditText && content instanceof Editable) {
+            spannable = (Editable) content;
+            ImageSpan[] imageSpans = ((Editable) content).getSpans(0, content.length(), ImageSpan.class);
+            for (ImageSpan span : imageSpans) {
+                ((Editable) content).removeSpan(span);
+            }
+        } else {
+            spannable = new SpannableStringBuilder(content);
+        }
+        String regex = "\\[(\\S+?)\\]";
+        Pattern p = Pattern.compile(regex);
+        Matcher m = p.matcher(content);
+        boolean imageFound = false;
+        while (m.find()) {
+            String emojiName = m.group();
+            Emoji emoji = getEmojiMap().get(emojiName);
+            if (emoji != null) {
+                Bitmap bitmap = emoji.getIcon();
+                if (bitmap != null) {
+                    imageFound = true;
+
+                    BitmapDrawable bitmapDrawable = new BitmapDrawable(getInstance().context.getResources(), bitmap);
+                    int size = getInstance().context.getResources().getDimensionPixelSize(R.dimen.common_default_emoji_size);
+                    bitmapDrawable.setBounds(0, 0, size, size);
+                    ImageSpan imageSpan = new CenterImageSpan(bitmapDrawable);
+                    spannable.setSpan(imageSpan, m.start(), m.end(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+                }
+            }
+        }
+        
+        // If no emoticon picture is found, and it is currently in the input state, the input box will not be reset.
+        if (!imageFound && typing) {
+            return false;
+        }
+        int selection = comment.getSelectionStart();
+        if (!(comment instanceof EditText)) {
+            comment.setText(spannable);
+        }
+        if (comment instanceof EditText) {
+            ((EditText) comment).setSelection(selection);
+        }
+
+        return true;
+    }
+
+    public static Bitmap getEmoji(String name) {
+        Emoji emoji = getEmojiMap().get(name);
+        if (emoji != null) {
+            return emoji.getIcon();
+        }
+        return null;
+    }
+
+    public static CharSequence emojiJudge(CharSequence text) {
+        if (TextUtils.isEmpty(text)) {
+            return "";
+        }
+
+        if (getEmojiCount() == 0) {
+            return text;
+        }
+
+        SpannableStringBuilder sb = new SpannableStringBuilder(text);
+        String regex = "\\[(\\S+?)\\]";
+        Pattern p = Pattern.compile(regex);
+        Matcher m = p.matcher(text);
+        ArrayList<EmojiData> emojiDataArrayList = new ArrayList<>();
+        
+        // Traverse to find matching characters and store
+        while (m.find()) {
+            String emojiKey = m.group();
+            int start = m.start();
+            int end = m.end();
+
+            Emoji emoji = getEmojiMap().get(emojiKey);
+            if (emoji == null) {
+                continue;
+            }
+            EmojiData emojiData = new EmojiData();
+            emojiData.setStart(start);
+            emojiData.setEnd(end);
+            emojiData.setEmojiText(emoji.getFaceName());
+
+            emojiDataArrayList.add(emojiData);
+        }
+
+        // flashback replacement
+        if (emojiDataArrayList.isEmpty()) {
+            return text;
+        }
+        for (int i = emojiDataArrayList.size() - 1; i >= 0; i--) {
+            EmojiData emojiData = emojiDataArrayList.get(i);
+            String emojiName = emojiData.getEmojiText();
+            int start = emojiData.getStart();
+            int end = emojiData.getEnd();
+
+            if (!TextUtils.isEmpty(emojiName) && start != -1 && end != -1) {
+                sb.replace(start, end, emojiName);
+            }
+        }
+        return sb;
+    }
+
+    public static List<String> splitEmojiText(String text) {
+        String regex = "\\[(\\S+?)\\]";
+        Pattern p = Pattern.compile(regex);
+        Matcher m = p.matcher(text);
+        ArrayList<EmojiData> emojiDataList = new ArrayList<>();
+        int lastMentionIndex = -1;
+        while (m.find()) {
+            String emojiKey = m.group();
+            int start;
+            if (lastMentionIndex != -1) {
+                start = text.indexOf(emojiKey, lastMentionIndex);
+            } else {
+                start = text.indexOf(emojiKey);
+            }
+            int end = start + emojiKey.length();
+            lastMentionIndex = end;
+
+            Emoji emoji = getEmojiMap().get(emojiKey);
+            if (emoji == null) {
+                continue;
+            }
+            EmojiData emojiData = new EmojiData();
+            emojiData.setStart(start);
+            emojiData.setEnd(end);
+            emojiDataList.add(emojiData);
+        }
+        List<String> stringList = new ArrayList<>();
+        int offset = 0;
+        for (EmojiData emojiData : emojiDataList) {
+            int start = emojiData.getStart() - offset;
+            int end = emojiData.getEnd() - offset;
+            String startStr = text.substring(0, start);
+            String middleStr = text.substring(start, end);
+            text = text.substring(end);
+            if (!TextUtils.isEmpty(startStr)) {
+                stringList.add(startStr);
+            }
+            stringList.add(middleStr);
+            offset += startStr.length() + middleStr.length();
+        }
+        if (!TextUtils.isEmpty(text)) {
+            stringList.add(text);
+        }
+        return stringList;
+    }
+
+    public static List<String> findEmojiKeyListFromText(String text) {
+        if (TextUtils.isEmpty(text)) {
+            return null;
+        }
+        List<String> emojiKeyList = new ArrayList<>();
+        // TUIKit custom emoji.
+        String regexOfCustomEmoji = "\\[(\\S+?)\\]";
+        Pattern patternOfCustomEmoji = Pattern.compile(regexOfCustomEmoji);
+        Matcher matcherOfCustomEmoji = patternOfCustomEmoji.matcher(text);
+        while (matcherOfCustomEmoji.find()) {
+            String emojiName = matcherOfCustomEmoji.group();
+            Emoji emoji = getEmojiMap().get(emojiName);
+            if (emoji != null) {
+                Bitmap bitmap = emoji.getIcon();
+                if (bitmap != null) {
+                    emojiKeyList.add(emojiName);
+                }
+            }
+        }
+
+        // Universal standard emoji.
+        String regexOfUniversalEmoji = getRegexOfUniversalEmoji();
+        Pattern patternOfUniversalEmoji = Pattern.compile(regexOfUniversalEmoji);
+        Matcher matcherOfUniversalEmoji = patternOfUniversalEmoji.matcher(text);
+        while (matcherOfUniversalEmoji.find()) {
+            String emojiKey = matcherOfUniversalEmoji.group();
+            if (!TextUtils.isEmpty(emojiKey)) {
+                emojiKeyList.add(matcherOfUniversalEmoji.group());
+            }
+        }
+
+        return emojiKeyList;
+    }
+
+    private static class EmojiData {
+        private int start;
+        private int end;
+        private String emojiText;
+
+        public int getEnd() {
+            return end;
+        }
+
+        public void setEnd(int end) {
+            this.end = end;
+        }
+
+        public int getStart() {
+            return start;
+        }
+
+        public void setStart(int start) {
+            this.start = start;
+        }
+
+        public String getEmojiText() {
+            return emojiText;
+        }
+
+        public void setEmojiText(String emojiText) {
+            this.emojiText = emojiText;
+        }
+    }
+
+    // Regex of universal emoji, refer to https://unicode.org/reports/tr51/#EBNF_and_Regex
+    private static String getRegexOfUniversalEmoji() {
+        String ri = "[\\U0001F1E6-\\U0001F1FF]";
+        // \u0023(#), \u002A(*), \u0030(keycap 0), \u0039(keycap 9), \u00A9(©), \u00AE(®) couldn't be added to NSString directly, need to transform a little
+        // bit.
+        String support = "\\U000000A9|\\U000000AE|\\u203C|\\u2049|\\u2122|\\u2139|[\\u2194-\\u2199]|[\\u21A9-\\u21AA]"
+            + "|[\\u231A-\\u231B]|\\u2328|\\u23CF|[\\u23E9-\\u23EF]|[\\u23F0-\\u23F3]|[\\u23F8-\\u23FA]|\\u24C2"
+            + "|[\\u25AA-\\u25AB]|\\u25B6|\\u25C0|[\\u25FB-\\u25FE]|[\\u2600-\\u2604]|\\u260E|\\u2611|[\\u2614-\\u2615]"
+            + "|\\u2618|\\u261D|\\u2620|[\\u2622-\\u2623]|\\u2626|\\u262A|[\\u262E-\\u262F]|[\\u2638-\\u263A]|\\u2640"
+            + "|\\u2642|[\\u2648-\\u264F]|[\\u2650-\\u2653]|\\u265F|\\u2660|\\u2663|[\\u2665-\\u2666]|\\u2668|\\u267B"
+            + "|[\\u267E-\\u267F]|[\\u2692-\\u2697]|\\u2699|[\\u269B-\\u269C]|[\\u26A0-\\u26A1]|\\u26A7|[\\u26AA-\\u26AB]"
+            + "|[\\u26B0-\\u26B1]|[\\u26BD-\\u26BE]|[\\u26C4-\\u26C5]|\\u26C8|[\\u26CE-\\u26CF]|\\u26D1|[\\u26D3-\\u26D4]"
+            + "|[\\u26E9-\\u26EA]|[\\u26F0-\\u26F5]|[\\u26F7-\\u26FA]|\\u26FD|\\u2702|\\u2705|[\\u2708-\\u270D]|\\u270F|\\u2712"
+            + "|\\u2714|\\u2716|\\u271D|\\u2721|\\u2728|[\\u2733-\\u2734]|\\u2744|\\u2747|\\u274C|\\u274E|[\\u2753-\\u2755]"
+            + "|\\u2757|[\\u2763-\\u2764]|[\\u2795-\\u2797]|\\u27A1|\\u27B0|\\u27BF|[\\u2934-\\u2935]|[\\u2B05-\\u2B07]"
+            + "|[\\u2B1B-\\u2B1C]|\\u2B50|\\u2B55|\\u3030|\\u303D|\\u3297|\\u3299|\\U0001F004|\\U0001F0CF|[\\U0001F170-\\U0001F171]"
+            + "|[\\U0001F17E-\\U0001F17F]|\\U0001F18E|[\\U0001F191-\\U0001F19A]|[\\U0001F1E6-\\U0001F1FF]|[\\U0001F201-\\U0001F202]"
+            + "|\\U0001F21A|\\U0001F22F|[\\U0001F232-\\U0001F23A]|[\\U0001F250-\\U0001F251]|[\\U0001F300-\\U0001F30F]"
+            + "|[\\U0001F310-\\U0001F31F]|[\\U0001F320-\\U0001F321]|[\\U0001F324-\\U0001F32F]|[\\U0001F330-\\U0001F33F]"
+            + "|[\\U0001F340-\\U0001F34F]|[\\U0001F350-\\U0001F35F]|[\\U0001F360-\\U0001F36F]|[\\U0001F370-\\U0001F37F]"
+            + "|[\\U0001F380-\\U0001F38F]|[\\U0001F390-\\U0001F393]|[\\U0001F396-\\U0001F397]|[\\U0001F399-\\U0001F39B]"
+            + "|[\\U0001F39E-\\U0001F39F]|[\\U0001F3A0-\\U0001F3AF]|[\\U0001F3B0-\\U0001F3BF]|[\\U0001F3C0-\\U0001F3CF]"
+            + "|[\\U0001F3D0-\\U0001F3DF]|[\\U0001F3E0-\\U0001F3EF]|\\U0001F3F0|[\\U0001F3F3-\\U0001F3F5]|[\\U0001F3F7-\\U0001F3FF]"
+            + "|[\\U0001F400-\\U0001F40F]|[\\U0001F410-\\U0001F41F]|[\\U0001F420-\\U0001F42F]|[\\U0001F430-\\U0001F43F]"
+            + "|[\\U0001F440-\\U0001F44F]|[\\U0001F450-\\U0001F45F]|[\\U0001F460-\\U0001F46F]|[\\U0001F470-\\U0001F47F]"
+            + "|[\\U0001F480-\\U0001F48F]|[\\U0001F490-\\U0001F49F]|[\\U0001F4A0-\\U0001F4AF]|[\\U0001F4B0-\\U0001F4BF]"
+            + "|[\\U0001F4C0-\\U0001F4CF]|[\\U0001F4D0-\\U0001F4DF]|[\\U0001F4E0-\\U0001F4EF]|[\\U0001F4F0-\\U0001F4FF]"
+            + "|[\\U0001F500-\\U0001F50F]|[\\U0001F510-\\U0001F51F]|[\\U0001F520-\\U0001F52F]|[\\U0001F530-\\U0001F53D]"
+            + "|[\\U0001F549-\\U0001F54E]|[\\U0001F550-\\U0001F55F]|[\\U0001F560-\\U0001F567]|\\U0001F56F|\\U0001F570"
+            + "|[\\U0001F573-\\U0001F57A]|\\U0001F587|[\\U0001F58A-\\U0001F58D]|\\U0001F590|[\\U0001F595-\\U0001F596]"
+            + "|[\\U0001F5A4-\\U0001F5A5]|\\U0001F5A8|[\\U0001F5B1-\\U0001F5B2]|\\U0001F5BC|[\\U0001F5C2-\\U0001F5C4]"
+            + "|[\\U0001F5D1-\\U0001F5D3]|[\\U0001F5DC-\\U0001F5DE]|\\U0001F5E1|\\U0001F5E3|\\U0001F5E8|\\U0001F5EF|\\U0001F5F3"
+            + "|[\\U0001F5FA-\\U0001F5FF]|[\\U0001F600-\\U0001F60F]|[\\U0001F610-\\U0001F61F]|[\\U0001F620-\\U0001F62F]"
+            + "|[\\U0001F630-\\U0001F63F]|[\\U0001F640-\\U0001F64F]|[\\U0001F650-\\U0001F65F]|[\\U0001F660-\\U0001F66F]"
+            + "|[\\U0001F670-\\U0001F67F]|[\\U0001F680-\\U0001F68F]|[\\U0001F690-\\U0001F69F]|[\\U0001F6A0-\\U0001F6AF]"
+            + "|[\\U0001F6B0-\\U0001F6BF]|[\\U0001F6C0-\\U0001F6C5]|[\\U0001F6CB-\\U0001F6CF]|[\\U0001F6D0-\\U0001F6D2]"
+            + "|[\\U0001F6D5-\\U0001F6D7]|[\\U0001F6DD-\\U0001F6DF]|[\\U0001F6E0-\\U0001F6E5]|\\U0001F6E9|[\\U0001F6EB-\\U0001F6EC]"
+            + "|\\U0001F6F0|[\\U0001F6F3-\\U0001F6FC]|[\\U0001F7E0-\\U0001F7EB]|\\U0001F7F0|[\\U0001F90C-\\U0001F90F]"
+            + "|[\\U0001F910-\\U0001F91F]|[\\U0001F920-\\U0001F92F]|[\\U0001F930-\\U0001F93A]|[\\U0001F93C-\\U0001F93F]"
+            + "|[\\U0001F940-\\U0001F945]|[\\U0001F947-\\U0001F94C]|[\\U0001F94D-\\U0001F94F]|[\\U0001F950-\\U0001F95F]"
+            + "|[\\U0001F960-\\U0001F96F]|[\\U0001F970-\\U0001F97F]|[\\U0001F980-\\U0001F98F]|[\\U0001F990-\\U0001F99F]"
+            + "|[\\U0001F9A0-\\U0001F9AF]|[\\U0001F9B0-\\U0001F9BF]|[\\U0001F9C0-\\U0001F9CF]|[\\U0001F9D0-\\U0001F9DF]"
+            + "|[\\U0001F9E0-\\U0001F9EF]|[\\U0001F9F0-\\U0001F9FF]|[\\U0001FA70-\\U0001FA74]|[\\U0001FA78-\\U0001FA7C]"
+            + "|[\\U0001FA80-\\U0001FA86]|[\\U0001FA90-\\U0001FA9F]|[\\U0001FAA0-\\U0001FAAC]|[\\U0001FAB0-\\U0001FABA]"
+            + "|[\\U0001FAC0-\\U0001FAC5]|[\\U0001FAD0-\\U0001FAD9]|[\\U0001FAE0-\\U0001FAE7]|[\\U0001FAF0-\\U0001FAF6]";
+        String unsupport = "\\u0023|\\u002A|[\\u0030-\\u0039]|";
+        String emoji = unsupport + support;
+
+        // Construct regex of emoji by the rules above.
+        String eMod = "[\\U0001F3FB-\\U0001F3FF]";
+
+        String variationSelector = "\\uFE0F";
+        String keycap = "\\u20E3";
+        String tags = "[\\U000E0020-\\U000E007E]";
+        String termTag = "\\U000E007F";
+        String zwj = "\\u200D";
+
+        String risequence = "[" + ri + "]"
+            + "[" + ri + "]";
+        String element = "[" + emoji + "]"
+            + "("
+            + "[" + eMod + "]|" + variationSelector + keycap + "?|[" + tags + "]+" + termTag + "?)?";
+        String regexEmoji = risequence + "|" + element + "(" + zwj + "(" + risequence + "|" + element + "))*";
+
+        return regexEmoji;
+    }
+}

+ 87 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/face/RecentEmojiManager.java

@@ -0,0 +1,87 @@
+package com.tencent.qcloud.tuikit.timcommon.component.face;
+
+import android.text.TextUtils;
+import android.util.Base64;
+import com.tencent.qcloud.tuicore.util.SPUtils;
+import com.tencent.qcloud.tuikit.timcommon.bean.Emoji;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+public class RecentEmojiManager {
+    public static final String PREFERENCE_NAME = "recentFace";
+    public static final int DEFAULT_RECENT_NUM = 10;
+    private static final String DEFAULT_RECENT_EMOJI_KEY = "recentEmoji";
+
+    private static final RecentEmojiManager instance = new RecentEmojiManager();
+
+    private RecentEmojiManager() {}
+
+    public static RecentEmojiManager getInstance() {
+        return instance;
+    }
+
+    public String getString(String key) {
+        return SPUtils.getInstance(PREFERENCE_NAME).getString(key);
+    }
+
+    public RecentEmojiManager putString(String key, String value) {
+        SPUtils.getInstance(PREFERENCE_NAME).put(key, value);
+        return this;
+    }
+
+    public static void putCollection(List<String> emojiList) {
+        getInstance().putCollection(DEFAULT_RECENT_EMOJI_KEY, emojiList);
+    }
+
+    public RecentEmojiManager putCollection(String key, List<String> emojiList) {
+        try {
+            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+            ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
+            objectOutputStream.writeObject(emojiList);
+            String collectionString = new String(Base64.encode(byteArrayOutputStream.toByteArray(), Base64.DEFAULT));
+            objectOutputStream.close();
+            return putString(key, collectionString);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return this;
+    }
+
+    public List<String> getCollection(String key) {
+        try {
+            String collectionString = getString(key);
+            if (TextUtils.isEmpty(collectionString) || TextUtils.isEmpty(collectionString.trim())) {
+                return null;
+            }
+            byte[] mobileBytes = Base64.decode(collectionString.getBytes(), Base64.DEFAULT);
+            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(mobileBytes);
+            ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
+            Object collectionObj = objectInputStream.readObject();
+            List<String> collection = null;
+            if (collectionObj instanceof List) {
+                collection = (List<String>) collectionObj;
+            }
+            return collection;
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    public static List<String> getCollection() {
+        return getInstance().getCollection(DEFAULT_RECENT_EMOJI_KEY);
+    }
+
+    public static void updateRecentUseEmoji(String emojiKey) {
+        List<String> recentList = getCollection();
+        recentList.remove(emojiKey);
+        recentList.add(0, emojiKey);
+        if (recentList.size() > DEFAULT_RECENT_NUM) {
+            recentList.remove(recentList.size() - 1);
+        }
+        putCollection(recentList);
+    }
+}

+ 99 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/gatherimage/MultiImageData.java

@@ -0,0 +1,99 @@
+package com.tencent.qcloud.tuikit.timcommon.component.gatherimage;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Multiple image data
+ */
+
+public class MultiImageData implements Cloneable {
+    static final int maxSize = 9;
+    List<Object> imageUrls;
+    int defaultImageResId;
+    Map<Integer, Bitmap> bitmapMap;
+    int bgColor = Color.parseColor("#cfd3d8");
+
+    int targetImageSize;
+    int maxWidth;
+    int maxHeight;
+    int rowCount;
+    int columnCount;
+    int gap = 6;
+
+    public MultiImageData() {}
+
+    public MultiImageData(int defaultImageResId) {
+        this.defaultImageResId = defaultImageResId;
+    }
+
+    public MultiImageData(List<Object> imageUrls, int defaultImageResId) {
+        this.imageUrls = imageUrls;
+        this.defaultImageResId = defaultImageResId;
+    }
+
+    public int getDefaultImageResId() {
+        return defaultImageResId;
+    }
+
+    public void setDefaultImageResId(int defaultImageResId) {
+        this.defaultImageResId = defaultImageResId;
+    }
+
+    public List<Object> getImageUrls() {
+        return imageUrls;
+    }
+
+    public void setImageUrls(List<Object> imageUrls) {
+        this.imageUrls = imageUrls;
+    }
+
+    public void putBitmap(Bitmap bitmap, int position) {
+        if (null != bitmapMap) {
+            synchronized (bitmapMap) {
+                bitmapMap.put(position, bitmap);
+            }
+        } else {
+            bitmapMap = new HashMap<>();
+            synchronized (bitmapMap) {
+                bitmapMap.put(position, bitmap);
+            }
+        }
+    }
+
+    public Bitmap getBitmap(int position) {
+        if (null != bitmapMap) {
+            synchronized (bitmapMap) {
+                return bitmapMap.get(position);
+            }
+        }
+        return null;
+    }
+
+    public int size() {
+        if (null != imageUrls) {
+            return imageUrls.size() > maxSize ? maxSize : imageUrls.size();
+        } else {
+            return 0;
+        }
+    }
+
+    @Override
+    protected MultiImageData clone() throws CloneNotSupportedException {
+        MultiImageData multiImageData = (MultiImageData) super.clone();
+        if (imageUrls != null) {
+            multiImageData.imageUrls = new ArrayList<>(imageUrls.size());
+            multiImageData.imageUrls.addAll(imageUrls);
+        }
+        if (bitmapMap != null) {
+            multiImageData.bitmapMap = new HashMap<>();
+            multiImageData.bitmapMap.putAll(bitmapMap);
+        }
+        return multiImageData;
+    }
+}

+ 92 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/gatherimage/ShadeImageView.java

@@ -0,0 +1,92 @@
+package com.tencent.qcloud.tuikit.timcommon.component.gatherimage;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.TypedArray;
+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.RectF;
+import android.util.AttributeSet;
+import android.util.SparseArray;
+import android.widget.ImageView;
+
+import androidx.annotation.Nullable;
+
+import com.tencent.qcloud.tuikit.timcommon.R;
+import com.tencent.qcloud.tuikit.timcommon.util.ScreenUtil;
+
+@SuppressLint("AppCompatCustomView")
+public class ShadeImageView extends ImageView {
+    private static SparseArray<Bitmap> sRoundBitmapArray = new SparseArray();
+    private Paint mShadePaint = new Paint();
+    private Bitmap mRoundBitmap;
+    private int radius;
+
+    public ShadeImageView(Context context) {
+        super(context);
+    }
+
+    public ShadeImageView(Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+        init(context, attrs);
+    }
+
+    public ShadeImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init(context, attrs);
+    }
+
+    private void init(Context context, AttributeSet attrs) {
+        radius = (int) ScreenUtil.dp2px(4.0f, getResources().getDisplayMetrics());
+        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.core_round_rect_image_style);
+        radius = array.getDimensionPixelSize(R.styleable.core_round_rect_image_style_round_radius, radius);
+        array.recycle();
+        setLayerType(LAYER_TYPE_HARDWARE, null);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        mShadePaint.setColor(Color.RED);
+        mShadePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
+        mRoundBitmap = sRoundBitmapArray.get(getMeasuredWidth() + radius);
+        if (mRoundBitmap == null) {
+            mRoundBitmap = getRoundBitmap();
+            sRoundBitmapArray.put(getMeasuredWidth() + radius, mRoundBitmap);
+        }
+        canvas.drawBitmap(mRoundBitmap, 0, 0, mShadePaint);
+    }
+
+    /**
+     * Get rounded rectangle
+     *
+     * @return Bitmap
+     */
+    private Bitmap getRoundBitmap() {
+        Bitmap output = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(output);
+        final int color = Color.parseColor("#cfd3d8");
+        final Rect rect = new Rect(0, 0, getMeasuredWidth(), getMeasuredHeight());
+        final RectF rectF = new RectF(rect);
+        Paint paint = new Paint();
+        paint.setAntiAlias(true);
+        canvas.drawARGB(0, 0, 0, 0);
+        paint.setColor(color);
+        canvas.drawRoundRect(rectF, radius, radius, paint);
+        return output;
+    }
+
+    public int getRadius() {
+        return radius;
+    }
+
+    public void setRadius(int radius) {
+        this.radius = radius;
+        invalidate();
+    }
+}

+ 87 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/gatherimage/SynthesizedImageView.java

@@ -0,0 +1,87 @@
+package com.tencent.qcloud.tuikit.timcommon.component.gatherimage;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.util.AttributeSet;
+
+import androidx.annotation.Nullable;
+
+import com.tencent.qcloud.tuikit.timcommon.R;
+
+import java.util.List;
+
+public class SynthesizedImageView extends ShadeImageView {
+    /**
+     *
+     * Group Chat Avatar Synthesizer
+     */
+    TeamHeadSynthesizer teamHeadSynthesizer;
+    int imageSize = 100;
+    int synthesizedBg = Color.parseColor("#cfd3d8");
+    int defaultImageResId = 0;
+    int imageGap = 6;
+
+    public SynthesizedImageView(Context context) {
+        super(context);
+        init(context);
+    }
+
+    public SynthesizedImageView(Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+        initAttrs(attrs);
+        init(context);
+    }
+
+    public SynthesizedImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        initAttrs(attrs);
+        init(context);
+    }
+
+    private void initAttrs(AttributeSet attributeSet) {
+        TypedArray ta = getContext().obtainStyledAttributes(attributeSet, R.styleable.SynthesizedImageView);
+        if (null != ta) {
+            synthesizedBg = ta.getColor(R.styleable.SynthesizedImageView_synthesized_image_bg, synthesizedBg);
+            defaultImageResId = ta.getResourceId(R.styleable.SynthesizedImageView_synthesized_default_image, defaultImageResId);
+            imageSize = ta.getDimensionPixelSize(R.styleable.SynthesizedImageView_synthesized_image_size, imageSize);
+            imageGap = ta.getDimensionPixelSize(R.styleable.SynthesizedImageView_synthesized_image_gap, imageGap);
+            ta.recycle();
+        }
+    }
+
+    private void init(Context context) {
+        teamHeadSynthesizer = new TeamHeadSynthesizer(context, this);
+        teamHeadSynthesizer.setMaxWidthHeight(imageSize, imageSize);
+        teamHeadSynthesizer.setDefaultImage(defaultImageResId);
+        teamHeadSynthesizer.setBgColor(synthesizedBg);
+        teamHeadSynthesizer.setGap(imageGap);
+    }
+
+    public SynthesizedImageView displayImage(List<Object> imageUrls) {
+        teamHeadSynthesizer.getMultiImageData().setImageUrls(imageUrls);
+        return this;
+    }
+
+    public SynthesizedImageView defaultImage(int defaultImage) {
+        teamHeadSynthesizer.setDefaultImage(defaultImage);
+        return this;
+    }
+
+    public SynthesizedImageView synthesizedWidthHeight(int maxWidth, int maxHeight) {
+        teamHeadSynthesizer.setMaxWidthHeight(maxWidth, maxHeight);
+        return this;
+    }
+
+    public void setImageId(String id) {
+        teamHeadSynthesizer.setImageId(id);
+    }
+
+    public void load(String imageId) {
+        teamHeadSynthesizer.load(imageId);
+    }
+
+    public void clear() {
+        teamHeadSynthesizer.clearImage();
+    }
+}

+ 12 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/gatherimage/Synthesizer.java

@@ -0,0 +1,12 @@
+package com.tencent.qcloud.tuikit.timcommon.component.gatherimage;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+
+public interface Synthesizer {
+    Bitmap synthesizeImageList(MultiImageData imageData);
+
+    boolean asyncLoadImageList(MultiImageData imageData);
+
+    void drawDrawable(Canvas canvas, MultiImageData imageData);
+}

+ 334 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/gatherimage/TeamHeadSynthesizer.java

@@ -0,0 +1,334 @@
+package com.tencent.qcloud.tuikit.timcommon.component.gatherimage;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.text.TextUtils;
+import android.widget.ImageView;
+import com.tencent.qcloud.tuicore.TUIConfig;
+import com.tencent.qcloud.tuikit.timcommon.TIMCommonConfig;
+import com.tencent.qcloud.tuikit.timcommon.component.impl.GlideEngine;
+import com.tencent.qcloud.tuikit.timcommon.util.ImageUtil;
+import com.tencent.qcloud.tuikit.timcommon.util.ThreadUtils;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+public class TeamHeadSynthesizer implements Synthesizer {
+    MultiImageData multiImageData;
+    Context mContext;
+
+    ImageView imageView;
+
+    // It is safe to set and get only in the main thread
+    private String currentImageId = "";
+    Callback callback = new Callback() {
+        @Override
+        public void onCall(Bitmap bitmap, String targetID) {
+            if (!TextUtils.equals(getImageId(), targetID)) {
+                return;
+            }
+            GlideEngine.loadUserIcon(imageView, bitmap);
+        }
+    };
+
+    public TeamHeadSynthesizer(Context mContext, ImageView imageView) {
+        this.mContext = mContext;
+        this.imageView = imageView;
+        init();
+    }
+
+    private void init() {
+        multiImageData = new MultiImageData();
+    }
+
+    public void setMaxWidthHeight(int maxWidth, int maxHeight) {
+        multiImageData.maxWidth = maxWidth;
+        multiImageData.maxHeight = maxHeight;
+    }
+
+    public MultiImageData getMultiImageData() {
+        return multiImageData;
+    }
+
+    public int getDefaultImage() {
+        return multiImageData.getDefaultImageResId();
+    }
+
+    public void setDefaultImage(int defaultImageResId) {
+        multiImageData.setDefaultImageResId(defaultImageResId);
+    }
+
+    public void setBgColor(int bgColor) {
+        multiImageData.bgColor = bgColor;
+    }
+
+    public void setGap(int gap) {
+        multiImageData.gap = gap;
+    }
+
+    /**
+     * Set Grid params
+     *
+     * @param imagesSize   Number of pictures
+     * @return gridParam[0] Rows gridParam[1] columns
+     */
+    protected int[] calculateGridParam(int imagesSize) {
+        int[] gridParam = new int[2];
+        if (imagesSize < 3) {
+            gridParam[0] = 1;
+            gridParam[1] = imagesSize;
+        } else if (imagesSize <= 4) {
+            gridParam[0] = 2;
+            gridParam[1] = 2;
+        } else {
+            gridParam[0] = imagesSize / 3 + (imagesSize % 3 == 0 ? 0 : 1);
+            gridParam[1] = 3;
+        }
+        return gridParam;
+    }
+
+    @Override
+    public Bitmap synthesizeImageList(MultiImageData imageData) {
+        Bitmap mergeBitmap = Bitmap.createBitmap(imageData.maxWidth, imageData.maxHeight, Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(mergeBitmap);
+        drawDrawable(canvas, imageData);
+        canvas.save();
+        canvas.restore();
+        return mergeBitmap;
+    }
+
+    @Override
+    public boolean asyncLoadImageList(MultiImageData imageData) {
+        boolean loadSuccess = true;
+        List<Object> imageUrls = imageData.getImageUrls();
+        for (int i = 0; i < imageUrls.size(); i++) {
+            Bitmap defaultIcon = BitmapFactory.decodeResource(mContext.getResources(), TIMCommonConfig.getDefaultAvatarImage());
+            try {
+                Bitmap bitmap = asyncLoadImage(imageUrls.get(i), imageData.targetImageSize);
+                imageData.putBitmap(bitmap, i);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+                imageData.putBitmap(defaultIcon, i);
+            } catch (ExecutionException e) {
+                e.printStackTrace();
+                imageData.putBitmap(defaultIcon, i);
+            }
+        }
+        return loadSuccess;
+    }
+
+    @Override
+    public void drawDrawable(Canvas canvas, MultiImageData imageData) {
+        canvas.drawColor(imageData.bgColor);
+        int size = imageData.size();
+        int tCenter = (imageData.maxHeight + imageData.gap) / 2;
+        int bCenter = (imageData.maxHeight - imageData.gap) / 2;
+        int lCenter = (imageData.maxWidth + imageData.gap) / 2;
+        int rCenter = (imageData.maxWidth - imageData.gap) / 2;
+        int center = (imageData.maxHeight - imageData.targetImageSize) / 2;
+        for (int i = 0; i < size; i++) {
+            int rowNum = i / imageData.columnCount;
+            int columnNum = i % imageData.columnCount;
+
+            int left = ((int) (imageData.targetImageSize * (imageData.columnCount == 1 ? columnNum + 0.5 : columnNum) + imageData.gap * (columnNum + 1)));
+            int top = ((int) (imageData.targetImageSize * (imageData.columnCount == 1 ? rowNum + 0.5 : rowNum) + imageData.gap * (rowNum + 1)));
+            int right = left + imageData.targetImageSize;
+            int bottom = top + imageData.targetImageSize;
+
+            Bitmap bitmap = imageData.getBitmap(i);
+            if (size == 1) {
+                drawBitmapAtPosition(canvas, left, top, right, bottom, bitmap);
+            } else if (size == 2) {
+                drawBitmapAtPosition(canvas, left, center, right, center + imageData.targetImageSize, bitmap);
+            } else if (size == 3) {
+                if (i == 0) {
+                    drawBitmapAtPosition(canvas, center, top, center + imageData.targetImageSize, bottom, bitmap);
+                } else {
+                    drawBitmapAtPosition(canvas, imageData.gap * i + imageData.targetImageSize * (i - 1), tCenter,
+                        imageData.gap * i + imageData.targetImageSize * i, tCenter + imageData.targetImageSize, bitmap);
+                }
+            } else if (size == 4) {
+                drawBitmapAtPosition(canvas, left, top, right, bottom, bitmap);
+            } else if (size == 5) {
+                if (i == 0) {
+                    drawBitmapAtPosition(canvas, rCenter - imageData.targetImageSize, rCenter - imageData.targetImageSize, rCenter, rCenter, bitmap);
+                } else if (i == 1) {
+                    drawBitmapAtPosition(canvas, lCenter, rCenter - imageData.targetImageSize, lCenter + imageData.targetImageSize, rCenter, bitmap);
+                } else {
+                    drawBitmapAtPosition(canvas, imageData.gap * (i - 1) + imageData.targetImageSize * (i - 2), tCenter,
+                        imageData.gap * (i - 1) + imageData.targetImageSize * (i - 1), tCenter + imageData.targetImageSize, bitmap);
+                }
+            } else if (size == 6) {
+                if (i < 3) {
+                    drawBitmapAtPosition(canvas, imageData.gap * (i + 1) + imageData.targetImageSize * i, bCenter - imageData.targetImageSize,
+                        imageData.gap * (i + 1) + imageData.targetImageSize * (i + 1), bCenter, bitmap);
+                } else {
+                    drawBitmapAtPosition(canvas, imageData.gap * (i - 2) + imageData.targetImageSize * (i - 3), tCenter,
+                        imageData.gap * (i - 2) + imageData.targetImageSize * (i - 2), tCenter + imageData.targetImageSize, bitmap);
+                }
+            } else if (size == 7) {
+                if (i == 0) {
+                    drawBitmapAtPosition(canvas, center, imageData.gap, center + imageData.targetImageSize, imageData.gap + imageData.targetImageSize, bitmap);
+                } else if (i > 0 && i < 4) {
+                    drawBitmapAtPosition(canvas, imageData.gap * i + imageData.targetImageSize * (i - 1), center,
+                        imageData.gap * i + imageData.targetImageSize * i, center + imageData.targetImageSize, bitmap);
+                } else {
+                    drawBitmapAtPosition(canvas, imageData.gap * (i - 3) + imageData.targetImageSize * (i - 4), tCenter + imageData.targetImageSize / 2,
+                        imageData.gap * (i - 3) + imageData.targetImageSize * (i - 3), tCenter + imageData.targetImageSize / 2 + imageData.targetImageSize,
+                        bitmap);
+                }
+            } else if (size == 8) {
+                if (i == 0) {
+                    drawBitmapAtPosition(
+                        canvas, rCenter - imageData.targetImageSize, imageData.gap, rCenter, imageData.gap + imageData.targetImageSize, bitmap);
+                } else if (i == 1) {
+                    drawBitmapAtPosition(
+                        canvas, lCenter, imageData.gap, lCenter + imageData.targetImageSize, imageData.gap + imageData.targetImageSize, bitmap);
+                } else if (i > 1 && i < 5) {
+                    drawBitmapAtPosition(canvas, imageData.gap * (i - 1) + imageData.targetImageSize * (i - 2), center,
+                        imageData.gap * (i - 1) + imageData.targetImageSize * (i - 1), center + imageData.targetImageSize, bitmap);
+                } else {
+                    drawBitmapAtPosition(canvas, imageData.gap * (i - 4) + imageData.targetImageSize * (i - 5), tCenter + imageData.targetImageSize / 2,
+                        imageData.gap * (i - 4) + imageData.targetImageSize * (i - 4), tCenter + imageData.targetImageSize / 2 + imageData.targetImageSize,
+                        bitmap);
+                }
+            } else if (size == 9) {
+                drawBitmapAtPosition(canvas, left, top, right, bottom, bitmap);
+            }
+        }
+    }
+
+    /**
+     * DrawBitmap
+     *
+     * @param canvas
+     * @param left
+     * @param top
+     * @param right
+     * @param bottom
+     * @param bitmap
+     */
+    public void drawBitmapAtPosition(Canvas canvas, int left, int top, int right, int bottom, Bitmap bitmap) {
+        if (null == bitmap) {
+            if (multiImageData.getDefaultImageResId() > 0) {
+                bitmap = BitmapFactory.decodeResource(mContext.getResources(), multiImageData.getDefaultImageResId());
+            }
+        }
+        if (null != bitmap) {
+            Rect rect = new Rect(left, top, right, bottom);
+            canvas.drawBitmap(bitmap, null, rect, null);
+        }
+    }
+
+    private Bitmap asyncLoadImage(Object imageUrl, int targetImageSize) throws ExecutionException, InterruptedException {
+        return GlideEngine.loadBitmap(imageUrl, targetImageSize);
+    }
+
+    public void setImageId(String id) {
+        currentImageId = id;
+    }
+
+    public String getImageId() {
+        return currentImageId;
+    }
+
+    public void load(String imageId) {
+        if (multiImageData.size() == 0) {
+            
+            // The image id when the request is initiated is inconsistent with the current image id,
+            // indicating that multiplexing has occurred, and the image should not be set at this time.
+            if (imageId != null && !TextUtils.equals(imageId, currentImageId)) {
+                return;
+            }
+            GlideEngine.loadUserIcon(imageView, getDefaultImage());
+            return;
+        }
+
+        if (multiImageData.size() == 1) {
+            
+            // The image id when the request is initiated is inconsistent with the current image id,
+            // indicating that multiplexing has occurred, and the image should not be set at this time.
+            if (imageId != null && !TextUtils.equals(imageId, currentImageId)) {
+                return;
+            }
+            GlideEngine.loadUserIcon(imageView, multiImageData.getImageUrls().get(0));
+            return;
+        }
+
+        
+        // Clear the content before loading images asynchronously to avoid flickering
+        clearImage();
+
+        
+        
+        // Initialize the image information. Since it is asynchronous loading and synthesizing the avatar,
+        // a local object needs to be passed to the synthesis thread, which is only used in the asynchronous
+        // loading thread, so that when the image is reused, the external thread will not overwrite the local
+        // object by setting the url again.
+        MultiImageData copyMultiImageData;
+        try {
+            copyMultiImageData = multiImageData.clone();
+        } catch (CloneNotSupportedException e) {
+            e.printStackTrace();
+            List urlList = new ArrayList();
+            if (multiImageData.imageUrls != null) {
+                urlList.addAll(multiImageData.imageUrls);
+            }
+            copyMultiImageData = new MultiImageData(urlList, multiImageData.defaultImageResId);
+        }
+        int[] gridParam = calculateGridParam(multiImageData.size());
+        copyMultiImageData.rowCount = gridParam[0];
+        copyMultiImageData.columnCount = gridParam[1];
+        copyMultiImageData.targetImageSize = (copyMultiImageData.maxWidth - (copyMultiImageData.columnCount + 1) * copyMultiImageData.gap)
+            / (copyMultiImageData.columnCount == 1 ? 2 : copyMultiImageData.columnCount);
+        final String finalImageId = imageId;
+        final MultiImageData finalCopyMultiImageData = copyMultiImageData;
+        ThreadUtils.execute(new Runnable() {
+            @Override
+            public void run() {
+                final File file = new File(TUIConfig.getImageBaseDir() + finalImageId);
+                boolean cacheBitmapExists = false;
+                Bitmap existsBitmap = null;
+                if (file.exists() && file.isFile()) {
+                    BitmapFactory.Options options = new BitmapFactory.Options();
+                    existsBitmap = BitmapFactory.decodeFile(file.getPath(), options);
+                    if (options.outWidth > 0 && options.outHeight > 0) {
+                        cacheBitmapExists = true;
+                    }
+                }
+                if (!cacheBitmapExists) {
+                    asyncLoadImageList(finalCopyMultiImageData);
+                    final Bitmap bitmap = synthesizeImageList(finalCopyMultiImageData);
+                    ImageUtil.storeBitmap(file, bitmap);
+                    ImageUtil.setGroupConversationAvatar(finalImageId, file.getAbsolutePath());
+                    ThreadUtils.postOnUiThread(new Runnable() {
+                        @Override
+                        public void run() {
+                            callback.onCall(bitmap, finalImageId);
+                        }
+                    });
+                } else {
+                    final Bitmap finalExistsBitmap = existsBitmap;
+                    ThreadUtils.postOnUiThread(new Runnable() {
+                        @Override
+                        public void run() {
+                            callback.onCall(finalExistsBitmap, finalImageId);
+                        }
+                    });
+                }
+            }
+        });
+    }
+
+    public void clearImage() {
+        GlideEngine.clear(imageView);
+    }
+
+    interface Callback {
+        void onCall(Bitmap bitmap, String targetID);
+    }
+}

+ 65 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/gatherimage/UserIconView.java

@@ -0,0 +1,65 @@
+package com.tencent.qcloud.tuikit.timcommon.component.gatherimage;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.widget.RelativeLayout;
+
+import com.tencent.qcloud.tuikit.timcommon.R;
+
+import java.util.List;
+
+public class UserIconView extends RelativeLayout {
+    private SynthesizedImageView mIconView;
+    private int mDefaultImageResId;
+    private int mIconRadius;
+
+    public UserIconView(Context context) {
+        super(context);
+        init(null);
+    }
+
+    public UserIconView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(attrs);
+    }
+
+    public UserIconView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init(attrs);
+    }
+
+    private void init(AttributeSet attributeSet) {
+        inflate(getContext(), R.layout.common_profile_icon_view, this);
+        if (attributeSet != null) {
+            TypedArray ta = getContext().obtainStyledAttributes(attributeSet, R.styleable.UserIconView);
+            if (null != ta) {
+                mDefaultImageResId = ta.getResourceId(R.styleable.UserIconView_default_image, mDefaultImageResId);
+                mIconRadius = ta.getDimensionPixelSize(R.styleable.UserIconView_image_radius, mIconRadius);
+                ta.recycle();
+            }
+        }
+
+        mIconView = findViewById(R.id.profile_icon);
+        if (mDefaultImageResId > 0) {
+            mIconView.defaultImage(mDefaultImageResId);
+        }
+        if (mIconRadius > 0) {
+            mIconView.setRadius(mIconRadius);
+        }
+    }
+
+    public void setDefaultImageResId(int resId) {
+        mDefaultImageResId = resId;
+        mIconView.defaultImage(resId);
+    }
+
+    public void setRadius(int radius) {
+        mIconRadius = radius;
+        mIconView.setRadius(mIconRadius);
+    }
+
+    public void setIconUrls(List<Object> iconUrls) {
+        mIconView.displayImage(iconUrls).load(null);
+    }
+}

+ 136 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/highlight/HighlightPresenter.java

@@ -0,0 +1,136 @@
+package com.tencent.qcloud.tuikit.timcommon.component.highlight;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ArgbEvaluator;
+import android.animation.ValueAnimator;
+import com.tencent.qcloud.tuikit.timcommon.R;
+import com.tencent.qcloud.tuikit.timcommon.TIMCommonService;
+import com.tencent.qcloud.tuikit.timcommon.interfaces.HighlightListener;
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.Map;
+
+public class HighlightPresenter {
+    public static final int DEFAULT_DURATION = 250;
+    public static final int DEFAULT_REPEAT_COUNT = 3;
+
+    private static final class HighlightPresenterHolder {
+        private static final HighlightPresenter INSTANCE = new HighlightPresenter();
+    }
+
+    private static HighlightPresenter getInstance() {
+        return HighlightPresenterHolder.INSTANCE;
+    }
+
+    private final Map<String, WeakReference<HighlightListener>> highlightListenerMap = new HashMap<>();
+
+    private final Map<String, ValueAnimator> highlightMap = new HashMap<>();
+
+    private int highLightDarkColor = -1;
+    private int highLightLightColor = -1;
+
+    private HighlightPresenter() {}
+
+    public static void registerHighlightListener(String highlightID, HighlightListener listener) {
+        if (listener == null) {
+            return;
+        }
+        getInstance().highlightListenerMap.put(highlightID, new WeakReference<>(listener));
+    }
+
+    public static void unregisterHighlightListener(String highlightID) {
+        getInstance().highlightListenerMap.remove(highlightID);
+    }
+
+    public static void startHighlight(String highlightID) {
+        getInstance().internalStartHighlight(highlightID);
+    }
+
+    public static void stopHighlight(String highlightID) {
+        getInstance().internalStopHighlight(highlightID);
+    }
+
+    public static void setHighlightDarkColor(int color) {
+        getInstance().highLightDarkColor = color;
+    }
+
+    public static void setHighlightLightColor(int color) {
+        getInstance().highLightLightColor = color;
+    }
+
+    private void internalStartHighlight(String highlightID) {
+        ValueAnimator highlightAnimator = new ValueAnimator();
+        if (highLightDarkColor == highLightLightColor && highLightLightColor == -1) {
+            highLightDarkColor = TIMCommonService.getAppContext().getResources().getColor(R.color.chat_message_bubble_high_light_dark_color);
+            highLightLightColor = TIMCommonService.getAppContext().getResources().getColor(R.color.chat_message_bubble_high_light_light_color);
+        }
+
+        highlightAnimator.setIntValues(highLightDarkColor, highLightLightColor);
+        highlightAnimator.setEvaluator(new ArgbEvaluator());
+        highlightAnimator.setRepeatCount(DEFAULT_REPEAT_COUNT);
+        highlightAnimator.setDuration(DEFAULT_DURATION);
+        highlightAnimator.setRepeatMode(ValueAnimator.REVERSE);
+        highlightAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                onHighlightStart(highlightID);
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                onHighlightEnd(highlightID);
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                onHighlightEnd(highlightID);
+            }
+        });
+        highlightAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                onHighlightUpdate(highlightID, (Integer) animation.getAnimatedValue());
+            }
+        });
+        highlightAnimator.start();
+        highlightMap.put(highlightID, highlightAnimator);
+    }
+
+    private void internalStopHighlight(String highlightID) {
+        ValueAnimator highlightAnimator = highlightMap.get(highlightID);
+        if (highlightAnimator != null) {
+            highlightAnimator.cancel();
+        }
+    }
+
+    private void onHighlightStart(String highlightID) {
+        HighlightListener lightListener = getInstance().getHighlightListener(highlightID);
+        if (lightListener != null) {
+            lightListener.onHighlightStart();
+        }
+    }
+
+    private void onHighlightEnd(String highlightID) {
+        highlightMap.remove(highlightID);
+        HighlightListener lightListener = getInstance().getHighlightListener(highlightID);
+        if (lightListener != null) {
+            lightListener.onHighlightEnd();
+        }
+    }
+
+    private void onHighlightUpdate(String highlightID, int color) {
+        HighlightListener lightListener = getHighlightListener(highlightID);
+        if (lightListener != null) {
+            lightListener.onHighlightUpdate(color);
+        }
+    }
+
+    private HighlightListener getHighlightListener(String highlightID) {
+        WeakReference<HighlightListener> listener = highlightListenerMap.get(highlightID);
+        if (listener != null) {
+            return listener.get();
+        }
+        return null;
+    }
+}

+ 120 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/impl/GlideEngine.java

@@ -0,0 +1,120 @@
+package com.tencent.qcloud.tuikit.timcommon.component.impl;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.widget.ImageView;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.Priority;
+import com.bumptech.glide.load.engine.DiskCacheStrategy;
+import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
+import com.bumptech.glide.request.RequestListener;
+import com.bumptech.glide.request.RequestOptions;
+import com.tencent.qcloud.tuicore.TUILogin;
+import com.tencent.qcloud.tuicore.TUIThemeManager;
+import com.tencent.qcloud.tuikit.timcommon.R;
+import java.io.File;
+import java.util.concurrent.ExecutionException;
+
+public class GlideEngine {
+    public static void loadCornerImageWithoutPlaceHolder(ImageView imageView, Object uri, RequestListener listener, float radius) {
+        RoundedCorners transform = null;
+        if ((int) radius > 0) {
+            transform = new RoundedCorners((int) radius);
+        }
+
+        RequestOptions options = new RequestOptions().centerCrop();
+        if (transform != null) {
+            options = options.transform(transform);
+        }
+        Glide.with(TUILogin.getAppContext()).load(uri).apply(options).listener(listener).into(imageView);
+    }
+
+    public static void clear(ImageView imageView) {
+        Glide.with(TUILogin.getAppContext()).clear(imageView);
+    }
+
+    public static void loadImage(ImageView imageView, String filePath, RequestListener listener) {
+        Glide.with(TUILogin.getAppContext())
+            .load(filePath)
+            .listener(listener)
+            .apply(new RequestOptions().error(TUIThemeManager.getAttrResId(TUILogin.getAppContext(), R.attr.core_default_user_icon)))
+            .into(imageView);
+    }
+
+    public static void loadImage(ImageView imageView, String filePath) {
+        Glide.with(TUILogin.getAppContext())
+            .load(filePath)
+            .apply(new RequestOptions().error(TUIThemeManager.getAttrResId(TUILogin.getAppContext(), R.attr.core_default_user_icon)))
+            .into(imageView);
+    }
+
+    public static void loadImage(ImageView imageView, Uri uri) {
+        if (uri == null) {
+            return;
+        }
+        Glide.with(TUILogin.getAppContext())
+            .load(uri)
+            .apply(new RequestOptions().error(TUIThemeManager.getAttrResId(TUILogin.getAppContext(), R.attr.core_default_user_icon)))
+            .into(imageView);
+    }
+
+    public static void loadImage(String filePath, String url) {
+        try {
+            File file = Glide.with(TUILogin.getAppContext()).asFile().load(url).submit().get();
+            File destFile = new File(filePath);
+            file.renameTo(destFile);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        } catch (ExecutionException e) {
+            e.printStackTrace();
+        }
+    }
+
+    public void loadImage(Context context, int resizeX, int resizeY, ImageView imageView, Uri uri) {
+        Glide.with(context).load(uri).apply(new RequestOptions().override(resizeX, resizeY).priority(Priority.HIGH).fitCenter()).into(imageView);
+    }
+
+    public static void loadImage(ImageView imageView, Object uri) {
+        if (uri == null) {
+            return;
+        }
+        Glide.with(TUILogin.getAppContext())
+            .load(uri)
+            .apply(new RequestOptions().error(TUIThemeManager.getAttrResId(TUILogin.getAppContext(), R.attr.core_default_user_icon)))
+            .into(imageView);
+    }
+
+    public static void loadUserIcon(ImageView imageView, Object uri) {
+        loadUserIcon(imageView, uri, 0);
+    }
+
+    public static void loadUserIcon(ImageView imageView, Object uri, int radius) {
+        Glide.with(TUILogin.getAppContext())
+            .load(uri)
+            .diskCacheStrategy(DiskCacheStrategy.ALL)
+            .placeholder(TUIThemeManager.getAttrResId(TUILogin.getAppContext(), R.attr.core_default_user_icon))
+            .apply(new RequestOptions().centerCrop().error(TUIThemeManager.getAttrResId(TUILogin.getAppContext(), R.attr.core_default_user_icon)))
+            .into(imageView);
+    }
+
+    public static void loadUserIcon(ImageView imageView, Object uri, int defaultResId, int radius) {
+        Glide.with(TUILogin.getAppContext()).load(uri).placeholder(defaultResId).apply(new RequestOptions().centerCrop().error(defaultResId)).into(imageView);
+    }
+
+    public static Bitmap loadBitmap(Object imageUrl, int targetImageSize) throws InterruptedException, ExecutionException {
+        if (imageUrl == null) {
+            return null;
+        }
+        return Glide.with(TUILogin.getAppContext())
+            .asBitmap()
+            .load(imageUrl)
+            .apply(new RequestOptions().error(TUIThemeManager.getAttrResId(TUILogin.getAppContext(), R.attr.core_default_user_icon)))
+            .into(targetImageSize, targetImageSize)
+            .get();
+    }
+
+    public static void loadImageSetDefault(ImageView imageView, Object uri, int defaultResId) {
+        Glide.with(TUILogin.getAppContext()).load(uri).placeholder(defaultResId).apply(new RequestOptions().centerCrop().error(defaultResId)).into(imageView);
+    }
+}

+ 19 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/interfaces/ILayout.java

@@ -0,0 +1,19 @@
+package com.tencent.qcloud.tuikit.timcommon.component.interfaces;
+
+import com.tencent.qcloud.tuikit.timcommon.component.TitleBarLayout;
+
+public interface ILayout {
+    /**
+     * get title bar
+     *
+     * @return
+     */
+    TitleBarLayout getTitleBar();
+
+    /**
+     * Set the parent container of this Layout
+     *
+     * @param parent
+     */
+    void setParentLayout(Object parent);
+}

+ 131 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/interfaces/ITitleBarLayout.java

@@ -0,0 +1,131 @@
+package com.tencent.qcloud.tuikit.timcommon.component.interfaces;
+
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+/**
+ * Conversation list window {@link ConversationLayout}、chat window {@link ChatLayout} have title bar,
+ * The title bar is designed as a three-part title on the left, middle and right. The left can be
+ * picture + text, the middle is text, and the right can also be picture + text. These areas return the
+ * standard Android View,These Views can be interactively processed according to business needs。
+ */
+public interface ITitleBarLayout {
+    /**
+     *
+     * Set the click event of the left header
+     *
+     * @param listener
+     */
+    void setOnLeftClickListener(View.OnClickListener listener);
+
+    /**
+     *
+     * Set the click event of the right title
+     *
+     * @param listener
+     */
+    void setOnRightClickListener(View.OnClickListener listener);
+
+    /**
+     * 
+     * set Title
+     *
+     */
+    void setTitle(String title, Position position);
+
+    /**
+     *
+     * Return to the left header area
+     *
+     * @return
+     */
+    LinearLayout getLeftGroup();
+
+    /**
+     *
+     * Return to the right header area
+     *
+     * @return
+     */
+    LinearLayout getRightGroup();
+
+    /**
+     *
+     * Returns the image for the left header
+     *
+     * @return
+     */
+    ImageView getLeftIcon();
+
+    /**
+     *
+     * Set the image for the left header
+     *
+     * @param resId
+     */
+    void setLeftIcon(int resId);
+
+    /**
+     *
+     * Returns the image with the right header
+     *
+     * @return
+     */
+    ImageView getRightIcon();
+
+    /**
+     *
+     * Set the image for the title on the right
+     *
+     * @param resId
+     */
+    void setRightIcon(int resId);
+
+    /**
+     *
+     * Returns the text of the left header
+     *
+     * @return
+     */
+    TextView getLeftTitle();
+
+    /**
+     *
+     * Returns the text of the middle title
+     *
+     * @return
+     */
+    TextView getMiddleTitle();
+
+    /**
+     *
+     * Returns the text of the title on the right
+     *
+     * @return
+     */
+    TextView getRightTitle();
+
+    /**
+     *
+     * enumeration value of the header area
+     */
+    enum Position {
+        /**
+         *
+         * left title
+         */
+        LEFT,
+        /**
+         *
+         * middle title
+         */
+        MIDDLE,
+        /**
+         *
+         * right title
+         */
+        RIGHT
+    }
+}

+ 23 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/interfaces/IUIKitCallback.java

@@ -0,0 +1,23 @@
+package com.tencent.qcloud.tuikit.timcommon.component.interfaces;
+
+public abstract class IUIKitCallback<T> {
+    public void onSuccess(T data){}
+
+    public void onError(String module, int errCode, String errMsg) {}
+
+    public void onError(int errCode, String errMsg, T data) {}
+
+    public void onProgress(Object data) {}
+
+    public static <O> void callbackOnSuccess(IUIKitCallback<O> callback, O data) {
+        if (callback != null) {
+            callback.onSuccess(data);
+        }
+    }
+
+    public static <O> void callbackOnError(IUIKitCallback<O> callback, int errCode, String errMsg, O data) {
+        if (callback != null) {
+            callback.onError(errCode, errMsg, data);
+        }
+    }
+}

+ 40 - 0
frame/tuikit/TIMCommon/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);
+    }
+}

+ 221 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/CustomGestureDetector.java

@@ -0,0 +1,221 @@
+/*
+ Copyright 2011, 2012 Chris Banes.
+ <p/>
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ <p/>
+ http://www.apache.org/licenses/LICENSE-2.0
+ <p/>
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+package com.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;
+    }
+}

+ 29 - 0
frame/tuikit/TIMCommon/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);
+}

+ 18 - 0
frame/tuikit/TIMCommon/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);
+}

+ 14 - 0
frame/tuikit/TIMCommon/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);
+}

+ 22 - 0
frame/tuikit/TIMCommon/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);
+}

+ 17 - 0
frame/tuikit/TIMCommon/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);
+}

+ 21 - 0
frame/tuikit/TIMCommon/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);
+}

+ 16 - 0
frame/tuikit/TIMCommon/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);
+}

+ 16 - 0
frame/tuikit/TIMCommon/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);
+}

+ 257 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/PhotoView.java

@@ -0,0 +1,257 @@
+/*
+ Copyright 2011, 2012 Chris Banes.
+ <p>
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ <p>
+ http://www.apache.org/licenses/LICENSE-2.0
+ <p>
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+package com.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);
+    }
+}

+ 807 - 0
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/PhotoViewAttacher.java

@@ -0,0 +1,807 @@
+/*
+ Copyright 2011, 2012 Chris Banes.
+ <p>
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ <p>
+ http://www.apache.org/licenses/LICENSE-2.0
+ <p>
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+package com.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);
+            }
+        }
+    }
+}

+ 39 - 0
frame/tuikit/TIMCommon/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;
+    }
+}

+ 40 - 0
frame/tuikit/TIMCommon/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;
+    }
+}

+ 5 - 0
frame/tuikit/TIMCommon/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 }
+}

+ 85 - 0
frame/tuikit/TIMCommon/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<VH extends RecyclerView.ViewHolder>
+    extends RecyclerView.Adapter<VH> 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<Integer> getOpenItems() {
+        return mItemManger.getOpenItems();
+    }
+
+    @Override
+    public List<SwipeLayout> 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);
+    }
+}

+ 21 - 0
frame/tuikit/TIMCommon/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) {}
+}

+ 9 - 0
frame/tuikit/TIMCommon/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);
+}

+ 219 - 0
frame/tuikit/TIMCommon/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<Integer> mOpenPositions = new HashSet<Integer>();
+    protected Set<SwipeLayout> mShownLayouts = new HashSet<SwipeLayout>();
+
+    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<Integer> getOpenItems() {
+        if (mode == Attributes.Mode.Multiple) {
+            return new ArrayList<Integer>(mOpenPositions);
+        } else {
+            return Collections.singletonList(mOpenPosition);
+        }
+    }
+
+    @Override
+    public List<SwipeLayout> getOpenLayouts() {
+        return new ArrayList<SwipeLayout>(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;
+        }
+    }
+}

+ 27 - 0
frame/tuikit/TIMCommon/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<Integer> getOpenItems();
+
+    List<SwipeLayout> getOpenLayouts();
+
+    void removeShownLayouts(SwipeLayout layout);
+
+    boolean isOpen(int position);
+
+    Attributes.Mode getMode();
+
+    void setMode(Attributes.Mode mode);
+
+    void switchAllSwipeEnable(boolean enable);
+}

+ 1806 - 0
frame/tuikit/TIMCommon/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<DragEdge, View> mDragEdges = new LinkedHashMap<>();
+    private ShowMode mShowMode;
+
+    private float[] mEdgeSwipesOffset = new float[4];
+
+    private List<SwipeListener> mSwipeListeners = new ArrayList<>();
+    private List<SwipeDenier> mSwipeDeniers = new ArrayList<>();
+    private Map<View, ArrayList<OnRevealListener>> mRevealListeners = new HashMap<>();
+    private Map<View, Boolean> mShowEntirely = new HashMap<>();
+    private Map<View, Rect> 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<OnRevealListener>());
+        }
+
+        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<View> 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<View, ArrayList<OnRevealListener>> 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<OnLayout> mOnLayoutListeners;
+
+    public void addOnLayoutListener(OnLayout l) {
+        if (mOnLayoutListeners == null) {
+            mOnLayoutListeners = new ArrayList<OnLayout>();
+        }
+        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<DragEdge, View> 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<View> 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<View> getBottomViews() {
+        ArrayList<View> bottoms = new ArrayList<View>();
+        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<DragEdge, View> entry : new HashMap<DragEdge, View>(mDragEdges).entrySet()) {
+            if (entry.getValue() == child) {
+                mDragEdges.remove(entry.getKey());
+            }
+        }
+    }
+
+    public Map<DragEdge, View> getDragEdgeMap() {
+        return mDragEdges;
+    }
+
+    /**
+     * Deprecated, use {@link #getDragEdgeMap()}
+     */
+    @Deprecated
+    public List<DragEdge> getDragEdges() {
+        return new ArrayList<DragEdge>(mDragEdges.keySet());
+    }
+
+    /**
+     * Deprecated, use {@link #setDrag(com.tencent.qcloud.tuikit.timcommon.component.swipe.SwipeLayout.DragEdge, android.view.View)}
+     */
+    @Deprecated
+    public void setDragEdges(List<DragEdge> 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();
+    }
+}

+ 74 - 0
frame/tuikit/TIMCommon/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);
+    }
+}

+ 120 - 0
frame/tuikit/TIMCommon/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();
+    }
+}

+ 153 - 0
frame/tuikit/TIMCommon/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();
+    }
+}

+ 441 - 0
frame/tuikit/TIMCommon/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) {}
+    }
+}

+ 324 - 0
frame/tuikit/TIMCommon/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);
+    }
+}

+ 336 - 0
frame/tuikit/TIMCommon/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;
+    }
+}

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů