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