From 21e28e134208a81c0e04a457e83e787cee5dd8bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=81=E5=B0=8F=E6=B1=9F?= <461355754@qq.com> Date: Thu, 29 May 2025 09:01:12 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E4=B8=AA=E4=BA=BA=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=E5=AE=8C=E5=96=84=202=E3=80=81=E9=9B=86=E6=88=90=E8=81=8A?= =?UTF-8?q?=E5=A4=A9=E5=8A=9F=E8=83=BD=203=E3=80=81=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- timcommon/build.gradle | 56 + timcommon/src/main/AndroidManifest.xml | 38 + .../tuikit/timcommon/TIMCommonConfig.java | 61 + .../tuikit/timcommon/TIMCommonService.java | 26 + .../tuikit/timcommon/bean/ChatFace.java | 69 + .../tuikit/timcommon/bean/CustomFace.java | 15 + .../qcloud/tuikit/timcommon/bean/Emoji.java | 15 + .../tuikit/timcommon/bean/FaceGroup.java | 98 + .../timcommon/bean/FriendProfileBean.java | 15 + .../timcommon/bean/GroupProfileBean.java | 128 + .../tuikit/timcommon/bean/MessageFeature.java | 30 + .../timcommon/bean/MessageReceiptInfo.java | 55 + .../timcommon/bean/MessageRepliesBean.java | 117 + .../tuikit/timcommon/bean/TUIMessageBean.java | 472 ++++ .../timcommon/bean/TUIReplyQuoteBean.java | 42 + .../tuikit/timcommon/bean/UserBean.java | 82 + .../widget/message/MessageBaseHolder.java | 181 ++ .../widget/message/MessageContentHolder.java | 692 ++++++ .../widget/message/SelectionHelper.java | 512 ++++ .../widget/message/TUIReplyQuoteView.java | 31 + .../component/BottomSelectSheet.java | 93 + .../component/CustomLinearLayoutManager.java | 34 + .../timcommon/component/IndicatorView.java | 126 + .../component/LineControllerView.java | 144 ++ .../component/MaxWidthFrameLayout.java | 44 + .../component/MaxWidthLinearLayout.java | 44 + .../MinimalistLineControllerView.java | 159 ++ .../component/MinimalistTitleBar.java | 39 + .../timcommon/component/PopupInputCard.java | 278 +++ .../component/RoundCornerImageView.java | 126 + .../timcommon/component/RoundFrameLayout.java | 127 + .../component/SwitchCustomWidth.java | 59 + .../timcommon/component/TitleBarLayout.java | 209 ++ .../component/UnreadCountTextView.java | 126 + .../action/PopActionClickListener.java | 5 + .../component/action/PopDialogAdapter.java | 63 + .../component/action/PopMenuAction.java | 51 + .../component/action/PopMenuAdapter.java | 81 + .../activities/BaseLightActivity.java | 48 + .../BaseMinimalistLightActivity.java | 44 + .../activities/ImageSelectActivity.java | 450 ++++ .../ImageSelectMinimalistActivity.java | 483 ++++ .../activities/SelectionActivity.java | 241 ++ .../SelectionMinimalistActivity.java | 241 ++ .../component/dialog/TUIKitDialog.java | 329 +++ .../component/face/CenterImageSpan.java | 51 + .../timcommon/component/face/FaceManager.java | 546 +++++ .../component/face/RecentEmojiManager.java | 87 + .../component/gatherimage/MultiImageData.java | 99 + .../component/gatherimage/ShadeImageView.java | 92 + .../gatherimage/SynthesizedImageView.java | 87 + .../component/gatherimage/Synthesizer.java | 12 + .../gatherimage/TeamHeadSynthesizer.java | 334 +++ .../component/gatherimage/UserIconView.java | 65 + .../highlight/HighlightPresenter.java | 136 ++ .../timcommon/component/impl/GlideEngine.java | 120 + .../component/interfaces/ILayout.java | 19 + .../component/interfaces/ITitleBarLayout.java | 131 + .../component/interfaces/IUIKitCallback.java | 23 + .../timcommon/component/photoview/Compat.java | 40 + .../photoview/CustomGestureDetector.java | 221 ++ .../photoview/OnGestureListener.java | 29 + .../photoview/OnMatrixChangedListener.java | 18 + .../photoview/OnOutsidePhotoTapListener.java | 14 + .../photoview/OnPhotoTapListener.java | 22 + .../photoview/OnScaleChangedListener.java | 17 + .../photoview/OnSingleFlingListener.java | 21 + .../photoview/OnViewDragListener.java | 16 + .../photoview/OnViewTapListener.java | 16 + .../component/photoview/PhotoView.java | 257 ++ .../photoview/PhotoViewAttacher.java | 807 +++++++ .../timcommon/component/photoview/Util.java | 39 + .../scroller/CenteredSmoothScroller.java | 40 + .../timcommon/component/swipe/Attributes.java | 5 + .../component/swipe/RecyclerSwipeAdapter.java | 85 + .../component/swipe/SimpleSwipeListener.java | 21 + .../swipe/SwipeAdapterInterface.java | 9 + .../component/swipe/SwipeItemMangerImpl.java | 219 ++ .../swipe/SwipeItemMangerInterface.java | 27 + .../component/swipe/SwipeLayout.java | 1806 ++++++++++++++ .../component/videoview/IPlayer.java | 74 + .../component/videoview/MediaPlayerProxy.java | 120 + .../videoview/SystemMediaPlayerWrapper.java | 153 ++ .../videoview/VideoGestureScaleAttacher.java | 441 ++++ .../component/videoview/VideoView.java | 324 +++ .../config/classicui/TUIConfigClassic.java | 336 +++ .../minimalistui/TUIConfigMinimalist.java | 273 +++ .../interfaces/ChatInputMoreListener.java | 10 + .../interfaces/HighlightListener.java | 10 + .../interfaces/ICommonMessageAdapter.java | 15 + .../interfaces/IMessageProperties.java | 295 +++ .../OnChatPopActionClickListener.java | 25 + .../interfaces/OnFaceInputListener.java | 14 + .../interfaces/OnItemClickListener.java | 29 + .../interfaces/UserFaceUrlCache.java | 8 + .../widget/message/MessageBaseHolder.java | 240 ++ .../widget/message/MessageContentHolder.java | 714 ++++++ .../widget/message/MessageStatusTimeView.java | 62 + .../message/MinimalistMessageLayout.java | 219 ++ .../widget/message/ReplyPreviewView.java | 168 ++ .../widget/message/TUIReplyQuoteView.java | 31 + .../widget/message/TimeInLineTextLayout.java | 160 ++ .../util/ActivityResultResolver.java | 281 +++ .../tuikit/timcommon/util/DateTimeUtil.java | 150 ++ .../tuikit/timcommon/util/FileProvider.java | 3 + .../tuikit/timcommon/util/FileUtil.java | 553 +++++ .../tuikit/timcommon/util/ImageUtil.java | 300 +++ .../tuikit/timcommon/util/LayoutUtil.java | 17 + .../tuikit/timcommon/util/MessageBuilder.java | 34 + .../tuikit/timcommon/util/MessageParser.java | 69 + .../tuikit/timcommon/util/PopWindowUtil.java | 19 + .../tuikit/timcommon/util/ScreenUtil.java | 39 + .../timcommon/util/SoftKeyBoardUtil.java | 59 + .../timcommon/util/TIMCommonConstants.java | 11 + .../tuikit/timcommon/util/TIMCommonLog.java | 77 + .../tuikit/timcommon/util/TIMCommonUtil.java | 12 + .../qcloud/tuikit/timcommon/util/TUIUtil.java | 82 + .../tuikit/timcommon/util/TextUtil.java | 155 ++ .../tuikit/timcommon/util/ThreadUtils.java | 38 + .../chat_bubble_other_bg_light.xml | 18 + .../chat_bubble_self_bg_light.xml | 17 + ...ore_default_group_icon_community_light.png | Bin 0 -> 5871 bytes .../core_default_group_icon_meeting_light.png | Bin 0 -> 3318 bytes .../core_default_group_icon_public_light.png | Bin 0 -> 3628 bytes .../core_default_group_icon_work_light.png | Bin 0 -> 4142 bytes .../core_default_user_icon_light.png | Bin 0 -> 21503 bytes .../core_online_status_light.png | Bin 0 -> 2014 bytes .../core_selected_icon_light.png | Bin 0 -> 5117 bytes .../core_title_bar_back_light.png | Bin 0 -> 1299 bytes .../drawable/chat_bubble_other_bg_light.xml | 18 + .../drawable/chat_bubble_self_bg_light.xml | 17 + .../drawable/chat_reply_icon_light.png | Bin 0 -> 1142 bytes .../drawable/core_title_bar_bg_light.xml | 9 + .../main/res-light/values/light_colors.xml | 10 + .../main/res-light/values/light_styles.xml | 28 + .../chat_bubble_other_bg_lively.xml | 22 + .../chat_bubble_self_bg_lively.xml | 17 + ...re_default_group_icon_community_lively.png | Bin 0 -> 5389 bytes ...core_default_group_icon_meeting_lively.png | Bin 0 -> 2973 bytes .../core_default_group_icon_public_lively.png | Bin 0 -> 3652 bytes .../core_default_group_icon_work_lively.png | Bin 0 -> 3817 bytes .../core_default_user_icon_lively.png | Bin 0 -> 21503 bytes .../core_online_status_lively.png | Bin 0 -> 2014 bytes .../core_selected_icon_lively.png | Bin 0 -> 5096 bytes .../core_title_bar_back_lively.png | Bin 0 -> 1677 bytes .../drawable/chat_bubble_other_bg_lively.xml | 22 + .../drawable/chat_bubble_self_bg_lively.xml | 17 + .../drawable/chat_reply_icon_lively.png | Bin 0 -> 1172 bytes .../drawable/core_title_bar_bg_lively.xml | 8 + .../main/res-lively/values/lively_colors.xml | 10 + .../main/res-lively/values/lively_styles.xml | 30 + .../chat_bubble_other_bg_serious.xml | 22 + .../chat_bubble_self_bg_serious.xml | 17 + ...e_default_group_icon_community_serious.png | Bin 0 -> 6023 bytes ...ore_default_group_icon_meeting_serious.png | Bin 0 -> 3260 bytes ...core_default_group_icon_public_serious.png | Bin 0 -> 3894 bytes .../core_default_group_icon_work_serious.png | Bin 0 -> 4171 bytes .../core_default_user_icon_serious.png | Bin 0 -> 21503 bytes .../core_online_status_serious.png | Bin 0 -> 2012 bytes .../core_selected_icon_serious.png | Bin 0 -> 5224 bytes .../core_title_bar_back_serious.png | Bin 0 -> 1677 bytes .../drawable/chat_bubble_other_bg_serious.xml | 22 + .../drawable/chat_bubble_self_bg_serious.xml | 17 + .../drawable/chat_reply_icon_serious.png | Bin 0 -> 1142 bytes .../drawable/core_title_bar_bg_serious.xml | 9 + .../res-serious/values/serious_colors.xml | 9 + .../res-serious/values/serious_styles.xml | 30 + .../anim/common_bottom_select_sheet_enter.xml | 8 + .../anim/common_bottom_select_sheet_exit.xml | 8 + .../src/main/res/anim/core_popup_in_anim.xml | 8 + .../src/main/res/anim/core_popup_out_anim.xml | 8 + .../main/res/color/common_bg_negative_btn.xml | 17 + .../main/res/color/common_bg_positive_btn.xml | 17 + .../chat_bubble_other_transparent_bg.xml | 12 + .../chat_bubble_self_transparent_bg.xml | 16 + .../chat_message_popup_fill_border_right.xml | 8 + ...message_popup_risk_content_border_left.xml | 17 + ...essage_popup_risk_content_border_right.xml | 18 + .../chat_message_popup_stroke_border_left.xml | 9 + ...chat_message_popup_stroke_border_right.xml | 9 + .../chat_bubble_other_transparent_bg.xml | 12 + .../chat_bubble_self_transparent_bg.xml | 16 + .../res/drawable/chat_checkbox_selector.xml | 8 + .../res/drawable/chat_gray_round_rect_bg.xml | 8 + .../drawable/chat_message_bottom_area_bg.xml | 9 + .../chat_message_bottom_area_risk_bg.xml | 9 + .../chat_message_popup_fill_border.xml | 8 + .../chat_message_popup_fill_border_right.xml | 8 + ...message_popup_risk_content_border_left.xml | 17 + ...essage_popup_risk_content_border_right.xml | 18 + .../chat_message_popup_stroke_border.xml | 9 + .../chat_message_popup_stroke_border_left.xml | 9 + ...chat_message_popup_stroke_border_right.xml | 9 + .../chat_minimalist_anim_loading00.png | Bin 0 -> 6618 bytes .../chat_minimalist_anim_loading01.png | Bin 0 -> 490 bytes .../chat_minimalist_anim_loading02.png | Bin 0 -> 474 bytes .../chat_minimalist_anim_loading03.png | Bin 0 -> 476 bytes .../chat_minimalist_anim_loading04.png | Bin 0 -> 493 bytes .../chat_minimalist_anim_loading05.png | Bin 0 -> 494 bytes .../chat_minimalist_anim_loading06.png | Bin 0 -> 496 bytes .../chat_minimalist_anim_loading07.png | Bin 0 -> 495 bytes .../chat_minimalist_anim_loading08.png | Bin 0 -> 490 bytes .../chat_minimalist_anim_loading09.png | Bin 0 -> 490 bytes .../chat_minimalist_anim_loading10.png | Bin 0 -> 485 bytes .../chat_minimalist_anim_loading11.png | Bin 0 -> 488 bytes .../chat_minimalist_anim_loading12.png | Bin 0 -> 487 bytes .../chat_minimalist_anim_loading13.png | Bin 0 -> 485 bytes .../chat_minimalist_anim_loading14.png | Bin 0 -> 488 bytes .../chat_minimalist_anim_loading15.png | Bin 0 -> 490 bytes .../chat_minimalist_anim_loading16.png | Bin 0 -> 486 bytes .../chat_minimalist_anim_loading17.png | Bin 0 -> 490 bytes .../chat_minimalist_anim_loading18.png | Bin 0 -> 489 bytes .../chat_minimalist_anim_loading19.png | Bin 0 -> 488 bytes .../chat_minimalist_anim_loading20.png | Bin 0 -> 492 bytes .../chat_minimalist_anim_loading21.png | Bin 0 -> 491 bytes .../chat_minimalist_anim_loading22.png | Bin 0 -> 487 bytes .../chat_minimalist_anim_loading23.png | Bin 0 -> 488 bytes .../chat_minimalist_anim_loading24.png | Bin 0 -> 485 bytes .../chat_minimalist_anim_loading25.png | Bin 0 -> 486 bytes .../chat_minimalist_anim_loading26.png | Bin 0 -> 495 bytes .../chat_minimalist_anim_loading27.png | Bin 0 -> 490 bytes .../chat_minimalist_anim_loading28.png | Bin 0 -> 496 bytes .../chat_minimalist_anim_loading29.png | Bin 0 -> 486 bytes .../chat_minimalist_anim_loading30.png | Bin 0 -> 485 bytes .../chat_minimalist_anim_loading31.png | Bin 0 -> 492 bytes .../chat_minimalist_anim_loading32.png | Bin 0 -> 485 bytes .../chat_minimalist_anim_loading33.png | Bin 0 -> 488 bytes .../chat_minimalist_anim_loading34.png | Bin 0 -> 484 bytes .../chat_minimalist_anim_loading35.png | Bin 0 -> 467 bytes .../chat_minimalist_anim_loading36.png | Bin 0 -> 483 bytes .../chat_minimalist_anim_loading37.png | Bin 0 -> 489 bytes .../chat_minimalist_anim_loading38.png | Bin 0 -> 480 bytes .../chat_minimalist_anim_loading39.png | Bin 0 -> 486 bytes .../chat_minimalist_anim_loading40.png | Bin 0 -> 490 bytes .../chat_minimalist_anim_loading41.png | Bin 0 -> 486 bytes .../chat_minimalist_anim_loading42.png | Bin 0 -> 498 bytes .../chat_minimalist_anim_loading43.png | Bin 0 -> 492 bytes .../chat_minimalist_anim_loading44.png | Bin 0 -> 490 bytes .../chat_minimalist_file_download_icon.png | Bin 0 -> 633 bytes ...inimalist_message_status_send_all_read.png | Bin 0 -> 358 bytes ..._minimalist_message_status_send_failed.png | Bin 0 -> 616 bytes ...minimalist_message_status_send_no_read.png | Bin 0 -> 258 bytes ...nimalist_message_status_send_part_read.png | Bin 0 -> 317 bytes .../chat_minimalist_status_loading_anim.xml | 94 + .../src/main/res/drawable/chat_react_bg.xml | 8 + .../res/drawable/chat_reply_more_icon.png | Bin 0 -> 437 bytes .../res/drawable/chat_unselected_icon.png | Bin 0 -> 1027 bytes .../main/res/drawable/common_arrow_right.png | Bin 0 -> 1224 bytes .../drawable/common_bottom_sheet_border.xml | 5 + .../drawable/common_check_box_selected.png | Bin 0 -> 2427 bytes .../drawable/common_check_box_unselected.png | Bin 0 -> 2058 bytes .../res/drawable/common_dialog_react_bg.xml | 8 + .../main/res/drawable/common_edit_cursor.xml | 6 + .../main/res/drawable/common_edit_text_bg.xml | 5 + .../common_item_pressed_effect_background.xml | 10 + .../drawable/common_title_bar_home_icon.png | Bin 0 -> 1690 bytes .../src/main/res/drawable/common_trans_bg.png | Bin 0 -> 122 bytes .../src/main/res/drawable/core_close_icon.png | Bin 0 -> 507 bytes .../core_default_group_icon_community.png | Bin 0 -> 14690 bytes .../main/res/drawable/core_delete_icon.png | Bin 0 -> 3385 bytes .../res/drawable/core_icon_offline_status.png | Bin 0 -> 1143 bytes .../main/res/drawable/core_list_divider.xml | 11 + .../drawable/core_minimalist_back_icon.png | Bin 0 -> 293 bytes .../res/drawable/core_positive_btn_bg.xml | 7 + .../drawable/core_positive_btn_disable_bg.xml | 11 + .../drawable/core_positive_btn_normal_bg.xml | 11 + .../drawable/core_positive_btn_pressed_bg.xml | 11 + .../main/res/drawable/core_search_icon.png | Bin 0 -> 1286 bytes .../res/drawable/indicator_point_nomal.png | Bin 0 -> 216 bytes .../res/drawable/indicator_point_select.png | Bin 0 -> 248 bytes .../main/res/drawable/message_send_fail.png | Bin 0 -> 701 bytes .../res/drawable/minimalist_switch_thumb.xml | 25 + .../res/drawable/minimalist_switch_track.xml | 18 + .../minimalist_translation_area_bg.xml | 9 + .../src/main/res/drawable/popup_card_bg.xml | 13 + .../res/drawable/quote_message_area_bg.xml | 9 + .../src/main/res/drawable/selected_border.xml | 9 + .../src/main/res/drawable/switch_thumb.xml | 5 + .../main/res/drawable/switch_thumb_blue.xml | 14 + .../main/res/drawable/switch_thumb_gray.xml | 16 + .../src/main/res/drawable/switch_track.xml | 5 + .../main/res/drawable/switch_track_blue.xml | 13 + .../main/res/drawable/switch_track_gray.xml | 16 + .../chat_minimalist_reply_preview_layout.xml | 53 + .../chat_minimalist_text_status_layout.xml | 30 + .../res/layout/common_bottom_select_sheet.xml | 29 + .../res/layout/common_bottom_sheet_item.xml | 15 + .../res/layout/common_dialog_view_layout.xml | 71 + .../res/layout/common_profile_icon_view.xml | 14 + .../core_activity_image_select_layout.xml | 24 + ...inimalist_activity_image_select_layout.xml | 22 + .../core_minimalist_selection_activity.xml | 27 + .../src/main/res/layout/core_pop_menu.xml | 16 + .../layout/core_select_image_item_layout.xml | 60 + .../res/layout/core_select_item_layout.xml | 50 + .../layout/message_adapter_item_content.xml | 277 +++ .../minimalist_line_controller_view.xml | 104 + ...inimalist_message_adapter_item_content.xml | 206 ++ .../main/res/layout/pop_dialog_adapter.xml | 20 + .../src/main/res/layout/pop_menu_adapter.xml | 25 + .../layout/timcommon_layout_popup_card.xml | 83 + .../layout/timcommon_line_controller_view.xml | 100 + .../res/layout/timcommon_title_bar_layout.xml | 112 + .../res/layout/tuicore_selection_activity.xml | 27 + timcommon/src/main/res/values-ar/strings.xml | 33 + .../src/main/res/values-zh-rHK/strings.xml | 32 + timcommon/src/main/res/values-zh/strings.xml | 34 + timcommon/src/main/res/values/attrs.xml | 85 + timcommon/src/main/res/values/colors.xml | 49 + timcommon/src/main/res/values/dimens.xml | 57 + timcommon/src/main/res/values/strings.xml | 34 + timcommon/src/main/res/values/styles.xml | 45 + .../src/main/res/values/tui_theme_attrs.xml | 25 + .../src/main/res/xml/file_paths_public.xml | 26 + tuichat/build.gradle | 69 + tuichat/src/main/AndroidManifest.xml | 99 + .../main/assets/chatbuildinemojis/emoji_0.png | Bin 0 -> 27901 bytes .../main/assets/chatbuildinemojis/emoji_1.png | Bin 0 -> 29015 bytes .../assets/chatbuildinemojis/emoji_10.png | Bin 0 -> 26412 bytes .../assets/chatbuildinemojis/emoji_11.png | Bin 0 -> 26589 bytes .../assets/chatbuildinemojis/emoji_12.png | Bin 0 -> 30220 bytes .../assets/chatbuildinemojis/emoji_13.png | Bin 0 -> 26317 bytes .../assets/chatbuildinemojis/emoji_14.png | Bin 0 -> 31141 bytes .../assets/chatbuildinemojis/emoji_15.png | Bin 0 -> 26937 bytes .../assets/chatbuildinemojis/emoji_16.png | Bin 0 -> 27688 bytes .../assets/chatbuildinemojis/emoji_17.png | Bin 0 -> 26747 bytes .../assets/chatbuildinemojis/emoji_18.png | Bin 0 -> 26471 bytes .../assets/chatbuildinemojis/emoji_19.png | Bin 0 -> 30065 bytes .../main/assets/chatbuildinemojis/emoji_2.png | Bin 0 -> 28589 bytes .../assets/chatbuildinemojis/emoji_20.png | Bin 0 -> 26832 bytes .../assets/chatbuildinemojis/emoji_21.png | Bin 0 -> 30027 bytes .../assets/chatbuildinemojis/emoji_22.png | Bin 0 -> 26535 bytes .../assets/chatbuildinemojis/emoji_23.png | Bin 0 -> 26389 bytes .../assets/chatbuildinemojis/emoji_24.png | Bin 0 -> 32967 bytes .../assets/chatbuildinemojis/emoji_25.png | Bin 0 -> 28457 bytes .../assets/chatbuildinemojis/emoji_26.png | Bin 0 -> 25598 bytes .../assets/chatbuildinemojis/emoji_27.png | Bin 0 -> 27320 bytes .../assets/chatbuildinemojis/emoji_28.png | Bin 0 -> 26646 bytes .../assets/chatbuildinemojis/emoji_29.png | Bin 0 -> 25354 bytes .../main/assets/chatbuildinemojis/emoji_3.png | Bin 0 -> 28794 bytes .../assets/chatbuildinemojis/emoji_30.png | Bin 0 -> 25307 bytes .../assets/chatbuildinemojis/emoji_31.png | Bin 0 -> 29128 bytes .../assets/chatbuildinemojis/emoji_32.png | Bin 0 -> 11105 bytes .../assets/chatbuildinemojis/emoji_33.png | Bin 0 -> 24337 bytes .../assets/chatbuildinemojis/emoji_34.png | Bin 0 -> 28007 bytes .../assets/chatbuildinemojis/emoji_35.png | Bin 0 -> 27913 bytes .../assets/chatbuildinemojis/emoji_36.png | Bin 0 -> 24586 bytes .../assets/chatbuildinemojis/emoji_37.png | Bin 0 -> 13295 bytes .../assets/chatbuildinemojis/emoji_38.png | Bin 0 -> 21595 bytes .../assets/chatbuildinemojis/emoji_39.png | Bin 0 -> 23536 bytes .../main/assets/chatbuildinemojis/emoji_4.png | Bin 0 -> 27558 bytes .../assets/chatbuildinemojis/emoji_40.png | Bin 0 -> 16114 bytes .../assets/chatbuildinemojis/emoji_41.png | Bin 0 -> 22226 bytes .../assets/chatbuildinemojis/emoji_42.png | Bin 0 -> 9600 bytes .../assets/chatbuildinemojis/emoji_43.png | Bin 0 -> 14158 bytes .../assets/chatbuildinemojis/emoji_44.png | Bin 0 -> 16098 bytes .../assets/chatbuildinemojis/emoji_45.png | Bin 0 -> 29173 bytes .../assets/chatbuildinemojis/emoji_46.png | Bin 0 -> 21639 bytes .../assets/chatbuildinemojis/emoji_47.png | Bin 0 -> 28823 bytes .../assets/chatbuildinemojis/emoji_48.png | Bin 0 -> 14016 bytes .../assets/chatbuildinemojis/emoji_49.png | Bin 0 -> 24449 bytes .../main/assets/chatbuildinemojis/emoji_5.png | Bin 0 -> 27868 bytes .../assets/chatbuildinemojis/emoji_50.png | Bin 0 -> 21667 bytes .../assets/chatbuildinemojis/emoji_51.png | Bin 0 -> 25282 bytes .../assets/chatbuildinemojis/emoji_52.png | Bin 0 -> 20685 bytes .../assets/chatbuildinemojis/emoji_53.png | Bin 0 -> 16999 bytes .../assets/chatbuildinemojis/emoji_54.png | Bin 0 -> 21790 bytes .../assets/chatbuildinemojis/emoji_55.png | Bin 0 -> 15568 bytes .../assets/chatbuildinemojis/emoji_56.png | Bin 0 -> 15163 bytes .../assets/chatbuildinemojis/emoji_57.png | Bin 0 -> 15187 bytes .../assets/chatbuildinemojis/emoji_58.png | Bin 0 -> 15799 bytes .../assets/chatbuildinemojis/emoji_59.png | Bin 0 -> 28142 bytes .../main/assets/chatbuildinemojis/emoji_6.png | Bin 0 -> 28127 bytes .../assets/chatbuildinemojis/emoji_60.png | Bin 0 -> 18420 bytes .../assets/chatbuildinemojis/emoji_61.png | Bin 0 -> 23302 bytes .../main/assets/chatbuildinemojis/emoji_7.png | Bin 0 -> 26410 bytes .../main/assets/chatbuildinemojis/emoji_8.png | Bin 0 -> 23987 bytes .../main/assets/chatbuildinemojis/emoji_9.png | Bin 0 -> 27463 bytes .../tuikit/tuichat/TUIChatConstants.java | 98 + .../qcloud/tuikit/tuichat/TUIChatService.java | 876 +++++++ .../tuikit/tuichat/bean/C2CChatInfo.java | 8 + .../qcloud/tuikit/tuichat/bean/CallModel.java | 580 +++++ .../qcloud/tuikit/tuichat/bean/ChatInfo.java | 245 ++ .../tuichat/bean/CustomHelloMessage.java | 17 + .../qcloud/tuikit/tuichat/bean/DraftInfo.java | 30 + .../tuikit/tuichat/bean/GroupApplyInfo.java | 25 + .../tuikit/tuichat/bean/GroupChatInfo.java | 81 + .../tuikit/tuichat/bean/GroupMemberBean.java | 15 + .../tuikit/tuichat/bean/GroupMemberInfo.java | 123 + .../bean/GroupMessageReadMembersInfo.java | 37 + .../tuikit/tuichat/bean/InputMoreItem.java | 111 + .../tuikit/tuichat/bean/LocalTipsMessage.java | 8 + .../tuikit/tuichat/bean/MessageCustom.java | 12 + .../tuikit/tuichat/bean/MessageTyping.java | 35 + .../tuikit/tuichat/bean/OfflinePushInfo.java | 182 ++ .../tuikit/tuichat/bean/ReplyPreviewBean.java | 111 + .../tuikit/tuichat/bean/UserStatusBean.java | 29 + .../bean/message/CallingMessageBean.java | 96 + .../bean/message/CallingTipsMessageBean.java | 72 + .../message/CustomEvaluationMessageBean.java | 70 + .../bean/message/CustomLinkMessageBean.java | 58 + .../bean/message/CustomOrderMessageBean.java | 94 + .../bean/message/EmptyMessageBean.java | 16 + .../tuichat/bean/message/FaceMessageBean.java | 50 + .../tuichat/bean/message/FileMessageBean.java | 55 + .../bean/message/ImageMessageBean.java | 158 ++ .../bean/message/LocationMessageBean.java | 41 + .../bean/message/MergeMessageBean.java | 57 + .../bean/message/MessageTypingBean.java | 40 + .../bean/message/QuoteMessageBean.java | 152 ++ .../bean/message/ReplyMessageBean.java | 19 + .../bean/message/SoundMessageBean.java | 77 + .../bean/message/TextAtMessageBean.java | 3 + .../tuichat/bean/message/TextMessageBean.java | 36 + .../tuichat/bean/message/TipsMessageBean.java | 346 +++ .../bean/message/VideoMessageBean.java | 71 + ...CustomEvaluationMessageReplyQuoteBean.java | 13 + .../reply/CustomLinkReplyQuoteBean.java | 13 + .../CustomOrderMessageReplyQuoteBean.java | 13 + .../message/reply/FaceReplyQuoteBean.java | 30 + .../message/reply/FileReplyQuoteBean.java | 20 + .../message/reply/ImageReplyQuoteBean.java | 12 + .../message/reply/LocationReplyQuoteBean.java | 9 + .../message/reply/MergeReplyQuoteBean.java | 9 + .../message/reply/ReplyReplyQuoteBean.java | 13 + .../message/reply/SoundReplyQuoteBean.java | 24 + .../message/reply/TextReplyQuoteBean.java | 24 + .../message/reply/VideoReplyQuoteBean.java | 9 + .../tuichat/classicui/ClassicUIService.java | 358 +++ .../component/noticelayout/NoticeLayout.java | 71 + .../noticelayout/NoticeLayoutConfig.java | 23 + .../component/popmenu/ChatPopMenu.java | 382 +++ .../classicui/interfaces/IChatLayout.java | 43 + .../classicui/page/FriendProfileActivity.java | 110 + .../classicui/page/GroupInfoActivity.java | 21 + .../classicui/page/GroupInfoFragment.java | 119 + .../classicui/page/GroupNoticeActivity.java | 137 ++ .../page/MessageReceiptDetailActivity.java | 400 +++ .../page/MessageReplyDetailActivity.java | 194 ++ .../classicui/page/TUIBaseChatActivity.java | 103 + .../classicui/page/TUIBaseChatFragment.java | 439 ++++ .../classicui/page/TUIC2CChatActivity.java | 45 + .../classicui/page/TUIC2CChatFragment.java | 132 + .../page/TUIForwardChatActivity.java | 107 + .../classicui/page/TUIGroupChatActivity.java | 29 + .../classicui/page/TUIGroupChatFragment.java | 143 ++ .../tuichat/classicui/widget/ChatView.java | 1623 +++++++++++++ .../widget/input/BaseInputFragment.java | 18 + .../classicui/widget/input/InputView.java | 1328 ++++++++++ .../widget/input/ReplyPreviewBar.java | 28 + .../classicui/widget/input/ShortcutView.java | 93 + .../inputmore/ActionsGridViewAdapter.java | 65 + .../input/inputmore/ActionsPagerAdapter.java | 80 + .../input/inputmore/InputMoreFragment.java | 32 + .../input/inputmore/InputMoreLayout.java | 41 + .../widget/message/MessageAdapter.java | 486 ++++ .../widget/message/MessageRecyclerView.java | 825 +++++++ .../message/reply/FaceReplyQuoteView.java | 40 + .../message/reply/FileReplyQuoteView.java | 49 + .../message/reply/ImageReplyQuoteView.java | 79 + .../message/reply/LocationReplyQuoteView.java | 20 + .../message/reply/MergeReplyQuoteView.java | 68 + .../message/reply/ReplyDetailsView.java | 131 + .../message/reply/SoundReplyQuoteView.java | 50 + .../message/reply/TextReplyQuoteView.java | 44 + .../message/reply/VideoReplyQuoteView.java | 55 + .../viewholder/CallingMessageHolder.java | 90 + .../CustomEvaluationMessageHolder.java | 53 + .../viewholder/CustomLinkMessageHolder.java | 68 + .../viewholder/CustomOrderMessageHolder.java | 90 + .../viewholder/EmptyMessageHolder.java | 15 + .../message/viewholder/FaceMessageHolder.java | 73 + .../message/viewholder/FileMessageHolder.java | 407 ++++ .../viewholder/ImageMessageHolder.java | 254 ++ .../viewholder/LocationMessageHolder.java | 24 + .../viewholder/MergeMessageHolder.java | 167 ++ .../message/viewholder/MessageHeadHolder.java | 27 + .../message/viewholder/MessageTailHolder.java | 19 + .../viewholder/MessageViewHolderFactory.java | 69 + .../viewholder/QuoteMessageHolder.java | 327 +++ .../viewholder/ReplyMessageHolder.java | 234 ++ .../viewholder/SoundMessageHolder.java | 158 ++ .../message/viewholder/TextMessageHolder.java | 133 + .../message/viewholder/TipsMessageHolder.java | 160 ++ .../viewholder/VideoMessageHolder.java | 288 +++ .../widget/profile/FriendProfileLayout.java | 385 +++ .../widget/profile/GroupInfoLayout.java | 695 ++++++ .../tuichat/component/album/AlbumPicker.java | 27 + .../album/ChatMultimediaRecorderImpl.java | 54 + .../album/SystemAlbumPickerImpl.java | 32 + .../album/SystemMultimediaRecorderImpl.java | 117 + .../component/album/VideoRecorder.java | 108 + .../audio/AIDenoiseAudioRecordImpl.java | 246 ++ .../tuichat/component/audio/AudioPlayer.java | 104 + .../component/audio/AudioRecorder.java | 217 ++ .../audio/SystemAudioRecordImpl.java | 109 + .../component/camera/CameraActivity.java | 156 ++ .../tuichat/component/camera/CameraUtil.java | 222 ++ .../camera/listener/CameraListener.java | 7 + .../camera/listener/CaptureListener.java | 15 + .../camera/listener/ClickListener.java | 5 + .../camera/listener/ErrorListener.java | 5 + .../camera/listener/ReturnListener.java | 5 + .../camera/listener/TypeListener.java | 7 + .../camera/state/BrowserPictureState.java | 47 + .../camera/state/BrowserVideoState.java | 48 + .../component/camera/state/CameraMachine.java | 115 + .../component/camera/state/PreviewState.java | 105 + .../tuichat/component/camera/state/State.java | 38 + .../camera/view/CameraInterface.java | 572 +++++ .../component/camera/view/CameraView.java | 584 +++++ .../component/camera/view/CaptureButton.java | 352 +++ .../component/camera/view/CaptureLayout.java | 270 +++ .../component/camera/view/FocusView.java | 55 + .../component/camera/view/ICameraView.java | 21 + .../component/camera/view/ReturnButton.java | 52 + .../component/camera/view/TypeButton.java | 106 + .../component/face/EmojiViewHolder.java | 164 ++ .../tuichat/component/face/FaceFragment.java | 60 + .../component/face/FaceListAdapter.java | 86 + .../component/face/FacePagerAdapter.java | 59 + .../tuichat/component/face/FaceView.java | 215 ++ .../component/face/FaceViewHolder.java | 71 + .../ImageVideoBrowseActivity.java | 201 ++ .../ImageVideoBrowseAdapter.java | 695 ++++++ .../ImageVideoBrowseListener.java | 22 + .../ImageVideoBrowsePresenter.java | 271 +++ .../ImageVideoBrowseProvider.java | 143 ++ .../inputedittext/TIMMentionEditText.java | 301 +++ .../component/pinned/GroupPinnedView.java | 376 +++ .../progress/ChatRingProgressBar.java | 59 + .../component/progress/ProgressPresenter.java | 106 + .../tuichat/config/ChatEventConfig.java | 20 + .../tuikit/tuichat/config/FriendConfig.java | 134 ++ .../tuikit/tuichat/config/GeneralConfig.java | 196 ++ .../tuikit/tuichat/config/GroupConfig.java | 160 ++ .../tuichat/config/ShortcutMenuConfig.java | 46 + .../tuikit/tuichat/config/TUIChatConfigs.java | 118 + .../classicui/TUIChatConfigClassic.java | 776 ++++++ .../minimalistui/TUIChatConfigMinimalist.java | 758 ++++++ .../interfaces/AlbumPickerListener.java | 15 + .../interfaces/C2CChatEventListener.java | 33 + .../tuichat/interfaces/ChatEventListener.java | 44 + .../interfaces/FriendProfileListener.java | 15 + .../interfaces/GroupChatEventListener.java | 48 + .../interfaces/GroupProfileListener.java | 17 + .../tuichat/interfaces/IAlbumPicker.java | 10 + .../interfaces/IBaseMessageSender.java | 15 + .../tuichat/interfaces/IDownloadProvider.java | 15 + .../tuichat/interfaces/IGroupPinnedView.java | 9 + .../tuichat/interfaces/IMessageAdapter.java | 14 + .../interfaces/IMessageDetailListener.java | 7 + .../interfaces/IMessageRecyclerView.java | 34 + .../interfaces/IMultimediaRecorder.java | 10 + .../interfaces/IReplyMessageHandler.java | 12 + .../MultimediaRecorderListener.java | 10 + .../interfaces/NetworkConnectionListener.java | 5 + .../interfaces/OnEmptySpaceClickListener.java | 6 + .../interfaces/OnGestureScrollListener.java | 7 + .../interfaces/TotalUnreadCountListener.java | 5 + .../minimalistui/MinimalistUIService.java | 304 +++ .../dialog/ChatBottomSelectSheet.java | 91 + .../component/noticelayout/NoticeLayout.java | 72 + .../noticelayout/NoticeLayoutConfig.java | 23 + .../minimalistui/interfaces/IChatLayout.java | 39 + .../page/FriendProfileMinimalistActivity.java | 100 + .../page/GroupInfoMinimalistActivity.java | 21 + .../page/GroupInfoMinimalistFragment.java | 121 + .../page/GroupNoticeMinimalistActivity.java | 141 ++ .../page/MessageDetailMinimalistActivity.java | 358 +++ .../page/TUIBaseChatMinimalistActivity.java | 102 + .../page/TUIBaseChatMinimalistFragment.java | 401 ++++ .../page/TUIC2CChatMinimalistActivity.java | 28 + .../page/TUIC2CChatMinimalistFragment.java | 82 + .../TUIForwardChatMinimalistActivity.java | 104 + .../page/TUIGroupChatMinimalistActivity.java | 32 + .../page/TUIGroupChatMinimalistFragment.java | 97 + .../tuichat/minimalistui/widget/ChatView.java | 1610 +++++++++++++ .../minimalistui/widget/input/InputView.java | 1545 ++++++++++++ .../inputmore/InputMoreDialogFragment.java | 133 + .../widget/input/waveview/VoiceWaveView.kt | 309 +++ .../widget/input/waveview/WaveMode.kt | 11 + .../message/ChatReplyDialogFragment.java | 178 ++ .../widget/message/MessageAdapter.java | 523 ++++ .../widget/message/MessageRecyclerView.java | 755 ++++++ .../message/reply/FaceReplyQuoteView.java | 40 + .../message/reply/FileReplyQuoteView.java | 49 + .../message/reply/ImageReplyQuoteView.java | 79 + .../message/reply/LocationReplyQuoteView.java | 22 + .../message/reply/MergeReplyQuoteView.java | 67 + .../message/reply/ReplyDetailsView.java | 163 ++ .../message/reply/SoundReplyQuoteView.java | 49 + .../message/reply/TextReplyQuoteView.java | 44 + .../message/reply/VideoReplyQuoteView.java | 53 + .../viewholder/CallingMessageHolder.java | 100 + .../CustomEvaluationMessageHolder.java | 54 + .../viewholder/CustomLinkMessageHolder.java | 69 + .../viewholder/CustomOrderMessageHolder.java | 90 + .../viewholder/EmptyMessageHolder.java | 15 + .../message/viewholder/FaceMessageHolder.java | 74 + .../message/viewholder/FileMessageHolder.java | 218 ++ .../viewholder/ImageMessageHolder.java | 273 +++ .../viewholder/LocationMessageHolder.java | 24 + .../viewholder/MergeMessageHolder.java | 142 ++ .../message/viewholder/MessageHeadHolder.java | 26 + .../message/viewholder/MessageTailHolder.java | 15 + .../viewholder/MessageViewHolderFactory.java | 68 + .../viewholder/QuoteMessageHolder.java | 289 +++ .../viewholder/ReplyMessageHolder.java | 165 ++ .../viewholder/SoundMessageHolder.java | 154 ++ .../message/viewholder/TextMessageHolder.java | 69 + .../message/viewholder/TipsMessageHolder.java | 161 ++ .../viewholder/VideoMessageHolder.java | 254 ++ .../messagepopmenu/ChatPopActivity.java | 400 +++ .../messagepopmenu/ChatPopDataHolder.java | 74 + .../widget/profile/FriendProfileLayout.java | 453 ++++ .../widget/profile/GroupInfoLayout.java | 784 ++++++ .../model/AIDenoiseSignatureManager.java | 63 + .../model/ChatFileDownloadProvider.java | 186 ++ .../tuikit/tuichat/model/ChatProvider.java | 1094 +++++++++ .../tuikit/tuichat/model/ProfileProvider.java | 496 ++++ .../tuichat/presenter/C2CChatPresenter.java | 331 +++ .../presenter/ChatFileDownloadPresenter.java | 174 ++ .../presenter/ChatFileDownloadProxy.java | 239 ++ .../presenter/ChatModifyMessageHelper.java | 132 + .../tuichat/presenter/ChatPresenter.java | 2136 +++++++++++++++++ .../tuichat/presenter/ForwardPresenter.java | 141 ++ .../presenter/FriendProfilePresenter.java | 265 ++ .../tuichat/presenter/GroupChatPresenter.java | 531 ++++ .../presenter/GroupProfilePresenter.java | 316 +++ .../presenter/MessageReceiptPresenter.java | 107 + .../tuichat/presenter/ReplyPresenter.java | 212 ++ .../qcloud/tuikit/tuichat/util/AngleUtil.java | 44 + .../qcloud/tuikit/tuichat/util/BlurUtils.java | 32 + .../tuichat/util/ChatMessageBuilder.java | 356 +++ .../tuichat/util/ChatMessageParser.java | 591 +++++ .../tuikit/tuichat/util/DataStoreUtil.java | 90 + .../tuikit/tuichat/util/DeviceUtil.java | 34 + .../qcloud/tuikit/tuichat/util/FileUtil.java | 316 +++ .../tuichat/util/OfflinePushInfoUtils.java | 25 + .../tuikit/tuichat/util/PermissionHelper.java | 91 + .../tuikit/tuichat/util/TUIChatLog.java | 71 + .../tuikit/tuichat/util/TUIChatUtils.java | 163 ++ .../chat_ic_arrow_down_light.png | Bin 0 -> 692 bytes .../chat_ic_arrow_up_light.png | Bin 0 -> 689 bytes .../chat_title_bar_more_menu_light.png | Bin 0 -> 345 bytes .../main/res-light/values/light_colors.xml | 31 + .../main/res-light/values/light_styles.xml | 35 + .../chat_ic_arrow_down_lively.png | Bin 0 -> 1576 bytes .../chat_ic_arrow_up_lively.png | Bin 0 -> 1576 bytes .../chat_title_bar_more_menu_lively.png | Bin 0 -> 636 bytes .../main/res-lively/values/lively_colors.xml | 32 + .../main/res-lively/values/lively_styles.xml | 36 + .../anim/chat_bottom_select_sheet_enter.xml | 8 + .../anim/chat_bottom_select_sheet_exit.xml | 8 + .../drawable/chat_audio_play_btn_ic.png | Bin 0 -> 336 bytes .../drawable/chat_audio_stop_btn_ic.png | Bin 0 -> 211 bytes .../drawable/chat_bottom_sheet_border.xml | 5 + .../chat_bottom_sheet_dialog_border.xml | 5 + .../drawable/chat_bottom_sheet_hide.png | Bin 0 -> 351 bytes ...ct_profile_item_extension_message_icon.png | Bin 0 -> 1226 bytes ...hat_face_group_list_item_select_border.xml | 7 + .../drawable/chat_file_type_icon_image.png | Bin 0 -> 1888 bytes .../drawable/chat_file_type_icon_none.png | Bin 0 -> 1584 bytes .../drawable/chat_file_type_icon_pdf.png | Bin 0 -> 2853 bytes .../drawable/chat_file_type_icon_ppt.png | Bin 0 -> 1102 bytes .../drawable/chat_file_type_icon_text.png | Bin 0 -> 1045 bytes .../drawable/chat_file_type_icon_word.png | Bin 0 -> 2484 bytes .../drawable/chat_file_type_icon_xls.png | Bin 0 -> 2117 bytes .../drawable/chat_file_type_icon_zip.png | Bin 0 -> 1287 bytes .../drawable/chat_minimalist_delete_icon.png | Bin 0 -> 410 bytes .../drawable/chat_minimalist_forward_icon.png | Bin 0 -> 533 bytes .../chat_minimalist_input_add_icon.png | Bin 0 -> 307 bytes .../chat_minimalist_input_camera_icon.png | Bin 0 -> 767 bytes .../chat_minimalist_input_edittext_border.xml | 9 + .../chat_minimalist_input_face_icon.png | Bin 0 -> 3006 bytes .../chat_minimalist_input_voice_icon.png | Bin 0 -> 759 bytes .../drawable/chat_minimalist_jump_at_icon.png | Bin 0 -> 1217 bytes .../chat_minimalist_jump_down_icon.png | Bin 0 -> 892 bytes ...chat_minimalist_menu_emoji_list_border.xml | 8 + .../chat_minimalist_menu_popup_border.xml | 8 + .../chat_minimalist_merge_message_border.xml | 6 + ...minimalist_message_deatail_read_border.xml | 8 + ...chat_minimalist_message_status_sending.png | Bin 0 -> 578 bytes ...hat_minimalist_more_action_camera_icon.png | Bin 0 -> 777 bytes ...hat_minimalist_more_action_custom_icon.png | Bin 0 -> 559 bytes .../chat_minimalist_more_action_file_icon.png | Bin 0 -> 406 bytes ...at_minimalist_more_action_picture_icon.png | Bin 0 -> 695 bytes ...hat_minimalist_more_action_record_icon.png | Bin 0 -> 505 bytes .../drawable/chat_minimalist_play_icon.png | Bin 0 -> 10060 bytes .../chat_minimalist_pop_menu_cancel_pin.png | Bin 0 -> 683 bytes .../chat_minimalist_pop_menu_copy.png | Bin 0 -> 518 bytes .../chat_minimalist_pop_menu_delete.png | Bin 0 -> 429 bytes .../drawable/chat_minimalist_pop_menu_ear.png | Bin 0 -> 791 bytes .../chat_minimalist_pop_menu_forward.png | Bin 0 -> 665 bytes .../chat_minimalist_pop_menu_info.png | Bin 0 -> 738 bytes .../chat_minimalist_pop_menu_more.png | Bin 0 -> 313 bytes .../chat_minimalist_pop_menu_multi_select.png | Bin 0 -> 242 bytes .../drawable/chat_minimalist_pop_menu_pin.png | Bin 0 -> 618 bytes .../chat_minimalist_pop_menu_quote.png | Bin 0 -> 448 bytes .../chat_minimalist_pop_menu_reply.png | Bin 0 -> 652 bytes .../chat_minimalist_pop_menu_revoke.png | Bin 0 -> 457 bytes .../chat_minimalist_pop_menu_speaker.png | Bin 0 -> 472 bytes .../chat_minimalist_video_call_icon.png | Bin 0 -> 691 bytes .../chat_minimalist_voice_call_icon.png | Bin 0 -> 843 bytes .../chat_more_item_panel_bg_border.xml | 8 + .../drawable/chat_voice_sound_waves.png | Bin 0 -> 789 bytes .../drawable/chat_white_panel_border.xml | 8 + .../contact_minimalist_message_icon.png | Bin 0 -> 1276 bytes .../drawable/contact_profile_btn_shape.xml | 7 + .../drawable/gray_round_rect_border.xml | 8 + .../drawable/group_edit_name_icon.png | Bin 0 -> 544 bytes .../group_minimalist_message_icon.png | Bin 0 -> 1276 bytes .../drawable/group_profile_btn_shape.xml | 7 + .../drawable/minimalist_corner_bg_blue.xml | 8 + .../drawable/minimalist_corner_bg_red.xml | 8 + .../drawable/minimalist_delete_icon.png | Bin 0 -> 547 bytes .../drawable/minimalist_delete_start_icon.png | Bin 0 -> 1088 bytes .../minimalist_selector_face_text.xml | 13 + .../drawable/white_round_rect_border.xml | 8 + .../chat_group_minimalist_info_fragment.xml | 13 + .../chat_group_minimalist_info_layout.xml | 309 +++ .../chat_minimalist_bottom_select_sheet.xml | 29 + .../chat_minimalist_bottom_sheet_item.xml | 15 + .../layout/chat_minimalist_forward_layout.xml | 78 + .../layout/chat_minimalist_fragment.xml | 11 + ...t_minimalist_group_profile_item_layout.xml | 35 + ...t_minimalist_group_receipt_member_item.xml | 37 + .../layout/chat_minimalist_header_layout.xml | 80 + .../chat_minimalist_input_camera_view.xml | 54 + .../layout/chat_minimalist_input_layout.xml | 236 ++ ...malist_message_adapter_content_calling.xml | 32 + ...inimalist_message_adapter_content_text.xml | 13 + .../chat_minimalist_message_detail_layout.xml | 148 ++ ..._minimalist_pop_menu_action_item_layou.xml | 28 + .../chat_minimalist_pop_menu_layout.xml | 106 + ...minimalist_profile_warning_item_layout.xml | 16 + ...t_minimalist_reply_details_item_layout.xml | 51 + .../layout/chat_minimalist_text_layout.xml | 11 + ...minimalist_title_extension_item_layout.xml | 11 + .../layout/chat_reply_dialog_layout.xml | 86 + ..._minimalist_friend_profile_item_layout.xml | 35 + ...minimalist_profile_warning_item_layout.xml | 16 + ...t_stranger_profile_warning_item_layout.xml | 19 + .../layout/group_minimalist_notice.xml | 30 + ..._minimalist_profile_bottom_item_layout.xml | 16 + ...inimalist_profile_settings_item_layout.xml | 16 + ...malist_contact_friend_profile_activity.xml | 13 + ...nimalist_contact_friend_profile_layout.xml | 251 ++ .../layout/minimalist_face_group_icon.xml | 22 + ...minimalist_face_message_content_layout.xml | 22 + .../layout/minimalist_forward_msg_holder.xml | 74 + .../layout/minimalist_fragment_face.xml | 44 + .../layout/minimalist_input_more.xml | 31 + .../layout/minimalist_input_more_item.xml | 41 + ...nimalist_message_adapter_content_audio.xml | 51 + ...inimalist_message_adapter_content_file.xml | 69 + ...nimalist_message_adapter_content_image.xml | 71 + ...nimalist_message_adapter_content_reply.xml | 64 + ...inimalist_quote_message_content_layout.xml | 94 + .../layout/tuichat_chat_minimalist_layout.xml | 222 ++ .../values-ar/mininmalist_strings.xml | 10 + .../values-zh-rHK/mininmalist_strings.xml | 8 + .../values-zh/mininmalist_strings.xml | 10 + .../main/res-minimalistui/values/attrs.xml | 21 + .../main/res-minimalistui/values/colors.xml | 6 + .../main/res-minimalistui/values/dimens.xml | 5 + .../values/minimalist_strings.xml | 10 + .../values/minimalist_styles.xml | 35 + .../chat_ic_arrow_down_serious.png | Bin 0 -> 1480 bytes .../chat_ic_arrow_up_serious.png | Bin 0 -> 1484 bytes .../chat_title_bar_more_menu_serious.png | Bin 0 -> 636 bytes .../res-serious/values/serious_colors.xml | 30 + .../res-serious/values/serious_styles.xml | 36 + .../main/res/anim/chat_pop_menu_in_anim.xml | 7 + .../main/res/anim/chat_pop_menu_out_anim.xml | 7 + .../src/main/res/anim/translate_dialog_in.xml | 8 + .../main/res/anim/translate_dialog_out.xml | 8 + .../chat_bubble_other_cavity_bg.xml | 22 + .../chat_bubble_self_cavity_bg.xml | 21 + .../main/res/drawable-xxhdpi/chat_back.png | Bin 0 -> 736 bytes .../chat_camera_switchcamera.png | Bin 0 -> 1820 bytes .../res/drawable-xxhdpi/chat_input_face.png | Bin 0 -> 3634 bytes .../drawable-xxhdpi/chat_input_keyboard.png | Bin 0 -> 2333 bytes .../res/drawable-xxhdpi/chat_input_more.png | Bin 0 -> 3225 bytes .../res/drawable-xxhdpi/chat_input_voice.png | Bin 0 -> 3963 bytes .../chat_more_input_custom_message.png | Bin 0 -> 2242 bytes .../chat_permission_icon_camera.png | Bin 0 -> 925 bytes .../chat_permission_icon_file.png | Bin 0 -> 472 bytes .../chat_permission_icon_mic.png | Bin 0 -> 1689 bytes .../res/drawable-xxhdpi/emoji_default.png | Bin 0 -> 1957 bytes .../main/res/drawable-xxhdpi/file_icon.png | Bin 0 -> 1570 bytes .../res/drawable-xxhdpi/ic_chat_play_icon.png | Bin 0 -> 2460 bytes .../res/drawable-xxhdpi/ic_more_camera.png | Bin 0 -> 1830 bytes .../main/res/drawable-xxhdpi/ic_more_file.png | Bin 0 -> 1196 bytes .../res/drawable-xxhdpi/ic_more_picture.png | Bin 0 -> 1848 bytes .../res/drawable-xxhdpi/ic_more_video.png | Bin 0 -> 1968 bytes .../main/res/drawable-xxhdpi/ic_volume_1.png | Bin 0 -> 2026 bytes .../main/res/drawable-xxhdpi/ic_volume_2.png | Bin 0 -> 2091 bytes .../main/res/drawable-xxhdpi/ic_volume_3.png | Bin 0 -> 2152 bytes .../main/res/drawable-xxhdpi/ic_volume_4.png | Bin 0 -> 2193 bytes .../main/res/drawable-xxhdpi/ic_volume_5.png | Bin 0 -> 2223 bytes .../main/res/drawable-xxhdpi/ic_volume_6.png | Bin 0 -> 2247 bytes .../main/res/drawable-xxhdpi/ic_volume_7.png | Bin 0 -> 2273 bytes .../main/res/drawable-xxhdpi/ic_volume_8.png | Bin 0 -> 2319 bytes .../ic_volume_dialog_cancel.png | Bin 0 -> 1783 bytes .../ic_volume_dialog_length_short.png | Bin 0 -> 1634 bytes .../drawable-xxhdpi/multi_select_delete.png | Bin 0 -> 2716 bytes .../multi_select_forward_merge.png | Bin 0 -> 3577 bytes .../multi_select_forward_one.png | Bin 0 -> 3648 bytes .../res/drawable-xxhdpi/pop_menu_copy.png | Bin 0 -> 638 bytes .../res/drawable-xxhdpi/pop_menu_delete.png | Bin 0 -> 769 bytes .../main/res/drawable-xxhdpi/pop_menu_ear.png | Bin 0 -> 930 bytes .../res/drawable-xxhdpi/pop_menu_forward.png | Bin 0 -> 1997 bytes .../drawable-xxhdpi/pop_menu_multi_select.png | Bin 0 -> 1463 bytes .../res/drawable-xxhdpi/pop_menu_quote.png | Bin 0 -> 841 bytes .../res/drawable-xxhdpi/pop_menu_reply.png | Bin 0 -> 1134 bytes .../res/drawable-xxhdpi/pop_menu_revoke.png | Bin 0 -> 1332 bytes .../res/drawable-xxhdpi/pop_menu_speaker.png | Bin 0 -> 566 bytes .../res/drawable-xxhdpi/reply_close_btn.png | Bin 0 -> 1081 bytes .../chat_risk_image_replace_icon.png | Bin 0 -> 5812 bytes .../res/drawable/action_audio_selector.xml | 7 + .../res/drawable/action_face_selector.xml | 7 + .../res/drawable/action_more_selector.xml | 7 + .../drawable/chat_bubble_other_cavity_bg.xml | 22 + .../drawable/chat_bubble_self_cavity_bg.xml | 21 + .../drawable/chat_camera_back_btn_icon.png | Bin 0 -> 467 bytes .../drawable/chat_camera_cancel_btn_icon.png | Bin 0 -> 2634 bytes .../res/drawable/chat_camera_commit_bg.png | Bin 0 -> 1723 bytes .../drawable/chat_camera_commit_btn_icon.png | Bin 0 -> 1038 bytes .../chat_camera_select_image_icon.png | Bin 0 -> 4708 bytes .../main/res/drawable/chat_divide_line.xml | 11 + .../drawable/chat_face_delete_btn_icon.png | Bin 0 -> 1404 bytes .../drawable/chat_face_item_bg_selector.xml | 16 + .../src/main/res/drawable/chat_ic_back.xml | 9 + .../drawable/chat_input_shortcut_item_bg.xml | 13 + .../res/drawable/chat_less_item_button.png | Bin 0 -> 1015 bytes .../res/drawable/chat_more_item_button.png | Bin 0 -> 1068 bytes .../res/drawable/chat_pinned_list_divider.xml | 8 + .../chat_pinned_message_delete_button.png | Bin 0 -> 950 bytes .../main/res/drawable/chat_pop_item_bg.xml | 13 + .../res/drawable/chat_pop_menu_cancel_pin.png | Bin 0 -> 773 bytes .../res/drawable/chat_pop_menu_divider.xml | 11 + .../main/res/drawable/chat_pop_menu_pin.png | Bin 0 -> 727 bytes .../drawable/chat_progress_download_icon.png | Bin 0 -> 329 bytes .../chat_title_bar_more_menu_icon.png | Bin 0 -> 345 bytes .../chat_translucent_round_rect_border.xml | 8 + .../main/res/drawable/chat_up_arrow_icon.png | Bin 0 -> 692 bytes .../main/res/drawable/chat_upload_icon.png | Bin 0 -> 339 bytes .../res/drawable/chat_voice_msg_playing_1.png | Bin 0 -> 2099 bytes .../res/drawable/chat_voice_msg_playing_2.png | Bin 0 -> 1926 bytes .../res/drawable/chat_voice_msg_playing_3.png | Bin 0 -> 806 bytes .../res/drawable/chat_voice_reply_icon.png | Bin 0 -> 581 bytes .../res/drawable/contact_btn_bg_color.xml | 6 + .../face_view_delete_button_border.xml | 8 + .../drawable/face_view_send_button_border.xml | 8 + .../drawable/group_msg_receipt_line_bg.xml | 9 + .../src/main/res/drawable/ic_audio_call.xml | 10 + tuichat/src/main/res/drawable/ic_camera.xml | 9 + .../src/main/res/drawable/ic_close_button.xml | 14 + .../main/res/drawable/ic_download_button.xml | 14 + .../main/res/drawable/ic_other_video_call.xml | 10 + .../src/main/res/drawable/ic_pause_center.xml | 16 + .../src/main/res/drawable/ic_pause_icon.xml | 14 + .../src/main/res/drawable/ic_play_icon.xml | 14 + .../main/res/drawable/ic_self_video_call.xml | 10 + .../main/res/drawable/ic_volume_dialog_bg.xml | 8 + .../res/drawable/layer_live_rating_bar.xml | 16 + .../src/main/res/drawable/layer_progress.xml | 26 + .../main/res/drawable/message_send_border.xml | 6 + .../main/res/drawable/msg_editor_border.xml | 8 + .../main/res/drawable/play_voice_message.xml | 7 + .../main/res/drawable/ratingbar_opreview.png | Bin 0 -> 1873 bytes .../res/drawable/ratingbar_opreview_grey.png | Bin 0 -> 552 bytes .../main/res/drawable/recording_volume.xml | 28 + .../main/res/drawable/selector_face_text.xml | 13 + .../src/main/res/drawable/shape_circle.xml | 11 + .../src/main/res/drawable/shape_dialog.xml | 7 + .../tuiemoji_default_emoji_group_icon.png | Bin 0 -> 1177 bytes .../tuiemoji_emoji_page_scroll_bar.xml | 19 + .../res/drawable/tuiemoji_tab_item_bg.xml | 19 + .../drawable/view_original_image_border.xml | 7 + .../main/res/drawable/voice_btn_selector.xml | 18 + .../main/res/layout/activity_group_notice.xml | 30 + .../layout/activity_message_reply_detail.xml | 37 + .../layout/chat_camera_activity_layout.xml | 16 + .../res/layout/chat_camera_capture_layout.xml | 79 + .../res/layout/chat_face_detail_layout.xml | 17 + .../main/res/layout/chat_face_item_layout.xml | 14 + .../res/layout/chat_face_page_item_layout.xml | 107 + .../res/layout/chat_face_tab_item_layout.xml | 14 + .../main/res/layout/chat_face_view_layout.xml | 38 + .../chat_forward_msg_content_line_layout.xml | 37 + tuichat/src/main/res/layout/chat_fragment.xml | 11 + .../chat_group_pinned_item_view_layout.xml | 62 + .../chat_group_pinned_view_dialog_layout.xml | 36 + .../layout/chat_group_pinned_view_layout.xml | 29 + .../res/layout/chat_input_camera_view.xml | 51 + .../src/main/res/layout/chat_input_layout.xml | 109 + .../res/layout/chat_input_layout_actoin.xml | 31 + .../res/layout/chat_input_shortcut_item.xml | 20 + .../res/layout/chat_inputmore_fragment.xml | 12 + .../main/res/layout/chat_inputmore_layout.xml | 22 + .../res/layout/chat_loading_progress_bar.xml | 13 + .../res/layout/chat_pop_menu_item_layout.xml | 27 + .../main/res/layout/chat_pop_menu_layout.xml | 22 + .../layout/chat_reply_details_item_layout.xml | 87 + .../layout/chat_reply_quote_face_layout.xml | 9 + .../layout/chat_reply_quote_file_layout.xml | 30 + .../layout/chat_reply_quote_image_layout.xml | 18 + .../layout/chat_reply_quote_merge_layout.xml | 31 + .../layout/chat_reply_quote_sound_layout.xml | 30 + .../layout/chat_reply_quote_text_layout.xml | 15 + .../contact_friend_profile_activity.xml | 15 + .../contact_friend_profile_item_layout.xml | 19 + .../layout/contact_friend_profile_layout.xml | 228 ++ ...act_friend_profile_warning_item_layout.xml | 18 + .../custom_evaluation_message_layout.xml | 34 + .../layout/custom_order_message_layout.xml | 77 + .../main/res/layout/forward_chat_layout.xml | 28 + .../main/res/layout/forward_dialog_layout.xml | 47 + .../main/res/layout/forward_msg_holder.xml | 48 + .../main/res/layout/group_info_activity.xml | 11 + .../main/res/layout/group_info_fragment.xml | 13 + .../src/main/res/layout/group_info_layout.xml | 295 +++ .../group_profile_bottom_item_layout.xml | 19 + .../group_profile_settings_item_layout.xml | 17 + .../group_profile_warning_item_layout.xml | 18 + .../res/layout/group_receipt_member_item.xml | 45 + .../main/res/layout/image_video_scan_item.xml | 188 ++ tuichat/src/main/res/layout/item_tab.xml | 23 + .../layout/message_adapter_content_audio.xml | 31 + .../message_adapter_content_calling.xml | 36 + .../layout/message_adapter_content_file.xml | 66 + .../layout/message_adapter_content_image.xml | 87 + .../layout/message_adapter_content_reply.xml | 63 + .../layout/message_adapter_content_text.xml | 20 + .../layout/message_adapter_content_tips.xml | 35 + .../res/layout/message_adapter_item_empty.xml | 28 + .../res/layout/message_content_layout.xml | 6 + .../res/layout/msg_receipt_detail_layout.xml | 204 ++ tuichat/src/main/res/layout/notice_layout.xml | 30 + .../layout/quote_message_content_layout.xml | 89 + .../main/res/layout/reply_preview_layout.xml | 31 + .../layout/test_custom_message_layout1.xml | 30 + .../layout/tuichat_chat_activity_layout.xml | 13 + .../main/res/layout/tuichat_chat_layout.xml | 296 +++ .../tuichat_image_video_scan_layout.xml | 22 + .../src/main/res/values-ar/strings-emoji.xml | 65 + tuichat/src/main/res/values-ar/strings.xml | 343 +++ .../main/res/values-zh-rHK/strings-emoji.xml | 66 + .../src/main/res/values-zh-rHK/strings.xml | 328 +++ .../src/main/res/values-zh/strings-emoji.xml | 65 + tuichat/src/main/res/values-zh/strings.xml | 338 +++ tuichat/src/main/res/values/arrays.xml | 223 ++ tuichat/src/main/res/values/attrs.xml | 19 + tuichat/src/main/res/values/colors.xml | 40 + tuichat/src/main/res/values/dimens.xml | 66 + tuichat/src/main/res/values/strings-emoji.xml | 65 + tuichat/src/main/res/values/strings.xml | 336 +++ tuichat/src/main/res/values/styles.xml | 23 + .../src/main/res/values/tui_theme_attrs.xml | 35 + tuiconversation/build.gradle | 53 + tuiconversation/src/main/AndroidManifest.xml | 33 + .../TUIConversationConstants.java | 38 + .../TUIConversationService.java | 542 +++++ .../bean/ConversationGroupBean.java | 109 + .../bean/ConversationPopMenuItem.java | 10 + .../bean/ConversationUserStatusBean.java | 31 + .../tuiconversation/bean/DraftInfo.java | 30 + .../interfaces/IConversationLayout.java | 37 + .../interfaces/IConversationListLayout.java | 25 + .../interfaces/IConversationProvider.java | 46 + .../OnConversationAdapterListener.java | 15 + .../page/TUIConversationFragment.java | 402 ++++ .../TUIConversationFragmentContainer.java | 441 ++++ .../page/TUIFoldedConversationActivity.java | 23 + .../page/TUIFoldedConversationFragment.java | 273 +++ .../page/TUIForwardSelectActivity.java | 39 + .../page/TUIForwardSelectFragment.java | 336 +++ .../classicui/util/TUIConversationUtils.java | 34 + .../widget/ConversationBaseHolder.java | 23 + .../widget/ConversationCommonHolder.java | 317 +++ .../widget/ConversationCustomHolder.java | 53 + .../widget/ConversationIconView.java | 165 ++ .../classicui/widget/ConversationLayout.java | 129 + .../widget/ConversationListAdapter.java | 569 +++++ .../widget/ConversationListLayout.java | 135 ++ .../widget/ConversationTabLayoutMediator.java | 312 +++ .../widget/FoldedConversationLayout.java | 103 + .../widget/FoldedConversationListLayout.java | 91 + .../ForwardConversationSelectorAdapter.java | 80 + .../classicui/widget/ForwardSelectLayout.java | 87 + .../commonutil/ConversationUtils.java | 215 ++ .../commonutil/TUIConversationLog.java | 35 + .../commonutil/TUIConversationUtils.java | 33 + .../config/TUIConversationConfig.java | 23 + .../TUIConversationConfigClassic.java | 227 ++ .../TUIConversationConfigMinimalist.java | 227 ++ .../interfaces/ConversationEventListener.java | 45 + .../ConversationGroupNotifyListener.java | 18 + .../interfaces/IConversationListAdapter.java | 33 + .../interfaces/IConversationLayout.java | 38 + .../interfaces/IConversationListLayout.java | 23 + .../interfaces/IConversationProvider.java | 49 + .../OnConversationAdapterListener.java | 23 + .../TUIConversationMinimalistFragment.java | 567 +++++ ...IFoldedConversationMinimalistActivity.java | 23 + ...IFoldedConversationMinimalistFragment.java | 244 ++ .../TUIForwardSelectMinimalistActivity.java | 39 + .../TUIForwardSelectMinimalistFragment.java | 131 + .../util/TUIConversationUtils.java | 33 + .../widget/ConversationBaseHolder.java | 23 + .../widget/ConversationCommonHolder.java | 327 +++ .../widget/ConversationCustomHolder.java | 54 + .../widget/ConversationIconView.java | 166 ++ .../widget/ConversationLayout.java | 277 +++ .../widget/ConversationListAdapter.java | 559 +++++ .../widget/ConversationListLayout.java | 125 + .../widget/FoldedConversationLayout.java | 102 + .../widget/FoldedConversationListLayout.java | 91 + .../widget/ForwardSelectLayout.java | 67 + .../minimalistui/widget/Menu.java | 162 ++ .../model/ConversationProvider.java | 543 +++++ .../presenter/ConversationFoldPresenter.java | 506 ++++ .../presenter/ConversationIconPresenter.java | 18 + .../presenter/ConversationPresenter.java | 1432 +++++++++++ .../main/res-light/values/light_colors.xml | 6 + .../main/res-light/values/light_styles.xml | 7 + .../main/res-lively/values/lively_colors.xml | 6 + .../main/res-lively/values/lively_styles.xml | 7 + ...versation_forward_selected_page_border.xml | 5 + .../conversation_minimalist_create_icon.png | Bin 0 -> 914 bytes .../conversation_minimalist_edit_icon.png | Bin 0 -> 698 bytes ...inimalist_message_status_send_all_read.png | Bin 0 -> 344 bytes ...minimalist_message_status_send_no_read.png | Bin 0 -> 273 bytes .../conversation_minimalist_more_icon.png | Bin 0 -> 784 bytes ...nversation_minimalist_not_disturb_icon.png | Bin 0 -> 418 bytes .../conversation_minimalist_online_icon.png | Bin 0 -> 392 bytes .../conversation_minimalist_search_border.xml | 7 + .../conversation_minimalist_search_icon.png | Bin 0 -> 644 bytes ...ation_minimalist_forward_label_adapter.xml | 23 + .../conversation_minimalist_pop_menu_item.xml | 20 + .../layout/minimalist_bottom_bar.xml | 64 + ...list_conversation_list_item_sub_layout.xml | 259 ++ .../layout/minimalist_folded_fragment.xml | 12 + .../layout/minimalist_folded_layout.xml | 16 + ...ist_forward_conversation_selector_item.xml | 17 + .../layout/minimalist_forward_fragment.xml | 60 + .../layout/minimalist_forward_layout.xml | 58 + .../layout/minimalist_header_view_layout.xml | 81 + .../layout/minimalist_more_dialog.xml | 37 + ..._conversation_forward_list_item_layout.xml | 125 + .../minimalistui_conversation_fragment.xml | 17 + .../minimalistui_conversation_layout.xml | 46 + ...malistui_conversation_list_item_layout.xml | 134 ++ .../values-ar/minimalist_strings.xml | 14 + .../values-zh-rHK/minimalist_strings.xml | 14 + .../values-zh/minimalist_strings.xml | 14 + .../values/minimalist_attrs.xml | 6 + .../values/minimalist_colors.xml | 9 + .../values/minimalist_strings.xml | 14 + .../res-serious/values/serious_colors.xml | 6 + .../res-serious/values/serious_styles.xml | 7 + .../conversation_create_c2c.png | Bin 0 -> 823 bytes .../conversation_group_icon.png | Bin 0 -> 1365 bytes .../drawable-xxhdpi/conversation_ic_fold.png | Bin 0 -> 2714 bytes .../conversation_mark_banner.png | Bin 0 -> 285 bytes .../conversation_null_data.png | Bin 0 -> 28093 bytes .../drawable-xxhdpi/conversation_tab_menu.png | Bin 0 -> 173 bytes .../conversation_checkbox_selector.xml | 8 + .../res/drawable/conversation_ic_disturb.xml | 12 + .../drawable/conversation_ic_send_failed.xml | 14 + .../conversation_ic_sending_status.xml | 10 + .../conversation_shape_full_tab_indicator.xml | 10 + .../drawable/conversation_shape_tab_bg.xml | 5 + .../res/layout/conversation_base_fragment.xml | 67 + .../layout/conversation_custom_adapter.xml | 70 + .../layout/conversation_folded_activity.xml | 12 + .../layout/conversation_folded_fragment.xml | 12 + .../res/layout/conversation_folded_layout.xml | 16 + .../conversation_forward_activity_layout.xml | 13 + ...ion_forward_conversation_selector_item.xml | 18 + .../layout/conversation_forward_fragment.xml | 41 + .../conversation_forward_label_adapter.xml | 22 + .../layout/conversation_forward_layout.xml | 16 + .../conversation_forward_select_adapter.xml | 34 + .../main/res/layout/conversation_fragment.xml | 12 + .../layout/conversation_group_tab_item.xml | 27 + .../main/res/layout/conversation_layout.xml | 12 + .../layout/conversation_list_item_layout.xml | 277 +++ .../conversation_loading_progress_bar.xml | 13 + .../res/layout/conversation_null_layout.xml | 24 + .../layout/conversation_pop_menu_layout.xml | 20 + .../res/layout/convesation_list_header.xml | 126 + .../src/main/res/values-ar/strings.xml | 33 + .../src/main/res/values-zh-rHK/strings.xml | 33 + .../src/main/res/values-zh/strings.xml | 33 + tuiconversation/src/main/res/values/attrs.xml | 4 + .../src/main/res/values/colors.xml | 15 + .../src/main/res/values/dimens.xml | 19 + .../src/main/res/values/strings.xml | 31 + .../src/main/res/values/styles.xml | 11 + 1103 files changed, 87807 insertions(+) create mode 100644 timcommon/build.gradle create mode 100644 timcommon/src/main/AndroidManifest.xml create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/TIMCommonConfig.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/TIMCommonService.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/ChatFace.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/CustomFace.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/Emoji.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/FaceGroup.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/FriendProfileBean.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/GroupProfileBean.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/MessageFeature.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/MessageReceiptInfo.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/MessageRepliesBean.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/TUIMessageBean.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/TUIReplyQuoteBean.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/UserBean.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/classicui/widget/message/MessageBaseHolder.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/classicui/widget/message/MessageContentHolder.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/classicui/widget/message/SelectionHelper.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/classicui/widget/message/TUIReplyQuoteView.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/BottomSelectSheet.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/CustomLinearLayoutManager.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/IndicatorView.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/LineControllerView.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/MaxWidthFrameLayout.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/MaxWidthLinearLayout.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/MinimalistLineControllerView.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/MinimalistTitleBar.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/PopupInputCard.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/RoundCornerImageView.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/RoundFrameLayout.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/SwitchCustomWidth.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/TitleBarLayout.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/UnreadCountTextView.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/action/PopActionClickListener.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/action/PopDialogAdapter.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/action/PopMenuAction.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/action/PopMenuAdapter.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/activities/BaseLightActivity.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/activities/BaseMinimalistLightActivity.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/activities/ImageSelectActivity.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/activities/ImageSelectMinimalistActivity.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/activities/SelectionActivity.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/activities/SelectionMinimalistActivity.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/dialog/TUIKitDialog.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/face/CenterImageSpan.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/face/FaceManager.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/face/RecentEmojiManager.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/gatherimage/MultiImageData.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/gatherimage/ShadeImageView.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/gatherimage/SynthesizedImageView.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/gatherimage/Synthesizer.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/gatherimage/TeamHeadSynthesizer.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/gatherimage/UserIconView.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/highlight/HighlightPresenter.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/impl/GlideEngine.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/interfaces/ILayout.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/interfaces/ITitleBarLayout.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/interfaces/IUIKitCallback.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/Compat.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/CustomGestureDetector.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnGestureListener.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnMatrixChangedListener.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnOutsidePhotoTapListener.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnPhotoTapListener.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnScaleChangedListener.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnSingleFlingListener.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnViewDragListener.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnViewTapListener.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/PhotoView.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/PhotoViewAttacher.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/Util.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/scroller/CenteredSmoothScroller.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/Attributes.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/RecyclerSwipeAdapter.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SimpleSwipeListener.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SwipeAdapterInterface.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SwipeItemMangerImpl.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SwipeItemMangerInterface.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SwipeLayout.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/IPlayer.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/MediaPlayerProxy.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/SystemMediaPlayerWrapper.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/VideoGestureScaleAttacher.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/VideoView.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/config/classicui/TUIConfigClassic.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/config/minimalistui/TUIConfigMinimalist.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/ChatInputMoreListener.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/HighlightListener.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/ICommonMessageAdapter.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/IMessageProperties.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/OnChatPopActionClickListener.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/OnFaceInputListener.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/OnItemClickListener.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/UserFaceUrlCache.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/MessageBaseHolder.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/MessageContentHolder.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/MessageStatusTimeView.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/MinimalistMessageLayout.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/ReplyPreviewView.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/TUIReplyQuoteView.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/TimeInLineTextLayout.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/ActivityResultResolver.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/DateTimeUtil.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/FileProvider.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/FileUtil.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/ImageUtil.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/LayoutUtil.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/MessageBuilder.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/MessageParser.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/PopWindowUtil.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/ScreenUtil.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/SoftKeyBoardUtil.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TIMCommonConstants.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TIMCommonLog.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TIMCommonUtil.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TUIUtil.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TextUtil.java create mode 100644 timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/ThreadUtils.java create mode 100644 timcommon/src/main/res-light/drawable-ldrtl/chat_bubble_other_bg_light.xml create mode 100644 timcommon/src/main/res-light/drawable-ldrtl/chat_bubble_self_bg_light.xml create mode 100644 timcommon/src/main/res-light/drawable-xxhdpi/core_default_group_icon_community_light.png create mode 100644 timcommon/src/main/res-light/drawable-xxhdpi/core_default_group_icon_meeting_light.png create mode 100644 timcommon/src/main/res-light/drawable-xxhdpi/core_default_group_icon_public_light.png create mode 100644 timcommon/src/main/res-light/drawable-xxhdpi/core_default_group_icon_work_light.png create mode 100644 timcommon/src/main/res-light/drawable-xxhdpi/core_default_user_icon_light.png create mode 100644 timcommon/src/main/res-light/drawable-xxhdpi/core_online_status_light.png create mode 100644 timcommon/src/main/res-light/drawable-xxhdpi/core_selected_icon_light.png create mode 100644 timcommon/src/main/res-light/drawable-xxhdpi/core_title_bar_back_light.png create mode 100644 timcommon/src/main/res-light/drawable/chat_bubble_other_bg_light.xml create mode 100644 timcommon/src/main/res-light/drawable/chat_bubble_self_bg_light.xml create mode 100644 timcommon/src/main/res-light/drawable/chat_reply_icon_light.png create mode 100644 timcommon/src/main/res-light/drawable/core_title_bar_bg_light.xml create mode 100644 timcommon/src/main/res-light/values/light_colors.xml create mode 100644 timcommon/src/main/res-light/values/light_styles.xml create mode 100644 timcommon/src/main/res-lively/drawable-ldrtl/chat_bubble_other_bg_lively.xml create mode 100644 timcommon/src/main/res-lively/drawable-ldrtl/chat_bubble_self_bg_lively.xml create mode 100644 timcommon/src/main/res-lively/drawable-xxhdpi/core_default_group_icon_community_lively.png create mode 100644 timcommon/src/main/res-lively/drawable-xxhdpi/core_default_group_icon_meeting_lively.png create mode 100644 timcommon/src/main/res-lively/drawable-xxhdpi/core_default_group_icon_public_lively.png create mode 100644 timcommon/src/main/res-lively/drawable-xxhdpi/core_default_group_icon_work_lively.png create mode 100644 timcommon/src/main/res-lively/drawable-xxhdpi/core_default_user_icon_lively.png create mode 100644 timcommon/src/main/res-lively/drawable-xxhdpi/core_online_status_lively.png create mode 100644 timcommon/src/main/res-lively/drawable-xxhdpi/core_selected_icon_lively.png create mode 100644 timcommon/src/main/res-lively/drawable-xxhdpi/core_title_bar_back_lively.png create mode 100644 timcommon/src/main/res-lively/drawable/chat_bubble_other_bg_lively.xml create mode 100644 timcommon/src/main/res-lively/drawable/chat_bubble_self_bg_lively.xml create mode 100644 timcommon/src/main/res-lively/drawable/chat_reply_icon_lively.png create mode 100644 timcommon/src/main/res-lively/drawable/core_title_bar_bg_lively.xml create mode 100644 timcommon/src/main/res-lively/values/lively_colors.xml create mode 100644 timcommon/src/main/res-lively/values/lively_styles.xml create mode 100644 timcommon/src/main/res-serious/drawable-ldrtl/chat_bubble_other_bg_serious.xml create mode 100644 timcommon/src/main/res-serious/drawable-ldrtl/chat_bubble_self_bg_serious.xml create mode 100644 timcommon/src/main/res-serious/drawable-xxhdpi/core_default_group_icon_community_serious.png create mode 100644 timcommon/src/main/res-serious/drawable-xxhdpi/core_default_group_icon_meeting_serious.png create mode 100644 timcommon/src/main/res-serious/drawable-xxhdpi/core_default_group_icon_public_serious.png create mode 100644 timcommon/src/main/res-serious/drawable-xxhdpi/core_default_group_icon_work_serious.png create mode 100644 timcommon/src/main/res-serious/drawable-xxhdpi/core_default_user_icon_serious.png create mode 100644 timcommon/src/main/res-serious/drawable-xxhdpi/core_online_status_serious.png create mode 100644 timcommon/src/main/res-serious/drawable-xxhdpi/core_selected_icon_serious.png create mode 100644 timcommon/src/main/res-serious/drawable-xxhdpi/core_title_bar_back_serious.png create mode 100644 timcommon/src/main/res-serious/drawable/chat_bubble_other_bg_serious.xml create mode 100644 timcommon/src/main/res-serious/drawable/chat_bubble_self_bg_serious.xml create mode 100644 timcommon/src/main/res-serious/drawable/chat_reply_icon_serious.png create mode 100644 timcommon/src/main/res-serious/drawable/core_title_bar_bg_serious.xml create mode 100644 timcommon/src/main/res-serious/values/serious_colors.xml create mode 100644 timcommon/src/main/res-serious/values/serious_styles.xml create mode 100644 timcommon/src/main/res/anim/common_bottom_select_sheet_enter.xml create mode 100644 timcommon/src/main/res/anim/common_bottom_select_sheet_exit.xml create mode 100644 timcommon/src/main/res/anim/core_popup_in_anim.xml create mode 100644 timcommon/src/main/res/anim/core_popup_out_anim.xml create mode 100644 timcommon/src/main/res/color/common_bg_negative_btn.xml create mode 100644 timcommon/src/main/res/color/common_bg_positive_btn.xml create mode 100644 timcommon/src/main/res/drawable-ldrtl/chat_bubble_other_transparent_bg.xml create mode 100644 timcommon/src/main/res/drawable-ldrtl/chat_bubble_self_transparent_bg.xml create mode 100644 timcommon/src/main/res/drawable-ldrtl/chat_message_popup_fill_border_right.xml create mode 100644 timcommon/src/main/res/drawable-ldrtl/chat_message_popup_risk_content_border_left.xml create mode 100644 timcommon/src/main/res/drawable-ldrtl/chat_message_popup_risk_content_border_right.xml create mode 100644 timcommon/src/main/res/drawable-ldrtl/chat_message_popup_stroke_border_left.xml create mode 100644 timcommon/src/main/res/drawable-ldrtl/chat_message_popup_stroke_border_right.xml create mode 100644 timcommon/src/main/res/drawable/chat_bubble_other_transparent_bg.xml create mode 100644 timcommon/src/main/res/drawable/chat_bubble_self_transparent_bg.xml create mode 100644 timcommon/src/main/res/drawable/chat_checkbox_selector.xml create mode 100644 timcommon/src/main/res/drawable/chat_gray_round_rect_bg.xml create mode 100644 timcommon/src/main/res/drawable/chat_message_bottom_area_bg.xml create mode 100644 timcommon/src/main/res/drawable/chat_message_bottom_area_risk_bg.xml create mode 100644 timcommon/src/main/res/drawable/chat_message_popup_fill_border.xml create mode 100644 timcommon/src/main/res/drawable/chat_message_popup_fill_border_right.xml create mode 100644 timcommon/src/main/res/drawable/chat_message_popup_risk_content_border_left.xml create mode 100644 timcommon/src/main/res/drawable/chat_message_popup_risk_content_border_right.xml create mode 100644 timcommon/src/main/res/drawable/chat_message_popup_stroke_border.xml create mode 100644 timcommon/src/main/res/drawable/chat_message_popup_stroke_border_left.xml create mode 100644 timcommon/src/main/res/drawable/chat_message_popup_stroke_border_right.xml create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading00.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading01.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading02.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading03.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading04.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading05.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading06.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading07.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading08.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading09.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading10.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading11.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading12.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading13.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading14.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading15.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading16.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading17.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading18.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading19.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading20.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading21.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading22.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading23.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading24.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading25.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading26.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading27.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading28.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading29.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading30.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading31.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading32.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading33.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading34.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading35.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading36.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading37.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading38.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading39.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading40.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading41.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading42.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading43.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_anim_loading44.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_file_download_icon.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_message_status_send_all_read.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_message_status_send_failed.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_message_status_send_no_read.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_message_status_send_part_read.png create mode 100644 timcommon/src/main/res/drawable/chat_minimalist_status_loading_anim.xml create mode 100644 timcommon/src/main/res/drawable/chat_react_bg.xml create mode 100644 timcommon/src/main/res/drawable/chat_reply_more_icon.png create mode 100644 timcommon/src/main/res/drawable/chat_unselected_icon.png create mode 100644 timcommon/src/main/res/drawable/common_arrow_right.png create mode 100644 timcommon/src/main/res/drawable/common_bottom_sheet_border.xml create mode 100644 timcommon/src/main/res/drawable/common_check_box_selected.png create mode 100644 timcommon/src/main/res/drawable/common_check_box_unselected.png create mode 100644 timcommon/src/main/res/drawable/common_dialog_react_bg.xml create mode 100644 timcommon/src/main/res/drawable/common_edit_cursor.xml create mode 100644 timcommon/src/main/res/drawable/common_edit_text_bg.xml create mode 100644 timcommon/src/main/res/drawable/common_item_pressed_effect_background.xml create mode 100644 timcommon/src/main/res/drawable/common_title_bar_home_icon.png create mode 100644 timcommon/src/main/res/drawable/common_trans_bg.png create mode 100644 timcommon/src/main/res/drawable/core_close_icon.png create mode 100644 timcommon/src/main/res/drawable/core_default_group_icon_community.png create mode 100644 timcommon/src/main/res/drawable/core_delete_icon.png create mode 100644 timcommon/src/main/res/drawable/core_icon_offline_status.png create mode 100644 timcommon/src/main/res/drawable/core_list_divider.xml create mode 100644 timcommon/src/main/res/drawable/core_minimalist_back_icon.png create mode 100644 timcommon/src/main/res/drawable/core_positive_btn_bg.xml create mode 100644 timcommon/src/main/res/drawable/core_positive_btn_disable_bg.xml create mode 100644 timcommon/src/main/res/drawable/core_positive_btn_normal_bg.xml create mode 100644 timcommon/src/main/res/drawable/core_positive_btn_pressed_bg.xml create mode 100644 timcommon/src/main/res/drawable/core_search_icon.png create mode 100644 timcommon/src/main/res/drawable/indicator_point_nomal.png create mode 100644 timcommon/src/main/res/drawable/indicator_point_select.png create mode 100644 timcommon/src/main/res/drawable/message_send_fail.png create mode 100644 timcommon/src/main/res/drawable/minimalist_switch_thumb.xml create mode 100644 timcommon/src/main/res/drawable/minimalist_switch_track.xml create mode 100644 timcommon/src/main/res/drawable/minimalist_translation_area_bg.xml create mode 100644 timcommon/src/main/res/drawable/popup_card_bg.xml create mode 100644 timcommon/src/main/res/drawable/quote_message_area_bg.xml create mode 100644 timcommon/src/main/res/drawable/selected_border.xml create mode 100644 timcommon/src/main/res/drawable/switch_thumb.xml create mode 100644 timcommon/src/main/res/drawable/switch_thumb_blue.xml create mode 100644 timcommon/src/main/res/drawable/switch_thumb_gray.xml create mode 100644 timcommon/src/main/res/drawable/switch_track.xml create mode 100644 timcommon/src/main/res/drawable/switch_track_blue.xml create mode 100644 timcommon/src/main/res/drawable/switch_track_gray.xml create mode 100644 timcommon/src/main/res/layout/chat_minimalist_reply_preview_layout.xml create mode 100644 timcommon/src/main/res/layout/chat_minimalist_text_status_layout.xml create mode 100644 timcommon/src/main/res/layout/common_bottom_select_sheet.xml create mode 100644 timcommon/src/main/res/layout/common_bottom_sheet_item.xml create mode 100644 timcommon/src/main/res/layout/common_dialog_view_layout.xml create mode 100644 timcommon/src/main/res/layout/common_profile_icon_view.xml create mode 100644 timcommon/src/main/res/layout/core_activity_image_select_layout.xml create mode 100644 timcommon/src/main/res/layout/core_minimalist_activity_image_select_layout.xml create mode 100644 timcommon/src/main/res/layout/core_minimalist_selection_activity.xml create mode 100644 timcommon/src/main/res/layout/core_pop_menu.xml create mode 100644 timcommon/src/main/res/layout/core_select_image_item_layout.xml create mode 100644 timcommon/src/main/res/layout/core_select_item_layout.xml create mode 100644 timcommon/src/main/res/layout/message_adapter_item_content.xml create mode 100644 timcommon/src/main/res/layout/minimalist_line_controller_view.xml create mode 100644 timcommon/src/main/res/layout/minimalist_message_adapter_item_content.xml create mode 100644 timcommon/src/main/res/layout/pop_dialog_adapter.xml create mode 100644 timcommon/src/main/res/layout/pop_menu_adapter.xml create mode 100644 timcommon/src/main/res/layout/timcommon_layout_popup_card.xml create mode 100644 timcommon/src/main/res/layout/timcommon_line_controller_view.xml create mode 100644 timcommon/src/main/res/layout/timcommon_title_bar_layout.xml create mode 100644 timcommon/src/main/res/layout/tuicore_selection_activity.xml create mode 100644 timcommon/src/main/res/values-ar/strings.xml create mode 100644 timcommon/src/main/res/values-zh-rHK/strings.xml create mode 100644 timcommon/src/main/res/values-zh/strings.xml create mode 100644 timcommon/src/main/res/values/attrs.xml create mode 100644 timcommon/src/main/res/values/colors.xml create mode 100644 timcommon/src/main/res/values/dimens.xml create mode 100644 timcommon/src/main/res/values/strings.xml create mode 100644 timcommon/src/main/res/values/styles.xml create mode 100644 timcommon/src/main/res/values/tui_theme_attrs.xml create mode 100644 timcommon/src/main/res/xml/file_paths_public.xml create mode 100644 tuichat/build.gradle create mode 100644 tuichat/src/main/AndroidManifest.xml create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_0.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_1.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_10.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_11.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_12.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_13.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_14.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_15.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_16.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_17.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_18.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_19.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_2.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_20.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_21.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_22.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_23.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_24.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_25.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_26.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_27.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_28.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_29.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_3.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_30.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_31.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_32.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_33.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_34.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_35.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_36.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_37.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_38.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_39.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_4.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_40.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_41.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_42.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_43.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_44.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_45.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_46.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_47.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_48.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_49.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_5.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_50.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_51.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_52.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_53.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_54.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_55.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_56.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_57.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_58.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_59.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_6.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_60.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_61.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_7.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_8.png create mode 100644 tuichat/src/main/assets/chatbuildinemojis/emoji_9.png create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/TUIChatConstants.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/TUIChatService.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/C2CChatInfo.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/CallModel.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/ChatInfo.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/CustomHelloMessage.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/DraftInfo.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/GroupApplyInfo.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/GroupChatInfo.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/GroupMemberBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/GroupMemberInfo.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/GroupMessageReadMembersInfo.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/InputMoreItem.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/LocalTipsMessage.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/MessageCustom.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/MessageTyping.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/OfflinePushInfo.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/ReplyPreviewBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/UserStatusBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/message/CallingMessageBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/message/CallingTipsMessageBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/message/CustomEvaluationMessageBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/message/CustomLinkMessageBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/message/CustomOrderMessageBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/message/EmptyMessageBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/message/FaceMessageBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/message/FileMessageBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/message/ImageMessageBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/message/LocationMessageBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/message/MergeMessageBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/message/MessageTypingBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/message/QuoteMessageBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/message/ReplyMessageBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/message/SoundMessageBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/message/TextAtMessageBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/message/TextMessageBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/message/TipsMessageBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/message/VideoMessageBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/message/reply/CustomEvaluationMessageReplyQuoteBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/message/reply/CustomLinkReplyQuoteBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/message/reply/CustomOrderMessageReplyQuoteBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/message/reply/FaceReplyQuoteBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/message/reply/FileReplyQuoteBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/message/reply/ImageReplyQuoteBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/message/reply/LocationReplyQuoteBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/message/reply/MergeReplyQuoteBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/message/reply/ReplyReplyQuoteBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/message/reply/SoundReplyQuoteBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/message/reply/TextReplyQuoteBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/bean/message/reply/VideoReplyQuoteBean.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/ClassicUIService.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/component/noticelayout/NoticeLayout.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/component/noticelayout/NoticeLayoutConfig.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/component/popmenu/ChatPopMenu.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/interfaces/IChatLayout.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/page/FriendProfileActivity.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/page/GroupInfoActivity.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/page/GroupInfoFragment.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/page/GroupNoticeActivity.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/page/MessageReceiptDetailActivity.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/page/MessageReplyDetailActivity.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/page/TUIBaseChatActivity.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/page/TUIBaseChatFragment.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/page/TUIC2CChatActivity.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/page/TUIC2CChatFragment.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/page/TUIForwardChatActivity.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/page/TUIGroupChatActivity.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/page/TUIGroupChatFragment.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/ChatView.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/input/BaseInputFragment.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/input/InputView.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/input/ReplyPreviewBar.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/input/ShortcutView.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/input/inputmore/ActionsGridViewAdapter.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/input/inputmore/ActionsPagerAdapter.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/input/inputmore/InputMoreFragment.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/input/inputmore/InputMoreLayout.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/message/MessageAdapter.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/message/MessageRecyclerView.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/message/reply/FaceReplyQuoteView.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/message/reply/FileReplyQuoteView.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/message/reply/ImageReplyQuoteView.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/message/reply/LocationReplyQuoteView.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/message/reply/MergeReplyQuoteView.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/message/reply/ReplyDetailsView.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/message/reply/SoundReplyQuoteView.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/message/reply/TextReplyQuoteView.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/message/reply/VideoReplyQuoteView.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/message/viewholder/CallingMessageHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/message/viewholder/CustomEvaluationMessageHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/message/viewholder/CustomLinkMessageHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/message/viewholder/CustomOrderMessageHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/message/viewholder/EmptyMessageHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/message/viewholder/FaceMessageHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/message/viewholder/FileMessageHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/message/viewholder/ImageMessageHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/message/viewholder/LocationMessageHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/message/viewholder/MergeMessageHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/message/viewholder/MessageHeadHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/message/viewholder/MessageTailHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/message/viewholder/MessageViewHolderFactory.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/message/viewholder/QuoteMessageHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/message/viewholder/ReplyMessageHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/message/viewholder/SoundMessageHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/message/viewholder/TextMessageHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/message/viewholder/TipsMessageHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/message/viewholder/VideoMessageHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/profile/FriendProfileLayout.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/classicui/widget/profile/GroupInfoLayout.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/album/AlbumPicker.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/album/ChatMultimediaRecorderImpl.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/album/SystemAlbumPickerImpl.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/album/SystemMultimediaRecorderImpl.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/album/VideoRecorder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/audio/AIDenoiseAudioRecordImpl.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/audio/AudioPlayer.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/audio/AudioRecorder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/audio/SystemAudioRecordImpl.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/camera/CameraActivity.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/camera/CameraUtil.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/camera/listener/CameraListener.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/camera/listener/CaptureListener.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/camera/listener/ClickListener.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/camera/listener/ErrorListener.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/camera/listener/ReturnListener.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/camera/listener/TypeListener.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/camera/state/BrowserPictureState.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/camera/state/BrowserVideoState.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/camera/state/CameraMachine.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/camera/state/PreviewState.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/camera/state/State.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/camera/view/CameraInterface.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/camera/view/CameraView.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/camera/view/CaptureButton.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/camera/view/CaptureLayout.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/camera/view/FocusView.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/camera/view/ICameraView.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/camera/view/ReturnButton.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/camera/view/TypeButton.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/face/EmojiViewHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/face/FaceFragment.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/face/FaceListAdapter.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/face/FacePagerAdapter.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/face/FaceView.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/face/FaceViewHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/imagevideobrowse/ImageVideoBrowseActivity.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/imagevideobrowse/ImageVideoBrowseAdapter.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/imagevideobrowse/ImageVideoBrowseListener.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/imagevideobrowse/ImageVideoBrowsePresenter.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/imagevideobrowse/ImageVideoBrowseProvider.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/inputedittext/TIMMentionEditText.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/pinned/GroupPinnedView.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/progress/ChatRingProgressBar.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/progress/ProgressPresenter.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/config/ChatEventConfig.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/config/FriendConfig.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/config/GeneralConfig.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/config/GroupConfig.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/config/ShortcutMenuConfig.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/config/TUIChatConfigs.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/config/classicui/TUIChatConfigClassic.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/config/minimalistui/TUIChatConfigMinimalist.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/interfaces/AlbumPickerListener.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/interfaces/C2CChatEventListener.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/interfaces/ChatEventListener.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/interfaces/FriendProfileListener.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/interfaces/GroupChatEventListener.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/interfaces/GroupProfileListener.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/interfaces/IAlbumPicker.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/interfaces/IBaseMessageSender.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/interfaces/IDownloadProvider.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/interfaces/IGroupPinnedView.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/interfaces/IMessageAdapter.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/interfaces/IMessageDetailListener.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/interfaces/IMessageRecyclerView.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/interfaces/IMultimediaRecorder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/interfaces/IReplyMessageHandler.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/interfaces/MultimediaRecorderListener.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/interfaces/NetworkConnectionListener.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/interfaces/OnEmptySpaceClickListener.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/interfaces/OnGestureScrollListener.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/interfaces/TotalUnreadCountListener.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/MinimalistUIService.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/component/dialog/ChatBottomSelectSheet.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/component/noticelayout/NoticeLayout.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/component/noticelayout/NoticeLayoutConfig.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/interfaces/IChatLayout.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/page/FriendProfileMinimalistActivity.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/page/GroupInfoMinimalistActivity.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/page/GroupInfoMinimalistFragment.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/page/GroupNoticeMinimalistActivity.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/page/MessageDetailMinimalistActivity.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/page/TUIBaseChatMinimalistActivity.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/page/TUIBaseChatMinimalistFragment.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/page/TUIC2CChatMinimalistActivity.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/page/TUIC2CChatMinimalistFragment.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/page/TUIForwardChatMinimalistActivity.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/page/TUIGroupChatMinimalistActivity.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/page/TUIGroupChatMinimalistFragment.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/ChatView.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/input/InputView.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/input/inputmore/InputMoreDialogFragment.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/input/waveview/VoiceWaveView.kt create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/input/waveview/WaveMode.kt create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/message/ChatReplyDialogFragment.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/message/MessageAdapter.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/message/MessageRecyclerView.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/message/reply/FaceReplyQuoteView.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/message/reply/FileReplyQuoteView.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/message/reply/ImageReplyQuoteView.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/message/reply/LocationReplyQuoteView.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/message/reply/MergeReplyQuoteView.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/message/reply/ReplyDetailsView.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/message/reply/SoundReplyQuoteView.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/message/reply/TextReplyQuoteView.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/message/reply/VideoReplyQuoteView.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/message/viewholder/CallingMessageHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/message/viewholder/CustomEvaluationMessageHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/message/viewholder/CustomLinkMessageHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/message/viewholder/CustomOrderMessageHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/message/viewholder/EmptyMessageHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/message/viewholder/FaceMessageHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/message/viewholder/FileMessageHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/message/viewholder/ImageMessageHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/message/viewholder/LocationMessageHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/message/viewholder/MergeMessageHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/message/viewholder/MessageHeadHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/message/viewholder/MessageTailHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/message/viewholder/MessageViewHolderFactory.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/message/viewholder/QuoteMessageHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/message/viewholder/ReplyMessageHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/message/viewholder/SoundMessageHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/message/viewholder/TextMessageHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/message/viewholder/TipsMessageHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/message/viewholder/VideoMessageHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/messagepopmenu/ChatPopActivity.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/messagepopmenu/ChatPopDataHolder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/profile/FriendProfileLayout.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/minimalistui/widget/profile/GroupInfoLayout.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/model/AIDenoiseSignatureManager.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/model/ChatFileDownloadProvider.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/model/ChatProvider.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/model/ProfileProvider.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/presenter/C2CChatPresenter.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/presenter/ChatFileDownloadPresenter.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/presenter/ChatFileDownloadProxy.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/presenter/ChatModifyMessageHelper.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/presenter/ChatPresenter.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/presenter/ForwardPresenter.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/presenter/FriendProfilePresenter.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/presenter/GroupChatPresenter.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/presenter/GroupProfilePresenter.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/presenter/MessageReceiptPresenter.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/presenter/ReplyPresenter.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/util/AngleUtil.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/util/BlurUtils.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/util/ChatMessageBuilder.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/util/ChatMessageParser.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/util/DataStoreUtil.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/util/DeviceUtil.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/util/FileUtil.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/util/OfflinePushInfoUtils.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/util/PermissionHelper.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/util/TUIChatLog.java create mode 100644 tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/util/TUIChatUtils.java create mode 100644 tuichat/src/main/res-light/drawable-xxhdpi/chat_ic_arrow_down_light.png create mode 100644 tuichat/src/main/res-light/drawable-xxhdpi/chat_ic_arrow_up_light.png create mode 100644 tuichat/src/main/res-light/drawable-xxhdpi/chat_title_bar_more_menu_light.png create mode 100644 tuichat/src/main/res-light/values/light_colors.xml create mode 100644 tuichat/src/main/res-light/values/light_styles.xml create mode 100644 tuichat/src/main/res-lively/drawable-xxhdpi/chat_ic_arrow_down_lively.png create mode 100644 tuichat/src/main/res-lively/drawable-xxhdpi/chat_ic_arrow_up_lively.png create mode 100644 tuichat/src/main/res-lively/drawable-xxhdpi/chat_title_bar_more_menu_lively.png create mode 100644 tuichat/src/main/res-lively/values/lively_colors.xml create mode 100644 tuichat/src/main/res-lively/values/lively_styles.xml create mode 100644 tuichat/src/main/res-minimalistui/anim/chat_bottom_select_sheet_enter.xml create mode 100644 tuichat/src/main/res-minimalistui/anim/chat_bottom_select_sheet_exit.xml create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_audio_play_btn_ic.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_audio_stop_btn_ic.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_bottom_sheet_border.xml create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_bottom_sheet_dialog_border.xml create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_bottom_sheet_hide.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_contact_profile_item_extension_message_icon.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_face_group_list_item_select_border.xml create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_file_type_icon_image.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_file_type_icon_none.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_file_type_icon_pdf.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_file_type_icon_ppt.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_file_type_icon_text.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_file_type_icon_word.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_file_type_icon_xls.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_file_type_icon_zip.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_delete_icon.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_forward_icon.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_input_add_icon.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_input_camera_icon.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_input_edittext_border.xml create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_input_face_icon.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_input_voice_icon.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_jump_at_icon.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_jump_down_icon.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_menu_emoji_list_border.xml create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_menu_popup_border.xml create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_merge_message_border.xml create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_message_deatail_read_border.xml create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_message_status_sending.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_more_action_camera_icon.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_more_action_custom_icon.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_more_action_file_icon.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_more_action_picture_icon.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_more_action_record_icon.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_play_icon.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_pop_menu_cancel_pin.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_pop_menu_copy.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_pop_menu_delete.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_pop_menu_ear.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_pop_menu_forward.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_pop_menu_info.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_pop_menu_more.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_pop_menu_multi_select.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_pop_menu_pin.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_pop_menu_quote.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_pop_menu_reply.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_pop_menu_revoke.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_pop_menu_speaker.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_video_call_icon.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_minimalist_voice_call_icon.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_more_item_panel_bg_border.xml create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_voice_sound_waves.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/chat_white_panel_border.xml create mode 100644 tuichat/src/main/res-minimalistui/drawable/contact_minimalist_message_icon.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/contact_profile_btn_shape.xml create mode 100644 tuichat/src/main/res-minimalistui/drawable/gray_round_rect_border.xml create mode 100644 tuichat/src/main/res-minimalistui/drawable/group_edit_name_icon.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/group_minimalist_message_icon.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/group_profile_btn_shape.xml create mode 100644 tuichat/src/main/res-minimalistui/drawable/minimalist_corner_bg_blue.xml create mode 100644 tuichat/src/main/res-minimalistui/drawable/minimalist_corner_bg_red.xml create mode 100644 tuichat/src/main/res-minimalistui/drawable/minimalist_delete_icon.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/minimalist_delete_start_icon.png create mode 100644 tuichat/src/main/res-minimalistui/drawable/minimalist_selector_face_text.xml create mode 100644 tuichat/src/main/res-minimalistui/drawable/white_round_rect_border.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/chat_group_minimalist_info_fragment.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/chat_group_minimalist_info_layout.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/chat_minimalist_bottom_select_sheet.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/chat_minimalist_bottom_sheet_item.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/chat_minimalist_forward_layout.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/chat_minimalist_fragment.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/chat_minimalist_group_profile_item_layout.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/chat_minimalist_group_receipt_member_item.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/chat_minimalist_header_layout.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/chat_minimalist_input_camera_view.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/chat_minimalist_input_layout.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/chat_minimalist_message_adapter_content_calling.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/chat_minimalist_message_adapter_content_text.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/chat_minimalist_message_detail_layout.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/chat_minimalist_pop_menu_action_item_layou.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/chat_minimalist_pop_menu_layout.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/chat_minimalist_profile_warning_item_layout.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/chat_minimalist_reply_details_item_layout.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/chat_minimalist_text_layout.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/chat_minimalist_title_extension_item_layout.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/chat_reply_dialog_layout.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/contact_minimalist_friend_profile_item_layout.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/contact_minimalist_profile_warning_item_layout.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/contact_minimalist_stranger_profile_warning_item_layout.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/group_minimalist_notice.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/group_minimalist_profile_bottom_item_layout.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/group_minimalist_profile_settings_item_layout.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/minimalist_contact_friend_profile_activity.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/minimalist_contact_friend_profile_layout.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/minimalist_face_group_icon.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/minimalist_face_message_content_layout.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/minimalist_forward_msg_holder.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/minimalist_fragment_face.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/minimalist_input_more.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/minimalist_input_more_item.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/minimalist_message_adapter_content_audio.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/minimalist_message_adapter_content_file.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/minimalist_message_adapter_content_image.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/minimalist_message_adapter_content_reply.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/minimalist_quote_message_content_layout.xml create mode 100644 tuichat/src/main/res-minimalistui/layout/tuichat_chat_minimalist_layout.xml create mode 100644 tuichat/src/main/res-minimalistui/values-ar/mininmalist_strings.xml create mode 100644 tuichat/src/main/res-minimalistui/values-zh-rHK/mininmalist_strings.xml create mode 100644 tuichat/src/main/res-minimalistui/values-zh/mininmalist_strings.xml create mode 100644 tuichat/src/main/res-minimalistui/values/attrs.xml create mode 100644 tuichat/src/main/res-minimalistui/values/colors.xml create mode 100644 tuichat/src/main/res-minimalistui/values/dimens.xml create mode 100644 tuichat/src/main/res-minimalistui/values/minimalist_strings.xml create mode 100644 tuichat/src/main/res-minimalistui/values/minimalist_styles.xml create mode 100644 tuichat/src/main/res-serious/drawable-xxhdpi/chat_ic_arrow_down_serious.png create mode 100644 tuichat/src/main/res-serious/drawable-xxhdpi/chat_ic_arrow_up_serious.png create mode 100644 tuichat/src/main/res-serious/drawable-xxhdpi/chat_title_bar_more_menu_serious.png create mode 100644 tuichat/src/main/res-serious/values/serious_colors.xml create mode 100644 tuichat/src/main/res-serious/values/serious_styles.xml create mode 100644 tuichat/src/main/res/anim/chat_pop_menu_in_anim.xml create mode 100644 tuichat/src/main/res/anim/chat_pop_menu_out_anim.xml create mode 100644 tuichat/src/main/res/anim/translate_dialog_in.xml create mode 100644 tuichat/src/main/res/anim/translate_dialog_out.xml create mode 100644 tuichat/src/main/res/drawable-ldrtl/chat_bubble_other_cavity_bg.xml create mode 100644 tuichat/src/main/res/drawable-ldrtl/chat_bubble_self_cavity_bg.xml create mode 100644 tuichat/src/main/res/drawable-xxhdpi/chat_back.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/chat_camera_switchcamera.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/chat_input_face.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/chat_input_keyboard.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/chat_input_more.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/chat_input_voice.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/chat_more_input_custom_message.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/chat_permission_icon_camera.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/chat_permission_icon_file.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/chat_permission_icon_mic.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/emoji_default.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/file_icon.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/ic_chat_play_icon.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/ic_more_camera.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/ic_more_file.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/ic_more_picture.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/ic_more_video.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/ic_volume_1.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/ic_volume_2.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/ic_volume_3.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/ic_volume_4.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/ic_volume_5.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/ic_volume_6.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/ic_volume_7.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/ic_volume_8.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/ic_volume_dialog_cancel.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/ic_volume_dialog_length_short.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/multi_select_delete.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/multi_select_forward_merge.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/multi_select_forward_one.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/pop_menu_copy.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/pop_menu_delete.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/pop_menu_ear.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/pop_menu_forward.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/pop_menu_multi_select.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/pop_menu_quote.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/pop_menu_reply.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/pop_menu_revoke.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/pop_menu_speaker.png create mode 100644 tuichat/src/main/res/drawable-xxhdpi/reply_close_btn.png create mode 100644 tuichat/src/main/res/drawable-xxxhdpi/chat_risk_image_replace_icon.png create mode 100644 tuichat/src/main/res/drawable/action_audio_selector.xml create mode 100644 tuichat/src/main/res/drawable/action_face_selector.xml create mode 100644 tuichat/src/main/res/drawable/action_more_selector.xml create mode 100644 tuichat/src/main/res/drawable/chat_bubble_other_cavity_bg.xml create mode 100644 tuichat/src/main/res/drawable/chat_bubble_self_cavity_bg.xml create mode 100644 tuichat/src/main/res/drawable/chat_camera_back_btn_icon.png create mode 100644 tuichat/src/main/res/drawable/chat_camera_cancel_btn_icon.png create mode 100644 tuichat/src/main/res/drawable/chat_camera_commit_bg.png create mode 100644 tuichat/src/main/res/drawable/chat_camera_commit_btn_icon.png create mode 100644 tuichat/src/main/res/drawable/chat_camera_select_image_icon.png create mode 100644 tuichat/src/main/res/drawable/chat_divide_line.xml create mode 100644 tuichat/src/main/res/drawable/chat_face_delete_btn_icon.png create mode 100644 tuichat/src/main/res/drawable/chat_face_item_bg_selector.xml create mode 100644 tuichat/src/main/res/drawable/chat_ic_back.xml create mode 100644 tuichat/src/main/res/drawable/chat_input_shortcut_item_bg.xml create mode 100644 tuichat/src/main/res/drawable/chat_less_item_button.png create mode 100644 tuichat/src/main/res/drawable/chat_more_item_button.png create mode 100644 tuichat/src/main/res/drawable/chat_pinned_list_divider.xml create mode 100644 tuichat/src/main/res/drawable/chat_pinned_message_delete_button.png create mode 100644 tuichat/src/main/res/drawable/chat_pop_item_bg.xml create mode 100644 tuichat/src/main/res/drawable/chat_pop_menu_cancel_pin.png create mode 100644 tuichat/src/main/res/drawable/chat_pop_menu_divider.xml create mode 100644 tuichat/src/main/res/drawable/chat_pop_menu_pin.png create mode 100644 tuichat/src/main/res/drawable/chat_progress_download_icon.png create mode 100644 tuichat/src/main/res/drawable/chat_title_bar_more_menu_icon.png create mode 100644 tuichat/src/main/res/drawable/chat_translucent_round_rect_border.xml create mode 100644 tuichat/src/main/res/drawable/chat_up_arrow_icon.png create mode 100644 tuichat/src/main/res/drawable/chat_upload_icon.png create mode 100644 tuichat/src/main/res/drawable/chat_voice_msg_playing_1.png create mode 100644 tuichat/src/main/res/drawable/chat_voice_msg_playing_2.png create mode 100644 tuichat/src/main/res/drawable/chat_voice_msg_playing_3.png create mode 100644 tuichat/src/main/res/drawable/chat_voice_reply_icon.png create mode 100644 tuichat/src/main/res/drawable/contact_btn_bg_color.xml create mode 100644 tuichat/src/main/res/drawable/face_view_delete_button_border.xml create mode 100644 tuichat/src/main/res/drawable/face_view_send_button_border.xml create mode 100644 tuichat/src/main/res/drawable/group_msg_receipt_line_bg.xml create mode 100644 tuichat/src/main/res/drawable/ic_audio_call.xml create mode 100644 tuichat/src/main/res/drawable/ic_camera.xml create mode 100644 tuichat/src/main/res/drawable/ic_close_button.xml create mode 100644 tuichat/src/main/res/drawable/ic_download_button.xml create mode 100644 tuichat/src/main/res/drawable/ic_other_video_call.xml create mode 100644 tuichat/src/main/res/drawable/ic_pause_center.xml create mode 100644 tuichat/src/main/res/drawable/ic_pause_icon.xml create mode 100644 tuichat/src/main/res/drawable/ic_play_icon.xml create mode 100644 tuichat/src/main/res/drawable/ic_self_video_call.xml create mode 100644 tuichat/src/main/res/drawable/ic_volume_dialog_bg.xml create mode 100644 tuichat/src/main/res/drawable/layer_live_rating_bar.xml create mode 100644 tuichat/src/main/res/drawable/layer_progress.xml create mode 100644 tuichat/src/main/res/drawable/message_send_border.xml create mode 100644 tuichat/src/main/res/drawable/msg_editor_border.xml create mode 100644 tuichat/src/main/res/drawable/play_voice_message.xml create mode 100644 tuichat/src/main/res/drawable/ratingbar_opreview.png create mode 100644 tuichat/src/main/res/drawable/ratingbar_opreview_grey.png create mode 100644 tuichat/src/main/res/drawable/recording_volume.xml create mode 100644 tuichat/src/main/res/drawable/selector_face_text.xml create mode 100644 tuichat/src/main/res/drawable/shape_circle.xml create mode 100644 tuichat/src/main/res/drawable/shape_dialog.xml create mode 100644 tuichat/src/main/res/drawable/tuiemoji_default_emoji_group_icon.png create mode 100644 tuichat/src/main/res/drawable/tuiemoji_emoji_page_scroll_bar.xml create mode 100644 tuichat/src/main/res/drawable/tuiemoji_tab_item_bg.xml create mode 100644 tuichat/src/main/res/drawable/view_original_image_border.xml create mode 100644 tuichat/src/main/res/drawable/voice_btn_selector.xml create mode 100644 tuichat/src/main/res/layout/activity_group_notice.xml create mode 100644 tuichat/src/main/res/layout/activity_message_reply_detail.xml create mode 100644 tuichat/src/main/res/layout/chat_camera_activity_layout.xml create mode 100644 tuichat/src/main/res/layout/chat_camera_capture_layout.xml create mode 100644 tuichat/src/main/res/layout/chat_face_detail_layout.xml create mode 100644 tuichat/src/main/res/layout/chat_face_item_layout.xml create mode 100644 tuichat/src/main/res/layout/chat_face_page_item_layout.xml create mode 100644 tuichat/src/main/res/layout/chat_face_tab_item_layout.xml create mode 100644 tuichat/src/main/res/layout/chat_face_view_layout.xml create mode 100644 tuichat/src/main/res/layout/chat_forward_msg_content_line_layout.xml create mode 100644 tuichat/src/main/res/layout/chat_fragment.xml create mode 100644 tuichat/src/main/res/layout/chat_group_pinned_item_view_layout.xml create mode 100644 tuichat/src/main/res/layout/chat_group_pinned_view_dialog_layout.xml create mode 100644 tuichat/src/main/res/layout/chat_group_pinned_view_layout.xml create mode 100644 tuichat/src/main/res/layout/chat_input_camera_view.xml create mode 100644 tuichat/src/main/res/layout/chat_input_layout.xml create mode 100644 tuichat/src/main/res/layout/chat_input_layout_actoin.xml create mode 100644 tuichat/src/main/res/layout/chat_input_shortcut_item.xml create mode 100644 tuichat/src/main/res/layout/chat_inputmore_fragment.xml create mode 100644 tuichat/src/main/res/layout/chat_inputmore_layout.xml create mode 100644 tuichat/src/main/res/layout/chat_loading_progress_bar.xml create mode 100644 tuichat/src/main/res/layout/chat_pop_menu_item_layout.xml create mode 100644 tuichat/src/main/res/layout/chat_pop_menu_layout.xml create mode 100644 tuichat/src/main/res/layout/chat_reply_details_item_layout.xml create mode 100644 tuichat/src/main/res/layout/chat_reply_quote_face_layout.xml create mode 100644 tuichat/src/main/res/layout/chat_reply_quote_file_layout.xml create mode 100644 tuichat/src/main/res/layout/chat_reply_quote_image_layout.xml create mode 100644 tuichat/src/main/res/layout/chat_reply_quote_merge_layout.xml create mode 100644 tuichat/src/main/res/layout/chat_reply_quote_sound_layout.xml create mode 100644 tuichat/src/main/res/layout/chat_reply_quote_text_layout.xml create mode 100644 tuichat/src/main/res/layout/contact_friend_profile_activity.xml create mode 100644 tuichat/src/main/res/layout/contact_friend_profile_item_layout.xml create mode 100644 tuichat/src/main/res/layout/contact_friend_profile_layout.xml create mode 100644 tuichat/src/main/res/layout/contact_friend_profile_warning_item_layout.xml create mode 100644 tuichat/src/main/res/layout/custom_evaluation_message_layout.xml create mode 100644 tuichat/src/main/res/layout/custom_order_message_layout.xml create mode 100644 tuichat/src/main/res/layout/forward_chat_layout.xml create mode 100644 tuichat/src/main/res/layout/forward_dialog_layout.xml create mode 100644 tuichat/src/main/res/layout/forward_msg_holder.xml create mode 100644 tuichat/src/main/res/layout/group_info_activity.xml create mode 100644 tuichat/src/main/res/layout/group_info_fragment.xml create mode 100644 tuichat/src/main/res/layout/group_info_layout.xml create mode 100644 tuichat/src/main/res/layout/group_profile_bottom_item_layout.xml create mode 100644 tuichat/src/main/res/layout/group_profile_settings_item_layout.xml create mode 100644 tuichat/src/main/res/layout/group_profile_warning_item_layout.xml create mode 100644 tuichat/src/main/res/layout/group_receipt_member_item.xml create mode 100644 tuichat/src/main/res/layout/image_video_scan_item.xml create mode 100644 tuichat/src/main/res/layout/item_tab.xml create mode 100644 tuichat/src/main/res/layout/message_adapter_content_audio.xml create mode 100644 tuichat/src/main/res/layout/message_adapter_content_calling.xml create mode 100644 tuichat/src/main/res/layout/message_adapter_content_file.xml create mode 100644 tuichat/src/main/res/layout/message_adapter_content_image.xml create mode 100644 tuichat/src/main/res/layout/message_adapter_content_reply.xml create mode 100644 tuichat/src/main/res/layout/message_adapter_content_text.xml create mode 100644 tuichat/src/main/res/layout/message_adapter_content_tips.xml create mode 100644 tuichat/src/main/res/layout/message_adapter_item_empty.xml create mode 100644 tuichat/src/main/res/layout/message_content_layout.xml create mode 100644 tuichat/src/main/res/layout/msg_receipt_detail_layout.xml create mode 100644 tuichat/src/main/res/layout/notice_layout.xml create mode 100644 tuichat/src/main/res/layout/quote_message_content_layout.xml create mode 100644 tuichat/src/main/res/layout/reply_preview_layout.xml create mode 100644 tuichat/src/main/res/layout/test_custom_message_layout1.xml create mode 100644 tuichat/src/main/res/layout/tuichat_chat_activity_layout.xml create mode 100644 tuichat/src/main/res/layout/tuichat_chat_layout.xml create mode 100644 tuichat/src/main/res/layout/tuichat_image_video_scan_layout.xml create mode 100644 tuichat/src/main/res/values-ar/strings-emoji.xml create mode 100644 tuichat/src/main/res/values-ar/strings.xml create mode 100644 tuichat/src/main/res/values-zh-rHK/strings-emoji.xml create mode 100644 tuichat/src/main/res/values-zh-rHK/strings.xml create mode 100644 tuichat/src/main/res/values-zh/strings-emoji.xml create mode 100644 tuichat/src/main/res/values-zh/strings.xml create mode 100644 tuichat/src/main/res/values/arrays.xml create mode 100644 tuichat/src/main/res/values/attrs.xml create mode 100644 tuichat/src/main/res/values/colors.xml create mode 100644 tuichat/src/main/res/values/dimens.xml create mode 100644 tuichat/src/main/res/values/strings-emoji.xml create mode 100644 tuichat/src/main/res/values/strings.xml create mode 100644 tuichat/src/main/res/values/styles.xml create mode 100644 tuichat/src/main/res/values/tui_theme_attrs.xml create mode 100644 tuiconversation/build.gradle create mode 100644 tuiconversation/src/main/AndroidManifest.xml create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/TUIConversationConstants.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/TUIConversationService.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/bean/ConversationGroupBean.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/bean/ConversationPopMenuItem.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/bean/ConversationUserStatusBean.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/bean/DraftInfo.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/classicui/interfaces/IConversationLayout.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/classicui/interfaces/IConversationListLayout.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/classicui/interfaces/IConversationProvider.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/classicui/interfaces/OnConversationAdapterListener.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/classicui/page/TUIConversationFragment.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/classicui/page/TUIConversationFragmentContainer.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/classicui/page/TUIFoldedConversationActivity.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/classicui/page/TUIFoldedConversationFragment.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/classicui/page/TUIForwardSelectActivity.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/classicui/page/TUIForwardSelectFragment.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/classicui/util/TUIConversationUtils.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/classicui/widget/ConversationBaseHolder.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/classicui/widget/ConversationCommonHolder.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/classicui/widget/ConversationCustomHolder.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/classicui/widget/ConversationIconView.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/classicui/widget/ConversationLayout.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/classicui/widget/ConversationListAdapter.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/classicui/widget/ConversationListLayout.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/classicui/widget/ConversationTabLayoutMediator.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/classicui/widget/FoldedConversationLayout.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/classicui/widget/FoldedConversationListLayout.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/classicui/widget/ForwardConversationSelectorAdapter.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/classicui/widget/ForwardSelectLayout.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/commonutil/ConversationUtils.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/commonutil/TUIConversationLog.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/commonutil/TUIConversationUtils.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/config/TUIConversationConfig.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/config/classicui/TUIConversationConfigClassic.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/config/minimalistui/TUIConversationConfigMinimalist.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/interfaces/ConversationEventListener.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/interfaces/ConversationGroupNotifyListener.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/interfaces/IConversationListAdapter.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/minimalistui/interfaces/IConversationLayout.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/minimalistui/interfaces/IConversationListLayout.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/minimalistui/interfaces/IConversationProvider.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/minimalistui/interfaces/OnConversationAdapterListener.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/minimalistui/page/TUIConversationMinimalistFragment.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/minimalistui/page/TUIFoldedConversationMinimalistActivity.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/minimalistui/page/TUIFoldedConversationMinimalistFragment.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/minimalistui/page/TUIForwardSelectMinimalistActivity.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/minimalistui/page/TUIForwardSelectMinimalistFragment.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/minimalistui/util/TUIConversationUtils.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/minimalistui/widget/ConversationBaseHolder.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/minimalistui/widget/ConversationCommonHolder.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/minimalistui/widget/ConversationCustomHolder.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/minimalistui/widget/ConversationIconView.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/minimalistui/widget/ConversationLayout.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/minimalistui/widget/ConversationListAdapter.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/minimalistui/widget/ConversationListLayout.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/minimalistui/widget/FoldedConversationLayout.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/minimalistui/widget/FoldedConversationListLayout.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/minimalistui/widget/ForwardSelectLayout.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/minimalistui/widget/Menu.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/model/ConversationProvider.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/presenter/ConversationFoldPresenter.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/presenter/ConversationIconPresenter.java create mode 100644 tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/presenter/ConversationPresenter.java create mode 100644 tuiconversation/src/main/res-light/values/light_colors.xml create mode 100644 tuiconversation/src/main/res-light/values/light_styles.xml create mode 100644 tuiconversation/src/main/res-lively/values/lively_colors.xml create mode 100644 tuiconversation/src/main/res-lively/values/lively_styles.xml create mode 100644 tuiconversation/src/main/res-minimalistui/drawable/conversation_forward_selected_page_border.xml create mode 100644 tuiconversation/src/main/res-minimalistui/drawable/conversation_minimalist_create_icon.png create mode 100644 tuiconversation/src/main/res-minimalistui/drawable/conversation_minimalist_edit_icon.png create mode 100644 tuiconversation/src/main/res-minimalistui/drawable/conversation_minimalist_message_status_send_all_read.png create mode 100644 tuiconversation/src/main/res-minimalistui/drawable/conversation_minimalist_message_status_send_no_read.png create mode 100644 tuiconversation/src/main/res-minimalistui/drawable/conversation_minimalist_more_icon.png create mode 100644 tuiconversation/src/main/res-minimalistui/drawable/conversation_minimalist_not_disturb_icon.png create mode 100644 tuiconversation/src/main/res-minimalistui/drawable/conversation_minimalist_online_icon.png create mode 100644 tuiconversation/src/main/res-minimalistui/drawable/conversation_minimalist_search_border.xml create mode 100644 tuiconversation/src/main/res-minimalistui/drawable/conversation_minimalist_search_icon.png create mode 100644 tuiconversation/src/main/res-minimalistui/layout/conversation_minimalist_forward_label_adapter.xml create mode 100644 tuiconversation/src/main/res-minimalistui/layout/conversation_minimalist_pop_menu_item.xml create mode 100644 tuiconversation/src/main/res-minimalistui/layout/minimalist_bottom_bar.xml create mode 100644 tuiconversation/src/main/res-minimalistui/layout/minimalist_conversation_list_item_sub_layout.xml create mode 100644 tuiconversation/src/main/res-minimalistui/layout/minimalist_folded_fragment.xml create mode 100644 tuiconversation/src/main/res-minimalistui/layout/minimalist_folded_layout.xml create mode 100644 tuiconversation/src/main/res-minimalistui/layout/minimalist_forward_conversation_selector_item.xml create mode 100644 tuiconversation/src/main/res-minimalistui/layout/minimalist_forward_fragment.xml create mode 100644 tuiconversation/src/main/res-minimalistui/layout/minimalist_forward_layout.xml create mode 100644 tuiconversation/src/main/res-minimalistui/layout/minimalist_header_view_layout.xml create mode 100644 tuiconversation/src/main/res-minimalistui/layout/minimalist_more_dialog.xml create mode 100644 tuiconversation/src/main/res-minimalistui/layout/minimalistui_conversation_forward_list_item_layout.xml create mode 100644 tuiconversation/src/main/res-minimalistui/layout/minimalistui_conversation_fragment.xml create mode 100644 tuiconversation/src/main/res-minimalistui/layout/minimalistui_conversation_layout.xml create mode 100644 tuiconversation/src/main/res-minimalistui/layout/minimalistui_conversation_list_item_layout.xml create mode 100644 tuiconversation/src/main/res-minimalistui/values-ar/minimalist_strings.xml create mode 100644 tuiconversation/src/main/res-minimalistui/values-zh-rHK/minimalist_strings.xml create mode 100644 tuiconversation/src/main/res-minimalistui/values-zh/minimalist_strings.xml create mode 100644 tuiconversation/src/main/res-minimalistui/values/minimalist_attrs.xml create mode 100644 tuiconversation/src/main/res-minimalistui/values/minimalist_colors.xml create mode 100644 tuiconversation/src/main/res-minimalistui/values/minimalist_strings.xml create mode 100644 tuiconversation/src/main/res-serious/values/serious_colors.xml create mode 100644 tuiconversation/src/main/res-serious/values/serious_styles.xml create mode 100644 tuiconversation/src/main/res/drawable-xxhdpi/conversation_create_c2c.png create mode 100644 tuiconversation/src/main/res/drawable-xxhdpi/conversation_group_icon.png create mode 100644 tuiconversation/src/main/res/drawable-xxhdpi/conversation_ic_fold.png create mode 100644 tuiconversation/src/main/res/drawable-xxhdpi/conversation_mark_banner.png create mode 100644 tuiconversation/src/main/res/drawable-xxhdpi/conversation_null_data.png create mode 100644 tuiconversation/src/main/res/drawable-xxhdpi/conversation_tab_menu.png create mode 100644 tuiconversation/src/main/res/drawable/conversation_checkbox_selector.xml create mode 100644 tuiconversation/src/main/res/drawable/conversation_ic_disturb.xml create mode 100644 tuiconversation/src/main/res/drawable/conversation_ic_send_failed.xml create mode 100644 tuiconversation/src/main/res/drawable/conversation_ic_sending_status.xml create mode 100644 tuiconversation/src/main/res/drawable/conversation_shape_full_tab_indicator.xml create mode 100644 tuiconversation/src/main/res/drawable/conversation_shape_tab_bg.xml create mode 100644 tuiconversation/src/main/res/layout/conversation_base_fragment.xml create mode 100644 tuiconversation/src/main/res/layout/conversation_custom_adapter.xml create mode 100644 tuiconversation/src/main/res/layout/conversation_folded_activity.xml create mode 100644 tuiconversation/src/main/res/layout/conversation_folded_fragment.xml create mode 100644 tuiconversation/src/main/res/layout/conversation_folded_layout.xml create mode 100644 tuiconversation/src/main/res/layout/conversation_forward_activity_layout.xml create mode 100644 tuiconversation/src/main/res/layout/conversation_forward_conversation_selector_item.xml create mode 100644 tuiconversation/src/main/res/layout/conversation_forward_fragment.xml create mode 100644 tuiconversation/src/main/res/layout/conversation_forward_label_adapter.xml create mode 100644 tuiconversation/src/main/res/layout/conversation_forward_layout.xml create mode 100644 tuiconversation/src/main/res/layout/conversation_forward_select_adapter.xml create mode 100644 tuiconversation/src/main/res/layout/conversation_fragment.xml create mode 100644 tuiconversation/src/main/res/layout/conversation_group_tab_item.xml create mode 100644 tuiconversation/src/main/res/layout/conversation_layout.xml create mode 100644 tuiconversation/src/main/res/layout/conversation_list_item_layout.xml create mode 100644 tuiconversation/src/main/res/layout/conversation_loading_progress_bar.xml create mode 100644 tuiconversation/src/main/res/layout/conversation_null_layout.xml create mode 100644 tuiconversation/src/main/res/layout/conversation_pop_menu_layout.xml create mode 100644 tuiconversation/src/main/res/layout/convesation_list_header.xml create mode 100644 tuiconversation/src/main/res/values-ar/strings.xml create mode 100644 tuiconversation/src/main/res/values-zh-rHK/strings.xml create mode 100644 tuiconversation/src/main/res/values-zh/strings.xml create mode 100644 tuiconversation/src/main/res/values/attrs.xml create mode 100644 tuiconversation/src/main/res/values/colors.xml create mode 100644 tuiconversation/src/main/res/values/dimens.xml create mode 100644 tuiconversation/src/main/res/values/strings.xml create mode 100644 tuiconversation/src/main/res/values/styles.xml diff --git a/timcommon/build.gradle b/timcommon/build.gradle new file mode 100644 index 00000000..1edfdd6a --- /dev/null +++ b/timcommon/build.gradle @@ -0,0 +1,56 @@ +plugins { + id 'com.android.library' +} + +android { + compileSdkVersion 30 + buildToolsVersion "30.0.3" + namespace "com.tencent.qcloud.tuikit.timcommon" + defaultConfig { + minSdkVersion 19 + targetSdkVersion 30 + versionCode 1 + versionName "1.0" + } + buildFeatures { + buildConfig = true + } + buildTypes { + release { + minifyEnabled false + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + 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.5.6864" + + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + 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' + +} + diff --git a/timcommon/src/main/AndroidManifest.xml b/timcommon/src/main/AndroidManifest.xml new file mode 100644 index 00000000..3b4f17f4 --- /dev/null +++ b/timcommon/src/main/AndroidManifest.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/TIMCommonConfig.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/TIMCommonConfig.java new file mode 100644 index 00000000..bd556a8b --- /dev/null +++ b/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; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/TIMCommonService.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/TIMCommonService.java new file mode 100644 index 00000000..4de6813e --- /dev/null +++ b/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(); + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/ChatFace.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/ChatFace.java new file mode 100644 index 00000000..0c2ab9f0 --- /dev/null +++ b/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 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 faceGroup) { + this.faceGroup = faceGroup; + } + + public FaceGroup 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; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/CustomFace.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/CustomFace.java new file mode 100644 index 00000000..462079ff --- /dev/null +++ b/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; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/Emoji.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/Emoji.java new file mode 100644 index 00000000..3da7c63a --- /dev/null +++ b/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; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/FaceGroup.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/FaceGroup.java new file mode 100644 index 00000000..8d63bfc2 --- /dev/null +++ b/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 { + 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 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 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; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/FriendProfileBean.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/FriendProfileBean.java new file mode 100644 index 00000000..1ae95e9a --- /dev/null +++ b/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; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/GroupProfileBean.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/GroupProfileBean.java new file mode 100644 index 00000000..765bb06d --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/GroupProfileBean.java @@ -0,0 +1,128 @@ +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 roleInGroup; + private int recvOpt; + private int approveOpt; + private int addOpt; + private boolean isAllMuted = false; + + 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 int getRoleInGroup() { + return roleInGroup; + } + + public void setRoleInGroup(int roleInGroup) { + this.roleInGroup = roleInGroup; + } + + public boolean canManage() { + return roleInGroup == GroupMemberInfo.MEMBER_ROLE_OWNER || roleInGroup == GroupMemberInfo.MEMBER_ROLE_ADMINISTRATOR; + } + + public boolean isOwner() { + return roleInGroup == GroupMemberInfo.MEMBER_ROLE_OWNER; + } + + public boolean isAdmin() { + return roleInGroup == 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; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/MessageFeature.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/MessageFeature.java new file mode 100644 index 00000000..79cc95d9 --- /dev/null +++ b/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; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/MessageReceiptInfo.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/MessageReceiptInfo.java new file mode 100644 index 00000000..069db171 --- /dev/null +++ b/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; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/MessageRepliesBean.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/MessageRepliesBean.java new file mode 100644 index 00000000..7e4ab040 --- /dev/null +++ b/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 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 getReplies() { + return replies; + } + + public void setReplies(List 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; + } + } + +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/TUIMessageBean.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/TUIMessageBean.java new file mode 100644 index 00000000..0d5abfa4 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/TUIMessageBean.java @@ -0,0 +1,472 @@ +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 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 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 true; + } + + 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 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 getAdditionalUserIDList() { + Set userIdSet = new HashSet<>(); + MessageRepliesBean messageRepliesBean = getMessageRepliesBean(); + if (messageRepliesBean != null && messageRepliesBean.getRepliesSize() > 0) { + List 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 getReplyQuoteBeanClass() { + return null; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/TUIReplyQuoteBean.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/TUIReplyQuoteBean.java new file mode 100644 index 00000000..df293613 --- /dev/null +++ b/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 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; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/UserBean.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/bean/UserBean.java new file mode 100644 index 00000000..a13aad6b --- /dev/null +++ b/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; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/classicui/widget/message/MessageBaseHolder.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/classicui/widget/message/MessageBaseHolder.java new file mode 100644 index 00000000..bfdc342a --- /dev/null +++ b/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 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); + } + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/classicui/widget/message/MessageContentHolder.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/classicui/widget/message/MessageContentHolder.java new file mode 100644 index 00000000..4aa87ed8 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/classicui/widget/message/MessageContentHolder.java @@ -0,0 +1,692 @@ +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.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 extends MessageBaseHolder { + + 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 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 dataSource) { + if (dataSource == null || dataSource.isEmpty()) { + mForwardDataSource = null; + } + + List mediaSource = new ArrayList<>(); + for (TUIMessageBean messageBean : dataSource) { + int type = messageBean.getMsgType(); + if (type == V2TIMMessage.V2TIM_ELEM_TYPE_IMAGE || type == V2TIMMessage.V2TIM_ELEM_TYPE_VIDEO) { + mediaSource.add(messageBean); + } + } + mForwardDataSource = mediaSource; + } + + public List 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) { + if (onItemClickListener != null) { + msgContentFrame.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + onItemClickListener.onMessageLongClick(v, msg); + return true; + } + }); + + msgArea.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + onItemClickListener.onMessageLongClick(msgArea, msg); + return true; + } + }); + + leftUserIcon.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + onItemClickListener.onUserIconClick(view, msg); + } + }); + leftUserIcon.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View view) { + onItemClickListener.onUserIconLongClick(view, msg); + return true; + } + }); + rightUserIcon.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + onItemClickListener.onUserIconClick(view, msg); + } + }); + } + + if (msg.getStatus() == TUIMessageBean.MSG_STATUS_SEND_FAIL) { + msgContentFrame.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View 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 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 idList = new ArrayList<>(); + idList.add(msg.getSender()); + V2TIMManager.getInstance().getUsersInfo(idList, new V2TIMValueCallback>() { + @Override + public void onSuccess(List v2TIMUserFullInfos) { + if (v2TIMUserFullInfos == null || v2TIMUserFullInfos.isEmpty()) { + return; + } + V2TIMUserFullInfo userInfo = v2TIMUserFullInfos.get(0); + String faceUrl = userInfo.getFaceUrl(); + if (TextUtils.isEmpty(userInfo.getFaceUrl())) { + faceUrl = ""; + } + mAdapter.getUserFaceUrlCache().pushFaceUrl(userInfo.getUserID(), faceUrl); + mAdapter.onItemRefresh(msg); + } + + @Override + public void onError(int code, String desc) { + setupAvatar(""); + } + }); + } else { + setupAvatar(cachedFaceUrl); + } + } else { + setupAvatar(msg.getFaceUrl()); + } + } + + private void setupAvatar(Object faceUrl) { + int avatarSize = 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; + } + + RequestBuilder errorRequestBuilder = + Glide.with(itemView.getContext()) + .load(TUIThemeManager.getAttrResId(leftUserIcon.getContext(), com.tencent.qcloud.tuikit.timcommon.R.attr.core_default_user_icon)) + .placeholder(TUIThemeManager.getAttrResId(leftUserIcon.getContext(), com.tencent.qcloud.tuikit.timcommon.R.attr.core_default_user_icon)) + .transform(new RoundedCorners(radius)); + + Glide.with(itemView.getContext()).load(faceUrl).transform(new RoundedCorners(radius)).error(errorRequestBuilder).into(renderedView); + } + + 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 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; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/classicui/widget/message/SelectionHelper.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/classicui/widget/message/SelectionHelper.java new file mode 100644 index 00000000..e12176f6 --- /dev/null +++ b/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 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; + } +} \ No newline at end of file diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/classicui/widget/message/TUIReplyQuoteView.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/classicui/widget/message/TUIReplyQuoteView.java new file mode 100644 index 00000000..a0b4399c --- /dev/null +++ b/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> 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) {} + +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/BottomSelectSheet.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/BottomSelectSheet.java new file mode 100644 index 00000000..46eaa5d2 --- /dev/null +++ b/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 selectList = new ArrayList<>(); + private Dialog dialog; + private ArrayAdapter 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 selectList) { + this.selectList.clear(); + this.selectList.addAll(selectList); + } + + public void setOnClickListener(BottomSelectSheetOnClickListener onClickListener) { + this.onClickListener = onClickListener; + } + + public interface BottomSelectSheetOnClickListener { + void onSheetClick(int index); + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/CustomLinearLayoutManager.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/CustomLinearLayoutManager.java new file mode 100644 index 00000000..d2ad0ea0 --- /dev/null +++ b/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()); + } + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/IndicatorView.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/IndicatorView.java new file mode 100644 index 00000000..76151237 --- /dev/null +++ b/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 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(); + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/LineControllerView.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/LineControllerView.java new file mode 100644 index 00000000..d36cabc4 --- /dev/null +++ b/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); + } + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/MaxWidthFrameLayout.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/MaxWidthFrameLayout.java new file mode 100644 index 00000000..bd76dc78 --- /dev/null +++ b/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); + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/MaxWidthLinearLayout.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/MaxWidthLinearLayout.java new file mode 100644 index 00000000..3a416f32 --- /dev/null +++ b/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); + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/MinimalistLineControllerView.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/MinimalistLineControllerView.java new file mode 100644 index 00000000..c8105f25 --- /dev/null +++ b/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); + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/MinimalistTitleBar.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/MinimalistTitleBar.java new file mode 100644 index 00000000..4159247f --- /dev/null +++ b/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); + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/PopupInputCard.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/PopupInputCard.java new file mode 100644 index 00000000..51a15c5e --- /dev/null +++ b/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(); + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/RoundCornerImageView.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/RoundCornerImageView.java new file mode 100644 index 00000000..6712bb92 --- /dev/null +++ b/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); + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/RoundFrameLayout.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/RoundFrameLayout.java new file mode 100644 index 00000000..fc9e056e --- /dev/null +++ b/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); + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/SwitchCustomWidth.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/SwitchCustomWidth.java new file mode 100644 index 00000000..afff9757 --- /dev/null +++ b/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 clazz = SwitchCompat.class; + Field mSwitchWidthFiled = clazz.getDeclaredField("mSwitchWidth"); + mSwitchWidthFiled.setAccessible(true); + mSwitchWidthFiled.set(this, customSwitchWidth); + } catch (Exception e) { + Log.w(TAG, e.getMessage()); + } + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/TitleBarLayout.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/TitleBarLayout.java new file mode 100644 index 00000000..14311fbb --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/TitleBarLayout.java @@ -0,0 +1,209 @@ +package com.tencent.qcloud.tuikit.timcommon.component; + +import android.annotation.SuppressLint; +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 ImageView mLeftUserAvatar; + private TextView mUserNickName; + 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); + } + + @SuppressLint("WrongViewCast") + 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); + + mLeftUserAvatar = findViewById(R.id.tab_icon); + mUserNickName = findViewById(R.id.tab_nickname); + + 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; + } + + public TextView getmUserNickName() { + return mUserNickName; + } + + public void setmUserNickName(TextView mUserNickName) { + this.mUserNickName = mUserNickName; + } + + public ImageView getmLeftUserAvatar() { + return mLeftUserAvatar; + } + + public void setmLeftUserAvatar(ImageView mLeftUserAvatar) { + this.mLeftUserAvatar = mLeftUserAvatar; + } + + @Override + public TextView getMiddleTitle() { + return mCenterTitle; + } + + @Override + public TextView getRightTitle() { + return mRightTitle; + } + + public UnreadCountTextView getUnreadCountTextView() { + return unreadCountTextView; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/UnreadCountTextView.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/UnreadCountTextView.java new file mode 100644 index 00000000..682d4166 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/UnreadCountTextView.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.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); + + // 获取文本的宽度 +// float textWidth = getPaint().measureText(getText().toString()); +// +// float unreadMarkerLeft = textWidth + dp2px(4); +// float unreadMarkerTop = (getMeasuredHeight() - mNormalSize) / 2; +// float unreadMarkerRight = unreadMarkerLeft + mNormalSize; +// float unreadMarkerBottom = unreadMarkerTop + mNormalSize; +// +// 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(unreadMarkerLeft, unreadMarkerTop, unreadMarkerRight, unreadMarkerBottom), mPaint); +// } else if (getText().length() > 1) { +// canvas.drawRoundRect( +// new RectF(unreadMarkerLeft, unreadMarkerTop, unreadMarkerRight + dp2px((getText().length() - 1) * 10), unreadMarkerBottom), +// 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); + + // 固定高度为 mNormalSize +// int height = mNormalSize; +// +// // 计算文本宽度 + 未读标记宽度(包含间距) +// float textWidth = getPaint().measureText(getText().toString()); +// float unreadMarkerWidth = mNormalSize + dp2px(4); // 未读标记 + 间距 +// +// int width = (int) (textWidth + unreadMarkerWidth); +// +// // 如果是多个字符的计数(如 "99+"),增加额外宽度 +// if (getText().length() > 1) { +// width += 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); + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/action/PopActionClickListener.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/action/PopActionClickListener.java new file mode 100644 index 00000000..0e9f9f30 --- /dev/null +++ b/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); +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/action/PopDialogAdapter.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/action/PopDialogAdapter.java new file mode 100644 index 00000000..4ed6ebfc --- /dev/null +++ b/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 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; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/action/PopMenuAction.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/action/PopMenuAction.java new file mode 100644 index 00000000..b7f150a7 --- /dev/null +++ b/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; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/action/PopMenuAdapter.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/action/PopMenuAdapter.java new file mode 100644 index 00000000..1bb3eb39 --- /dev/null +++ b/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 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; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/activities/BaseLightActivity.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/activities/BaseLightActivity.java new file mode 100644 index 00000000..220e1203 --- /dev/null +++ b/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); + } + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/activities/BaseMinimalistLightActivity.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/activities/BaseMinimalistLightActivity.java new file mode 100644 index 00000000..99c069eb --- /dev/null +++ b/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); + } + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/activities/ImageSelectActivity.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/activities/ImageSelectActivity.java new file mode 100644 index 00000000..24ab1c7a --- /dev/null +++ b/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 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) 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() { + @Override + public boolean onLoadFailed(@Nullable GlideException e, Object model, Target 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 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 { + private int itemWidth; + private int itemHeight; + + private List data; + private ImageBean selected; + private int placeHolder; + private OnItemClickListener onItemClickListener; + + public void setData(List 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 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 getGroupGridAvatar() { + return groupGridAvatar; + } + + public void setGroupGridAvatar(List groupGridAvatar) { + this.groupGridAvatar = groupGridAvatar; + } + + public String getImageId() { + return imageId; + } + + public void setImageId(String imageId) { + this.imageId = imageId; + } + } +} \ No newline at end of file diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/activities/ImageSelectMinimalistActivity.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/activities/ImageSelectMinimalistActivity.java new file mode 100644 index 00000000..673126f2 --- /dev/null +++ b/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 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) 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() { + @Override + public boolean onLoadFailed(@Nullable GlideException e, Object model, Target 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 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 { + private int itemWidth; + private int itemHeight; + + private List data; + private ImageBean selected; + private int placeHolder; + private OnItemClickListener onItemClickListener; + + public void setData(List 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 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 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 getGroupGridAvatar() { + return groupGridAvatar; + } + + public void setGroupGridAvatar(List groupGridAvatar) { + this.groupGridAvatar = groupGridAvatar; + } + + public String getImageId() { + return imageId; + } + + public void setImageId(String imageId) { + this.imageId = imageId; + } + } +} \ No newline at end of file diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/activities/SelectionActivity.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/activities/SelectionActivity.java new file mode 100644 index 00000000..ad79fb51 --- /dev/null +++ b/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 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 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 { + int selectedItem = -1; + ArrayList data = new ArrayList<>(); + + public void setData(ArrayList 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; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/activities/SelectionMinimalistActivity.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/activities/SelectionMinimalistActivity.java new file mode 100644 index 00000000..67d9f91f --- /dev/null +++ b/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 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 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 { + int selectedItem = -1; + ArrayList data = new ArrayList<>(); + + public void setData(ArrayList 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; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/dialog/TUIKitDialog.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/dialog/TUIKitDialog.java new file mode 100644 index 00000000..268ea2e6 --- /dev/null +++ b/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; + + 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(); + } + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/face/CenterImageSpan.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/face/CenterImageSpan.java new file mode 100644 index 00000000..f62f7d92 --- /dev/null +++ b/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); + } + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/face/FaceManager.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/face/FaceManager.java new file mode 100644 index 00000000..41bac0ce --- /dev/null +++ b/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 emojiMap = new LinkedHashMap<>(); + private final Context context; + + private final Map> faceGroupMap = new ConcurrentHashMap<>(); + + private FaceManager() { + context = TIMCommonService.getAppContext(); + } + + private static FaceManager getInstance() { + return FaceManagerHolder.instance; + } + + public static ArrayList getEmojiList() { + return new ArrayList<>(getInstance().emojiMap.values()); + } + + public static Map 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 void addFaceGroup(int groupID, FaceGroup faceGroup) { + faceGroup.setGroupID(groupID); + getInstance().faceGroupMap.put(groupID, faceGroup); + if (faceGroup.isEmojiGroup()) { + List faces = faceGroup.getFaces(); + for (T face : faces) { + getInstance().emojiMap.put(face.getFaceKey(), (Emoji) face); + } + } + } + + public static List 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() { + @Override + public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { + return false; + } + + @Override + public boolean onResourceReady(Drawable resource, Object model, Target 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() { + @Override + public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { + return false; + } + + @Override + public boolean onResourceReady(Drawable resource, Object model, Target 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() { + @Override + public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { + return false; + } + + @Override + public boolean onResourceReady(Bitmap resource, Object model, Target 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() { + @Override + public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { + return false; + } + + @Override + public boolean onResourceReady(Drawable resource, Object model, Target 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 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 splitEmojiText(String text) { + String regex = "\\[(\\S+?)\\]"; + Pattern p = Pattern.compile(regex); + Matcher m = p.matcher(text); + ArrayList 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 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 findEmojiKeyListFromText(String text) { + if (TextUtils.isEmpty(text)) { + return null; + } + List 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; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/face/RecentEmojiManager.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/face/RecentEmojiManager.java new file mode 100644 index 00000000..f11c4032 --- /dev/null +++ b/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 emojiList) { + getInstance().putCollection(DEFAULT_RECENT_EMOJI_KEY, emojiList); + } + + public RecentEmojiManager putCollection(String key, List 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 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 collection = null; + if (collectionObj instanceof List) { + collection = (List) collectionObj; + } + return collection; + } catch (Exception e) { + return null; + } + } + + public static List getCollection() { + return getInstance().getCollection(DEFAULT_RECENT_EMOJI_KEY); + } + + public static void updateRecentUseEmoji(String emojiKey) { + List recentList = getCollection(); + recentList.remove(emojiKey); + recentList.add(0, emojiKey); + if (recentList.size() > DEFAULT_RECENT_NUM) { + recentList.remove(recentList.size() - 1); + } + putCollection(recentList); + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/gatherimage/MultiImageData.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/gatherimage/MultiImageData.java new file mode 100644 index 00000000..8230a7c6 --- /dev/null +++ b/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 imageUrls; + int defaultImageResId; + Map 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 imageUrls, int defaultImageResId) { + this.imageUrls = imageUrls; + this.defaultImageResId = defaultImageResId; + } + + public int getDefaultImageResId() { + return defaultImageResId; + } + + public void setDefaultImageResId(int defaultImageResId) { + this.defaultImageResId = defaultImageResId; + } + + public List getImageUrls() { + return imageUrls; + } + + public void setImageUrls(List 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; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/gatherimage/ShadeImageView.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/gatherimage/ShadeImageView.java new file mode 100644 index 00000000..2f455921 --- /dev/null +++ b/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 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(); + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/gatherimage/SynthesizedImageView.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/gatherimage/SynthesizedImageView.java new file mode 100644 index 00000000..83ef6545 --- /dev/null +++ b/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 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(); + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/gatherimage/Synthesizer.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/gatherimage/Synthesizer.java new file mode 100644 index 00000000..25d46fdd --- /dev/null +++ b/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); +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/gatherimage/TeamHeadSynthesizer.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/gatherimage/TeamHeadSynthesizer.java new file mode 100644 index 00000000..679196ab --- /dev/null +++ b/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 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); + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/gatherimage/UserIconView.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/gatherimage/UserIconView.java new file mode 100644 index 00000000..35cf3b83 --- /dev/null +++ b/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 iconUrls) { + mIconView.displayImage(iconUrls).load(null); + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/highlight/HighlightPresenter.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/highlight/HighlightPresenter.java new file mode 100644 index 00000000..3bc6e7a9 --- /dev/null +++ b/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> highlightListenerMap = new HashMap<>(); + + private final Map 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 listener = highlightListenerMap.get(highlightID); + if (listener != null) { + return listener.get(); + } + return null; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/impl/GlideEngine.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/impl/GlideEngine.java new file mode 100644 index 00000000..94378b44 --- /dev/null +++ b/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); + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/interfaces/ILayout.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/interfaces/ILayout.java new file mode 100644 index 00000000..88879bb4 --- /dev/null +++ b/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); +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/interfaces/ITitleBarLayout.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/interfaces/ITitleBarLayout.java new file mode 100644 index 00000000..0331c7c7 --- /dev/null +++ b/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 + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/interfaces/IUIKitCallback.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/interfaces/IUIKitCallback.java new file mode 100644 index 00000000..0b181a7f --- /dev/null +++ b/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 { + 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 void callbackOnSuccess(IUIKitCallback callback, O data) { + if (callback != null) { + callback.onSuccess(data); + } + } + + public static void callbackOnError(IUIKitCallback callback, int errCode, String errMsg, O data) { + if (callback != null) { + callback.onError(errCode, errMsg, data); + } + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/Compat.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/Compat.java new file mode 100644 index 00000000..d0909d04 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/Compat.java @@ -0,0 +1,40 @@ +/* + Copyright 2011, 2012 Chris Banes. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +package com.tencent.qcloud.tuikit.timcommon.component.photoview; + +import android.annotation.TargetApi; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.view.View; + +class Compat { + + private static final int SIXTY_FPS_INTERVAL = 1000 / 60; + + public static void postOnAnimation(View view, Runnable runnable) { + if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) { + postOnAnimationJellyBean(view, runnable); + } else { + view.postDelayed(runnable, SIXTY_FPS_INTERVAL); + } + } + + @TargetApi(16) + private static void postOnAnimationJellyBean(View view, Runnable runnable) { + view.postOnAnimation(runnable); + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/CustomGestureDetector.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/CustomGestureDetector.java new file mode 100644 index 00000000..8e5a8468 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/CustomGestureDetector.java @@ -0,0 +1,221 @@ +/* + Copyright 2011, 2012 Chris Banes. +

+ Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at +

+ http://www.apache.org/licenses/LICENSE-2.0 +

+ Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +package com.tencent.qcloud.tuikit.timcommon.component.photoview; + +import android.content.Context; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.view.VelocityTracker; +import android.view.ViewConfiguration; + +/** + * Does a whole lot of gesture detecting. + */ +class CustomGestureDetector { + + private static final int INVALID_POINTER_ID = -1; + + private int mActivePointerId = INVALID_POINTER_ID; + private int mActivePointerIndex = 0; + private final ScaleGestureDetector mDetector; + + private VelocityTracker mVelocityTracker; + private boolean mIsDragging; + private float mLastTouchX; + private float mLastTouchY; + private final float mTouchSlop; + private final float mMinimumVelocity; + private OnGestureListener mListener; + + CustomGestureDetector(Context context, OnGestureListener listener) { + final ViewConfiguration configuration = ViewConfiguration + .get(context); + mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); + mTouchSlop = configuration.getScaledTouchSlop(); + + mListener = listener; + ScaleGestureDetector.OnScaleGestureListener mScaleListener = new ScaleGestureDetector.OnScaleGestureListener() { + private float lastFocusX = 0; + private float lastFocusY = 0; + + @Override + public boolean onScale(ScaleGestureDetector detector) { + float scaleFactor = detector.getScaleFactor(); + + if (Float.isNaN(scaleFactor) || Float.isInfinite(scaleFactor)) { + return false; + } + + if (scaleFactor >= 0) { + mListener.onScale(scaleFactor, + detector.getFocusX(), + detector.getFocusY(), + detector.getFocusX() - lastFocusX, + detector.getFocusY() - lastFocusY + ); + lastFocusX = detector.getFocusX(); + lastFocusY = detector.getFocusY(); + } + return true; + } + + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + lastFocusX = detector.getFocusX(); + lastFocusY = detector.getFocusY(); + return true; + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + // NO-OP + } + }; + mDetector = new ScaleGestureDetector(context, mScaleListener); + } + + private float getActiveX(MotionEvent ev) { + try { + return ev.getX(mActivePointerIndex); + } catch (Exception e) { + return ev.getX(); + } + } + + private float getActiveY(MotionEvent ev) { + try { + return ev.getY(mActivePointerIndex); + } catch (Exception e) { + return ev.getY(); + } + } + + public boolean isScaling() { + return mDetector.isInProgress(); + } + + public boolean isDragging() { + return mIsDragging; + } + + public boolean onTouchEvent(MotionEvent ev) { + try { + mDetector.onTouchEvent(ev); + return processTouchEvent(ev); + } catch (IllegalArgumentException e) { + // Fix for support lib bug, happening when onDestroy is called + return true; + } + } + + private boolean processTouchEvent(MotionEvent ev) { + final int action = ev.getAction(); + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + mActivePointerId = ev.getPointerId(0); + + mVelocityTracker = VelocityTracker.obtain(); + if (null != mVelocityTracker) { + mVelocityTracker.addMovement(ev); + } + + mLastTouchX = getActiveX(ev); + mLastTouchY = getActiveY(ev); + mIsDragging = false; + break; + case MotionEvent.ACTION_MOVE: + final float x = getActiveX(ev); + final float y = getActiveY(ev); + final float dx = x - mLastTouchX; + final float dy = y - mLastTouchY; + + if (!mIsDragging) { + // Use Pythagoras to see if drag length is larger than + // touch slop + mIsDragging = Math.sqrt((dx * dx) + (dy * dy)) >= mTouchSlop; + } + + if (mIsDragging) { + mListener.onDrag(dx, dy); + mLastTouchX = x; + mLastTouchY = y; + + if (null != mVelocityTracker) { + mVelocityTracker.addMovement(ev); + } + } + break; + case MotionEvent.ACTION_CANCEL: + mActivePointerId = INVALID_POINTER_ID; + // Recycle Velocity Tracker + if (null != mVelocityTracker) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + break; + case MotionEvent.ACTION_UP: + mActivePointerId = INVALID_POINTER_ID; + if (mIsDragging) { + if (null != mVelocityTracker) { + mLastTouchX = getActiveX(ev); + mLastTouchY = getActiveY(ev); + + // Compute velocity within the last 1000ms + mVelocityTracker.addMovement(ev); + mVelocityTracker.computeCurrentVelocity(1000); + + final float vX = mVelocityTracker.getXVelocity(); + final float vY = mVelocityTracker + .getYVelocity(); + + // If the velocity is greater than minVelocity, call + // listener + if (Math.max(Math.abs(vX), Math.abs(vY)) >= mMinimumVelocity) { + mListener.onFling(mLastTouchX, mLastTouchY, -vX, + -vY); + } + } + } + + // Recycle Velocity Tracker + if (null != mVelocityTracker) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + break; + case MotionEvent.ACTION_POINTER_UP: + final int pointerIndex = Util.getPointerIndex(ev.getAction()); + final int pointerId = ev.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mActivePointerId = ev.getPointerId(newPointerIndex); + mLastTouchX = ev.getX(newPointerIndex); + mLastTouchY = ev.getY(newPointerIndex); + } + break; + default: + break; + } + + mActivePointerIndex = ev + .findPointerIndex(mActivePointerId != INVALID_POINTER_ID ? mActivePointerId + : 0); + return true; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnGestureListener.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnGestureListener.java new file mode 100644 index 00000000..f3190ba8 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnGestureListener.java @@ -0,0 +1,29 @@ +/* + Copyright 2011, 2012 Chris Banes. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +package com.tencent.qcloud.tuikit.timcommon.component.photoview; + +interface OnGestureListener { + + void onDrag(float dx, float dy); + + void onFling(float startX, float startY, float velocityX, + float velocityY); + + void onScale(float scaleFactor, float focusX, float focusY); + + void onScale(float scaleFactor, float focusX, float focusY, float dx, float dy); +} \ No newline at end of file diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnMatrixChangedListener.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnMatrixChangedListener.java new file mode 100644 index 00000000..97528d75 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnMatrixChangedListener.java @@ -0,0 +1,18 @@ +package com.tencent.qcloud.tuikit.timcommon.component.photoview; + +import android.graphics.RectF; + +/** + * Interface definition for a callback to be invoked when the internal Matrix has changed for + * this View. + */ +public interface OnMatrixChangedListener { + + /** + * Callback for when the Matrix displaying the Drawable has changed. This could be because + * the View's bounds have changed, or the user has zoomed. + * + * @param rect - Rectangle displaying the Drawable's new bounds. + */ + void onMatrixChanged(RectF rect); +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnOutsidePhotoTapListener.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnOutsidePhotoTapListener.java new file mode 100644 index 00000000..3c67b769 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnOutsidePhotoTapListener.java @@ -0,0 +1,14 @@ +package com.tencent.qcloud.tuikit.timcommon.component.photoview; + +import android.widget.ImageView; + +/** + * Callback when the user tapped outside of the photo + */ +public interface OnOutsidePhotoTapListener { + + /** + * The outside of the photo has been tapped + */ + void onOutsidePhotoTap(ImageView imageView); +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnPhotoTapListener.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnPhotoTapListener.java new file mode 100644 index 00000000..a03a5645 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnPhotoTapListener.java @@ -0,0 +1,22 @@ +package com.tencent.qcloud.tuikit.timcommon.component.photoview; + +import android.widget.ImageView; + +/** + * A callback to be invoked when the Photo is tapped with a single + * tap. + */ +public interface OnPhotoTapListener { + + /** + * A callback to receive where the user taps on a photo. You will only receive a callback if + * the user taps on the actual photo, tapping on 'whitespace' will be ignored. + * + * @param view ImageView the user tapped. + * @param x where the user tapped from the of the Drawable, as percentage of the + * Drawable width. + * @param y where the user tapped from the top of the Drawable, as percentage of the + * Drawable height. + */ + void onPhotoTap(ImageView view, float x, float y); +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnScaleChangedListener.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnScaleChangedListener.java new file mode 100644 index 00000000..3c82f204 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnScaleChangedListener.java @@ -0,0 +1,17 @@ +package com.tencent.qcloud.tuikit.timcommon.component.photoview; + + +/** + * Interface definition for callback to be invoked when attached ImageView scale changes + */ +public interface OnScaleChangedListener { + + /** + * Callback for when the scale changes + * + * @param scaleFactor the scale factor (less than 1 for zoom out, greater than 1 for zoom in) + * @param focusX focal point X position + * @param focusY focal point Y position + */ + void onScaleChange(float scaleFactor, float focusX, float focusY); +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnSingleFlingListener.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnSingleFlingListener.java new file mode 100644 index 00000000..46c93109 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnSingleFlingListener.java @@ -0,0 +1,21 @@ +package com.tencent.qcloud.tuikit.timcommon.component.photoview; + +import android.view.MotionEvent; + +/** + * A callback to be invoked when the ImageView is flung with a single + * touch + */ +public interface OnSingleFlingListener { + + /** + * A callback to receive where the user flings on a ImageView. You will receive a callback if + * the user flings anywhere on the view. + * + * @param e1 MotionEvent the user first touch. + * @param e2 MotionEvent the user last touch. + * @param velocityX distance of user's horizontal fling. + * @param velocityY distance of user's vertical fling. + */ + boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY); +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnViewDragListener.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnViewDragListener.java new file mode 100644 index 00000000..5e5d2c30 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnViewDragListener.java @@ -0,0 +1,16 @@ +package com.tencent.qcloud.tuikit.timcommon.component.photoview; + +/** + * Interface definition for a callback to be invoked when the photo is experiencing a drag event + */ +public interface OnViewDragListener { + + /** + * Callback for when the photo is experiencing a drag event. This cannot be invoked when the + * user is scaling. + * + * @param dx The change of the coordinates in the x-direction + * @param dy The change of the coordinates in the y-direction + */ + void onDrag(float dx, float dy); +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnViewTapListener.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnViewTapListener.java new file mode 100644 index 00000000..00e5eb6b --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/OnViewTapListener.java @@ -0,0 +1,16 @@ +package com.tencent.qcloud.tuikit.timcommon.component.photoview; + +import android.view.View; + +public interface OnViewTapListener { + + /** + * A callback to receive where the user taps on a ImageView. You will receive a callback if + * the user taps anywhere on the view, tapping on 'whitespace' will not be ignored. + * + * @param view - View the user tapped. + * @param x - where the user tapped from the left of the View. + * @param y - where the user tapped from the top of the View. + */ + void onViewTap(View view, float x, float y); +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/PhotoView.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/PhotoView.java new file mode 100644 index 00000000..493d84fe --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/PhotoView.java @@ -0,0 +1,257 @@ +/* + Copyright 2011, 2012 Chris Banes. +

+ Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at +

+ http://www.apache.org/licenses/LICENSE-2.0 +

+ Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +package com.tencent.qcloud.tuikit.timcommon.component.photoview; + +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.util.AttributeSet; +import android.view.GestureDetector; + +import androidx.appcompat.widget.AppCompatImageView; + +/** + * A zoomable ImageView. See {@link PhotoViewAttacher} for most of the details on how the zooming + * is accomplished + */ +@SuppressWarnings("unused") +public class PhotoView extends AppCompatImageView { + + private PhotoViewAttacher attacher; + private ScaleType pendingScaleType; + + public PhotoView(Context context) { + this(context, null); + } + + public PhotoView(Context context, AttributeSet attr) { + this(context, attr, 0); + } + + public PhotoView(Context context, AttributeSet attr, int defStyle) { + super(context, attr, defStyle); + init(); + } + + private void init() { + attacher = new PhotoViewAttacher(this); + //We always pose as a Matrix scale type, though we can change to another scale type + //via the attacher + super.setScaleType(ScaleType.MATRIX); + //apply the previously applied scale type + if (pendingScaleType != null) { + setScaleType(pendingScaleType); + pendingScaleType = null; + } + } + + /** + * Get the current {@link PhotoViewAttacher} for this view. Be wary of holding on to references + * to this attacher, as it has a reference to this view, which, if a reference is held in the + * wrong place, can cause memory leaks. + * + * @return the attacher. + */ + public PhotoViewAttacher getAttacher() { + return attacher; + } + + @Override + public ScaleType getScaleType() { + return attacher.getScaleType(); + } + + @Override + public Matrix getImageMatrix() { + return attacher.getImageMatrix(); + } + + @Override + public void setOnLongClickListener(OnLongClickListener l) { + attacher.setOnLongClickListener(l); + } + + @Override + public void setOnClickListener(OnClickListener l) { + attacher.setOnClickListener(l); + } + + @Override + public void setScaleType(ScaleType scaleType) { + if (attacher == null) { + pendingScaleType = scaleType; + } else { + attacher.setScaleType(scaleType); + } + } + + @Override + public void setImageDrawable(Drawable drawable) { + super.setImageDrawable(drawable); + // setImageBitmap calls through to this method + if (attacher != null) { + attacher.update(); + } + } + + @Override + public void setImageResource(int resId) { + super.setImageResource(resId); + if (attacher != null) { + attacher.update(); + } + } + + @Override + public void setImageURI(Uri uri) { + super.setImageURI(uri); + if (attacher != null) { + attacher.update(); + } + } + + @Override + protected boolean setFrame(int l, int t, int r, int b) { + boolean changed = super.setFrame(l, t, r, b); + if (changed) { + attacher.update(); + } + return changed; + } + + public void setRotationTo(float rotationDegree) { + attacher.setRotationTo(rotationDegree); + } + + public void setRotationBy(float rotationDegree) { + attacher.setRotationBy(rotationDegree); + } + + public boolean isZoomable() { + return attacher.isZoomable(); + } + + public void setZoomable(boolean zoomable) { + attacher.setZoomable(zoomable); + } + + public RectF getDisplayRect() { + return attacher.getDisplayRect(); + } + + public void getDisplayMatrix(Matrix matrix) { + attacher.getDisplayMatrix(matrix); + } + + @SuppressWarnings("UnusedReturnValue") public boolean setDisplayMatrix(Matrix finalRectangle) { + return attacher.setDisplayMatrix(finalRectangle); + } + + public void getSuppMatrix(Matrix matrix) { + attacher.getSuppMatrix(matrix); + } + + public boolean setSuppMatrix(Matrix matrix) { + return attacher.setDisplayMatrix(matrix); + } + + public float getMinimumScale() { + return attacher.getMinimumScale(); + } + + public float getMediumScale() { + return attacher.getMediumScale(); + } + + public float getMaximumScale() { + return attacher.getMaximumScale(); + } + + public float getScale() { + return attacher.getScale(); + } + + public void setAllowParentInterceptOnEdge(boolean allow) { + attacher.setAllowParentInterceptOnEdge(allow); + } + + public void setMinimumScale(float minimumScale) { + attacher.setMinimumScale(minimumScale); + } + + public void setMediumScale(float mediumScale) { + attacher.setMediumScale(mediumScale); + } + + public void setMaximumScale(float maximumScale) { + attacher.setMaximumScale(maximumScale); + } + + public void setScaleLevels(float minimumScale, float mediumScale, float maximumScale) { + attacher.setScaleLevels(minimumScale, mediumScale, maximumScale); + } + + public void setOnMatrixChangeListener(OnMatrixChangedListener listener) { + attacher.setOnMatrixChangeListener(listener); + } + + public void setOnPhotoTapListener(OnPhotoTapListener listener) { + attacher.setOnPhotoTapListener(listener); + } + + public void setOnOutsidePhotoTapListener(OnOutsidePhotoTapListener listener) { + attacher.setOnOutsidePhotoTapListener(listener); + } + + public void setOnViewTapListener(OnViewTapListener listener) { + attacher.setOnViewTapListener(listener); + } + + public void setOnViewDragListener(OnViewDragListener listener) { + attacher.setOnViewDragListener(listener); + } + + public void setScale(float scale) { + attacher.setScale(scale); + } + + public void setScale(float scale, boolean animate) { + attacher.setScale(scale, animate); + } + + public void setScale(float scale, float focalX, float focalY, boolean animate) { + attacher.setScale(scale, focalX, focalY, animate); + } + + public void setZoomTransitionDuration(int milliseconds) { + attacher.setZoomTransitionDuration(milliseconds); + } + + public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener onDoubleTapListener) { + attacher.setOnDoubleTapListener(onDoubleTapListener); + } + + public void setOnScaleChangeListener(OnScaleChangedListener onScaleChangedListener) { + attacher.setOnScaleChangeListener(onScaleChangedListener); + } + + public void setOnSingleFlingListener(OnSingleFlingListener onSingleFlingListener) { + attacher.setOnSingleFlingListener(onSingleFlingListener); + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/PhotoViewAttacher.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/PhotoViewAttacher.java new file mode 100644 index 00000000..1dc0e99b --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/PhotoViewAttacher.java @@ -0,0 +1,807 @@ +/* + Copyright 2011, 2012 Chris Banes. +

+ Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at +

+ http://www.apache.org/licenses/LICENSE-2.0 +

+ Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +package com.tencent.qcloud.tuikit.timcommon.component.photoview; + +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.Matrix.ScaleToFit; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnLongClickListener; +import android.view.ViewParent; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.Interpolator; +import android.widget.ImageView; +import android.widget.ImageView.ScaleType; +import android.widget.OverScroller; + +/** + * The component of {@link PhotoView} which does the work allowing for zooming, scaling, panning, etc. + * It is made public in case you need to subclass something other than AppCompatImageView and still + * gain the functionality that {@link PhotoView} offers + */ +public class PhotoViewAttacher implements View.OnTouchListener, View.OnLayoutChangeListener { + private static float DEFAULT_MAX_SCALE = 3.0f; + private static float DEFAULT_MID_SCALE = 1.75f; + private static float DEFAULT_MIN_SCALE = 1.0f; + private static int DEFAULT_ZOOM_DURATION = 200; + + private static final int HORIZONTAL_EDGE_NONE = -1; + private static final int HORIZONTAL_EDGE_LEFT = 0; + private static final int HORIZONTAL_EDGE_RIGHT = 1; + private static final int HORIZONTAL_EDGE_BOTH = 2; + private static final int VERTICAL_EDGE_NONE = -1; + private static final int VERTICAL_EDGE_TOP = 0; + private static final int VERTICAL_EDGE_BOTTOM = 1; + private static final int VERTICAL_EDGE_BOTH = 2; + private static int SINGLE_TOUCH = 1; + + private Interpolator mInterpolator = new AccelerateDecelerateInterpolator(); + private int mZoomDuration = DEFAULT_ZOOM_DURATION; + private float mMinScale = DEFAULT_MIN_SCALE; + private float mMidScale = DEFAULT_MID_SCALE; + private float mMaxScale = DEFAULT_MAX_SCALE; + + private boolean mAllowParentInterceptOnEdge = true; + private boolean mBlockParentIntercept = false; + + private ImageView mImageView; + + // Gesture Detectors + private GestureDetector mGestureDetector; + private CustomGestureDetector mScaleDragDetector; + + // These are set so we don't keep allocating them on the heap + private final Matrix mBaseMatrix = new Matrix(); + private final Matrix mDrawMatrix = new Matrix(); + private final Matrix mSuppMatrix = new Matrix(); + private final RectF mDisplayRect = new RectF(); + private final float[] mMatrixValues = new float[9]; + + // Listeners + private OnMatrixChangedListener mMatrixChangeListener; + private OnPhotoTapListener mPhotoTapListener; + private OnOutsidePhotoTapListener mOutsidePhotoTapListener; + private OnViewTapListener mViewTapListener; + private View.OnClickListener mOnClickListener; + private OnLongClickListener mLongClickListener; + private OnScaleChangedListener mScaleChangeListener; + private OnSingleFlingListener mSingleFlingListener; + private OnViewDragListener mOnViewDragListener; + + private FlingRunnable mCurrentFlingRunnable; + private int mHorizontalScrollEdge = HORIZONTAL_EDGE_BOTH; + private int mVerticalScrollEdge = VERTICAL_EDGE_BOTH; + private float mBaseRotation; + + private boolean mZoomEnabled = true; + private ScaleType mScaleType = ScaleType.FIT_CENTER; + + private OnGestureListener onGestureListener = new OnGestureListener() { + @Override + public void onDrag(float dx, float dy) { + if (mScaleDragDetector.isScaling()) { + return; // Do not drag if we are already scaling + } + if (mOnViewDragListener != null) { + mOnViewDragListener.onDrag(dx, dy); + } + mSuppMatrix.postTranslate(dx, dy); + checkAndDisplayMatrix(); + + /* + * Here we decide whether to let the ImageView's parent to start taking + * over the touch event. + * + * First we check whether this function is enabled. We never want the + * parent to take over if we're scaling. We then check the edge we're + * on, and the direction of the scroll (i.e. if we're pulling against + * the edge, aka 'overscrolling', let the parent take over). + */ + ViewParent parent = mImageView.getParent(); + if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isScaling() && !mBlockParentIntercept) { + if (mHorizontalScrollEdge == HORIZONTAL_EDGE_BOTH || (mHorizontalScrollEdge == HORIZONTAL_EDGE_LEFT && dx >= 1f) + || (mHorizontalScrollEdge == HORIZONTAL_EDGE_RIGHT && dx <= -1f) || (mVerticalScrollEdge == VERTICAL_EDGE_TOP && dy >= 1f) + || (mVerticalScrollEdge == VERTICAL_EDGE_BOTTOM && dy <= -1f)) { + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(false); + } + } + } else { + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + } + } + + @Override + public void onFling(float startX, float startY, float velocityX, float velocityY) { + mCurrentFlingRunnable = new FlingRunnable(mImageView.getContext()); + mCurrentFlingRunnable.fling(getImageViewWidth(mImageView), getImageViewHeight(mImageView), (int) velocityX, (int) velocityY); + mImageView.post(mCurrentFlingRunnable); + } + + @Override + public void onScale(float scaleFactor, float focusX, float focusY) { + onScale(scaleFactor, focusX, focusY, 0, 0); + } + + @Override + public void onScale(float scaleFactor, float focusX, float focusY, float dx, float dy) { + if (getScale() < mMaxScale || scaleFactor < 1f) { + if (mScaleChangeListener != null) { + mScaleChangeListener.onScaleChange(scaleFactor, focusX, focusY); + } + mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY); + mSuppMatrix.postTranslate(dx, dy); + checkAndDisplayMatrix(); + } + } + }; + + public PhotoViewAttacher(ImageView imageView) { + mImageView = imageView; + imageView.setOnTouchListener(this); + imageView.addOnLayoutChangeListener(this); + if (imageView.isInEditMode()) { + return; + } + mBaseRotation = 0.0f; + // Create Gesture Detectors... + mScaleDragDetector = new CustomGestureDetector(imageView.getContext(), onGestureListener); + mGestureDetector = new GestureDetector(imageView.getContext(), new GestureDetector.SimpleOnGestureListener() { + // forward long click listener + @Override + public void onLongPress(MotionEvent e) { + if (mLongClickListener != null) { + mLongClickListener.onLongClick(mImageView); + } + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + if (mSingleFlingListener != null) { + if (getScale() > DEFAULT_MIN_SCALE) { + return false; + } + if (e1.getPointerCount() > SINGLE_TOUCH || e2.getPointerCount() > SINGLE_TOUCH) { + return false; + } + return mSingleFlingListener.onFling(e1, e2, velocityX, velocityY); + } + return false; + } + }); + mGestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() { + @Override + public boolean onSingleTapConfirmed(MotionEvent e) { + if (mOnClickListener != null) { + mOnClickListener.onClick(mImageView); + } + final RectF displayRect = getDisplayRect(); + final float x = e.getX(); + final float y = e.getY(); + if (mViewTapListener != null) { + mViewTapListener.onViewTap(mImageView, x, y); + } + if (displayRect != null) { + // Check to see if the user tapped on the photo + if (displayRect.contains(x, y)) { + float xResult = (x - displayRect.left) / displayRect.width(); + float yResult = (y - displayRect.top) / displayRect.height(); + if (mPhotoTapListener != null) { + mPhotoTapListener.onPhotoTap(mImageView, xResult, yResult); + } + return true; + } else { + if (mOutsidePhotoTapListener != null) { + mOutsidePhotoTapListener.onOutsidePhotoTap(mImageView); + } + } + } + return false; + } + + @Override + public boolean onDoubleTap(MotionEvent ev) { + try { + float scale = getScale(); + float x = ev.getX(); + float y = ev.getY(); + if (scale < getMediumScale()) { + setScale(getMediumScale(), x, y, true); + } else if (scale >= getMediumScale() && scale < getMaximumScale()) { + setScale(getMaximumScale(), x, y, true); + } else { + setScale(getMinimumScale(), x, y, true); + } + } catch (ArrayIndexOutOfBoundsException e) { + // Can sometimes happen when getX() and getY() is called + } + return true; + } + + @Override + public boolean onDoubleTapEvent(MotionEvent e) { + // Wait for the confirmed onDoubleTap() instead + return false; + } + }); + } + + public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener newOnDoubleTapListener) { + this.mGestureDetector.setOnDoubleTapListener(newOnDoubleTapListener); + } + + public void setOnScaleChangeListener(OnScaleChangedListener onScaleChangeListener) { + this.mScaleChangeListener = onScaleChangeListener; + } + + public void setOnSingleFlingListener(OnSingleFlingListener onSingleFlingListener) { + this.mSingleFlingListener = onSingleFlingListener; + } + + @Deprecated + public boolean isZoomEnabled() { + return mZoomEnabled; + } + + + /** + * Helper method that maps the supplied Matrix to the current Drawable + * + * @param matrix - Matrix to map Drawable against + * @return RectF - Displayed Rectangle + */ + private RectF getDisplayRect(Matrix matrix) { + Drawable d = mImageView.getDrawable(); + if (d != null) { + mDisplayRect.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight()); + matrix.mapRect(mDisplayRect); + return mDisplayRect; + } + return null; + } + + public RectF getDisplayRect() { + checkMatrixBounds(); + return getDisplayRect(getDrawMatrix()); + } + + public boolean setDisplayMatrix(Matrix finalMatrix) { + if (finalMatrix == null) { + throw new IllegalArgumentException("Matrix cannot be null"); + } + if (mImageView.getDrawable() == null) { + return false; + } + mSuppMatrix.set(finalMatrix); + checkAndDisplayMatrix(); + return true; + } + + public void setBaseRotation(final float degrees) { + mBaseRotation = degrees % 360; + update(); + setRotationBy(mBaseRotation); + checkAndDisplayMatrix(); + } + + public void setRotationTo(float degrees) { + mSuppMatrix.setRotate(degrees % 360); + checkAndDisplayMatrix(); + } + + public void setRotationBy(float degrees) { + mSuppMatrix.postRotate(degrees % 360); + checkAndDisplayMatrix(); + } + + public float getMinimumScale() { + return mMinScale; + } + + public float getMediumScale() { + return mMidScale; + } + + public float getMaximumScale() { + return mMaxScale; + } + + public float getScale() { + return (float) Math.sqrt((float) Math.pow(getValue(mSuppMatrix, Matrix.MSCALE_X), 2) + (float) Math.pow(getValue(mSuppMatrix, Matrix.MSKEW_Y), 2)); + } + + public ScaleType getScaleType() { + return mScaleType; + } + + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { + // Update our base matrix, as the bounds have changed + if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) { + updateBaseMatrix(mImageView.getDrawable()); + } + } + + @Override + public boolean onTouch(View v, MotionEvent ev) { + boolean handled = false; + if (mZoomEnabled && Util.hasDrawable((ImageView) v)) { + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: + ViewParent parent = v.getParent(); + // First, disable the Parent from intercepting the touch + // event + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + // If we're flinging, and the user presses down, cancel + // fling + cancelFling(); + break; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + // If the user has zoomed less than min scale, zoom back + // to min scale + if (getScale() < mMinScale) { + RectF rect = getDisplayRect(); + if (rect != null) { + v.post(new AnimatedZoomRunnable(getScale(), mMinScale, rect.centerX(), rect.centerY())); + handled = true; + } + } else if (getScale() > mMaxScale) { + RectF rect = getDisplayRect(); + if (rect != null) { + v.post(new AnimatedZoomRunnable(getScale(), mMaxScale, rect.centerX(), rect.centerY())); + handled = true; + } + } + break; + default: + break; + } + // Try the Scale/Drag detector + if (mScaleDragDetector != null) { + boolean wasScaling = mScaleDragDetector.isScaling(); + boolean wasDragging = mScaleDragDetector.isDragging(); + handled = mScaleDragDetector.onTouchEvent(ev); + boolean didntScale = !wasScaling && !mScaleDragDetector.isScaling(); + boolean didntDrag = !wasDragging && !mScaleDragDetector.isDragging(); + mBlockParentIntercept = didntScale && didntDrag; + } + // Check to see if the user double tapped + if (mGestureDetector != null && mGestureDetector.onTouchEvent(ev)) { + handled = true; + } + } + return handled; + } + + public void setAllowParentInterceptOnEdge(boolean allow) { + mAllowParentInterceptOnEdge = allow; + } + + public void setMinimumScale(float minimumScale) { + Util.checkZoomLevels(minimumScale, mMidScale, mMaxScale); + mMinScale = minimumScale; + } + + public void setMediumScale(float mediumScale) { + Util.checkZoomLevels(mMinScale, mediumScale, mMaxScale); + mMidScale = mediumScale; + } + + public void setMaximumScale(float maximumScale) { + Util.checkZoomLevels(mMinScale, mMidScale, maximumScale); + mMaxScale = maximumScale; + } + + public void setScaleLevels(float minimumScale, float mediumScale, float maximumScale) { + Util.checkZoomLevels(minimumScale, mediumScale, maximumScale); + mMinScale = minimumScale; + mMidScale = mediumScale; + mMaxScale = maximumScale; + } + + public void setOnLongClickListener(OnLongClickListener listener) { + mLongClickListener = listener; + } + + public void setOnClickListener(View.OnClickListener listener) { + mOnClickListener = listener; + } + + public void setOnMatrixChangeListener(OnMatrixChangedListener listener) { + mMatrixChangeListener = listener; + } + + public void setOnPhotoTapListener(OnPhotoTapListener listener) { + mPhotoTapListener = listener; + } + + public void setOnOutsidePhotoTapListener(OnOutsidePhotoTapListener mOutsidePhotoTapListener) { + this.mOutsidePhotoTapListener = mOutsidePhotoTapListener; + } + + public void setOnViewTapListener(OnViewTapListener listener) { + mViewTapListener = listener; + } + + public void setOnViewDragListener(OnViewDragListener listener) { + mOnViewDragListener = listener; + } + + public void setScale(float scale) { + setScale(scale, false); + } + + public void setScale(float scale, boolean animate) { + setScale(scale, (mImageView.getRight()) / 2, (mImageView.getBottom()) / 2, animate); + } + + public void setScale(float scale, float focalX, float focalY, boolean animate) { + // Check to see if the scale is within bounds + if (scale < mMinScale || scale > mMaxScale) { + throw new IllegalArgumentException("Scale must be within the range of minScale and maxScale"); + } + if (animate) { + mImageView.post(new AnimatedZoomRunnable(getScale(), scale, focalX, focalY)); + } else { + mSuppMatrix.setScale(scale, scale, focalX, focalY); + checkAndDisplayMatrix(); + } + } + + /** + * Set the zoom interpolator + * + * @param interpolator the zoom interpolator + */ + public void setZoomInterpolator(Interpolator interpolator) { + mInterpolator = interpolator; + } + + public void setScaleType(ScaleType scaleType) { + if (Util.isSupportedScaleType(scaleType) && scaleType != mScaleType) { + mScaleType = scaleType; + update(); + } + } + + public boolean isZoomable() { + return mZoomEnabled; + } + + public void setZoomable(boolean zoomable) { + mZoomEnabled = zoomable; + update(); + } + + public void update() { + if (mZoomEnabled) { + // Update the base matrix using the current drawable + updateBaseMatrix(mImageView.getDrawable()); + } else { + // Reset the Matrix... + resetMatrix(); + } + } + + /** + * Get the display matrix + * + * @param matrix target matrix to copy to + */ + public void getDisplayMatrix(Matrix matrix) { + matrix.set(getDrawMatrix()); + } + + /** + * Get the current support matrix + */ + public void getSuppMatrix(Matrix matrix) { + matrix.set(mSuppMatrix); + } + + private Matrix getDrawMatrix() { + mDrawMatrix.set(mBaseMatrix); + mDrawMatrix.postConcat(mSuppMatrix); + return mDrawMatrix; + } + + public Matrix getImageMatrix() { + return mDrawMatrix; + } + + public void setZoomTransitionDuration(int milliseconds) { + this.mZoomDuration = milliseconds; + } + + /** + * Helper method that 'unpacks' a Matrix and returns the required value + * + * @param matrix Matrix to unpack + * @param whichValue Which value from Matrix.M* to return + * @return returned value + */ + private float getValue(Matrix matrix, int whichValue) { + matrix.getValues(mMatrixValues); + return mMatrixValues[whichValue]; + } + + /** + * Resets the Matrix back to FIT_CENTER, and then displays its contents + */ + private void resetMatrix() { + mSuppMatrix.reset(); + setRotationBy(mBaseRotation); + setImageViewMatrix(getDrawMatrix()); + checkMatrixBounds(); + } + + private void setImageViewMatrix(Matrix matrix) { + mImageView.setImageMatrix(matrix); + // Call MatrixChangedListener if needed + if (mMatrixChangeListener != null) { + RectF displayRect = getDisplayRect(matrix); + if (displayRect != null) { + mMatrixChangeListener.onMatrixChanged(displayRect); + } + } + } + + /** + * Helper method that simply checks the Matrix, and then displays the result + */ + private void checkAndDisplayMatrix() { + if (checkMatrixBounds()) { + setImageViewMatrix(getDrawMatrix()); + } + } + + /** + * Calculate Matrix for FIT_CENTER + * + * @param drawable - Drawable being displayed + */ + private void updateBaseMatrix(Drawable drawable) { + if (drawable == null) { + return; + } + final float viewWidth = getImageViewWidth(mImageView); + final float viewHeight = getImageViewHeight(mImageView); + final int drawableWidth = drawable.getIntrinsicWidth(); + final int drawableHeight = drawable.getIntrinsicHeight(); + mBaseMatrix.reset(); + final float widthScale = viewWidth / drawableWidth; + final float heightScale = viewHeight / drawableHeight; + if (mScaleType == ScaleType.CENTER) { + mBaseMatrix.postTranslate((viewWidth - drawableWidth) / 2F, (viewHeight - drawableHeight) / 2F); + + } else if (mScaleType == ScaleType.CENTER_CROP) { + float scale = Math.max(widthScale, heightScale); + mBaseMatrix.postScale(scale, scale); + mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F, (viewHeight - drawableHeight * scale) / 2F); + + } else if (mScaleType == ScaleType.CENTER_INSIDE) { + float scale = Math.min(1.0f, Math.min(widthScale, heightScale)); + mBaseMatrix.postScale(scale, scale); + mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F, (viewHeight - drawableHeight * scale) / 2F); + + } else { + RectF mTempSrc = new RectF(0, 0, drawableWidth, drawableHeight); + RectF mTempDst = new RectF(0, 0, viewWidth, viewHeight); + if ((int) mBaseRotation % 180 != 0) { + mTempSrc = new RectF(0, 0, drawableHeight, drawableWidth); + } + switch (mScaleType) { + case FIT_CENTER: + mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.CENTER); + break; + case FIT_START: + mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.START); + break; + case FIT_END: + mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.END); + break; + case FIT_XY: + mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.FILL); + break; + default: + break; + } + } + resetMatrix(); + } + + private boolean checkMatrixBounds() { + final RectF rect = getDisplayRect(getDrawMatrix()); + if (rect == null) { + return false; + } + final float height = rect.height(); + final float width = rect.width(); + float deltaX = 0; + float deltaY = 0; + final int viewHeight = getImageViewHeight(mImageView); + if (height <= viewHeight) { + switch (mScaleType) { + case FIT_START: + deltaY = -rect.top; + break; + case FIT_END: + deltaY = viewHeight - height - rect.top; + break; + default: + deltaY = (viewHeight - height) / 2 - rect.top; + break; + } + mVerticalScrollEdge = VERTICAL_EDGE_BOTH; + } else if (rect.top > 0) { + mVerticalScrollEdge = VERTICAL_EDGE_TOP; + deltaY = -rect.top; + } else if (rect.bottom < viewHeight) { + mVerticalScrollEdge = VERTICAL_EDGE_BOTTOM; + deltaY = viewHeight - rect.bottom; + } else { + mVerticalScrollEdge = VERTICAL_EDGE_NONE; + } + final int viewWidth = getImageViewWidth(mImageView); + if (width <= viewWidth) { + switch (mScaleType) { + case FIT_START: + deltaX = -rect.left; + break; + case FIT_END: + deltaX = viewWidth - width - rect.left; + break; + default: + deltaX = (viewWidth - width) / 2 - rect.left; + break; + } + mHorizontalScrollEdge = HORIZONTAL_EDGE_BOTH; + } else if (rect.left > 0) { + mHorizontalScrollEdge = HORIZONTAL_EDGE_LEFT; + deltaX = -rect.left; + } else if (rect.right < viewWidth) { + deltaX = viewWidth - rect.right; + mHorizontalScrollEdge = HORIZONTAL_EDGE_RIGHT; + } else { + mHorizontalScrollEdge = HORIZONTAL_EDGE_NONE; + } + // Finally actually translate the matrix + mSuppMatrix.postTranslate(deltaX, deltaY); + return true; + } + + private int getImageViewWidth(ImageView imageView) { + return imageView.getWidth() - imageView.getPaddingLeft() - imageView.getPaddingRight(); + } + + private int getImageViewHeight(ImageView imageView) { + return imageView.getHeight() - imageView.getPaddingTop() - imageView.getPaddingBottom(); + } + + private void cancelFling() { + if (mCurrentFlingRunnable != null) { + mCurrentFlingRunnable.cancelFling(); + mCurrentFlingRunnable = null; + } + } + + private class AnimatedZoomRunnable implements Runnable { + private final float mFocalX; + private final float mFocalY; + private final long mStartTime; + private final float mZoomStart; + private final float mZoomEnd; + + public AnimatedZoomRunnable(final float currentZoom, final float targetZoom, final float focalX, final float focalY) { + mFocalX = focalX; + mFocalY = focalY; + mStartTime = System.currentTimeMillis(); + mZoomStart = currentZoom; + mZoomEnd = targetZoom; + } + + @Override + public void run() { + float t = interpolate(); + float scale = mZoomStart + t * (mZoomEnd - mZoomStart); + float deltaScale = scale / getScale(); + onGestureListener.onScale(deltaScale, mFocalX, mFocalY); + // We haven't hit our target scale yet, so post ourselves again + if (t < 1f) { + Compat.postOnAnimation(mImageView, this); + } + } + + private float interpolate() { + float t = 1f * (System.currentTimeMillis() - mStartTime) / mZoomDuration; + t = Math.min(1f, t); + t = mInterpolator.getInterpolation(t); + return t; + } + } + + private class FlingRunnable implements Runnable { + private final OverScroller mScroller; + private int mCurrentX; + private int mCurrentY; + + public FlingRunnable(Context context) { + mScroller = new OverScroller(context); + } + + public void cancelFling() { + mScroller.forceFinished(true); + } + + public void fling(int viewWidth, int viewHeight, int velocityX, int velocityY) { + final RectF rect = getDisplayRect(); + if (rect == null) { + return; + } + final int startX = Math.round(-rect.left); + final int minX; + final int maxX; + final int minY; + final int maxY; + if (viewWidth < rect.width()) { + minX = 0; + maxX = Math.round(rect.width() - viewWidth); + } else { + minX = maxX = startX; + } + final int startY = Math.round(-rect.top); + if (viewHeight < rect.height()) { + minY = 0; + maxY = Math.round(rect.height() - viewHeight); + } else { + minY = maxY = startY; + } + mCurrentX = startX; + mCurrentY = startY; + // If we actually can move, fling the scroller + if (startX != maxX || startY != maxY) { + mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, 0, 0); + } + } + + @Override + public void run() { + if (mScroller.isFinished()) { + return; // remaining post that should not be handled + } + if (mScroller.computeScrollOffset()) { + final int newX = mScroller.getCurrX(); + final int newY = mScroller.getCurrY(); + mSuppMatrix.postTranslate(mCurrentX - newX, mCurrentY - newY); + checkAndDisplayMatrix(); + mCurrentX = newX; + mCurrentY = newY; + // Post On animation + Compat.postOnAnimation(mImageView, this); + } + } + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/Util.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/Util.java new file mode 100644 index 00000000..3b5ce765 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/photoview/Util.java @@ -0,0 +1,39 @@ +package com.tencent.qcloud.tuikit.timcommon.component.photoview; + +import android.view.MotionEvent; +import android.widget.ImageView; + +class Util { + + static void checkZoomLevels(float minZoom, float midZoom, + float maxZoom) { + if (minZoom >= midZoom) { + throw new IllegalArgumentException( + "Minimum zoom has to be less than Medium zoom. Call setMinimumZoom() with a more appropriate value"); + } else if (midZoom >= maxZoom) { + throw new IllegalArgumentException( + "Medium zoom has to be less than Maximum zoom. Call setMaximumZoom() with a more appropriate value"); + } + } + + static boolean hasDrawable(ImageView imageView) { + return imageView.getDrawable() != null; + } + + static boolean isSupportedScaleType(final ImageView.ScaleType scaleType) { + if (scaleType == null) { + return false; + } + switch (scaleType) { + case MATRIX: + throw new IllegalStateException("Matrix scale type is not supported"); + default: + break; + } + return true; + } + + static int getPointerIndex(int action) { + return (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/scroller/CenteredSmoothScroller.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/scroller/CenteredSmoothScroller.java new file mode 100644 index 00000000..2003da35 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/scroller/CenteredSmoothScroller.java @@ -0,0 +1,40 @@ +package com.tencent.qcloud.tuikit.timcommon.component.scroller; + +import android.content.Context; +import android.view.View; + +import androidx.recyclerview.widget.LinearSmoothScroller; +import androidx.recyclerview.widget.OrientationHelper; +import androidx.recyclerview.widget.RecyclerView; + +public class CenteredSmoothScroller extends LinearSmoothScroller { + + public CenteredSmoothScroller(Context context) { + super(context); + } + + @Override + protected void onTargetFound(View targetView, RecyclerView.State state, Action action) { + RecyclerView.LayoutManager layoutManager = getLayoutManager(); + if (layoutManager == null) { + return; + } + int distance = calculateDistanceToCenter(targetView, layoutManager); + int time = calculateTimeForDeceleration(distance); + if (time > 0) { + action.update(0, distance, time, mDecelerateInterpolator); + } + } + + private int calculateDistanceToCenter(View targetView, RecyclerView.LayoutManager layoutManager) { + OrientationHelper helper = OrientationHelper.createVerticalHelper(layoutManager); + int childCenter = helper.getDecoratedStart(targetView) + helper.getDecoratedMeasurement(targetView) / 2; + int containerCenter; + if (layoutManager.getClipToPadding()) { + containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2; + } else { + containerCenter = helper.getEnd() / 2; + } + return childCenter - containerCenter; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/Attributes.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/Attributes.java new file mode 100644 index 00000000..a3ffdaa4 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/Attributes.java @@ -0,0 +1,5 @@ +package com.tencent.qcloud.tuikit.timcommon.component.swipe; + +public class Attributes { + public enum Mode { Single, Multiple } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/RecyclerSwipeAdapter.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/RecyclerSwipeAdapter.java new file mode 100644 index 00000000..95b2c0d9 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/RecyclerSwipeAdapter.java @@ -0,0 +1,85 @@ +package com.tencent.qcloud.tuikit.timcommon.component.swipe; + +import android.view.ViewGroup; + +import androidx.recyclerview.widget.RecyclerView; + +import java.util.List; + +public abstract class RecyclerSwipeAdapter + extends RecyclerView.Adapter implements SwipeItemMangerInterface, SwipeAdapterInterface { + public SwipeItemMangerImpl mItemManger = new SwipeItemMangerImpl(this); + + @Override public abstract VH onCreateViewHolder(ViewGroup parent, int viewType); + + @Override public abstract void onBindViewHolder(VH viewHolder, final int position); + + @Override + public void notifyDatasetChanged() { + super.notifyDataSetChanged(); + } + + @Override + public void notifySwipeItemChanged(int position) { + super.notifyItemChanged(position); + } + + @Override + public void openItem(int position) { + mItemManger.openItem(position); + } + + @Override + public void closeItem(int position) { + mItemManger.closeItem(position); + } + + @Override + public void closeAllExcept(SwipeLayout layout) { + mItemManger.closeAllExcept(layout); + } + + @Override + public void closeAllSwipeItems() { + mItemManger.closeAllSwipeItems(); + } + + @Override + public List getOpenItems() { + return mItemManger.getOpenItems(); + } + + @Override + public List getOpenLayouts() { + return mItemManger.getOpenLayouts(); + } + + @Override + public void removeShownLayouts(SwipeLayout layout) { + mItemManger.removeShownLayouts(layout); + } + + @Override + public boolean isOpen(int position) { + return mItemManger.isOpen(position); + } + + @Override + public Attributes.Mode getMode() { + return mItemManger.getMode(); + } + + @Override + public void setMode(Attributes.Mode mode) { + mItemManger.setMode(mode); + } + + @Override + public void switchAllSwipeEnable(boolean enable) { + mItemManger.switchAllSwipeEnable(enable); + } + + public void setSwipeEnabled(boolean enabled) { + mItemManger.setSwipeEnabled(enabled); + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SimpleSwipeListener.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SimpleSwipeListener.java new file mode 100644 index 00000000..286c6075 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SimpleSwipeListener.java @@ -0,0 +1,21 @@ +package com.tencent.qcloud.tuikit.timcommon.component.swipe; + +public class SimpleSwipeListener implements SwipeLayout.SwipeListener { + @Override + public void onStartOpen(SwipeLayout layout) {} + + @Override + public void onOpen(SwipeLayout layout) {} + + @Override + public void onStartClose(SwipeLayout layout) {} + + @Override + public void onClose(SwipeLayout layout) {} + + @Override + public void onUpdate(SwipeLayout layout, int leftOffset, int topOffset) {} + + @Override + public void onHandRelease(SwipeLayout layout, float xvel, float yvel) {} +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SwipeAdapterInterface.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SwipeAdapterInterface.java new file mode 100644 index 00000000..35bd2966 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SwipeAdapterInterface.java @@ -0,0 +1,9 @@ +package com.tencent.qcloud.tuikit.timcommon.component.swipe; + +public interface SwipeAdapterInterface { + int getSwipeLayoutResourceId(int position); + + void notifyDatasetChanged(); + + void notifySwipeItemChanged(int position); +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SwipeItemMangerImpl.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SwipeItemMangerImpl.java new file mode 100644 index 00000000..2fd1ff5c --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SwipeItemMangerImpl.java @@ -0,0 +1,219 @@ +package com.tencent.qcloud.tuikit.timcommon.component.swipe; + +import android.view.View; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class SwipeItemMangerImpl implements SwipeItemMangerInterface { + private Attributes.Mode mode = Attributes.Mode.Single; + public static final int INVALID_POSITION = -1; + + protected int mOpenPosition = INVALID_POSITION; + + protected Set mOpenPositions = new HashSet(); + protected Set mShownLayouts = new HashSet(); + + protected boolean isSwipeEnabled = true; + protected SwipeAdapterInterface swipeAdapterInterface; + + public SwipeItemMangerImpl(SwipeAdapterInterface swipeAdapterInterface) { + if (swipeAdapterInterface == null) { + throw new IllegalArgumentException("SwipeAdapterInterface can not be null"); + } + + this.swipeAdapterInterface = swipeAdapterInterface; + } + + public Attributes.Mode getMode() { + return mode; + } + + public void setMode(Attributes.Mode mode) { + this.mode = mode; + mOpenPositions.clear(); + mShownLayouts.clear(); + mOpenPosition = INVALID_POSITION; + } + + public void bind(View view, int position) { + int resId = swipeAdapterInterface.getSwipeLayoutResourceId(position); + SwipeLayout swipeLayout = (SwipeLayout) view.findViewById(resId); + if (swipeLayout == null) { + throw new IllegalStateException("can not find SwipeLayout in target view"); + } + + swipeLayout.setSwipeEnabled(isSwipeEnabled); + if (swipeLayout.getTag(resId) == null) { + OnLayoutListener onLayoutListener = new OnLayoutListener(position); + SwipeMemory swipeMemory = new SwipeMemory(position); + swipeLayout.addSwipeListener(swipeMemory); + swipeLayout.addOnLayoutListener(onLayoutListener); + swipeLayout.setTag(resId, new ValueBox(position, swipeMemory, onLayoutListener)); + mShownLayouts.add(swipeLayout); + } else { + ValueBox valueBox = (ValueBox) swipeLayout.getTag(resId); + valueBox.swipeMemory.setPosition(position); + valueBox.onLayoutListener.setPosition(position); + valueBox.position = position; + } + } + + @Override + public void openItem(int position) { + if (mode == Attributes.Mode.Multiple) { + if (!mOpenPositions.contains(position)) { + mOpenPositions.add(position); + } + } else { + mOpenPosition = position; + } + swipeAdapterInterface.notifySwipeItemChanged(position); + } + + @Override + public void closeItem(int position) { + if (mode == Attributes.Mode.Multiple) { + mOpenPositions.remove(position); + } else { + if (mOpenPosition == position) { + mOpenPosition = INVALID_POSITION; + } + } + swipeAdapterInterface.notifySwipeItemChanged(position); + } + + @Override + public void closeAllExcept(SwipeLayout layout) { + for (SwipeLayout s : mShownLayouts) { + if (s != layout) { + s.close(); + } + } + } + + @Override + public void closeAllSwipeItems() { + if (mode == Attributes.Mode.Multiple) { + mOpenPositions.clear(); + } else { + mOpenPosition = INVALID_POSITION; + } + for (SwipeLayout s : mShownLayouts) { + s.close(); + } + } + + @Override + public void switchAllSwipeEnable(boolean enable) { + for (SwipeLayout s : mShownLayouts) { + s.setSwipeEnabled(enable); + } + } + + @Override + public void removeShownLayouts(SwipeLayout layout) { + mShownLayouts.remove(layout); + } + + @Override + public List getOpenItems() { + if (mode == Attributes.Mode.Multiple) { + return new ArrayList(mOpenPositions); + } else { + return Collections.singletonList(mOpenPosition); + } + } + + @Override + public List getOpenLayouts() { + return new ArrayList(mShownLayouts); + } + + @Override + public boolean isOpen(int position) { + if (mode == Attributes.Mode.Multiple) { + return mOpenPositions.contains(position); + } else { + return mOpenPosition == position; + } + } + + public void setSwipeEnabled(boolean swipeEnabled) { + isSwipeEnabled = swipeEnabled; + } + + class ValueBox { + OnLayoutListener onLayoutListener; + SwipeMemory swipeMemory; + int position; + + ValueBox(int position, SwipeMemory swipeMemory, OnLayoutListener onLayoutListener) { + this.swipeMemory = swipeMemory; + this.onLayoutListener = onLayoutListener; + this.position = position; + } + } + + class OnLayoutListener implements SwipeLayout.OnLayout { + private int position; + + OnLayoutListener(int position) { + this.position = position; + } + + public void setPosition(int position) { + this.position = position; + } + + @Override + public void onLayout(SwipeLayout v) { + if (isOpen(position)) { + v.open(false, false); + } else { + v.close(false, false); + } + } + } + + class SwipeMemory extends SimpleSwipeListener { + private int position; + + SwipeMemory(int position) { + this.position = position; + } + + @Override + public void onClose(SwipeLayout layout) { + if (mode == Attributes.Mode.Multiple) { + mOpenPositions.remove(position); + } else { + mOpenPosition = INVALID_POSITION; + } + } + + @Override + public void onStartOpen(SwipeLayout layout) { + if (mode == Attributes.Mode.Single) { + closeAllExcept(layout); + } + } + + @Override + public void onOpen(SwipeLayout layout) { + if (mode == Attributes.Mode.Multiple) { + mOpenPositions.add(position); + } else { + closeAllExcept(layout); + mOpenPosition = position; + } + } + + public void setPosition(int position) { + this.position = position; + } + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SwipeItemMangerInterface.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SwipeItemMangerInterface.java new file mode 100644 index 00000000..137441ed --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SwipeItemMangerInterface.java @@ -0,0 +1,27 @@ +package com.tencent.qcloud.tuikit.timcommon.component.swipe; + +import java.util.List; + +public interface SwipeItemMangerInterface { + void openItem(int position); + + void closeItem(int position); + + void closeAllExcept(SwipeLayout layout); + + void closeAllSwipeItems(); + + List getOpenItems(); + + List getOpenLayouts(); + + void removeShownLayouts(SwipeLayout layout); + + boolean isOpen(int position); + + Attributes.Mode getMode(); + + void setMode(Attributes.Mode mode); + + void switchAllSwipeEnable(boolean enable); +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SwipeLayout.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SwipeLayout.java new file mode 100644 index 00000000..72a757f2 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/swipe/SwipeLayout.java @@ -0,0 +1,1806 @@ +package com.tencent.qcloud.tuikit.timcommon.component.swipe; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.GestureDetector; +import android.view.Gravity; +import android.view.HapticFeedbackConstants; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.AbsListView; +import android.widget.AdapterView; +import android.widget.FrameLayout; + +import androidx.core.view.GravityCompat; +import androidx.core.view.ViewCompat; +import androidx.customview.widget.ViewDragHelper; + +import com.tencent.qcloud.tuikit.timcommon.R; +import com.tencent.qcloud.tuikit.timcommon.util.LayoutUtil; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class SwipeLayout extends FrameLayout { + @Deprecated public static final int EMPTY_LAYOUT = -1; + private static final int DRAG_LEFT = 1; + private static final int DRAG_RIGHT = 2; + private static final int DRAG_TOP = 4; + private static final int DRAG_BOTTOM = 8; + private static final DragEdge DefaultDragEdge = DragEdge.Right; + + private int mTouchSlop; + + private DragEdge mCurrentDragEdge = DefaultDragEdge; + private ViewDragHelper mDragHelper; + + private int mDragDistance = 0; + private LinkedHashMap mDragEdges = new LinkedHashMap<>(); + private ShowMode mShowMode; + + private float[] mEdgeSwipesOffset = new float[4]; + + private List mSwipeListeners = new ArrayList<>(); + private List mSwipeDeniers = new ArrayList<>(); + private Map> mRevealListeners = new HashMap<>(); + private Map mShowEntirely = new HashMap<>(); + private Map mViewBoundCache = new HashMap<>(); // save all children's bound, restore in onLayout + + private DoubleClickListener mDoubleClickListener; + + private boolean mSwipeEnabled = true; + private boolean[] mSwipesEnabled = new boolean[] {true, true, true, true}; + private boolean mClickToClose = false; + private float mWillOpenPercentAfterOpen = 0.75f; + private float mWillOpenPercentAfterClose = 0.25f; + + public enum DragEdge { Left, Top, Right, Bottom } + + public enum ShowMode { LayDown, PullOut } + + public SwipeLayout(Context context) { + this(context, null); + } + + public SwipeLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SwipeLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + mDragHelper = ViewDragHelper.create(this, mDragHelperCallback); + mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SwipeLayout); + mEdgeSwipesOffset[DragEdge.Left.ordinal()] = a.getDimension(R.styleable.SwipeLayout_leftEdgeSwipeOffset, 0); + mEdgeSwipesOffset[DragEdge.Right.ordinal()] = a.getDimension(R.styleable.SwipeLayout_rightEdgeSwipeOffset, 0); + mEdgeSwipesOffset[DragEdge.Top.ordinal()] = a.getDimension(R.styleable.SwipeLayout_topEdgeSwipeOffset, 0); + mEdgeSwipesOffset[DragEdge.Bottom.ordinal()] = a.getDimension(R.styleable.SwipeLayout_bottomEdgeSwipeOffset, 0); + setClickToClose(a.getBoolean(R.styleable.SwipeLayout_clickToClose, mClickToClose)); + int defaultDragEdge = DRAG_RIGHT; + if (LayoutUtil.isRTL()) { + defaultDragEdge = DRAG_LEFT; + } + int dragEdgeChoices = a.getInt(R.styleable.SwipeLayout_drag_edge, defaultDragEdge); + if ((dragEdgeChoices & DRAG_LEFT) == DRAG_LEFT) { + mDragEdges.put(DragEdge.Left, null); + } + if ((dragEdgeChoices & DRAG_TOP) == DRAG_TOP) { + mDragEdges.put(DragEdge.Top, null); + } + if ((dragEdgeChoices & DRAG_RIGHT) == DRAG_RIGHT) { + mDragEdges.put(DragEdge.Right, null); + } + if ((dragEdgeChoices & DRAG_BOTTOM) == DRAG_BOTTOM) { + mDragEdges.put(DragEdge.Bottom, null); + } + int ordinal = a.getInt(R.styleable.SwipeLayout_show_mode, ShowMode.PullOut.ordinal()); + mShowMode = ShowMode.values()[ordinal]; + a.recycle(); + } + + public interface SwipeListener { + void onStartOpen(SwipeLayout layout); + + void onOpen(SwipeLayout layout); + + void onStartClose(SwipeLayout layout); + + void onClose(SwipeLayout layout); + + void onUpdate(SwipeLayout layout, int leftOffset, int topOffset); + + void onHandRelease(SwipeLayout layout, float xvel, float yvel); + } + + public void addSwipeListener(SwipeListener l) { + mSwipeListeners.add(l); + } + + public void removeSwipeListener(SwipeListener l) { + mSwipeListeners.remove(l); + } + + public void removeAllSwipeListener() { + mSwipeListeners.clear(); + } + + public interface SwipeDenier { + boolean shouldDenySwipe(MotionEvent ev); + } + + public void addSwipeDenier(SwipeDenier denier) { + mSwipeDeniers.add(denier); + } + + public void removeSwipeDenier(SwipeDenier denier) { + mSwipeDeniers.remove(denier); + } + + public void removeAllSwipeDeniers() { + mSwipeDeniers.clear(); + } + + public interface OnRevealListener { + void onReveal(View child, DragEdge edge, float fraction, int distance); + } + + /** + * bind a view with a specific + * {@link com.tencent.qcloud.tuikit.timcommon.component.swipe.SwipeLayout.OnRevealListener} + * + * @param childId the view id. + * @param l the target + * {@link com.tencent.qcloud.tuikit.timcommon.component.swipe.SwipeLayout.OnRevealListener} + */ + public void addRevealListener(int childId, OnRevealListener l) { + View child = findViewById(childId); + if (child == null) { + throw new IllegalArgumentException("Child does not belong to SwipeListener."); + } + + if (!mShowEntirely.containsKey(child)) { + mShowEntirely.put(child, false); + } + if (mRevealListeners.get(child) == null) { + mRevealListeners.put(child, new ArrayList()); + } + + mRevealListeners.get(child).add(l); + } + + /** + * bind multiple views with an + * {@link com.tencent.qcloud.tuikit.timcommon.component.swipe.SwipeLayout.OnRevealListener}. + * + * @param childIds the view id. + * @param l the {@link com.tencent.qcloud.tuikit.timcommon.component.swipe.SwipeLayout.OnRevealListener} + */ + public void addRevealListener(int[] childIds, OnRevealListener l) { + for (int i : childIds) { + addRevealListener(i, l); + } + } + + public void removeRevealListener(int childId, OnRevealListener l) { + View child = findViewById(childId); + + if (child == null) { + return; + } + + mShowEntirely.remove(child); + if (mRevealListeners.containsKey(child)) { + mRevealListeners.get(child).remove(l); + } + } + + public void removeAllRevealListeners(int childId) { + View child = findViewById(childId); + if (child != null) { + mRevealListeners.remove(child); + mShowEntirely.remove(child); + } + } + + private ViewDragHelper.Callback mDragHelperCallback = new ViewDragHelper.Callback() { + @Override + public int clampViewPositionHorizontal(View child, int left, int dx) { + return handleClampHorizontal(child, left); + } + + @Override + public int clampViewPositionVertical(View child, int top, int dy) { + return handleClampVertical(child, top, dy); + } + + @Override + public boolean tryCaptureView(View child, int pointerId) { + boolean result = child == getSurfaceView() || getBottomViews().contains(child); + if (result) { + isCloseBeforeDrag = getOpenStatus() == Status.Close; + } + return result; + } + + @Override + public int getViewHorizontalDragRange(View child) { + return mDragDistance; + } + + @Override + public int getViewVerticalDragRange(View child) { + return mDragDistance; + } + + boolean isCloseBeforeDrag = true; + + @Override + public void onViewReleased(View releasedChild, float xvel, float yvel) { + super.onViewReleased(releasedChild, xvel, yvel); + processHandRelease(xvel, yvel, isCloseBeforeDrag); + for (SwipeListener l : mSwipeListeners) { + l.onHandRelease(SwipeLayout.this, xvel, yvel); + } + + invalidate(); + } + + @Override + public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { + View surfaceView = getSurfaceView(); + if (surfaceView == null) { + return; + } + View currentBottomView = getCurrentBottomView(); + int evLeft = surfaceView.getLeft(); + int evRight = surfaceView.getRight(); + int evTop = surfaceView.getTop(); + int evBottom = surfaceView.getBottom(); + if (changedView == surfaceView) { + if (mShowMode == ShowMode.PullOut && currentBottomView != null) { + if (mCurrentDragEdge == DragEdge.Left || mCurrentDragEdge == DragEdge.Right) { + currentBottomView.offsetLeftAndRight(dx); + } else { + currentBottomView.offsetTopAndBottom(dy); + } + } + + } else if (getBottomViews().contains(changedView)) { + if (mShowMode == ShowMode.PullOut) { + surfaceView.offsetLeftAndRight(dx); + surfaceView.offsetTopAndBottom(dy); + } else { + Rect rect = computeBottomLayDown(mCurrentDragEdge); + if (currentBottomView != null) { + currentBottomView.layout(rect.left, rect.top, rect.right, rect.bottom); + } + + int newLeft = surfaceView.getLeft() + dx; + int newTop = surfaceView.getTop() + dy; + + if (mCurrentDragEdge == DragEdge.Left && newLeft < getPaddingLeft()) { + newLeft = getPaddingLeft(); + } else if (mCurrentDragEdge == DragEdge.Right && newLeft > getPaddingLeft()) { + newLeft = getPaddingLeft(); + } else if (mCurrentDragEdge == DragEdge.Top && newTop < getPaddingTop()) { + newTop = getPaddingTop(); + } else if (mCurrentDragEdge == DragEdge.Bottom && newTop > getPaddingTop()) { + newTop = getPaddingTop(); + } + + surfaceView.layout(newLeft, newTop, newLeft + getMeasuredWidth(), newTop + getMeasuredHeight()); + } + } + + dispatchRevealEvent(evLeft, evTop, evRight, evBottom); + + dispatchSwipeEvent(evLeft, evTop, dx, dy); + + invalidate(); + + captureChildrenBound(); + } + }; + + private int handleClampVertical(View child, int top, int dy) { + if (child == getSurfaceView()) { + switch (mCurrentDragEdge) { + case Left: + return getPaddingTop(); + case Right: + return getPaddingTop(); + case Top: + if (top < getPaddingTop()) { + return getPaddingTop(); + } + if (top > getPaddingTop() + mDragDistance) { + return getPaddingTop() + mDragDistance; + } + break; + case Bottom: + if (top < getPaddingTop() - mDragDistance) { + return getPaddingTop() - mDragDistance; + } + if (top > getPaddingTop()) { + return getPaddingTop(); + } + break; + default: + break; + } + } else { + View surfaceView = getSurfaceView(); + int surfaceViewTop = surfaceView == null ? 0 : surfaceView.getTop(); + switch (mCurrentDragEdge) { + case Left: + return getPaddingTop(); + case Right: + return getPaddingTop(); + case Top: + if (mShowMode == ShowMode.PullOut) { + if (top > getPaddingTop()) { + return getPaddingTop(); + } + } else { + if (surfaceViewTop + dy < getPaddingTop()) { + return getPaddingTop(); + } + if (surfaceViewTop + dy > getPaddingTop() + mDragDistance) { + return getPaddingTop() + mDragDistance; + } + } + break; + case Bottom: + if (mShowMode == ShowMode.PullOut) { + if (top < getMeasuredHeight() - mDragDistance) { + return getMeasuredHeight() - mDragDistance; + } + } else { + if (surfaceViewTop + dy >= getPaddingTop()) { + return getPaddingTop(); + } + if (surfaceViewTop + dy <= getPaddingTop() - mDragDistance) { + return getPaddingTop() - mDragDistance; + } + } + break; + default: + break; + } + } + return top; + } + + private int handleClampHorizontal(View child, int left) { + if (child == getSurfaceView()) { + switch (mCurrentDragEdge) { + case Top: + return getPaddingLeft(); + case Bottom: + return getPaddingLeft(); + case Left: + if (left < getPaddingLeft()) { + return getPaddingLeft(); + } + if (left > getPaddingLeft() + mDragDistance) { + return getPaddingLeft() + mDragDistance; + } + break; + case Right: + if (left > getPaddingLeft()) { + return getPaddingLeft(); + } + if (left < getPaddingLeft() - mDragDistance) { + return getPaddingLeft() - mDragDistance; + } + break; + default: + break; + } + } else if (getCurrentBottomView() == child) { + switch (mCurrentDragEdge) { + case Top: + return getPaddingLeft(); + case Bottom: + return getPaddingLeft(); + case Left: + if (mShowMode == ShowMode.PullOut) { + if (left > getPaddingLeft()) { + return getPaddingLeft(); + } + } + break; + case Right: + if (mShowMode == ShowMode.PullOut) { + if (left < getMeasuredWidth() - mDragDistance) { + return getMeasuredWidth() - mDragDistance; + } + } + break; + default: + break; + } + } + return left; + } + + private void captureChildrenBound() { + View currentBottomView = getCurrentBottomView(); + if (getOpenStatus() == Status.Close) { + mViewBoundCache.remove(currentBottomView); + return; + } + + View[] views = new View[] {getSurfaceView(), currentBottomView}; + for (View child : views) { + Rect rect = mViewBoundCache.get(child); + if (rect == null) { + rect = new Rect(); + mViewBoundCache.put(child, rect); + } + rect.left = child.getLeft(); + rect.top = child.getTop(); + rect.right = child.getRight(); + rect.bottom = child.getBottom(); + } + } + + /** + * the dispatchRevealEvent method may not always get accurate position, it + * makes the view may not always get the event when the view is totally + * show( fraction = 1), so , we need to calculate every time. + */ + protected boolean isViewTotallyFirstShowed( + View child, Rect relativePosition, DragEdge edge, int surfaceLeft, int surfaceTop, int surfaceRight, int surfaceBottom) { + if (mShowEntirely.get(child)) { + return false; + } + int childLeft = relativePosition.left; + int childRight = relativePosition.right; + int childTop = relativePosition.top; + int childBottom = relativePosition.bottom; + boolean r = false; + if (getShowMode() == ShowMode.LayDown) { + if ((edge == DragEdge.Right && surfaceRight <= childLeft) || (edge == DragEdge.Left && surfaceLeft >= childRight) + || (edge == DragEdge.Top && surfaceTop >= childBottom) || (edge == DragEdge.Bottom && surfaceBottom <= childTop)) { + r = true; + } + } else if (getShowMode() == ShowMode.PullOut) { + if ((edge == DragEdge.Right && childRight <= getWidth()) || (edge == DragEdge.Left && childLeft >= getPaddingLeft()) + || (edge == DragEdge.Top && childTop >= getPaddingTop()) || (edge == DragEdge.Bottom && childBottom <= getHeight())) { + r = true; + } + } + return r; + } + + protected boolean isViewShowing( + View child, Rect relativePosition, DragEdge availableEdge, int surfaceLeft, int surfaceTop, int surfaceRight, int surfaceBottom) { + int childLeft = relativePosition.left; + int childRight = relativePosition.right; + int childTop = relativePosition.top; + int childBottom = relativePosition.bottom; + if (getShowMode() == ShowMode.LayDown) { + switch (availableEdge) { + case Right: + if (surfaceRight > childLeft && surfaceRight <= childRight) { + return true; + } + break; + case Left: + if (surfaceLeft < childRight && surfaceLeft >= childLeft) { + return true; + } + break; + case Top: + if (surfaceTop >= childTop && surfaceTop < childBottom) { + return true; + } + break; + case Bottom: + if (surfaceBottom > childTop && surfaceBottom <= childBottom) { + return true; + } + break; + default: + break; + } + } else if (getShowMode() == ShowMode.PullOut) { + switch (availableEdge) { + case Right: + if (childLeft <= getWidth() && childRight > getWidth()) { + return true; + } + break; + case Left: + if (childRight >= getPaddingLeft() && childLeft < getPaddingLeft()) { + return true; + } + break; + case Top: + if (childTop < getPaddingTop() && childBottom >= getPaddingTop()) { + return true; + } + break; + case Bottom: + if (childTop < getHeight() && childTop >= getPaddingTop()) { + return true; + } + break; + default: + break; + } + } + return false; + } + + protected Rect getRelativePosition(View child) { + View t = child; + Rect r = new Rect(t.getLeft(), t.getTop(), 0, 0); + while (t.getParent() != null && t != getRootView()) { + t = (View) t.getParent(); + if (t == this) { + break; + } + r.left += t.getLeft(); + r.top += t.getTop(); + } + r.right = r.left + child.getMeasuredWidth(); + r.bottom = r.top + child.getMeasuredHeight(); + return r; + } + + private int mEventCounter = 0; + + protected void dispatchSwipeEvent(int surfaceLeft, int surfaceTop, int dx, int dy) { + DragEdge edge = getDragEdge(); + boolean open = true; + if (edge == DragEdge.Left) { + if (dx < 0) { + open = false; + } + } else if (edge == DragEdge.Right) { + if (dx > 0) { + open = false; + } + } else if (edge == DragEdge.Top) { + if (dy < 0) { + open = false; + } + } else if (edge == DragEdge.Bottom) { + if (dy > 0) { + open = false; + } + } + + dispatchSwipeEvent(surfaceLeft, surfaceTop, open); + } + + protected void dispatchSwipeEvent(int surfaceLeft, int surfaceTop, boolean open) { + safeBottomView(); + Status status = getOpenStatus(); + + if (!mSwipeListeners.isEmpty()) { + mEventCounter++; + for (SwipeListener l : mSwipeListeners) { + if (mEventCounter == 1) { + if (open) { + l.onStartOpen(this); + } else { + l.onStartClose(this); + } + } + l.onUpdate(SwipeLayout.this, surfaceLeft - getPaddingLeft(), surfaceTop - getPaddingTop()); + } + + if (status == Status.Close) { + for (SwipeListener l : mSwipeListeners) { + l.onClose(SwipeLayout.this); + } + mEventCounter = 0; + mClickToClose = false; + } + + if (status == Status.Open) { + View currentBottomView = getCurrentBottomView(); + if (currentBottomView != null) { + currentBottomView.setEnabled(true); + } + for (SwipeListener l : mSwipeListeners) { + l.onOpen(SwipeLayout.this); + } + mEventCounter = 0; + mClickToClose = true; + } + } + } + + /** + * prevent bottom view get any touch event. Especially in LayDown mode. + */ + private void safeBottomView() { + Status status = getOpenStatus(); + List bottoms = getBottomViews(); + + if (status == Status.Close) { + for (View bottom : bottoms) { + if (bottom != null && bottom.getVisibility() != INVISIBLE) { + bottom.setVisibility(INVISIBLE); + } + } + } else { + View currentBottomView = getCurrentBottomView(); + if (currentBottomView != null && currentBottomView.getVisibility() != VISIBLE) { + currentBottomView.setVisibility(VISIBLE); + } + } + } + + protected void dispatchRevealEvent(final int surfaceLeft, final int surfaceTop, final int surfaceRight, final int surfaceBottom) { + if (mRevealListeners.isEmpty()) { + return; + } + for (Map.Entry> entry : mRevealListeners.entrySet()) { + View child = entry.getKey(); + Rect rect = getRelativePosition(child); + if (isViewShowing(child, rect, mCurrentDragEdge, surfaceLeft, surfaceTop, surfaceRight, surfaceBottom)) { + mShowEntirely.put(child, false); + int distance = 0; + float fraction = 0f; + if (getShowMode() == ShowMode.LayDown) { + switch (mCurrentDragEdge) { + case Left: + distance = rect.left - surfaceLeft; + fraction = distance / (float) child.getWidth(); + break; + case Right: + distance = rect.right - surfaceRight; + fraction = distance / (float) child.getWidth(); + break; + case Top: + distance = rect.top - surfaceTop; + fraction = distance / (float) child.getHeight(); + break; + case Bottom: + distance = rect.bottom - surfaceBottom; + fraction = distance / (float) child.getHeight(); + break; + default: + break; + } + } else if (getShowMode() == ShowMode.PullOut) { + switch (mCurrentDragEdge) { + case Left: + distance = rect.right - getPaddingLeft(); + fraction = distance / (float) child.getWidth(); + break; + case Right: + distance = rect.left - getWidth(); + fraction = distance / (float) child.getWidth(); + break; + case Top: + distance = rect.bottom - getPaddingTop(); + fraction = distance / (float) child.getHeight(); + break; + case Bottom: + distance = rect.top - getHeight(); + fraction = distance / (float) child.getHeight(); + break; + default: + break; + } + } + + for (OnRevealListener l : entry.getValue()) { + l.onReveal(child, mCurrentDragEdge, Math.abs(fraction), distance); + if (Math.abs(fraction) == 1) { + mShowEntirely.put(child, true); + } + } + } + + if (isViewTotallyFirstShowed(child, rect, mCurrentDragEdge, surfaceLeft, surfaceTop, surfaceRight, surfaceBottom)) { + mShowEntirely.put(child, true); + for (OnRevealListener l : entry.getValue()) { + if (mCurrentDragEdge == DragEdge.Left || mCurrentDragEdge == DragEdge.Right) { + l.onReveal(child, mCurrentDragEdge, 1, child.getWidth()); + } else { + l.onReveal(child, mCurrentDragEdge, 1, child.getHeight()); + } + } + } + } + } + + @Override + public void computeScroll() { + super.computeScroll(); + if (mDragHelper.continueSettling(true)) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + + /** + * {@link android.view.View.OnLayoutChangeListener} added in API 11. I need + * to support it from API 8. + */ + public interface OnLayout { + void onLayout(SwipeLayout v); + } + + private List mOnLayoutListeners; + + public void addOnLayoutListener(OnLayout l) { + if (mOnLayoutListeners == null) { + mOnLayoutListeners = new ArrayList(); + } + mOnLayoutListeners.add(l); + } + + public void removeOnLayoutListener(OnLayout l) { + if (mOnLayoutListeners != null) { + mOnLayoutListeners.remove(l); + } + } + + public void clearDragEdge() { + mDragEdges.clear(); + } + + public void setDrag(DragEdge dragEdge, int childId) { + clearDragEdge(); + addDrag(dragEdge, childId); + } + + public void setDrag(DragEdge dragEdge, View child) { + clearDragEdge(); + addDrag(dragEdge, child); + } + + public void addDrag(DragEdge dragEdge, int childId) { + addDrag(dragEdge, findViewById(childId), null); + } + + public void addDrag(DragEdge dragEdge, View child) { + addDrag(dragEdge, child, null); + } + + public void addDrag(DragEdge dragEdge, View child, ViewGroup.LayoutParams params) { + if (child == null) { + return; + } + + if (params == null) { + params = generateDefaultLayoutParams(); + } + if (!checkLayoutParams(params)) { + params = generateLayoutParams(params); + } + int gravity = -1; + switch (dragEdge) { + case Left: + gravity = Gravity.LEFT; + break; + case Right: + gravity = Gravity.RIGHT; + break; + case Top: + gravity = Gravity.TOP; + break; + case Bottom: + gravity = Gravity.BOTTOM; + break; + default: + break; + } + if (params instanceof LayoutParams) { + ((LayoutParams) params).gravity = gravity; + } + addView(child, 0, params); + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + if (child == null) { + return; + } + int gravity = Gravity.NO_GRAVITY; + try { + gravity = (Integer) params.getClass().getField("gravity").get(params); + } catch (Exception e) { + e.printStackTrace(); + } + + if (gravity > 0) { + gravity = GravityCompat.getAbsoluteGravity(gravity, ViewCompat.getLayoutDirection(this)); + + if ((gravity & Gravity.LEFT) == Gravity.LEFT) { + mDragEdges.put(DragEdge.Left, child); + } + if ((gravity & Gravity.RIGHT) == Gravity.RIGHT) { + mDragEdges.put(DragEdge.Right, child); + } + if ((gravity & Gravity.TOP) == Gravity.TOP) { + mDragEdges.put(DragEdge.Top, child); + } + if ((gravity & Gravity.BOTTOM) == Gravity.BOTTOM) { + mDragEdges.put(DragEdge.Bottom, child); + } + } else { + for (Map.Entry entry : mDragEdges.entrySet()) { + if (entry.getValue() == null) { + // means used the drag_edge attr, the no gravity child should be use set + mDragEdges.put(entry.getKey(), child); + break; + } + } + } + if (child.getParent() == this) { + return; + } + super.addView(child, index, params); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + + updateBottomViews(); + + if (mOnLayoutListeners != null) { + for (int i = 0; i < mOnLayoutListeners.size(); i++) { + mOnLayoutListeners.get(i).onLayout(this); + } + } + } + + void layoutPullOut() { + View surfaceView = getSurfaceView(); + Rect surfaceRect = mViewBoundCache.get(surfaceView); + if (surfaceRect == null) { + surfaceRect = computeSurfaceLayoutArea(false); + } + if (surfaceView != null) { + surfaceView.layout(surfaceRect.left, surfaceRect.top, surfaceRect.right, surfaceRect.bottom); + bringChildToFront(surfaceView); + } + View currentBottomView = getCurrentBottomView(); + Rect bottomViewRect = mViewBoundCache.get(currentBottomView); + if (bottomViewRect == null) { + bottomViewRect = computeBottomLayoutAreaViaSurface(ShowMode.PullOut, surfaceRect); + } + if (currentBottomView != null) { + currentBottomView.layout(bottomViewRect.left, bottomViewRect.top, bottomViewRect.right, bottomViewRect.bottom); + } + } + + void layoutLayDown() { + View surfaceView = getSurfaceView(); + Rect surfaceRect = mViewBoundCache.get(surfaceView); + if (surfaceRect == null) { + surfaceRect = computeSurfaceLayoutArea(false); + } + if (surfaceView != null) { + surfaceView.layout(surfaceRect.left, surfaceRect.top, surfaceRect.right, surfaceRect.bottom); + bringChildToFront(surfaceView); + } + View currentBottomView = getCurrentBottomView(); + Rect bottomViewRect = mViewBoundCache.get(currentBottomView); + if (bottomViewRect == null) { + bottomViewRect = computeBottomLayoutAreaViaSurface(ShowMode.LayDown, surfaceRect); + } + if (currentBottomView != null) { + currentBottomView.layout(bottomViewRect.left, bottomViewRect.top, bottomViewRect.right, bottomViewRect.bottom); + } + } + + private boolean mIsBeingDragged; + + private void checkCanDrag(MotionEvent ev) { + if (mIsBeingDragged) { + return; + } + if (getOpenStatus() == Status.Middle) { + mIsBeingDragged = true; + return; + } + Status status = getOpenStatus(); + float distanceX = ev.getRawX() - sX; + float distanceY = ev.getRawY() - sY; + float angle = Math.abs(distanceY / distanceX); + angle = (float) Math.toDegrees(Math.atan(angle)); + if (getOpenStatus() == Status.Close) { + DragEdge dragEdge; + if (angle < 45) { + if (distanceX > 0 && isLeftSwipeEnabled()) { + dragEdge = DragEdge.Left; + } else if (distanceX < 0 && isRightSwipeEnabled()) { + dragEdge = DragEdge.Right; + } else { + return; + } + + } else { + if (distanceY > 0 && isTopSwipeEnabled()) { + dragEdge = DragEdge.Top; + } else if (distanceY < 0 && isBottomSwipeEnabled()) { + dragEdge = DragEdge.Bottom; + } else { + return; + } + } + setCurrentDragEdge(dragEdge); + } + + boolean doNothing = isDoNothing(status, distanceX, distanceY, angle); + mIsBeingDragged = !doNothing; + } + + private boolean isDoNothing(Status status, float distanceX, float distanceY, float angle) { + boolean doNothing = false; + if (mCurrentDragEdge == DragEdge.Right) { + boolean suitable = (status == Status.Open && distanceX > mTouchSlop) || (status == Status.Close && distanceX < -mTouchSlop); + suitable = suitable || (status == Status.Middle); + + if (angle > 30 || !suitable) { + doNothing = true; + } + } + + if (mCurrentDragEdge == DragEdge.Left) { + boolean suitable = (status == Status.Open && distanceX < -mTouchSlop) || (status == Status.Close && distanceX > mTouchSlop); + suitable = suitable || status == Status.Middle; + + if (angle > 30 || !suitable) { + doNothing = true; + } + } + + if (mCurrentDragEdge == DragEdge.Top) { + boolean suitable = (status == Status.Open && distanceY < -mTouchSlop) || (status == Status.Close && distanceY > mTouchSlop); + suitable = suitable || status == Status.Middle; + + if (angle < 60 || !suitable) { + doNothing = true; + } + } + + if (mCurrentDragEdge == DragEdge.Bottom) { + boolean suitable = (status == Status.Open && distanceY > mTouchSlop) || (status == Status.Close && distanceY < -mTouchSlop); + suitable = suitable || status == Status.Middle; + + if (angle < 60 || !suitable) { + doNothing = true; + } + } + return doNothing; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (!isSwipeEnabled()) { + return false; + } + if (mClickToClose && getOpenStatus() == Status.Open && isTouchOnSurface(ev)) { + return true; + } + for (SwipeDenier denier : mSwipeDeniers) { + if (denier != null && denier.shouldDenySwipe(ev)) { + return false; + } + } + + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: + mDragHelper.processTouchEvent(ev); + mIsBeingDragged = false; + sX = ev.getRawX(); + sY = ev.getRawY(); + // if the swipe is in middle state(scrolling), should intercept the touch + if (getOpenStatus() == Status.Middle) { + mIsBeingDragged = true; + } + break; + case MotionEvent.ACTION_MOVE: + boolean beforeCheck = mIsBeingDragged; + checkCanDrag(ev); + if (mIsBeingDragged) { + ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + } + if (!beforeCheck && mIsBeingDragged) { + // let children has one chance to catch the touch, and request the swipe not intercept + // useful when swipeLayout wrap a swipeLayout or other gestural layout + return false; + } + break; + + case MotionEvent.ACTION_CANCEL: + mIsBeingDragged = false; + mDragHelper.processTouchEvent(ev); + break; + case MotionEvent.ACTION_UP: + mIsBeingDragged = false; + mDragHelper.processTouchEvent(ev); + break; + default: // handle other action, such as ACTION_POINTER_DOWN/UP + mDragHelper.processTouchEvent(ev); + } + return mIsBeingDragged; + } + + private float sX = -1; + private float sY = -1; + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (!isSwipeEnabled()) { + return super.onTouchEvent(event); + } + + int action = event.getActionMasked(); + gestureDetector.onTouchEvent(event); + + switch (action) { + case MotionEvent.ACTION_DOWN: + mDragHelper.processTouchEvent(event); + sX = event.getRawX(); + sY = event.getRawY(); + checkCanDrag(event); + if (mIsBeingDragged) { + getParent().requestDisallowInterceptTouchEvent(true); + mDragHelper.processTouchEvent(event); + } + break; + case MotionEvent.ACTION_MOVE: { + // the drag state and the direction are already judged at onInterceptTouchEvent + checkCanDrag(event); + if (mIsBeingDragged) { + getParent().requestDisallowInterceptTouchEvent(true); + mDragHelper.processTouchEvent(event); + } + break; + } + case MotionEvent.ACTION_UP: + mIsBeingDragged = false; + mDragHelper.processTouchEvent(event); + break; + case MotionEvent.ACTION_CANCEL: + mIsBeingDragged = false; + mDragHelper.processTouchEvent(event); + break; + + default: // handle other action, such as ACTION_POINTER_DOWN/UP + mDragHelper.processTouchEvent(event); + } + + return super.onTouchEvent(event) || mIsBeingDragged || action == MotionEvent.ACTION_DOWN; + } + + public boolean isClickToClose() { + return mClickToClose; + } + + public void setClickToClose(boolean mClickToClose) { + this.mClickToClose = mClickToClose; + } + + public void setSwipeEnabled(boolean enabled) { + mSwipeEnabled = enabled; + } + + public boolean isSwipeEnabled() { + return mSwipeEnabled; + } + + public boolean isLeftSwipeEnabled() { + View bottomView = mDragEdges.get(DragEdge.Left); + return bottomView != null && bottomView.getParent() == this && bottomView != getSurfaceView() && mSwipesEnabled[DragEdge.Left.ordinal()]; + } + + public void setLeftSwipeEnabled(boolean leftSwipeEnabled) { + this.mSwipesEnabled[DragEdge.Left.ordinal()] = leftSwipeEnabled; + } + + public boolean isRightSwipeEnabled() { + View bottomView = mDragEdges.get(DragEdge.Right); + return bottomView != null && bottomView.getParent() == this && bottomView != getSurfaceView() && mSwipesEnabled[DragEdge.Right.ordinal()]; + } + + public void setRightSwipeEnabled(boolean rightSwipeEnabled) { + this.mSwipesEnabled[DragEdge.Right.ordinal()] = rightSwipeEnabled; + } + + public boolean isTopSwipeEnabled() { + View bottomView = mDragEdges.get(DragEdge.Top); + return bottomView != null && bottomView.getParent() == this && bottomView != getSurfaceView() && mSwipesEnabled[DragEdge.Top.ordinal()]; + } + + public void setTopSwipeEnabled(boolean topSwipeEnabled) { + this.mSwipesEnabled[DragEdge.Top.ordinal()] = topSwipeEnabled; + } + + public boolean isBottomSwipeEnabled() { + View bottomView = mDragEdges.get(DragEdge.Bottom); + return bottomView != null && bottomView.getParent() == this && bottomView != getSurfaceView() && mSwipesEnabled[DragEdge.Bottom.ordinal()]; + } + + public void setBottomSwipeEnabled(boolean bottomSwipeEnabled) { + this.mSwipesEnabled[DragEdge.Bottom.ordinal()] = bottomSwipeEnabled; + } + + /*** + * Returns the percentage of revealing at which the view below should the view finish opening + * if it was already open before dragging + * + * @returns The percentage of view revealed to trigger, default value is 0.25 + */ + public float getWillOpenPercentAfterOpen() { + return mWillOpenPercentAfterOpen; + } + + /*** + * Allows to stablish at what percentage of revealing the view below should the view finish opening + * if it was already open before dragging + * + * @param willOpenPercentAfterOpen The percentage of view revealed to trigger, default value is 0.25 + */ + public void setWillOpenPercentAfterOpen(float willOpenPercentAfterOpen) { + this.mWillOpenPercentAfterOpen = willOpenPercentAfterOpen; + } + + /*** + * Returns the percentage of revealing at which the view below should the view finish opening + * if it was already closed before dragging + * + * @returns The percentage of view revealed to trigger, default value is 0.25 + */ + public float getWillOpenPercentAfterClose() { + return mWillOpenPercentAfterClose; + } + + /*** + * Allows to stablish at what percentage of revealing the view below should the view finish opening + * if it was already closed before dragging + * + * @param willOpenPercentAfterClose The percentage of view revealed to trigger, default value is 0.75 + */ + public void setWillOpenPercentAfterClose(float willOpenPercentAfterClose) { + this.mWillOpenPercentAfterClose = willOpenPercentAfterClose; + } + + private boolean insideAdapterView() { + return getAdapterView() != null; + } + + private AdapterView getAdapterView() { + ViewParent t = getParent(); + if (t instanceof AdapterView) { + return (AdapterView) t; + } + return null; + } + + public void performAdapterViewItemClick() { + if (getOpenStatus() != Status.Close) { + return; + } + ViewParent t = getParent(); + if (t instanceof AdapterView) { + AdapterView view = (AdapterView) t; + int p = view.getPositionForView(SwipeLayout.this); + if (p != AdapterView.INVALID_POSITION) { + view.performItemClick(view.getChildAt(p - view.getFirstVisiblePosition()), p, view.getAdapter().getItemId(p)); + } + } + } + + private boolean performAdapterViewItemLongClick() { + if (getOpenStatus() != Status.Close) { + return false; + } + ViewParent t = getParent(); + if (t instanceof AdapterView) { + AdapterView view = (AdapterView) t; + int p = view.getPositionForView(SwipeLayout.this); + if (p == AdapterView.INVALID_POSITION) { + return false; + } + long vId = view.getItemIdAtPosition(p); + boolean handled = false; + try { + Method m = AbsListView.class.getDeclaredMethod("performLongPress", View.class, int.class, long.class); + m.setAccessible(true); + handled = (boolean) m.invoke(view, SwipeLayout.this, p, vId); + + } catch (Exception e) { + e.printStackTrace(); + + if (view.getOnItemLongClickListener() != null) { + handled = view.getOnItemLongClickListener().onItemLongClick(view, SwipeLayout.this, p, vId); + } + if (handled) { + view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + } + } + return handled; + } + return false; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (insideAdapterView()) { + if (clickListener == null) { + setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + performAdapterViewItemClick(); + } + }); + } + if (longClickListener == null) { + setOnLongClickListener(new OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + performAdapterViewItemLongClick(); + return true; + } + }); + } + } + } + + OnClickListener clickListener; + + @Override + public void setOnClickListener(OnClickListener l) { + super.setOnClickListener(l); + clickListener = l; + } + + OnLongClickListener longClickListener; + + @Override + public void setOnLongClickListener(OnLongClickListener l) { + super.setOnLongClickListener(l); + longClickListener = l; + } + + private Rect hitSurfaceRect; + + private boolean isTouchOnSurface(MotionEvent ev) { + View surfaceView = getSurfaceView(); + if (surfaceView == null) { + return false; + } + if (hitSurfaceRect == null) { + hitSurfaceRect = new Rect(); + } + surfaceView.getHitRect(hitSurfaceRect); + return hitSurfaceRect.contains((int) ev.getX(), (int) ev.getY()); + } + + private GestureDetector gestureDetector = new GestureDetector(getContext(), new SwipeDetector()); + + class SwipeDetector extends GestureDetector.SimpleOnGestureListener { + @Override + public boolean onSingleTapUp(MotionEvent e) { + if (isTouchOnSurface(e)) { + if (mClickToClose && getOpenStatus() != Status.Close) { + close(); + } else { + if (mDoubleClickListener != null) { + mDoubleClickListener.onClick(); + } + } + } + return super.onSingleTapUp(e); + } + + @Override + public boolean onDoubleTap(MotionEvent e) { + if (mDoubleClickListener != null) { + View target; + View bottom = getCurrentBottomView(); + View surface = getSurfaceView(); + if (bottom != null && e.getX() > bottom.getLeft() && e.getX() < bottom.getRight() && e.getY() > bottom.getTop() + && e.getY() < bottom.getBottom()) { + target = bottom; + } else { + target = surface; + } + mDoubleClickListener.onDoubleClick(SwipeLayout.this, target == surface); + } + return true; + } + } + + /** + * set the drag distance, it will force set the bottom view's width or + * height via this value. + * + * @param max max distance in dp unit + */ + public void setDragDistance(int max) { + if (max < 0) { + max = 0; + } + mDragDistance = dp2px(max); + requestLayout(); + } + + /** + * There are 2 diffirent show mode. + * {@link com.tencent.qcloud.tuikit.timcommon.component.swipe.SwipeLayout.ShowMode}.PullOut and + * {@link com.tencent.qcloud.tuikit.timcommon.component.swipe.SwipeLayout.ShowMode}.LayDown. + * + * @param mode + */ + public void setShowMode(ShowMode mode) { + mShowMode = mode; + requestLayout(); + } + + public DragEdge getDragEdge() { + return mCurrentDragEdge; + } + + public int getDragDistance() { + return mDragDistance; + } + + public ShowMode getShowMode() { + return mShowMode; + } + + /** + * return null if there is no surface view(no children) + */ + public View getSurfaceView() { + if (getChildCount() == 0) { + return null; + } + return getChildAt(getChildCount() - 1); + } + + /** + * return null if there is no bottom view + */ + public View getCurrentBottomView() { + List bottoms = getBottomViews(); + if (mCurrentDragEdge.ordinal() < bottoms.size()) { + return bottoms.get(mCurrentDragEdge.ordinal()); + } + return null; + } + + /** + * @return all bottomViews: left, top, right, bottom (may null if the edge is not set) + */ + public List getBottomViews() { + ArrayList bottoms = new ArrayList(); + for (DragEdge dragEdge : DragEdge.values()) { + bottoms.add(mDragEdges.get(dragEdge)); + } + return bottoms; + } + + public enum Status { Middle, Open, Close } + + /** + * get the open status. + * + * @return {@link com.tencent.qcloud.tuikit.timcommon.component.swipe.SwipeLayout.Status} Open , Close or + * Middle. + */ + public Status getOpenStatus() { + View surfaceView = getSurfaceView(); + if (surfaceView == null) { + return Status.Close; + } + int surfaceLeft = surfaceView.getLeft(); + int surfaceTop = surfaceView.getTop(); + if (surfaceLeft == getPaddingLeft() && surfaceTop == getPaddingTop()) { + return Status.Close; + } + + if (surfaceLeft == (getPaddingLeft() - mDragDistance) || surfaceLeft == (getPaddingLeft() + mDragDistance) + || surfaceTop == (getPaddingTop() - mDragDistance) || surfaceTop == (getPaddingTop() + mDragDistance)) { + return Status.Open; + } + + return Status.Middle; + } + + /** + * Process the surface release event. + * + * @param xvel xVelocity + * @param yvel yVelocity + * @param isCloseBeforeDragged the open state before drag + */ + protected void processHandRelease(float xvel, float yvel, boolean isCloseBeforeDragged) { + float minVelocity = mDragHelper.getMinVelocity(); + View surfaceView = getSurfaceView(); + DragEdge currentDragEdge = mCurrentDragEdge; + if (currentDragEdge == null || surfaceView == null) { + return; + } + float willOpenPercent = (isCloseBeforeDragged ? mWillOpenPercentAfterClose : mWillOpenPercentAfterOpen); + if (currentDragEdge == DragEdge.Left) { + if (xvel > minVelocity) { + open(); + } else if (xvel < -minVelocity) { + close(); + } else { + float openPercent = 1f * getSurfaceView().getLeft() / mDragDistance; + if (openPercent > willOpenPercent) { + open(); + } else { + close(); + } + } + } else if (currentDragEdge == DragEdge.Right) { + if (xvel > minVelocity) { + close(); + } else if (xvel < -minVelocity) { + open(); + } else { + float openPercent = 1f * (-getSurfaceView().getLeft()) / mDragDistance; + if (openPercent > willOpenPercent) { + open(); + } else { + close(); + } + } + } else if (currentDragEdge == DragEdge.Top) { + if (yvel > minVelocity) { + open(); + } else if (yvel < -minVelocity) { + close(); + } else { + float openPercent = 1f * getSurfaceView().getTop() / mDragDistance; + if (openPercent > willOpenPercent) { + open(); + } else { + close(); + } + } + } else if (currentDragEdge == DragEdge.Bottom) { + if (yvel > minVelocity) { + close(); + } else if (yvel < -minVelocity) { + open(); + } else { + float openPercent = 1f * (-getSurfaceView().getTop()) / mDragDistance; + if (openPercent > willOpenPercent) { + open(); + } else { + close(); + } + } + } + } + + /** + * smoothly open surface. + */ + public void open() { + open(true, true); + } + + public void open(boolean smooth) { + open(smooth, true); + } + + public void open(boolean smooth, boolean notify) { + View surface = getSurfaceView(); + View bottom = getCurrentBottomView(); + if (surface == null) { + return; + } + int dx; + int dy; + Rect rect = computeSurfaceLayoutArea(true); + if (smooth) { + mDragHelper.smoothSlideViewTo(surface, rect.left, rect.top); + } else { + dx = rect.left - surface.getLeft(); + dy = rect.top - surface.getTop(); + surface.layout(rect.left, rect.top, rect.right, rect.bottom); + if (getShowMode() == ShowMode.PullOut) { + Rect bRect = computeBottomLayoutAreaViaSurface(ShowMode.PullOut, rect); + if (bottom != null) { + bottom.layout(bRect.left, bRect.top, bRect.right, bRect.bottom); + } + } + if (notify) { + dispatchRevealEvent(rect.left, rect.top, rect.right, rect.bottom); + dispatchSwipeEvent(rect.left, rect.top, dx, dy); + } else { + safeBottomView(); + } + } + invalidate(); + } + + public void open(DragEdge edge) { + setCurrentDragEdge(edge); + open(true, true); + } + + public void open(boolean smooth, DragEdge edge) { + setCurrentDragEdge(edge); + open(smooth, true); + } + + public void open(boolean smooth, boolean notify, DragEdge edge) { + setCurrentDragEdge(edge); + open(smooth, notify); + } + + /** + * smoothly close surface. + */ + public void close() { + close(true, true); + } + + public void close(boolean smooth) { + close(smooth, true); + } + + /** + * close surface + * + * @param smooth smoothly or not. + * @param notify if notify all the listeners. + */ + public void close(boolean smooth, boolean notify) { + View surface = getSurfaceView(); + if (surface == null) { + return; + } + int dx; + int dy; + if (smooth) { + mDragHelper.smoothSlideViewTo(getSurfaceView(), getPaddingLeft(), getPaddingTop()); + } else { + Rect rect = computeSurfaceLayoutArea(false); + dx = rect.left - surface.getLeft(); + dy = rect.top - surface.getTop(); + surface.layout(rect.left, rect.top, rect.right, rect.bottom); + if (notify) { + dispatchRevealEvent(rect.left, rect.top, rect.right, rect.bottom); + dispatchSwipeEvent(rect.left, rect.top, dx, dy); + } else { + safeBottomView(); + } + } + invalidate(); + } + + public void toggle() { + toggle(true); + } + + public void toggle(boolean smooth) { + if (getOpenStatus() == Status.Open) { + close(smooth); + } else if (getOpenStatus() == Status.Close) { + open(smooth); + } + } + + /** + * a helper function to compute the Rect area that surface will hold in. + * + * @param open open status or close status. + */ + private Rect computeSurfaceLayoutArea(boolean open) { + int l = getPaddingLeft(); + int t = getPaddingTop(); + if (open) { + if (mCurrentDragEdge == DragEdge.Left) { + l = getPaddingLeft() + mDragDistance; + } else if (mCurrentDragEdge == DragEdge.Right) { + l = getPaddingLeft() - mDragDistance; + } else if (mCurrentDragEdge == DragEdge.Top) { + t = getPaddingTop() + mDragDistance; + } else { + t = getPaddingTop() - mDragDistance; + } + } + return new Rect(l, t, l + getMeasuredWidth(), t + getMeasuredHeight()); + } + + private Rect computeBottomLayoutAreaViaSurface(ShowMode mode, Rect surfaceArea) { + Rect rect = surfaceArea; + View bottomView = getCurrentBottomView(); + + int bl = rect.left; + int bt = rect.top; + int br = rect.right; + int bb = rect.bottom; + if (mode == ShowMode.PullOut) { + if (mCurrentDragEdge == DragEdge.Left) { + bl = rect.left - mDragDistance; + } else if (mCurrentDragEdge == DragEdge.Right) { + bl = rect.right; + } else if (mCurrentDragEdge == DragEdge.Top) { + bt = rect.top - mDragDistance; + } else { + bt = rect.bottom; + } + + if (mCurrentDragEdge == DragEdge.Left || mCurrentDragEdge == DragEdge.Right) { + bb = rect.bottom; + br = bl + (bottomView == null ? 0 : bottomView.getMeasuredWidth()); + } else { + bb = bt + (bottomView == null ? 0 : bottomView.getMeasuredHeight()); + br = rect.right; + } + } else if (mode == ShowMode.LayDown) { + if (mCurrentDragEdge == DragEdge.Left) { + br = bl + mDragDistance; + } else if (mCurrentDragEdge == DragEdge.Right) { + bl = br - mDragDistance; + } else if (mCurrentDragEdge == DragEdge.Top) { + bb = bt + mDragDistance; + } else { + bt = bb - mDragDistance; + } + } + return new Rect(bl, bt, br, bb); + } + + private Rect computeBottomLayDown(DragEdge dragEdge) { + int bl = getPaddingLeft(); + int bt = getPaddingTop(); + int br; + int bb; + if (dragEdge == DragEdge.Right) { + bl = getMeasuredWidth() - mDragDistance; + } else if (dragEdge == DragEdge.Bottom) { + bt = getMeasuredHeight() - mDragDistance; + } + if (dragEdge == DragEdge.Left || dragEdge == DragEdge.Right) { + br = bl + mDragDistance; + bb = bt + getMeasuredHeight(); + } else { + br = bl + getMeasuredWidth(); + bb = bt + mDragDistance; + } + return new Rect(bl, bt, br, bb); + } + + public void setOnDoubleClickListener(DoubleClickListener doubleClickListener) { + mDoubleClickListener = doubleClickListener; + } + + public interface DoubleClickListener { + void onDoubleClick(SwipeLayout layout, boolean surface); + + void onClick(); + } + + private int dp2px(float dp) { + return (int) (dp * getContext().getResources().getDisplayMetrics().density + 0.5f); + } + + /** + * Deprecated, use {@link #setDrag(com.tencent.qcloud.tuikit.timcommon.component.swipe.SwipeLayout.DragEdge, android.view.View)} + */ + @Deprecated + public void setDragEdge(DragEdge dragEdge) { + clearDragEdge(); + if (getChildCount() >= 2) { + mDragEdges.put(dragEdge, getChildAt(getChildCount() - 2)); + } + setCurrentDragEdge(dragEdge); + } + + public void onViewRemoved(View child) { + for (Map.Entry entry : new HashMap(mDragEdges).entrySet()) { + if (entry.getValue() == child) { + mDragEdges.remove(entry.getKey()); + } + } + } + + public Map getDragEdgeMap() { + return mDragEdges; + } + + /** + * Deprecated, use {@link #getDragEdgeMap()} + */ + @Deprecated + public List getDragEdges() { + return new ArrayList(mDragEdges.keySet()); + } + + /** + * Deprecated, use {@link #setDrag(com.tencent.qcloud.tuikit.timcommon.component.swipe.SwipeLayout.DragEdge, android.view.View)} + */ + @Deprecated + public void setDragEdges(List dragEdges) { + clearDragEdge(); + for (int i = 0, size = Math.min(dragEdges.size(), getChildCount() - 1); i < size; i++) { + DragEdge dragEdge = dragEdges.get(i); + mDragEdges.put(dragEdge, getChildAt(i)); + } + if (dragEdges.size() == 0 || dragEdges.contains(DefaultDragEdge)) { + setCurrentDragEdge(DefaultDragEdge); + } else { + setCurrentDragEdge(dragEdges.get(0)); + } + } + + /** + * Deprecated, use {@link #addDrag(com.tencent.qcloud.tuikit.timcommon.component.swipe.SwipeLayout.DragEdge, android.view.View)} + */ + @Deprecated + public void setDragEdges(DragEdge... mDragEdges) { + clearDragEdge(); + setDragEdges(Arrays.asList(mDragEdges)); + } + + /** + * Deprecated, use {@link #addDrag(com.tencent.qcloud.tuikit.timcommon.component.swipe.SwipeLayout.DragEdge, android.view.View)} + * When using multiple drag edges it's a good idea to pass the ids of the views that + * you're using for the left, right, top bottom views (-1 if you're not using a particular view) + */ + @Deprecated + public void setBottomViewIds(int leftId, int rightId, int topId, int bottomId) { + addDrag(DragEdge.Left, findViewById(leftId)); + addDrag(DragEdge.Right, findViewById(rightId)); + addDrag(DragEdge.Top, findViewById(topId)); + addDrag(DragEdge.Bottom, findViewById(bottomId)); + } + + private float getCurrentOffset() { + if (mCurrentDragEdge == null) { + return 0; + } + return mEdgeSwipesOffset[mCurrentDragEdge.ordinal()]; + } + + private void setCurrentDragEdge(DragEdge dragEdge) { + mCurrentDragEdge = dragEdge; + updateBottomViews(); + } + + private void updateBottomViews() { + View currentBottomView = getCurrentBottomView(); + if (currentBottomView != null) { + if (mCurrentDragEdge == DragEdge.Left || mCurrentDragEdge == DragEdge.Right) { + mDragDistance = currentBottomView.getMeasuredWidth() - dp2px(getCurrentOffset()); + } else { + mDragDistance = currentBottomView.getMeasuredHeight() - dp2px(getCurrentOffset()); + } + } + + if (mShowMode == ShowMode.PullOut) { + layoutPullOut(); + } else if (mShowMode == ShowMode.LayDown) { + layoutLayDown(); + } + + safeBottomView(); + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/IPlayer.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/IPlayer.java new file mode 100644 index 00000000..54307945 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/IPlayer.java @@ -0,0 +1,74 @@ +package com.tencent.qcloud.tuikit.timcommon.component.videoview; + +import android.content.Context; +import android.net.Uri; +import android.view.Surface; +import android.view.SurfaceHolder; + +import java.io.IOException; + +public interface IPlayer { + void setOnPreparedListener(final OnPreparedListener l); + + void setOnErrorListener(final OnErrorListener l); + + void setOnCompletionListener(final OnCompletionListener l); + + void setOnVideoSizeChangedListener(final OnVideoSizeChangedListener l); + + void setOnSeekCompleteListener(final OnSeekCompleteListener l); + + void setOnInfoListener(final OnInfoListener l); + + void setDisplay(SurfaceHolder sh); + + void setSurface(Surface sh); + + void setDataSource(Context context, Uri uri) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException; + + void prepareAsync(); + + void release(); + + void start(); + + void stop(); + + void pause(); + + boolean isPlaying(); + + int getVideoWidth(); + + int getVideoHeight(); + + void seekTo(int progress); + + int getCurrentPosition(); + + int getDuration(); + + interface OnPreparedListener { + void onPrepared(IPlayer mp); + } + + interface OnErrorListener { + boolean onError(IPlayer mp, int what, int extra); + } + + interface OnCompletionListener { + void onCompletion(IPlayer mp); + } + + interface OnVideoSizeChangedListener { + void onVideoSizeChanged(IPlayer mp, int width, int height); + } + + interface OnInfoListener { + void onInfo(IPlayer mp, int what, int extra); + } + + interface OnSeekCompleteListener { + void onSeekComplete(IPlayer mp); + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/MediaPlayerProxy.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/MediaPlayerProxy.java new file mode 100644 index 00000000..27913482 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/MediaPlayerProxy.java @@ -0,0 +1,120 @@ +package com.tencent.qcloud.tuikit.timcommon.component.videoview; + +import android.content.Context; +import android.net.Uri; +import android.util.Log; +import android.view.Surface; +import android.view.SurfaceHolder; + +import java.io.IOException; + +public class MediaPlayerProxy implements IPlayer { + private static final String TAG = MediaPlayerProxy.class.getSimpleName(); + + private IPlayer mMediaPlayer; + + public MediaPlayerProxy() { + mMediaPlayer = new SystemMediaPlayerWrapper(); + Log.i(TAG, "use mMediaPlayer: " + mMediaPlayer); + } + + @Override + public void setOnPreparedListener(final OnPreparedListener l) { + mMediaPlayer.setOnPreparedListener(l); + } + + @Override + public void setOnErrorListener(final OnErrorListener l) { + mMediaPlayer.setOnErrorListener(l); + } + + @Override + public void setOnCompletionListener(final OnCompletionListener l) { + mMediaPlayer.setOnCompletionListener(l); + } + + @Override + public void setOnVideoSizeChangedListener(final OnVideoSizeChangedListener l) { + mMediaPlayer.setOnVideoSizeChangedListener(l); + } + + @Override + public void setOnSeekCompleteListener(OnSeekCompleteListener l) { + mMediaPlayer.setOnSeekCompleteListener(l); + } + + @Override + public void setOnInfoListener(final OnInfoListener l) { + mMediaPlayer.setOnInfoListener(l); + } + + @Override + public void setDisplay(SurfaceHolder sh) { + mMediaPlayer.setDisplay(sh); + } + + @Override + public void setSurface(Surface sh) { + mMediaPlayer.setSurface(sh); + } + + @Override + public void setDataSource(Context context, Uri uri) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException { + mMediaPlayer.setDataSource(context, uri); + } + + @Override + public void prepareAsync() { + mMediaPlayer.prepareAsync(); + } + + @Override + public void release() { + mMediaPlayer.release(); + } + + @Override + public void start() { + mMediaPlayer.start(); + } + + @Override + public void stop() { + mMediaPlayer.stop(); + } + + @Override + public void pause() { + mMediaPlayer.pause(); + } + + @Override + public boolean isPlaying() { + return mMediaPlayer.isPlaying(); + } + + @Override + public int getVideoWidth() { + return mMediaPlayer.getVideoWidth(); + } + + @Override + public int getVideoHeight() { + return mMediaPlayer.getVideoHeight(); + } + + @Override + public void seekTo(int progress) { + mMediaPlayer.seekTo(progress); + } + + @Override + public int getCurrentPosition() { + return mMediaPlayer.getCurrentPosition(); + } + + @Override + public int getDuration() { + return mMediaPlayer.getDuration(); + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/SystemMediaPlayerWrapper.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/SystemMediaPlayerWrapper.java new file mode 100644 index 00000000..936b4d55 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/SystemMediaPlayerWrapper.java @@ -0,0 +1,153 @@ +package com.tencent.qcloud.tuikit.timcommon.component.videoview; + +import android.content.Context; +import android.media.MediaPlayer; +import android.net.Uri; +import android.os.Build; +import android.view.Surface; +import android.view.SurfaceHolder; + +import java.io.IOException; + +public class SystemMediaPlayerWrapper implements IPlayer { + private MediaPlayer mMediaPlayer; + + public SystemMediaPlayerWrapper() { + mMediaPlayer = new MediaPlayer(); + } + + @Override + public void setOnPreparedListener(final OnPreparedListener l) { + mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { + @Override + public void onPrepared(MediaPlayer mp) { + l.onPrepared(SystemMediaPlayerWrapper.this); + } + }); + } + + @Override + public void setOnErrorListener(final OnErrorListener l) { + mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() { + @Override + public boolean onError(MediaPlayer mp, int what, int extra) { + return l.onError(SystemMediaPlayerWrapper.this, what, extra); + } + }); + } + + @Override + public void setOnCompletionListener(final OnCompletionListener l) { + mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { + @Override + public void onCompletion(MediaPlayer mp) { + l.onCompletion(SystemMediaPlayerWrapper.this); + } + }); + } + + @Override + public void setOnSeekCompleteListener(final OnSeekCompleteListener l) { + mMediaPlayer.setOnSeekCompleteListener(new MediaPlayer.OnSeekCompleteListener() { + @Override + public void onSeekComplete(MediaPlayer mediaPlayer) { + l.onSeekComplete(SystemMediaPlayerWrapper.this); + } + }); + } + + @Override + public void setOnVideoSizeChangedListener(final OnVideoSizeChangedListener l) { + mMediaPlayer.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() { + @Override + public void onVideoSizeChanged(MediaPlayer mp, int width, int height) { + l.onVideoSizeChanged(SystemMediaPlayerWrapper.this, width, height); + } + }); + } + + @Override + public void setOnInfoListener(final OnInfoListener l) { + mMediaPlayer.setOnInfoListener(new MediaPlayer.OnInfoListener() { + @Override + public boolean onInfo(MediaPlayer mp, int what, int extra) { + l.onInfo(SystemMediaPlayerWrapper.this, what, extra); + return false; + } + }); + } + + @Override + public void setDisplay(SurfaceHolder sh) { + mMediaPlayer.setDisplay(sh); + } + + @Override + public void setSurface(Surface sh) { + mMediaPlayer.setSurface(sh); + } + + @Override + public void setDataSource(Context context, Uri uri) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException { + mMediaPlayer.setDataSource(context, uri); + } + + @Override + public void prepareAsync() { + mMediaPlayer.prepareAsync(); + } + + @Override + public void release() { + mMediaPlayer.release(); + } + + @Override + public void start() { + mMediaPlayer.start(); + } + + @Override + public void stop() { + mMediaPlayer.stop(); + } + + @Override + public void pause() { + mMediaPlayer.pause(); + } + + @Override + public boolean isPlaying() { + return mMediaPlayer.isPlaying(); + } + + @Override + public int getVideoWidth() { + return mMediaPlayer.getVideoWidth(); + } + + @Override + public int getVideoHeight() { + return mMediaPlayer.getVideoHeight(); + } + + @Override + public void seekTo(int progress) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + mMediaPlayer.seekTo(progress, MediaPlayer.SEEK_CLOSEST); + } else { + mMediaPlayer.seekTo(progress); + } + } + + @Override + public int getCurrentPosition() { + return mMediaPlayer.getCurrentPosition(); + } + + @Override + public int getDuration() { + return mMediaPlayer.getDuration(); + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/VideoGestureScaleAttacher.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/VideoGestureScaleAttacher.java new file mode 100644 index 00000000..429d25b3 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/VideoGestureScaleAttacher.java @@ -0,0 +1,441 @@ +package com.tencent.qcloud.tuikit.timcommon.component.videoview; + +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.RectF; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.view.TextureView; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewParent; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.Interpolator; +import android.widget.ImageView; +import android.widget.OverScroller; +import com.tencent.qcloud.tuikit.timcommon.util.ThreadUtils; + +public class VideoGestureScaleAttacher { + private static final float EDGE_DRAG_EVENT_INTERCEPT_THRESHOLD = 50f; + private static final int EDGE_NONE = -1; + private static final int EDGE_LEFT = 0; + private static final int EDGE_RIGHT = 1; + private static final int EDGE_BOTH = 2; + private static final float DEFAULT_MAX_SCALE = 3.0f; + private static final float DEFAULT_MID_SCALE = 1.75f; + private static final float DEFAULT_MIN_SCALE = 1.0f; + private static final int DEFAULT_ZOOM_DURATION = 200; + private ScaleGestureDetector scaleGestureDetector; + private float mTouchSlop; + private float mMinimumVelocity; + private VelocityTracker mVelocityTracker; + private boolean mIsDragging; + private float mLastTouchX; + private float mLastTouchY; + private OnScaleListener internalScaleListener; + private VideoView view; + private float minScale = DEFAULT_MIN_SCALE; + private float middleScale = DEFAULT_MID_SCALE; + private float maxScale = DEFAULT_MAX_SCALE; + private int scrollEdge; + private final Matrix transferMatrix = new Matrix(); + private final RectF rectF = new RectF(); + private final float[] mMatrixValues = new float[9]; + private ImageView.ScaleType scaleType = ImageView.ScaleType.FIT_CENTER; + private final Interpolator interpolator = new AccelerateDecelerateInterpolator(); + + private int zoomDuration = DEFAULT_ZOOM_DURATION; + + private FlingRunnable currentFlingRunnable; + + private VideoGestureScaleAttacher() {} + + public static void attach(VideoView view) { + VideoGestureScaleAttacher attacher = new VideoGestureScaleAttacher(); + attacher.view = view; + if (view == null || view.getContext() == null) { + return; + } + Context context = view.getContext(); + final ViewConfiguration configuration = ViewConfiguration.get(context); + attacher.mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); + attacher.mTouchSlop = configuration.getScaledTouchSlop(); + attacher.internalScaleListener = new OnScaleListener() { + @Override + public boolean onScale(ScaleGestureDetector detector) { + float scaleFactor = detector.getScaleFactor(); + if (Float.isNaN(scaleFactor) || Float.isInfinite(scaleFactor)) { + return false; + } + float focusX = detector.getFocusX(); + float focusY = detector.getFocusY(); + return attacher.scale(scaleFactor, focusX, focusY); + } + + @Override + public void onFling(float startX, float startY, float velocityX, float velocityY) { + if (attacher.scaleGestureDetector.isInProgress()) { + return; + } + attacher.currentFlingRunnable = attacher.new FlingRunnable(attacher.view); + attacher.currentFlingRunnable.fling(attacher.getViewWidth(), attacher.getViewHeight(), (int) velocityX, (int) velocityY); + ThreadUtils.runOnUiThread(attacher.currentFlingRunnable); + } + + @Override + public void onDrag(float dx, float dy) { + if (attacher.scaleGestureDetector.isInProgress()) { + return; + } + attacher.transferMatrix.postTranslate(dx, dy); + attacher.invalidateView(); + attacher.checkMatrixBounds(); + if (attacher.scrollEdge == EDGE_BOTH && Math.abs(dx) >= EDGE_DRAG_EVENT_INTERCEPT_THRESHOLD + || (attacher.scrollEdge == EDGE_LEFT && dx >= EDGE_DRAG_EVENT_INTERCEPT_THRESHOLD) + || (attacher.scrollEdge == EDGE_RIGHT && dx <= -EDGE_DRAG_EVENT_INTERCEPT_THRESHOLD)) { + ViewParent viewParent = view.getParent(); + if (viewParent != null) { + viewParent.requestDisallowInterceptTouchEvent(false); + } + } else { + ViewParent viewParent = view.getParent(); + if (viewParent != null) { + viewParent.requestDisallowInterceptTouchEvent(true); + } + } + } + }; + attacher.scaleGestureDetector = new ScaleGestureDetector(context, attacher.internalScaleListener); + + view.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + attacher.scaleGestureDetector.onTouchEvent(event); + attacher.processTouchEvent(event); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: { + attacher.cancelFling(); + ViewParent viewParent = view.getParent(); + if (viewParent != null) { + viewParent.requestDisallowInterceptTouchEvent(true); + } + break; + } + case MotionEvent.ACTION_CANCEL: + onActionCancel(); + break; + case MotionEvent.ACTION_UP: { + onActionCancel(); + break; + } + default: + break; + } + return true; + } + + private void onActionCancel() { + if (attacher.getScale() < attacher.minScale) { + RectF rect = attacher.getDisplayRect(); + view.post(attacher.new AnimatedZoomRunnable(attacher.getScale(), attacher.minScale, rect.centerX(), rect.centerY())); + } else if (attacher.getScale() > attacher.maxScale) { + RectF rect = attacher.getDisplayRect(); + view.post(attacher.new AnimatedZoomRunnable(attacher.getScale(), attacher.maxScale, rect.centerX(), rect.centerY())); + } + } + }); + } + + public void cancelFling() { + if (currentFlingRunnable != null) { + currentFlingRunnable.cancelFling(); + currentFlingRunnable = null; + } + } + + private boolean scale(float scaleFactor, float focusX, float focusY) { + if ((getScale() <= maxScale || scaleFactor < 1f) && (getScale() >= minScale || scaleFactor > 1f)) { + transferMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY); + invalidateView(); + checkMatrixBounds(); + return true; + } + return false; + } + + private void invalidateView() { + Matrix baseMatrix = view.getBaseMatrix(); + Matrix matrix = new Matrix(); + matrix.setConcat(baseMatrix, transferMatrix); + view.setTransform(matrix); + view.invalidate(); + } + + private boolean processTouchEvent(MotionEvent ev) { + final int action = ev.getAction(); + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + + mVelocityTracker = VelocityTracker.obtain(); + if (null != mVelocityTracker) { + mVelocityTracker.addMovement(ev); + } + + mLastTouchX = ev.getX(); + mLastTouchY = ev.getY(); + mIsDragging = false; + + break; + case MotionEvent.ACTION_MOVE: + final float x = ev.getX(); + final float y = ev.getY(); + final float dx = x - mLastTouchX; + final float dy = y - mLastTouchY; + + if (!mIsDragging) { + // Use Pythagoras to see if drag length is larger than + // touch slop + mIsDragging = Math.sqrt((dx * dx) + (dy * dy)) >= mTouchSlop; + } + + if (mIsDragging) { + internalScaleListener.onDrag(dx, dy); + mLastTouchX = x; + mLastTouchY = y; + + if (null != mVelocityTracker) { + mVelocityTracker.addMovement(ev); + } + } + break; + case MotionEvent.ACTION_CANCEL: + // Recycle Velocity Tracker + if (null != mVelocityTracker) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + break; + case MotionEvent.ACTION_UP: + if (mIsDragging) { + if (null != mVelocityTracker) { + mLastTouchX = ev.getX(); + mLastTouchY = ev.getY(); + + // Compute velocity within the last 1000ms + mVelocityTracker.addMovement(ev); + mVelocityTracker.computeCurrentVelocity(1000); + + final float vX = mVelocityTracker.getXVelocity(); + final float vY = mVelocityTracker.getYVelocity(); + + // If the velocity is greater than minVelocity, call + // listener + if (Math.max(Math.abs(vX), Math.abs(vY)) >= mMinimumVelocity) { + internalScaleListener.onFling(mLastTouchX, mLastTouchY, -vX, -vY); + } + } + } + + // Recycle Velocity Tracker + if (null != mVelocityTracker) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + break; + default: + break; + } + return true; + } + + private RectF getDisplayRect() { + rectF.set(0, 0, view.getWidth(), view.getHeight()); + Matrix matrix = new Matrix(); + view.getTransform(matrix); + matrix.mapRect(rectF); + return rectF; + } + + public float getScale() { + return (float) Math.sqrt((Math.pow(getValue(transferMatrix, Matrix.MSCALE_X), 2) + Math.pow(getValue(transferMatrix, Matrix.MSCALE_Y), 2)) / 2); + } + + private void checkMatrixBounds() { + final RectF rect = getDisplayRect(); + final float height = rect.height(); + final float width = rect.width(); + float deltaX = 0; + float deltaY = 0; + final int viewHeight = getViewHeight(); + if (height <= viewHeight) { + switch (scaleType) { + case FIT_START: + deltaY = -rect.top; + break; + case FIT_END: + deltaY = viewHeight - height - rect.top; + break; + default: + deltaY = (viewHeight - height) / 2 - rect.top; + break; + } + } else if (rect.top > 0) { + deltaY = -rect.top; + } else if (rect.bottom < viewHeight) { + deltaY = viewHeight - rect.bottom; + } + final int viewWidth = getViewWidth(); + if (width <= viewWidth) { + switch (scaleType) { + case FIT_START: + deltaX = -rect.left; + break; + case FIT_END: + deltaX = viewWidth - width - rect.left; + break; + default: + deltaX = (viewWidth - width) / 2 - rect.left; + break; + } + scrollEdge = EDGE_BOTH; + } else if (rect.left > 0) { + scrollEdge = EDGE_LEFT; + deltaX = -rect.left; + } else if (rect.right < viewWidth) { + deltaX = viewWidth - rect.right; + scrollEdge = EDGE_RIGHT; + } else { + scrollEdge = EDGE_NONE; + } + // Finally actually translate the matrix + transferMatrix.postTranslate(deltaX, deltaY); + invalidateView(); + } + + private int getViewWidth() { + return view.getWidth() - view.getPaddingLeft() - view.getPaddingRight(); + } + + private int getViewHeight() { + return view.getHeight() - view.getPaddingTop() - view.getPaddingBottom(); + } + + /** + * Helper method that 'unpacks' a Matrix and returns the required value + * + * @param matrix Matrix to unpack + * @param whichValue Which value from Matrix.M* to return + * @return returned value + */ + private float getValue(Matrix matrix, int whichValue) { + matrix.getValues(mMatrixValues); + return mMatrixValues[whichValue]; + } + + private class AnimatedZoomRunnable implements Runnable { + private final float focalX; + private final float focalY; + private final long startTime; + private final float zoomStart; + private final float zoomEnd; + + public AnimatedZoomRunnable(final float currentZoom, final float targetZoom, final float focalX, final float focalY) { + this.focalX = focalX; + this.focalY = focalY; + startTime = System.currentTimeMillis(); + zoomStart = currentZoom; + zoomEnd = targetZoom; + } + + @Override + public void run() { + float t = interpolate(); + float scale = zoomStart + t * (zoomEnd - zoomStart); + float deltaScale = scale / getScale(); + scale(deltaScale, focalX, focalY); + // We haven't hit our target scale yet, so post ourselves again + if (t < 1f) { + view.postOnAnimation(this); + } + } + + private float interpolate() { + float t = 1f * (System.currentTimeMillis() - startTime) / zoomDuration; + t = Math.min(1f, t); + t = interpolator.getInterpolation(t); + return t; + } + } + + public class FlingRunnable implements Runnable { + private final OverScroller mScroller; + private int mCurrentX; + private int mCurrentY; + private TextureView view; + + public FlingRunnable(TextureView view) { + mScroller = new OverScroller(view.getContext()); + this.view = view; + } + + public void cancelFling() { + mScroller.forceFinished(true); + } + + public void fling(int viewWidth, int viewHeight, int velocityX, int velocityY) { + final RectF rect = getDisplayRect(); + if (rect == null) { + return; + } + final int startX = Math.round(-rect.left); + final int minX; + final int maxX; + final int minY; + final int maxY; + if (viewWidth < rect.width()) { + minX = 0; + maxX = Math.round(rect.width() - viewWidth); + } else { + minX = maxX = startX; + } + final int startY = Math.round(-rect.top); + if (viewHeight < rect.height()) { + minY = 0; + maxY = Math.round(rect.height() - viewHeight); + } else { + minY = maxY = startY; + } + mCurrentX = startX; + mCurrentY = startY; + // If we actually can move, fling the scroller + if (startX != maxX || startY != maxY) { + mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, 0, 0); + } + } + + @Override + public void run() { + if (mScroller.isFinished()) { + return; // remaining post that should not be handled + } + if (mScroller.computeScrollOffset()) { + final int newX = mScroller.getCurrX(); + final int newY = mScroller.getCurrY(); + transferMatrix.postTranslate(mCurrentX - newX, mCurrentY - newY); + invalidateView(); + checkMatrixBounds(); + mCurrentX = newX; + mCurrentY = newY; + // Post On animation + view.postOnAnimation(this); + } + } + } + + public static class OnScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { + public void onFling(float startX, float startY, float velocityX, float velocityY) {} + + public void onDrag(float dx, float dy) {} + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/VideoView.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/VideoView.java new file mode 100644 index 00000000..3c1eed8f --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/component/videoview/VideoView.java @@ -0,0 +1,324 @@ +package com.tencent.qcloud.tuikit.timcommon.component.videoview; + +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.SurfaceTexture; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Build; +import android.util.AttributeSet; +import android.util.Log; +import android.view.Surface; +import android.view.TextureView; + +import androidx.annotation.Nullable; + +public class VideoView extends TextureView { + private static final String TAG = VideoView.class.getSimpleName(); + + public static final int STATE_ERROR = -1; + public static final int STATE_IDLE = 0; + public static final int STATE_PREPARING = 1; + public static final int STATE_PREPARED = 2; + public static final int STATE_PLAYING = 3; + public static final int STATE_PAUSED = 4; + public static final int STATE_PLAYBACK_COMPLETED = 5; + public static final int STATE_STOPPED = 6; + + private int mCurrentState = STATE_IDLE; + + private Context mContext; + private Surface mSurface; + private MediaPlayerProxy mMediaPlayer; + + private Uri mUri; + private int mVideoRotationDegree; + private Matrix baseMatrix = new Matrix(); + private IPlayer.OnPreparedListener mOutOnPreparedListener; + private IPlayer.OnErrorListener mOutOnErrorListener; + private IPlayer.OnCompletionListener mOutOnCompletionListener; + private IPlayer.OnSeekCompleteListener mOnSeekCompleteListener; + private IPlayer.OnPreparedListener mOnPreparedListener = new IPlayer.OnPreparedListener() { + public void onPrepared(IPlayer mp) { + mCurrentState = STATE_PREPARED; + // Video fit center + float videoHeight = mp.getVideoHeight(); + float videoWidth = mp.getVideoWidth(); + float viewHeight = getHeight() - getPaddingBottom() - getPaddingTop(); + float viewWidth = getWidth() - getPaddingLeft() - getPaddingRight(); + + float finalVideoHeight = viewHeight; + float finalVideoWidth = viewHeight * videoWidth / videoHeight; + + if (finalVideoWidth > viewWidth) { + finalVideoWidth = viewWidth; + finalVideoHeight = viewWidth * videoHeight / videoWidth; + } + + float scaleX = finalVideoWidth / viewWidth; + float scaleY = finalVideoHeight / viewHeight; + float dx = (viewWidth - finalVideoWidth) / 2; + float dy = (viewHeight - finalVideoHeight) / 2; + Matrix matrix = new Matrix(); + matrix.postScale(scaleX, scaleY); + matrix.postTranslate(dx, dy); + baseMatrix.set(matrix); + setTransform(matrix); + invalidate(); + + Log.i(TAG, "onPrepared mVideoWidth: " + videoWidth + " mVideoHeight: " + videoHeight + " mVideoRotationDegree: " + mVideoRotationDegree); + if (mOutOnPreparedListener != null) { + mOutOnPreparedListener.onPrepared(mp); + } + } + }; + + private IPlayer.OnErrorListener mOnErrorListener = new IPlayer.OnErrorListener() { + public boolean onError(IPlayer mp, int what, int extra) { + Log.w(TAG, "onError: what/extra: " + what + "/" + extra); + mCurrentState = STATE_ERROR; + stopMedia(); + if (mOutOnErrorListener != null) { + mOutOnErrorListener.onError(mp, what, extra); + } + return true; + } + }; + private IPlayer.OnInfoListener mOnInfoListener = new IPlayer.OnInfoListener() { + public void onInfo(IPlayer mp, int what, int extra) { + Log.w(TAG, "onInfo: what/extra: " + what + "/" + extra); + if (what == 10001) { // IJK: MEDIA_INFO_VIDEO_ROTATION_CHANGED + mVideoRotationDegree = extra; + setRotation(mVideoRotationDegree); + requestLayout(); + } + } + }; + private IPlayer.OnCompletionListener mOnCompletionListener = new IPlayer.OnCompletionListener() { + public void onCompletion(IPlayer mp) { + Log.i(TAG, "onCompletion"); + mCurrentState = STATE_PLAYBACK_COMPLETED; + if (mOutOnCompletionListener != null) { + mOutOnCompletionListener.onCompletion(mp); + } + } + }; + private IPlayer.OnVideoSizeChangedListener mOnVideoSizeChangedListener = new IPlayer.OnVideoSizeChangedListener() { + @Override + public void onVideoSizeChanged(IPlayer mp, int width, int height) { + // TUIChatLog.i(TAG, "onVideoSizeChanged width: " + width + " height: " + height); + } + }; + + private IPlayer.OnSeekCompleteListener onSeekCompleteListener = new IPlayer.OnSeekCompleteListener() { + @Override + public void onSeekComplete(IPlayer mp) { + if (mOnSeekCompleteListener != null) { + mOnSeekCompleteListener.onSeekComplete(mp); + } + } + }; + private SurfaceTextureListener mSurfaceTextureListener = new SurfaceTextureListener() { + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { + Log.i(TAG, "onSurfaceTextureAvailable"); + mSurface = new Surface(surface); + openVideo(); + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { + Log.i(TAG, "onSurfaceTextureSizeChanged"); + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + Log.i(TAG, "onSurfaceTextureDestroyed"); + return true; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surface) { + // TUIChatLog.i(TAG,"onSurfaceTextureUpdated"); + } + }; + + public VideoView(Context context) { + super(context); + initVideoView(context); + } + + public VideoView(Context context, AttributeSet attrs) { + super(context, attrs); + initVideoView(context); + } + + public VideoView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initVideoView(context); + } + + private void initVideoView(Context context) { + Log.i(TAG, "initVideoView"); + mContext = context; + setSurfaceTextureListener(mSurfaceTextureListener); + mCurrentState = STATE_IDLE; + + VideoGestureScaleAttacher.attach(this); + } + + public Matrix getBaseMatrix() { + return baseMatrix; + } + + public void setOnPreparedListener(IPlayer.OnPreparedListener l) { + mOutOnPreparedListener = l; + } + + public void setOnSeekCompleteListener(IPlayer.OnSeekCompleteListener l) { + mOnSeekCompleteListener = l; + } + + public void setOnErrorListener(IPlayer.OnErrorListener l) { + mOutOnErrorListener = l; + } + + public void setOnCompletionListener(IPlayer.OnCompletionListener l) { + mOutOnCompletionListener = l; + } + + public void setVideoURI(Uri uri) { + mUri = uri; + openVideo(); + } + + public void resetVideo() { + openVideo(); + } + + private void openVideo() { + if (mUri == null) { + Log.e(TAG, "openVideo: mUri is null "); + return; + } + Log.i(TAG, "openVideo: mUri: " + mUri.getPath() + " mSurface: " + mSurface); + if (mSurface == null) { + Log.e(TAG, "openVideo: mSurface is null "); + return; + } + + stopMedia(); + try { + mMediaPlayer = new MediaPlayerProxy(); + mMediaPlayer.setOnPreparedListener(mOnPreparedListener); + mMediaPlayer.setOnCompletionListener(mOnCompletionListener); + mMediaPlayer.setOnErrorListener(mOnErrorListener); + mMediaPlayer.setOnInfoListener(mOnInfoListener); + mMediaPlayer.setOnVideoSizeChangedListener(mOnVideoSizeChangedListener); + mMediaPlayer.setOnSeekCompleteListener(onSeekCompleteListener); + mMediaPlayer.setSurface(mSurface); + mMediaPlayer.setDataSource(getContext(), mUri); + mMediaPlayer.prepareAsync(); + mCurrentState = STATE_PREPARING; + } catch (Exception ex) { + Log.w(TAG, "ex = " + ex.getMessage()); + mCurrentState = STATE_ERROR; + } + } + + public boolean start() { + Log.i(TAG, "start mCurrentState:" + mCurrentState); + if (mMediaPlayer != null) { + mMediaPlayer.start(); + mCurrentState = STATE_PLAYING; + } + return true; + } + + public boolean stop() { + Log.i(TAG, "stop mCurrentState:" + mCurrentState); + stopMedia(); + return true; + } + + public boolean pause() { + Log.i(TAG, "pause mCurrentState:" + mCurrentState); + if (mMediaPlayer != null) { + mMediaPlayer.pause(); + mCurrentState = STATE_PAUSED; + } + return true; + } + + public void stopMedia() { + if (mMediaPlayer != null) { + mMediaPlayer.stop(); + mMediaPlayer.release(); + mMediaPlayer = null; + mCurrentState = STATE_IDLE; + } + } + + public int getCurrentState() { + return mCurrentState; + } + + public boolean isPlaying() { + if (mMediaPlayer != null) { + return mMediaPlayer.isPlaying(); + } + return false; + } + + public void seekTo(int progress) { + if (mMediaPlayer != null) { + mMediaPlayer.seekTo(progress); + } + } + + public boolean isPrepared() { + if (mUri == null) { + Log.e(TAG, "isPrepared: mUri is null "); + return false; + } + Log.i(TAG, "isPrepared: mUri: " + mUri.getPath() + " mSurface: " + mSurface); + if (mSurface == null) { + Log.e(TAG, "isPrepared: mSurface is null "); + return false; + } + + return true; + } + + public int getCurrentPosition() { + if (mMediaPlayer != null) { + return mMediaPlayer.getCurrentPosition(); + } + return 0; + } + + public int getDuration() { + if (mMediaPlayer != null) { + return mMediaPlayer.getDuration(); + } + return 0; + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + stopMedia(); + } + + @Override + public void setBackgroundDrawable(Drawable background) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N && background != null) { + super.setBackgroundDrawable(background); + } + } + + @Override + public void setOnClickListener(@Nullable OnClickListener l) { + super.setOnClickListener(l); + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/config/classicui/TUIConfigClassic.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/config/classicui/TUIConfigClassic.java new file mode 100644 index 00000000..5c683253 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/config/classicui/TUIConfigClassic.java @@ -0,0 +1,336 @@ +package com.tencent.qcloud.tuikit.timcommon.config.classicui; + +import static com.tencent.qcloud.tuikit.timcommon.util.TUIUtil.newDrawable; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.view.View; + +import com.tencent.qcloud.tuicore.TUIConfig; +import com.tencent.qcloud.tuicore.TUIThemeManager; +import com.tencent.qcloud.tuicore.util.ToastUtil; +import com.tencent.qcloud.tuikit.timcommon.component.highlight.HighlightPresenter; + +public class TUIConfigClassic { + private TUIConfigClassic() {} + + private static final class TUIConfigClassicHolder { + private static final TUIConfigClassic INSTANCE = new TUIConfigClassic(); + } + + private static TUIConfigClassic getInstance() { + return TUIConfigClassicHolder.INSTANCE; + } + + public static final int UNDEFINED = -1; + // message bubble + private boolean enableMessageBubbleStyle = true; + private Drawable sendBubbleBackground; + private Drawable receiveBubbleBackground; + private Drawable sendErrorBubbleBackground; + private Drawable receiveErrorBubbleBackground; + + // message style + private Drawable chatTimeBubble; + private int chatTimeFontSize = UNDEFINED; + private int chatTimeFontColor = UNDEFINED; + private Drawable defaultAvatarImage; + private int avatarRadius = UNDEFINED; + private int avatarSize = UNDEFINED; + private int receiveNickNameVisibility = UNDEFINED; + private int receiveNickNameFontSize = UNDEFINED; + private int receiveNickNameColor = UNDEFINED; + + /** + * Set the default app directory.The default dir is "file". + * @param appDir + */ + public static void setDefaultAppDir(String appDir) { + TUIConfig.setDefaultAppDir(appDir); + TUIConfig.initPath(); + } + + /** + * Set whether show the toast prompt built into TUIKit.The default value is true. + * @param enableToast + */ + public static void enableToast(boolean enableToast) { + ToastUtil.setEnableToast(enableToast); + } + + /** + * Set whether to enable language switching.The default value is false. + * @param enableLanguageSwitch + */ + public static void enableLanguageSwitch(boolean enableLanguageSwitch) { + TUIThemeManager.setEnableLanguageSwitch(enableLanguageSwitch); + } + + /** + * Switch the language of TUIKit. + * The currently supported languages are "en", "zh", and "ar". + * @param context + * @param targetLanguage + */ + public static void switchLanguageToTarget(Context context, String targetLanguage) { + TUIThemeManager.getInstance().changeLanguage(context, targetLanguage); + } + + /** + * Switch theme to target. + * The currently supported themes are THEME_LIGHT, THEME_LIVELY, and THEME_SERIOUS. + * @param context + * @param themeID + */ + public static void switchThemeToTarget(Context context, int themeID) { + TUIThemeManager.getInstance().changeTheme(context, themeID); + } + + /** + * Set whether to enable message bubble style.The default value is true. + * @param enable + */ + public static void setEnableMessageBubbleStyle(boolean enable) { + getInstance().enableMessageBubbleStyle = enable; + } + + /** + * Get whether to enable message bubble style. + * @return true is enable, false is not + */ + public static boolean isEnableMessageBubbleStyle() { + return getInstance().enableMessageBubbleStyle; + } + + /** + * Set the background of the send message bubble. + * @param drawable + */ + public static void setSendBubbleBackground(Drawable drawable) { + getInstance().sendBubbleBackground = drawable; + } + + /** + * Get the background of the send message bubble. + * @return the background + */ + public static Drawable getSendBubbleBackground() { + return newDrawable(getInstance().sendBubbleBackground); + } + + /** + * Set the background of the receive message bubble. + * @param drawable + */ + public static void setReceiveBubbleBackground(Drawable drawable) { + getInstance().receiveBubbleBackground = drawable; + } + + /** + * Get the background of the receive message bubble. + * @return the background + */ + public static Drawable getReceiveBubbleBackground() { + return newDrawable(getInstance().receiveBubbleBackground); + } + + /** + * Set the background of the receive error message bubble. + * @param receiveErrorBubbleBackground + */ + public static void setReceiveErrorBubbleBackground(Drawable receiveErrorBubbleBackground) { + getInstance().receiveErrorBubbleBackground = receiveErrorBubbleBackground; + } + + /** + * Get the background of the receive error message bubble. + * @return the background + */ + public static Drawable getReceiveErrorBubbleBackground() { + return newDrawable(getInstance().receiveErrorBubbleBackground); + } + + /** + * Set the background of the send error message bubble. + * @param sendErrorBubbleBackground + */ + public static void setSendErrorBubbleBackground(Drawable sendErrorBubbleBackground) { + getInstance().sendErrorBubbleBackground = sendErrorBubbleBackground; + } + + /** + * Get the background of the send error message bubble. + * @return the background + */ + public static Drawable getSendErrorBubbleBackground() { + return newDrawable(getInstance().sendErrorBubbleBackground); + } + + /** + * Set the light color of the message bubble in highlight status.. + * @param color + */ + public static void setBubbleHighlightLightColor(int color) { + HighlightPresenter.setHighlightLightColor(color); + } + + /** + * Set the dark color of the message bubble in highlight status.. + * @param color + */ + public static void setBubbleHighlightDarkColor(int color) { + HighlightPresenter.setHighlightDarkColor(color); + } + + /** + * Set the chat time bubble. + * @param drawable + */ + public static void setChatTimeBubble(Drawable drawable) { + getInstance().chatTimeBubble = drawable; + } + + /** + * Get the chat time bubble. + * @return + */ + public static Drawable getChatTimeBubble() { + return newDrawable(getInstance().chatTimeBubble); + } + + /** + * Set the font size of the chat time text. + * @param size + */ + public static void setChatTimeFontSize(int size) { + getInstance().chatTimeFontSize = size; + } + + /** + * Get the font size of the chat time text. + * @return + */ + public static int getChatTimeFontSize() { + return getInstance().chatTimeFontSize; + } + + /** + * Set the font color of the chat time text. + * @param color + */ + public static void setChatTimeFontColor(int color) { + getInstance().chatTimeFontColor = color; + } + + /** + * Get the font color of the chat time text. + * @return + */ + public static int getChatTimeFontColor() { + return getInstance().chatTimeFontColor; + } + + /** + * Set the default avatar image. + * @param drawable + */ + public static void setDefaultAvatarImage(Drawable drawable) { + getInstance().defaultAvatarImage = drawable; + } + + /** + * Get the default avatar image. + * @return the default avatar image + */ + public static Drawable getDefaultAvatarImage() { + return newDrawable(getInstance().defaultAvatarImage); + } + + /** + * Set the radius of the avatar in the message list. + * @param radius + */ + public static void setMessageListAvatarRadius(int radius) { + getInstance().avatarRadius = radius; + } + + /** + * Get the radius of the avatar in the message list. + * @return + */ + public static int getMessageListAvatarRadius() { + return getInstance().avatarRadius; + } + + /** + * Set whether to enable the grid avatar with the group chat.The default value is true. + * @param enableGroupGridAvatar + */ + public static void setEnableGroupGridAvatar(boolean enableGroupGridAvatar) { + TUIConfig.setEnableGroupGridAvatar(enableGroupGridAvatar); + } + + /** + * Set the avatar size in the message list. + * @param size + */ + public static void setMessageListAvatarSize(int size) { + getInstance().avatarSize = size; + } + + /** + * Get the avatar size in the message list. + * @return + */ + public static int getMessageListAvatarSize() { + return getInstance().avatarSize; + } + + /** + * Set whether to hide the nickname of the received message. + * @param hideReceiveNickName + */ + public static void setHideReceiveNickName(boolean hideReceiveNickName) { + getInstance().receiveNickNameVisibility = hideReceiveNickName ? View.GONE : View.VISIBLE; + } + + /** + * Get the visibility of the nickname of the received message. + * @return + */ + public static int getReceiveNickNameVisibility() { + return getInstance().receiveNickNameVisibility; + } + + /** + * Set the font size of the nickname of the received message. + * @param size + */ + public static void setReceiveNickNameFontSize(int size) { + getInstance().receiveNickNameFontSize = size; + } + + /** + * Get the font size of the nickname of the received message. + * @return + */ + public static int getReceiveNickNameFontSize() { + return getInstance().receiveNickNameFontSize; + } + + /** + * Set the font color of the nickname of the received message. + * @param color + */ + public static void setReceiveNickNameColor(int color) { + getInstance().receiveNickNameColor = color; + } + + /** + * Get the font color of the nickname of the received message. + * @return + */ + public static int getReceiveNickNameColor() { + return getInstance().receiveNickNameColor; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/config/minimalistui/TUIConfigMinimalist.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/config/minimalistui/TUIConfigMinimalist.java new file mode 100644 index 00000000..d47dc8ff --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/config/minimalistui/TUIConfigMinimalist.java @@ -0,0 +1,273 @@ +package com.tencent.qcloud.tuikit.timcommon.config.minimalistui; + +import static com.tencent.qcloud.tuikit.timcommon.util.TUIUtil.newDrawable; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import com.tencent.qcloud.tuicore.TUIConfig; +import com.tencent.qcloud.tuicore.TUIThemeManager; +import com.tencent.qcloud.tuicore.util.ToastUtil; +import com.tencent.qcloud.tuikit.timcommon.component.highlight.HighlightPresenter; + +public class TUIConfigMinimalist { + private TUIConfigMinimalist() {} + + private static final class TUIConfigMinimalistHolder { + private static final TUIConfigMinimalist INSTANCE = new TUIConfigMinimalist(); + } + + private static TUIConfigMinimalist getInstance() { + return TUIConfigMinimalistHolder.INSTANCE; + } + + public static final int UNDEFINED = -1; + // message bubble + private boolean enableMessageBubbleStyle = true; + private Drawable sendBubbleBackground; + private Drawable sendLastBubbleBackground; + private Drawable receiveBubbleBackground; + private Drawable receiveLastBubbleBackground; + + // message style + private Drawable chatTimeBubble; + private int chatTimeFontSize = UNDEFINED; + private int chatTimeFontColor = UNDEFINED; + private Drawable defaultAvatarImage; + private int avatarRadius = UNDEFINED; + private int avatarSize = UNDEFINED; + + /** + * Set the default app directory.The default dir is "file". + * @param appDir + */ + public static void setDefaultAppDir(String appDir) { + TUIConfig.setDefaultAppDir(appDir); + TUIConfig.initPath(); + } + + /** + * Set whether show the toast prompt built into TUIKit.The default value is true. + * @param enableToast + */ + public static void enableToast(boolean enableToast) { + ToastUtil.setEnableToast(enableToast); + } + + /** + * Set whether to enable language switching.The default value is false. + * @param enableLanguageSwitch + */ + public static void enableLanguageSwitch(boolean enableLanguageSwitch) { + TUIThemeManager.setEnableLanguageSwitch(enableLanguageSwitch); + } + + /** + * Switch the language of TUIKit. + * The currently supported languages are "en", "zh", and "ar". + * @param context + * @param targetLanguage + */ + public static void switchLanguageToTarget(Context context, String targetLanguage) { + TUIThemeManager.getInstance().changeLanguage(context, targetLanguage); + } + + /** + * Set whether to enable message bubble style.The default value is true. + * @param enable + */ + public static void setEnableMessageBubbleStyle(boolean enable) { + getInstance().enableMessageBubbleStyle = enable; + } + + /** + * Get whether to enable message bubble style. + * @return true is enable, false is not + */ + public static boolean isEnableMessageBubbleStyle() { + return getInstance().enableMessageBubbleStyle; + } + + /** + * Set the background of the send message's bubble in consecutive messages. + * @param drawable + */ + public static void setSendBubbleBackground(Drawable drawable) { + getInstance().sendBubbleBackground = drawable; + } + + /** + * Get the background of the send message's bubble in consecutive messages. + * @return the background + */ + public static Drawable getSendBubbleBackground() { + return newDrawable(getInstance().sendBubbleBackground); + } + + /** + * Set the background of the receive message's bubble in consecutive messages. + * @param drawable + */ + public static void setReceiveBubbleBackground(Drawable drawable) { + getInstance().receiveBubbleBackground = drawable; + } + + /** + * Get the background of the receive message's bubble in consecutive messages. + * @return the background + */ + public static Drawable getReceiveBubbleBackground() { + return newDrawable(getInstance().receiveBubbleBackground); + } + + /** + * Set the background image of the last sent message's bubble in consecutive messages. + * @param drawable + */ + public static void setSendLastBubbleBackground(Drawable drawable) { + getInstance().sendLastBubbleBackground = drawable; + } + + /** + * Get the background image of the last sent message's bubble in consecutive messages. + * @return drawable + */ + public static Drawable getSendLastBubbleBackground() { + return newDrawable(getInstance().sendLastBubbleBackground); + } + + /** + * Set the background image of the last received message's bubble in consecutive messages. + * @param drawable + */ + public static void setReceiveLastBubbleBackground(Drawable drawable) { + getInstance().receiveLastBubbleBackground = drawable; + } + + /** + * Get the background image of the last received message's bubble in consecutive messages. + * @return drawable + */ + public static Drawable getReceiveLastBubbleBackground() { + return newDrawable(getInstance().receiveLastBubbleBackground); + } + + /** + * Set the light color of the message bubble in highlight status.. + * @param color + */ + public static void setBubbleHighlightLightColor(int color) { + HighlightPresenter.setHighlightLightColor(color); + } + + /** + * Set the dark color of the message bubble in highlight status.. + * @param color + */ + public static void setBubbleHighlightDarkColor(int color) { + HighlightPresenter.setHighlightDarkColor(color); + } + + /** + * Set the chat time bubble. + * @param drawable + */ + public static void setChatTimeBubble(Drawable drawable) { + getInstance().chatTimeBubble = drawable; + } + + /** + * Get the chat time bubble. + * @return + */ + public static Drawable getChatTimeBubble() { + return newDrawable(getInstance().chatTimeBubble); + } + + /** + * Set the font size of the chat time text. + * @param size + */ + public static void setChatTimeFontSize(int size) { + getInstance().chatTimeFontSize = size; + } + + /** + * Get the font size of the chat time text. + * @return + */ + public static int getChatTimeFontSize() { + return getInstance().chatTimeFontSize; + } + + /** + * Set the font color of the chat time text. + * @param color + */ + public static void setChatTimeFontColor(int color) { + getInstance().chatTimeFontColor = color; + } + + /** + * Get the font color of the chat time text. + * @return + */ + public static int getChatTimeFontColor() { + return getInstance().chatTimeFontColor; + } + + /** + * Set the default avatar image. + * @param drawable + */ + public static void setDefaultAvatarImage(Drawable drawable) { + getInstance().defaultAvatarImage = drawable; + } + + /** + * Get the default avatar image. + * @return the default avatar image + */ + public static Drawable getDefaultAvatarImage() { + return newDrawable(getInstance().defaultAvatarImage); + } + + /** + * Set the radius of the avatar in the message list. + * @param radius + */ + public static void setMessageListAvatarRadius(int radius) { + getInstance().avatarRadius = radius; + } + + /** + * Get the radius of the avatar in the message list. + * @return + */ + public static int getMessageListAvatarRadius() { + return getInstance().avatarRadius; + } + + /** + * Set whether to enable the grid avatar with the group chat.The default value is true. + * @param enableGroupGridAvatar + */ + public static void setEnableGroupGridAvatar(boolean enableGroupGridAvatar) { + TUIConfig.setEnableGroupGridAvatar(enableGroupGridAvatar); + } + + /** + * Set the avatar size in the message list. + * @param size + */ + public static void setMessageListAvatarSize(int size) { + getInstance().avatarSize = size; + } + + /** + * Get the avatar size in the message list. + * @return + */ + public static int getMessageListAvatarSize() { + return getInstance().avatarSize; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/ChatInputMoreListener.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/ChatInputMoreListener.java new file mode 100644 index 00000000..46ad8ff7 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/ChatInputMoreListener.java @@ -0,0 +1,10 @@ +package com.tencent.qcloud.tuikit.timcommon.interfaces; + +import com.tencent.qcloud.tuikit.timcommon.bean.TUIMessageBean; +import com.tencent.qcloud.tuikit.timcommon.component.interfaces.IUIKitCallback; + +public abstract class ChatInputMoreListener { + public String sendMessage(TUIMessageBean msg, IUIKitCallback callback) { + return null; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/HighlightListener.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/HighlightListener.java new file mode 100644 index 00000000..938b36d1 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/HighlightListener.java @@ -0,0 +1,10 @@ +package com.tencent.qcloud.tuikit.timcommon.interfaces; + +public interface HighlightListener { + + void onHighlightStart(); + + void onHighlightEnd(); + + void onHighlightUpdate(int color); +} \ No newline at end of file diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/ICommonMessageAdapter.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/ICommonMessageAdapter.java new file mode 100644 index 00000000..afee786b --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/ICommonMessageAdapter.java @@ -0,0 +1,15 @@ +package com.tencent.qcloud.tuikit.timcommon.interfaces; + +import com.tencent.qcloud.tuikit.timcommon.bean.TUIMessageBean; + +public interface ICommonMessageAdapter { + TUIMessageBean getItem(int position); + + TUIMessageBean getFirstMessageBean(); + + TUIMessageBean getLastMessageBean(); + + void onItemRefresh(TUIMessageBean messageBean); + + UserFaceUrlCache getUserFaceUrlCache(); +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/IMessageProperties.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/IMessageProperties.java new file mode 100644 index 00000000..ff839420 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/IMessageProperties.java @@ -0,0 +1,295 @@ +package com.tencent.qcloud.tuikit.timcommon.interfaces; + +import android.graphics.drawable.Drawable; + +public interface IMessageProperties { + + /** + * + * Get default avatar + * + * @return + */ + int getAvatar(); + + /** + * + * Set the default avatar, the default is the same as the avatar on the left and right + * + * @param resId + */ + void setAvatar(int resId); + + /** + * + * Get avatar rounded corners + * + * @return + */ + int getAvatarRadius(); + + /** + * + * Set avatar rounded corners + * + * @param radius + */ + void setAvatarRadius(int radius); + + /** + * + * Get avatar size + * + * @return + */ + int[] getAvatarSize(); + + /** + * + * Set avatar size + * + * @param size + */ + void setAvatarSize(int[] size); + + /** + * + * + * Get nickname text size + * + * @return + */ + int getNameFontSize(); + + /** + * + * Set nickname text size + * + * @param size + */ + void setNameFontSize(int size); + + /** + * + * Get nickname text color + * + * @return + */ + int getNameFontColor(); + + /** + * + * Set nickname text color + * + * @param color + */ + void setNameFontColor(int color); + + /** + * + * Get the display status of the nickname on the left + * + * @return + */ + int getLeftNameVisibility(); + + /** + * + * Set the display status of the nickname on the left + * + * @param visibility + */ + void setLeftNameVisibility(int visibility); + + /** + * + * Get the display status of the nickname on the right + * + * @return + */ + int getRightNameVisibility(); + + /** + * + * Set the display status of the nickname on the right + * + * @param visibility + */ + void setRightNameVisibility(int visibility); + + /** + * + * Get the background of the chat bubble on the right + * + * @return + */ + Drawable getRightBubble(); + + /** + * + * Set the background of the chat bubble on the right + * + * @param drawable + */ + void setRightBubble(Drawable drawable); + + /** + * + * Get the background of the left chat bubble + * + * @return + */ + Drawable getLeftBubble(); + + /** + * + * Set the background of the left chat bubble + * + * @param drawable + */ + void setLeftBubble(Drawable drawable); + + /** + * + * Get chat content font size + * + * @return + */ + int getChatContextFontSize(); + + /** + * + * Set chat content font size + * + * @param size + */ + void setChatContextFontSize(int size); + + /** + * + * Get the font color of the chat content on the right + * + * @return + */ + int getRightChatContentFontColor(); + + /** + * + * Set the font color of the chat content on the right + * + * @param color + */ + void setRightChatContentFontColor(int color); + + /** + * + * Get the font color of the chat content on the left + * + * @return + */ + int getLeftChatContentFontColor(); + + /** + * + * Set the font color of the chat content on the left + * + * @param color + */ + void setLeftChatContentFontColor(int color); + + /** + * + * Get the context of the chat time + * + * @return + */ + Drawable getChatTimeBubble(); + + /** + * + * Set the context of the chat time + * + * @param drawable + */ + void setChatTimeBubble(Drawable drawable); + + /** + * + * Get the text size of the chat time + * + * @return + */ + int getChatTimeFontSize(); + + /** + * + * Set the text size of the chat time + * + * @param size + */ + void setChatTimeFontSize(int size); + + /** + * + * Get the font color of chat time + * + * @return + */ + int getChatTimeFontColor(); + + /** + * + * Set the font color of chat time + * + * @param color + */ + void setChatTimeFontColor(int color); + + /** + * + * Get context for chat alerts + * + * @return + */ + Drawable getTipsMessageBubble(); + + /** + * + * Set context for chat alerts + * + * @param drawable + */ + void setTipsMessageBubble(Drawable drawable); + + /** + * + * Get the text size of the chat prompt message + * + * @return + */ + int getTipsMessageFontSize(); + + /** + * + * Set the text size of the chat prompt message + * + * @param size + */ + void setTipsMessageFontSize(int size); + + /** + * + * Get the text color of the chat prompt message + * + * @return + */ + int getTipsMessageFontColor(); + + /** + * + * Set the text color of the chat prompt message + * + * @param color + */ + void setTipsMessageFontColor(int color); +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/OnChatPopActionClickListener.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/OnChatPopActionClickListener.java new file mode 100644 index 00000000..868c7169 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/OnChatPopActionClickListener.java @@ -0,0 +1,25 @@ +package com.tencent.qcloud.tuikit.timcommon.interfaces; + +import com.tencent.qcloud.tuikit.timcommon.bean.TUIMessageBean; + +public abstract class OnChatPopActionClickListener { + public void onCopyClick(TUIMessageBean msg) {} + + public void onSendMessageClick(TUIMessageBean msg, boolean retry) {} + + public void onDeleteMessageClick(TUIMessageBean msg) {} + + public void onRevokeMessageClick(TUIMessageBean msg) {} + + public void onMultiSelectMessageClick(TUIMessageBean msg) {} + + public void onForwardMessageClick(TUIMessageBean msg) {} + + public void onReplyMessageClick(TUIMessageBean msg) {} + + public void onQuoteMessageClick(TUIMessageBean msg) {} + + public void onInfoMessageClick(TUIMessageBean msg) {} + + public void onSpeakerModeSwitchClick(TUIMessageBean msg) {} +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/OnFaceInputListener.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/OnFaceInputListener.java new file mode 100644 index 00000000..a9072547 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/OnFaceInputListener.java @@ -0,0 +1,14 @@ +package com.tencent.qcloud.tuikit.timcommon.interfaces; + +import com.tencent.qcloud.tuikit.timcommon.bean.ChatFace; + +public interface OnFaceInputListener { + + void onEmojiClicked(String emojiKey); + + void onDeleteClicked(); + + void onSendClicked(); + + void onFaceClicked(ChatFace chatFace); +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/OnItemClickListener.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/OnItemClickListener.java new file mode 100644 index 00000000..74a207ed --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/OnItemClickListener.java @@ -0,0 +1,29 @@ +package com.tencent.qcloud.tuikit.timcommon.interfaces; + +import android.view.View; + +import com.tencent.qcloud.tuikit.timcommon.bean.TUIMessageBean; + +public abstract class OnItemClickListener { + public void onMessageLongClick(View view, TUIMessageBean messageBean) {} + + public void onMessageClick(View view, TUIMessageBean messageBean) {} + + public void onUserIconClick(View view, TUIMessageBean messageBean) {} + + public void onUserIconLongClick(View view, TUIMessageBean messageBean) {} + + public void onReEditRevokeMessage(View view, TUIMessageBean messageBean) {} + + public void onRecallClick(View view, TUIMessageBean messageBean) {} + + public void onReplyMessageClick(View view, TUIMessageBean messageBean) {} + + public void onReplyDetailClick(TUIMessageBean messageBean) {} + + public void onSendFailBtnClick(View view, TUIMessageBean messageBean) {} + + public void onTextSelected(View view, int position, TUIMessageBean messageBean) {} + + public void onMessageReadStatusClick(View view, TUIMessageBean messageBean) {} +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/UserFaceUrlCache.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/UserFaceUrlCache.java new file mode 100644 index 00000000..568c54f9 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/interfaces/UserFaceUrlCache.java @@ -0,0 +1,8 @@ +package com.tencent.qcloud.tuikit.timcommon.interfaces; + +public interface UserFaceUrlCache { + + String getCachedFaceUrl(String userID); + + void pushFaceUrl(String userID, String faceUrl); +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/MessageBaseHolder.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/MessageBaseHolder.java new file mode 100644 index 00000000..0fcd3816 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/MessageBaseHolder.java @@ -0,0 +1,240 @@ +package com.tencent.qcloud.tuikit.timcommon.minimalistui.widget.message; + +import android.content.Context; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.view.View; +import android.widget.CheckBox; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; +import androidx.recyclerview.widget.RecyclerView; +import com.tencent.qcloud.tuicore.TUIConfig; +import com.tencent.qcloud.tuicore.TUIThemeManager; +import com.tencent.qcloud.tuikit.timcommon.R; +import com.tencent.qcloud.tuikit.timcommon.bean.TUIMessageBean; +import com.tencent.qcloud.tuikit.timcommon.component.highlight.HighlightPresenter; +import com.tencent.qcloud.tuikit.timcommon.config.minimalistui.TUIConfigMinimalist; +import com.tencent.qcloud.tuikit.timcommon.interfaces.HighlightListener; +import com.tencent.qcloud.tuikit.timcommon.interfaces.ICommonMessageAdapter; +import com.tencent.qcloud.tuikit.timcommon.interfaces.OnItemClickListener; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; + +public abstract class MessageBaseHolder extends RecyclerView.ViewHolder { + public ICommonMessageAdapter mAdapter; + protected OnItemClickListener onItemClickListener; + + public FrameLayout msgContentFrame; + public LinearLayout msgArea; + public LinearLayout msgAreaAndReply; + public FrameLayout reactionArea; + public CheckBox mMutiSelectCheckBox; + public View mContentLayout; + + public TextView chatTimeText; + + protected boolean floatMode = false; + + protected boolean isLayoutOnStart = true; + private HighlightListener highlightListener; + protected TUIMessageBean currentMessageBean; + + public MessageBaseHolder(View itemView) { + super(itemView); + msgContentFrame = itemView.findViewById(R.id.msg_content_fl); + reactionArea = itemView.findViewById(R.id.message_reaction_area); + msgArea = itemView.findViewById(R.id.msg_area); + msgAreaAndReply = itemView.findViewById(R.id.msg_area_and_reply); + mMutiSelectCheckBox = itemView.findViewById(R.id.select_checkbox); + chatTimeText = itemView.findViewById(R.id.message_top_time_tv); + mContentLayout = itemView.findViewById(R.id.message_content_layout); + initVariableLayout(); + } + + public abstract int getVariableLayout(); + + private void setVariableLayout(int resId) { + if (msgContentFrame.getChildCount() == 0) { + View.inflate(itemView.getContext(), resId, msgContentFrame); + } + } + + private void initVariableLayout() { + if (getVariableLayout() != 0) { + setVariableLayout(getVariableLayout()); + } + } + + public void setAdapter(ICommonMessageAdapter adapter) { + mAdapter = adapter; + } + + public void setOnItemClickListener(OnItemClickListener listener) { + this.onItemClickListener = listener; + } + + public OnItemClickListener getOnItemClickListener() { + return this.onItemClickListener; + } + + public void layoutViews(final TUIMessageBean msg, final int position) { + currentMessageBean = msg; + registerHighlightListener(msg.getId()); + + setChatTimeStyle(); + + if (position > 1) { + TUIMessageBean last = mAdapter.getItem(position - 1); + if (last != null) { + if (msg.getMessageTime() - last.getMessageTime() >= 5 * 60) { + chatTimeText.setVisibility(View.VISIBLE); + chatTimeText.setText(getTimeFormatText(new Date(msg.getMessageTime() * 1000))); + } else { + chatTimeText.setVisibility(View.GONE); + } + } + } else { + chatTimeText.setVisibility(View.VISIBLE); + chatTimeText.setText(getTimeFormatText(new Date(msg.getMessageTime() * 1000))); + } + } + + private void setChatTimeStyle() { + Drawable chatTimeBubble = TUIConfigMinimalist.getChatTimeBubble(); + if (chatTimeBubble != null) { + chatTimeText.setBackground(chatTimeBubble); + } + int chatTimeFontColor = TUIConfigMinimalist.getChatTimeFontColor(); + if (chatTimeFontColor != TUIConfigMinimalist.UNDEFINED) { + chatTimeText.setTextColor(chatTimeFontColor); + } + int chatTimeFontSize = TUIConfigMinimalist.getChatTimeFontSize(); + if (chatTimeFontSize != TUIConfigMinimalist.UNDEFINED) { + chatTimeText.setTextSize(chatTimeFontSize); + } + } + + private void registerHighlightListener(String msgID) { + if (highlightListener == null) { + highlightListener = new HighlightListener() { + @Override + public void onHighlightStart() {} + + @Override + public void onHighlightEnd() { + clearHighLightBackground(); + } + + @Override + public void onHighlightUpdate(int color) { + setHighLightBackground(color); + } + }; + } + HighlightPresenter.registerHighlightListener(msgID, highlightListener); + } + + public void onRecycled() { + if (currentMessageBean != null) { + HighlightPresenter.unregisterHighlightListener(currentMessageBean.getId()); + } + } + + public static String getTimeFormatText(Date date) { + if (date == null) { + return ""; + } + Context context = TUIConfig.getAppContext(); + Locale locale; + if (context == null) { + locale = Locale.getDefault(); + } else { + locale = TUIThemeManager.getInstance().getLocale(context); + } + String timeText; + Calendar dayCalendar = Calendar.getInstance(); + dayCalendar.set(Calendar.HOUR_OF_DAY, 0); + dayCalendar.set(Calendar.MINUTE, 0); + dayCalendar.set(Calendar.SECOND, 0); + dayCalendar.set(Calendar.MILLISECOND, 0); + Calendar weekCalendar = Calendar.getInstance(); + weekCalendar.setFirstDayOfWeek(Calendar.SUNDAY); + weekCalendar.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY); + weekCalendar.set(Calendar.HOUR_OF_DAY, 0); + weekCalendar.set(Calendar.MINUTE, 0); + weekCalendar.set(Calendar.SECOND, 0); + weekCalendar.set(Calendar.MILLISECOND, 0); + Calendar yearCalendar = Calendar.getInstance(); + yearCalendar.set(Calendar.DAY_OF_YEAR, 1); + yearCalendar.set(Calendar.HOUR_OF_DAY, 0); + yearCalendar.set(Calendar.MINUTE, 0); + yearCalendar.set(Calendar.SECOND, 0); + yearCalendar.set(Calendar.MILLISECOND, 0); + long dayStartTimeInMillis = dayCalendar.getTimeInMillis(); + long weekStartTimeInMillis = weekCalendar.getTimeInMillis(); + long yearStartTimeInMillis = yearCalendar.getTimeInMillis(); + long outTimeMillis = date.getTime(); + if (outTimeMillis < yearStartTimeInMillis) { + timeText = String.format(Locale.US, "%1$tY/%1$tm/%1$td", date); + } else if (outTimeMillis < weekStartTimeInMillis) { + timeText = String.format(Locale.US, "%1$tm/%1$td", date); + } else if (outTimeMillis < dayStartTimeInMillis) { + timeText = String.format(locale, "%tA", date); + } else { + timeText = context.getResources().getString(R.string.chat_time_today); + } + return timeText; + } + + public void setFloatMode(boolean floatMode) { + this.floatMode = floatMode; + } + + protected boolean isShowAvatar(TUIMessageBean messageBean) { + return false; + } + + public void setMessageBubbleZeroPadding() { + if (msgArea == null) { + return; + } + msgArea.setPaddingRelative(0, 0, 0, 0); + } + + public void setMessageBubbleBackground(int resID) { + if (msgArea == null) { + return; + } + msgArea.setBackgroundResource(resID); + } + + public void setMessageBubbleBackground(Drawable drawable) { + if (msgArea == null) { + return; + } + msgArea.setBackground(drawable); + } + + public Drawable getMessageBubbleBackground() { + if (msgArea == null) { + return null; + } + return msgArea.getBackground(); + } + + public void setHighLightBackground(int color) { + Drawable drawable = getMessageBubbleBackground(); + if (drawable != null) { + drawable.setColorFilter(color, PorterDuff.Mode.SRC_IN); + } + } + + public void clearHighLightBackground() { + Drawable drawable = getMessageBubbleBackground(); + if (drawable != null) { + drawable.setColorFilter(null); + } + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/MessageContentHolder.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/MessageContentHolder.java new file mode 100644 index 00000000..72f3d41e --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/MessageContentHolder.java @@ -0,0 +1,714 @@ +package com.tencent.qcloud.tuikit.timcommon.minimalistui.widget.message; + +import android.app.Activity; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.RecyclerView; +import com.bumptech.glide.Glide; +import com.bumptech.glide.RequestBuilder; +import com.bumptech.glide.load.resource.bitmap.RoundedCorners; +import com.tencent.imsdk.v2.V2TIMManager; +import com.tencent.imsdk.v2.V2TIMMessage; +import com.tencent.imsdk.v2.V2TIMUserFullInfo; +import com.tencent.imsdk.v2.V2TIMValueCallback; +import com.tencent.qcloud.tuicore.TUIConstants; +import com.tencent.qcloud.tuicore.TUICore; +import com.tencent.qcloud.tuicore.TUIThemeManager; +import com.tencent.qcloud.tuikit.timcommon.R; +import com.tencent.qcloud.tuikit.timcommon.bean.MessageRepliesBean; +import com.tencent.qcloud.tuikit.timcommon.bean.TUIMessageBean; +import com.tencent.qcloud.tuikit.timcommon.component.UnreadCountTextView; +import com.tencent.qcloud.tuikit.timcommon.config.minimalistui.TUIConfigMinimalist; +import com.tencent.qcloud.tuikit.timcommon.util.DateTimeUtil; +import com.tencent.qcloud.tuikit.timcommon.util.ScreenUtil; +import com.tencent.qcloud.tuikit.timcommon.util.TUIUtil; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public abstract class MessageContentHolder extends MessageBaseHolder { + protected static final int READ_STATUS_UNREAD = 1; + protected static final int READ_STATUS_PART_READ = 2; + protected static final int READ_STATUS_ALL_READ = 3; + protected static final int READ_STATUS_HIDE = 4; + protected static final int READ_STATUS_SENDING = 5; + + public ImageView leftUserIcon; + public ImageView rightUserIcon; + public TextView usernameText; + public LinearLayout msgContentLinear; + public ImageView messageStatusImage; + public ImageView fileStatusImage; + public UnreadCountTextView unreadAudioText; + public LinearLayout extraInfoArea; + protected FrameLayout bottomContentFrameLayout; + + public boolean isForwardMode = false; + public boolean isMessageDetailMode = false; + public boolean isMultiSelectMode = false; + public boolean isOptimize = true; + public boolean isShowSelfAvatar = false; + protected boolean isShowAvatar = true; + + protected TimeInLineTextLayout timeInLineTextLayout; + protected MinimalistMessageLayout rootLayout; + protected ReplyPreviewView replyPreviewView; + + private List mDataSource = new ArrayList<>(); + + // Whether to display the bottom content. The merged-forwarded message details activity does not display the bottom content. + protected boolean isNeedShowBottom = true; + protected boolean isShowRead = false; + private Fragment fragment; + private RecyclerView recyclerView; + + public MessageContentHolder(View itemView) { + super(itemView); + rootLayout = (MinimalistMessageLayout) itemView; + leftUserIcon = itemView.findViewById(R.id.left_user_icon_view); + rightUserIcon = itemView.findViewById(R.id.right_user_icon_view); + usernameText = itemView.findViewById(R.id.user_name_tv); + msgContentLinear = itemView.findViewById(R.id.msg_content_ll); + messageStatusImage = itemView.findViewById(R.id.message_status_iv); + fileStatusImage = itemView.findViewById(R.id.file_status_iv); + unreadAudioText = itemView.findViewById(R.id.unread_audio_text); + replyPreviewView = itemView.findViewById(R.id.msg_reply_preview); + extraInfoArea = itemView.findViewById(R.id.extra_info_area); + bottomContentFrameLayout = itemView.findViewById(R.id.bottom_content_fl); + } + + public void setFragment(Fragment fragment) { + this.fragment = fragment; + } + + public void setRecyclerView(RecyclerView recyclerView) { + this.recyclerView = recyclerView; + } + + public void setDataSource(List dataSource) { + if (dataSource == null || dataSource.isEmpty()) { + mDataSource = null; + } + + List mediaSource = new ArrayList<>(); + for (TUIMessageBean messageBean : dataSource) { + int type = messageBean.getMsgType(); + if (type == V2TIMMessage.V2TIM_ELEM_TYPE_IMAGE || type == V2TIMMessage.V2TIM_ELEM_TYPE_VIDEO) { + mediaSource.add(messageBean); + } + } + mDataSource = mediaSource; + } + + public List getDataSource() { + return mDataSource; + } + + @Override + public void layoutViews(final TUIMessageBean msg, final int position) { + Context context = itemView.getContext(); + if (TUIUtil.isActivityDestroyed(context)) { + return; + } + + super.layoutViews(msg, position); + setLayoutAlignment(msg); + setIsShowAvatar(msg, position); + setMessageGravity(); + setUserNameText(msg); + setMessageBubbleBackground(); + setMessageStatusImage(msg); + setMessageTimeVisibility(); + setAvatarVisibility(); + setEventListener(msg); + + msgContentLinear.setVisibility(View.VISIBLE); + + if (!isForwardMode && !isMessageDetailMode) { + setTimeInLineStatus(msg); + setShowReadStatusClickListener(msg); + } + if (timeInLineTextLayout != null && timeInLineTextLayout.getTextView() != null) { + if (isMultiSelectMode) { + timeInLineTextLayout.getTextView().setActivated(false); + } else { + timeInLineTextLayout.getTextView().setActivated(true); + } + } + + if (timeInLineTextLayout != null) { + timeInLineTextLayout.setTimeText(DateTimeUtil.getHMTimeString(new Date(msg.getMessageTime() * 1000))); + } + + extraInfoArea.setVisibility(View.GONE); + setReplyContent(msg); + setReactContent(msg); + if (isNeedShowBottom) { + setBottomContent(msg); + } + setMessageBubbleDefaultPadding(); + if (floatMode) { + itemView.setPaddingRelative(0, 0, 0, 0); + leftUserIcon.setVisibility(View.GONE); + rightUserIcon.setVisibility(View.GONE); + usernameText.setVisibility(View.GONE); + replyPreviewView.setVisibility(View.GONE); + reactionArea.setVisibility(View.GONE); + } + if (isMessageDetailMode) { + replyPreviewView.setVisibility(View.GONE); + } + + optimizePadding(position, msg); + loadAvatar(msg); + layoutVariableViews(msg, position); + } + + private void setEventListener(TUIMessageBean msg) { + if (!isForwardMode && !isMessageDetailMode) { + if (onItemClickListener != null) { + msgContentFrame.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + onItemClickListener.onMessageLongClick(msgArea, msg); + return true; + } + }); + + msgArea.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + onItemClickListener.onMessageLongClick(msgArea, msg); + return true; + } + }); + + leftUserIcon.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + onItemClickListener.onUserIconClick(view, msg); + } + }); + leftUserIcon.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View view) { + onItemClickListener.onUserIconLongClick(view, msg); + return true; + } + }); + rightUserIcon.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + onItemClickListener.onUserIconClick(view, msg); + } + }); + } + + if (msg.getStatus() != TUIMessageBean.MSG_STATUS_SEND_FAIL) { + msgContentFrame.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (onItemClickListener != null) { + onItemClickListener.onMessageClick(msgContentFrame, msg); + } + } + }); + } + } + } + + private void setMessageTimeVisibility() { + if (isForwardMode || floatMode) { + chatTimeText.setVisibility(View.GONE); + } + } + + private void setLayoutAlignment(TUIMessageBean msg) { + if (isForwardMode || isMessageDetailMode) { + isLayoutOnStart = true; + } else { + if (msg.isSelf()) { + isLayoutOnStart = false; + } else { + isLayoutOnStart = true; + } + } + + if (isForwardMode || isMessageDetailMode) { + rootLayout.setIsStart(true); + msgContentLinear.removeView(msgAreaAndReply); + msgContentLinear.addView(msgAreaAndReply); + } else { + if (msg.isSelf()) { + msgContentLinear.removeView(msgAreaAndReply); + msgContentLinear.addView(msgAreaAndReply); + } else { + msgContentLinear.removeView(msgAreaAndReply); + msgContentLinear.addView(msgAreaAndReply, 0); + } + } + setGravity(isLayoutOnStart); + rootLayout.setIsStart(isLayoutOnStart); + } + + private void setUserNameText(TUIMessageBean msg) { + if (isForwardMode || isMessageDetailMode) { + usernameText.setVisibility(View.GONE); + } else { + if (msg.isSelf()) { + usernameText.setVisibility(View.GONE); + } else { + if (msg.isGroup()) { + usernameText.setVisibility(View.GONE); + } else { + usernameText.setVisibility(View.GONE); + } + } + } + + usernameText.setText(msg.getUserDisplayName()); + } + + public void setIsShowAvatar(TUIMessageBean msg, int position) { + isShowAvatar = true; + if (mAdapter == null) { + return; + } + if (isMessageDetailMode || !isOptimize) { + return; + } + TUIMessageBean nextMessage = mAdapter.getItem(position + 1); + if (nextMessage != null) { + if (TextUtils.equals(msg.getSender(), nextMessage.getSender())) { + boolean longPeriod = nextMessage.getMessageTime() - msg.getMessageTime() >= 5 * 60; + if (!isShowAvatar(nextMessage) && nextMessage.getStatus() != TUIMessageBean.MSG_STATUS_REVOKE && !longPeriod) { + isShowAvatar = false; + } + } + } + } + + public void setMessageBubbleBackground() { + if (!TUIConfigMinimalist.isEnableMessageBubbleStyle()) { + setMessageBubbleBackground(null); + return; + } + Drawable sendBubble = TUIConfigMinimalist.getSendBubbleBackground(); + Drawable receiveBubble = TUIConfigMinimalist.getReceiveBubbleBackground(); + Drawable sendLastBubble = TUIConfigMinimalist.getSendLastBubbleBackground(); + Drawable receiveLastBubble = TUIConfigMinimalist.getReceiveLastBubbleBackground(); + if (isShowAvatar) { + if (isLayoutOnStart) { + if (receiveLastBubble != null) { + setMessageBubbleBackground(receiveLastBubble); + } else { + setMessageBubbleBackground(R.drawable.chat_message_popup_stroke_border_left); + } + } else { + if (sendLastBubble != null) { + setMessageBubbleBackground(sendLastBubble); + } else { + setMessageBubbleBackground(R.drawable.chat_message_popup_fill_border_right); + } + } + } else { + if (isLayoutOnStart) { + if (receiveBubble != null) { + setMessageBubbleBackground(receiveBubble); + } else { + setMessageBubbleBackground(R.drawable.chat_message_popup_stroke_border); + } + } else { + if (sendBubble != null) { + setMessageBubbleBackground(sendBubble); + } else { + setMessageBubbleBackground(R.drawable.chat_message_popup_fill_border); + } + } + } + } + + public void setMessageStatusImage(TUIMessageBean msg) { + if (isForwardMode || isMessageDetailMode || floatMode) { + messageStatusImage.setVisibility(View.GONE); + } else { + if (msg.hasRiskContent()) { + messageStatusImage.setVisibility(View.VISIBLE); + } else if (msg.getStatus() == TUIMessageBean.MSG_STATUS_SEND_FAIL) { + messageStatusImage.setVisibility(View.VISIBLE); + messageStatusImage.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (onItemClickListener != null) { + onItemClickListener.onSendFailBtnClick(messageStatusImage, msg); + } + } + }); + } else { + messageStatusImage.setVisibility(View.GONE); + } + } + } + + public void setBottomContent(TUIMessageBean msg) { + HashMap param = new HashMap<>(); + param.put(TUIConstants.TUIChat.MESSAGE_BEAN, msg); + param.put(TUIConstants.TUIChat.CHAT_RECYCLER_VIEW, recyclerView); + param.put(TUIConstants.TUIChat.FRAGMENT, fragment); + + TUICore.raiseExtension(TUIConstants.TUIChat.Extension.MessageBottom.MINIMALIST_EXTENSION_ID, bottomContentFrameLayout, param); + } + + private void loadAvatar(TUIMessageBean msg) { + Drawable drawable = TUIConfigMinimalist.getDefaultAvatarImage(); + if (drawable != null) { + setupAvatar(drawable); + return; + } + + if (msg.isUseMsgReceiverAvatar() && mAdapter != null) { + String cachedFaceUrl = mAdapter.getUserFaceUrlCache().getCachedFaceUrl(msg.getSender()); + if (cachedFaceUrl == null) { + List idList = new ArrayList<>(); + idList.add(msg.getSender()); + V2TIMManager.getInstance().getUsersInfo(idList, new V2TIMValueCallback>() { + @Override + public void onSuccess(List v2TIMUserFullInfos) { + if (v2TIMUserFullInfos == null || v2TIMUserFullInfos.isEmpty()) { + return; + } + V2TIMUserFullInfo userInfo = v2TIMUserFullInfos.get(0); + String faceUrl = userInfo.getFaceUrl(); + if (TextUtils.isEmpty(userInfo.getFaceUrl())) { + faceUrl = ""; + } + mAdapter.getUserFaceUrlCache().pushFaceUrl(userInfo.getUserID(), faceUrl); + mAdapter.onItemRefresh(msg); + } + + @Override + public void onError(int code, String desc) { + setupAvatar(""); + } + }); + } else { + setupAvatar(cachedFaceUrl); + } + } else { + setupAvatar(msg.getFaceUrl()); + } + } + + private void setupAvatar(Object faceUrl) { + int avatarSize = TUIConfigMinimalist.getMessageListAvatarSize(); + if (avatarSize == TUIConfigMinimalist.UNDEFINED) { + avatarSize = ScreenUtil.dip2px(32); + } + ViewGroup.LayoutParams params = leftUserIcon.getLayoutParams(); + params.width = avatarSize; + if (leftUserIcon.getVisibility() == View.INVISIBLE) { + params.height = 1; + } else { + params.height = avatarSize; + } + leftUserIcon.setLayoutParams(params); + + params = rightUserIcon.getLayoutParams(); + params.width = avatarSize; + if (rightUserIcon.getVisibility() == View.INVISIBLE) { + params.height = 1; + } else { + params.height = avatarSize; + } + rightUserIcon.setLayoutParams(params); + + int radius = ScreenUtil.dip2px(100); + if (TUIConfigMinimalist.getMessageListAvatarRadius() != TUIConfigMinimalist.UNDEFINED) { + radius = TUIConfigMinimalist.getMessageListAvatarRadius(); + } + ImageView renderedView; + if (isLayoutOnStart) { + renderedView = leftUserIcon; + } else { + renderedView = rightUserIcon; + } + + RequestBuilder errorRequestBuilder = Glide.with(itemView.getContext()) + .load(TUIThemeManager.getAttrResId(leftUserIcon.getContext(), com.tencent.qcloud.tuikit.timcommon.R.attr.core_default_user_icon)) + .placeholder(TUIThemeManager.getAttrResId(leftUserIcon.getContext(), com.tencent.qcloud.tuikit.timcommon.R.attr.core_default_user_icon)) + .transform(new RoundedCorners(radius)); + + Glide.with(itemView.getContext()) + .load(faceUrl) + .transform(new RoundedCorners(radius)) + .error(errorRequestBuilder) + .into(renderedView); + } + + private void setAvatarVisibility() { + if (isShowAvatar) { + if (isLayoutOnStart) { + leftUserIcon.setVisibility(View.VISIBLE); + rightUserIcon.setVisibility(View.GONE); + } else { + leftUserIcon.setVisibility(View.GONE); + if (isShowSelfAvatar) { + rightUserIcon.setVisibility(View.VISIBLE); + } else { + rightUserIcon.setVisibility(View.GONE); + } + } + } else { + leftUserIcon.setVisibility(View.INVISIBLE); + if (isShowSelfAvatar) { + rightUserIcon.setVisibility(View.INVISIBLE); + } else { + rightUserIcon.setVisibility(View.GONE); + } + } + } + + private void optimizePadding(int position, TUIMessageBean messageBean) { + if (mAdapter == null) { + return; + } + if (isMessageDetailMode || !isOptimize) { + return; + } + + TUIMessageBean nextMessage = mAdapter.getItem(position + 1); + int horizontalPadding = ScreenUtil.dip2px(16); + int verticalPadding = ScreenUtil.dip2px(25f); + if (!isShowAvatar) { + horizontalPadding = ScreenUtil.dip2px(16); + verticalPadding = ScreenUtil.dip2px(2); + } + if (nextMessage != null) { + rootLayout.setPaddingRelative(horizontalPadding, 0, horizontalPadding, verticalPadding); + } else { + rootLayout.setPaddingRelative(horizontalPadding, 0, horizontalPadding, ScreenUtil.dip2px(5)); + } + optimizeMessageContent(isShowAvatar); + } + + protected void optimizeMessageContent(boolean isShowAvatar) {} + + private void setMessageGravity() { + if (isLayoutOnStart) { + msgContentLinear.setGravity(Gravity.START | Gravity.BOTTOM); + extraInfoArea.setGravity(Gravity.START); + } else { + msgContentLinear.setGravity(Gravity.END | Gravity.BOTTOM); + extraInfoArea.setGravity(Gravity.END); + } + } + + private void setTimeInLineStatus(TUIMessageBean msg) { + if (isShowRead) { + if (msg.isSelf()) { + if (TUIMessageBean.MSG_STATUS_SEND_SUCCESS == msg.getStatus()) { + if (!msg.isNeedReadReceipt()) { + setReadStatus(READ_STATUS_HIDE); + } else { + processReadStatus(msg); + } + } else if (TUIMessageBean.MSG_STATUS_SENDING == msg.getStatus()) { + setReadStatus(READ_STATUS_SENDING); + } else { + setReadStatus(READ_STATUS_HIDE); + } + } else { + setReadStatus(READ_STATUS_HIDE); + } + } + } + + protected void setOnTimeInLineTextClickListener(TUIMessageBean messageBean) { + timeInLineTextLayout.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (onItemClickListener != null) { + onItemClickListener.onMessageClick(msgArea, messageBean); + } + } + }); + timeInLineTextLayout.getTextView().setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (onItemClickListener != null) { + onItemClickListener.onMessageClick(msgArea, messageBean); + } + } + }); + timeInLineTextLayout.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + if (onItemClickListener != null) { + onItemClickListener.onMessageLongClick(msgArea, messageBean); + } + return true; + } + }); + timeInLineTextLayout.getTextView().setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + if (onItemClickListener != null) { + onItemClickListener.onMessageLongClick(msgArea, messageBean); + } + return true; + } + }); + } + + protected void setReadStatus(int readStatus) { + if (timeInLineTextLayout != null) { + int statusIconResID = 0; + switch (readStatus) { + case READ_STATUS_UNREAD: { + statusIconResID = R.drawable.chat_minimalist_message_status_send_no_read; + break; + } + case READ_STATUS_PART_READ: { + statusIconResID = R.drawable.chat_minimalist_message_status_send_part_read; + break; + } + case READ_STATUS_ALL_READ: { + statusIconResID = R.drawable.chat_minimalist_message_status_send_all_read; + break; + } + case READ_STATUS_SENDING: { + statusIconResID = R.drawable.chat_minimalist_status_loading_anim; + break; + } + default: { + } + } + timeInLineTextLayout.setStatusIcon(statusIconResID); + } + } + + protected void setMessageBubbleDefaultPadding() { + // after setting background, the padding will be reset + int paddingHorizontal = itemView.getResources().getDimensionPixelSize(R.dimen.chat_minimalist_message_area_padding_left_right); + int paddingVertical = itemView.getResources().getDimensionPixelSize(R.dimen.chat_minimalist_message_area_padding_top_bottom); + msgArea.setPaddingRelative(paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical); + } + + protected void setGravity(boolean isStart) { + int gravity = isStart ? Gravity.START : Gravity.END; + msgAreaAndReply.setGravity(gravity); + ViewGroup.LayoutParams layoutParams = msgContentFrame.getLayoutParams(); + if (layoutParams instanceof FrameLayout.LayoutParams) { + ((FrameLayout.LayoutParams) layoutParams).gravity = gravity; + } else if (layoutParams instanceof LinearLayout.LayoutParams) { + ((LinearLayout.LayoutParams) layoutParams).gravity = gravity; + } + msgContentFrame.setLayoutParams(layoutParams); + } + + private void setReplyContent(TUIMessageBean messageBean) { + MessageRepliesBean messageRepliesBean = messageBean.getMessageRepliesBean(); + if (messageRepliesBean != null && messageRepliesBean.getRepliesSize() > 0) { + extraInfoArea.setVisibility(View.VISIBLE); + replyPreviewView.setVisibility(View.VISIBLE); + replyPreviewView.setMessageRepliesBean(messageRepliesBean); + replyPreviewView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (onItemClickListener != null) { + onItemClickListener.onReplyDetailClick(messageBean); + } + } + }); + } else { + replyPreviewView.setVisibility(View.GONE); + } + } + + private void setReactContent(TUIMessageBean messageBean) { + Map param = new HashMap<>(); + param.put(TUIConstants.TUIChat.Extension.MessageReactPreviewExtension.MESSAGE, messageBean); + param.put(TUIConstants.TUIChat.Extension.MessageReactPreviewExtension.VIEW_TYPE, + TUIConstants.TUIChat.Extension.MessageReactPreviewExtension.VIEW_TYPE_MINIMALIST); + TUICore.raiseExtension(TUIConstants.TUIChat.Extension.MessageReactPreviewExtension.EXTENSION_ID, reactionArea, param); + } + + private void processReadStatus(TUIMessageBean msg) { + if (msg.isGroup()) { + if (msg.isAllRead()) { + setReadStatus(READ_STATUS_ALL_READ); + } else if (msg.isUnread()) { + setReadStatus(READ_STATUS_UNREAD); + } else { + long readCount = msg.getReadCount(); + if (readCount > 0) { + setReadStatus(READ_STATUS_PART_READ); + } + } + } else { + if (msg.isPeerRead()) { + setReadStatus(READ_STATUS_ALL_READ); + } else { + setReadStatus(READ_STATUS_UNREAD); + } + } + } + + private void setShowReadStatusClickListener(TUIMessageBean messageBean) { + if (timeInLineTextLayout != null) { + timeInLineTextLayout.setOnStatusAreaClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (onItemClickListener != null) { + onItemClickListener.onMessageReadStatusClick(v, messageBean); + } + } + }); + timeInLineTextLayout.setOnStatusAreaLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + if (onItemClickListener != null) { + onItemClickListener.onMessageLongClick(msgArea, messageBean); + } + return true; + } + }); + + timeInLineTextLayout.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + if (onItemClickListener != null) { + onItemClickListener.onMessageLongClick(msgArea, messageBean); + } + return true; + } + }); + } + } + + public abstract void layoutVariableViews(final TUIMessageBean msg, final int position); + + public void onRecycled() { + super.onRecycled(); + } + + public void setNeedShowBottom(boolean needShowBottom) { + isNeedShowBottom = needShowBottom; + } + + public void setShowRead(boolean showRead) { + isShowRead = showRead; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/MessageStatusTimeView.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/MessageStatusTimeView.java new file mode 100644 index 00000000..dcbb310b --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/MessageStatusTimeView.java @@ -0,0 +1,62 @@ +package com.tencent.qcloud.tuikit.timcommon.minimalistui.widget.message; + +import android.content.Context; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.tencent.qcloud.tuikit.timcommon.R; + +public class MessageStatusTimeView extends FrameLayout { + private TextView timeView; + private ImageView statusIconView; + + public MessageStatusTimeView(@NonNull Context context) { + super(context); + init(context, null); + } + + public MessageStatusTimeView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(context, attrs); + } + + public MessageStatusTimeView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs); + } + + private void init(Context context, AttributeSet attrs) { + LayoutInflater.from(context).inflate(R.layout.chat_minimalist_text_status_layout, this); + statusIconView = findViewById(R.id.status_icon); + timeView = findViewById(R.id.time); + } + + public void setStatusIcon(int resID) { + if (resID == 0) { + statusIconView.setVisibility(GONE); + } else { + statusIconView.setBackgroundResource(resID); + Drawable drawable = statusIconView.getBackground(); + if (drawable instanceof Animatable) { + ((Animatable) drawable).start(); + } + statusIconView.setVisibility(VISIBLE); + } + } + + public void setTimeText(CharSequence charSequence) { + timeView.setText(charSequence); + } + + public void setTimeColor(int color) { + timeView.setTextColor(color); + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/MinimalistMessageLayout.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/MinimalistMessageLayout.java new file mode 100644 index 00000000..0ad18b3b --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/MinimalistMessageLayout.java @@ -0,0 +1,219 @@ +package com.tencent.qcloud.tuikit.timcommon.minimalistui.widget.message; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Rect; +import android.os.Build; +import android.util.AttributeSet; +import android.view.View; + +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.constraintlayout.widget.ConstraintLayout; + +import com.tencent.qcloud.tuikit.timcommon.R; +import com.tencent.qcloud.tuikit.timcommon.util.LayoutUtil; + +public class MinimalistMessageLayout extends ConstraintLayout { + private View msgArea; + private View quoteArea; + private View bottomArea; + private View replyArea; + + private boolean isStart = false; + private Paint paint; + private Path quotePath; + private Path bottomPath; + private Path replyPath; + + private Rect msgAreaRect; + private Rect bottomRect; + private Rect quoteRect; + private Rect replyRect; + private float strokeWidth; + + private boolean isRTL = false; + + public MinimalistMessageLayout(Context context) { + super(context); + init(); + } + + public MinimalistMessageLayout(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(); + } + + public MinimalistMessageLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public MinimalistMessageLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + private void init() { + setWillNotDraw(false); + isRTL = LayoutUtil.isRTL(); + quotePath = new Path(); + bottomPath = new Path(); + replyPath = new Path(); + msgAreaRect = new Rect(); + bottomRect = new Rect(); + quoteRect = new Rect(); + replyRect = new Rect(); + paint = new Paint(); + strokeWidth = getResources().getDimension(R.dimen.chat_minimalist_message_quato_line_width); + paint.setStrokeWidth(strokeWidth); + paint.setStyle(Paint.Style.STROKE); + paint.setAntiAlias(true); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + msgArea = findViewById(R.id.msg_area); + bottomArea = findViewById(R.id.bottom_content_fl); + quoteArea = findViewById(R.id.quote_content_fl); + replyArea = findViewById(R.id.msg_reply_preview); + drawLine(canvas); + } + + public void setIsStart(boolean isStart) { + this.isStart = isStart; + } + + private Rect getChildRectInParent(View child) { + int[] location = new int[2]; + getLocationInWindow(location); + + Rect rect = new Rect(); + int[] childLocation = new int[2]; + child.getLocationInWindow(childLocation); + int left = childLocation[0] - location[0]; + int top = childLocation[1] - location[1]; + int right = left + child.getWidth(); + int bottom = top + child.getHeight(); + rect.set(left, top, right, bottom); + return rect; + } + + private void drawLine(Canvas canvas) { + canvas.drawColor(0x00FFFFFF); + if (msgArea.getVisibility() == VISIBLE) { + msgAreaRect = getChildRectInParent(msgArea); + float msgX; + if (isStart) { + if (isRTL) { + paint.setColor(getResources().getColor(R.color.chat_minimalist_right_message_bubble_color)); + msgX = msgAreaRect.right - strokeWidth / 2; + } else { + paint.setColor(getResources().getColor(R.color.chat_minimalist_left_message_bubble_color)); + msgX = msgAreaRect.left + strokeWidth / 2; + } + } else { + if (isRTL) { + paint.setColor(getResources().getColor(R.color.chat_minimalist_left_message_bubble_color)); + msgX = msgAreaRect.left + strokeWidth / 2; + } else { + paint.setColor(getResources().getColor(R.color.chat_minimalist_right_message_bubble_color)); + msgX = msgAreaRect.right - strokeWidth / 2; + } + } + float msgCenterY; + msgCenterY = msgAreaRect.top + msgAreaRect.height() * 1.0f / 2; + drawBottomArea(canvas, msgX, msgCenterY); + drawQuoteArea(canvas, msgX, msgCenterY); + drawReplyArea(canvas, msgX, msgCenterY); + } + } + + private void drawReplyArea(Canvas canvas, float msgX, float msgCenterY) { + if (replyArea.getVisibility() == VISIBLE) { + float replyX; + float replyCenterY; + replyRect = getChildRectInParent(replyArea); + if (isStart) { + if (isRTL) { + replyX = replyRect.right; + } else { + replyX = replyRect.left; + } + } else { + if (isRTL) { + replyX = replyRect.left; + } else { + replyX = replyRect.right; + } + } + replyCenterY = replyRect.top + replyRect.height() * 1.0f / 2; + int replyControlRadius = (int) Math.abs(replyX - msgX); + replyPath.reset(); + replyPath.moveTo(msgX, msgCenterY); + replyPath.quadTo(msgX, replyCenterY - replyControlRadius, msgX, replyCenterY - replyControlRadius); + replyPath.quadTo(msgX, replyCenterY, replyX, replyCenterY); + canvas.drawPath(replyPath, paint); + } + } + + private void drawQuoteArea(Canvas canvas, float msgX, float msgCenterY) { + if (quoteArea.getVisibility() == VISIBLE) { + float quoteX; + float quoteCenterY; + quoteRect = getChildRectInParent(quoteArea); + if (isStart) { + if (isRTL) { + quoteX = quoteRect.right; + } else { + quoteX = quoteRect.left; + } + } else { + if (isRTL) { + quoteX = quoteRect.left; + } else { + quoteX = quoteRect.right; + } + } + quoteCenterY = quoteRect.top + quoteRect.height() * 1.0f / 2; + int quoteControlRadius = (int) Math.abs(quoteX - msgX); + quotePath.reset(); + quotePath.moveTo(msgX, msgCenterY); + quotePath.quadTo(msgX, quoteCenterY - quoteControlRadius, msgX, quoteCenterY - quoteControlRadius); + quotePath.quadTo(msgX, quoteCenterY, quoteX, quoteCenterY); + canvas.drawPath(quotePath, paint); + } + } + + private void drawBottomArea(Canvas canvas, float msgX, float msgCenterY) { + if (bottomArea.getVisibility() == VISIBLE) { + float bottomX; + float bottomCenterY; + bottomRect = getChildRectInParent(bottomArea); + if (isStart) { + if (isRTL) { + bottomX = bottomRect.right; + } else { + bottomX = bottomRect.left; + } + } else { + if (isRTL) { + bottomX = bottomRect.left; + } else { + bottomX = bottomRect.right; + } + } + bottomCenterY = bottomRect.top + bottomRect.height() * 1.0f / 2; + int bottomControlRadius = (int) Math.abs(bottomX - msgX); + bottomPath.reset(); + bottomPath.moveTo(msgX, msgCenterY); + bottomPath.quadTo(msgX, bottomCenterY - bottomControlRadius, msgX, bottomCenterY - bottomControlRadius); + bottomPath.quadTo(msgX, bottomCenterY, bottomX, bottomCenterY); + canvas.drawPath(bottomPath, paint); + } + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/ReplyPreviewView.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/ReplyPreviewView.java new file mode 100644 index 00000000..9896e659 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/ReplyPreviewView.java @@ -0,0 +1,168 @@ +package com.tencent.qcloud.tuikit.timcommon.minimalistui.widget.message; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import com.bumptech.glide.Glide; +import com.bumptech.glide.RequestBuilder; +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.bumptech.glide.load.resource.bitmap.CircleCrop; +import com.tencent.qcloud.tuikit.timcommon.R; +import com.tencent.qcloud.tuikit.timcommon.bean.MessageRepliesBean; +import com.tencent.qcloud.tuikit.timcommon.util.ScreenUtil; +import com.tencent.qcloud.tuikit.timcommon.util.TUIUtil; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +public class ReplyPreviewView extends FrameLayout { + private ImageView firstImg; + private ImageView secondImg; + private ImageView thirdImg; + private TextView replyText; + + private MessageRepliesBean messageRepliesBean; + + public ReplyPreviewView(@NonNull Context context) { + super(context); + init(context); + } + + public ReplyPreviewView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public ReplyPreviewView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + } + + public void setMessageRepliesBean(MessageRepliesBean messageRepliesBean) { + this.messageRepliesBean = messageRepliesBean; + setView(); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public ReplyPreviewView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context); + } + + private void init(Context context) { + LayoutInflater.from(context).inflate(R.layout.chat_minimalist_reply_preview_layout, this); + firstImg = findViewById(R.id.first_avatar); + secondImg = findViewById(R.id.second_avatar); + thirdImg = findViewById(R.id.third_avatar); + replyText = findViewById(R.id.reply_text); + } + + private void setView() { + if (messageRepliesBean != null && messageRepliesBean.getRepliesSize() > 0) { + setVisibility(VISIBLE); + firstImg.setVisibility(GONE); + secondImg.setVisibility(GONE); + thirdImg.setVisibility(GONE); + replyText.setText(String.format(Locale.US, getResources().getString(R.string.chat_reply_num), messageRepliesBean.getRepliesSize())); + List repliesBeanList = messageRepliesBean.getReplies(); + List iconList = getReplyUserIconLt(repliesBeanList); + if (iconList.isEmpty()) { + return; + } + String secondIconUrl = null; + String thirdIconUrl = null; + + if (iconList.size() > 1) { + secondIconUrl = iconList.get(1); + } + if (iconList.size() > 2) { + thirdIconUrl = iconList.get(2); + } + String firstIconUrl = iconList.get(0); + firstImg.setVisibility(VISIBLE); + loadAvatar(firstImg, firstIconUrl); + if (secondIconUrl != null) { + secondImg.setVisibility(VISIBLE); + loadAvatar(secondImg, secondIconUrl); + } + + if (iconList.size() == 3 && thirdIconUrl != null) { + thirdImg.setVisibility(VISIBLE); + loadAvatar(thirdImg, thirdIconUrl); + } else if (iconList.size() > 3) { + thirdImg.setVisibility(VISIBLE); + loadAvatar(thirdImg, R.drawable.chat_reply_more_icon); + } + } else { + setVisibility(GONE); + } + } + + private List getReplyUserIconLt(List repliesBeanList) { + Set iconUrlSet = new LinkedHashSet<>(); + for (MessageRepliesBean.ReplyBean replyBean : repliesBeanList) { + iconUrlSet.add(replyBean.getSenderFaceUrl()); + if (iconUrlSet.size() >= 3) { + break; + } + } + return new ArrayList<>(iconUrlSet); + } + + private void loadAvatar(ImageView imageView, Object url) { + if (TUIUtil.isActivityDestroyed(getContext())) { + return; + } + + RequestBuilder errorRequestBuilder = Glide.with(getContext()) + .load(com.tencent.qcloud.tuikit.timcommon.R.drawable.core_default_user_icon_light) + .transform(new ReplyRingCircleCrop()); + + Glide.with(getContext()) + .load(url) + .centerCrop() + .transform(new ReplyRingCircleCrop()) + .error(errorRequestBuilder) + .into(imageView); + } + + static class ReplyRingCircleCrop extends CircleCrop { + @Override + protected Bitmap transform(@NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) { + Bitmap outBitmap = pool.get(outWidth, outHeight, Bitmap.Config.ARGB_8888); + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); + paint.setColor(Color.WHITE); + Canvas canvas = new Canvas(outBitmap); + int borderWidth = ScreenUtil.dip2px(1); + int innerWidth = outWidth - 2 * borderWidth; + canvas.drawCircle(outWidth / 2, outHeight / 2, innerWidth / 2, paint); + paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); + Rect rect = new Rect(borderWidth, borderWidth, outWidth - borderWidth, outHeight - borderWidth); + int innerHeight = outHeight - 2 * borderWidth; + Bitmap bitmap = super.transform(pool, toTransform, innerWidth, innerHeight); + canvas.drawBitmap(bitmap, null, rect, paint); + paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OVER)); + canvas.drawCircle(outWidth / 2, outHeight / 2, outWidth / 2, paint); + return outBitmap; + } + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/TUIReplyQuoteView.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/TUIReplyQuoteView.java new file mode 100644 index 00000000..bcb215a6 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/TUIReplyQuoteView.java @@ -0,0 +1,31 @@ +package com.tencent.qcloud.tuikit.timcommon.minimalistui.widget.message; + +import android.content.Context; +import android.view.LayoutInflater; +import android.widget.FrameLayout; + +import com.tencent.qcloud.tuikit.timcommon.bean.TUIReplyQuoteBean; + +public abstract class TUIReplyQuoteView extends FrameLayout { + + public abstract int getLayoutResourceId(); + + public TUIReplyQuoteView(Context context) { + super(context); + int resId = getLayoutResourceId(); + if (resId != 0) { + LayoutInflater.from(context).inflate(resId, this, true); + } + } + + public abstract void onDrawReplyQuote(TUIReplyQuoteBean quoteBean); + + /** + * + * Whether the original message sender is himself, used for different UI displays + * + * @param isSelf + */ + public void setSelf(boolean isSelf) {} + +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/TimeInLineTextLayout.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/TimeInLineTextLayout.java new file mode 100644 index 00000000..81f8c42a --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/minimalistui/widget/message/TimeInLineTextLayout.java @@ -0,0 +1,160 @@ +package com.tencent.qcloud.tuikit.timcommon.minimalistui.widget.message; + +import android.content.Context; +import android.graphics.Color; +import android.text.Layout; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.widget.FrameLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.tencent.qcloud.tuikit.timcommon.R; +import com.tencent.qcloud.tuikit.timcommon.util.LayoutUtil; + +public class TimeInLineTextLayout extends FrameLayout { + private TextView textView; + private MessageStatusTimeView statusArea; + private int lineCount; + private boolean isRTL = false; + private int lastLineWidth = 0; + private boolean lastLineRunRTL = true; + + public TimeInLineTextLayout(@NonNull Context context) { + super(context); + init(context, null); + } + + public TimeInLineTextLayout(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(context, attrs); + } + + public TimeInLineTextLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs); + } + + private void init(Context context, AttributeSet attrs) { + isRTL = LayoutUtil.isRTL(); + textView = new TextView(context, null, R.style.ChatMinimalistMessageTextStyle); + textView.setTextColor(Color.BLACK); + textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textView.getResources().getDimension(R.dimen.chat_minimalist_message_text_size)); + LayoutParams textViewParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + addView(textView, textViewParams); + statusArea = new MessageStatusTimeView(context); + LayoutParams statusAreaParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + addView(statusArea, statusAreaParams); + } + + public void setText(CharSequence charSequence) { + textView.setText(charSequence); + } + + public void setText(int resID) { + textView.setText(resID); + } + + public void setStatusIcon(int resID) { + statusArea.setStatusIcon(resID); + } + + public void setTimeText(CharSequence charSequence) { + statusArea.setTimeText(charSequence); + } + + public void setTimeColor(int color) { + statusArea.setTimeColor(color); + } + + public void setTextColor(int color) { + textView.setTextColor(color); + } + + public void setTextSize(int size) { + textView.setTextSize(size); + } + + public TextView getTextView() { + return textView; + } + + public void setOnStatusAreaClickListener(OnClickListener listener) { + statusArea.setOnClickListener(listener); + } + + public void setOnStatusAreaLongClickListener(OnLongClickListener listener) { + statusArea.setOnLongClickListener(listener); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int textViewWidth = textView.getMeasuredWidth(); + int textViewHeight = textView.getMeasuredHeight(); + int statusAreaWidth = statusArea.getMeasuredWidth(); + int statusAreaHeight = statusArea.getMeasuredHeight(); + if (isRTL) { + if (lineCount <= 1) { + textView.layout(statusAreaWidth, 0, statusAreaWidth + textViewWidth, textViewHeight); + } else { + textView.layout(0, 0, textViewWidth, textViewHeight); + } + } else { + textView.layout(0, 0, textViewWidth, textViewHeight); + } + + if (isRTL) { + if (lineCount <= 1) { + statusArea.layout(0, bottom - top - statusAreaHeight, statusAreaWidth, bottom - top); + } else { + if (lastLineRunRTL) { + statusArea.layout(0, bottom - top - statusAreaHeight, statusAreaWidth, bottom - top); + } else { + statusArea.layout(right - left - statusAreaWidth, bottom - top - statusAreaHeight, right - left, bottom - top); + } + } + } else { + statusArea.layout(right - left - statusAreaWidth, bottom - top - statusAreaHeight, right - left, bottom - top); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int maxWidth; + int maxHeight; + // measure text view + measureChildren(widthMeasureSpec, heightMeasureSpec); + + int textWidth = textView.getMeasuredWidth(); + int textHeight = textView.getMeasuredHeight(); + lineCount = textView.getLineCount(); + + // get last line's width + Layout layout = textView.getLayout(); + if (layout != null) { + lastLineWidth = (int) layout.getLineWidth(lineCount - 1); + int direction = layout.getParagraphDirection(lineCount - 1); + lastLineRunRTL = direction == Layout.DIR_RIGHT_TO_LEFT; + } + + int statusAreaWidth = statusArea.getMeasuredWidth(); + int statusAreaHeight = statusArea.getMeasuredHeight(); + MarginLayoutParams lp = (MarginLayoutParams) statusArea.getLayoutParams(); + statusAreaWidth += lp.leftMargin + lp.rightMargin; + + int layoutWidth = MeasureSpec.getSize(widthMeasureSpec); + // switch a new line + if (lastLineWidth + statusAreaWidth > layoutWidth) { + maxHeight = textHeight + statusAreaHeight; + lineCount++; + maxWidth = Math.max(textWidth, statusAreaWidth); + } else { + maxHeight = Math.max(textHeight, statusAreaHeight); + maxWidth = Math.max(textWidth, lastLineWidth + statusAreaWidth); + } + + setMeasuredDimension(maxWidth, maxHeight); + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/ActivityResultResolver.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/ActivityResultResolver.java new file mode 100644 index 00000000..d3d480f6 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/ActivityResultResolver.java @@ -0,0 +1,281 @@ +package com.tencent.qcloud.tuikit.timcommon.util; + +import android.app.Activity; +import android.content.ClipData; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.provider.MediaStore; +import android.text.TextUtils; +import android.util.Pair; +import androidx.activity.result.ActivityResult; +import androidx.activity.result.ActivityResultCallback; +import androidx.activity.result.ActivityResultCaller; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContract; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentActivity; +import com.tencent.qcloud.tuicore.TUICore; +import com.tencent.qcloud.tuicore.interfaces.TUIValueCallback; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; + +public class ActivityResultResolver { + public static final String CONTENT_TYPE_ALL = "*/*"; + public static final String CONTENT_TYPE_IMAGE = "image/*"; + public static final String CONTENT_TYPE_VIDEO = "video/*"; + + private static final String CONTENT_KEY_TYPE = "ContentType"; + private static final String URI = "Uri"; + private static final String METHOD = "Method"; + private static final String KEY_DATA = "Data"; + + private static final String METHOD_GET_SINGLE_CONTENT = "MethodGetSingleContent"; + private static final String METHOD_GET_MULTIPLE_CONTENT = "MethodGetMultipleContent"; + private static final String METHOD_TAKE_PICTURE = "MethodTakePicture"; + private static final String METHOD_TAKE_VIDEO = "MethodTakeVideo"; + + private ActivityResultResolver() {} + + public static void getSingleContent(ActivityResultCaller activityResultCaller, @NonNull String type, TUIValueCallback callback) { + getContent(activityResultCaller, new String[] {type}, false, new TUIValueCallback>() { + @Override + public void onSuccess(List list) { + if (list != null && !list.isEmpty()) { + TUIValueCallback.onSuccess(callback, list.get(0)); + } else { + TUIValueCallback.onError(callback, -1, "getSingleContent result list is empty"); + } + } + + @Override + public void onError(int errorCode, String errorMessage) { + TUIValueCallback.onError(callback, errorCode, errorMessage); + } + }); + } + + public static void getSingleContent(ActivityResultCaller activityResultCaller, @NonNull String[] type, TUIValueCallback callback) { + getContent(activityResultCaller, type, false, new TUIValueCallback>() { + @Override + public void onSuccess(List list) { + if (list != null && !list.isEmpty()) { + TUIValueCallback.onSuccess(callback, list.get(0)); + } else { + TUIValueCallback.onError(callback, -1, "getSingleContent result list is empty"); + } + } + + @Override + public void onError(int errorCode, String errorMessage) { + TUIValueCallback.onError(callback, errorCode, errorMessage); + } + }); + } + + public static void getMultipleContent(ActivityResultCaller activityResultCaller, @NonNull String type, TUIValueCallback> callback) { + getContent(activityResultCaller, new String[] {type}, true, callback); + } + + public static void getMultipleContent(ActivityResultCaller activityResultCaller, @NonNull String[] type, TUIValueCallback> callback) { + getContent(activityResultCaller, type, true, callback); + } + + private static void getContent( + ActivityResultCaller activityResultCaller, @NonNull String[] types, boolean isMultiContent, TUIValueCallback> callback) { + Bundle bundle = new Bundle(); + bundle.putStringArray(CONTENT_KEY_TYPE, types); + bundle.putString(METHOD, isMultiContent ? METHOD_GET_MULTIPLE_CONTENT : METHOD_GET_SINGLE_CONTENT); + TUICore.startActivityForResult(activityResultCaller, ActivityResultProxyActivity.class, bundle, new ActivityResultCallback() { + @Override + public void onActivityResult(ActivityResult result) { + if (result.getData() != null) { + TUIValueCallback.onSuccess(callback, (List) result.getData().getSerializableExtra(KEY_DATA)); + } + } + }); + } + + public static void takePicture(ActivityResultCaller activityResultCaller, @NonNull Uri uri, TUIValueCallback callback) { + takePictureVideo(activityResultCaller, uri, true, callback); + } + + public static void takeVideo(ActivityResultCaller activityResultCaller, @NonNull Uri uri, TUIValueCallback callback) { + takePictureVideo(activityResultCaller, uri, false, callback); + } + + private static void takePictureVideo(ActivityResultCaller activityResultCaller, @NonNull Uri uri, boolean isPicture, TUIValueCallback callback) { + Bundle bundle = new Bundle(); + if (isPicture) { + bundle.putString(METHOD, METHOD_TAKE_PICTURE); + } else { + bundle.putString(METHOD, METHOD_TAKE_VIDEO); + } + bundle.putParcelable(URI, uri); + TUICore.startActivityForResult(activityResultCaller, ActivityResultProxyActivity.class, bundle, new ActivityResultCallback() { + @Override + public void onActivityResult(ActivityResult result) { + if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { + TUIValueCallback.onSuccess(callback, result.getData().getBooleanExtra(KEY_DATA, false)); + } + } + }); + } + + private static class GetContentsContract extends ActivityResultContract, List> { + @NonNull + @Override + public Intent createIntent(@NonNull Context context, @NonNull Pair input) { + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + String[] type = input.first; + boolean allowMultiple = input.second; + if (type.length == 1) { + intent.setType(type[0]); + } else if (type.length > 1) { + intent.setType(type[0]); + intent.putExtra(Intent.EXTRA_MIME_TYPES, type); + } + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple); + return intent; + } + + @NonNull + @Override + public final List parseResult(int resultCode, @Nullable Intent intent) { + if (intent == null || resultCode != Activity.RESULT_OK) { + return Collections.emptyList(); + } else { + return getClipDataUris(intent); + } + } + + @NonNull + static List getClipDataUris(@NonNull Intent intent) { + // Use a LinkedHashSet to maintain any ordering that may be + // present in the ClipData + LinkedHashSet resultSet = new LinkedHashSet<>(); + if (intent.getData() != null) { + resultSet.add(intent.getData()); + } + ClipData clipData = intent.getClipData(); + if (clipData == null && resultSet.isEmpty()) { + return Collections.emptyList(); + } else if (clipData != null) { + for (int i = 0; i < clipData.getItemCount(); i++) { + Uri uri = clipData.getItemAt(i).getUri(); + if (uri != null) { + resultSet.add(uri); + } + } + } + return new ArrayList<>(resultSet); + } + } + + private static class TakePictureVideoContract extends ActivityResultContract, Boolean> { + private boolean isTakePicture; + + @NonNull + @Override + public Intent createIntent(@NonNull Context context, @NonNull Pair input) { + isTakePicture = input.second; + if (isTakePicture) { + return new Intent(MediaStore.ACTION_IMAGE_CAPTURE) + .putExtra(MediaStore.EXTRA_OUTPUT, input.first) + .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + } else { + return new Intent(MediaStore.ACTION_VIDEO_CAPTURE) + .putExtra(MediaStore.EXTRA_OUTPUT, input.first) + .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + } + } + + @NonNull + @Override + public final Boolean parseResult(int resultCode, @Nullable Intent intent) { + if (isTakePicture) { + return resultCode == Activity.RESULT_OK; + } else { + return intent != null && resultCode == Activity.RESULT_OK; + } + } + } + + public static class ActivityResultProxyActivity extends FragmentActivity { + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Intent intent = getIntent(); + String method = intent.getStringExtra(METHOD); + if (TextUtils.equals(method, METHOD_GET_SINGLE_CONTENT)) { + getSingleContent(intent); + } else if (TextUtils.equals(method, METHOD_GET_MULTIPLE_CONTENT)) { + getMultipleContent(intent); + } else if (TextUtils.equals(method, METHOD_TAKE_PICTURE)) { + takePicture(intent); + } else if (TextUtils.equals(method, METHOD_TAKE_VIDEO)) { + takeVideo(intent); + } + } + + private void getSingleContent(Intent intent) { + String[] types = intent.getStringArrayExtra(CONTENT_KEY_TYPE); + getContent(types, false); + } + + private void getMultipleContent(Intent intent) { + String[] types = intent.getStringArrayExtra(CONTENT_KEY_TYPE); + getContent(types, true); + } + + private void getContent(String[] types, boolean isMultiple) { + ActivityResultLauncher> launcher = + this.registerForActivityResult(new GetContentsContract(), new ActivityResultCallback>() { + @Override + public void onActivityResult(List result) { + Intent dataIntent = new Intent(); + dataIntent.putExtra(KEY_DATA, new ArrayList<>(result)); + setResult(Activity.RESULT_OK, dataIntent); + finish(); + } + }); + try { + launcher.launch(Pair.create(types, isMultiple)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void takePicture(Intent intent) { + takePictureVideo(intent, true); + } + + private void takeVideo(Intent intent) { + takePictureVideo(intent, false); + } + + private void takePictureVideo(Intent intent, boolean isPicture) { + Uri uri = intent.getParcelableExtra(URI); + ActivityResultLauncher> launcher = + this.registerForActivityResult(new TakePictureVideoContract(), new ActivityResultCallback() { + @Override + public void onActivityResult(Boolean result) { + Intent dataIntent = new Intent(); + dataIntent.putExtra(KEY_DATA, result); + setResult(Activity.RESULT_OK, dataIntent); + finish(); + } + }); + try { + launcher.launch(Pair.create(uri, isPicture)); + } catch (Exception e) { + e.printStackTrace(); + } + } + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/DateTimeUtil.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/DateTimeUtil.java new file mode 100644 index 00000000..d65dbe45 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/DateTimeUtil.java @@ -0,0 +1,150 @@ +package com.tencent.qcloud.tuikit.timcommon.util; + +import android.content.Context; + +import com.tencent.qcloud.tuicore.TUIConfig; +import com.tencent.qcloud.tuicore.TUIThemeManager; +import com.tencent.qcloud.tuikit.timcommon.R; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; + +public class DateTimeUtil { + private static final long minute = 60 * 1000; + private static final long hour = 60 * minute; + private static final long day = 24 * hour; + private static final long week = 7 * day; + private static final long month = 31 * day; + private static final long year = 12 * month; + + /** + * return format text for time + * you can see https://docs.oracle.com/javase/8/docs/api/java/util/Formatter.html + * today:HH:MM + * this week:Sunday, Friday .. + * this year:MM/DD + * before this year:YYYY/MM/DD + * @param date current time + * @return format text + */ + public static String getTimeFormatText(Date date) { + if (date == null) { + return ""; + } + Context context = TUIConfig.getAppContext(); + Locale locale; + if (context == null) { + locale = Locale.getDefault(); + } else { + locale = TUIThemeManager.getInstance().getLocale(context); + } + String timeText; + Calendar dayStartCalendar = Calendar.getInstance(); + dayStartCalendar.set(Calendar.HOUR_OF_DAY, 0); + dayStartCalendar.set(Calendar.MINUTE, 0); + dayStartCalendar.set(Calendar.SECOND, 0); + dayStartCalendar.set(Calendar.MILLISECOND, 0); + Calendar weekStartCalendar = Calendar.getInstance(); + weekStartCalendar.setFirstDayOfWeek(Calendar.SUNDAY); + weekStartCalendar.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY); + weekStartCalendar.set(Calendar.HOUR_OF_DAY, 0); + weekStartCalendar.set(Calendar.MINUTE, 0); + weekStartCalendar.set(Calendar.SECOND, 0); + weekStartCalendar.set(Calendar.MILLISECOND, 0); + Calendar yearStartCalendar = Calendar.getInstance(); + yearStartCalendar.set(Calendar.DAY_OF_YEAR, 1); + yearStartCalendar.set(Calendar.HOUR_OF_DAY, 0); + yearStartCalendar.set(Calendar.MINUTE, 0); + yearStartCalendar.set(Calendar.SECOND, 0); + yearStartCalendar.set(Calendar.MILLISECOND, 0); + long dayStartTimeInMillis = dayStartCalendar.getTimeInMillis(); + long weekStartTimeInMillis = weekStartCalendar.getTimeInMillis(); + long yearStartTimeInMillis = yearStartCalendar.getTimeInMillis(); + long outTimeMillis = date.getTime(); + if (outTimeMillis < yearStartTimeInMillis) { + timeText = String.format(Locale.US, "%tD", date); + } else if (outTimeMillis < weekStartTimeInMillis) { + timeText = String.format(Locale.US, "%1$tm/%1$td", date); + } else if (outTimeMillis < dayStartTimeInMillis) { + timeText = String.format(locale, "%tA", date); + } else { + timeText = String.format(Locale.US, "%tR", date); + } + return timeText; + } + + /** + * HH:MM + * @param date current time + * @return format text e.g. "12:12" + */ + public static String getHMTimeString(Date date) { + if (date == null) { + return ""; + } + return String.format(Locale.US, "%tR", date); + } + + public static String formatSeconds(long seconds) { + Context context = TUIConfig.getAppContext(); + String timeStr = seconds + context.getString(R.string.date_second_short); + if (seconds > 60) { + long second = seconds % 60; + long min = seconds / 60; + timeStr = min + context.getString(R.string.date_minute_short) + second + context.getString(R.string.date_second_short); + if (min > 60) { + min = (seconds / 60) % 60; + long hour = (seconds / 60) / 60; + timeStr = hour + context.getString(R.string.date_hour_short) + min + context.getString(R.string.date_minute_short) + second + + context.getString(R.string.date_second_short); + if (hour % 24 == 0) { + long day = (((seconds / 60) / 60) / 24); + timeStr = day + context.getString(R.string.date_day_short); + } else if (hour > 24) { + hour = ((seconds / 60) / 60) % 24; + long day = (((seconds / 60) / 60) / 24); + timeStr = day + context.getString(R.string.date_day_short) + hour + context.getString(R.string.date_hour_short) + min + + context.getString(R.string.date_minute_short) + second + context.getString(R.string.date_second_short); + } + } + } + return timeStr; + } + + public static String formatSecondsTo00(int timeSeconds) { + int second = timeSeconds % 60; + int minuteTemp = timeSeconds / 60; + if (minuteTemp > 0) { + int minute = minuteTemp % 60; + int hour = minuteTemp / 60; + if (hour > 0) { + return (hour >= 10 ? (hour + "") : ("0" + hour)) + ":" + (minute >= 10 ? (minute + "") : ("0" + minute)) + ":" + + (second >= 10 ? (second + "") : ("0" + second)); + } else { + return (minute >= 10 ? (minute + "") : ("0" + minute)) + ":" + (second >= 10 ? (second + "") : ("0" + second)); + } + } else { + return "00:" + (second >= 10 ? (second + "") : ("0" + second)); + } + } + + public static long getStringToDate(String dateString, String pattern) { + SimpleDateFormat dateFormat = new SimpleDateFormat(pattern, Locale.US); + Date date = new Date(); + try { + date = dateFormat.parse(dateString); + } catch (ParseException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return date.getTime(); + } + + public static String getTimeStringFromDate(Date date, String pattern) { + SimpleDateFormat simpleDateFormat = new SimpleDateFormat(pattern, Locale.US); + return simpleDateFormat.format(date); + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/FileProvider.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/FileProvider.java new file mode 100644 index 00000000..9e5021ca --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/FileProvider.java @@ -0,0 +1,3 @@ +package com.tencent.qcloud.tuikit.timcommon.util; + +public class FileProvider extends androidx.core.content.FileProvider {} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/FileUtil.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/FileUtil.java new file mode 100644 index 00000000..2bbc9229 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/FileUtil.java @@ -0,0 +1,553 @@ +package com.tencent.qcloud.tuikit.timcommon.util; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.provider.DocumentsContract; +import android.provider.MediaStore; +import android.provider.OpenableColumns; +import android.text.TextUtils; +import android.util.Log; +import android.webkit.MimeTypeMap; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.tencent.qcloud.tuicore.ServiceInitializer; +import com.tencent.qcloud.tuicore.TUIConfig; +import com.tencent.qcloud.tuicore.TUILogin; +import com.tencent.qcloud.tuikit.timcommon.R; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.Locale; +import java.util.Random; + +public class FileUtil { + public static final String DOCUMENTS_DIR = "documents"; + + public static final String FILE_PROVIDER_AUTH = ".timcommon.fileprovider"; + + public static final int SIZETYPE_B = 1; + public static final int SIZETYPE_KB = 2; + public static final int SIZETYPE_MB = 3; + public static final int SIZETYPE_GB = 4; + + public static boolean deleteFile(String path) { + if (TextUtils.isEmpty(path)) { + return false; + } + boolean result = false; + File file = new File(path); + if (file.exists()) { + result = file.delete(); + } + return result; + } + + public static String getPathFromUri(Uri uri) { + String path = ""; + try { + int sdkVersion = Build.VERSION.SDK_INT; + if (sdkVersion >= 19) { + path = getPathByCopyFile(TUILogin.getAppContext(), uri); + } else { + path = getRealFilePath(uri); + } + } catch (Exception e) { + e.printStackTrace(); + } + if (path == null) { + path = ""; + } + return path; + } + + public static String getRealFilePath(Uri uri) { + if (null == uri) { + return null; + } + final String scheme = uri.getScheme(); + String data = null; + if (scheme == null) { + data = uri.getPath(); + } else if (ContentResolver.SCHEME_FILE.equals(scheme)) { + data = uri.getPath(); + } else if (ContentResolver.SCHEME_CONTENT.equals(scheme)) { + Cursor cursor = TUILogin.getAppContext().getContentResolver().query(uri, new String[] {MediaStore.Images.ImageColumns.DATA}, null, null, null); + if (null != cursor) { + if (cursor.moveToFirst()) { + int index = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA); + if (index > -1) { + data = cursor.getString(index); + } + } + cursor.close(); + } + } + return data; + } + + public static Uri getUriFromPath(String path) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return FileProvider.getUriForFile( + TUIConfig.getAppContext(), TUIConfig.getAppContext().getApplicationInfo().packageName + FILE_PROVIDER_AUTH, new File(path)); + } else { + return Uri.fromFile(new File(path)); + } + + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + /** + * + * Get file path from Uri specially designed for Android4.4 and above + */ + public static String getPath(final Context context, final Uri uri) { + final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + + // DocumentProvider + if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { + // ExternalStorageProvider + if (isExternalStorageDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + if ("primary".equalsIgnoreCase(type)) { + return Environment.getExternalStorageDirectory() + "/" + split[1]; + } else { + return getPathByCopyFile(context, uri); + } + } + // DownloadsProvider + else if (isDownloadsDocument(uri)) { + final String id = DocumentsContract.getDocumentId(uri); + if (id.startsWith("raw:")) { + final String path = id.replaceFirst("raw:", ""); + return path; + } + String[] contentUriPrefixesToTry = + new String[] {"content://downloads/public_downloads", "content://downloads/my_downloads", "content://downloads/all_downloads"}; + + for (String contentUriPrefix : contentUriPrefixesToTry) { + Uri contentUri = ContentUris.withAppendedId(Uri.parse(contentUriPrefix), Long.parseLong(id)); + try { + String path = getDataColumn(context, contentUri, null, null); + if (path != null && Build.VERSION.SDK_INT < 29) { + return path; + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + + // On some android8+ mobile phones, the path cannot be obtained, so the new file name is obtained by copying, and then the file is sent out + return getPathByCopyFile(context, uri); + } + // MediaProvider + else if (isMediaDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + Uri contentUri = null; + if ("image".equals(type)) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } else if ("video".equals(type)) { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + } else if ("audio".equals(type)) { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + } + + final String selection = "_id=?"; + final String[] selectionArgs = new String[] {split[1]}; + + String path = getDataColumn(context, contentUri, selection, selectionArgs); + if (TextUtils.isEmpty(path) || Build.VERSION.SDK_INT >= 29) { + path = getPathByCopyFile(context, uri); + } + return path; + } + } + // MediaStore (and general) + else if ("content".equalsIgnoreCase(uri.getScheme())) { + String path = getDataColumn(context, uri, null, null); + if (TextUtils.isEmpty(path) || Build.VERSION.SDK_INT >= 29) { + + path = getPathByCopyFile(context, uri); + } + return path; + } + // File + else if ("file".equalsIgnoreCase(uri.getScheme())) { + return uri.getPath(); + } + + return null; + } + + private static String getPathByCopyFile(Context context, Uri uri) { + String fileName = getFileName(context, uri); + File cacheDir = getDocumentCacheDir(context); + File file = generateFileName(fileName, cacheDir); + String destinationPath = null; + if (file != null) { + destinationPath = file.getAbsolutePath(); + boolean saveSuccess = saveFileFromUri(context, uri, destinationPath); + if (!saveSuccess) { + file.delete(); + return null; + } + } + + return destinationPath; + } + + @Nullable + public static File generateFileName(@Nullable String name, File directory) { + if (name == null) { + return null; + } + + File file = new File(directory, name); + + if (file.exists()) { + String fileName = name; + String extension = ""; + int dotIndex = name.lastIndexOf('.'); + if (dotIndex > 0) { + fileName = name.substring(0, dotIndex); + extension = name.substring(dotIndex); + } + + int index = 0; + + while (file.exists()) { + index++; + name = fileName + '(' + index + ')' + extension; + file = new File(directory, name); + } + } + + try { + if (!file.createNewFile()) { + return null; + } + } catch (IOException e) { + e.printStackTrace(); + return null; + } + + return file; + } + + public static String getFileName(@NonNull Context context, Uri uri) { + String mimeType = context.getContentResolver().getType(uri); + String filename = null; + + if (mimeType == null) { + filename = getName(uri.toString()); + } else { + Cursor returnCursor = context.getContentResolver().query(uri, null, null, null, null); + if (returnCursor != null) { + int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); + returnCursor.moveToFirst(); + filename = returnCursor.getString(nameIndex); + returnCursor.close(); + } + } + + return filename; + } + + public static String getName(String filePath) { + if (filePath == null) { + return null; + } + int index = filePath.lastIndexOf('/'); + return filePath.substring(index + 1); + } + + public static File getDocumentCacheDir(@NonNull Context context) { + File dir = new File(context.getCacheDir(), DOCUMENTS_DIR); + if (!dir.exists()) { + dir.mkdirs(); + } + + return dir; + } + + private static boolean saveFileFromUri(Context context, Uri uri, String destinationPath) { + InputStream is = null; + BufferedOutputStream bos = null; + try { + is = context.getContentResolver().openInputStream(uri); + bos = new BufferedOutputStream(new FileOutputStream(destinationPath, false)); + byte[] buf = new byte[1024]; + + int actualBytes; + while ((actualBytes = is.read(buf)) != -1) { + bos.write(buf, 0, actualBytes); + } + } catch (IOException e) { + e.printStackTrace(); + return false; + } finally { + try { + if (is != null) { + is.close(); + } + if (bos != null) { + bos.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + return true; + } + + /** + * Get the value of the data column for this Uri. This is useful for + * MediaStore Uris, and other file-based ContentProviders. + * + * @param context The context. + * @param uri The Uri to query. + * @param selection (Optional) Filter used in the query. + * @param selectionArgs (Optional) Selection arguments used in the query. + * @return The value of the _data column, which is typically a file path. + */ + public static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { + Cursor cursor = null; + final String column = "_data"; + final String[] projection = {column}; + + try { + cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); + if (cursor != null && cursor.moveToFirst()) { + final int column_index = cursor.getColumnIndexOrThrow(column); + return cursor.getString(column_index); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (cursor != null) { + cursor.close(); + } + } + return null; + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is ExternalStorageProvider. + */ + public static boolean isExternalStorageDocument(Uri uri) { + return "com.android.externalstorage.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is DownloadsProvider. + */ + public static boolean isDownloadsDocument(Uri uri) { + return "com.android.providers.downloads.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is MediaProvider. + */ + public static boolean isMediaDocument(Uri uri) { + return "com.android.providers.media.documents".equals(uri.getAuthority()); + } + + /** + * + * Convert file size to string + * + * @param fileS + * @return + */ + public static String formatFileSize(long fileS) { + DecimalFormatSymbols symbols = new DecimalFormatSymbols(Locale.US); + DecimalFormat df = new DecimalFormat("#.00", symbols); + String fileSizeString = ""; + String wrongSize = "0B"; + if (fileS == 0) { + return wrongSize; + } + if (fileS < 1024) { + fileSizeString = df.format((double) fileS) + "B"; + } else if (fileS < 1048576) { + fileSizeString = df.format((double) fileS / 1024) + "KB"; + } else if (fileS < 1073741824) { + fileSizeString = df.format((double) fileS / 1048576) + "MB"; + } else { + fileSizeString = df.format((double) fileS / 1073741824) + "GB"; + } + return fileSizeString; + } + + + // fix the problem that getFileExtensionFromUrl does not support Chinese + public static String getFileExtensionFromUrl(String url) { + if (!TextUtils.isEmpty(url)) { + int fragment = url.lastIndexOf('#'); + if (fragment > 0) { + url = url.substring(0, fragment); + } + + int query = url.lastIndexOf('?'); + if (query > 0) { + url = url.substring(0, query); + } + + int filenamePos = url.lastIndexOf('/'); + String filename = 0 <= filenamePos ? url.substring(filenamePos + 1) : url; + + // if the filename contains special characters, we don't + // consider it valid for our matching purposes: + + // if (!filename.isEmpty() && Pattern.matches("[a-zA-Z_0-9\\.\\-\\(\\)\\%]+", filename)) + if (!filename.isEmpty()) { + int dotPos = filename.lastIndexOf('.'); + if (0 <= dotPos) { + return filename.substring(dotPos + 1).toLowerCase(); + } + } + } + + return ""; + } + + public static void openFile(String path, String fileName) { + Uri uri = FileProvider.getUriForFile( + TUIConfig.getAppContext(), TUIConfig.getAppContext().getApplicationInfo().packageName + FILE_PROVIDER_AUTH, new File(path)); + if (uri == null) { + Log.e("FileUtil", "openFile failed , uri is null"); + return; + } + String fileExtension; + if (TextUtils.isEmpty(fileName)) { + fileExtension = FileUtil.getFileExtensionFromUrl(path); + } else { + fileExtension = FileUtil.getFileExtensionFromUrl(fileName); + } + String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.setDataAndType(uri, mimeType); + try { + Intent chooserIntent = Intent.createChooser(intent, TUIConfig.getAppContext().getString(R.string.open_file_tips)); + chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + TUIConfig.getAppContext().startActivity(chooserIntent); + } catch (Exception e) { + Log.e("FileUtil", "openFile failed , " + e.getMessage()); + } + } + + public static long getFileSize(String path) { + File file = new File(path); + if (file.exists()) { + return file.length(); + } + return 0; + } + + public static String generateImageFilePath() { + String name = System.nanoTime() + "_" + Math.abs(new Random().nextInt()); + return TUIConfig.getImageBaseDir() + name + ".jpg"; + } + + public static String generateExternalStorageImageFilePath() { + File dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + File.separatorChar + TUIConfig.getAppContext().getPackageName() + + TUIConfig.IMAGE_BASE_DIR_SUFFIX); + if (!dir.exists()) { + dir.mkdirs(); + } + return dir.getAbsolutePath() + File.separatorChar + System.nanoTime() + "_" + Math.abs(new Random().nextInt()) + ".jpg"; + } + + public static String generateVideoFilePath() { + String name = System.nanoTime() + "_" + Math.abs(new Random().nextInt()); + return TUIConfig.getVideoBaseDir() + name + ".mp4"; + } + + public static String generateExternalStorageVideoFilePath() { + File dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + File.separatorChar + TUIConfig.getAppContext().getPackageName() + + TUIConfig.VIDEO_BASE_DIR_SUFFIX); + if (!dir.exists()) { + dir.mkdirs(); + } + return dir.getAbsolutePath() + File.separatorChar + System.nanoTime() + "_" + Math.abs(new Random().nextInt()) + ".mp4"; + } + + public static boolean saveBitmap(String path, Bitmap b) { + try { + FileOutputStream fout = new FileOutputStream(path); + BufferedOutputStream bos = new BufferedOutputStream(fout); + b.compress(Bitmap.CompressFormat.JPEG, 100, bos); + bos.flush(); + bos.close(); + return true; + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + + public static boolean isFileExists(String path) { + try { + File file = new File(path); + return file.exists() && file.isFile(); + } catch (Exception e) { + return false; + } + } + + public static boolean isDirExists(String path) { + try { + File file = new File(path); + return file.exists() && file.isDirectory(); + } catch (Exception e) { + return false; + } + } + + public static boolean isFileSizeExceedsLimit(Uri data, int maxSize) { + try { + Cursor returnCursor = ServiceInitializer.getAppContext().getContentResolver().query(data, null, null, null, null); + if (returnCursor != null) { + int sizeIndex = returnCursor.getColumnIndex(OpenableColumns.SIZE); + returnCursor.moveToFirst(); + int size = returnCursor.getInt(sizeIndex); + if (size > maxSize) { + return true; + } + returnCursor.close(); + return false; + } + return false; + } catch (Exception e) { + return false; + } + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/ImageUtil.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/ImageUtil.java new file mode 100644 index 00000000..dd2c3758 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/ImageUtil.java @@ -0,0 +1,300 @@ +package com.tencent.qcloud.tuikit.timcommon.util; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.RectF; +import android.media.ExifInterface; +import android.text.TextUtils; + +import com.tencent.imsdk.v2.V2TIMImageElem; +import com.tencent.qcloud.tuicore.TUIConfig; +import com.tencent.qcloud.tuicore.TUILogin; +import com.tencent.qcloud.tuicore.util.SPUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +public class ImageUtil { + public static final String SP_IMAGE = "_conversation_group_face"; + + /** + * @param outFile + * @param bitmap + * @return + */ + public static File storeBitmap(File outFile, Bitmap bitmap) { + if (!outFile.exists() || outFile.isDirectory()) { + outFile.getParentFile().mkdirs(); + } + FileOutputStream fOut = null; + try { + outFile.deleteOnExit(); + outFile.createNewFile(); + fOut = new FileOutputStream(outFile); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut); + fOut.flush(); + } catch (IOException e1) { + outFile.deleteOnExit(); + } finally { + if (null != fOut) { + try { + fOut.close(); + } catch (IOException e) { + e.printStackTrace(); + outFile.deleteOnExit(); + } + } + } + return outFile; + } + + /** + * + * Read the rotation angle of the image + */ + public static int getBitmapDegree(String fileName) { + int degree = 0; + try { + ExifInterface exifInterface = new ExifInterface(fileName); + int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); + switch (orientation) { + case ExifInterface.ORIENTATION_ROTATE_90: + degree = 90; + break; + case ExifInterface.ORIENTATION_ROTATE_180: + degree = 180; + break; + case ExifInterface.ORIENTATION_ROTATE_270: + degree = 270; + break; + default: + break; + } + } catch (IOException e) { + e.printStackTrace(); + } + return degree; + } + + /** + * + * + * Rotate the image by an angle + * + * @param bm image to be rotated + * @param degree Rotation angle + * @return rotated image + */ + public static Bitmap rotateBitmapByDegree(Bitmap bm, int degree) { + Bitmap returnBm = null; + + Matrix matrix = new Matrix(); + matrix.postRotate(degree); + try { + returnBm = Bitmap.createBitmap(bm, 0, 0, bm.getWidth(), bm.getHeight(), matrix, true); + } catch (OutOfMemoryError e) { + e.printStackTrace(); + } + if (returnBm == null) { + returnBm = bm; + } + if (bm != returnBm) { + bm.recycle(); + } + return returnBm; + } + + public static int[] getImageSize(String path) { + int[] size = new int[2]; + try { + BitmapFactory.Options onlyBoundsOptions = new BitmapFactory.Options(); + onlyBoundsOptions.inJustDecodeBounds = true; + BitmapFactory.decodeFile(path, onlyBoundsOptions); + int originalWidth = onlyBoundsOptions.outWidth; + int originalHeight = onlyBoundsOptions.outHeight; + // size[0] = originalWidth; + // size[1] = originalHeight; + + int degree = getBitmapDegree(path); + if (degree == 0) { + size[0] = originalWidth; + size[1] = originalHeight; + } else { + float hh = 800f; + float ww = 480f; + if (degree == 90 || degree == 270) { + hh = 480; + ww = 800; + } + int be = 1; + if (originalWidth > originalHeight && originalWidth > ww) { + be = (int) (originalWidth / ww); + } else if (originalWidth < originalHeight && originalHeight > hh) { + be = (int) (originalHeight / hh); + } + if (be <= 0) { + be = 1; + } + BitmapFactory.Options bitmapOptions = new BitmapFactory.Options(); + bitmapOptions.inSampleSize = be; + bitmapOptions.inDither = true; + bitmapOptions.inPreferredConfig = Bitmap.Config.ARGB_8888; + Bitmap bitmap = BitmapFactory.decodeFile(path, bitmapOptions); + bitmap = rotateBitmapByDegree(bitmap, degree); + size[0] = bitmap.getWidth(); + size[1] = bitmap.getHeight(); + } + } catch (Exception e) { + e.printStackTrace(); + } + return size; + } + + + // The image file is rotated locally, and the path of the image file after rotation is returned. + public static String getImagePathAfterRotate(final String imagePath) { + try { + Bitmap originBitmap = BitmapFactory.decodeFile(imagePath, null); + int degree = ImageUtil.getBitmapDegree(imagePath); + if (degree == 0) { + return imagePath; + } else { + Bitmap newBitmap = ImageUtil.rotateBitmapByDegree(originBitmap, degree); + String oldName = FileUtil.getName(imagePath); + File newImageFile = FileUtil.generateFileName(oldName, FileUtil.getDocumentCacheDir(TUIConfig.getAppContext())); + if (newImageFile == null) { + return imagePath; + } + ImageUtil.storeBitmap(newImageFile, newBitmap); + newBitmap.recycle(); + return newImageFile.getAbsolutePath(); + } + } catch (Exception e) { + return imagePath; + } + } + + /** + * + * Convert image to circle + * + * @param bitmap Pass in a Bitmap object + * @return + */ + public static Bitmap toRoundBitmap(Bitmap bitmap) { + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + float roundPx; + float left; + float top; + float right; + float bottom; + float dstLeft; + float dstTop; + float dstRight; + float dstBottom; + if (width <= height) { + roundPx = width / 2; + left = 0; + top = 0; + right = width; + bottom = width; + height = width; + dstLeft = 0; + dstTop = 0; + dstRight = width; + dstBottom = width; + } else { + roundPx = height / 2; + float clip = (width - height) / 2; + left = clip; + right = width - clip; + top = 0; + bottom = height; + width = height; + dstLeft = 0; + dstTop = 0; + dstRight = height; + dstBottom = height; + } + + Bitmap output = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(output); + + final int color = 0xff424242; + final Paint paint = new Paint(); + final Rect src = new Rect((int) left, (int) top, (int) right, (int) bottom); + final Rect dst = new Rect((int) dstLeft, (int) dstTop, (int) dstRight, (int) dstBottom); + final RectF rectF = new RectF(dst); + + paint.setAntiAlias(true); + + canvas.drawARGB(0, 0, 0, 0); + paint.setColor(color); + + // canvas.drawRoundRect(rectF, roundPx, roundPx, paint); + canvas.drawCircle(roundPx, roundPx, roundPx, paint); + + paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); + canvas.drawBitmap(bitmap, src, dst, paint); + + return output; + } + + public static Bitmap zoomImg(Bitmap bm, int targetWidth, int targetHeight) { + int srcWidth = bm.getWidth(); + int srcHeight = bm.getHeight(); + float widthScale = targetWidth * 1.0f / srcWidth; + float heightScale = targetHeight * 1.0f / srcHeight; + Matrix matrix = new Matrix(); + matrix.postScale(widthScale, heightScale, 0, 0); + Bitmap bmpRet = Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.RGB_565); + Canvas canvas = new Canvas(bmpRet); + Paint paint = new Paint(); + canvas.drawBitmap(bm, matrix, paint); + return bmpRet; + } + + /** + * + * Get the image file path based on the image UUID and type + * @param uuid + * @param imageType V2TIMImageElem.V2TIM_IMAGE_TYPE_THUMB , V2TIMImageElem.V2TIM_IMAGE_TYPE_ORIGIN , + * V2TIMImageElem.V2TIM_IMAGE_TYPE_LARGE + * @return path + */ + public static String generateImagePath(String uuid, int imageType) { + String imageTypePreStr; + if (imageType == V2TIMImageElem.V2TIM_IMAGE_TYPE_THUMB) { + imageTypePreStr = "thumb_"; + } else if (imageType == V2TIMImageElem.V2TIM_IMAGE_TYPE_ORIGIN) { + imageTypePreStr = "origin_"; + } else if (imageType == V2TIMImageElem.V2TIM_IMAGE_TYPE_LARGE) { + imageTypePreStr = "large_"; + } else { + imageTypePreStr = "other_"; + } + return TUIConfig.getImageDownloadDir() + imageTypePreStr + uuid; + } + + public static String getGroupConversationAvatar(String conversationID) { + SPUtils spUtils = SPUtils.getInstance(TUILogin.getSdkAppId() + SP_IMAGE); + final String savedIcon = spUtils.getString(conversationID, ""); + if (!TextUtils.isEmpty(savedIcon) && new File(savedIcon).isFile() && new File(savedIcon).exists()) { + return savedIcon; + } + return ""; + } + + public static void setGroupConversationAvatar(String conversationId, String url) { + SPUtils spUtils = SPUtils.getInstance(TUILogin.getSdkAppId() + SP_IMAGE); + spUtils.put(conversationId, url); + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/LayoutUtil.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/LayoutUtil.java new file mode 100644 index 00000000..2e4ff0d5 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/LayoutUtil.java @@ -0,0 +1,17 @@ +package com.tencent.qcloud.tuikit.timcommon.util; + +import android.content.Context; +import android.content.res.Configuration; +import android.view.View; + +import com.tencent.qcloud.tuicore.ServiceInitializer; + +public class LayoutUtil { + + public static boolean isRTL() { + Context context = ServiceInitializer.getAppContext(); + Configuration configuration = context.getResources().getConfiguration(); + int layoutDirection = configuration.getLayoutDirection(); + return layoutDirection == View.LAYOUT_DIRECTION_RTL; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/MessageBuilder.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/MessageBuilder.java new file mode 100644 index 00000000..266cfd5e --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/MessageBuilder.java @@ -0,0 +1,34 @@ +package com.tencent.qcloud.tuikit.timcommon.util; + +import android.text.TextUtils; +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import com.tencent.qcloud.tuikit.timcommon.bean.TUIMessageBean; +import java.util.HashMap; + +public class MessageBuilder { + private static final String TAG = MessageBuilder.class.getSimpleName(); + + public static void mergeCloudCustomData(TUIMessageBean messageBean, String key, Object data) { + if (messageBean == null || messageBean.getV2TIMMessage() == null) { + return; + } + String cloudCustomData = messageBean.getV2TIMMessage().getCloudCustomData(); + Gson gson = new Gson(); + HashMap hashMap = null; + if (TextUtils.isEmpty(cloudCustomData)) { + hashMap = new HashMap(); + } else { + try { + hashMap = gson.fromJson(cloudCustomData, HashMap.class); + } catch (JsonSyntaxException e) { + TIMCommonLog.e(TAG, " mergeCloudCustomData error " + e.getMessage()); + } + } + if (hashMap != null) { + hashMap.put(key, data); + cloudCustomData = gson.toJson(hashMap); + } + messageBean.getV2TIMMessage().setCloudCustomData(cloudCustomData); + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/MessageParser.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/MessageParser.java new file mode 100644 index 00000000..3ca8b3bb --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/MessageParser.java @@ -0,0 +1,69 @@ +package com.tencent.qcloud.tuikit.timcommon.util; + +import android.text.TextUtils; +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import com.tencent.imsdk.v2.V2TIMMessage; +import com.tencent.qcloud.tuikit.timcommon.bean.MessageFeature; +import com.tencent.qcloud.tuikit.timcommon.bean.MessageRepliesBean; +import com.tencent.qcloud.tuikit.timcommon.bean.TUIMessageBean; +import java.util.HashMap; +import java.util.Map; + +public class MessageParser { + private static final String TAG = MessageParser.class.getSimpleName(); + + public static MessageRepliesBean parseMessageReplies(TUIMessageBean messageBean) { + V2TIMMessage message = messageBean.getV2TIMMessage(); + String cloudCustomData = message.getCloudCustomData(); + + try { + Gson gson = new Gson(); + HashMap hashMap = gson.fromJson(cloudCustomData, HashMap.class); + if (hashMap != null) { + Object repliesContentObj = hashMap.get(TIMCommonConstants.MESSAGE_REPLIES_KEY); + MessageRepliesBean repliesBean = null; + if (repliesContentObj instanceof Map) { + repliesBean = gson.fromJson(gson.toJson(repliesContentObj), MessageRepliesBean.class); + } + if (repliesBean != null) { + if (repliesBean.getVersion() > MessageRepliesBean.VERSION) { + return null; + } + return repliesBean; + } + } + } catch (JsonSyntaxException e) { + TIMCommonLog.e(TAG, " getCustomJsonMap error "); + } + return null; + } + + public static MessageFeature isSupportTyping(TUIMessageBean messageBean) { + String cloudCustomData = messageBean.getV2TIMMessage().getCloudCustomData(); + if (TextUtils.isEmpty(cloudCustomData)) { + return null; + } + try { + Gson gson = new Gson(); + HashMap featureHashMap = gson.fromJson(cloudCustomData, HashMap.class); + if (featureHashMap != null) { + Object featureContentObj = featureHashMap.get(TIMCommonConstants.MESSAGE_FEATURE_KEY); + MessageFeature messageFeature = null; + if (featureContentObj instanceof Map) { + messageFeature = gson.fromJson(gson.toJson(featureContentObj), MessageFeature.class); + } + if (messageFeature != null) { + if (messageFeature.getVersion() > MessageFeature.VERSION) { + return null; + } + + return messageFeature; + } + } + } catch (JsonSyntaxException e) { + TIMCommonLog.e(TAG, " isSupportTyping exception e = " + e); + } + return null; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/PopWindowUtil.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/PopWindowUtil.java new file mode 100644 index 00000000..4f517a55 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/PopWindowUtil.java @@ -0,0 +1,19 @@ +package com.tencent.qcloud.tuikit.timcommon.util; + +import android.graphics.drawable.ColorDrawable; +import android.view.Gravity; +import android.view.View; +import android.view.WindowManager; +import android.widget.PopupWindow; + +public class PopWindowUtil { + + public static PopupWindow popupWindow(View windowView, View parent, int x, int y) { + PopupWindow popup = new PopupWindow(windowView, WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT); + popup.setOutsideTouchable(true); + popup.setFocusable(true); + popup.setBackgroundDrawable(new ColorDrawable(0xAEEEEE00)); + popup.showAtLocation(windowView, Gravity.CENTER | Gravity.TOP, x, y); + return popup; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/ScreenUtil.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/ScreenUtil.java new file mode 100644 index 00000000..08567d9c --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/ScreenUtil.java @@ -0,0 +1,39 @@ +package com.tencent.qcloud.tuikit.timcommon.util; + +import android.content.Context; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.WindowManager; +import com.tencent.qcloud.tuicore.TUIConfig; + +public class ScreenUtil { + private static final String TAG = ScreenUtil.class.getSimpleName(); + + public static int getScreenHeight(Context context) { + DisplayMetrics metric = new DisplayMetrics(); + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + wm.getDefaultDisplay().getMetrics(metric); + return metric.heightPixels; + } + + public static int getScreenWidth(Context context) { + DisplayMetrics metric = new DisplayMetrics(); + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + wm.getDefaultDisplay().getMetrics(metric); + return metric.widthPixels; + } + + public static int getPxByDp(float dp) { + float scale = TUIConfig.getAppContext().getResources().getDisplayMetrics().density; + return (int) (dp * scale + 0.5f); + } + + public static int dip2px(float dpValue) { + final float scale = TUIConfig.getAppContext().getResources().getDisplayMetrics().density; + return (int) (dpValue * scale + 0.5f); + } + + public static float dp2px(float dpValue, DisplayMetrics displayMetrics) { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, displayMetrics); + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/SoftKeyBoardUtil.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/SoftKeyBoardUtil.java new file mode 100644 index 00000000..d41cd87d --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/SoftKeyBoardUtil.java @@ -0,0 +1,59 @@ +package com.tencent.qcloud.tuikit.timcommon.util; + +import android.content.Context; +import android.graphics.Rect; +import android.os.IBinder; +import android.util.DisplayMetrics; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; +import com.tencent.qcloud.tuicore.TUIConfig; + +public class SoftKeyBoardUtil { + public static void hideKeyBoard(IBinder token) { + InputMethodManager imm = (InputMethodManager) TUIConfig.getAppContext().getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.hideSoftInputFromWindow(token, 0); + } + } + + public static void hideKeyBoard(Window window) { + InputMethodManager imm = (InputMethodManager) TUIConfig.getAppContext().getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + if (isSoftInputShown(window)) { + imm.toggleSoftInput(0, 0); + } + } + } + + public static void showKeyBoard(Window window) { + InputMethodManager imm = (InputMethodManager) TUIConfig.getAppContext().getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + if (!isSoftInputShown(window)) { + imm.toggleSoftInput(0, 0); + } + } + } + + private static boolean isSoftInputShown(Window window) { + View decorView = window.getDecorView(); + int screenHeight = decorView.getHeight(); + Rect rect = new Rect(); + decorView.getWindowVisibleDisplayFrame(rect); + return screenHeight - rect.bottom - getNavigateBarHeight(window.getWindowManager()) >= 0; + } + + private static int getNavigateBarHeight(WindowManager windowManager) { + DisplayMetrics metrics = new DisplayMetrics(); + windowManager.getDefaultDisplay().getMetrics(metrics); + int usableHeight = metrics.heightPixels; + windowManager.getDefaultDisplay().getRealMetrics(metrics); + int realHeight = metrics.heightPixels; + if (realHeight > usableHeight) { + return realHeight - usableHeight; + } else { + return 0; + } + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TIMCommonConstants.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TIMCommonConstants.java new file mode 100644 index 00000000..937a2de4 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TIMCommonConstants.java @@ -0,0 +1,11 @@ +package com.tencent.qcloud.tuikit.timcommon.util; + +public class TIMCommonConstants { + public static final String MESSAGE_REPLY_KEY = "messageReply"; + public static final String MESSAGE_REPLIES_KEY = "messageReplies"; + public static final String MESSAGE_REACT_KEY = "messageReact"; + public static final String MESSAGE_FEATURE_KEY = "messageFeature"; + public static final String CHAT_SETTINGS_SP_NAME = "chat_settings_sp"; + public static final String CHAT_REPLY_GUIDE_SHOW_SP_KEY = "chat_reply_guide_show"; + +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TIMCommonLog.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TIMCommonLog.java new file mode 100644 index 00000000..3b642aa6 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TIMCommonLog.java @@ -0,0 +1,77 @@ +package com.tencent.qcloud.tuikit.timcommon.util; + +import com.tencent.imsdk.common.IMLog; + +public class TIMCommonLog extends IMLog { + private static final String PRE = "TIMCommon-"; + + private static String mixTag(String tag) { + return PRE + tag; + } + + /** + * + * print INFO level log + * + * @param strTag TAG + * @param strInfo + */ + public static void v(String strTag, String strInfo) { + IMLog.v(mixTag(strTag), strInfo); + } + + /** + * + * print DEBUG level log + * + * @param strTag TAG + * @param strInfo + */ + public static void d(String strTag, String strInfo) { + IMLog.d(mixTag(strTag), strInfo); + } + + /** + * + * print INFO level log + * + * @param strTag TAG + * @param strInfo + */ + public static void i(String strTag, String strInfo) { + IMLog.i(mixTag(strTag), strInfo); + } + + /** + * + * print WARN level log + * + * @param strTag TAG + * @param strInfo + */ + public static void w(String strTag, String strInfo) { + IMLog.w(mixTag(strTag), strInfo); + } + + /** + * + * print WARN level log + * + * @param strTag TAG + * @param strInfo + */ + public static void w(String strTag, String strInfo, Throwable e) { + IMLog.w(mixTag(strTag), strInfo + e.getMessage()); + } + + /** + * + * print ERROR level log + * + * @param strTag TAG + * @param strInfo + */ + public static void e(String strTag, String strInfo) { + IMLog.e(mixTag(strTag), strInfo); + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TIMCommonUtil.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TIMCommonUtil.java new file mode 100644 index 00000000..dfa25303 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TIMCommonUtil.java @@ -0,0 +1,12 @@ +package com.tencent.qcloud.tuikit.timcommon.util; + +import static com.tencent.qcloud.tuicore.TUIConstants.TUIConversation.CONVERSATION_C2C_PREFIX; +import static com.tencent.qcloud.tuicore.TUIConstants.TUIConversation.CONVERSATION_GROUP_PREFIX; + +public class TIMCommonUtil { + + public static String getConversationIdByID(String chatID, boolean isGroup) { + String conversationIdPrefix = isGroup ? CONVERSATION_GROUP_PREFIX : CONVERSATION_C2C_PREFIX; + return conversationIdPrefix + chatID; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TUIUtil.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TUIUtil.java new file mode 100644 index 00000000..d4b680c4 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TUIUtil.java @@ -0,0 +1,82 @@ +package com.tencent.qcloud.tuikit.timcommon.util; + +import android.app.Activity; +import android.app.Application; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.text.TextUtils; +import com.tencent.imsdk.v2.V2TIMManager; +import com.tencent.qcloud.tuicore.TUIThemeManager; +import com.tencent.qcloud.tuikit.timcommon.R; +import java.lang.reflect.Method; + +public class TUIUtil { + private static String currentProcessName = ""; + + public static String getProcessName() { + if (!TextUtils.isEmpty(currentProcessName)) { + return currentProcessName; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + currentProcessName = Application.getProcessName(); + return currentProcessName; + } + + try { + final Method declaredMethod = Class.forName("android.app.ActivityThread", false, Application.class.getClassLoader()) + .getDeclaredMethod("currentProcessName", (Class[]) new Class[0]); + declaredMethod.setAccessible(true); + final Object invoke = declaredMethod.invoke(null, new Object[0]); + if (invoke instanceof String) { + currentProcessName = (String) invoke; + } + } catch (Throwable e) { + e.printStackTrace(); + } + + return currentProcessName; + } + + public static int getDefaultGroupIconResIDByGroupType(Context context, String groupType) { + if (context == null || TextUtils.isEmpty(groupType)) { + return R.drawable.core_default_group_icon_community; + } + if (TextUtils.equals(groupType, V2TIMManager.GROUP_TYPE_WORK)) { + return TUIThemeManager.getAttrResId(context, R.attr.core_default_group_icon_work); + } else if (TextUtils.equals(groupType, V2TIMManager.GROUP_TYPE_MEETING)) { + return TUIThemeManager.getAttrResId(context, R.attr.core_default_group_icon_meeting); + } else if (TextUtils.equals(groupType, V2TIMManager.GROUP_TYPE_PUBLIC)) { + return TUIThemeManager.getAttrResId(context, R.attr.core_default_group_icon_public); + } else if (TextUtils.equals(groupType, V2TIMManager.GROUP_TYPE_COMMUNITY)) { + return TUIThemeManager.getAttrResId(context, R.attr.core_default_group_icon_community); + } + return R.drawable.core_default_group_icon_community; + } + + public static Drawable newDrawable(Drawable drawable) { + if (drawable == null) { + return null; + } + Drawable.ConstantState state = drawable.getConstantState(); + if (state != null) { + return state.newDrawable().mutate(); + } + return drawable.mutate(); + } + + public static String identityHashCode(Object object) { + return System.identityHashCode(object) + ""; + } + + + public static boolean isActivityDestroyed(Context context) { + if (context instanceof Activity) { + if (((Activity) context).isFinishing() || ((Activity) context).isDestroyed()) { + return true; + } + } + return false; + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TextUtil.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TextUtil.java new file mode 100644 index 00000000..8b7763e1 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TextUtil.java @@ -0,0 +1,155 @@ +package com.tencent.qcloud.tuikit.timcommon.util; + +import android.graphics.Path; +import android.graphics.RectF; +import android.graphics.Region; +import android.text.Layout; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.TextPaint; +import android.text.method.LinkMovementMethod; +import android.text.style.ClickableSpan; +import android.text.style.URLSpan; +import android.text.util.Linkify; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import com.tencent.qcloud.tuicore.TUIThemeManager; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +public class TextUtil { + + public static final Pattern PHONE_NUMBER_PATTERN = + Pattern.compile("(\\+?(\\d{1,4}[-\\s]?)?)?(\\(?\\d+\\)?[-\\s]?)?[\\d\\s-]{5,14}"); + + public static void linkifyUrls(TextView textView) { + Linkify.addLinks(textView, Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES); + Linkify.addLinks(textView, PHONE_NUMBER_PATTERN, "tel:"); + SpannableString spannableString = new SpannableString(textView.getText()); + + URLSpan[] urlSpans = spannableString.getSpans(0, spannableString.length(), URLSpan.class); + int urlColor = 0xFF6495ED; + if (TUIThemeManager.getInstance().getCurrentTheme() != TUIThemeManager.THEME_LIGHT) { + urlColor = 0xFF87CEFA; + } + if (urlSpans != null) { + for (URLSpan span : urlSpans) { + int start = spannableString.getSpanStart(span); + int end = spannableString.getSpanEnd(span); + spannableString.removeSpan(span); + spannableString.setSpan(new TextLinkSpan(span.getURL(), urlColor), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + GestureDetector gestureDetector = new GestureDetector(textView.getContext(), new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onSingleTapUp(MotionEvent e) { + if (!textView.isActivated()) { + return false; + } + ClickableSpan[] spans = findSpansByLocation(textView, Math.round(e.getX()), Math.round(e.getY())); + if (spans != null && spans.length > 0) { + ClickableSpan span = spans[0]; + span.onClick(textView); + } + return false; + } + }); + textView.setText(spannableString); + textView.setMovementMethod(new LinkMovementMethod() { + @Override + public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) { + gestureDetector.onTouchEvent(event); + return false; + } + }); + } + + public static ClickableSpan[] findSpansByLocation(TextView textView, int x, int y) { + if (textView == null || !(textView.getText() instanceof Spannable)) { + return null; + } + Spannable spannable = (Spannable) textView.getText(); + Layout layout = textView.getLayout(); + int offset = getPreciseOffset(textView, x, y); + ClickableSpan[] spans = spannable.getSpans(offset, offset, ClickableSpan.class); + List result = new ArrayList<>(); + for (ClickableSpan span : spans) { + int spanStart = spannable.getSpanStart(span); + int spanEnd = spannable.getSpanEnd(span); + Path path = new Path(); + layout.getSelectionPath(spanStart, spanEnd, path); + RectF rect = new RectF(); + path.computeBounds(rect, true); + Region region = new Region(); + Region pathClipRegion = new Region((int) rect.left, (int) rect.top, (int) rect.right, (int) rect.bottom); + region.setPath(path, pathClipRegion); + if (region.contains(x, y)) { + result.add(span); + } + } + return result.toArray(new ClickableSpan[] {}); + } + + private static int getPreciseOffset(TextView textView, int x, int y) { + Layout layout = textView.getLayout(); + if (layout != null) { + int topVisibleLine = layout.getLineForVertical(y); + int offset = layout.getOffsetForHorizontal(topVisibleLine, x); + + int offsetX = (int) layout.getPrimaryHorizontal(offset); + + if (offsetX > x) { + return layout.getOffsetToLeftOf(offset); + } else { + return offset; + } + } else { + return -1; + } + } + + public static class TextLinkSpan extends URLSpan { + private final int color; + + public TextLinkSpan(String url, int color) { + super(url); + this.color = color; + } + + @Override + public void updateDrawState(@NonNull TextPaint ds) { + ds.setColor(color); + } + } + + public static class ForegroundColorClickableSpan extends ClickableSpan { + private final int color; + private final View.OnClickListener listener; + + public ForegroundColorClickableSpan(int color, View.OnClickListener listener) { + super(); + this.color = color; + this.listener = listener; + } + + @Override + public void updateDrawState(@NonNull TextPaint ds) { + ds.setColor(color); + } + + @Override + public void onClick(@NonNull View widget) { + if (listener != null) { + listener.onClick(widget); + } + } + } +} diff --git a/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/ThreadUtils.java b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/ThreadUtils.java new file mode 100644 index 00000000..4782eb95 --- /dev/null +++ b/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/ThreadUtils.java @@ -0,0 +1,38 @@ +package com.tencent.qcloud.tuikit.timcommon.util; + +import android.os.Handler; +import android.os.Looper; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class ThreadUtils { + private static final Handler handler = new Handler(Looper.getMainLooper()); + private static final ExecutorService executors = Executors.newCachedThreadPool(); + + private ThreadUtils() {} + + public static void execute(Runnable runnable) { + executors.execute(runnable); + } + + public static boolean isOnMainThread() { + return Thread.currentThread() == Looper.getMainLooper().getThread(); + } + + public static void runOnUiThread(Runnable runnable) { + if (isOnMainThread()) { + runnable.run(); + } else { + postOnUiThread(runnable); + } + } + + public static boolean postOnUiThread(Runnable runnable) { + return handler.post(runnable); + } + + public static boolean postOnUiThreadDelayed(Runnable runnable, long delayMillis) { + return handler.postDelayed(runnable, delayMillis); + } +} diff --git a/timcommon/src/main/res-light/drawable-ldrtl/chat_bubble_other_bg_light.xml b/timcommon/src/main/res-light/drawable-ldrtl/chat_bubble_other_bg_light.xml new file mode 100644 index 00000000..e68d78c9 --- /dev/null +++ b/timcommon/src/main/res-light/drawable-ldrtl/chat_bubble_other_bg_light.xml @@ -0,0 +1,18 @@ + + + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res-light/drawable-ldrtl/chat_bubble_self_bg_light.xml b/timcommon/src/main/res-light/drawable-ldrtl/chat_bubble_self_bg_light.xml new file mode 100644 index 00000000..4803d45a --- /dev/null +++ b/timcommon/src/main/res-light/drawable-ldrtl/chat_bubble_self_bg_light.xml @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res-light/drawable-xxhdpi/core_default_group_icon_community_light.png b/timcommon/src/main/res-light/drawable-xxhdpi/core_default_group_icon_community_light.png new file mode 100644 index 0000000000000000000000000000000000000000..67d36c20fa2caccdd4b69b041c7aee0bebaf0946 GIT binary patch literal 5871 zcmd^D_d6S2)K4OcnoVsjDPkA3i%M)&n;NxSqb2sxZ}B>=Q*Ey&vVYX=bQusJx$u{?AHMR0PRC9H6zlB`)^QQBh5w& zND9&d^fA&@0lXi%wFLk$S3OiyHVL%Z&8L3*c*^fv1aSn1XEn6f_<1-Xq?8y{F2gND zTijc1_iU!C_GDshw2_IX+sOHc ze2DeNbxUXC&J_FiFY4-=rrdO`kSii9uW))}^FArg*BN?$y7*dHRS1pg|#a%|Lp z>K1vfALRz_6Nngg2)TuqwDZ(J31Dp!5gzIYEvS6ww0ZB8z9eEzo1~(wm$*RJW$c}&4ZWa$@h)}-Uckvq^DoCWAOp4)lLkDAI^1qchf7M zh^eEk7}6{TwW$wh7TkO7>(W2!{2W#u9{#6jG-R|cmVG(x{|kZpsOo05*=yMJWoV(8@N-pOV`RQ-Mcs`7Rf53GLY=o`-$13!?aNHpLf4K)2E1jE zjQJpyQ=dafOC*;ozDD~s6ugj>w(4Rm*{>9{3Oc^JIeGEJ1S={&LsRh7R@XwKl34O2 z9SY9la0nWn)N8D%wU>n- zpoWWe$!@7A2*Qthj8XsyS-kiW2t#cnif1v*x$%XD^pjS-6x+iAUdwEwfQUZ$nsKTN1d zVqL4)L4f&Uw!j#~NLH4#&B0;)ap(a>*Cv_WuS?I(J#h!X(SGh7L=6w(;+pvpP$ckX zR$UxzvN*cFiKZ?;%S-h4pQ9WQg8_&Uz`VBI-1?=QPwuJ5TTiJZF1l!n-~45Dw9Hpj zv2!8vTN~SF0G!qN*wKKK5HIiB4k#VW51pp2n76?y4}wmu?n?Y)uWkKw{x5%7X%n zSN3b&?&Vsd$yAA^+bWzUAm5lQfgoe*Nt5oIH&@Y5WY;a&eC9mac4JLcqN=cQRn&F{ z@ci3CYOi<}YQUS6ie6q{b#ub$Lc?N+9fWxvK*6D*ZIh-IdZN60f9Yj@CfKVx?@<8+ z$|7W>XnKAr8P{F=Do+6-AJNG_TYMGWK9tVmsUD0cfA3?b7fK@d}r9=#87%Hz6zII

    O2SLg>y+cnkG%rULWVu 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 LdTQ@g?4te$6A%k+ literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res-light/drawable-xxhdpi/core_default_group_icon_meeting_light.png b/timcommon/src/main/res-light/drawable-xxhdpi/core_default_group_icon_meeting_light.png new file mode 100644 index 0000000000000000000000000000000000000000..f32ec4721c14159fef1698db571130ab2f048a75 GIT binary patch literal 3318 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 b{3GR=yUl?un$NwCe*j%A1I;RR81jDr;7#TW literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res-light/drawable-xxhdpi/core_default_group_icon_work_light.png b/timcommon/src/main/res-light/drawable-xxhdpi/core_default_group_icon_work_light.png new file mode 100644 index 0000000000000000000000000000000000000000..9002d453d754b56c28e6dbef7d9ab97870a1f103 GIT binary patch literal 4142 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 fP?Q~ literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res-light/drawable-xxhdpi/core_title_bar_back_light.png b/timcommon/src/main/res-light/drawable-xxhdpi/core_title_bar_back_light.png new file mode 100644 index 0000000000000000000000000000000000000000..83d658d4a1880408323edfe9094904917b0a1c9f GIT binary patch literal 1299 zcmV+u1?>8XP)Px(%}GQ-RCr$Pn@@-vRUF5^?`0B{-MzGzx(oIo_M~1EPl_kot%#Mj3o0UE%gkgq zh}}}xgGI)JB6x_p|7KnaD84?nUq<_29vS;6V{q5dX23r9vk8l^5p8Bbn)@ zn}3t!1%|+6k~j0o_xI=h-tRG(R}%qdm^2eGP-0YJpcp8vISZo{qm&dY#q&Jwd0tJu zw|1+&yQ=5rqd?5e%w#q;HmYd^(^Du4gyT3b12_X$)!y(L2#?RRH9L5Es&ifJ_t7Cjjo$lp)8i0|9VR2=Sbzpy7m~fY`SE7-Q@+022v7E>BKQo`@n(Dpb-H zr6?eULgC?Bt@Z_g{c%8uXmfmg{8*_}x{($KtDIOz%}3=Sb8~YKRIAmm02}~N1(1mb zR6gXJTrT(I($dnuX#>(xr6?e_Z69KcT?KG34hVqn02~!U{Fz1|9fP8P6pO`!l}cq3 zz`YGXlv;ks<#I=smzRH2-|1W2y~V(7n~!?9`T6++VHiq<+@ApCC(ij{-}iq>Gmy5G zqU67@u&{4uXGi*whZ2DN3gCzk;>WZDX$y)1vbeZ7vAw%K)quOtAGbrDwx zAqE*pOHj1*{WZqe>oGuNq{(p3&-lJy8gwALLeUg*mWbYt@(`&A<#1K{eohGS#vmkS ztxPKt^AxQ}G*3h;QSPD2i1N4VKse`b`o90_umDL^ipoU_g+jhotF6ZXsV8TQv3G+Y zI2{2|#pqW4j8tFKq!b0jb={LuT~(Tk4p3#Q(xZdrzGesCi=)|(-e+JCi*B&THY_aUO`a=AR*>pqQOipApoFbuyWBDq4SPasthYBW{| z8v-^(=H{4sORkpfPD!~fLF)H#-1Mfo;NKj)qCxC>y9L%OH)%*Cx^Z}vJs5yx~7j- zx2jeTTTNgMIVpuexH~rf!P_>U`q9}gV{sa zY)WRgX*=t>{rGN2=RW;-fNqRwpu|u$Pz;pTjLj&;D5W)}+ytXJp>r4PyH@}J002ov JPDHLkV1ipjY104z literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res-light/drawable/chat_bubble_other_bg_light.xml b/timcommon/src/main/res-light/drawable/chat_bubble_other_bg_light.xml new file mode 100644 index 00000000..ad3338f1 --- /dev/null +++ b/timcommon/src/main/res-light/drawable/chat_bubble_other_bg_light.xml @@ -0,0 +1,18 @@ + + + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res-light/drawable/chat_bubble_self_bg_light.xml b/timcommon/src/main/res-light/drawable/chat_bubble_self_bg_light.xml new file mode 100644 index 00000000..493753cd --- /dev/null +++ b/timcommon/src/main/res-light/drawable/chat_bubble_self_bg_light.xml @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res-light/drawable/chat_reply_icon_light.png b/timcommon/src/main/res-light/drawable/chat_reply_icon_light.png new file mode 100644 index 0000000000000000000000000000000000000000..19e4b81e0854d37aed0aadd266948ec5e93b664e GIT binary patch literal 1142 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC#^NA%Cx&(BWL^R}E~ycoX}-P; zT0k}j17mw80}DtA5K93u0|WB{Mh0de%?J`(zyy~ySip>6gB0F2&*1?of8goj7?Qzy zHEMtUWe17c=SCNl92|8-J#=JuZ440QY%6PMWYtnpn<4Nj;uJ%VJL8~cbQg19G!IY)wb%*ui4)HQq6g+^{9WaK3*Ph>=;UwwmdmtVdC%;5i%WUZIU*j+(Os0Y zDlm9;$#0ft4+v#w{lF8RKxXU6CCLO-g`ALO2L zC#06!xoye$2G;)?i}T+5uwAzh?vIt-#d#w4h}@kAY|h(uR{HF`vvOub?1uc`kIW*Q zmzq9IRNA@WakfMB(!05zZmMl&*tRLtmt(epn~I;q@0#`7PH}W>n|w&EM=*28gxbSl zZ0Aaq5}KF9tUR(g?8mXZGYx(0m3tE>E9^SSz1a3tOU;)H2XnaRdnDwaGrP->vwYn- zzrz*_nHS7utX`$VRv~!Bf!p|~(1nRRGK7-ME*w9>w@3fCG**pKU^MqGRq{Wy`Ul(>gwfNr8@Nxy^jR z^7j$)Pn~8PrYbGI>&9)SdFkY|DSJ*I|E2s*(Px@|z|39TfB$gX++=X$XLR3LP;dPI zk!?Zc&Pw*6$CcCj1sU#TNXIUczUw2=HPN(Uf$N?yo-KY;vgOV%UTsoZBL6aH)%R1Y zHdG%J-JaLkt#;_l>&oA97evc1svODTd{K8Vu28(WZ37oD2sa#TZB1IhAE0-INoiU` z376gt>4yiOC9V$@%T|zPDLKFB!J!4+25yTic$t{io#<%dZq@=tVyAFGg!{yajwPyz zLfc$kX!<#v$%#mBTltpbVx4+xs#>M7ZopZVC7}_=AGA$kRy;IGD62?VAj{Q7MDpc( zAB_zYrREBrIrer-k)CGB1pgM3(ypE_Mdh0kuXUPi@9Zny#qH{ynK=Jo=BhMLmxt$T zj%>4){&Ll=Cqf}=cYsh<;{U7(DnU2wt5rHR?jPTO`w#oRiLvkN-YTsH<#|t6KbLh* G2~7aS!uvM> literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res-light/drawable/core_title_bar_bg_light.xml b/timcommon/src/main/res-light/drawable/core_title_bar_bg_light.xml new file mode 100644 index 00000000..7a5193b6 --- /dev/null +++ b/timcommon/src/main/res-light/drawable/core_title_bar_bg_light.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res-light/values/light_colors.xml b/timcommon/src/main/res-light/values/light_colors.xml new file mode 100644 index 00000000..5df62e8a --- /dev/null +++ b/timcommon/src/main/res-light/values/light_colors.xml @@ -0,0 +1,10 @@ + + + #FF000000 + #ECECEC + @color/core_bubble_bg_color_light + #888888 + + #679CE1 + #888888 + \ No newline at end of file diff --git a/timcommon/src/main/res-light/values/light_styles.xml b/timcommon/src/main/res-light/values/light_styles.xml new file mode 100644 index 00000000..16d8503c --- /dev/null +++ b/timcommon/src/main/res-light/values/light_styles.xml @@ -0,0 +1,28 @@ + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res-lively/drawable-ldrtl/chat_bubble_other_bg_lively.xml b/timcommon/src/main/res-lively/drawable-ldrtl/chat_bubble_other_bg_lively.xml new file mode 100644 index 00000000..5c1677a0 --- /dev/null +++ b/timcommon/src/main/res-lively/drawable-ldrtl/chat_bubble_other_bg_lively.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res-lively/drawable-ldrtl/chat_bubble_self_bg_lively.xml b/timcommon/src/main/res-lively/drawable-ldrtl/chat_bubble_self_bg_lively.xml new file mode 100644 index 00000000..b77ba545 --- /dev/null +++ b/timcommon/src/main/res-lively/drawable-ldrtl/chat_bubble_self_bg_lively.xml @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res-lively/drawable-xxhdpi/core_default_group_icon_community_lively.png b/timcommon/src/main/res-lively/drawable-xxhdpi/core_default_group_icon_community_lively.png new file mode 100644 index 0000000000000000000000000000000000000000..028d844611f8703ed1752b901dbd8ae6e22aceee GIT binary patch literal 5389 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 literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res-lively/drawable-xxhdpi/core_default_group_icon_meeting_lively.png b/timcommon/src/main/res-lively/drawable-xxhdpi/core_default_group_icon_meeting_lively.png new file mode 100644 index 0000000000000000000000000000000000000000..5480bcbd3943e5fb40a3a29d22620c15d4e5e7ec GIT binary patch literal 2973 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 T>haZn00000NkvXXu0mjfMnJ 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 literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res-lively/drawable-xxhdpi/core_selected_icon_lively.png b/timcommon/src/main/res-lively/drawable-xxhdpi/core_selected_icon_lively.png new file mode 100644 index 0000000000000000000000000000000000000000..c86e1ac4b8c4e237ee02781f9c3080bc7fd6b9b4 GIT binary patch literal 5096 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

    ?fU$XVZx1GuR2`Ar}qt z@?fmIsGuPRy9Z7;b9s)@4rQ|L+n|VsJOn>zXm9ROFHCQ(i^-G^Xl9cW)L*?@2*}bL zGawdnVPd;u_U6c8rsM>=s~6Pyyvyg)^J|M~GK6S)phKJ*b1TVo&Ta2_=3j-n0K}RIm?pp+I;$+?SUJtg(-N)n3FHl852 zE_9~29X|$NUY%%UeddtouE&~ z7*-lAq$bf_wn3iZ~JkTW|KE-LW}x{ZBcy6cm^lU7jYz@PU;qs?P@Ea zOndpEZJzs>J+%Bq^dJ|l(IUA+9vODSu1iVe_Qt9`@{QcJjW zp*$8PtpxbQY2R8}&O(rm?;J_cMQ=aSPwfLSH2l%fs|h`S?6$Wx891?WEYpR+c`nwQXXP=;X2 zzn32N?d~xlI??m(vw9*U^!uc)EMDBoId}5nqdMzE-)2*m?r%4P~_Ui`-~p~IYILXFVsw3X0-bV4pdh-8CLaoQ_L z4}&i3Bp6G{(n*E$0}YZlFe2WkY<<AKypaA2uj0Eq7fpunIBB_O9z2s zqjQ=a^i?I`GE3B6K;FGjSVA!#m&*wGxv~UXZ-I@prSH_g7vx2Sv*Lgfi5XJoUR3+< zWSm0<(E6jFgR<4QjN*dw%sfP84r0hypShz>#?ZYJ6QUHw-VJ*JY!kx%Mfe2WU1r|A z%)i^MCp5<58us8o<-F<|x9k+3)gSqv0{Jdi8$ZP33OAW_@l*RjC8MqF)*R;Wxd_z_ z4S&f{%j~_Rz%hT=zvkmV78ZBW+vGV-{UArmX}-WAg4+CRe_lutm(}y?_3*3a5+C1= z=M=IT`yX#x9uqZ<_qml#kJkPB&F*d{1A_F8ZDP&`zwvZJ6Jx=CJ2m|7>%tHZm3oFW zfnIOYS~nUPU*oSd-Aj8dSUIn5Tr;QDvj4O&9#HSS^Z9LuZ(H!eLHntR+l6z@yB|qQHYYFZ!S<&gd z6H$4s!KQW9W99nO_4#x0nNswL{~VDcZ(T(*oxtnUGua#pLViu!2P0_ZupoD~zh~+n zU`Prd3>elJ&=iW&S!iE1v!U literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res-lively/drawable-xxhdpi/core_title_bar_back_lively.png b/timcommon/src/main/res-lively/drawable-xxhdpi/core_title_bar_back_lively.png new file mode 100644 index 0000000000000000000000000000000000000000..20726344a37756f74c234df3b51d7dd4b32a41ba GIT binary patch literal 1677 zcmeAS@N?(olHy`uVBq!ia0vp^9zg8G!3HG1lub|sQY`6?zK#qG8~eHcB(ehe3dtTp zz6=aiY77hwEes65fIz8sVAd>&u`8WOFdE zG72#;16hnf$iOJY05T28V(?;=hO>hhHK1yk7#P|!8CaldqJT6AfSd=?1EHB0Fd|G` zzyw$Av49!Qb^>YB+-(eWGXrOVM`SSr1Gg{;GcwGYBLOrmGBYHiB*NFnDmgz_FEJ%Q zDOIl`w*aJz!KT6r$jnVGNmQuF&B-gas<2f8tFQvHLBje<3ScEA*|tg%z5xo(`9-M; zW_kvC21<5Z3JMA~MJZ`kK`w4k?LeNbQbtKhft9{~d3m{Bxv^e;QM$gNrKP35fswwE zkuFe$ZgFK^Nn(X=Ua>O75STeGsl~}fnFS@8`FRQ;a}$&DOG|8(lt3220mPjpnP~`{ z@`|C}0(wv%B%^PrXP^%^8>rO=Bx>bfl$i>&8Dzelp$%9iiWt-$8-0-FNREN{6f6q# zsvVb&K0Mg$xFq8qvI7$lpQnpsNCo5D*|r6~fda=rt9WoN_LyuEm@3hFae*OMo8%b@ zt&+f?E}>Yr=Zm}hjFvh%s?QDKaox3I(INvMmlL0V-!K1He(p%`l9Lm5r>B2=d++nR z+0*|%eKW7WIxaqbqSFEa4Hm9OAaz5fF>kwd1K*;#zI7_+a+@A!d@T2@`mFV-&7w-mO9q+*B9y>47X*m?%C(4s%?9@Hbo-lxVL$e*&N#+ z>xHMhzY+hUzvSjKoWnD78j?L$*{ZWJMK4dvKDfl;^-Xif`xy<7XDzlWQLA0% zcl6p}#TQwNxvSSQ_PJ~CEmb_YvO42~#Vrf$JDHL!=CQ2G^(s*Lk@(E&YEeT2|H3F|Pab#E z6WU*X+nQ{AutVWJ`w{kujy%T`u9(RR`b|}0S+6coXY=@wjt1Koi{{I(nqu^&WePS* z3oMv!ag^`Dbdl^wtTTd^%s%iW*Fo!W4d0TJ=M|=QCJ8HSWjyKK8~h_~O4W_?)1}NW z|BY(jT9xvvjpwZHL+=CENdI?@p}`uu5Hj`I|ROrQBTS(EO5Hb)sLtBnaBCyWo6h@~QsFYp>ZP zmjwR#z9Qw+_s7?>t+%&sZflTrKF$AXRux0Sr>(QUP20AH&8+tSwhUpFEq_*&NB`M= mNBDKWsZD#y#TtW>EtwWIbK|T-G@yGywqDY=}?* literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res-lively/drawable/chat_bubble_other_bg_lively.xml b/timcommon/src/main/res-lively/drawable/chat_bubble_other_bg_lively.xml new file mode 100644 index 00000000..a3cd187f --- /dev/null +++ b/timcommon/src/main/res-lively/drawable/chat_bubble_other_bg_lively.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res-lively/drawable/chat_bubble_self_bg_lively.xml b/timcommon/src/main/res-lively/drawable/chat_bubble_self_bg_lively.xml new file mode 100644 index 00000000..b7575804 --- /dev/null +++ b/timcommon/src/main/res-lively/drawable/chat_bubble_self_bg_lively.xml @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res-lively/drawable/chat_reply_icon_lively.png b/timcommon/src/main/res-lively/drawable/chat_reply_icon_lively.png new file mode 100644 index 0000000000000000000000000000000000000000..5e9158c43f6195623e9cf6941ac99634f1080056 GIT binary patch literal 1172 zcmV;F1Z(?=P)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NF{z*hZRA>d&T02Y>Q53yzmPFWy z0iz~_!o*M*?Tp68LJKWuPz(G7Yz)>&qF`*cF)Bu&Hn9<-hC-#W&_W9vKfz8-BqkPw zM2!$Juq43qo;x$_%6>Wm(k;_f=YxX#;>0_kyUaqo( zKdt;}cB)_C=Nx~gNvp49sn@i#?UfoBU2^nQKUB3P_0V|+xYk8lcQNP_q$yibU5XZ& z%u7;glE|7=w;q9D#ZoZ=ox7B!rTGDF=_-HR9^Gn(JYTfjt-o@ue!Te%c-U;0{Z%ec3N3}PNWZ(Q|ZR@sE^dJ z<)pgtV5U@EB?;(jXAhHJdlp--hatphDC?BE`Q%+#7NlEINUc!i8mcnA>h|M@rUs1!>}i9Z*Nvt^HBVD2&)e-$({w5#APaUi|Di6^ zCaPz!CNhnOf(zTX24t(mZWGrw83nME4wx95VD#km5wQ39T5Xjc6lK3MHaUw51=gpK zoUr+MwpC&}qzY|r>0{{Qh2RrdATg-iM~t` zjo&Taflzte`Mm@by;pkh%qSmg{oeHSysI%w9Ifd=>_^H@Vhdhkf? zgQ!g)^yDJ|UgG`b$OyZ$+2~1#h0BO$XOJUSyd@f9Vh4)TGR3>`;C_^pP!6SswEOo<4k;EU>guP)VrZXc1f{?))s m0`IB)vqzAKkg4O}ZT + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res-lively/values/lively_colors.xml b/timcommon/src/main/res-lively/values/lively_colors.xml new file mode 100644 index 00000000..c22b28d2 --- /dev/null +++ b/timcommon/src/main/res-lively/values/lively_colors.xml @@ -0,0 +1,10 @@ + + + #FFFFFFFF + #F8F8F9 + @color/core_bubble_bg_color_lively + #FFFFFF + + #ECA08E + #888888 + \ No newline at end of file diff --git a/timcommon/src/main/res-lively/values/lively_styles.xml b/timcommon/src/main/res-lively/values/lively_styles.xml new file mode 100644 index 00000000..caf8e022 --- /dev/null +++ b/timcommon/src/main/res-lively/values/lively_styles.xml @@ -0,0 +1,30 @@ + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res-serious/drawable-ldrtl/chat_bubble_other_bg_serious.xml b/timcommon/src/main/res-serious/drawable-ldrtl/chat_bubble_other_bg_serious.xml new file mode 100644 index 00000000..26df0f84 --- /dev/null +++ b/timcommon/src/main/res-serious/drawable-ldrtl/chat_bubble_other_bg_serious.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res-serious/drawable-ldrtl/chat_bubble_self_bg_serious.xml b/timcommon/src/main/res-serious/drawable-ldrtl/chat_bubble_self_bg_serious.xml new file mode 100644 index 00000000..b04976ac --- /dev/null +++ b/timcommon/src/main/res-serious/drawable-ldrtl/chat_bubble_self_bg_serious.xml @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res-serious/drawable-xxhdpi/core_default_group_icon_community_serious.png b/timcommon/src/main/res-serious/drawable-xxhdpi/core_default_group_icon_community_serious.png new file mode 100644 index 0000000000000000000000000000000000000000..1a2e2685cc2bae9d852160c9516375d29b5b3974 GIT binary patch literal 6023 zcmd^D^;;8A_us~d(M&*Tlx_q8k?vBMGy*b0T1sHRhyhXxIFxP$k#3~jC@EDchwl$Jo^#K6&U2r0&gY){iG%5DQIRu~0{{Rj9c^_(!ixRxAR{5nhI8(u zga!D@P)ik1In4SO0JyE9qpo5c@O&o+f;O4V=nXq<_h<>{9bji`nCn65C70YmKtv-S zEt3El#_l8xe6r`2@-FG*HySp!du{6G8%q%zkJj<}nX%D?xE{Rs+kk9K6m6D3jkJw9E zQV*u)?rHqTL>XZHW`c!2e1q&Pz@5A6OF$c>9BVI-SDmTxT#xf6SZT;B*fAhu`1w?G z=kUH!`FD!f&sV2l9XWwj>k||f}qKjMm0p=+fs|(B7 zBqHDz^>zBp0_#}F3aFpkEYocNA-~W_6`3GtH5!%Cbr6`wn27F|P(zj6Fa%s>U0kpJ zTKt{MN|qh~D7rseCIwDkQ=-s5>CGh5(5K@!1IH&K?M zH~U$xd@w9W`vFK7rH;TzKZoQ$LOX+#0dc#bFWaEXew<@u{6OgZ_3r>4)TZ($Iv9w% z<+9sfIjyQLd`l zKWcrp5~L4-@xS-)9{HISRCA4pGK%i-zU+*hhr^z*0GTECSEbGIRKhkC_+#F33z`G> z(gFJ2e&1)`n@6iAE-BQNAuuxd@@DMY6`9TsWXEbRHj2v@ws=(-`}S!3Qrt~I{)EK6 zEYi54hQyWfpi6wMH5qUY67PPK-qM#DGX9xm^Pc|#?j!A2KHtJ&=a<5D_OII$@S@~Gy7P*M6B&;$ zv-^^_*7%kjOMdYFQ@O+P=kQ68fiNL3EfHgacyH3KPkOe_O3uS~dPdmCTHk-TM#8Yt z_MjJcN3Ml%19()lX7 z7(+u{Im6kxz1OTgdbi92X8c$#@HqrcrH{kQbi^tw9f!p$t!5Z#{e}lDZHeD_6c^vW zJ^?b6j^c$+e+_Bw4_uKx#Fbu;Rm&wbg|xCOJF3FljR``z($IB z_Yr1&p5vTx3)Ftilr7F8?H+9$wad0q6oEKShjQKqR?0Gjh71q$U}J2UDXMqtyaHcW z((mYIv>vwp?^uD#LrLz&PwD&!-2m8Wf5GXkkEe(jibjYRE5Ob)8Be10Wtm(@BtO7vZ9nsgmR;p}MiF_2i}m2s zWKp>Rff9=!!F@WYpgZ5b%#cY|8;pEbtGGPGk*CX>O#zPqd?|ot<*%l=VTz^Al^{oz zZ0lFtS#}N}_nuaC)rD@{{am*ngx`;}ka zdkz_;>+@H;TIK6dSebtYlsi9OH&{Be$h_PG_B2+(K)xhjtyyK3x4^K;` zlkkI&&MuKlQBrHu>IyRTTFwMVlxx3sl3dIK1hW0K+qkQ_!a{Dvx6-g9sRS=J4%XS9 zh8#k9l+ECzruoDdCLNe;Dl_&8|Ju4j{?-q78!m%;GJ|WVcohK#mE+lAW+6*B1R-kq zByyb+j&(ndJ}lj~{~_!RqrrO0Ema6vbkxMIfxF>pGjII%KWO0+{MLd;9iN;bh;8RE zLcjwJV|vr!Zr94luG5&jchQoX=>?-Z;8X=NnM<@~;BYRt(;jkNvgsAmm1AIlYW>+& z%a+h$W$d9MkMdn-5+3xQANeHOFK~TCwpUADYh=@&Xb4)a<*Rh3-WGgJ*L+{l{RY&w z@Ez)mwbT~t4yuCY%XyLTsSW}gkH6=Y6i0XY$8_OIPFToVkEAc-c$%)ygaVL6%>rVjT-E?g9kvn|2AeB}ch+LN)g9ZM?wz8ui~|4&w9a zToX_?@#o{?*%7*F!K7y*5~79nDFxtIB7K%W{=N}bLXdiTLsqX|0W|`^XMWYrmB0Px za3{|%4D@p3J@X&=t~n=-rY=3*FVP)UEviu`%H^j*K=SaqhPDzg=F=Y&m=RT1G*(5; zw(4eoCloecRh7n41f;EP5H59a zXU?yMeyihSZKa84C(+uWQgdrrP!bMyaVL@8TR+Ei*6npt>~(xkh}QNwd1vKH*Rq*p z9%e>JD<`-6+mH53m;Ts#?ZkO)e}i%Ph0a=k1w21dnr+IzKL4Z|dn(=eNe0)EhRb+7u(nV+cD&`)qDDJGTz<;D|Y7BBo++I zJKY)O;E)Z5rWFhGOS-~~baR!8@K^Wvf&M6SOgquDY;xjm4Y%Sb4^-45AKa%Y;}z)e+9M)POJ%27oKrqm9{I#z zPQi|1bp|1SC~74!l}-M|#BNy=DHo^lCjdU{$?#eccsZC`QMOVq#9hH$1nZFs z!c(L_pvkqND*5ILVYRIS)52eB^eMLEZr;M@mTX-LL06X#<@k|l(@mF4e@$RxmNEm~ zao+rT5I7>_Sf^`F4>zZ_GJiZduV1%L_tNck-8$CD)43si*KVf&l`blu+f`vsaiQ?x zXGOEm1$NUaTCahgia7!noEi61g~w!1JI68Ulu0Dt&-2r!6#98l4*r>qSidh_V!fGH z$n2X?wA8_@KWcT7M|O>o`(au-jug>hGleh|=Nj>KM|>-IoTnoSbZ|(K$7F-A&sIkb z1nb_`elJv437iyfU(P&XTIrmW+I08Hy-p{XSB^+G?e4{I-8$_PGj%u)YC1WscCV-r3_&)~ zmAAHHw}Vx*a#(0p9MO`Tmf~A4#;hL~4jX;c-TC^5j`QJKA>@SFnHn>XR1OALs|T$T zrSspi$QnB7v&vevc6(!My|Y)yrk$x{uZ#Bm15)w) zciuYwzjz|?GE|(TyDie-jusxb-&Po5Mdc3~DD3G778|<%>sVZ!x;>#^kATG$1|36T z;-s5j%|guv1s?e&bOK}*bNWR}q-Z_EFV=P?;y`>gyUmaD)DAX(v<$a zuX!sl(2+5X??3h|U?$?e)k6tK3R=_mZGUVf53^nZx1AIum^5YYq5L8p!fMdUU%mYi zwH2-NFV@;pGF?QiS z+_RcPwC8-D;$*dVQGZ6S6p*)Sj~ckmpR*SS4SZ0i?GQY_{33r^ofr8#_7XF zeGH-J4XU^F!IYarX#Wgkz2&nv(l$HRW#LI1q~?9U%O-6^=-U$&d_3>dVRuhc=9+}F z(Ab7#{{V;!&kk~$UHp>O=uUTwvMMUlDf3v%6&R%6aER0)Xt(G!iiK~5(h#k7$23Kt zu5huzX@$uzW_qX9a0n&WTVKGjloc$%9(91+GyQamcNmuwNHR09Fs&P3Rf$4)I9tXP zg1rhbNuL?HfZvq>@1+DZKxDag{SGXDGq;9BrJ9ogNhXsCC_8R!6Dba(mjf`_zg+{+ z1<1T%N&9KXjWS0W@}aWP?Q0|r0PnvglCnM8sKZ?$qT9vf3(YzrD9N4#J+J#v+^4{` z^bdydz(O1D93e*Bab5{=te!zmzknvFPjMb6J_p5==nxL~q4}{(f@c%8P7!|h z%Hd~oO0aS;KM@h_w)ay|)6|l-C%-93-PM>*fgt`g+;=RX3ut5`+~cjM!(UxgAFaQj zG6cuC*f*X6NV;nbYKj`I@Z~{S22X;TdSq&Sd6XT<2;2WZ(j#|Ju=E70&ccLW1V;vC z0d;JnYKQ~e4mzrf(!5jT7h$g|rFp-ZjJDwHm+DNKBISSURH`W2^rV4MsK&eCQHc`o zQj_3X5avCRted;;cvR^n%tO<_Ur|V`*@`MFr|^+ zi&3Ov;_AtgJ%e#Xh8t@1H@U0(by^=wCEWRjv$QIvRS$j|?@jyjD1%|v3W8k8 z6iQZexr!!5&Q_wWneR_qTz~4P3bET1S>wd=3c=I;Y!eezw|#bg&-w<}1`p0@zwJr6 zqdhlt@8}YGhp5T%HQ~#)WI0)BV|;bM^RK!A_*IH=r%@1??VEtk2VIy%HYq^#B*A}Q zxYY57(g|UF+o4zpQ1#<7_3_^qeVi_xK8UyoFwysUx*ktK{YJjl!4!Zf1pea7gySde z@hI;n-;TQxw~we73lXbgu;Au5N>gQE{^Ad<$U3%*w>EcMW>pWZ{x%pvXe)Y;|3bA0 zguyafaUKU1f$ey3b@YEF2}c2Z(lwerCu2!IC|XyNfx+f9lvJmO{B#2iWewIdWYa_y zG69c|&6d53p~_MOTFD$#PES!y_RxU(-S9keBeeKR6*RIg7#21z|F$=mnI`!oId`Lu^d26vAL5AQNDp7ED^RL$QT-nq zNxxCG3-ggGiht%&r)UGd^{uXb{h41#jE0vqd-`tPn~`8Re1lrY)p!b$l4Q!AF96+t zb1?X4msegCo_gx=^?-szPOyQegFcMjg%Kyo%^fcMVe~T;GMX*nYzM0oGrlqS$7E2^ zSl)iH&WpM#_}^m-f6gV#d+d3seylvN zDltiW^`MyI^N?;*W`8XNt1eGX>M!DhIy!tGg_Xg&IhbolH%s(0#9ws%iwtjzEZcf& zDcs@O2Cw~L9{t?i?x7bYyH=9zLjivx61V(p;e^?^ij<^KF2${X0Wp+%sI=+MEP31{ lslYH(Umi!{sa*372XcKOy;hoOkie`1bTstUD^=kU{{eu5UX1_% literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res-serious/drawable-xxhdpi/core_default_group_icon_meeting_serious.png b/timcommon/src/main/res-serious/drawable-xxhdpi/core_default_group_icon_meeting_serious.png new file mode 100644 index 0000000000000000000000000000000000000000..76b9e1531f86004760983f1e51ecaa857a912b1b GIT binary patch literal 3260 zcmZvfXE+;xyTyYNGg^DM(da;`D7E)qwP%gmY80_)?LDs%wPQu7YV3;AsG6a6i`ZKT zRiRe@uKV@g`{BIj%kMeg-sd?d+0a0fftHIF001y(YpEIEc-a4ln(F5IT4`?F7#Yl1 zQw7lQgJ%~2pbyekQ#K8;BbIo3u?%nyTsLpUu23{NQR3*RZfHmxv+?AGYq@eooot`!jR)&aAw_1zO#xlcw<Hl?lu^%7_+D7_^2shmRaDU5IWCTA?bR`b$)(GMg2zId=YMLdk+lyuNZ zdZ)EZ8-zAwW;gax=}RutEpBG8TnNF^$ddg?sDJ+48;uPHBmt5r##t8}{;NyVhZ;kJ z1>By(N*1Pa1?#j4Ys-fPNUU!S08)rR>L$#eG4fkQvkd-7#rOo5%Va;5iHC)C1$~(S zlv&ns=02~#@T+!MI&|P$zX6$bD&JgxN9#~SgZ2esIFinA%JV6*%SS?Zc?3n*Cwy!I zFv$%rxenkJ$da%A@N(*|i#6%@gCC8fr%Qv)<`%1#QF8w2qF%1TVLKGQr)lK2Qa9)w zM9*1bn#5fnpkZYlG3FNZ!y$-V9`&t|l4i~d1t!;@o-tctGepqwpoL4xKPn}DCkH=9 z8n=-1GSS6Vz1Nwb4bT$4o$V;7E;J5qRMJYRW%+-hcQ#7(k`8(!?k_TWk)XkO+?@^i zVwfm*_w7$HM$dXGhKxv@+dRcKQ3C7*=*~}~cYYI-Ik=!!lkSA7v;K+z+PJW#*q<~| ziZ!-q0eOFSOtkcRijjpWMB%QWV2^XJQUS8^rS3@NeycFa>H%iWub7hyd%mjN0%Ayh zfvYNLq91?85ujKjaQi%6?u|0D!w)IFsasN->Q&S)^ibHsCjWXK`;5@OzDKUR`mcF6 z*sQ65e(104mlt72heh%`$qYu^0NYL<5W3M{>}Q^ZW0>;- z%B76$Gpg&bUCgll9)t`ZJj@wr-=EOf+0HGC>GVA_&kQLKfHv}^PyVP8`lPg%1~#B| z#TL>qAK@NPH3q4uGu<{@m~I$uyLcC6qCCWAiv;gc#9+2v8SOeR1Wp&ffb0BKf4%y1 z6FtKDu#m5 z8Z|3+=M`6*sp2D3IlHNBKGA;)?Cf27lg)?i!sQ1aORm+^Y@W8Pe2^VSP;P|qG^gnP zHqhra&x1cK27}5S6rUK`id*Lo`>vRalpebG#rKeLB;47llS3nFMYvN3j!sgv&rH1K z>jG3snaxJ(HJ9H81xx;fjHayviEc+@rF%&n$#PUjv=H~&jdZh_v-Ob<{wOf}71dCZ z*o#|#6WPaJ4YdBi3d;H-m~}E%t82y4Z~OP1wZyvk{0{7ZoMP2iT@kemZ>|oR_%$HD z!yuNMpzRNJjg?3PlG$U_Zkq(0xQp5j*H1xKnCsxr3>(N>ark#1CCRTcDrTJbgpYIX zqUtZz(%u2nrK`Zj0?(M4*?MD>{{$ptk!MgYxZlY#^b}Ui%@_59q@*?x0fZ(}d#vY1 zj#@>1f(u{_9ns(V1i;Hpdi7GF^lm_)A;04iJ!!FG(wOJ{*IReO&m#(LN zr9K=hV+jvQ=MoGwdLY7g#2ZjCoUv-bIK4LGAs)}H3M#WxwxOrDDk%OrKK(uxeY;GV0_n2)Px|<1 z^v|&K$o$7bm;E9F1^K_TT~|_7RKqj+XD*!2HdXtVmk){knBy9R$*g2fjLTX58$@+} z7xD{J`1X@Q%b`h-f%+xQ{Q0E(y93DSNzf%B{*tP2;lA@ z!%7YB$L`P-X`p@v)94CnoI!0Hs-izkDx-g!;Y?g<7l&W01%6MM7@thz6Mv$D?%-P= zmUiEbd)w|Wwp1iBc1&%SRV+$lqp{i@TuvEDJowZ1_?UroTG$a%``WsnEPI;kUJ}9u zu$k^@@!Nl(!#4^J(t*8=rOplEg@O3pt@^j3-w7*l2XZcq)*In!E7bE-Y)~RxnNExn z2-7LKlh3g`fwIQ|xF+<59*bMzMS1fAm7zcFyfn-C#Qvv{hAl)4GD6!-aWQB%RCl*MxR49r+K9wFsxXAuntCqa#Nwj(-O$lZF*I58N8g_njIi^h1#6=9v{ypq$ zSaD;A9cFFvwQvrUd2X*>=HYJ3_FG5| z$0CE8zCA?;-#n7`_1C*{F)yn`S*kn4#9PH7%8`@5VkJJ735^$=uxWxO^cbRL%&c^D z9R_|Us(ofts^;rCotF`YLSc(0)B^5`bv|xh(j_x2(nB0k`2J;uJ^^qw6KK>)d6Dhz&MVu*XYu%SrhMk( zDshdy#^w5tnw9U*4t-uJqf`(b9Mirr4_E{HoH!Z&nB>`)?>%A-&;Q)1!NwZrY}m`$ zC1bZ7rEbN645%0k$oMS&m2?LC4o_Rq@Kse>&ed7rJkF9_e<)&<@k&D0 zY(~6^xzhVz0P@4yCcxs3ic$+iZ5&}x#iIDQlF)53aS%!EL6b7I@HadQ)CU|+_zcjFwPv!Gdfye2>wEeg60zO*t>yl;*`?*Gv}b00001b5ch_0Itp) z=>Px@@JU2LRCr$PoeOYO)fvbC=k6oBc@W+a5hIF9VnBr|XpugI!N^#(DpFgKI*!)X zb}US5OSM94wRObSI&E$1Ock***p509Gt@p55Q+-;z{Eim0tMuege1G0ecXFb=R12( z*n~WGZ#M3|yXVd@!))%k_nz-}zwdnCcfNBjL(gM5*Kj{L7~?hTn*K!XQFk{jy9bi>|4A_9g+z<~SI8Dgo`m-B*?!g~d`fI04W4Lv?-oEN+@>0F1W) z(}n1treHhnO(QO(xx`WaJHRw;VMFb(O{uVxpe@K%^=%7b@>KwQ0CWHz1=n$K`+6XP zZUFECNI+wY8f(kfNVcpCa$U!4z*r9;U^O7f>TsU~2sW{c0-kw|^UF36W1-PCFNG>h zufCN7l?t-^$oBHY0yYDhG~GL6=hdZ$1?cMfww2uEODuCus;>_BOMr%6FVong4YlP< zS4^Xh@H$picf1L}>B?;9C1`2B_Yvr*N^9mN`&0$m^yD3ER{jaHxwL;?lUO{LKabb&~qK)R_kT7fPQ2^2^-l}0Pj1tNh0>88?X z1-d{aP$1n@8m&MVhy)6xo37Vrjh$GcK+NQ_63&zVS4UC;t^`dr$^=+9ftbk=Hv!Y4 zO27LwAfPdLwdC =o`z#*NZ+{aKg%!BH34@UWus@)RfXlOUk*lwUL&QaoJ0=!Y+ zv|XjqwDh3E0(^625w1DShcin(Lc3EBGTWbo4ZL(9fz^9rXi0FCd09cjb`5BPd_2za z#R@MToLYLuT}a5WEy+P z6w9%?H-}4(&D568z4yHcRyV~97PjL-OLP6Dv0glRaj952M&Odm0|!3nT(Pj50k^#w z#;czuFsuMI+tHwjrOqM1yf~{AS=z&BhQ~;v7E9ybshiU;gfB z(86RVIPRGo#9ikUWu(bH0ZA+Hhuu*;)DRUOdm(w^js;EHOdx_DVBPF8q0NKYNJX|i z)M?_X4IK#SN%LPQQI}&u6NL5z$CoR7_{)rvK|z+v|J!r#C7mR`L`t6CX0a_Y#r6T8H?$Q(EtbX$Vsm*)p@Q{~{s=boU~ z?u+9G??h1MV|~IRi$bZ@u+D{v_iM~byK=-hZ6yGdc$o0!-2mM2pdY!Q7zH;QC^G>YQkS2k9~KCiPw)Lu(id2!8uA@kacr3 zX!5x4tq$UjN!iutKYtv<&pzlpDS0CE=oHf3GSQDaCIv9Ii1q0o{I^*bL%>@t&d<6z z9`ub9{CMQT;;d@)kKT=7_1<{5Fw9>0`&ks2)%Op(HNM_yE z?vIOj->GKYv7kxtG)A5pWO#XYnGj+&!mCb$WB!H^_J>XQY10R50H^!Wl;>m9iZTCG zznB6g4eu{GBeHH*hVIU{!l*yw+^m~pL6iJRh@k|>6Bm`>igCU{X|?pYDFu4_TVXMM zDYcKBt=2flZD$o>c}-A2Ce2RgkXg4QQ4`m03gJkUcYA@-xyEO4j^ja-*L%ST51zlY z3?57PX{NcmknvTlwCE#K;x!SS1E#kgvGGSupCic+>oovM80U;^LU9ua2MilD@4 z@(Pv*f(tsVe>J)*U&AS8k*^~TmhQE5H!-vpPU!K;)%(<;Zy6uq=u(D zR}>S|@6YUs;em#z$Y{x;1SzPb69zmrqXe_Ycx`DkT}ztpySGO`ak|sYw<|!CAY}%M zyyiI-UVQhQAkH7&{|3bGA2;y$M^XIaPy#_O>q=dwY6F2zaNxO1N-%xo;6X2YFM?2Pag{8t(dbm-vdSc4a1jd8xLZu#7Lw2?{-*G#*u%Oi zQu=zsWN7u%njmgJ%U%Xfj?d&rT=zx@jqRpLmpUnV;!4mWkztXeP>5jD_)Q1#Cdl%; z1N8U)iupyXu5)~Tv=7f-VsAQB#?tS!>R7Nj?8xdBSAiz&BcakbO%7DR!%*yjs1~6F zh}3Axkw|hjMFBt>XH)79Ocb8F(b|PJ?}>FTvObU)hn(5Ej(qO z9Dj>6V9Ib0W{vh?+6XVsEYrkzD7Bb?QQZAW$iR+P0~?Pf@ZND#q&I0=QhM;Ird(?E zjFDbEduge#3hCCS$rznNs&8%$r(Lu-C}+h+W5)R#PUOqO+Fbl#iIIv)6-6Wd!2F>}>(T^4C} zM}p(}34Z)+N>IEIsnsR&9&#dsuDQKM$K5+3IMB%*=YVw!Bb^9ZI#O*q$2ThdxUVKC zJZPzf>DF}jmLZE8T)7wU#QYh8i5bn4!TAFLBu%jO{}w~e{G6`CL<`hLe3gheUj&W*%&WQ3242Ba&+FV z4ij|;<6<55fld>{EOXxNd%l@I)DWHT2+(By$oV$4qBunt;vWNFDu1arJl6Y zkLkk9`+lcmX0n_CI?l0T zN)Wf6`Pq2e*#O$#HRSmH^(`T6Y1YNstXw;1jsQ)?^S}RGF}`9|cn&tzIn*Ery1oqC z{%I{1pg3EBwpK~dV)wa&sM#7w___Fd!sNJiV+f5Mxi2kpj7G1TSt52f$YjT%T)<2B zp0XdmYK(#w-jB@j>J0@tU2++oCqdJ~yKleUeXUo9m&(fN3UpS)c=APLDbk1S2Cmx_ z>YAV#qT3C4Sp|AP5!pLIrdMeKWZr8Xh~;=amI7^$iB zCrHq==gH*?bTUOqadf_U-3oLM1yP{wS&U3uDbt;)#L_%pX|&SlG+&kMo~AT9yYlS1 zn$l>c(L?lV^?zBV(L*G5rO`^Ghv?N(8l4ltDvj<#qo1Bxg3I%0no^}e3k)>Xe9wKY z9i2J;w1Z=uu}3dV?#oTZ^qGDICnvUffSb}ZB^C0295QhA#!zyxptW;Ftxz`sJ@6$?3T2$`z)rsl6 zP+N`}*nBL}wZtf$HbZreBS1^oG&xVj{P_Tx`a~}*Re+|htsW|n16|iKpy@$rd5+sl zeNzQaOO0|}<}07aQJ@EUE7jlaOF)6PFFRMYRG?jj%l^YE(Dr5Ls+J10t8m$WSOwa? z>|E7Sfp!%x`wy!?+n1fIS}M@4!e#$q6=?gib5%cZ<$!D4KRVew?FIl6w zk5$#Rt^?S7Ym?nHKX{){vdHVcSRWeTb!>87`w|9or2?Iozy)XyZ7Hi+ADLk2@ofOc zDD$0Hu%+2P>hb!gi!r&nzHKEp`4akYEA5e0ch&jmvUG-PRg9xPx+Rb9tyz*tXp=L)u~VM|4g0-kw| z^UF36<77A`Vb`}UgvnRY_q!}JPMWPkE_ZMjpZ2hZ-UKwZsIj(ujbwWQq6NFUzI_%q z%{2hVTh5u-P@cRu1>JGP#n+lFF7n&02biWUY^WW!NkXQl?yjvC>`AY5jPb!-2F@1) z7_Pi=#{o`#An9En2V+kqpgp+z%Ce)WkWU6pzbxk(?gs~Byk>p#1YP&t0GM+bPz|6$ zrAr+HJqF+~2O0qFRj)Vk{LXo!_JebV6<}D-x_waZ|0x5s6#DvsjQ{`u07*qoM6N<$ Ef*z-RNdN!< literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res-serious/drawable-xxhdpi/core_default_group_icon_work_serious.png b/timcommon/src/main/res-serious/drawable-xxhdpi/core_default_group_icon_work_serious.png new file mode 100644 index 0000000000000000000000000000000000000000..708139984047070ea44e98dfd7dc698ff53e5560 GIT binary patch literal 4171 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 Ms=6xGaGR+A0^fkPeEz8sVAd>&u`8WOFdE zG72#;16hnf$iOJY05T28V(?;=hO>hhHK1yk7#P|!8CaldqJT6AfSd=?1EHB0Fd|G` zzyw$Av49!Qb^>YB+-(eWGXrOVM`SSr1Gg{;GcwGYBLOrmGBYHiB*NFnDmgz_FEJ%Q zDOIl`w*aJz!KT6r$jnVGNmQuF&B-gas<2f8tFQvHLBje<3ScEA*|tg%z5xo(`9-M; zW_kvC21<5Z3JMA~MJZ`kK`w4k?LeNbQbtKhft9{~d3m{Bxv^e;QM$gNrKP35fswwE zkuFe$ZgFK^Nn(X=Ua>O75STeGsl~}fnFS@8`FRQ;a}$&DOG|8(lt3220mPjpnP~`{ z@`|C}0(wv%B%^PrXP^%^8>rO=Bx>bfl$i>&8Dzelp$%9iiWt-$8-0-FNREN{6f6q# zsvVb&K0Mg$xFq8qvI7$lpQnpsNCo5D*|r6~fda=rt9WoN_LyuEm@3hFae*OMo8%b@ zt&+f?E}>Yr=Zm}hjFvh%s?QDKaox3I(INvMmlL0V-!K1He(p%`l9Lm5r>B2=d++nR z+0*|%eKW7WIxaqbqSFEa4Hm9OAaz5fF>kwd1K*;#zI7_+a+@A!d@T2@`mFV-&7w-mO9q+*B9y>47X*m?%C(4s%?9@Hbo-lxVL$e*&N#+ z>xHMhzY+hUzvSjKoWnD78j?L$*{ZWJMK4dvKDfl;^-Xif`xy<7XDzlWQLA0% zcl6p}#TQwNxvSSQ_PJ~CEmb_YvO42~#Vrf$JDHL!=CQ2G^(s*Lk@(E&YEeT2|H3F|Pab#E z6WU*X+nQ{AutVWJ`w{kujy%T`u9(RR`b|}0S+6coXY=@wjt1Koi{{I(nqu^&WePS* z3oMv!ag^`Dbdl^wtTTd^%s%iW*Fo!W4d0TJ=M|=QCJ8HSWjyKK8~h_~O4W_?)1}NW z|BY(jT9xvvjpwZHL+=CENdI?@p}`uu5Hj`I|ROrQBTS(EO5Hb)sLtBnaBCyWo6h@~QsFYp>ZP zmjwR#z9Qw+_s7?>t+%&sZflTrKF$AXRux0Sr>(QUP20AH&8+tSwhUpFEq_*&NB`M= mNBDKWsZD#y#TtW>EtwWIbK|T-G@yGywqDY=}?* literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res-serious/drawable/chat_bubble_other_bg_serious.xml b/timcommon/src/main/res-serious/drawable/chat_bubble_other_bg_serious.xml new file mode 100644 index 00000000..66767015 --- /dev/null +++ b/timcommon/src/main/res-serious/drawable/chat_bubble_other_bg_serious.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res-serious/drawable/chat_bubble_self_bg_serious.xml b/timcommon/src/main/res-serious/drawable/chat_bubble_self_bg_serious.xml new file mode 100644 index 00000000..5e21bac9 --- /dev/null +++ b/timcommon/src/main/res-serious/drawable/chat_bubble_self_bg_serious.xml @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res-serious/drawable/chat_reply_icon_serious.png b/timcommon/src/main/res-serious/drawable/chat_reply_icon_serious.png new file mode 100644 index 0000000000000000000000000000000000000000..19e4b81e0854d37aed0aadd266948ec5e93b664e GIT binary patch literal 1142 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC#^NA%Cx&(BWL^R}E~ycoX}-P; zT0k}j17mw80}DtA5K93u0|WB{Mh0de%?J`(zyy~ySip>6gB0F2&*1?of8goj7?Qzy zHEMtUWe17c=SCNl92|8-J#=JuZ440QY%6PMWYtnpn<4Nj;uJ%VJL8~cbQg19G!IY)wb%*ui4)HQq6g+^{9WaK3*Ph>=;UwwmdmtVdC%;5i%WUZIU*j+(Os0Y zDlm9;$#0ft4+v#w{lF8RKxXU6CCLO-g`ALO2L zC#06!xoye$2G;)?i}T+5uwAzh?vIt-#d#w4h}@kAY|h(uR{HF`vvOub?1uc`kIW*Q zmzq9IRNA@WakfMB(!05zZmMl&*tRLtmt(epn~I;q@0#`7PH}W>n|w&EM=*28gxbSl zZ0Aaq5}KF9tUR(g?8mXZGYx(0m3tE>E9^SSz1a3tOU;)H2XnaRdnDwaGrP->vwYn- zzrz*_nHS7utX`$VRv~!Bf!p|~(1nRRGK7-ME*w9>w@3fCG**pKU^MqGRq{Wy`Ul(>gwfNr8@Nxy^jR z^7j$)Pn~8PrYbGI>&9)SdFkY|DSJ*I|E2s*(Px@|z|39TfB$gX++=X$XLR3LP;dPI zk!?Zc&Pw*6$CcCj1sU#TNXIUczUw2=HPN(Uf$N?yo-KY;vgOV%UTsoZBL6aH)%R1Y zHdG%J-JaLkt#;_l>&oA97evc1svODTd{K8Vu28(WZ37oD2sa#TZB1IhAE0-INoiU` z376gt>4yiOC9V$@%T|zPDLKFB!J!4+25yTic$t{io#<%dZq@=tVyAFGg!{yajwPyz zLfc$kX!<#v$%#mBTltpbVx4+xs#>M7ZopZVC7}_=AGA$kRy;IGD62?VAj{Q7MDpc( zAB_zYrREBrIrer-k)CGB1pgM3(ypE_Mdh0kuXUPi@9Zny#qH{ynK=Jo=BhMLmxt$T zj%>4){&Ll=Cqf}=cYsh<;{U7(DnU2wt5rHR?jPTO`w#oRiLvkN-YTsH<#|t6KbLh* G2~7aS!uvM> literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res-serious/drawable/core_title_bar_bg_serious.xml b/timcommon/src/main/res-serious/drawable/core_title_bar_bg_serious.xml new file mode 100644 index 00000000..9471fbc3 --- /dev/null +++ b/timcommon/src/main/res-serious/drawable/core_title_bar_bg_serious.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res-serious/values/serious_colors.xml b/timcommon/src/main/res-serious/values/serious_colors.xml new file mode 100644 index 00000000..6c2f1dc0 --- /dev/null +++ b/timcommon/src/main/res-serious/values/serious_colors.xml @@ -0,0 +1,9 @@ + + + #FFFFFFFF + #F8F8F9 + @color/core_bubble_bg_color_serious + #FFFFFF + #5695E7 + #888888 + \ No newline at end of file diff --git a/timcommon/src/main/res-serious/values/serious_styles.xml b/timcommon/src/main/res-serious/values/serious_styles.xml new file mode 100644 index 00000000..702f9174 --- /dev/null +++ b/timcommon/src/main/res-serious/values/serious_styles.xml @@ -0,0 +1,30 @@ + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/anim/common_bottom_select_sheet_enter.xml b/timcommon/src/main/res/anim/common_bottom_select_sheet_enter.xml new file mode 100644 index 00000000..b3d4b5c6 --- /dev/null +++ b/timcommon/src/main/res/anim/common_bottom_select_sheet_enter.xml @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/timcommon/src/main/res/anim/common_bottom_select_sheet_exit.xml b/timcommon/src/main/res/anim/common_bottom_select_sheet_exit.xml new file mode 100644 index 00000000..802d60a3 --- /dev/null +++ b/timcommon/src/main/res/anim/common_bottom_select_sheet_exit.xml @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/timcommon/src/main/res/anim/core_popup_in_anim.xml b/timcommon/src/main/res/anim/core_popup_in_anim.xml new file mode 100644 index 00000000..74069b9f --- /dev/null +++ b/timcommon/src/main/res/anim/core_popup_in_anim.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/anim/core_popup_out_anim.xml b/timcommon/src/main/res/anim/core_popup_out_anim.xml new file mode 100644 index 00000000..7dd95120 --- /dev/null +++ b/timcommon/src/main/res/anim/core_popup_out_anim.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/color/common_bg_negative_btn.xml b/timcommon/src/main/res/color/common_bg_negative_btn.xml new file mode 100644 index 00000000..40cd9cd4 --- /dev/null +++ b/timcommon/src/main/res/color/common_bg_negative_btn.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + diff --git a/timcommon/src/main/res/color/common_bg_positive_btn.xml b/timcommon/src/main/res/color/common_bg_positive_btn.xml new file mode 100644 index 00000000..8322df96 --- /dev/null +++ b/timcommon/src/main/res/color/common_bg_positive_btn.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable-ldrtl/chat_bubble_other_transparent_bg.xml b/timcommon/src/main/res/drawable-ldrtl/chat_bubble_other_transparent_bg.xml new file mode 100644 index 00000000..3f74d665 --- /dev/null +++ b/timcommon/src/main/res/drawable-ldrtl/chat_bubble_other_transparent_bg.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable-ldrtl/chat_bubble_self_transparent_bg.xml b/timcommon/src/main/res/drawable-ldrtl/chat_bubble_self_transparent_bg.xml new file mode 100644 index 00000000..6c2ccc1b --- /dev/null +++ b/timcommon/src/main/res/drawable-ldrtl/chat_bubble_self_transparent_bg.xml @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable-ldrtl/chat_message_popup_fill_border_right.xml b/timcommon/src/main/res/drawable-ldrtl/chat_message_popup_fill_border_right.xml new file mode 100644 index 00000000..da1d6b05 --- /dev/null +++ b/timcommon/src/main/res/drawable-ldrtl/chat_message_popup_fill_border_right.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable-ldrtl/chat_message_popup_risk_content_border_left.xml b/timcommon/src/main/res/drawable-ldrtl/chat_message_popup_risk_content_border_left.xml new file mode 100644 index 00000000..cb1a1817 --- /dev/null +++ b/timcommon/src/main/res/drawable-ldrtl/chat_message_popup_risk_content_border_left.xml @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable-ldrtl/chat_message_popup_risk_content_border_right.xml b/timcommon/src/main/res/drawable-ldrtl/chat_message_popup_risk_content_border_right.xml new file mode 100644 index 00000000..1ca644a3 --- /dev/null +++ b/timcommon/src/main/res/drawable-ldrtl/chat_message_popup_risk_content_border_right.xml @@ -0,0 +1,18 @@ + + + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable-ldrtl/chat_message_popup_stroke_border_left.xml b/timcommon/src/main/res/drawable-ldrtl/chat_message_popup_stroke_border_left.xml new file mode 100644 index 00000000..a95f4c0b --- /dev/null +++ b/timcommon/src/main/res/drawable-ldrtl/chat_message_popup_stroke_border_left.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable-ldrtl/chat_message_popup_stroke_border_right.xml b/timcommon/src/main/res/drawable-ldrtl/chat_message_popup_stroke_border_right.xml new file mode 100644 index 00000000..b1547a6c --- /dev/null +++ b/timcommon/src/main/res/drawable-ldrtl/chat_message_popup_stroke_border_right.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/chat_bubble_other_transparent_bg.xml b/timcommon/src/main/res/drawable/chat_bubble_other_transparent_bg.xml new file mode 100644 index 00000000..a29c9ec9 --- /dev/null +++ b/timcommon/src/main/res/drawable/chat_bubble_other_transparent_bg.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/chat_bubble_self_transparent_bg.xml b/timcommon/src/main/res/drawable/chat_bubble_self_transparent_bg.xml new file mode 100644 index 00000000..e89b11e0 --- /dev/null +++ b/timcommon/src/main/res/drawable/chat_bubble_self_transparent_bg.xml @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/chat_checkbox_selector.xml b/timcommon/src/main/res/drawable/chat_checkbox_selector.xml new file mode 100644 index 00000000..fa67e5ce --- /dev/null +++ b/timcommon/src/main/res/drawable/chat_checkbox_selector.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/chat_gray_round_rect_bg.xml b/timcommon/src/main/res/drawable/chat_gray_round_rect_bg.xml new file mode 100644 index 00000000..900bad8d --- /dev/null +++ b/timcommon/src/main/res/drawable/chat_gray_round_rect_bg.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/chat_message_bottom_area_bg.xml b/timcommon/src/main/res/drawable/chat_message_bottom_area_bg.xml new file mode 100644 index 00000000..198b8916 --- /dev/null +++ b/timcommon/src/main/res/drawable/chat_message_bottom_area_bg.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/chat_message_bottom_area_risk_bg.xml b/timcommon/src/main/res/drawable/chat_message_bottom_area_risk_bg.xml new file mode 100644 index 00000000..586bae43 --- /dev/null +++ b/timcommon/src/main/res/drawable/chat_message_bottom_area_risk_bg.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/chat_message_popup_fill_border.xml b/timcommon/src/main/res/drawable/chat_message_popup_fill_border.xml new file mode 100644 index 00000000..b4c5cdf5 --- /dev/null +++ b/timcommon/src/main/res/drawable/chat_message_popup_fill_border.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/chat_message_popup_fill_border_right.xml b/timcommon/src/main/res/drawable/chat_message_popup_fill_border_right.xml new file mode 100644 index 00000000..306aac31 --- /dev/null +++ b/timcommon/src/main/res/drawable/chat_message_popup_fill_border_right.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/chat_message_popup_risk_content_border_left.xml b/timcommon/src/main/res/drawable/chat_message_popup_risk_content_border_left.xml new file mode 100644 index 00000000..5a02bc55 --- /dev/null +++ b/timcommon/src/main/res/drawable/chat_message_popup_risk_content_border_left.xml @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/chat_message_popup_risk_content_border_right.xml b/timcommon/src/main/res/drawable/chat_message_popup_risk_content_border_right.xml new file mode 100644 index 00000000..4612a7db --- /dev/null +++ b/timcommon/src/main/res/drawable/chat_message_popup_risk_content_border_right.xml @@ -0,0 +1,18 @@ + + + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/chat_message_popup_stroke_border.xml b/timcommon/src/main/res/drawable/chat_message_popup_stroke_border.xml new file mode 100644 index 00000000..9b0dba2e --- /dev/null +++ b/timcommon/src/main/res/drawable/chat_message_popup_stroke_border.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/chat_message_popup_stroke_border_left.xml b/timcommon/src/main/res/drawable/chat_message_popup_stroke_border_left.xml new file mode 100644 index 00000000..b1547a6c --- /dev/null +++ b/timcommon/src/main/res/drawable/chat_message_popup_stroke_border_left.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/chat_message_popup_stroke_border_right.xml b/timcommon/src/main/res/drawable/chat_message_popup_stroke_border_right.xml new file mode 100644 index 00000000..a95f4c0b --- /dev/null +++ b/timcommon/src/main/res/drawable/chat_message_popup_stroke_border_right.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading00.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading00.png new file mode 100644 index 0000000000000000000000000000000000000000..062ce9d3796a34ce0293c486490b36d4ab84915b GIT binary patch literal 6618 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 literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading01.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading01.png new file mode 100644 index 0000000000000000000000000000000000000000..41c1724ce1588b0639e1dca2c9023f64224c5d32 GIT binary patch literal 490 zcmVG100^0LOvz75Rq#zs$xKvm%}dTtNi6~ZK^6wQkU7ii00009a7bBm0000100001 z08b^v)&KwjY)M2xRCoc!!A)q+0UQ9}-@9)+tVu3RO37i-a3pNm0b);KsqndVel^xBt7nwQsVDwS zlRh7$lu}Bs9a4(=Eb~~PAcige$im`j(B(p@W}z-r^t3!PC*lgHAahL}Z* zVd+lPa$FkK7l04OPgXI4obJ8$ad+Kj;O;DpH%cjEkn|ng)xhz zo=aC_HtilrZ|zZvDViMhSsK07*qoM6N<$g0{cZyZ`_I literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading02.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading02.png new file mode 100644 index 0000000000000000000000000000000000000000..23aac51880a442246e5372c52073a01d64e48040 GIT binary patch literal 474 zcmV<00VV#4P);QDApA183}i8oP=?E&hy|%L5GloQF&GS{&$C>* z$rV}j75#YnYLqS{d_MYf^fwKT2#eA-z$HcB-CBcXUSwJ;tO*(ave9p%M$w z>y@-WT6Fnn+J-1GXnV}EQyPyJo24hw#-wvG$6GU2MU#`##VAoo{V~UFX?HZaA`M2# z-K+HvBQ-TOwTK88q!ZEPl5`|WG^CVL8jNsRIvPz5OV^^r3WY+Uu*H%Hb!kU5Sueea zMd>!>r@m-0A?=DqI40eXGJB8#d~jH0`EJEXIHben=^$@9x&3~QO9LtQR9^<%c9CLsV@pF^2MpBab4OTRW3;T zqoC5~vvSlpD;bVG7wVRUbD000P?b4%VpP037DaLr53Pf0BT06`W8 zypTD|>i_@&Z%IT!RCoc!!B1$-0UQAE?{Du}vx7)3{sedZ$VWJL}%4(oAPQKBd*PD~Dt(x$bqTJPC=Z^MnxC;mMxy7jqY&{m4$sH*ln+KM$Jf%Esy!E_DOnZZPc;Jxb)i6Xr%0!bVExt z(P~7xAC2@%qe`&=ZC*Dg+GCD!X-70sIH4y(k8~&M*(1HRBo<($&t`N*Bg0a~ zvS?zJSJJg;rrntI!9hh-DLdthG@=yqS!_s36K=U|(6F!4w5v+-JG$L9DW#OYxouG100^0LOvz75Rq#zs$xKvm%}dTtNi6~ZK^6wQkU7ii00009a7bBm0000100001 z08b^v)&Kwja7jc#RCoc!!A)q+0UQ9}-@Co7Z3iFW;74iB_r0lVDSwl>s zNuP8tigZf-8edQRSsE3!=s$>0}f*@0)c| z<&v~1YMhaFN1ZF3m0wqP#W*LVgHhv@v^VNhZdblo5#yYe_C<|7(%Gn^$$+=!N0o}S zK58tJo7 zgXYF07I`l9#AI3xNbl`biYjG?eUkb###CBVq%`chbGr5VERDLTF=nE}9p9yt(pNXE kioelptsS;oqbdG_-@=~Yw20J;4gdfE07*qoM6N<$f)`WPA^-pY literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading06.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading06.png new file mode 100644 index 0000000000000000000000000000000000000000..9002debd9f0c2f8be73941b04c8d34440acdcd53 GIT binary patch literal 496 zcmVbVG7wVRUbD000P?b4%VpP037DaLr53Pf0BT06`W8 zypTD|>i_@&a!Eu%RCoc!!A)q+0UQ9}-`;nv*#S!~L&{}Ev>fb2QVt~44)PI^d|c*0 zRMx)Xz%cGt~Po^+Hf3ehI4)g?I(GQx zK~%XR?Ti|ir2|pmWaXDz%2DOKbS!FIlnzBfWuP+N-yAbsk`70WgHlfvSZ3U*n4uzV zjT&pEXHmyB@04Pik}*HE#4MxI_9)P1LRU<4M7kSAc1v$8jRLm~MMP-x*_4i`XFwX! z8Z)$NkFd&1>3Yni#S`g+E)7wo(FtFqLFJgw5)~

    32o10biv_eaf*Io$mP|rIfzA mV?+FnW?SsH&t^^WAN&R)XW*_=Z8rD-0000bVG7wVRUbD000P?b4%VpP037DaLr53Pf0BT06`W8 zypTD|>i_@&aY;l$RCoc!!B1$-0UQAE@Ah76rbOChNVzDYO|lDeQAj97{!LtPnFEo7 z?65dEIJoTd4w81VvRpI{+G#Z%HENuaE=C<~22EHO zQ{0kvMvW`d;V9_qs(h=A^=*zS7o?L>9r*>&8Vv}MVpT%wMRYu(y(Pw z<%C@k5mp$JZpLg{JeA%#s3EG9o%Tr@P>Q)MQjyZQyL$BM_gR{7Ln#)b(*r-Gl+t(i ltdGCZY>NZ-+oUP}gWtxN;B3732)FbVG7wVRUbD000P?b4%VpP037DaLr53Pf0BT06`W8 zypTD|>i_@&Y)M2xRCoc!!B1$-0UQAE@3z;P9Yk8mNVzzOmV=$h#ew8c4zh{3;4%jy zr|qyP4h|0byuOtUP>vY^u{4gF*mC`lYYD7xb1d2Y1E|jz_OUlN*|PG^!=$pv`k>S4`97tqE&m9=1qhG0g_)ek{N%lRBct5$SX^P|~AfZ&bM~ zZHpS`r2SE#r~KIuS1pe!r=-JCPI*;{7orBV?D6&I(ZE;NTMQ=>8!bg+3qLE=~%+jcGQD;=? z^g_B6GimowdS{=en4;vUPtu5%n9U+(DUG{kz@TAYr3n|b#C&wS?WdGd`tF8x@i*FR gvd3;4wZ?z&2eNtIxgX?B#sB~S07*qoM6N<$g6ag{7ytkO literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading09.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading09.png new file mode 100644 index 0000000000000000000000000000000000000000..89a5fa3390139a69de878b97d1aa1da06e1592f7 GIT binary patch literal 490 zcmVbVG7wVRUbD001}%VpP037DaLr53Pf0BT06`W8 zymd&^uK)l6Y)M2xR5%gE!B1!n02~0|-`;!H>>$F0NpWxx7PXyd7YEWPhs8vcaG3+q zP8=4+!NKW!C+VH6$VKCz<*=eeS!u%F(PzixbA#6PE?Q=c0KT+klN zQ_|;?lu}A*%t=kLIGendCOvZA0f$^PWLkP=Z7ihCXX%M8(ZCKP(mShTE+sFey9zOn zW`ok>n9Dh7NHG?n#T%(RW@+-llx?vD`=sHRWvBEsmSNPi^-<%rbUEg+POm*N!yRdV z)aaLbqJf_Bs9(N28#7#zPDPC?((!0eu9PQqSrJvPO1)9zm~<`b6pXnQ4V0z5QDd|8 zD(ZBb)E0FL#{JY9vy4bxQRRh7G;l(C5RDv^-dh!a*edOcIvad3tv%*ak%p~_P}C81 zHhL@Fj`_5DE`4-VQ&cHA6L7Vk||chbE+y(hv7-kH68P g!x4w=)*S!AZ=<*1daD_!cK`qY07*qoM6N<$g5UwwG100^0LOvz75Rq#zs$xKvm%}dTtNi6~ZK^6wQkU7ii00009a7bBm0000100001 z08b^v)&KwjXGugsRCoc!!B1$-0UQAE@7`N$b`XgJ)5^s`v>fb2N)Dt=N%AMc0hdu^ zIn7~laBvXySq{=pR^-As=$%%S5GBQl$-%*aV&;9-+Oyx=pXbKs6aSu)F1;=oa6)G^ zrdhA|Qc5YMHx6owxmoF{H13Y$w%OsN5mo7-B{7?JAEbL$MS-1JbQ1vR!&@VT5-5%28mskE%MOo*}7Xam-+a=hCH^ zNt;K~JG(T+6wQwKBn>OaY!(=l(wH008!+UvG~uFh%tx2ozDp^kuWnisf1}j~JM~zv bCH{lo*A(89mc!7w00000NkvXXu0mjfUr*Bl literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading11.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading11.png new file mode 100644 index 0000000000000000000000000000000000000000..7b0c3094e838cc5eb1c423750a517c499e711caa GIT binary patch literal 488 zcmVP)G100^0LOvz75Rq#zs$xKvm%}dTtNi6~ZK^6wQkU7ii00009a7bBm0000100001 z08b^v)&KwjYDq*vRCoc!!99o#02~1D|GW3P?rspt; zmFWx@gTcx4dlqq%D<8{s3_8md5uv1*I2lY*K05EOUY>jR?!(6K7yq6XU3#3;r(0*t zPDzh+Eb}7VfN)Gub4JyV|+Et`9=Bl&$4EQ9CJFgfs(dDLZQcCHIYgWbIXtUl< eJFL?h|G_V1bl;=>Y-Mc#0000G100^0LOvz75Rq#zs$xKvm%}dTtNi6~ZK^6wQkU7ii00009a7bBm0000100001 z08b^v)&KwjX-PyuRCoc!!B1!n02~0|-`-}M9fsuMPfE%`v>dFIKNp3Bl4L|U;4*59 zoH#5F4h~xRUMXoOD{|2&+G$0JqNF(S<&w4-g@_KJ?dvoLa;@?xytKStv&ghA` zY18kclu}CRougV}c2;;H{dC`HJMDJXn6mWP;+V;DpQMLYMIGx*NUtr7>9lz!-B5@I z+KoyNVmha!F|9ERonA_NqDG7NrmTr3Y>~#J#(L>d%)uLFOQOngX)xyCy0ks2T$Bz* z9eu^&n@ZxMluF8OMTHB};iz&*x)c#%&^Nz4an9-p9p*)a zqO>`xtdyQdMA+nj1<^o3$q!voV?x>%O*kUmje2%TZ*|8kEcIDgPfTY-8n-AKSmu>< zEgI?aRQh1QmiR-P6TV2JT4N^j6{S>i+hs#WOiEL(YK>;}y63x;Qu^wSwedGPZM4r` d8??uN@Ef^k-*YGe1MmO<002ovPDHLkV1m?2)4~7% literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading13.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading13.png new file mode 100644 index 0000000000000000000000000000000000000000..917febaede7ac304949a39ef85b189d6b9da6991 GIT binary patch literal 485 zcmVG100^0LOvz75Rq#zs$xKvm%}dTtNi6~ZK^6wQkU7ii00009a7bBm0000100001 z08b^v)&KwjXGugsRCoc!!B1$-0UQAE@7r6i*+EZoF;croqF)Y{1GzYmP!7wVh=V_= zIS@HnS{xi4l=^HAOFLPSi^f4ay`n@>Qk-mZaCAWHebw@8Z`*rrd_M8->Ck7ubwkc7 zqCK4ke3DX1DZO`0DVAofm(ouUow3_q=S`?fPpyo_tT81$)*JKKq$a&ljb=JMmu~Bb z1yqbnkD{5=(u8s>L6=w3zL=xrgBk0i1v{k4m}9f_B--#+-KuDCLb@1jxFzk123Ms1 zhzJ!`)zPX`u8kD&hYQm2XmCk791)==rIb>d(jT*2mX1V&L(T5&yq0dp zLb^SZK02rrzv*<+7inBM7PHK-l%|auG-S+IX~qrZXhoj~zDp^kZ|>O`f1}G*2kf^+ bCH{k74$j}+@YHG<00000NkvXXu0mjf1uV}F literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading14.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading14.png new file mode 100644 index 0000000000000000000000000000000000000000..b5f1ad34a8d7a1c2a63c6b9cfb7f76d8c600fc04 GIT binary patch literal 488 zcmVP)bVG7wVRUbD000P?b4%VpP037DaLr53Pf0BT06`W8 zypTD|>i_@&YDq*vRCoc!!A)ol02~1D|J!@bENPO9&vM`(T9RFmiv!7&gM36>MrjV@ z3^YuL2(%+jc3rO(n6Yhwl*jY@B|#cWDmNVhe` z9GVSDk7G7n(y(IGp~Y)yUsNghV9NSvzz%67s%(;;#yq?;ZCO+}A)Swiu+A2RsNtrx zD=J);4n{=il_m`I-F4~CqWHr(>3CGQARUe}t9A81GAVs?O@Ckhombp!>t9j z^_?-MGs^T!+oQr7>18ye$+$_aQDs!x84Wlp-H(~G100^0LOvz75Rq#zs$xKvm%}dTtNi6~ZK^6wQkU7ii00009a7bBm0000100001 z08b^v)&KwjY)M2xRCoc!!A)ol02~1D|Jz$@+hO_0#YZb82Vptbi8wfrP!2xxb-`sj z(B#Bnp&T4s{N730$%7>xe=$XN70dFL#}?-A;qXl%((Z|LU0c-A zVN|*owVag36r%~9o=dx;O2K>6R!0jqOXE>xo%A3gLWe<%qJh`SmPCc)(%FazJ>L0d zf7EeJ+7=bgOZ#GmE>}#KUK&z}InGE&qr#AMFy>hAqr5R;Rm^fuIusQSNEc!r#nLg4 zP3espO46pN&?`NSItsQbMnq^c>8I|fG9hh=795st$9#53Z}dbHminTsFKQW)#x0CG zmU$svje5F0l0Mk05VN#9;j1*N7>#r(Nomqe7Y!TnU7B`TF@P)bVG7wVRUbD000P?b4%VpP037DaLr53Pf0BT06`W8 zypTD|>i_@&Xh}ptRCoc!!99o#02~1D|NH1VH^@g8A1RB$N0-56Ad7*7GU)C^ELhG! zz82JlI%Tlj?^&dqT#-e`pv!PYiK3*KI2nu**X#Y&^`3k0-5ndhU;KNDdJMQ?&mz*=4O=_(v7F~iBr7;)Hy5lM}uDJVl+{5-FLU5 zPDR=p4OUCfqm3x{tVm^A@^FvB0eRIdU_#2%z+h?y$ cI^sY01smVpI@a1o`Tzg`07*qoM6N<$f)KIRqW}N^ literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading17.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading17.png new file mode 100644 index 0000000000000000000000000000000000000000..e6305cb45a2870c59497b116a0b9df995bd5eaf4 GIT binary patch literal 490 zcmVbVG7wVRUbD000P?b4%VpP037DaLr53Pf0BT06`W8 zypTD|>i_@&Y)M2xRCoc!!B1$-0UQAE@7sH=*+GPoKdD`aXgS!4Ty`L#9K=LiaG3*< zlO5J}aBz^rXD&-S+4AS2QMA*F5~8FyF*#5U%Aa2Qs@LB3d;7zU&nNypO*(Yz)9bYM z_%jvV-b*Q^l-@Y16!WvzQ|Xu6PT6ITGe%8H4=jthtnop*r!5L>HZHxiBpRuBEM3(U zvuHLf-Hk?0N~6kAV`UU*@l4tqGnBklwLWTWP}LR%c1U9}!xrg&Oi}W}wP@g#Nh_kx zap_!4)8)J6QQ(Ty8Fem72V+`K#TO${;H-2k>YSGjM}ZM}y)&k{AazBZL(-)v7;N#- zR~uuB0cm^GStmV-2FmW}j~Y!T{L~sVj7vLX0ggyFqsVUQwZ$d|Y+XQ`@RIToVBEkC4`(lk07*qoM6N<$f_T@{oB#j- literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading18.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading18.png new file mode 100644 index 0000000000000000000000000000000000000000..06f900835cba1e49f96c7c472fca84982799f660 GIT binary patch literal 489 zcmVG100^0LOvz75Rq#zs$xKvm%}dTtNi6~ZK^6wQkU7ii00009a7bBm0000100001 z08b^v)&KwjYe_^wRCoc!!B1$-0UQAE@7`N$c95lAhLnpU8p%%N;y^;}Ab%n*D9wRL z4jdK-2gT|0N})+vSyCDY?X)7IC=?e=4lc?8t@l-H@4esKh8v$x{Ci4z^*d+4VLkC@ zTJ`%NrIb>7W3QH&okgBXKMgx%lP!)KQI#H;A2V6#qjb-bD6qnq^uoMorqv_qvQkW= z&5(2_nmH(qD902>bjB3zo=IDyL5sI0ER8x}^+lca(r7eTDcz3(Pn?bdFI9C#js4Q` zC~(8IC~#5Q7&T5wJEFkd%Ihd_OxhPUPDs0=z)jUF-BIVHv^#3-l+HxKrLM|*!%?Rq zt&19qrN`048WUEF`i`r%y{v(dvND(vWh@WR8lI#$9#RfI(lR3FnpLcl5gLyOdJ;=DOwaH`=YW f-8QSW#eeV%;@sUO_6_q&00000NkvXXu0mjf=*HK4 literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading19.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading19.png new file mode 100644 index 0000000000000000000000000000000000000000..672181d1eb123b3bba615e8bf35567ea957119d3 GIT binary patch literal 488 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 bivQp@D+1svm<_2P00000NkvXXu0mjfD$dXq literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading25.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading25.png new file mode 100644 index 0000000000000000000000000000000000000000..5830e1cefe1b7591fcdcfe6173ecfbbfb717c26d GIT binary patch literal 486 zcmV@P)bVG7wVRUbD000P?b4%VpP037DaLr53Pf0BT06`W8 zypTD|>i_@&Xh}ptRCoc!!A)ol02~1D|Mp&Mp3)>2ACb$7XgS!4TpUQIB>9Lq*rY}g zIdK>c4h|0X+ezBVid;05c3M%QsHLnFlY@hU^3iL5wf5fL+sAX`_lti|vrfIP>2pd) zG^VK6M=7P0(mO{r#r&-HQu^(I9y{%J#;{50iDfaDRX$0Ntcw~p8kOE?i+YNlOSd$~ zELsdn52K!LX;>lVq17vCPt4Hdy|VULfNj!9%&58(~1&8Wq+^ zFQUqrt1-oT-4<&$;g?d(Fe+`2X}YvU4M(JVQOhoA%#xUg6+WBP5%mm8BbLT2R(dVn zh}o1plg91S6w?%)@I@L@h`B5>Af*X+T+wIHSE=l}LM%k5`+iC(rSIG100^0LOvz75Rq#zs$xKvm%}dTtNi6~ZK^6wQkU7ii00009a7bBm0000100001 z08b^v)&KwjaY;l$RCoc!!A)q+0UQ9}-@Es1%?={D_-Lhu5-kTi5eEkn%0XDZQlm5n zvbbovg@36_OC~aE%srA10?$eFuiGNRnc3p1haZXz- zPDPi`Qc5YM4~{Fv{H*m#`sIWD&x}zax z(WqZ~95d;Z29#qSn!J|w$227$P1q0%utOS(X|_mDV-en&v@&X(k}ky}+?IAnjVsdO zs8gKR6?I&cPDG8%($VAj^f5377NsV!#G zCkxz=esoFrgAJqyNAZ4l+q9P lZHm9qWSfHy*s3x9gWq1>;S{9_>x=*Z002ovPDHLkV1jfa*!2Ja literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading27.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading27.png new file mode 100644 index 0000000000000000000000000000000000000000..f9653c1ec2476555e62379f68363ae5a60650d6f GIT binary patch literal 490 zcmVbVG7wVRUbD000P?b4%VpP037DaLr53Pf0BT06`W8 zypTD|>i_@&Y)M2xRCoc!!A)ol02~1D|J&BucJL7{K8u5crsZHckc$HeMaf6R1*JJm zlM{!rQO_x9RBOyZhZoYGs8aC3wDmC$+obWRvQc^zO?a(jSyVV7osTA5lXgUfi_*cU zp}#l~HS|lzqr!l6IA+{9pyZo%F~bGvNK`l^U5YYWia!h*E55fh$`qxoQDKerEXoYI znFR}rZ#>Z%5uwGTU%H~ogtR^CSZ&IkhzLieyHU$7>8%A(N4K*PmYY)26ZH&B;}%B) zE4-A3qLD6R(ntFhqD-5UzDOflV>X?NQkrzjWrK!&m8MATxD#NX(! g**<%1(jNc89|5u8R$OpDjQ{`u07*qoM6N<$f_cBx^#A|> literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading28.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading28.png new file mode 100644 index 0000000000000000000000000000000000000000..842fef49d7b4903978085f609b99b2361b519dc2 GIT binary patch literal 496 zcmVbVG7wVRUbD000P?b4%VpP037DaLr53Pf0BT06`W8 zypTD|>i_@&a!Eu%RCoc!!A)ol02~1D|K5A8*+G_EhLp>SXgS!4c5xtIwS#;_9DH2n zK*Yrk%Zh`8lHZG3+R4htWsQS&T2Z1X5ho@Gg>qo6{ndJJdv9NE{C@H8Dd^DcvR~Wl>MbQ|Y=w z%%aht^f2ljO7GSG15;JU+9>pTOHfcpvIVzovMYtwyiz*kS zeNn^3a!=H7MmiEz&PfNNLeUKqzV$c7H0Py*QDwh$DJneh{noDX565Ggva}_ttdX8Y zg}$5XBFb0F?-XK+f-%1}#|)!VXVlPYTvtqSNV*%f?2z7A8a3QD6cM4-XOr5ao&jk@ zOH9+EEy7AKrK>TUW>2IKx->+El4HI|gNiYiCCXA7)2~Ob0bivFeTuOV9q#!hrIdcU mV}1OMCY$ZG$0m*OAN&ET0N@3Sx7z6d0000@P)bVG7wVRUbD001}%VpP037DaLr53Pf0BT06`W8 zymd&^uK)l6Xh}ptR5%gE!A)ol02~1D|NCgolt{byNVzDYO|lcYC?r;je8&ZsIgsTb z)FL@JIQ@1KJ6TyS8VBvPnvxYI#fiy*Tqv~mr`LOXZ=Y`bei8qiq7GfI>2XF|%uiXD z_fkqJr8kZ##O$o}T>53eX*=z9)}RULiKQ`<6+TFhtd0iO82TCIC|!;UYmFPwDeobVG7wVRUbD000P?b4%VpP037DaLr53Pf0BT06`W8 zypTD|>i_@&XGugsRCoc!!A)ol02~1D|K5A8*+Hb0jFgLmXgS!4TpUO~a*$2L1(!Jx zIZawq6bA?WrsQKMD{|2|XcrSDijv~Qq^w+&kJkR`wfC{F=f>|B|DJ+QJ^J-IrXw0t z(&M9)QcCHa1DaxPR(K)(cF$2;ZFk(TN$HU#F`MN+Ne`@wI@TJMURxB?DS0McQHU8d z8k@iN_ z^|ovME=3(Dq=Ql6l++bvmb&VPao?hj(^7X-*eCTyncYTpIp;~#F(7S>3M-}OQO9jJ zqe{V;Us|KasI)n%Jk=Xj_Dgr7o^8@w3uB6}x?_rEKAY4L(;1XTERHf4v`3kCucV7H zlU9$V5B6w^DM}9eA`L0VY!(=h(wH00>N99uns8n*=A+YHKc$q?cekvGztLiYU3OZp bIsSt`GqT<|3g`6800000NkvXXu0mjfG6>Mt literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading31.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading31.png new file mode 100644 index 0000000000000000000000000000000000000000..281a9e3630c1e4164c5889128f917f5b3d93c8e8 GIT binary patch literal 492 zcmVG100^0LOvz75Rq#zs$xKvm%}dTtNi6~ZK^6wQkU7ii00009a7bBm0000100001 z08b^v)&KwjZb?KzRCoc!!B1!n02~0|-`jiE>>v^cCdI))Sk!i+T^vZG9K=MFaG3*< z6Ng0+4i0?JMcT=VTr>_^4l7C&rInqS92^`d*6gR&dv9-l-T1!v_cUnN?UEiRwZ{CE zbo(f!lu{aUNFiotl~>ZJhfdgGms9$UOHVD0nY8#MJ+>z5*kDk4YjI4aD@iFvpoZI3GFrF~Ih z!R-#$%h#fg)6(Ina#lJJ6CCrc{JT8tN7Qjn>WV7+r3*2^mOEXymK!Qxi8{*C)~K>t zdKoo3jcSP+4MzOZ9Fq)69Z})A-l*fCbU*6ZDZRHSBEni}Q`A`DvvI94m0oGUk_bg@ zQDdby($$zwvuDzbVG7wVRUbD000P?b4%VpP037DaLr53Pf0BT06`W8 zypTD|>i_@&XGugsRCoc!!B1$-0UQAE@7sH=X9tluFs)o1M9aZWq~t)_l!N?lNGsW9JJGl5~5I?m>e7&s9CeGT6^2??VlT;PyBltwCQw7uhUv% zaVk1}l2S@3y>~<@=4Y+v(wO^B*=3J222Dtht%$ia`z$@QE(&ZkEWNfY>Zy1tUDpt^ zXfz-_hMr!_wU-vRisFb8h2Sa7NX5Pqf$!ghdVaJ->BMRuMV3v b#((e!iSplEpYkdS00000NkvXXu0mjfM-|Y2 literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading33.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading33.png new file mode 100644 index 0000000000000000000000000000000000000000..7ebe0b193b30cfe3f55e3aa398070964d3bc7d92 GIT binary patch literal 488 zcmVP)bVG7wVRUbD000P?b4%VpP037DaLr53Pf0BT06`W8 zypTD|>i_@&YDq*vRCoc!!A)ol02~1D|Jz<`dj}CNK2k~!!g8<^xj2we4$DVGie2X5 zE2r(SI5;4u-zz2UWW{sQIA|^_B8rkC2POw6DIcx<)yuQ@-nQq)?-&1`Ha+^CH>6K@ zw5F)vdnu)q(rX75Vs@5$D*bfZaog;0!l)_fzJ)Q9Wj;uEt&BR>8kb&}AB_|}lCElt zCOV8rccPJF(x~>Bg-*|;oiR!!_ui}!6j*H)Ho;g zMwO!~D&f}cj~aWWfv8f@=Zh!SM;&EpQ`A@? zJ&uUb<)Lp*D@Gk{Cj3x}X~v~35lXID6-^wFZbm)ZrB~*~EG+TSlbVG7wVRUbD001}%VpP037DaLr53Pf0BT06`W8 zymd&^uK)l6W=TXrR5%gE!A)om02lz^_uFP>2R~^Sza`}$S`JowB%QefIrrZah!KKc`8TZkP2sp)+Qt zMYs1-N-3q+4k<)qmU=4vbkA{HZFkb3qV&kZsAq`}(gQ1^f;C2^7v@JbEgnlZG(`=q z2BiB@%`s_Eb2OmMGigUmQSipNRWS{lq>-3nt@JQv;H9FDD04*Wi4wESjykSMTcXSb zX>XLcspy>{r=x;1(%~p`PTC(O*E^gw-1kLV7JoP|9f&gfq>C{@k8ge%a>~l6pikNu zWtK}%VuB6!m=`rP8S_JXOff2Lj%he3-HA%JNw3U}1}ySXQD;=sFO66bH7xdAx*D~# z8x$pgHQ9qfbg>Zn>mazt7URE1F|Ey4>|$N-2GH+v@lmZPwdmr*&H6 aKllxh$lgr_`LK=v0000G100^0LE=o--$u9x`3b6vw^~P%|00009a7bBm000010000108b^v)&KwjVM#XLH{d|DU2EqplfqRy7u<&!|sQN-3pxPAEio z)_EcQ^uQT=>~qePdFhE&v5>VsOOI5dh0W^HYb&CiKF_3EiqS#Yr1UV_IW0{oMHl+L zl=erHf)D0wh#u^erlZLg>2WN=8}n92gHzIl_|0}BD$&7BX?HZZEFF%Bm?+;FHY;oI zjYJFQrIXR%qI5JOLS0HJ3Q^|XvPl% z(WEZziXI%7CZd(S(p$@;3v0}pSB-YYrD-dpgF&yP8_~&tr_x7<6ygtkhJBGHm0}^w z)TA`yuB*n3`zp=3t`xl(a^H6;rS#1`o8n*e+vcDHwkpRG_yvH5-akFF;@1EG002ov JPDHLkV1k=m$`1el literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading36.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading36.png new file mode 100644 index 0000000000000000000000000000000000000000..2135b4781e7bca2ea302ae059ea714c0e40227fb GIT binary patch literal 483 zcmV<90UZ8`P)bVG7wVRUbD000P?b4%VpP037DaLr53Pf0BT06`W8 zypTD|>i_@&Wl2OqRCoc!!B1!n02~0|-`m#AJBZ}se-0c(OR^KWIFL{d@+aale`*es z<-}oe*ulZY_gti%tVl`Ypq*BfC~7HAOb$-k!PD#gl)UY|xA*48_r<@bL!SZH4Lhed z7N*O9Pf|)LrT0!ML~GV~CH?f!S$pkw(YUJg%<`DeYM-SiHbfm;OiFJpjYhh>kZ$RS zCc2GDkD`%&X8LM^S}=&r8MK7&G9#S>~P3I+jPf& Z@Czi~+y}{5K#KqX002ovPDHLkV1ms(*r@;j literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading37.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading37.png new file mode 100644 index 0000000000000000000000000000000000000000..f5582bf638cf0801610d1808744ff84b4a8a4a57 GIT binary patch literal 489 zcmVG100^0LOvz75Rq#zs$xKvm%}dTtNi6~ZK^6wQkU7ii00009a7bBm0000100001 z08b^v)&KwjYe_^wRCoc!!B1$-0UQAE?|W~pZHGmY@+alML9`s~L=GHCqa5T<#08g0 z7CCWP92CXr^K+4Q+KOB>4%%r&i6T<81CxWJ!)V@D$$NY6ZI2tDPyBnz`VG2l*eQMS zXF3e}Af=R2dh3W%EX*p;r0?!IX{X&z8&{PcSrYSE>7(?(nrL8yN$I6U(M*RY(oN-P zq0^XjKbkoqjjO~0ba^ff#2h8>%vu+1*eXrL92=#F5fP)E#hGFtOVPqBRZFABF=;3w z;zn;VRaA@D)0&fcv0XXp6w;Qcv08c>Z7G}fO;5}*DQ$~39F|6-kzLXoy|DnxeNxpI&5TGB7Do#! zypXO&D?J`d@9k5H-*h^XXPdY1(a<3>)!9nsr4b+R^W>87Zao)g9~OZ* literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading38.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading38.png new file mode 100644 index 0000000000000000000000000000000000000000..e6a366ccf64a71ac2674e68a520cc719421480e1 GIT binary patch literal 480 zcmV<60U!Q}P)G100^0LOvz75Rq#zs$xKvm%}dTtNi6~ZK^6wQkU7ii00009a7bBm0000100001 z08b^v)&KwjVo5|nRCoc!!99o#02~1D|NH1VH(Wll_()j{KDrEUB8!29xQc7#^j260# zOZTFg`lfLSWdCo}(qs9U0LNrh^pcwO1 zq^(h7jr24+QZ(bco~Sb|ZI2Edl5R&MyQJ58qYW#3G^a0`8Iz_gi56CRA&o>UJswH# z3@AjEvSU6;<4UoRMJiI7annV^#(b7$T~>-t^t@P)bVG7wVRUbD000P?b4%VpP037DaLr53Pf0BT06`W8 zypTD|>i_@&Xh}ptRCoc!!B1$-0UQAE@7rE$b`YWDPihw;S`Kz1mmNqb2l%HywwmoiqKJo8q(`&$GLr&|7 zKT|f~qm)ug>78Rru`p}Bkbb%ElwI~XV@yqYVp+^*jZe};-BDn(3F);Z(M;Jh>6*5f zLx)l6K{Rtx8q*%rtc(JkUP^momXi0X*2gp(RCPyz9nyHrvPF6vb%rb7T#qK+s96yW zj!WmF?#4#nRV|MKSEasaa8WuKGj8@(YL!P(;H=ai4bDr4qo8ujlyb~)K{^r*4oR1y zK&SpFP?5GrgLTsLXi1w%KXt_{6VlFT!BOdM6xl7kwKx`FmCtH=qL~qC+|roCYOka# zF_$h+r4J4$MV+z}zDT3mV?K*iq%`TaK|@A-m8yodM=N^W^FvB0O}S%J{EbfA?6=QW c9q}Lh27CP4zG100^0LOvz75Rq#zs$xKvm%}dTtNi6~ZK^6wQkU7ii00009a7bBm0000100001 z08b^v)&KwjY)M2xRCoc!!A)q+0UQ9}-`;nv*+G_EhLnpU8V5U(ivtO@gM8%cg3=s_ zWD!C?m%&%cz`PF9wS#z8x+C{aX;gP0toNcm{JKc)BG{`=y_^TfZWqDzlU`kc@i zf2LWFw^B+erI!wBin&?tk@VBB|aAYE07 z8MGLZZpU9uZ6QK#8aAEY7Gn9Tx(l*Zj~QJ+B{r3sf+V?Mgv@?Ai_@% literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading41.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading41.png new file mode 100644 index 0000000000000000000000000000000000000000..a0960b589ac7604c44d010abd558fdf1dd903dd0 GIT binary patch literal 486 zcmV@P)bVG7wVRUbD000P?b4%VpP037DaLr53Pf0BT06`W8 zypTD|>i_@&Xh}ptRCoc!!B1$-0UQAE@7sH=*+GQTkaBSlEy-NS#lez4IbarX!DS9a zE;uX>J2*)CJQryvD{|2|Xcw<2QIr%XhT^D(*1k%6zrD9V-1vOr-_xQ?pG*3k&>4TG zqR)FNrIgYehm>M=R(K};bk}j)?Qqht3F)ClF_Yy!NcXLZ2G*)cFD-~>DxOGJwZt@9 z4N3Q+nPbwhaujsi5Wi{jT-q5^l)N==bxgA7+ES0J=VOvB(nw6PPI?dpwd!?W^hUue z6P84sBhu+8=rJa5Y>I*_(zd8`PTCg*6`!QVQE*B+9Cgk}J<-5`v1s6|bTH~1kS;_6 z9ri~9RcUk7St&h@IcYKKhxVAFCT)#5=#_3qBi+(#^J5k|d^Djmni-TvER1O^^FkVk z>9l(!y|Y&-CaE~;voxd}GnuCbVG7wVRUbD000P?b4%VpP037DaLr53Pf0BT06`W8 zypTD|>i_@&bV)=(RCoc!!A)q+0UQ9}-}bJz*+JHD@R8!+AX=22$i-n2%0W!NO1R8{ zhzp0s!C?nQ&wm$bCo6JU-hFZ7dE(#Gs6)4FdYsW7 zf2K*dk5Wo0rFV{Ki1}IVh4jM%r*+!xtRYjz>gNkd9eM_F4$gk~?LJuyv#_a?26ioPwy$%vxYVEksgG#t}xk{(4(Uze}9 zmm7C1D!egeSyVYLosSwzi=PT(%2A|Ne3k~3VlImmQW|x~RXzHBktSVNiiPNK--MJ> o8h6)*_#4f(+GnrL%JCok0s$r8rTYT-82|tP07*qoM6N<$f;qv_oB#j- literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading43.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading43.png new file mode 100644 index 0000000000000000000000000000000000000000..012204272063227ff097b1b62f50ba6b83daa9e0 GIT binary patch literal 492 zcmVbVG7wVRUbD001}%VpP037DaLr53Pf0BT06`W8 zymd&^uK)l6Zb?KzR5%gE!A)q+0UQ9}-#%KigS?4@k5(%=h?avLG`TpCOdRAR;)2T@ zXmYZ{;^5#Q+Vkch?PMVi8V7b-Q9_ihI58=Xl#jI^t-X8y`{Ks)MErAVwdr(DkKF`askrAumK z1`Yb9J29Q3(tx^{gGSG!9Z{vm8)H_-6jv>c&}>)yW|K4+Rn|%OqrlhRjw>a@?#2YK zj9U^F4ojz^z>9KM`H^9znBbzcB`Tbe_C!JXrV*dC#3U!BLs6ki+7|^b``NoOrZ_F_ zj|zLGvr%A`_9#%6+M`0V^d#n`)~N5AqRNo8Ip*PjbR&vvlU`dGbFj<@<62`nebS&( z%wW0a(uJ5wlZVnG100^0LOvz75Rq#zs$xKvm%}dTtNi6~ZK^6wQkU7ii00009a7bBm0000100001 z08b^v)&KwjY)M2xRCoc!!B1$-0UQAE@7r5zcF-gje^MMAM9aZWM3P7-hviR13YR$$ zcH*!&NM1R8-oq?A%hADmE%#;oy5`suNA_S)})5jE+#<*}I6K1)xnivpXBNpE$=d|Hi5ceTU< z+6+lgVm@c35#?w=yVufzn4@IEv<)#!k25jLc4;)`*epGZ0u{p%5q8-V5#gPh6;bDu zbSVl{s>1`F?y0#N5#f%sJL>dFJyD=i9XDmd&iKtm>15QoEFF!4>V389Z5=Vg73o;i zIU-$)0_$}}fvU7E>a3MsMpIf$`k^D{7?XBH6OKy{qsTt#y`|BBmAoo1K-6l5$8 za(7}_cTVOdki(Mh=MPfsTDJIyD9;2D6$fF>vT2<0YL@#nzL&+^n7qX7nb#4cXEJwh>@OB@nN%#$w(Lj# zOiPi&4nf#sjy?9cMvUw`Y{^?$n?mi{%$&%btIch2+k`e#nn zMdr(WbCWOHz4?Ee!o(ZWszR!7OgFY`D{W#iNKaUMaF&z@a;!6idiTl-zx%R`J6+4hXo;q!QD5Gz^}Ch|Ie0U1F>~G( rO)fVK`s*DoF=t&I|1__v_I(VR4=k^~Kb{^9i~t5tS3j3^P64nJC|ZCpqw6%o1fU>e zage(c!@6@aFM%AEbVpxD28NCO+m9L+BH_uzKo4Ed%wiR2!`V-m?npL{jzDl^QK z|8zXK@V~)5mW$Ihtk!-z*RaG{WbWIUdT(ob8SX#$8h7tC&}B>ve*_o|;-7w$WHkHE z*H9(S&?u|a&@f}`M&{i!?BD-;D*fYc=mC3)bOwhT3nd=$H*{>=%53|SN?|;OXk;vd$@?2>_dGjne=C literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/chat_minimalist_message_status_send_failed.png b/timcommon/src/main/res/drawable/chat_minimalist_message_status_send_failed.png new file mode 100644 index 0000000000000000000000000000000000000000..cac29f387b60eb2b1fdfeaa728264168c9c0ffe0 GIT binary patch literal 616 zcmV-u0+;=XP)1G@wKf<$Z(ms^xW)(Jvn&sp(F zqu=@;R-gBF$IB*tjs?;N$QzM$lV$rl%S!jxwp88zFS5M=ixZG0LmmsfIg5OC5JrJI z$SC+wkd>1qsE7hx10X_%+G%;PvboZrkySY0S!YmvTWt;}JCLrI`Fzm{H0O6mI4A=L1DXQNeX7I;hW=3T6; zTH7-B8kjhh^z3qKl@F_`MSg@^bFW{$#bf%l|MP5NvDV+k@UYGVb5J}wee zMBl~`9U$fJx%j&3hiah39KTNazIL#v+^BiT)OcWcey{M#;5U&vz&?&z>$ZMr{E#q15cYLKwvs-~qWbkzLb6Mw<&;$S#;8%M9 literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/chat_minimalist_message_status_send_part_read.png b/timcommon/src/main/res/drawable/chat_minimalist_message_status_send_part_read.png new file mode 100644 index 0000000000000000000000000000000000000000..ada629c634d07a8f16776addd8705f631d9ea48e GIT binary patch literal 317 zcmeAS@N?(olHy`uVBq!ia0vp^hCpncTRC~t7Qq3}hZvtEf+Xe;~v#jRgBle-fi6j;cdo6p9dp)(<~cHiS>)AidK z*u=zMmv}Smy?Xz#rGt~jg6SL#+@UOseibI@Fq_V3e9XWpkx>;_!EDa^X_FYM`%^iG zxA_M9?>uC&B#cTT=OXlBmJw5U5L~h^g z`%ff322W8~_1gUd$I4R-tKZf4vSdok`edrotGd3}LWb{o`A?<~JZs9#&%D|W^c{nz LtDnm{r-UW|s!Da3 literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/chat_minimalist_status_loading_anim.xml b/timcommon/src/main/res/drawable/chat_minimalist_status_loading_anim.xml new file mode 100644 index 00000000..964ff925 --- /dev/null +++ b/timcommon/src/main/res/drawable/chat_minimalist_status_loading_anim.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/chat_react_bg.xml b/timcommon/src/main/res/drawable/chat_react_bg.xml new file mode 100644 index 00000000..52af45a1 --- /dev/null +++ b/timcommon/src/main/res/drawable/chat_react_bg.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/chat_reply_more_icon.png b/timcommon/src/main/res/drawable/chat_reply_more_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..20c7ce5edca590197b118f69a11d608f2fbb7780 GIT binary patch literal 437 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAifXMsm#F$0651qd^`PBTmZ3NjW4 zxjQkeJ16rJ$YDu$^mSxl*x1kgCy^D%cMb3fasB`Q|NHmv8yXrM92^uB6buXu5)u;j z@83UR!i0c;fCUQ{+`oTcKtKSfyr7^UK0bcKh7If2ub)4Eetv#_e}8{{eLc{G<8LFB zfhxpHg8YJkTrA+<9#>0Oppt8zE{-7;jBhWyPdaSC<8pBsgVv_GZ-3t}VG?Ca4Vt{? zX!Pbf?L(H&=M=w7T`1U_%dY*U*-bKWVRmThoDIo7XKpo|U2>+xC^vDL@~WzJNjI!I zySGejDhk#0?VQcV{nTxPMZshyFZXHeekQw8C%82SEit_H@#sRK@F@|ILQ>3|+OE|* zeL6kilEwS`w_4p=LL<3O>^gOruQNVs%9}=Qu|vIKVwYRGxWl7kFuERY1s zK6&y4s0_#k0*C^j6odg3L1se)Atpdbh~YqI01XGSA3uH!6o=Suy!l-^&;#}*L4Lsu zOqv1Z^A26Q|K_K5?)rECl^gCd#~fu#i+iH;?cj_;FI@#;*3RE2?G#Tns|CJ47hH3G z@`sh{Bpw(`32+`@obse*-c|+%rj?#9jv*C{Z!U*U`)nb=`at^G9fKQ*&(hxgU7zju zG9u9+qwe3mrAdE2@8)P#z8GOM(P!!9l_^Sc-EM~zB_^MI_Q_`A`BY za5yzpD}P1UsSm5e^e?ss?PZIX^4)bM=YX~Lx!SVTcVB+{)f|<3;eDy~+i7QO3%T#{ zxF^5!>AEb{cV*d8&hlf66>Vmm-8nTzr|@9W#3$GG@Lvzh&YHbjU>(QZ+!brX3trB< z)XVl>j{zo_7Oxa^j(tp56fmtR}^o=fF%y!?MTvSNboYYxSj z<-b3F{GAPeA4a zm$jZBzjQgo&DWS^DzjQT<3ilzR~A8ffi_bF1AaCxD)|%G>LuP-x-z6-h1bSLM(Hjs zK9Lo74mMb8vA)p`QgJp=n6gA8DZVp_QCUnx_07f=I*CUnb(L<~BP?NLX}NOFyIGR1 zT{7F8D!Vj!W(GZRzHmsSjJNGdPM5(Jj-%#1KbLhlbO&uo^qH;1rR#RF(NMNaVvB{V z_^}erM;C-dY$Yx)_HghH`jY4~Ux`!K)v&>kxl3e=i7W51D;k&V^$yg(y|tB*DUg9B zP{lcU2}6~`ulHQO7T3%7$sV}*?#%Og!Tl%xm_6l}st^4z{ch3!la*o7=N4W!{r_Nv z|FVBCKHpl@ef?JW@r<8)H-9?)+FD+r&grAziF6&gM{=qa5|0Y|*n{L&R%*Q1xQu=C z=9`BXrSc!Qz4yK8koL3iunEn|&UHx3vIVCg!06DoU00000 literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/common_arrow_right.png b/timcommon/src/main/res/drawable/common_arrow_right.png new file mode 100644 index 0000000000000000000000000000000000000000..a3ea33a4d4ab6c9efc45f9929ab227d28dad23cd GIT binary patch literal 1224 zcmeAS@N?(olHy`uVBq!ia0vp^Iv~u!0wlNePU-?uk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+m=!ZaB1$5BeXNr6bM+EIYV;~{3xK*A7;Nk-3KEmEQ%e+* zQqwc@Y?a>c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxOgGuk#7M#1QqR!L zz|zP>N5ROz&|KfZT;I@G*TB%qz|6|nQUMB-fVLH-q*(>IxIyg#@@$ndN=gc>^!3Zj z%k|2Q_413-^$jg8E%gnI^o@*kfhu&1EAvVcD|GXUm0>2hq!uR^WfqiV=I1GZOiWD5 zFD$Tv3bSNU;+l1ennz|zM-B0$V)JVzP|XC=H|jx7ncO3BHWAB;NpiyW)Z+ZoqGVvir744~DzI`cN=+=uFAB-e&w+(vKt_H^esM;Afr7KMf<|~|UP^v> zu_jo#udkJ7UU5lcUUI6Zi>(sS0KLr26e|}OGfP)P3r9yoH#b8=R|`i|LlX;2BLg=V zHxoB=b2pe?m;B_?+|;}hnBEkGUMHM-K`9}(0BEyIYEfocYKmJ?ey#%8$5xrR-C}{$ zJgD9j+-|YNsaGH97=2K@BE>UI2$*<4On9mVa^UGcH4m8Bi-4)R-!##dfq~K1)5S5Q z;?~=%JN1q*iX44tIPKVt(qws;g&$a4)(AR!Oo^KM{YPh*$B{{$9XB2^mTc?TDk^Nf z_mI#mL1(q^VY5E3x7hx_YxeA7=Y{|Oh$ZzH`YfB3`)H<$r`gGihbC)&%QjnL5}X;~ zefh`cuJ@7Z(eCD-lU|r^DkQt*E?F(<07HD!Z_pi7i zbdIOm&*iXKan=p)MN->3gb%A}T;9+sBmHes5pVQ?K!Z066W6Z@*vQ_tcbO5#w>B-& z-SRB6&(BNDUwLgq;DJ^&SOdlE@2k#>HhUC|GDs@Nz*u|g}yoiDqB2V{an^LB{Ts5aMrW+ literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/common_bottom_sheet_border.xml b/timcommon/src/main/res/drawable/common_bottom_sheet_border.xml new file mode 100644 index 00000000..7a4ca9f3 --- /dev/null +++ b/timcommon/src/main/res/drawable/common_bottom_sheet_border.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/common_check_box_selected.png b/timcommon/src/main/res/drawable/common_check_box_selected.png new file mode 100644 index 0000000000000000000000000000000000000000..f2d0c79a5f1ec5743d9f27c42157d1a8fc00848f GIT binary patch literal 2427 zcmaJ@dpwi-A6GJRl2cN-#2PBZZkSCY*%o2s61mSd47;?A4K2Usq_b2yDs~Q0m^6_J zg)Wxl6zRA-BGNHJrHe@6JnPW;{n6_@ujhHb-|y%3et$mi&*k;J9+4+;w}!f|x{{KT zhKn=K3s`q39yJi~#(epc4JtBD!@?Q zO!M*Q`n$Vf$V|E=NrAEC(^&u;aOS{gk;pMrE;yVTNn>CkZ))#Cz%&XL;%Ds+b7whG zqiD{FY^rx6(TAKELq<~|4))+3d<@`#PUVuoe0nT{gW+Q#U;Sc$y}}HIfWJbxF<8hy zqWs-G!A?vz6>M#3V?l;nA;2iKCENyvMp$hJBVce86o!JrkrpsG24;gnqQQ$71c=6_ zL}0vdgvD6E2@8qha#fMbVnN}SFesg_$m=Vb!}X&6 zE91Lpj!y!M3iYCLm~m_}(2oeCB{ERE{|=-80%|ZgCOM8yWpG_^SO{RTq|hiBG~Nbj z19RNv2q)}Y54 zr*o}(+&1aNLe9X(>R|o}DUP_srFu37Q(AI{*r}1|?}0aHeT;FcrI8;O&YSr`#=mR{ zd}QU^^fC%t;_xAT-Z!hJVMR@+gg94l_ro<}rQJd-+N&JXD~R-ag7AsGr%$%-ZmAmKxi zz9o>Svf=3U@X9M7iup~qB$3!IBn6>i*&5ZlTmOR8O*oD>yq-0h zc;+$tF;|OI4A!qLYXyC!x~GVf(L>4MwWcbXUEYv*ujqZH(H%Km1D`$H-7NX1dmh-- z4Pr@Wy-AOeVLv+%Dp zS<6;xu4!g(b<7WWXxh6aT3;)u=soUog>*e(KdXbn)nRek|6fst)b3 zw%Kl=F6ZcT<@|V)q~Xxg7})sen|o!I(iG)45b={_p|*#pDmFVqM+LR9xoXz0P~u`2 zwcFbCEGf1mKJ{E#-Eit#`G^ClDXpMqy$N#lv-&XIw$+Ry7m^(7eaq`5Fc#0*C-TSs zvQ4BZnSe zhHCPT-4`lUQjj7IVLDQLvO+jc`crGA{P{HLPRaq)n}ms2)w=9Zt>!qW(}J;aH1Y(1 zLx~?$2=z!O>j#@c)#MUr*u#RhuGQ0NeeBBBZRADgW}8MZTCRlk{h_*ULRGCf%^1qci7!I zjnZzycw|9uCGt*3is3@otxqLdV`UFi_gbLSv>j;0GM~|VA*B~U?l%nkX3S(?$iE-D zv0D~6RI4@F@MHz2=Qd8$jYw`#mcqp0zG)%&?)pjWU`FK1qQK96pz)klOo2OW!)G3- zBAnGiu8ljtM}yE@l#IL5A^YEI+ZR_w%$LwZo5gkmLQ+A>@YbwM-@WVHBnInl@6*(# zv+fJr4&=`KSk$k#wsViXAyC)qd2OaO#7M)lZAI$l##AX4myi4A@*-S@E|5rk& zD8%*B-qh|OmE6+C>??;-eThGG^#*-*<(Tu0?l`N8ju&K8K7dB*Y6p~BA{}Ie5yIBu zeJ1bayUwP{+T3P0$3Vx1lNv7zwruCGD;uZ|iBawkh}x59@Y?L!+Ws<7XK@HU;RODo zl(I)=G?g>@!oIcnWyrGf-)iPV^E6athp{-rxco;+x;s2Ki0tL|?)^`jGK<D<28GnrZ5((*TwBbC2YTme02hXwA4fyu=5W2r6UO?bjZ$ zYhSm`S27!ODxza~z?NeH&d8YZ*%Cxg#{Q3n9^G9p-en(zDt?Dt@I+kMt}x->4JjGl literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/common_check_box_unselected.png b/timcommon/src/main/res/drawable/common_check_box_unselected.png new file mode 100644 index 0000000000000000000000000000000000000000..630c7128d7e3fd64aec7d3a1925ccad4de84d8d6 GIT binary patch literal 2058 zcmaJ?Yg7|w8fF)1w+MFGqM|EH2o?-Tu0Y6Wq#;a7f}vyxa`lqSKn6%ABohdzRF*Ev z&446Gbt~ll4k9 z{vF4K>D3Zh28%ux*5` zdH{EzgfR(0ugp|w0X>`e(l3Co?Pdy*@DhS$u!$VIAR$2*L*Swsm=H`336#--=maK< zObem1=s}wabSjNWp)x5nMj(|2P(uI)i?DPNaW#!R6@VgmOIrAjO;li*8lX^ea&pKy z3^J-oqtI9^7KKWu(CL9VB2b&F!X)}YmDXq3Lj`SYp8ZRR^QN2H`hx9O(?cM|r*GiX)1PWOTpQIp&B zGZTj0vgT8|FFl`Jnwjmt`|+ov?u^aZRa+K&yB}P<%3~&{pCl!1Ka@Un@7DqBNhc=| zCRvP7qKI==xnnwr)p$i*dtp2aHs-&!5Fs-0Y%MIY}IKuLAp z6UFcDI%=&OtMmWI>5Zl{B}0snC}>O~6_-CV+fY&2HC8sic)o$?n(u9?*L@L*@sXZH z5j+8(H9i=0fARN}{ZLPw$g%lhqjAl^cSsvokX*;hG+JkL{mF?0OHWs5dVd5PN^(Q3 z_v4|&cw5CCXY0_|?Vf!J;KFf46g%G#85Pt|TCa-&$3?Oo0g-QKl*D3pO}n&-wi;wr zxCU18R%=h`^;b4Q~+C1SxX*G4&ZECep zwDv@0(N6b?`i(yv1|C(E90}yK?(2> zF0}eq5&yP6f5u5Vw2k~W~0L4*16bc0*6`SC|L z^jnVRf&QJr>AkuI3upHBi%yQ)CunT>6GKP)Q` z*^Rv?04aNW|L5W4b#AoO8Oj?^@}y<^cJ!yhB$Max?Y-NS>1FSo{^0W=)%z|{B(|dZ zWN32I*XM^17_H}SAJ*US3F+(`Y0`G7!rDh911@}$>At$N&4W_4^IrBdquB`guZLo& z<_4n0C^+#%d9ej-tx+_NUizn7w{wxn7$-KG+*kiKv!3iVsFI3neFIN+TptLWjcZRx lEB_|>(;@e!V^Wvrj^6WrUI#ayo3sDI`CxQJbNHS={s>WtPR{@U literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/common_dialog_react_bg.xml b/timcommon/src/main/res/drawable/common_dialog_react_bg.xml new file mode 100644 index 00000000..fa647364 --- /dev/null +++ b/timcommon/src/main/res/drawable/common_dialog_react_bg.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/common_edit_cursor.xml b/timcommon/src/main/res/drawable/common_edit_cursor.xml new file mode 100644 index 00000000..03016a8a --- /dev/null +++ b/timcommon/src/main/res/drawable/common_edit_cursor.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/common_edit_text_bg.xml b/timcommon/src/main/res/drawable/common_edit_text_bg.xml new file mode 100644 index 00000000..68c4f6ae --- /dev/null +++ b/timcommon/src/main/res/drawable/common_edit_text_bg.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/common_item_pressed_effect_background.xml b/timcommon/src/main/res/drawable/common_item_pressed_effect_background.xml new file mode 100644 index 00000000..d64661ff --- /dev/null +++ b/timcommon/src/main/res/drawable/common_item_pressed_effect_background.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/common_title_bar_home_icon.png b/timcommon/src/main/res/drawable/common_title_bar_home_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7897e8ba1695147135c587f490d023da79845fa8 GIT binary patch literal 1690 zcmX|B4Oo)r8U`^v;P1$D#GeW^%Qk0Ns~n~&L5hZgKbuWcu?gtW`GYGdvz?Zyh`Mq) zJ!XlszYRgMm8toIOqrSC0>h(ZeJfE5b&`6a*0=ZMw)?u?=e^(ixu5&JzVExPub3Vd zWQB3WAP@*EYA`th>}SAA{09oum*MXt!G_KZKEy>JTzvk&kd#XAL?9x$5kVw`c+hzY z98B4Pv_J$xT5hq*TnBnP=m(=HK=17AY-?+4Z*OmXhcyn4fB>wCz|+&yqf{!@YPCkA z(Q35=0|N?$0@wgR_t~>&)6>&SOG_{eudJ-h%*@Qs&)>a!cX4qM7+$=10qR;HFoR6M z`}+D?T3P@x7z~Y#jXIrfbaXU4Je*7>$HvBvjg0{tNB~tR6xP?*^ZESB%1SPm%i(b1 z;^O3TIh{@iv6GXNAcW0kzkdBXKRA}kVN8<6ph2L?&mom#GgD^b1CTGBPm*VkZ<4?ZP z+9qV^!^K~~I$tIDe5a`7z^GiLx~4E2>c^gqy(t~bWcT8SOLi&FSXqWWmhrylj$Isa z9*6%~w(aWerwXCEftXNmSSM+PjZ_nk&x|HFRaQfGt%xV?@}D3ow_hB9_mb@*Xvtji z&B*+CJR~ojsyRu(g}Py)tI#<~d}bh8o>RW@3&){{1+C^R3!Xfxc%P{a#Y^cIIT#{3 zh$@s)U+>p${lPz|XJHbvStfVflOeSH^74(4_H+39M-{Hu%md2&Ep@w>ll^S_v9mr6 z(Ye76s8hHz7shJsgd6cfkxE>O>c6?sBRNHk?!^t49Pj$pz?w-lk5-|sT^HZ1I&h`% zbZeg5FlFFX)YeBU$^Vp>C6{HDP`D^PkEi+)|rcIItn}#x+K+|0wfN%9}~=QJLhcWTzhm z$2rUolCGu8Jp^;JNGg@(M=ssa=`bhoI`=X@j% zvPEpMyR#AU^v3LuMmT#my>uCjn3s9r%;PML3%2ZSSVY^Q+H!S zWoD0Rpb$^5pxSmQSmQ<%R$)UKdnEQ%*tcE{neNJE!8ftF|H9*MEP(c2>W0v)i{ZgiVO9?lBK{ z%vH3m;SD3?`7vQZI)5nGv@uIQByI# zrTIjT(SewC1T>ns$7u$%;yg>28t$HO{ zOG_1r%4&h$^?<&@`PI+y5Zg+8#h6m}-MwlF4gWOHK@(y!?tBX6SdFxD+d6RySv~Q7 zf9m)n{DX`(%ras5Bu`{~MRFA#_^BLY&}S!d6lt4W9iN{uuIii!BVjJX7R`PS@H(gF zm!$0~TF}vU8pLpjFKNBAzG=Uw?gt`h;HBKiCv=0KAA(8=Ba2Cir~d#Z;BA!v literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/common_trans_bg.png b/timcommon/src/main/res/drawable/common_trans_bg.png new file mode 100644 index 0000000000000000000000000000000000000000..113bfca0beede3d26460f9c91829eb6b55718c7e GIT binary patch literal 122 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

    b23M~ty9JA@~)*VxgRPTX{Nq*N{~zp(hj zyZpo8db4}bn0EGREjz*uu@-D^dV1)?QAG)JiDXJC_pe<_DcxOKbtHEH6*fk9$RtC7 zxXSv$688M2yS~0UeRX_op7|!dxqN|;A_+VTH!(G<` zlJeG20mT=Fdx*zACkk`fP|Rdp4_25HE57~h(0XP^($)mrzL2(mn}gXlHPc4xo7SuK z>Dk%k`T1tEp3P>T&&q6nUR9L@(6f-GaN)MAP6-=s(?{TJ#vSn`+fr?o49f}<+3)3j z}h9lEU6=|tP+)oOWhc6N1rE%|(Zet2{=olaA#?m$h6bbB-) z3JQzWTAV761fgWxv_0`(Ot#hzkB9f(m1F7t8L{(_;^?lR zQw`lcnI*+_yWbO#-nRpXS}HDfnOV^U5xG>Uo2VR;#{9HpN7%U85A%yJPE{KXevLWecb>tTPS1myiac}m?J)MeCb zzIuuG9Q1EhgYd+?vXSDodF%C_kBa8H(!Q}k%zTIyJl!3)Ck%%VMijm=4R91LRvfW( ziO6YgETfJz07p?njzMTY7zHbRba)3P&~qEZ@Z4+L_UHQi{+rJsgJziq=B~m}gP#e! zBZ!1y^nXh>f9LI8@m+bcfkkCUiws?WVwqE#*b;c@e-6@b2=B0dKDT;%xxamgHOH1id%|UxX5p(fZ{+Di0q&gqvZxAF)5cL=Fb{ ztB=R|tLw2rvt?A5A`2;s3sPv-b<-4v3iRTxdtgDh!^L93&)idZHUd3ZIY+wA8kcED zDi72N3LC27jZI)!!@>#`+;>Z}9H!>xZ3yb>do^sEXR{*y@p#<#Bgg>lWrHzl+k;7v z>lZ$`!k`LM;m_LhGfa=nNdy@c6_$U?2hAe}n%lAfwAbWiMVFIgbj}yu!1K-P@%u6GDVMOzm7Sr9V zfD?drnE9$k$T%wGRY@%g(~CHjv3hXv-ujCFG0Fw-@!k&h75`M7Z|Ngl7kBjh&nC*e zRWQ9E^PEhn?!eg<%v~BTl_=;j$^nfLyF46i(c^@$aTOOIQCaXCfA>PbVTVSdc3FM< zOSx-5YDsGjk%c&uWsVB-jRl8*(FAOO9yHB07;$+#jR0m&ju?Lymii$_UB=Bv4PmhAz8+MmpV)PI1)}a*y@0d&o#YmQ8F=-`9-EB%lne)YbdECvB~|O{PF;&lqr0 zD$&tE5uI;{OACnJ)Cq7@`9-PDbx!r};!4wnwsbyeIbuzw7gft!t`l;9ti$fusjwM0 z?0$TGh07e@38nC@@l2}BDKmRPCjg%VVMGeVtO~_3*tM#b8F_7%WRY!0-n)lP@4oz$nWA6FOh zvj03@%z{|#EW)LEknBr;MW=~rp~2NMm1w;0+v^t8e}cuhI{skG42VJPOPAXZwk+VK-{ARJ+Cp%PG@f9)fqE=xb^ zCfw8Au)-=@E;Sy}3q}l@xGuX>pHzsiN};Ur_jfLaa{8*s$8s##IcJebErx`!gYG3^ z={03-R6o*HPk!DCv0lI#{f?cg_u4W(+(noBfjT|X8T(W%!TQb36Fl_0nvj|Rh1M!r zLqNA|l6oiM@9nj7KjYh}F<3WTPTeD3w<8l!SV zF{J#WY)3`BH7OeB{unUzLGfC|I_<7;4T@nrJ?npymRazD-POd!p%%{aGsp|c*weNT zvaZ~`FDlIoyoY|0PSE-sM!OAO`TonXJw+aZ+drQ5zN6&rC0<3a%5!g}LnxY-Q30e& zad>f_0EOZlVoR2y2aGBLuWkVz+>5a%Rxhgn+nbOUuH!k=E!%1}+33%cr>o-XeQCx6 kb1S}?ThMj!VC$Oy0Ghv-ZEASoJpcdz07*qoM6N<$f{_{CUjP6A literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/core_delete_icon.png b/timcommon/src/main/res/drawable/core_delete_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..02c74fef7e120bedc53a6b7eb309ca78fe63f4dc GIT binary patch literal 3385 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 JPDHLkV1gFpA`<`r literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/core_list_divider.xml b/timcommon/src/main/res/drawable/core_list_divider.xml new file mode 100644 index 00000000..59cf049f --- /dev/null +++ b/timcommon/src/main/res/drawable/core_list_divider.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/core_minimalist_back_icon.png b/timcommon/src/main/res/drawable/core_minimalist_back_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..257376f39a1038f040f0ceeaf18ef047c8db78e4 GIT binary patch literal 293 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAifXMsm#F$0651qd^`PBTmZ3NjW4 zxjQkeJ16rJ$YDu$^mSxl*x1kgCy^D%HxBR#aRt&08Q+;xe=(>21R|!i-^|HC#t#rm z`^A#>ogw8rL+URkplr%drj(zoDL*RA_FV+(5-bVw3uaK*|Nnk{z=r+t4GsqP*S{~| zcDUgSl#KIqaSW+oe0!#yx7k6&<)H2V?rofZ!?}f2HwY=*6EWB&%kWZor^P&(-C0$O zX3l%DOug;p&rcsde6aaE@%7J{a(^EOlw5l<>r%BE5G;E&YnD&&^h_xupJJWW3^7^E X*AKIOwoMHE1#-QotDnm{r-UW|N-t`^ literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/core_positive_btn_bg.xml b/timcommon/src/main/res/drawable/core_positive_btn_bg.xml new file mode 100644 index 00000000..9063d532 --- /dev/null +++ b/timcommon/src/main/res/drawable/core_positive_btn_bg.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/core_positive_btn_disable_bg.xml b/timcommon/src/main/res/drawable/core_positive_btn_disable_bg.xml new file mode 100644 index 00000000..a8b242b8 --- /dev/null +++ b/timcommon/src/main/res/drawable/core_positive_btn_disable_bg.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/core_positive_btn_normal_bg.xml b/timcommon/src/main/res/drawable/core_positive_btn_normal_bg.xml new file mode 100644 index 00000000..745901df --- /dev/null +++ b/timcommon/src/main/res/drawable/core_positive_btn_normal_bg.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/core_positive_btn_pressed_bg.xml b/timcommon/src/main/res/drawable/core_positive_btn_pressed_bg.xml new file mode 100644 index 00000000..f53a3218 --- /dev/null +++ b/timcommon/src/main/res/drawable/core_positive_btn_pressed_bg.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/core_search_icon.png b/timcommon/src/main/res/drawable/core_search_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..76585b3c79a05f3a89e8bde5d9ce6fc9d9fe09c8 GIT binary patch literal 1286 zcmV+h1^N1kP)Px(z)3_wRCwC$oNH)QM-<1ejVAHcYP#{6CTgi-g(^iO27@g@(_%jONvIZ@Ht~Zk zaf|(^M6IG96hw#;41!q2Di|Ts+SXdM6-7PgJ~p`;-6U&CRT{731-fNN<4*~5chxZDLS|vOQIPA1PtEC_0ZfG7J+#OtF3~dqs_wTKcviMuU^0W!~GZVC4Qw2LeAZYPH5QYaV;kp5zV0Gh2`6d~{ zUsXXn`X_yO?a)3Av)-$i-xQ8?T`)2VwuS=PSz(_ z=QmjRn@~Xf&IiuIk~eDeBdqlSJS1!{#HIMeqZ{{ORVq;j7Gu;Cuu2G=o$nPW<3%SY z)z+gKu-9`A!`$JlQ5f@D09uDK^6MYy)`6YY9Nvam>v61E!*t8}9PU#GvBet2bEo@; z`lIczg!wQ|ryr_tA2&ER3vN??aw!)dpNe;Bzh*ArWth9EbN3@RKVJxsDTC?45FQ)( z$Fz+XkMIncAWXHE?*%O?Xgk9>M@Kajz#GgnGxiM1+_guh@ZN~rbf~8QvOY)Peu|T? z6?5pKSWPTGnCY5HMk*K;mAu1!6u=Ue9 zOp+4bz8Wpfq?l(ZE8{398h>PF3(o4;1m1pDcn654>xAcZRug@=&I<1)H^WHDL7ZtU z#x0MsxxH-B?UC+bkugx`*h;78a(tifHz*3_SRjaFJRTqqdX4+b9FiEFzCa zEJp1p(d?Hp@SGf?A9q>I-k-$6QX~T}KA7BnSKqMBf&h zm=ji}(F1r+Fkt_RY@EpudZQwnmu;ODTocB-M2dLMxFf`xD8~Xf>hq6aKwjmE(B|=% z)4UI1q!%(SE(Nsn-7K&?08jZZSbWTsd1>(FcggH#&-0fAK=y^*5JJae22ZkzaQmIM zV_B9DA!@GW`G1^{1laA3^Kc!Tk1Cki(BSdkez%aS!7u6WGgF;2NN;laaI55RSX z3bH49oaVdSEF}P)9B86uKZ^`CRR10=Xix^vFd!FN5{)x5+E7PkUP89D(ojqGMEJc2 w?J;f@`JP{xtOBzFORWzKEiElAEv@AB4FA$3z%hR>r~m)}07*qoM6N<$fh($ literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/indicator_point_nomal.png b/timcommon/src/main/res/drawable/indicator_point_nomal.png new file mode 100644 index 0000000000000000000000000000000000000000..324923303c83c8bc5e4ed776a86a8721dd26b038 GIT binary patch literal 216 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_+i`JOJ0Ar-ggh8*NQqQG-nM$~vC zN04|D|F;LuXQZBbyQ)@Mt&F)i@kqS1r5fkO#fv=o8jZ#E<_J#eZcA(Bl@rZ%Xg&VT zZF|k13HA?|%a**iNdM9&{QL1&`!CT+`|mwZV@odA|DgSa?|yTQXIh&Suj+Q6{EU-= zO)d=%b5xnm9dT9B;A*@wX-8(^nn(S@{cTAVWItM9<8nL8PLwnd2DeE3EV P=u`$zS3j3^P6Qp!O8NLGvVB;nF$-Xk38GS`S6S_!;>|0Ia#+iSd|^J*eon@BmIygV>3(B z;~5OxGJ$lkgTe~DWM4f=PF#v literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/message_send_fail.png b/timcommon/src/main/res/drawable/message_send_fail.png new file mode 100644 index 0000000000000000000000000000000000000000..ede352a8d3bf33ee4395aee2c844cae4f50472d8 GIT binary patch literal 701 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD3?#3*wSy!Wi-X*q7}lMWc?slH2l#}z0_pz= z0skZX{zv-!kMR8;;R9tv0@*<1_dg24081r-xgaT@|1m%4{^_H1Qoi6VuB-pXS=W;#Bp|(&pJFU*|r~ z{MhjAlVPc?+{N(>d!X96kGSw!$Wc}o`-~#uQnGQ?ESG-|;)8Fw| zb#wi)H}+D$gAVSyu~Gl}8>w?V!bfJTUgqDkQ7XFQ;`L^6&2`)4WIYSZ+TxAg+<$x1 zk&%Icqh-UtwVoU0mwjlfzyFuvqwuMI2m9o|0^g@RFg#M+$FAw0CC6%;T{YwD7M}u{ z5BvB;EH=LISYj;BS88GUD59%)J>NCEoxgwYWB6nl{AKBMK~Q>R@O1TaS?83{1OO7- BN$>yw literal 0 HcmV?d00001 diff --git a/timcommon/src/main/res/drawable/minimalist_switch_thumb.xml b/timcommon/src/main/res/drawable/minimalist_switch_thumb.xml new file mode 100644 index 00000000..d21a1422 --- /dev/null +++ b/timcommon/src/main/res/drawable/minimalist_switch_thumb.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/minimalist_switch_track.xml b/timcommon/src/main/res/drawable/minimalist_switch_track.xml new file mode 100644 index 00000000..3f0c1d46 --- /dev/null +++ b/timcommon/src/main/res/drawable/minimalist_switch_track.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/timcommon/src/main/res/drawable/minimalist_translation_area_bg.xml b/timcommon/src/main/res/drawable/minimalist_translation_area_bg.xml new file mode 100644 index 00000000..64d5036f --- /dev/null +++ b/timcommon/src/main/res/drawable/minimalist_translation_area_bg.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/popup_card_bg.xml b/timcommon/src/main/res/drawable/popup_card_bg.xml new file mode 100644 index 00000000..325f6cd3 --- /dev/null +++ b/timcommon/src/main/res/drawable/popup_card_bg.xml @@ -0,0 +1,13 @@ + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/quote_message_area_bg.xml b/timcommon/src/main/res/drawable/quote_message_area_bg.xml new file mode 100644 index 00000000..87bd817a --- /dev/null +++ b/timcommon/src/main/res/drawable/quote_message_area_bg.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/selected_border.xml b/timcommon/src/main/res/drawable/selected_border.xml new file mode 100644 index 00000000..dd048921 --- /dev/null +++ b/timcommon/src/main/res/drawable/selected_border.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/switch_thumb.xml b/timcommon/src/main/res/drawable/switch_thumb.xml new file mode 100644 index 00000000..a76dc222 --- /dev/null +++ b/timcommon/src/main/res/drawable/switch_thumb.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/switch_thumb_blue.xml b/timcommon/src/main/res/drawable/switch_thumb_blue.xml new file mode 100644 index 00000000..2fba5a66 --- /dev/null +++ b/timcommon/src/main/res/drawable/switch_thumb_blue.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/switch_thumb_gray.xml b/timcommon/src/main/res/drawable/switch_thumb_gray.xml new file mode 100644 index 00000000..280eaff5 --- /dev/null +++ b/timcommon/src/main/res/drawable/switch_thumb_gray.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/switch_track.xml b/timcommon/src/main/res/drawable/switch_track.xml new file mode 100644 index 00000000..986f41bf --- /dev/null +++ b/timcommon/src/main/res/drawable/switch_track.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/timcommon/src/main/res/drawable/switch_track_blue.xml b/timcommon/src/main/res/drawable/switch_track_blue.xml new file mode 100644 index 00000000..51e0eb20 --- /dev/null +++ b/timcommon/src/main/res/drawable/switch_track_blue.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/drawable/switch_track_gray.xml b/timcommon/src/main/res/drawable/switch_track_gray.xml new file mode 100644 index 00000000..ee7467c6 --- /dev/null +++ b/timcommon/src/main/res/drawable/switch_track_gray.xml @@ -0,0 +1,16 @@ + + + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/layout/chat_minimalist_reply_preview_layout.xml b/timcommon/src/main/res/layout/chat_minimalist_reply_preview_layout.xml new file mode 100644 index 00000000..e98ec46f --- /dev/null +++ b/timcommon/src/main/res/layout/chat_minimalist_reply_preview_layout.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/layout/chat_minimalist_text_status_layout.xml b/timcommon/src/main/res/layout/chat_minimalist_text_status_layout.xml new file mode 100644 index 00000000..557a85c4 --- /dev/null +++ b/timcommon/src/main/res/layout/chat_minimalist_text_status_layout.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/timcommon/src/main/res/layout/common_bottom_select_sheet.xml b/timcommon/src/main/res/layout/common_bottom_select_sheet.xml new file mode 100644 index 00000000..9da8bb04 --- /dev/null +++ b/timcommon/src/main/res/layout/common_bottom_select_sheet.xml @@ -0,0 +1,29 @@ + + + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/layout/common_bottom_sheet_item.xml b/timcommon/src/main/res/layout/common_bottom_sheet_item.xml new file mode 100644 index 00000000..94adbc74 --- /dev/null +++ b/timcommon/src/main/res/layout/common_bottom_sheet_item.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/timcommon/src/main/res/layout/common_dialog_view_layout.xml b/timcommon/src/main/res/layout/common_dialog_view_layout.xml new file mode 100644 index 00000000..34483537 --- /dev/null +++ b/timcommon/src/main/res/layout/common_dialog_view_layout.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + +