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