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
17package com.android.internal.widget;
18
19import android.annotation.Nullable;
20import android.content.Context;
21import android.content.res.TypedArray;
22import android.graphics.Canvas;
23import android.util.AttributeSet;
24import android.view.RemotableViewMethod;
25import android.view.View;
26import android.view.ViewGroup;
27import android.widget.RemoteViews;
28
29import com.android.internal.R;
30
31/**
32 * A custom-built layout for the Notification.MessagingStyle.
33 *
34 * Evicts children until they all fit.
35 */
36@RemoteViews.RemoteView
37public class MessagingLinearLayout extends ViewGroup {
38
39    private static final int NOT_MEASURED_BEFORE = -1;
40    /**
41     * Spacing to be applied between views.
42     */
43    private int mSpacing;
44
45    /**
46     * The maximum height allowed.
47     */
48    private int mMaxHeight;
49
50    private int mIndentLines;
51
52    /**
53     * Id of the child that's also visible in the contracted layout.
54     */
55    private int mContractedChildId;
56    /**
57     * The last measured with in a layout pass if it was measured before or
58     * {@link #NOT_MEASURED_BEFORE} if this is the first layout pass.
59     */
60    private int mLastMeasuredWidth = NOT_MEASURED_BEFORE;
61
62    public MessagingLinearLayout(Context context, @Nullable AttributeSet attrs) {
63        super(context, attrs);
64
65        final TypedArray a = context.obtainStyledAttributes(attrs,
66                R.styleable.MessagingLinearLayout, 0,
67                0);
68
69        final int N = a.getIndexCount();
70        for (int i = 0; i < N; i++) {
71            int attr = a.getIndex(i);
72            switch (attr) {
73                case R.styleable.MessagingLinearLayout_spacing:
74                    mSpacing = a.getDimensionPixelSize(i, 0);
75                    break;
76            }
77        }
78
79        a.recycle();
80    }
81
82
83    @Override
84    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
85        // This is essentially a bottom-up linear layout that only adds children that fit entirely
86        // up to a maximum height.
87        int targetHeight = MeasureSpec.getSize(heightMeasureSpec);
88        switch (MeasureSpec.getMode(heightMeasureSpec)) {
89            case MeasureSpec.UNSPECIFIED:
90                targetHeight = Integer.MAX_VALUE;
91                break;
92        }
93        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
94        boolean recalculateVisibility = mLastMeasuredWidth == NOT_MEASURED_BEFORE
95                || getMeasuredHeight() != targetHeight
96                || mLastMeasuredWidth != widthSize;
97
98        final int count = getChildCount();
99        if (recalculateVisibility) {
100            // We only need to recalculate the view visibilities if the view wasn't measured already
101            // in this pass, otherwise we may drop messages here already since we are measured
102            // exactly with what we returned before, which was optimized already with the
103            // line-indents.
104            for (int i = 0; i < count; ++i) {
105                final View child = getChildAt(i);
106                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
107                lp.hide = true;
108            }
109
110            int totalHeight = mPaddingTop + mPaddingBottom;
111            boolean first = true;
112
113            // Starting from the bottom: we measure every view as if it were the only one. If it still
114
115            // fits, we take it, otherwise we stop there.
116            for (int i = count - 1; i >= 0 && totalHeight < targetHeight; i--) {
117                if (getChildAt(i).getVisibility() == GONE) {
118                    continue;
119                }
120                final View child = getChildAt(i);
121                LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
122                ImageFloatingTextView textChild = null;
123                if (child instanceof ImageFloatingTextView) {
124                    // Pretend we need the image padding for all views, we don't know which
125                    // one will end up needing to do this (might end up not using all the space,
126                    // but calculating this exactly would be more expensive).
127                    textChild = (ImageFloatingTextView) child;
128                    textChild.setNumIndentLines(mIndentLines == 2 ? 3 : mIndentLines);
129                }
130
131                int spacing = first ? 0 : mSpacing;
132                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, totalHeight
133                        - mPaddingTop - mPaddingBottom + spacing);
134
135                final int childHeight = child.getMeasuredHeight();
136                int newHeight = Math.max(totalHeight, totalHeight + childHeight + lp.topMargin +
137                        lp.bottomMargin + spacing);
138                first = false;
139                boolean measuredTooSmall = false;
140                if (textChild != null) {
141                    measuredTooSmall = childHeight < textChild.getLayoutHeight()
142                            + textChild.getPaddingTop() + textChild.getPaddingBottom();
143                }
144
145                if (newHeight <= targetHeight && !measuredTooSmall) {
146                    totalHeight = newHeight;
147                    lp.hide = false;
148                } else {
149                    break;
150                }
151            }
152        }
153
154        // Now that we know which views to take, fix up the indents and see what width we get.
155        int measuredWidth = mPaddingLeft + mPaddingRight;
156        int imageLines = mIndentLines;
157        // Need to redo the height because it may change due to changing indents.
158        int totalHeight = mPaddingTop + mPaddingBottom;
159        boolean first = true;
160        for (int i = 0; i < count; i++) {
161            final View child = getChildAt(i);
162            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
163
164            if (child.getVisibility() == GONE || lp.hide) {
165                continue;
166            }
167
168            if (child instanceof ImageFloatingTextView) {
169                ImageFloatingTextView textChild = (ImageFloatingTextView) child;
170                if (imageLines == 2 && textChild.getLineCount() > 2) {
171                    // HACK: If we need indent for two lines, and they're coming from the same
172                    // view, we need extra spacing to compensate for the lack of margins,
173                    // so add an extra line of indent.
174                    imageLines = 3;
175                }
176                boolean changed = textChild.setNumIndentLines(Math.max(0, imageLines));
177                if (changed || !recalculateVisibility) {
178                    final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
179                            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin,
180                            lp.width);
181                    // we want to measure it at most as high as it is currently, otherwise we'll
182                    // drop later lines
183                    final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
184                            targetHeight - child.getMeasuredHeight(), lp.height);
185
186                    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);;
187                }
188                imageLines -= textChild.getLineCount();
189            }
190
191            measuredWidth = Math.max(measuredWidth,
192                    child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin
193                            + mPaddingLeft + mPaddingRight);
194            totalHeight = Math.max(totalHeight, totalHeight + child.getMeasuredHeight() +
195                    lp.topMargin + lp.bottomMargin + (first ? 0 : mSpacing));
196            first = false;
197        }
198
199
200        setMeasuredDimension(
201                resolveSize(Math.max(getSuggestedMinimumWidth(), measuredWidth),
202                        widthMeasureSpec),
203                resolveSize(Math.max(getSuggestedMinimumHeight(), totalHeight),
204                        heightMeasureSpec));
205        mLastMeasuredWidth = widthSize;
206    }
207
208    @Override
209    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
210        final int paddingLeft = mPaddingLeft;
211
212        int childTop;
213
214        // Where right end of child should go
215        final int width = right - left;
216        final int childRight = width - mPaddingRight;
217
218        final int layoutDirection = getLayoutDirection();
219        final int count = getChildCount();
220
221        childTop = mPaddingTop;
222
223        boolean first = true;
224
225        for (int i = 0; i < count; i++) {
226            final View child = getChildAt(i);
227            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
228
229            if (child.getVisibility() == GONE || lp.hide) {
230                continue;
231            }
232
233            final int childWidth = child.getMeasuredWidth();
234            final int childHeight = child.getMeasuredHeight();
235
236            int childLeft;
237            if (layoutDirection == LAYOUT_DIRECTION_RTL) {
238                childLeft = childRight - childWidth - lp.rightMargin;
239            } else {
240                childLeft = paddingLeft + lp.leftMargin;
241            }
242
243            if (!first) {
244                childTop += mSpacing;
245            }
246
247            childTop += lp.topMargin;
248            child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
249
250            childTop += childHeight + lp.bottomMargin;
251
252            first = false;
253        }
254        mLastMeasuredWidth = NOT_MEASURED_BEFORE;
255    }
256
257    @Override
258    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
259        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
260        if (lp.hide) {
261            return true;
262        }
263        return super.drawChild(canvas, child, drawingTime);
264    }
265
266    @Override
267    public LayoutParams generateLayoutParams(AttributeSet attrs) {
268        return new LayoutParams(mContext, attrs);
269    }
270
271    @Override
272    protected LayoutParams generateDefaultLayoutParams() {
273        return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
274
275    }
276
277    @Override
278    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
279        LayoutParams copy = new LayoutParams(lp.width, lp.height);
280        if (lp instanceof MarginLayoutParams) {
281            copy.copyMarginsFrom((MarginLayoutParams) lp);
282        }
283        return copy;
284    }
285
286    /**
287     * Sets how many lines should be indented to avoid a floating image.
288     */
289    @RemotableViewMethod
290    public void setNumIndentLines(int numberLines) {
291        mIndentLines = numberLines;
292    }
293
294    /**
295     * Set id of the child that's also visible in the contracted layout.
296     */
297    @RemotableViewMethod
298    public void setContractedChildId(int contractedChildId) {
299        mContractedChildId = contractedChildId;
300    }
301
302    /**
303     * Get id of the child that's also visible in the contracted layout.
304     */
305    public int getContractedChildId() {
306        return mContractedChildId;
307    }
308
309    public static class LayoutParams extends MarginLayoutParams {
310
311        boolean hide = false;
312
313        public LayoutParams(Context c, AttributeSet attrs) {
314            super(c, attrs);
315        }
316
317        public LayoutParams(int width, int height) {
318            super(width, height);
319        }
320    }
321}
322