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