1 /*
2  * Copyright (C) 2016 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 android.widget.flags.Flags.messagingChildRequestLayout;
20 
21 import android.annotation.Nullable;
22 import android.annotation.Px;
23 import android.content.Context;
24 import android.content.res.TypedArray;
25 import android.graphics.Canvas;
26 import android.os.Build;
27 import android.os.Trace;
28 import android.util.AttributeSet;
29 import android.view.RemotableViewMethod;
30 import android.view.View;
31 import android.view.ViewGroup;
32 import android.view.ViewParent;
33 import android.widget.RemoteViews;
34 
35 import com.android.internal.R;
36 
37 /**
38  * A custom-built layout for the Notification.MessagingStyle.
39  *
40  * Evicts children until they all fit.
41  */
42 @RemoteViews.RemoteView
43 public class MessagingLinearLayout extends ViewGroup {
44 
45     /**
46      * Spacing to be applied between views.
47      */
48     private int mSpacing;
49 
50     private int mMaxDisplayedLines = Integer.MAX_VALUE;
51 
52     private static final boolean TRACE_ONMEASURE = Build.isDebuggable();
53 
MessagingLinearLayout(Context context, @Nullable AttributeSet attrs)54     public MessagingLinearLayout(Context context, @Nullable AttributeSet attrs) {
55         super(context, attrs);
56 
57         final TypedArray a = context.obtainStyledAttributes(attrs,
58                 R.styleable.MessagingLinearLayout, 0,
59                 0);
60 
61         final int N = a.getIndexCount();
62         for (int i = 0; i < N; i++) {
63             int attr = a.getIndex(i);
64             switch (attr) {
65                 case R.styleable.MessagingLinearLayout_spacing:
66                     mSpacing = a.getDimensionPixelSize(i, 0);
67                     break;
68             }
69         }
70 
71         a.recycle();
72     }
73 
74     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)75     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
76         if (TRACE_ONMEASURE) {
77             Trace.beginSection("MessagingLinearLayout#onMeasure");
78             trackMeasureSpecs(widthMeasureSpec, heightMeasureSpec);
79         }
80         // This is essentially a bottom-up linear layout that only adds children that fit entirely
81         // up to a maximum height.
82         int targetHeight = MeasureSpec.getSize(heightMeasureSpec);
83         switch (MeasureSpec.getMode(heightMeasureSpec)) {
84             case MeasureSpec.UNSPECIFIED:
85                 targetHeight = Integer.MAX_VALUE;
86                 break;
87         }
88 
89         // Now that we know which views to take, fix up the indents and see what width we get.
90         int measuredWidth = mPaddingLeft + mPaddingRight;
91         final int count = getChildCount();
92         int totalHeight;
93         for (int i = 0; i < count; ++i) {
94             final View child = getChildAt(i);
95             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
96             lp.hide = true;
97             // Child always needs to be measured to calculate hide property correctly in onMeasure.
98             if (messagingChildRequestLayout()) {
99                 child.requestLayout();
100             }
101             if (child instanceof MessagingChild) {
102                 MessagingChild messagingChild = (MessagingChild) child;
103                 // Whenever we encounter the message first, it's always first in the layout
104                 messagingChild.setIsFirstInLayout(true);
105             }
106         }
107 
108         totalHeight = mPaddingTop + mPaddingBottom;
109         boolean first = true;
110         int linesRemaining = mMaxDisplayedLines;
111         // Starting from the bottom: we measure every view as if it were the only one. If it still
112         // fits, we take it, otherwise we stop there.
113         MessagingChild previousChild = null;
114         View previousView = null;
115         int previousChildHeight = 0;
116         int previousTotalHeight = 0;
117         int previousLinesConsumed = 0;
118         for (int i = count - 1; i >= 0 && totalHeight < targetHeight; i--) {
119             if (getChildAt(i).getVisibility() == GONE) {
120                 continue;
121             }
122             final View child = getChildAt(i);
123             LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
124             MessagingChild messagingChild = null;
125             int spacing = mSpacing;
126             int previousChildIncrease = 0;
127             if (child instanceof MessagingChild) {
128                 // We need to remeasure the previous child again if it's not the first anymore
129                 if (previousChild != null && previousChild.hasDifferentHeightWhenFirst()) {
130                     previousChild.setIsFirstInLayout(false);
131                     measureChildWithMargins(previousView, widthMeasureSpec, 0, heightMeasureSpec,
132                             previousTotalHeight - previousChildHeight);
133                     previousChildIncrease = previousView.getMeasuredHeight() - previousChildHeight;
134                     linesRemaining -= previousChild.getConsumedLines() - previousLinesConsumed;
135                 }
136                 messagingChild = (MessagingChild) child;
137                 messagingChild.setMaxDisplayedLines(linesRemaining);
138                 spacing += messagingChild.getExtraSpacing();
139             }
140             spacing = first ? 0 : spacing;
141             measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, totalHeight
142                     - mPaddingTop - mPaddingBottom + spacing);
143 
144             final int childHeight = child.getMeasuredHeight();
145             int newHeight = Math.max(totalHeight, totalHeight + childHeight + lp.topMargin +
146                     lp.bottomMargin + spacing + previousChildIncrease);
147             int measureType = MessagingChild.MEASURED_NORMAL;
148             if (messagingChild != null) {
149                 measureType = messagingChild.getMeasuredType();
150             }
151 
152             // We never measure the first item as too small, we want to at least show something.
153             boolean isTooSmall = measureType == MessagingChild.MEASURED_TOO_SMALL && !first;
154             boolean isShortened = measureType == MessagingChild.MEASURED_SHORTENED
155                     || measureType == MessagingChild.MEASURED_TOO_SMALL && first;
156             boolean showView = newHeight <= targetHeight && !isTooSmall;
157             if (showView) {
158                 if (messagingChild != null) {
159                     previousLinesConsumed = messagingChild.getConsumedLines();
160                     linesRemaining -= previousLinesConsumed;
161                     previousChild = messagingChild;
162                     previousView = child;
163                     previousChildHeight = childHeight;
164                     previousTotalHeight = totalHeight;
165                 }
166                 totalHeight = newHeight;
167                 measuredWidth = Math.max(measuredWidth,
168                         child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin
169                                 + mPaddingLeft + mPaddingRight);
170                 lp.hide = false;
171                 if (isShortened || linesRemaining <= 0) {
172                     break;
173                 }
174             } else {
175                 // We now became too short, let's make sure to reset any previous views to be first
176                 // and remeasure it.
177                 if (previousChild != null && previousChild.hasDifferentHeightWhenFirst()) {
178                     previousChild.setIsFirstInLayout(true);
179                     // We need to remeasure the previous child again since it became first
180                     measureChildWithMargins(previousView, widthMeasureSpec, 0, heightMeasureSpec,
181                             previousTotalHeight - previousChildHeight);
182                     // The totalHeight is already correct here since we only set it during the
183                     // first pass
184                 }
185                 break;
186             }
187             first = false;
188         }
189 
190         setMeasuredDimension(
191                 resolveSize(Math.max(getSuggestedMinimumWidth(), measuredWidth),
192                         widthMeasureSpec),
193                 Math.max(getSuggestedMinimumHeight(), totalHeight));
194         if (TRACE_ONMEASURE) {
195             Trace.endSection();
196         }
197     }
198 
199     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)200     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
201         final int paddingLeft = mPaddingLeft;
202 
203         int childTop;
204 
205         // Where right end of child should go
206         final int width = right - left;
207         final int childRight = width - mPaddingRight;
208 
209         final int layoutDirection = getLayoutDirection();
210         final int count = getChildCount();
211 
212         childTop = mPaddingTop;
213 
214         boolean first = true;
215         final boolean shown = isShown();
216         for (int i = 0; i < count; i++) {
217             final View child = getChildAt(i);
218             if (child.getVisibility() == GONE) {
219                 continue;
220             }
221             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
222             MessagingChild messagingChild = (MessagingChild) child;
223 
224             final int childWidth = child.getMeasuredWidth();
225             final int childHeight = child.getMeasuredHeight();
226 
227             int childLeft;
228             if (layoutDirection == LAYOUT_DIRECTION_RTL) {
229                 childLeft = childRight - childWidth - lp.rightMargin;
230             } else {
231                 childLeft = paddingLeft + lp.leftMargin;
232             }
233             if (lp.hide) {
234                 if (shown && lp.visibleBefore) {
235                     // We still want to lay out the child to have great animations
236                     child.layout(childLeft, childTop, childLeft + childWidth,
237                             childTop + lp.lastVisibleHeight);
238                     messagingChild.hideAnimated();
239                 }
240                 lp.visibleBefore = false;
241                 continue;
242             } else {
243                 lp.visibleBefore = true;
244                 lp.lastVisibleHeight = childHeight;
245             }
246 
247             if (!first) {
248                 childTop += mSpacing;
249             }
250 
251             childTop += lp.topMargin;
252             child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
253 
254             childTop += childHeight + lp.bottomMargin;
255 
256             first = false;
257         }
258     }
259 
trackMeasureSpecs(int widthMeasureSpec, int heightMeasureSpec)260     private void trackMeasureSpecs(int widthMeasureSpec, int heightMeasureSpec) {
261         if (!TRACE_ONMEASURE) {
262             return;
263         }
264 
265         final int availableWidth = MeasureSpec.getSize(widthMeasureSpec);
266         final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
267         final int availableHeight = MeasureSpec.getSize(heightMeasureSpec);
268         final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
269         Trace.setCounter("MessagingLinearLayout#onMeasure_widthMeasureSpecSize",
270                 availableWidth);
271         Trace.setCounter("MessagingLinearLayout#onMeasure_widthMeasureSpecMode",
272                 widthMode);
273         Trace.setCounter("MessagingLinearLayout#onMeasure_heightMeasureSpecSize",
274                 availableHeight);
275         Trace.setCounter("MessagingLinearLayout#onMeasure_heightMeasureSpecMode",
276                 heightMode);
277     }
278 
279     @Override
drawChild(Canvas canvas, View child, long drawingTime)280     protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
281         final LayoutParams lp = (LayoutParams) child.getLayoutParams();
282         if (lp.hide) {
283             MessagingChild messagingChild = (MessagingChild) child;
284             if (!messagingChild.isHidingAnimated()) {
285                 return true;
286             }
287         }
288         return super.drawChild(canvas, child, drawingTime);
289     }
290 
291     /**
292      * Set the spacing to be applied between views.
293      */
setSpacing(@x int spacing)294     public void setSpacing(@Px int spacing) {
295         if (mSpacing != spacing) {
296             mSpacing = spacing;
297             requestLayout();
298         }
299     }
300 
301     @Override
generateLayoutParams(AttributeSet attrs)302     public LayoutParams generateLayoutParams(AttributeSet attrs) {
303         return new LayoutParams(mContext, attrs);
304     }
305 
306     @Override
generateDefaultLayoutParams()307     protected LayoutParams generateDefaultLayoutParams() {
308         return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
309 
310     }
311 
312     @Override
generateLayoutParams(ViewGroup.LayoutParams lp)313     protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
314         LayoutParams copy = new LayoutParams(lp.width, lp.height);
315         if (lp instanceof MarginLayoutParams) {
316             copy.copyMarginsFrom((MarginLayoutParams) lp);
317         }
318         return copy;
319     }
320 
isGone(View view)321     public static boolean isGone(View view) {
322         if (view.getVisibility() == View.GONE) {
323             return true;
324         }
325         final ViewGroup.LayoutParams lp = view.getLayoutParams();
326         if (lp instanceof MessagingLinearLayout.LayoutParams
327                 && ((MessagingLinearLayout.LayoutParams) lp).hide) {
328             return true;
329         }
330         return false;
331     }
332 
333     /**
334      * Sets how many lines should be displayed at most
335      */
336     @RemotableViewMethod
setMaxDisplayedLines(int numberLines)337     public void setMaxDisplayedLines(int numberLines) {
338         mMaxDisplayedLines = numberLines;
339     }
340 
getMessagingLayout()341     public IMessagingLayout getMessagingLayout() {
342         View view = this;
343         while (true) {
344             ViewParent p = view.getParent();
345             if (p instanceof View) {
346                 view = (View) p;
347                 if (view instanceof IMessagingLayout) {
348                     return (IMessagingLayout) view;
349                 }
350             } else {
351                 return null;
352             }
353         }
354     }
355 
356     @Override
getBaseline()357     public int getBaseline() {
358         // When placed in a horizontal linear layout (as is the case in a single-line MessageGroup),
359         // align with the last visible child (which is the one that will be displayed in the single-
360         // line group.
361         int childCount = getChildCount();
362         for (int i = childCount - 1; i >= 0; i--) {
363             final View child = getChildAt(i);
364             if (isGone(child)) {
365                 continue;
366             }
367             final int childBaseline = child.getBaseline();
368             if (childBaseline == -1) {
369                 return -1;
370             }
371             MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
372             return lp.topMargin + childBaseline;
373         }
374         return super.getBaseline();
375     }
376 
377     public interface MessagingChild {
378         int MEASURED_NORMAL = 0;
379         int MEASURED_SHORTENED = 1;
380         int MEASURED_TOO_SMALL = 2;
381 
getMeasuredType()382         int getMeasuredType();
getConsumedLines()383         int getConsumedLines();
setMaxDisplayedLines(int lines)384         void setMaxDisplayedLines(int lines);
hideAnimated()385         void hideAnimated();
isHidingAnimated()386         boolean isHidingAnimated();
387 
388         /**
389          * Set that this view is first in layout. Relevant and only set if
390          * {@link #hasDifferentHeightWhenFirst()}.
391          * @param first is this first?
392          */
setIsFirstInLayout(boolean first)393         default void setIsFirstInLayout(boolean first) {}
394 
395         /**
396          * @return if this layout has different height it is first in the layout
397          */
hasDifferentHeightWhenFirst()398         default boolean hasDifferentHeightWhenFirst() {
399             return false;
400         }
getExtraSpacing()401         default int getExtraSpacing() {
402             return 0;
403         }
recycle()404         void recycle();
405     }
406 
407     public static class LayoutParams extends MarginLayoutParams {
408 
409         public boolean hide = false;
410         public boolean visibleBefore = false;
411         public int lastVisibleHeight;
412 
LayoutParams(Context c, AttributeSet attrs)413         public LayoutParams(Context c, AttributeSet attrs) {
414             super(c, attrs);
415         }
416 
LayoutParams(int width, int height)417         public LayoutParams(int width, int height) {
418             super(width, height);
419         }
420     }
421 }
422