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.systemui.statusbar.notification; 18 19 import android.content.res.Resources; 20 import android.text.Layout; 21 import android.util.Pools; 22 import android.view.View; 23 import android.view.ViewGroup; 24 import android.widget.TextView; 25 26 import com.android.app.animation.Interpolators; 27 import com.android.internal.widget.IMessagingLayout; 28 import com.android.internal.widget.MessagingGroup; 29 import com.android.internal.widget.MessagingImageMessage; 30 import com.android.internal.widget.MessagingLinearLayout; 31 import com.android.internal.widget.MessagingMessage; 32 import com.android.internal.widget.MessagingPropertyAnimator; 33 34 import java.util.ArrayList; 35 import java.util.HashMap; 36 import java.util.List; 37 38 /** 39 * A transform state of the action list 40 */ 41 public class MessagingLayoutTransformState extends TransformState { 42 43 private static Pools.SimplePool<MessagingLayoutTransformState> sInstancePool 44 = new Pools.SimplePool<>(40); 45 private MessagingLinearLayout mMessageContainer; 46 private IMessagingLayout mMessagingLayout; 47 private HashMap<MessagingGroup, MessagingGroup> mGroupMap = new HashMap<>(); 48 private float mRelativeTranslationOffset; 49 obtain()50 public static MessagingLayoutTransformState obtain() { 51 MessagingLayoutTransformState instance = sInstancePool.acquire(); 52 if (instance != null) { 53 return instance; 54 } 55 return new MessagingLayoutTransformState(); 56 } 57 58 @Override initFrom(View view, TransformInfo transformInfo)59 public void initFrom(View view, TransformInfo transformInfo) { 60 super.initFrom(view, transformInfo); 61 if (mTransformedView instanceof MessagingLinearLayout) { 62 mMessageContainer = (MessagingLinearLayout) mTransformedView; 63 mMessagingLayout = mMessageContainer.getMessagingLayout(); 64 Resources resources = view.getContext().getResources(); 65 mRelativeTranslationOffset = resources.getDisplayMetrics().density * 8; 66 } 67 } 68 69 @Override transformViewTo(TransformState otherState, float transformationAmount)70 public boolean transformViewTo(TransformState otherState, float transformationAmount) { 71 if (otherState instanceof MessagingLayoutTransformState) { 72 // It's a party! Let's transform between these two layouts! 73 transformViewInternal((MessagingLayoutTransformState) otherState, transformationAmount, 74 true /* to */); 75 return true; 76 } else { 77 return super.transformViewTo(otherState, transformationAmount); 78 } 79 } 80 81 @Override transformViewFrom(TransformState otherState, float transformationAmount)82 public void transformViewFrom(TransformState otherState, float transformationAmount) { 83 if (otherState instanceof MessagingLayoutTransformState) { 84 // It's a party! Let's transform between these two layouts! 85 transformViewInternal((MessagingLayoutTransformState) otherState, transformationAmount, 86 false /* to */); 87 } else { 88 super.transformViewFrom(otherState, transformationAmount); 89 } 90 } 91 transformViewInternal(MessagingLayoutTransformState mlt, float transformationAmount, boolean to)92 private void transformViewInternal(MessagingLayoutTransformState mlt, 93 float transformationAmount, boolean to) { 94 ensureVisible(); 95 ArrayList<MessagingGroup> ownGroups = filterHiddenGroups( 96 mMessagingLayout.getMessagingGroups()); 97 ArrayList<MessagingGroup> otherGroups = filterHiddenGroups( 98 mlt.mMessagingLayout.getMessagingGroups()); 99 HashMap<MessagingGroup, MessagingGroup> pairs = findPairs(ownGroups, otherGroups); 100 MessagingGroup lastPairedGroup = null; 101 float currentTranslation = 0; 102 for (int i = ownGroups.size() - 1; i >= 0; i--) { 103 MessagingGroup ownGroup = ownGroups.get(i); 104 MessagingGroup matchingGroup = pairs.get(ownGroup); 105 if (!isGone(ownGroup)) { 106 if (matchingGroup != null) { 107 int totalTranslation = transformGroups(ownGroup, matchingGroup, 108 transformationAmount, to); 109 if (lastPairedGroup == null) { 110 lastPairedGroup = ownGroup; 111 if (to){ 112 currentTranslation = matchingGroup.getAvatar().getTranslationY() 113 - totalTranslation; 114 } else { 115 currentTranslation = ownGroup.getAvatar().getTranslationY(); 116 } 117 } 118 } else { 119 float groupTransformationAmount = transformationAmount; 120 if (lastPairedGroup != null) { 121 adaptGroupAppear(ownGroup, transformationAmount, currentTranslation, 122 to); 123 float newPosition = ownGroup.getTop() + currentTranslation; 124 125 if (!mTransformInfo.isAnimating()) { 126 // We fade the group away as soon as 1/2 of it is translated away on top 127 float fadeStart = -ownGroup.getHeight() * 0.5f; 128 groupTransformationAmount = (newPosition - fadeStart) 129 / Math.abs(fadeStart); 130 } else { 131 float fadeStart = -ownGroup.getHeight() * 0.75f; 132 // We want to fade out as soon as the animation starts, let's add the 133 // complete top in addition 134 groupTransformationAmount = (newPosition - fadeStart) 135 / (Math.abs(fadeStart) + ownGroup.getTop()); 136 } 137 groupTransformationAmount = Math.max(0.0f, Math.min(1.0f, 138 groupTransformationAmount)); 139 if (to) { 140 groupTransformationAmount = 1.0f - groupTransformationAmount; 141 } 142 } 143 if (to) { 144 disappear(ownGroup, groupTransformationAmount); 145 } else { 146 appear(ownGroup, groupTransformationAmount); 147 } 148 } 149 } 150 } 151 } 152 appear(MessagingGroup ownGroup, float transformationAmount)153 private void appear(MessagingGroup ownGroup, float transformationAmount) { 154 MessagingLinearLayout ownMessages = ownGroup.getMessageContainer(); 155 for (int j = 0; j < ownMessages.getChildCount(); j++) { 156 View child = ownMessages.getChildAt(j); 157 if (isGone(child)) { 158 continue; 159 } 160 appear(child, transformationAmount); 161 setClippingDeactivated(child, true); 162 } 163 appear(ownGroup.getAvatar(), transformationAmount); 164 appear(ownGroup.getSenderView(), transformationAmount); 165 appear(ownGroup.getIsolatedMessage(), transformationAmount); 166 setClippingDeactivated(ownGroup.getSenderView(), true); 167 setClippingDeactivated(ownGroup.getAvatar(), true); 168 } 169 adaptGroupAppear(MessagingGroup ownGroup, float transformationAmount, float overallTranslation, boolean to)170 private void adaptGroupAppear(MessagingGroup ownGroup, float transformationAmount, 171 float overallTranslation, boolean to) { 172 float relativeOffset; 173 if (to) { 174 relativeOffset = transformationAmount * mRelativeTranslationOffset; 175 } else { 176 relativeOffset = (1.0f - transformationAmount) * mRelativeTranslationOffset; 177 } 178 if (ownGroup.getSenderView().getVisibility() != View.GONE) { 179 relativeOffset *= 0.5f; 180 } 181 ownGroup.getMessageContainer().setTranslationY(relativeOffset); 182 ownGroup.getSenderView().setTranslationY(relativeOffset); 183 ownGroup.setTranslationY(overallTranslation * 0.9f); 184 } 185 disappear(MessagingGroup ownGroup, float transformationAmount)186 private void disappear(MessagingGroup ownGroup, float transformationAmount) { 187 MessagingLinearLayout ownMessages = ownGroup.getMessageContainer(); 188 for (int j = 0; j < ownMessages.getChildCount(); j++) { 189 View child = ownMessages.getChildAt(j); 190 if (isGone(child)) { 191 continue; 192 } 193 disappear(child, transformationAmount); 194 setClippingDeactivated(child, true); 195 } 196 disappear(ownGroup.getAvatar(), transformationAmount); 197 disappear(ownGroup.getSenderView(), transformationAmount); 198 disappear(ownGroup.getIsolatedMessage(), transformationAmount); 199 setClippingDeactivated(ownGroup.getSenderView(), true); 200 setClippingDeactivated(ownGroup.getAvatar(), true); 201 } 202 appear(View child, float transformationAmount)203 private void appear(View child, float transformationAmount) { 204 if (child == null || child.getVisibility() == View.GONE) { 205 return; 206 } 207 TransformState ownState = TransformState.createFrom(child, mTransformInfo); 208 ownState.appear(transformationAmount, null); 209 ownState.recycle(); 210 } 211 disappear(View child, float transformationAmount)212 private void disappear(View child, float transformationAmount) { 213 if (child == null || child.getVisibility() == View.GONE) { 214 return; 215 } 216 TransformState ownState = TransformState.createFrom(child, mTransformInfo); 217 ownState.disappear(transformationAmount, null); 218 ownState.recycle(); 219 } 220 filterHiddenGroups( ArrayList<MessagingGroup> groups)221 private ArrayList<MessagingGroup> filterHiddenGroups( 222 ArrayList<MessagingGroup> groups) { 223 ArrayList<MessagingGroup> result = new ArrayList<>(groups); 224 for (int i = 0; i < result.size(); i++) { 225 MessagingGroup messagingGroup = result.get(i); 226 if (isGone(messagingGroup)) { 227 result.remove(i); 228 i--; 229 } 230 } 231 return result; 232 } 233 hasEllipses(TextView textView)234 private boolean hasEllipses(TextView textView) { 235 Layout layout = textView.getLayout(); 236 return layout != null && layout.getEllipsisCount(layout.getLineCount() - 1) > 0; 237 } 238 needsReflow(TextView own, TextView other)239 private boolean needsReflow(TextView own, TextView other) { 240 return hasEllipses(own) != hasEllipses(other); 241 } 242 243 /** 244 * Transform two groups towards each other. 245 * 246 * @return the total transformation distance that the group goes through 247 */ transformGroups(MessagingGroup ownGroup, MessagingGroup otherGroup, float transformationAmount, boolean to)248 private int transformGroups(MessagingGroup ownGroup, MessagingGroup otherGroup, 249 float transformationAmount, boolean to) { 250 boolean useLinearTransformation = 251 otherGroup.getIsolatedMessage() == null && !mTransformInfo.isAnimating(); 252 TextView ownSenderView = ownGroup.getSenderView(); 253 TextView otherSenderView = otherGroup.getSenderView(); 254 transformView(transformationAmount, to, ownSenderView, otherSenderView, 255 // Normally this would be handled by the TextViewMessageState#sameAs check, but in 256 // this case it doesn't work because our text won't match, due to the appended colon 257 // in the collapsed view. 258 !needsReflow(ownSenderView, otherSenderView), 259 useLinearTransformation); 260 int totalAvatarTranslation = transformView(transformationAmount, to, ownGroup.getAvatar(), 261 otherGroup.getAvatar(), true /* sameAsAny */, useLinearTransformation); 262 List<MessagingMessage> ownMessages = ownGroup.getMessages(); 263 List<MessagingMessage> otherMessages = otherGroup.getMessages(); 264 float previousTranslation = 0; 265 boolean isLastView = true; 266 for (int i = 0; i < ownMessages.size(); i++) { 267 View child = ownMessages.get(ownMessages.size() - 1 - i).getView(); 268 if (isGone(child)) { 269 continue; 270 } 271 float messageAmount = transformationAmount; 272 int otherIndex = otherMessages.size() - 1 - i; 273 View otherChild = null; 274 if (otherIndex >= 0) { 275 otherChild = otherMessages.get(otherIndex).getView(); 276 if (isGone(otherChild)) { 277 otherChild = null; 278 } 279 } 280 if (otherChild == null && previousTranslation < 0) { 281 // Let's fade out as we approach the top of the screen. We can only do this if 282 // we're actually moving up 283 float distanceToTop = child.getTop() + child.getHeight() + previousTranslation; 284 messageAmount = distanceToTop / child.getHeight(); 285 messageAmount = Math.max(0.0f, Math.min(1.0f, messageAmount)); 286 if (to) { 287 messageAmount = 1.0f - messageAmount; 288 } 289 } 290 int totalTranslation = transformView(messageAmount, to, child, otherChild, 291 false /* sameAsAny */, useLinearTransformation); 292 boolean otherIsIsolated = otherGroup.getIsolatedMessage() == otherChild; 293 if (messageAmount == 0.0f 294 && (otherIsIsolated || otherGroup.isSingleLine())) { 295 ownGroup.setClippingDisabled(true); 296 mMessagingLayout.setMessagingClippingDisabled(true); 297 } 298 if (otherChild == null) { 299 if (isLastView) { 300 previousTranslation = ownSenderView.getTranslationY(); 301 } 302 child.setTranslationY(previousTranslation); 303 setClippingDeactivated(child, true); 304 } else if (ownGroup.getIsolatedMessage() == child || otherIsIsolated) { 305 // We don't want to add any translation for the image that is transforming 306 } else if (to) { 307 previousTranslation = otherChild.getTranslationY() - totalTranslation; 308 } else { 309 previousTranslation = child.getTranslationY(); 310 } 311 isLastView = false; 312 } 313 ownGroup.updateClipRect(); 314 return totalAvatarTranslation; 315 } 316 317 /** 318 * Transform a view to another view. 319 * 320 * @return the total translationY this view goes through 321 */ transformView(float transformationAmount, boolean to, View ownView, View otherView, boolean sameAsAny, boolean useLinearTransformation)322 private int transformView(float transformationAmount, boolean to, View ownView, 323 View otherView, boolean sameAsAny, boolean useLinearTransformation) { 324 TransformState ownState = TransformState.createFrom(ownView, mTransformInfo); 325 if (useLinearTransformation) { 326 ownState.setDefaultInterpolator(Interpolators.LINEAR); 327 } 328 ownState.setIsSameAsAnyView(sameAsAny && !isGone(otherView)); 329 int totalTranslationDistance = 0; 330 if (to) { 331 if (otherView != null) { 332 TransformState otherState = TransformState.createFrom(otherView, mTransformInfo); 333 if (!isGone(otherView)) { 334 ownState.transformViewTo(otherState, transformationAmount); 335 } else { 336 if (!isGone(ownView)) { 337 ownState.disappear(transformationAmount, null); 338 } 339 // We still want to transform vertically if the view is gone, 340 // since avatars serve as anchors for the rest of the layout transition 341 ownState.transformViewVerticalTo(otherState, transformationAmount); 342 } 343 totalTranslationDistance = ownState.getLaidOutLocationOnScreen()[1] 344 - otherState.getLaidOutLocationOnScreen()[1]; 345 otherState.recycle(); 346 } else { 347 ownState.disappear(transformationAmount, null); 348 } 349 } else { 350 if (otherView != null) { 351 TransformState otherState = TransformState.createFrom(otherView, mTransformInfo); 352 if (!isGone(otherView)) { 353 ownState.transformViewFrom(otherState, transformationAmount); 354 } else { 355 if (!isGone(ownView)) { 356 ownState.appear(transformationAmount, null); 357 } 358 // We still want to transform vertically if the view is gone, 359 // since avatars serve as anchors for the rest of the layout transition 360 ownState.transformViewVerticalFrom(otherState, transformationAmount); 361 } 362 totalTranslationDistance = ownState.getLaidOutLocationOnScreen()[1] 363 - otherState.getLaidOutLocationOnScreen()[1]; 364 otherState.recycle(); 365 } else { 366 ownState.appear(transformationAmount, null); 367 } 368 } 369 ownState.recycle(); 370 return totalTranslationDistance; 371 } 372 findPairs(ArrayList<MessagingGroup> ownGroups, ArrayList<MessagingGroup> otherGroups)373 private HashMap<MessagingGroup, MessagingGroup> findPairs(ArrayList<MessagingGroup> ownGroups, 374 ArrayList<MessagingGroup> otherGroups) { 375 mGroupMap.clear(); 376 int lastMatch = Integer.MAX_VALUE; 377 for (int i = ownGroups.size() - 1; i >= 0; i--) { 378 MessagingGroup ownGroup = ownGroups.get(i); 379 MessagingGroup bestMatch = null; 380 int bestCompatibility = 0; 381 for (int j = Math.min(otherGroups.size(), lastMatch) - 1; j >= 0; j--) { 382 MessagingGroup otherGroup = otherGroups.get(j); 383 int compatibility = ownGroup.calculateGroupCompatibility(otherGroup); 384 if (compatibility > bestCompatibility) { 385 bestCompatibility = compatibility; 386 bestMatch = otherGroup; 387 lastMatch = j; 388 } 389 } 390 if (bestMatch != null) { 391 mGroupMap.put(ownGroup, bestMatch); 392 } 393 } 394 return mGroupMap; 395 } 396 isGone(View view)397 private boolean isGone(View view) { 398 if (view == null) { 399 return true; 400 } 401 if (view.getVisibility() == View.GONE) { 402 return true; 403 } 404 if (view.getParent() == null) { 405 return true; 406 } 407 if (view.getWidth() == 0) { 408 return true; 409 } 410 final ViewGroup.LayoutParams lp = view.getLayoutParams(); 411 if (lp instanceof MessagingLinearLayout.LayoutParams 412 && ((MessagingLinearLayout.LayoutParams) lp).hide) { 413 return true; 414 } 415 return false; 416 } 417 418 @Override setVisible(boolean visible, boolean force)419 public void setVisible(boolean visible, boolean force) { 420 super.setVisible(visible, force); 421 resetTransformedView(); 422 ArrayList<MessagingGroup> ownGroups = mMessagingLayout.getMessagingGroups(); 423 for (int i = 0; i < ownGroups.size(); i++) { 424 MessagingGroup ownGroup = ownGroups.get(i); 425 if (!isGone(ownGroup)) { 426 MessagingLinearLayout ownMessages = ownGroup.getMessageContainer(); 427 for (int j = 0; j < ownMessages.getChildCount(); j++) { 428 View child = ownMessages.getChildAt(j); 429 setVisible(child, visible, force); 430 } 431 setVisible(ownGroup.getAvatar(), visible, force); 432 setVisible(ownGroup.getSenderView(), visible, force); 433 MessagingImageMessage isolatedMessage = ownGroup.getIsolatedMessage(); 434 if (isolatedMessage != null) { 435 setVisible(isolatedMessage, visible, force); 436 } 437 } 438 } 439 } 440 setVisible(View child, boolean visible, boolean force)441 private void setVisible(View child, boolean visible, boolean force) { 442 if (isGone(child) || MessagingPropertyAnimator.isAnimatingAlpha(child)) { 443 return; 444 } 445 TransformState ownState = TransformState.createFrom(child, mTransformInfo); 446 ownState.setVisible(visible, force); 447 ownState.recycle(); 448 } 449 450 @Override resetTransformedView()451 protected void resetTransformedView() { 452 super.resetTransformedView(); 453 ArrayList<MessagingGroup> ownGroups = mMessagingLayout.getMessagingGroups(); 454 for (int i = 0; i < ownGroups.size(); i++) { 455 MessagingGroup ownGroup = ownGroups.get(i); 456 if (!isGone(ownGroup)) { 457 MessagingLinearLayout ownMessages = ownGroup.getMessageContainer(); 458 for (int j = 0; j < ownMessages.getChildCount(); j++) { 459 View child = ownMessages.getChildAt(j); 460 if (isGone(child)) { 461 continue; 462 } 463 resetTransformedView(child); 464 setClippingDeactivated(child, false); 465 } 466 resetTransformedView(ownGroup.getAvatar()); 467 resetTransformedView(ownGroup.getSenderView()); 468 MessagingImageMessage isolatedMessage = ownGroup.getIsolatedMessage(); 469 if (isolatedMessage != null) { 470 resetTransformedView(isolatedMessage); 471 } 472 setClippingDeactivated(ownGroup.getAvatar(), false); 473 setClippingDeactivated(ownGroup.getSenderView(), false); 474 ownGroup.setTranslationY(0); 475 ownGroup.getMessageContainer().setTranslationY(0); 476 ownGroup.getSenderView().setTranslationY(0); 477 } 478 ownGroup.setClippingDisabled(false); 479 ownGroup.updateClipRect(); 480 } 481 mMessagingLayout.setMessagingClippingDisabled(false); 482 } 483 484 @Override prepareFadeIn()485 public void prepareFadeIn() { 486 super.prepareFadeIn(); 487 setVisible(true /* visible */, false /* force */); 488 } 489 resetTransformedView(View child)490 private void resetTransformedView(View child) { 491 TransformState ownState = TransformState.createFrom(child, mTransformInfo); 492 ownState.resetTransformedView(); 493 ownState.recycle(); 494 } 495 496 @Override reset()497 protected void reset() { 498 super.reset(); 499 mMessageContainer = null; 500 mMessagingLayout = null; 501 } 502 503 @Override recycle()504 public void recycle() { 505 super.recycle(); 506 mGroupMap.clear();; 507 sInstancePool.release(this); 508 } 509 } 510