| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License |
| */ |
| |
| package com.android.internal.widget; |
| |
| import android.annotation.AttrRes; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.StyleRes; |
| import android.app.Person; |
| import android.content.Context; |
| import android.content.res.ColorStateList; |
| import android.graphics.Color; |
| import android.graphics.Point; |
| import android.graphics.Rect; |
| import android.graphics.drawable.Icon; |
| import android.text.TextUtils; |
| import android.util.AttributeSet; |
| import android.util.DisplayMetrics; |
| import android.util.Pools; |
| import android.util.TypedValue; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.ViewParent; |
| import android.view.ViewTreeObserver; |
| import android.widget.ImageView; |
| import android.widget.LinearLayout; |
| import android.widget.ProgressBar; |
| import android.widget.RemoteViews; |
| |
| import com.android.internal.R; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * A message of a {@link MessagingLayout}. |
| */ |
| @RemoteViews.RemoteView |
| public class MessagingGroup extends LinearLayout implements MessagingLinearLayout.MessagingChild { |
| private static Pools.SimplePool<MessagingGroup> sInstancePool |
| = new Pools.SynchronizedPool<>(10); |
| private MessagingLinearLayout mMessageContainer; |
| private ImageFloatingTextView mSenderName; |
| private ImageView mAvatarView; |
| private String mAvatarSymbol = ""; |
| private int mLayoutColor; |
| private CharSequence mAvatarName = ""; |
| private Icon mAvatarIcon; |
| private int mTextColor; |
| private int mSendingTextColor; |
| private List<MessagingMessage> mMessages; |
| private ArrayList<MessagingMessage> mAddedMessages = new ArrayList<>(); |
| private boolean mFirstLayout; |
| private boolean mIsHidingAnimated; |
| private boolean mNeedsGeneratedAvatar; |
| private Person mSender; |
| private boolean mImagesAtEnd; |
| private ViewGroup mImageContainer; |
| private MessagingImageMessage mIsolatedMessage; |
| private boolean mTransformingImages; |
| private Point mDisplaySize = new Point(); |
| private ProgressBar mSendingSpinner; |
| private View mSendingSpinnerContainer; |
| |
| public MessagingGroup(@NonNull Context context) { |
| super(context); |
| } |
| |
| public MessagingGroup(@NonNull Context context, @Nullable AttributeSet attrs) { |
| super(context, attrs); |
| } |
| |
| public MessagingGroup(@NonNull Context context, @Nullable AttributeSet attrs, |
| @AttrRes int defStyleAttr) { |
| super(context, attrs, defStyleAttr); |
| } |
| |
| public MessagingGroup(@NonNull Context context, @Nullable AttributeSet attrs, |
| @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { |
| super(context, attrs, defStyleAttr, defStyleRes); |
| } |
| |
| @Override |
| protected void onFinishInflate() { |
| super.onFinishInflate(); |
| mMessageContainer = findViewById(R.id.group_message_container); |
| mSenderName = findViewById(R.id.message_name); |
| mAvatarView = findViewById(R.id.message_icon); |
| mImageContainer = findViewById(R.id.messaging_group_icon_container); |
| mSendingSpinner = findViewById(R.id.messaging_group_sending_progress); |
| mSendingSpinnerContainer = findViewById(R.id.messaging_group_sending_progress_container); |
| DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); |
| mDisplaySize.x = displayMetrics.widthPixels; |
| mDisplaySize.y = displayMetrics.heightPixels; |
| } |
| |
| public void updateClipRect() { |
| // We want to clip to the senderName if it's available, otherwise our images will come |
| // from a weird position |
| Rect clipRect; |
| if (mSenderName.getVisibility() != View.GONE && !mTransformingImages) { |
| ViewGroup parent = (ViewGroup) mSenderName.getParent(); |
| int top = getDistanceFromParent(mSenderName, parent) - getDistanceFromParent( |
| mMessageContainer, parent) + mSenderName.getHeight(); |
| int size = Math.max(mDisplaySize.x, mDisplaySize.y); |
| clipRect = new Rect(0, top, size, size); |
| } else { |
| clipRect = null; |
| } |
| mMessageContainer.setClipBounds(clipRect); |
| } |
| |
| private int getDistanceFromParent(View searchedView, ViewGroup parent) { |
| int position = 0; |
| View view = searchedView; |
| while(view != parent) { |
| position += view.getTop() + view.getTranslationY(); |
| view = (View) view.getParent(); |
| } |
| return position; |
| } |
| |
| public void setSender(Person sender, CharSequence nameOverride) { |
| mSender = sender; |
| if (nameOverride == null) { |
| nameOverride = sender.getName(); |
| } |
| mSenderName.setText(nameOverride); |
| mNeedsGeneratedAvatar = sender.getIcon() == null; |
| if (!mNeedsGeneratedAvatar) { |
| setAvatar(sender.getIcon()); |
| } |
| mAvatarView.setVisibility(VISIBLE); |
| mSenderName.setVisibility(TextUtils.isEmpty(nameOverride) ? GONE : VISIBLE); |
| } |
| |
| public void setSending(boolean sending) { |
| int visibility = sending ? View.VISIBLE : View.GONE; |
| if (mSendingSpinnerContainer.getVisibility() != visibility) { |
| mSendingSpinnerContainer.setVisibility(visibility); |
| updateMessageColor(); |
| } |
| } |
| |
| private int calculateSendingTextColor() { |
| TypedValue alphaValue = new TypedValue(); |
| mContext.getResources().getValue( |
| R.dimen.notification_secondary_text_disabled_alpha, alphaValue, true); |
| float alpha = alphaValue.getFloat(); |
| return Color.valueOf( |
| Color.red(mTextColor), |
| Color.green(mTextColor), |
| Color.blue(mTextColor), |
| alpha).toArgb(); |
| } |
| |
| public void setAvatar(Icon icon) { |
| mAvatarIcon = icon; |
| mAvatarView.setImageIcon(icon); |
| mAvatarSymbol = ""; |
| mAvatarName = ""; |
| } |
| |
| static MessagingGroup createGroup(MessagingLinearLayout layout) {; |
| MessagingGroup createdGroup = sInstancePool.acquire(); |
| if (createdGroup == null) { |
| createdGroup = (MessagingGroup) LayoutInflater.from(layout.getContext()).inflate( |
| R.layout.notification_template_messaging_group, layout, |
| false); |
| createdGroup.addOnLayoutChangeListener(MessagingLayout.MESSAGING_PROPERTY_ANIMATOR); |
| } |
| layout.addView(createdGroup); |
| return createdGroup; |
| } |
| |
| public void removeMessage(MessagingMessage messagingMessage) { |
| View view = messagingMessage.getView(); |
| boolean wasShown = view.isShown(); |
| ViewGroup messageParent = (ViewGroup) view.getParent(); |
| if (messageParent == null) { |
| return; |
| } |
| messageParent.removeView(view); |
| Runnable recycleRunnable = () -> { |
| messageParent.removeTransientView(view); |
| messagingMessage.recycle(); |
| }; |
| if (wasShown && !MessagingLinearLayout.isGone(view)) { |
| messageParent.addTransientView(view, 0); |
| performRemoveAnimation(view, recycleRunnable); |
| } else { |
| recycleRunnable.run(); |
| } |
| } |
| |
| public void recycle() { |
| if (mIsolatedMessage != null) { |
| mImageContainer.removeView(mIsolatedMessage); |
| } |
| for (int i = 0; i < mMessages.size(); i++) { |
| MessagingMessage message = mMessages.get(i); |
| mMessageContainer.removeView(message.getView()); |
| message.recycle(); |
| } |
| setAvatar(null); |
| mAvatarView.setAlpha(1.0f); |
| mAvatarView.setTranslationY(0.0f); |
| mSenderName.setAlpha(1.0f); |
| mSenderName.setTranslationY(0.0f); |
| setAlpha(1.0f); |
| mIsolatedMessage = null; |
| mMessages = null; |
| mAddedMessages.clear(); |
| mFirstLayout = true; |
| MessagingPropertyAnimator.recycle(this); |
| sInstancePool.release(MessagingGroup.this); |
| } |
| |
| public void removeGroupAnimated(Runnable endAction) { |
| performRemoveAnimation(this, () -> { |
| setAlpha(1.0f); |
| MessagingPropertyAnimator.setToLaidOutPosition(this); |
| if (endAction != null) { |
| endAction.run(); |
| } |
| }); |
| } |
| |
| public void performRemoveAnimation(View message, Runnable endAction) { |
| performRemoveAnimation(message, -message.getHeight(), endAction); |
| } |
| |
| private void performRemoveAnimation(View view, int disappearTranslation, Runnable endAction) { |
| MessagingPropertyAnimator.startLocalTranslationTo(view, disappearTranslation, |
| MessagingLayout.FAST_OUT_LINEAR_IN); |
| MessagingPropertyAnimator.fadeOut(view, endAction); |
| } |
| |
| public CharSequence getSenderName() { |
| return mSenderName.getText(); |
| } |
| |
| public static void dropCache() { |
| sInstancePool = new Pools.SynchronizedPool<>(10); |
| } |
| |
| @Override |
| public int getMeasuredType() { |
| if (mIsolatedMessage != null) { |
| // We only want to show one group if we have an inline image, so let's return shortened |
| // to avoid displaying the other ones. |
| return MEASURED_SHORTENED; |
| } |
| boolean hasNormal = false; |
| for (int i = mMessageContainer.getChildCount() - 1; i >= 0; i--) { |
| View child = mMessageContainer.getChildAt(i); |
| if (child.getVisibility() == GONE) { |
| continue; |
| } |
| if (child instanceof MessagingLinearLayout.MessagingChild) { |
| int type = ((MessagingLinearLayout.MessagingChild) child).getMeasuredType(); |
| boolean tooSmall = type == MEASURED_TOO_SMALL; |
| final MessagingLinearLayout.LayoutParams lp = |
| (MessagingLinearLayout.LayoutParams) child.getLayoutParams(); |
| tooSmall |= lp.hide; |
| if (tooSmall) { |
| if (hasNormal) { |
| return MEASURED_SHORTENED; |
| } else { |
| return MEASURED_TOO_SMALL; |
| } |
| } else if (type == MEASURED_SHORTENED) { |
| return MEASURED_SHORTENED; |
| } else { |
| hasNormal = true; |
| } |
| } |
| } |
| return MEASURED_NORMAL; |
| } |
| |
| @Override |
| public int getConsumedLines() { |
| int result = 0; |
| for (int i = 0; i < mMessageContainer.getChildCount(); i++) { |
| View child = mMessageContainer.getChildAt(i); |
| if (child instanceof MessagingLinearLayout.MessagingChild) { |
| result += ((MessagingLinearLayout.MessagingChild) child).getConsumedLines(); |
| } |
| } |
| result = mIsolatedMessage != null ? Math.max(result, 1) : result; |
| // A group is usually taking up quite some space with the padding and the name, let's add 1 |
| return result + 1; |
| } |
| |
| @Override |
| public void setMaxDisplayedLines(int lines) { |
| mMessageContainer.setMaxDisplayedLines(lines); |
| } |
| |
| @Override |
| public void hideAnimated() { |
| setIsHidingAnimated(true); |
| removeGroupAnimated(() -> setIsHidingAnimated(false)); |
| } |
| |
| @Override |
| public boolean isHidingAnimated() { |
| return mIsHidingAnimated; |
| } |
| |
| private void setIsHidingAnimated(boolean isHiding) { |
| ViewParent parent = getParent(); |
| mIsHidingAnimated = isHiding; |
| invalidate(); |
| if (parent instanceof ViewGroup) { |
| ((ViewGroup) parent).invalidate(); |
| } |
| } |
| |
| @Override |
| public boolean hasOverlappingRendering() { |
| return false; |
| } |
| |
| public Icon getAvatarSymbolIfMatching(CharSequence avatarName, String avatarSymbol, |
| int layoutColor) { |
| if (mAvatarName.equals(avatarName) && mAvatarSymbol.equals(avatarSymbol) |
| && layoutColor == mLayoutColor) { |
| return mAvatarIcon; |
| } |
| return null; |
| } |
| |
| public void setCreatedAvatar(Icon cachedIcon, CharSequence avatarName, String avatarSymbol, |
| int layoutColor) { |
| if (!mAvatarName.equals(avatarName) || !mAvatarSymbol.equals(avatarSymbol) |
| || layoutColor != mLayoutColor) { |
| setAvatar(cachedIcon); |
| mAvatarSymbol = avatarSymbol; |
| setLayoutColor(layoutColor); |
| mAvatarName = avatarName; |
| } |
| } |
| |
| public void setTextColors(int senderTextColor, int messageTextColor) { |
| mTextColor = messageTextColor; |
| mSendingTextColor = calculateSendingTextColor(); |
| updateMessageColor(); |
| mSenderName.setTextColor(senderTextColor); |
| } |
| |
| public void setLayoutColor(int layoutColor) { |
| if (layoutColor != mLayoutColor){ |
| mLayoutColor = layoutColor; |
| mSendingSpinner.setIndeterminateTintList(ColorStateList.valueOf(mLayoutColor)); |
| } |
| } |
| |
| private void updateMessageColor() { |
| if (mMessages != null) { |
| int color = mSendingSpinnerContainer.getVisibility() == View.VISIBLE |
| ? mSendingTextColor : mTextColor; |
| for (MessagingMessage message : mMessages) { |
| message.setColor(message.getMessage().isRemoteInputHistory() ? color : mTextColor); |
| } |
| } |
| } |
| |
| public void setMessages(List<MessagingMessage> group) { |
| // Let's now make sure all children are added and in the correct order |
| int textMessageIndex = 0; |
| MessagingImageMessage isolatedMessage = null; |
| for (int messageIndex = 0; messageIndex < group.size(); messageIndex++) { |
| MessagingMessage message = group.get(messageIndex); |
| if (message.getGroup() != this) { |
| message.setMessagingGroup(this); |
| mAddedMessages.add(message); |
| } |
| boolean isImage = message instanceof MessagingImageMessage; |
| if (mImagesAtEnd && isImage) { |
| isolatedMessage = (MessagingImageMessage) message; |
| } else { |
| if (removeFromParentIfDifferent(message, mMessageContainer)) { |
| ViewGroup.LayoutParams layoutParams = message.getView().getLayoutParams(); |
| if (layoutParams != null |
| && !(layoutParams instanceof MessagingLinearLayout.LayoutParams)) { |
| message.getView().setLayoutParams( |
| mMessageContainer.generateDefaultLayoutParams()); |
| } |
| mMessageContainer.addView(message.getView(), textMessageIndex); |
| } |
| if (isImage) { |
| ((MessagingImageMessage) message).setIsolated(false); |
| } |
| // Let's sort them properly |
| if (textMessageIndex != mMessageContainer.indexOfChild(message.getView())) { |
| mMessageContainer.removeView(message.getView()); |
| mMessageContainer.addView(message.getView(), textMessageIndex); |
| } |
| textMessageIndex++; |
| } |
| } |
| if (isolatedMessage != null) { |
| if (removeFromParentIfDifferent(isolatedMessage, mImageContainer)) { |
| mImageContainer.removeAllViews(); |
| mImageContainer.addView(isolatedMessage.getView()); |
| } |
| isolatedMessage.setIsolated(true); |
| } else if (mIsolatedMessage != null) { |
| mImageContainer.removeAllViews(); |
| } |
| mIsolatedMessage = isolatedMessage; |
| updateImageContainerVisibility(); |
| mMessages = group; |
| updateMessageColor(); |
| } |
| |
| private void updateImageContainerVisibility() { |
| mImageContainer.setVisibility(mIsolatedMessage != null && mImagesAtEnd |
| ? View.VISIBLE : View.GONE); |
| } |
| |
| /** |
| * Remove the message from the parent if the parent isn't the one provided |
| * @return whether the message was removed |
| */ |
| private boolean removeFromParentIfDifferent(MessagingMessage message, ViewGroup newParent) { |
| ViewParent parent = message.getView().getParent(); |
| if (parent != newParent) { |
| if (parent instanceof ViewGroup) { |
| ((ViewGroup) parent).removeView(message.getView()); |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| @Override |
| protected void onLayout(boolean changed, int left, int top, int right, int bottom) { |
| super.onLayout(changed, left, top, right, bottom); |
| if (!mAddedMessages.isEmpty()) { |
| final boolean firstLayout = mFirstLayout; |
| getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { |
| @Override |
| public boolean onPreDraw() { |
| for (MessagingMessage message : mAddedMessages) { |
| if (!message.getView().isShown()) { |
| continue; |
| } |
| MessagingPropertyAnimator.fadeIn(message.getView()); |
| if (!firstLayout) { |
| MessagingPropertyAnimator.startLocalTranslationFrom(message.getView(), |
| message.getView().getHeight(), |
| MessagingLayout.LINEAR_OUT_SLOW_IN); |
| } |
| } |
| mAddedMessages.clear(); |
| getViewTreeObserver().removeOnPreDrawListener(this); |
| return true; |
| } |
| }); |
| } |
| mFirstLayout = false; |
| updateClipRect(); |
| } |
| |
| /** |
| * Calculates the group compatibility between this and another group. |
| * |
| * @param otherGroup the other group to compare it with |
| * |
| * @return 0 if the groups are totally incompatible or 1 + the number of matching messages if |
| * they match. |
| */ |
| public int calculateGroupCompatibility(MessagingGroup otherGroup) { |
| if (TextUtils.equals(getSenderName(),otherGroup.getSenderName())) { |
| int result = 1; |
| for (int i = 0; i < mMessages.size() && i < otherGroup.mMessages.size(); i++) { |
| MessagingMessage ownMessage = mMessages.get(mMessages.size() - 1 - i); |
| MessagingMessage otherMessage = otherGroup.mMessages.get( |
| otherGroup.mMessages.size() - 1 - i); |
| if (!ownMessage.sameAs(otherMessage)) { |
| return result; |
| } |
| result++; |
| } |
| return result; |
| } |
| return 0; |
| } |
| |
| public View getSenderView() { |
| return mSenderName; |
| } |
| |
| public View getAvatar() { |
| return mAvatarView; |
| } |
| |
| public MessagingLinearLayout getMessageContainer() { |
| return mMessageContainer; |
| } |
| |
| public MessagingImageMessage getIsolatedMessage() { |
| return mIsolatedMessage; |
| } |
| |
| public boolean needsGeneratedAvatar() { |
| return mNeedsGeneratedAvatar; |
| } |
| |
| public Person getSender() { |
| return mSender; |
| } |
| |
| public void setTransformingImages(boolean transformingImages) { |
| mTransformingImages = transformingImages; |
| } |
| |
| public void setDisplayImagesAtEnd(boolean atEnd) { |
| if (mImagesAtEnd != atEnd) { |
| mImagesAtEnd = atEnd; |
| updateImageContainerVisibility(); |
| } |
| } |
| |
| public List<MessagingMessage> getMessages() { |
| return mMessages; |
| } |
| } |