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 00000000..67d36c20 Binary files /dev/null and b/timcommon/src/main/res-light/drawable-xxhdpi/core_default_group_icon_community_light.png differ 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 00000000..f32ec472 Binary files /dev/null and b/timcommon/src/main/res-light/drawable-xxhdpi/core_default_group_icon_meeting_light.png differ diff --git a/timcommon/src/main/res-light/drawable-xxhdpi/core_default_group_icon_public_light.png b/timcommon/src/main/res-light/drawable-xxhdpi/core_default_group_icon_public_light.png new file mode 100644 index 00000000..03f988e7 Binary files /dev/null and b/timcommon/src/main/res-light/drawable-xxhdpi/core_default_group_icon_public_light.png differ 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 00000000..9002d453 Binary files /dev/null and b/timcommon/src/main/res-light/drawable-xxhdpi/core_default_group_icon_work_light.png differ diff --git a/timcommon/src/main/res-light/drawable-xxhdpi/core_default_user_icon_light.png b/timcommon/src/main/res-light/drawable-xxhdpi/core_default_user_icon_light.png new file mode 100644 index 00000000..b384546a Binary files /dev/null and b/timcommon/src/main/res-light/drawable-xxhdpi/core_default_user_icon_light.png differ diff --git a/timcommon/src/main/res-light/drawable-xxhdpi/core_online_status_light.png b/timcommon/src/main/res-light/drawable-xxhdpi/core_online_status_light.png new file mode 100644 index 00000000..cf07e72e Binary files /dev/null and b/timcommon/src/main/res-light/drawable-xxhdpi/core_online_status_light.png differ diff --git a/timcommon/src/main/res-light/drawable-xxhdpi/core_selected_icon_light.png b/timcommon/src/main/res-light/drawable-xxhdpi/core_selected_icon_light.png new file mode 100644 index 00000000..ae46e899 Binary files /dev/null and b/timcommon/src/main/res-light/drawable-xxhdpi/core_selected_icon_light.png differ 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 00000000..83d658d4 Binary files /dev/null and b/timcommon/src/main/res-light/drawable-xxhdpi/core_title_bar_back_light.png differ 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 00000000..19e4b81e Binary files /dev/null and b/timcommon/src/main/res-light/drawable/chat_reply_icon_light.png differ 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 00000000..028d8446 Binary files /dev/null and b/timcommon/src/main/res-lively/drawable-xxhdpi/core_default_group_icon_community_lively.png differ 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 00000000..5480bcbd Binary files /dev/null and b/timcommon/src/main/res-lively/drawable-xxhdpi/core_default_group_icon_meeting_lively.png differ diff --git a/timcommon/src/main/res-lively/drawable-xxhdpi/core_default_group_icon_public_lively.png b/timcommon/src/main/res-lively/drawable-xxhdpi/core_default_group_icon_public_lively.png new file mode 100644 index 00000000..18dd84c0 Binary files /dev/null and b/timcommon/src/main/res-lively/drawable-xxhdpi/core_default_group_icon_public_lively.png differ diff --git a/timcommon/src/main/res-lively/drawable-xxhdpi/core_default_group_icon_work_lively.png b/timcommon/src/main/res-lively/drawable-xxhdpi/core_default_group_icon_work_lively.png new file mode 100644 index 00000000..32e2fbfa Binary files /dev/null and b/timcommon/src/main/res-lively/drawable-xxhdpi/core_default_group_icon_work_lively.png differ diff --git a/timcommon/src/main/res-lively/drawable-xxhdpi/core_default_user_icon_lively.png b/timcommon/src/main/res-lively/drawable-xxhdpi/core_default_user_icon_lively.png new file mode 100644 index 00000000..b384546a Binary files /dev/null and b/timcommon/src/main/res-lively/drawable-xxhdpi/core_default_user_icon_lively.png differ diff --git a/timcommon/src/main/res-lively/drawable-xxhdpi/core_online_status_lively.png b/timcommon/src/main/res-lively/drawable-xxhdpi/core_online_status_lively.png new file mode 100644 index 00000000..69b05d8a Binary files /dev/null and b/timcommon/src/main/res-lively/drawable-xxhdpi/core_online_status_lively.png differ 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 00000000..c86e1ac4 Binary files /dev/null and b/timcommon/src/main/res-lively/drawable-xxhdpi/core_selected_icon_lively.png differ 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 00000000..20726344 Binary files /dev/null and b/timcommon/src/main/res-lively/drawable-xxhdpi/core_title_bar_back_lively.png differ 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 00000000..5e9158c4 Binary files /dev/null and b/timcommon/src/main/res-lively/drawable/chat_reply_icon_lively.png differ diff --git a/timcommon/src/main/res-lively/drawable/core_title_bar_bg_lively.xml b/timcommon/src/main/res-lively/drawable/core_title_bar_bg_lively.xml new file mode 100644 index 00000000..3d6ec232 --- /dev/null +++ b/timcommon/src/main/res-lively/drawable/core_title_bar_bg_lively.xml @@ -0,0 +1,8 @@ + + + + + + \ 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 00000000..1a2e2685 Binary files /dev/null and b/timcommon/src/main/res-serious/drawable-xxhdpi/core_default_group_icon_community_serious.png differ 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 00000000..76b9e153 Binary files /dev/null and b/timcommon/src/main/res-serious/drawable-xxhdpi/core_default_group_icon_meeting_serious.png differ diff --git a/timcommon/src/main/res-serious/drawable-xxhdpi/core_default_group_icon_public_serious.png b/timcommon/src/main/res-serious/drawable-xxhdpi/core_default_group_icon_public_serious.png new file mode 100644 index 00000000..a4f3e7b0 Binary files /dev/null and b/timcommon/src/main/res-serious/drawable-xxhdpi/core_default_group_icon_public_serious.png differ 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 00000000..70813998 Binary files /dev/null and b/timcommon/src/main/res-serious/drawable-xxhdpi/core_default_group_icon_work_serious.png differ diff --git a/timcommon/src/main/res-serious/drawable-xxhdpi/core_default_user_icon_serious.png b/timcommon/src/main/res-serious/drawable-xxhdpi/core_default_user_icon_serious.png new file mode 100644 index 00000000..b384546a Binary files /dev/null and b/timcommon/src/main/res-serious/drawable-xxhdpi/core_default_user_icon_serious.png differ diff --git a/timcommon/src/main/res-serious/drawable-xxhdpi/core_online_status_serious.png b/timcommon/src/main/res-serious/drawable-xxhdpi/core_online_status_serious.png new file mode 100644 index 00000000..ec31c4a3 Binary files /dev/null and b/timcommon/src/main/res-serious/drawable-xxhdpi/core_online_status_serious.png differ diff --git a/timcommon/src/main/res-serious/drawable-xxhdpi/core_selected_icon_serious.png b/timcommon/src/main/res-serious/drawable-xxhdpi/core_selected_icon_serious.png new file mode 100644 index 00000000..3486026f Binary files /dev/null and b/timcommon/src/main/res-serious/drawable-xxhdpi/core_selected_icon_serious.png differ diff --git a/timcommon/src/main/res-serious/drawable-xxhdpi/core_title_bar_back_serious.png b/timcommon/src/main/res-serious/drawable-xxhdpi/core_title_bar_back_serious.png new file mode 100644 index 00000000..20726344 Binary files /dev/null and b/timcommon/src/main/res-serious/drawable-xxhdpi/core_title_bar_back_serious.png differ 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 00000000..19e4b81e Binary files /dev/null and b/timcommon/src/main/res-serious/drawable/chat_reply_icon_serious.png differ 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 00000000..062ce9d3 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading00.png differ 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 00000000..41c1724c Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading01.png differ 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 00000000..23aac518 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading02.png differ diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading03.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading03.png new file mode 100644 index 00000000..32cc0f67 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading03.png differ diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading04.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading04.png new file mode 100644 index 00000000..ef8899f6 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading04.png differ diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading05.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading05.png new file mode 100644 index 00000000..4141cdd1 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading05.png differ 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 00000000..9002debd Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading06.png differ diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading07.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading07.png new file mode 100644 index 00000000..84432618 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading07.png differ diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading08.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading08.png new file mode 100644 index 00000000..7879c4b7 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading08.png differ 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 00000000..89a5fa33 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading09.png differ diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading10.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading10.png new file mode 100644 index 00000000..3e7c1214 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading10.png differ 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 00000000..7b0c3094 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading11.png differ diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading12.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading12.png new file mode 100644 index 00000000..884cdffb Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading12.png differ 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 00000000..917febae Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading13.png differ 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 00000000..b5f1ad34 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading14.png differ diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading15.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading15.png new file mode 100644 index 00000000..8cd3a406 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading15.png differ diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading16.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading16.png new file mode 100644 index 00000000..f2cc6ee6 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading16.png differ 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 00000000..e6305cb4 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading17.png differ 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 00000000..06f90083 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading18.png differ 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 00000000..672181d1 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading19.png differ diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading20.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading20.png new file mode 100644 index 00000000..902e3e25 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading20.png differ diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading21.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading21.png new file mode 100644 index 00000000..afc12d52 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading21.png differ diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading22.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading22.png new file mode 100644 index 00000000..ca5d98a0 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading22.png differ diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading23.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading23.png new file mode 100644 index 00000000..ebc5872f Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading23.png differ diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading24.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading24.png new file mode 100644 index 00000000..63920441 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading24.png differ 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 00000000..5830e1ce Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading25.png differ diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading26.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading26.png new file mode 100644 index 00000000..819db1a2 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading26.png differ 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 00000000..f9653c1e Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading27.png differ 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 00000000..842fef49 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading28.png differ diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading29.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading29.png new file mode 100644 index 00000000..9555de63 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading29.png differ diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading30.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading30.png new file mode 100644 index 00000000..a6ebf210 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading30.png differ 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 00000000..281a9e36 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading31.png differ diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading32.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading32.png new file mode 100644 index 00000000..0dc299cd Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading32.png differ 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 00000000..7ebe0b19 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading33.png differ diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading34.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading34.png new file mode 100644 index 00000000..8c7a556a Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading34.png differ diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading35.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading35.png new file mode 100644 index 00000000..040edf72 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading35.png differ 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 00000000..2135b478 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading36.png differ 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 00000000..f5582bf6 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading37.png differ 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 00000000..e6a366cc Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading38.png differ diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading39.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading39.png new file mode 100644 index 00000000..bf0cc027 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading39.png differ diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading40.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading40.png new file mode 100644 index 00000000..43a4f7e7 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading40.png differ 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 00000000..a0960b58 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading41.png differ diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading42.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading42.png new file mode 100644 index 00000000..d52b140b Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading42.png differ 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 00000000..01220427 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading43.png differ diff --git a/timcommon/src/main/res/drawable/chat_minimalist_anim_loading44.png b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading44.png new file mode 100644 index 00000000..a7d64daa Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_anim_loading44.png differ diff --git a/timcommon/src/main/res/drawable/chat_minimalist_file_download_icon.png b/timcommon/src/main/res/drawable/chat_minimalist_file_download_icon.png new file mode 100644 index 00000000..4c83f965 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_file_download_icon.png differ diff --git a/timcommon/src/main/res/drawable/chat_minimalist_message_status_send_all_read.png b/timcommon/src/main/res/drawable/chat_minimalist_message_status_send_all_read.png new file mode 100644 index 00000000..02d8ea07 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_message_status_send_all_read.png differ 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 00000000..cac29f38 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_message_status_send_failed.png differ diff --git a/timcommon/src/main/res/drawable/chat_minimalist_message_status_send_no_read.png b/timcommon/src/main/res/drawable/chat_minimalist_message_status_send_no_read.png new file mode 100644 index 00000000..3d94f356 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_message_status_send_no_read.png differ 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 00000000..ada629c6 Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_minimalist_message_status_send_part_read.png differ 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 00000000..20c7ce5e Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_reply_more_icon.png differ diff --git a/timcommon/src/main/res/drawable/chat_unselected_icon.png b/timcommon/src/main/res/drawable/chat_unselected_icon.png new file mode 100644 index 00000000..357ddc8d Binary files /dev/null and b/timcommon/src/main/res/drawable/chat_unselected_icon.png differ 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 00000000..a3ea33a4 Binary files /dev/null and b/timcommon/src/main/res/drawable/common_arrow_right.png differ 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 00000000..f2d0c79a Binary files /dev/null and b/timcommon/src/main/res/drawable/common_check_box_selected.png differ 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 00000000..630c7128 Binary files /dev/null and b/timcommon/src/main/res/drawable/common_check_box_unselected.png differ 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 00000000..7897e8ba Binary files /dev/null and b/timcommon/src/main/res/drawable/common_title_bar_home_icon.png differ 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 00000000..113bfca0 Binary files /dev/null and b/timcommon/src/main/res/drawable/common_trans_bg.png differ diff --git a/timcommon/src/main/res/drawable/core_close_icon.png b/timcommon/src/main/res/drawable/core_close_icon.png new file mode 100644 index 00000000..57a14c2c Binary files /dev/null and b/timcommon/src/main/res/drawable/core_close_icon.png differ diff --git a/timcommon/src/main/res/drawable/core_default_group_icon_community.png b/timcommon/src/main/res/drawable/core_default_group_icon_community.png new file mode 100644 index 00000000..1cc8d447 Binary files /dev/null and b/timcommon/src/main/res/drawable/core_default_group_icon_community.png differ 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 00000000..02c74fef Binary files /dev/null and b/timcommon/src/main/res/drawable/core_delete_icon.png differ diff --git a/timcommon/src/main/res/drawable/core_icon_offline_status.png b/timcommon/src/main/res/drawable/core_icon_offline_status.png new file mode 100644 index 00000000..f326e14e Binary files /dev/null and b/timcommon/src/main/res/drawable/core_icon_offline_status.png differ 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 00000000..257376f3 Binary files /dev/null and b/timcommon/src/main/res/drawable/core_minimalist_back_icon.png differ 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 00000000..76585b3c Binary files /dev/null and b/timcommon/src/main/res/drawable/core_search_icon.png differ 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 00000000..32492330 Binary files /dev/null and b/timcommon/src/main/res/drawable/indicator_point_nomal.png differ diff --git a/timcommon/src/main/res/drawable/indicator_point_select.png b/timcommon/src/main/res/drawable/indicator_point_select.png new file mode 100644 index 00000000..3b6aa41b Binary files /dev/null and b/timcommon/src/main/res/drawable/indicator_point_select.png differ 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 00000000..ede352a8 Binary files /dev/null and b/timcommon/src/main/res/drawable/message_send_fail.png differ 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 @@ + + + + + + + + + + + + +