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 */
16package com.android.internal.widget;
17
18import android.content.Context;
19import android.graphics.drawable.Drawable;
20import android.graphics.Rect;
21import android.util.AttributeSet;
22import android.view.Gravity;
23import android.view.View;
24import android.view.ViewTreeObserver;
25import android.widget.ListView;
26import android.widget.FrameLayout;
27
28import java.util.ArrayList;
29
30
31/**
32 * Layout for the decor for ListViews on watch-type devices with small screens.
33 * <p>
34 * Supports one panel with the gravity set to top, and one panel with gravity set to bottom.
35 * <p>
36 * Use with one ListView child. The top and bottom panels will track the ListView's scrolling.
37 * If there is no ListView child, it will act like a normal FrameLayout.
38 */
39public class WatchListDecorLayout extends FrameLayout
40        implements ViewTreeObserver.OnScrollChangedListener {
41
42    private int mForegroundPaddingLeft = 0;
43    private int mForegroundPaddingTop = 0;
44    private int mForegroundPaddingRight = 0;
45    private int mForegroundPaddingBottom = 0;
46
47    private final ArrayList<View> mMatchParentChildren = new ArrayList<>(1);
48
49    /** Track the amount the ListView has to scroll up to account for padding change difference. */
50    private int mPendingScroll;
51    private View mBottomPanel;
52    private View mTopPanel;
53    private ListView mListView;
54    private ViewTreeObserver mObserver;
55
56
57    public WatchListDecorLayout(Context context, AttributeSet attrs) {
58        super(context, attrs);
59    }
60
61    public WatchListDecorLayout(Context context, AttributeSet attrs, int defStyleAttr) {
62        super(context, attrs, defStyleAttr);
63    }
64
65    public WatchListDecorLayout(
66            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
67        super(context, attrs, defStyleAttr, defStyleRes);
68    }
69
70    @Override
71    protected void onAttachedToWindow() {
72        super.onAttachedToWindow();
73
74        mPendingScroll = 0;
75
76        for (int i = 0; i < getChildCount(); ++i) {
77            View child = getChildAt(i);
78            if (child instanceof ListView) {
79                if (mListView != null) {
80                    throw new IllegalArgumentException("only one ListView child allowed");
81                }
82                mListView = (ListView) child;
83
84                mListView.setNestedScrollingEnabled(true);
85                mObserver = mListView.getViewTreeObserver();
86                mObserver.addOnScrollChangedListener(this);
87            } else {
88                int gravity = (((LayoutParams) child.getLayoutParams()).gravity
89                        & Gravity.VERTICAL_GRAVITY_MASK);
90                if (gravity == Gravity.TOP && mTopPanel == null) {
91                    mTopPanel = child;
92                } else if (gravity == Gravity.BOTTOM && mBottomPanel == null) {
93                    mBottomPanel = child;
94                }
95            }
96        }
97    }
98
99    @Override
100    public void onDetachedFromWindow() {
101        mListView = null;
102        mBottomPanel = null;
103        mTopPanel = null;
104        if (mObserver != null) {
105            if (mObserver.isAlive()) {
106                mObserver.removeOnScrollChangedListener(this);
107            }
108            mObserver = null;
109        }
110    }
111
112    private void applyMeasureToChild(View child, int widthMeasureSpec, int heightMeasureSpec) {
113        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
114
115        final int childWidthMeasureSpec;
116        if (lp.width == LayoutParams.MATCH_PARENT) {
117            final int width = Math.max(0, getMeasuredWidth()
118                    - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
119                    - lp.leftMargin - lp.rightMargin);
120            childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
121                    width, MeasureSpec.EXACTLY);
122        } else {
123            childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
124                    getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
125                    lp.leftMargin + lp.rightMargin,
126                    lp.width);
127        }
128
129        final int childHeightMeasureSpec;
130        if (lp.height == LayoutParams.MATCH_PARENT) {
131            final int height = Math.max(0, getMeasuredHeight()
132                    - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
133                    - lp.topMargin - lp.bottomMargin);
134            childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
135                    height, MeasureSpec.EXACTLY);
136        } else {
137            childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
138                    getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
139                    lp.topMargin + lp.bottomMargin,
140                    lp.height);
141        }
142
143        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
144    }
145
146    private int measureAndGetHeight(View child, int widthMeasureSpec, int heightMeasureSpec) {
147        if (child != null) {
148            if (child.getVisibility() != GONE) {
149                applyMeasureToChild(mBottomPanel, widthMeasureSpec, heightMeasureSpec);
150                return child.getMeasuredHeight();
151            } else if (getMeasureAllChildren()) {
152                applyMeasureToChild(mBottomPanel, widthMeasureSpec, heightMeasureSpec);
153            }
154        }
155        return 0;
156    }
157
158    @Override
159    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
160        int count = getChildCount();
161
162        final boolean measureMatchParentChildren =
163                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
164                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
165        mMatchParentChildren.clear();
166
167        int maxHeight = 0;
168        int maxWidth = 0;
169        int childState = 0;
170
171        for (int i = 0; i < count; i++) {
172            final View child = getChildAt(i);
173            if (getMeasureAllChildren() || child.getVisibility() != GONE) {
174                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
175                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
176                maxWidth = Math.max(maxWidth,
177                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
178                maxHeight = Math.max(maxHeight,
179                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
180                childState = combineMeasuredStates(childState, child.getMeasuredState());
181                if (measureMatchParentChildren) {
182                    if (lp.width == LayoutParams.MATCH_PARENT ||
183                            lp.height == LayoutParams.MATCH_PARENT) {
184                        mMatchParentChildren.add(child);
185                    }
186                }
187            }
188        }
189
190        // Account for padding too
191        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
192        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
193
194        // Check against our minimum height and width
195        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
196        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
197
198        // Check against our foreground's minimum height and width
199        final Drawable drawable = getForeground();
200        if (drawable != null) {
201            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
202            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
203        }
204
205        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
206                resolveSizeAndState(maxHeight, heightMeasureSpec,
207                        childState << MEASURED_HEIGHT_STATE_SHIFT));
208
209        if (mListView != null) {
210            if (mPendingScroll != 0) {
211                mListView.scrollListBy(mPendingScroll);
212                mPendingScroll = 0;
213            }
214
215            int paddingTop = Math.max(mListView.getPaddingTop(),
216                    measureAndGetHeight(mTopPanel, widthMeasureSpec, heightMeasureSpec));
217            int paddingBottom = Math.max(mListView.getPaddingBottom(),
218                    measureAndGetHeight(mBottomPanel, widthMeasureSpec, heightMeasureSpec));
219
220            if (paddingTop != mListView.getPaddingTop()
221                    || paddingBottom != mListView.getPaddingBottom()) {
222                mPendingScroll += mListView.getPaddingTop() - paddingTop;
223                mListView.setPadding(
224                        mListView.getPaddingLeft(), paddingTop,
225                        mListView.getPaddingRight(), paddingBottom);
226            }
227        }
228
229        count = mMatchParentChildren.size();
230        if (count > 1) {
231            for (int i = 0; i < count; i++) {
232                final View child = mMatchParentChildren.get(i);
233                if (mListView == null || (child != mTopPanel && child != mBottomPanel)) {
234                    applyMeasureToChild(child, widthMeasureSpec, heightMeasureSpec);
235                }
236            }
237        }
238    }
239
240    @Override
241    public void setForegroundGravity(int foregroundGravity) {
242        if (getForegroundGravity() != foregroundGravity) {
243            super.setForegroundGravity(foregroundGravity);
244
245            // calling get* again here because the set above may apply default constraints
246            final Drawable foreground = getForeground();
247            if (getForegroundGravity() == Gravity.FILL && foreground != null) {
248                Rect padding = new Rect();
249                if (foreground.getPadding(padding)) {
250                    mForegroundPaddingLeft = padding.left;
251                    mForegroundPaddingTop = padding.top;
252                    mForegroundPaddingRight = padding.right;
253                    mForegroundPaddingBottom = padding.bottom;
254                }
255            } else {
256                mForegroundPaddingLeft = 0;
257                mForegroundPaddingTop = 0;
258                mForegroundPaddingRight = 0;
259                mForegroundPaddingBottom = 0;
260            }
261        }
262    }
263
264    private int getPaddingLeftWithForeground() {
265        return isForegroundInsidePadding() ? Math.max(mPaddingLeft, mForegroundPaddingLeft) :
266            mPaddingLeft + mForegroundPaddingLeft;
267    }
268
269    private int getPaddingRightWithForeground() {
270        return isForegroundInsidePadding() ? Math.max(mPaddingRight, mForegroundPaddingRight) :
271            mPaddingRight + mForegroundPaddingRight;
272    }
273
274    private int getPaddingTopWithForeground() {
275        return isForegroundInsidePadding() ? Math.max(mPaddingTop, mForegroundPaddingTop) :
276            mPaddingTop + mForegroundPaddingTop;
277    }
278
279    private int getPaddingBottomWithForeground() {
280        return isForegroundInsidePadding() ? Math.max(mPaddingBottom, mForegroundPaddingBottom) :
281            mPaddingBottom + mForegroundPaddingBottom;
282    }
283
284    @Override
285    public void onScrollChanged() {
286        if (mListView == null) {
287            return;
288        }
289
290        if (mTopPanel != null) {
291            if (mListView.getChildCount() > 0) {
292                if (mListView.getFirstVisiblePosition() == 0) {
293                    View firstChild = mListView.getChildAt(0);
294                    setScrolling(mTopPanel,
295                            firstChild.getY() - mTopPanel.getHeight() - mTopPanel.getTop());
296                } else {
297                    // shift to hide the frame, last child is not the last position
298                    setScrolling(mTopPanel, -mTopPanel.getHeight());
299                }
300            } else {
301                setScrolling(mTopPanel, 0); // no visible child, fallback to default behaviour
302            }
303        }
304
305        if (mBottomPanel != null) {
306            if (mListView.getChildCount() > 0) {
307                if (mListView.getLastVisiblePosition() >= mListView.getCount() - 1) {
308                    View lastChild = mListView.getChildAt(mListView.getChildCount() - 1);
309                    setScrolling(mBottomPanel,
310                            lastChild.getY() + lastChild.getHeight() - mBottomPanel.getTop());
311                } else {
312                    // shift to hide the frame, last child is not the last position
313                    setScrolling(mBottomPanel, mBottomPanel.getHeight());
314                }
315            } else {
316                setScrolling(mBottomPanel, 0); // no visible child, fallback to default behaviour
317            }
318        }
319    }
320
321    /** Only set scrolling for the panel if there is a change in its translationY. */
322    private void setScrolling(View panel, float translationY) {
323        if (panel.getTranslationY() != translationY) {
324            panel.setTranslationY(translationY);
325        }
326    }
327}
328