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