1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.internal.widget; 18 19 import static com.android.internal.widget.MessagingGroup.IMAGE_DISPLAY_LOCATION_EXTERNAL; 20 import static com.android.internal.widget.MessagingGroup.IMAGE_DISPLAY_LOCATION_INLINE; 21 22 import android.annotation.AttrRes; 23 import android.annotation.NonNull; 24 import android.annotation.Nullable; 25 import android.annotation.StyleRes; 26 import android.app.Notification; 27 import android.app.Person; 28 import android.app.RemoteInputHistoryItem; 29 import android.content.Context; 30 import android.graphics.Rect; 31 import android.graphics.drawable.Icon; 32 import android.os.Bundle; 33 import android.os.Parcelable; 34 import android.text.TextUtils; 35 import android.util.ArrayMap; 36 import android.util.AttributeSet; 37 import android.util.DisplayMetrics; 38 import android.view.RemotableViewMethod; 39 import android.view.View; 40 import android.view.ViewGroup; 41 import android.view.ViewTreeObserver; 42 import android.view.animation.Interpolator; 43 import android.view.animation.PathInterpolator; 44 import android.widget.FrameLayout; 45 import android.widget.ImageView; 46 import android.widget.RemoteViews; 47 48 import com.android.internal.R; 49 import com.android.internal.util.ContrastColorUtil; 50 51 import java.util.ArrayList; 52 import java.util.List; 53 import java.util.Map; 54 55 /** 56 * A custom-built layout for the Notification.MessagingStyle allows dynamic addition and removal 57 * messages and adapts the layout accordingly. 58 */ 59 @RemoteViews.RemoteView 60 public class MessagingLayout extends FrameLayout 61 implements ImageMessageConsumer, IMessagingLayout { 62 63 private static final float COLOR_SHIFT_AMOUNT = 60; 64 public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f); 65 public static final Interpolator FAST_OUT_LINEAR_IN = new PathInterpolator(0.4f, 0f, 1f, 1f); 66 public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f); 67 public static final OnLayoutChangeListener MESSAGING_PROPERTY_ANIMATOR 68 = new MessagingPropertyAnimator(); 69 private final PeopleHelper mPeopleHelper = new PeopleHelper(); 70 private List<MessagingMessage> mMessages = new ArrayList<>(); 71 private List<MessagingMessage> mHistoricMessages = new ArrayList<>(); 72 private MessagingLinearLayout mMessagingLinearLayout; 73 private boolean mShowHistoricMessages; 74 private ArrayList<MessagingGroup> mGroups = new ArrayList<>(); 75 private MessagingLinearLayout mImageMessageContainer; 76 private ImageView mRightIconView; 77 private Rect mMessagingClipRect; 78 private int mLayoutColor; 79 private int mSenderTextColor; 80 private int mMessageTextColor; 81 private Icon mAvatarReplacement; 82 private boolean mIsOneToOne; 83 private ArrayList<MessagingGroup> mAddedGroups = new ArrayList<>(); 84 private Person mUser; 85 private CharSequence mNameReplacement; 86 private boolean mIsCollapsed; 87 private ImageResolver mImageResolver; 88 private CharSequence mConversationTitle; 89 private ArrayList<MessagingLinearLayout.MessagingChild> mToRecycle = new ArrayList<>(); 90 MessagingLayout(@onNull Context context)91 public MessagingLayout(@NonNull Context context) { 92 super(context); 93 } 94 MessagingLayout(@onNull Context context, @Nullable AttributeSet attrs)95 public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs) { 96 super(context, attrs); 97 } 98 MessagingLayout(@onNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr)99 public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs, 100 @AttrRes int defStyleAttr) { 101 super(context, attrs, defStyleAttr); 102 } 103 MessagingLayout(@onNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes)104 public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs, 105 @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { 106 super(context, attrs, defStyleAttr, defStyleRes); 107 } 108 109 @Override onFinishInflate()110 protected void onFinishInflate() { 111 super.onFinishInflate(); 112 mPeopleHelper.init(getContext()); 113 mMessagingLinearLayout = findViewById(R.id.notification_messaging); 114 mImageMessageContainer = findViewById(R.id.conversation_image_message_container); 115 mRightIconView = findViewById(R.id.right_icon); 116 // We still want to clip, but only on the top, since views can temporarily out of bounds 117 // during transitions. 118 DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); 119 int size = Math.max(displayMetrics.widthPixels, displayMetrics.heightPixels); 120 mMessagingClipRect = new Rect(0, 0, size, size); 121 setMessagingClippingDisabled(false); 122 } 123 124 @RemotableViewMethod setAvatarReplacement(Icon icon)125 public void setAvatarReplacement(Icon icon) { 126 mAvatarReplacement = icon; 127 } 128 129 @RemotableViewMethod setNameReplacement(CharSequence nameReplacement)130 public void setNameReplacement(CharSequence nameReplacement) { 131 mNameReplacement = nameReplacement; 132 } 133 134 /** 135 * Set this layout to show the collapsed representation. 136 * 137 * @param isCollapsed is it collapsed 138 */ 139 @RemotableViewMethod setIsCollapsed(boolean isCollapsed)140 public void setIsCollapsed(boolean isCollapsed) { 141 mIsCollapsed = isCollapsed; 142 } 143 144 @RemotableViewMethod setLargeIcon(Icon largeIcon)145 public void setLargeIcon(Icon largeIcon) { 146 // Unused 147 } 148 149 /** 150 * Sets the conversation title of this conversation. 151 * 152 * @param conversationTitle the conversation title 153 */ 154 @RemotableViewMethod setConversationTitle(CharSequence conversationTitle)155 public void setConversationTitle(CharSequence conversationTitle) { 156 mConversationTitle = conversationTitle; 157 } 158 159 /** 160 * Set Messaging data 161 * @param extras Bundle contains messaging data 162 */ 163 @RemotableViewMethod(asyncImpl = "setDataAsync") setData(Bundle extras)164 public void setData(Bundle extras) { 165 Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES); 166 List<Notification.MessagingStyle.Message> newMessages 167 = Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages); 168 Parcelable[] histMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES); 169 List<Notification.MessagingStyle.Message> newHistoricMessages 170 = Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages); 171 setUser(extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON, android.app.Person.class)); 172 RemoteInputHistoryItem[] history = (RemoteInputHistoryItem[]) 173 extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS, android.app.RemoteInputHistoryItem.class); 174 addRemoteInputHistoryToMessages(newMessages, history); 175 176 final Person user = extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON, Person.class); 177 boolean showSpinner = 178 extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false); 179 180 final List<MessagingMessage> historicMessagingMessages = createMessages(newHistoricMessages, 181 /* isHistoric= */true, /* usePrecomputedText= */ false); 182 final List<MessagingMessage> newMessagingMessages = 183 createMessages(newMessages, /* isHistoric= */false, /* usePrecomputedText= */false); 184 bindViews(user, showSpinner, historicMessagingMessages, newMessagingMessages); 185 } 186 187 /** 188 * RemotableViewMethod's asyncImpl of {@link #setData(Bundle)}. 189 * This should be called on a background thread, and returns a Runnable which is then must be 190 * called on the main thread to complete the operation and set text. 191 * @param extras Bundle contains messaging data 192 * @hide 193 */ 194 @NonNull setDataAsync(Bundle extras)195 public Runnable setDataAsync(Bundle extras) { 196 return () -> setData(extras); 197 } 198 199 @Override setImageResolver(ImageResolver resolver)200 public void setImageResolver(ImageResolver resolver) { 201 mImageResolver = resolver; 202 } 203 addRemoteInputHistoryToMessages( List<Notification.MessagingStyle.Message> newMessages, RemoteInputHistoryItem[] remoteInputHistory)204 private void addRemoteInputHistoryToMessages( 205 List<Notification.MessagingStyle.Message> newMessages, 206 RemoteInputHistoryItem[] remoteInputHistory) { 207 if (remoteInputHistory == null || remoteInputHistory.length == 0) { 208 return; 209 } 210 for (int i = remoteInputHistory.length - 1; i >= 0; i--) { 211 RemoteInputHistoryItem historyMessage = remoteInputHistory[i]; 212 Notification.MessagingStyle.Message message = new Notification.MessagingStyle.Message( 213 historyMessage.getText(), 0, (Person) null, true /* remoteHistory */); 214 if (historyMessage.getUri() != null) { 215 message.setData(historyMessage.getMimeType(), historyMessage.getUri()); 216 } 217 newMessages.add(message); 218 } 219 } 220 bindViews(Person user, boolean showSpinner, List<MessagingMessage> historicMessagingMessages, List<MessagingMessage> newMessagingMessages)221 private void bindViews(Person user, boolean showSpinner, 222 List<MessagingMessage> historicMessagingMessages, 223 List<MessagingMessage> newMessagingMessages) { 224 setUser(user); 225 bind(showSpinner, historicMessagingMessages, newMessagingMessages); 226 } 227 bind(boolean showSpinner, List<MessagingMessage> historicMessages, List<MessagingMessage> messages)228 private void bind(boolean showSpinner, List<MessagingMessage> historicMessages, 229 List<MessagingMessage> messages) { 230 ArrayList<MessagingGroup> oldGroups = new ArrayList<>(mGroups); 231 addMessagesToGroups(historicMessages, messages, showSpinner); 232 233 // Let's first check which groups were removed altogether and remove them in one animation 234 removeGroups(oldGroups); 235 236 // Let's remove the remaining messages 237 for (MessagingMessage message : mMessages) { 238 message.removeMessage(mToRecycle); 239 } 240 for (MessagingMessage historicMessage : mHistoricMessages) { 241 historicMessage.removeMessage(mToRecycle); 242 } 243 244 mMessages = messages; 245 mHistoricMessages = historicMessages; 246 247 updateHistoricMessageVisibility(); 248 updateTitleAndNamesDisplay(); 249 // after groups are finalized, hide the first sender name if it's showing as the title 250 mPeopleHelper.maybeHideFirstSenderName(mGroups, mIsOneToOne, mConversationTitle); 251 updateImageMessages(); 252 253 // Recycle everything at the end of the update, now that we know it's no longer needed. 254 for (MessagingLinearLayout.MessagingChild child : mToRecycle) { 255 child.recycle(); 256 } 257 mToRecycle.clear(); 258 } 259 updateImageMessages()260 private void updateImageMessages() { 261 View newMessage = null; 262 if (mImageMessageContainer == null) { 263 return; 264 } 265 if (mIsCollapsed && !mGroups.isEmpty()) { 266 // When collapsed, we're displaying the image message in a dedicated container 267 // on the right of the layout instead of inline. Let's add the isolated image there 268 MessagingGroup messagingGroup = mGroups.get(mGroups.size() - 1); 269 MessagingImageMessage isolatedMessage = messagingGroup.getIsolatedMessage(); 270 if (isolatedMessage != null) { 271 newMessage = isolatedMessage.getView(); 272 } 273 } 274 // Remove all messages that don't belong into the image layout 275 View previousMessage = mImageMessageContainer.getChildAt(0); 276 if (previousMessage != newMessage) { 277 mImageMessageContainer.removeView(previousMessage); 278 if (newMessage != null) { 279 mImageMessageContainer.addView(newMessage); 280 } 281 } 282 mImageMessageContainer.setVisibility(newMessage != null ? VISIBLE : GONE); 283 284 // When showing an image message, do not show the large icon. Removing the drawable 285 // prevents it from being shown in the left_icon view (by the grouping util). 286 if (newMessage != null && mRightIconView != null && mRightIconView.getDrawable() != null) { 287 mRightIconView.setImageDrawable(null); 288 mRightIconView.setVisibility(GONE); 289 } 290 } 291 removeGroups(ArrayList<MessagingGroup> oldGroups)292 private void removeGroups(ArrayList<MessagingGroup> oldGroups) { 293 int size = oldGroups.size(); 294 for (int i = 0; i < size; i++) { 295 MessagingGroup group = oldGroups.get(i); 296 if (!mGroups.contains(group)) { 297 List<MessagingMessage> messages = group.getMessages(); 298 299 boolean wasShown = group.isShown(); 300 mMessagingLinearLayout.removeView(group); 301 if (wasShown && !MessagingLinearLayout.isGone(group)) { 302 mMessagingLinearLayout.addTransientView(group, 0); 303 group.removeGroupAnimated(() -> { 304 mMessagingLinearLayout.removeTransientView(group); 305 group.recycle(); 306 }); 307 } else { 308 mToRecycle.add(group); 309 } 310 mMessages.removeAll(messages); 311 mHistoricMessages.removeAll(messages); 312 } 313 } 314 } 315 updateTitleAndNamesDisplay()316 private void updateTitleAndNamesDisplay() { 317 Map<CharSequence, String> uniqueNames = mPeopleHelper.mapUniqueNamesToPrefix(mGroups); 318 319 // Now that we have the correct symbols, let's look what we have cached 320 ArrayMap<CharSequence, Icon> cachedAvatars = new ArrayMap<>(); 321 for (int i = 0; i < mGroups.size(); i++) { 322 // Let's now set the avatars 323 MessagingGroup group = mGroups.get(i); 324 boolean isOwnMessage = group.getSender() == mUser; 325 CharSequence senderName = group.getSenderName(); 326 if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName) 327 || (mIsOneToOne && mAvatarReplacement != null && !isOwnMessage)) { 328 continue; 329 } 330 String symbol = uniqueNames.get(senderName); 331 Icon cachedIcon = group.getAvatarSymbolIfMatching(senderName, 332 symbol, mLayoutColor); 333 if (cachedIcon != null) { 334 cachedAvatars.put(senderName, cachedIcon); 335 } 336 } 337 338 for (int i = 0; i < mGroups.size(); i++) { 339 // Let's now set the avatars 340 MessagingGroup group = mGroups.get(i); 341 CharSequence senderName = group.getSenderName(); 342 if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) { 343 continue; 344 } 345 if (mIsOneToOne && mAvatarReplacement != null && group.getSender() != mUser) { 346 group.setAvatar(mAvatarReplacement); 347 } else { 348 Icon cachedIcon = cachedAvatars.get(senderName); 349 if (cachedIcon == null) { 350 cachedIcon = createAvatarSymbol(senderName, uniqueNames.get(senderName), 351 mLayoutColor); 352 cachedAvatars.put(senderName, cachedIcon); 353 } 354 group.setCreatedAvatar(cachedIcon, senderName, uniqueNames.get(senderName), 355 mLayoutColor); 356 } 357 } 358 } 359 createAvatarSymbol(CharSequence senderName, String symbol, int layoutColor)360 public Icon createAvatarSymbol(CharSequence senderName, String symbol, int layoutColor) { 361 return mPeopleHelper.createAvatarSymbol(senderName, symbol, layoutColor); 362 } 363 findColor(CharSequence senderName, int layoutColor)364 private int findColor(CharSequence senderName, int layoutColor) { 365 double luminance = ContrastColorUtil.calculateLuminance(layoutColor); 366 float shift = Math.abs(senderName.hashCode()) % 5 / 4.0f - 0.5f; 367 368 // we need to offset the range if the luminance is too close to the borders 369 shift += Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - luminance, 0); 370 shift -= Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - (1.0f - luminance), 0); 371 return ContrastColorUtil.getShiftedColor(layoutColor, 372 (int) (shift * COLOR_SHIFT_AMOUNT)); 373 } 374 findNameSplit(String existingName)375 private String findNameSplit(String existingName) { 376 String[] split = existingName.split(" "); 377 if (split.length > 1) { 378 return Character.toString(split[0].charAt(0)) 379 + Character.toString(split[1].charAt(0)); 380 } 381 return existingName.substring(0, 1); 382 } 383 384 @RemotableViewMethod setLayoutColor(int color)385 public void setLayoutColor(int color) { 386 mLayoutColor = color; 387 } 388 389 @RemotableViewMethod setIsOneToOne(boolean oneToOne)390 public void setIsOneToOne(boolean oneToOne) { 391 mIsOneToOne = oneToOne; 392 } 393 394 @RemotableViewMethod setSenderTextColor(int color)395 public void setSenderTextColor(int color) { 396 mSenderTextColor = color; 397 } 398 399 400 /** 401 * @param color the color of the notification background 402 */ 403 @RemotableViewMethod setNotificationBackgroundColor(int color)404 public void setNotificationBackgroundColor(int color) { 405 // Nothing to do with this 406 } 407 408 @RemotableViewMethod setMessageTextColor(int color)409 public void setMessageTextColor(int color) { 410 mMessageTextColor = color; 411 } 412 setUser(Person user)413 public void setUser(Person user) { 414 mUser = user; 415 if (mUser.getIcon() == null) { 416 Icon userIcon = Icon.createWithResource(getContext(), 417 com.android.internal.R.drawable.messaging_user); 418 userIcon.setTint(mLayoutColor); 419 mUser = mUser.toBuilder().setIcon(userIcon).build(); 420 } 421 } 422 addMessagesToGroups(List<MessagingMessage> historicMessages, List<MessagingMessage> messages, boolean showSpinner)423 private void addMessagesToGroups(List<MessagingMessage> historicMessages, 424 List<MessagingMessage> messages, boolean showSpinner) { 425 // Let's first find our groups! 426 List<List<MessagingMessage>> groups = new ArrayList<>(); 427 List<Person> senders = new ArrayList<>(); 428 429 // Lets first find the groups 430 findGroups(historicMessages, messages, groups, senders); 431 432 // Let's now create the views and reorder them accordingly 433 createGroupViews(groups, senders, showSpinner); 434 } 435 createGroupViews(List<List<MessagingMessage>> groups, List<Person> senders, boolean showSpinner)436 private void createGroupViews(List<List<MessagingMessage>> groups, 437 List<Person> senders, boolean showSpinner) { 438 mGroups.clear(); 439 for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) { 440 List<MessagingMessage> group = groups.get(groupIndex); 441 MessagingGroup newGroup = null; 442 // we'll just take the first group that exists or create one there is none 443 for (int messageIndex = group.size() - 1; messageIndex >= 0; messageIndex--) { 444 MessagingMessage message = group.get(messageIndex); 445 newGroup = message.getGroup(); 446 if (newGroup != null) { 447 break; 448 } 449 } 450 if (newGroup == null) { 451 newGroup = MessagingGroup.createGroup(mMessagingLinearLayout); 452 mAddedGroups.add(newGroup); 453 } else if (newGroup.getParent() != mMessagingLinearLayout) { 454 throw new IllegalStateException( 455 "group parent was " + newGroup.getParent() + " but expected " 456 + mMessagingLinearLayout); 457 } 458 newGroup.setImageDisplayLocation(mIsCollapsed 459 ? IMAGE_DISPLAY_LOCATION_EXTERNAL 460 : IMAGE_DISPLAY_LOCATION_INLINE); 461 newGroup.setIsInConversation(false); 462 newGroup.setLayoutColor(mLayoutColor); 463 newGroup.setTextColors(mSenderTextColor, mMessageTextColor); 464 Person sender = senders.get(groupIndex); 465 CharSequence nameOverride = null; 466 if (sender != mUser && mNameReplacement != null) { 467 nameOverride = mNameReplacement; 468 } 469 newGroup.setSingleLine(mIsCollapsed); 470 newGroup.setShowingAvatar(!mIsCollapsed); 471 newGroup.setSender(sender, nameOverride); 472 newGroup.setSending(groupIndex == (groups.size() - 1) && showSpinner); 473 mGroups.add(newGroup); 474 475 if (mMessagingLinearLayout.indexOfChild(newGroup) != groupIndex) { 476 mMessagingLinearLayout.removeView(newGroup); 477 mMessagingLinearLayout.addView(newGroup, groupIndex); 478 } 479 newGroup.setMessages(group); 480 } 481 } 482 findGroups(List<MessagingMessage> historicMessages, List<MessagingMessage> messages, List<List<MessagingMessage>> groups, List<Person> senders)483 private void findGroups(List<MessagingMessage> historicMessages, 484 List<MessagingMessage> messages, List<List<MessagingMessage>> groups, 485 List<Person> senders) { 486 CharSequence currentSenderKey = null; 487 List<MessagingMessage> currentGroup = null; 488 int histSize = historicMessages.size(); 489 for (int i = 0; i < histSize + messages.size(); i++) { 490 MessagingMessage message; 491 if (i < histSize) { 492 message = historicMessages.get(i); 493 } else { 494 message = messages.get(i - histSize); 495 } 496 boolean isNewGroup = currentGroup == null; 497 Person sender = 498 message.getMessage() == null ? null : message.getMessage().getSenderPerson(); 499 CharSequence key = sender == null ? null 500 : sender.getKey() == null ? sender.getName() : sender.getKey(); 501 isNewGroup |= !TextUtils.equals(key, currentSenderKey); 502 if (isNewGroup) { 503 currentGroup = new ArrayList<>(); 504 groups.add(currentGroup); 505 if (sender == null) { 506 sender = mUser; 507 } 508 senders.add(sender); 509 currentSenderKey = key; 510 } 511 currentGroup.add(message); 512 } 513 } 514 515 /** 516 * Creates new messages, reusing existing ones if they are available. 517 * 518 * @param newMessages the messages to parse. 519 */ createMessages( List<Notification.MessagingStyle.Message> newMessages, boolean isHistoric, boolean usePrecomputedText)520 private List<MessagingMessage> createMessages( 521 List<Notification.MessagingStyle.Message> newMessages, boolean isHistoric, 522 boolean usePrecomputedText) { 523 List<MessagingMessage> result = new ArrayList<>(); 524 for (int i = 0; i < newMessages.size(); i++) { 525 Notification.MessagingStyle.Message m = newMessages.get(i); 526 MessagingMessage message = findAndRemoveMatchingMessage(m); 527 if (message == null) { 528 message = MessagingMessage.createMessage(this, m, 529 mImageResolver, usePrecomputedText); 530 } 531 message.setIsHistoric(isHistoric); 532 result.add(message); 533 } 534 return result; 535 } 536 findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m)537 private MessagingMessage findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m) { 538 for (int i = 0; i < mMessages.size(); i++) { 539 MessagingMessage existing = mMessages.get(i); 540 if (existing.sameAs(m)) { 541 mMessages.remove(i); 542 return existing; 543 } 544 } 545 for (int i = 0; i < mHistoricMessages.size(); i++) { 546 MessagingMessage existing = mHistoricMessages.get(i); 547 if (existing.sameAs(m)) { 548 mHistoricMessages.remove(i); 549 return existing; 550 } 551 } 552 return null; 553 } 554 showHistoricMessages(boolean show)555 public void showHistoricMessages(boolean show) { 556 mShowHistoricMessages = show; 557 updateHistoricMessageVisibility(); 558 } 559 updateHistoricMessageVisibility()560 private void updateHistoricMessageVisibility() { 561 int numHistoric = mHistoricMessages.size(); 562 for (int i = 0; i < numHistoric; i++) { 563 MessagingMessage existing = mHistoricMessages.get(i); 564 existing.setVisibility(mShowHistoricMessages ? VISIBLE : GONE); 565 } 566 int numGroups = mGroups.size(); 567 for (int i = 0; i < numGroups; i++) { 568 MessagingGroup group = mGroups.get(i); 569 int visibleChildren = 0; 570 List<MessagingMessage> messages = group.getMessages(); 571 int numGroupMessages = messages.size(); 572 for (int j = 0; j < numGroupMessages; j++) { 573 MessagingMessage message = messages.get(j); 574 if (message.getVisibility() != GONE) { 575 visibleChildren++; 576 } 577 } 578 if (visibleChildren > 0 && group.getVisibility() == GONE) { 579 group.setVisibility(VISIBLE); 580 } else if (visibleChildren == 0 && group.getVisibility() != GONE) { 581 group.setVisibility(GONE); 582 } 583 } 584 } 585 586 @Override onLayout(boolean changed, int left, int top, int right, int bottom)587 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 588 super.onLayout(changed, left, top, right, bottom); 589 if (!mAddedGroups.isEmpty()) { 590 getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 591 @Override 592 public boolean onPreDraw() { 593 for (MessagingGroup group : mAddedGroups) { 594 if (!group.isShown()) { 595 continue; 596 } 597 MessagingPropertyAnimator.fadeIn(group.getAvatar()); 598 MessagingPropertyAnimator.fadeIn(group.getSenderView()); 599 MessagingPropertyAnimator.startLocalTranslationFrom(group, 600 group.getHeight(), LINEAR_OUT_SLOW_IN); 601 } 602 mAddedGroups.clear(); 603 getViewTreeObserver().removeOnPreDrawListener(this); 604 return true; 605 } 606 }); 607 } 608 } 609 getMessagingLinearLayout()610 public MessagingLinearLayout getMessagingLinearLayout() { 611 return mMessagingLinearLayout; 612 } 613 614 @Nullable getImageMessageContainer()615 public ViewGroup getImageMessageContainer() { 616 return mImageMessageContainer; 617 } 618 getMessagingGroups()619 public ArrayList<MessagingGroup> getMessagingGroups() { 620 return mGroups; 621 } 622 623 @Override setMessagingClippingDisabled(boolean clippingDisabled)624 public void setMessagingClippingDisabled(boolean clippingDisabled) { 625 mMessagingLinearLayout.setClipBounds(clippingDisabled ? null : mMessagingClipRect); 626 } 627 } 628