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