/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.internal.widget; import android.content.Context; import android.graphics.drawable.Drawable; import android.graphics.Rect; import android.util.AttributeSet; import android.view.Gravity; import android.view.View; import android.view.ViewTreeObserver; import android.widget.ListView; import android.widget.FrameLayout; import java.util.ArrayList; /** * Layout for the decor for ListViews on watch-type devices with small screens. * <p> * Supports one panel with the gravity set to top, and one panel with gravity set to bottom. * <p> * Use with one ListView child. The top and bottom panels will track the ListView's scrolling. * If there is no ListView child, it will act like a normal FrameLayout. */ public class WatchListDecorLayout extends FrameLayout implements ViewTreeObserver.OnScrollChangedListener { private int mForegroundPaddingLeft = 0; private int mForegroundPaddingTop = 0; private int mForegroundPaddingRight = 0; private int mForegroundPaddingBottom = 0; private final ArrayList<View> mMatchParentChildren = new ArrayList<>(1); /** Track the amount the ListView has to scroll up to account for padding change difference. */ private int mPendingScroll; private View mBottomPanel; private View mTopPanel; private ListView mListView; private ViewTreeObserver mObserver; public WatchListDecorLayout(Context context, AttributeSet attrs) { super(context, attrs); } public WatchListDecorLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public WatchListDecorLayout( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); mPendingScroll = 0; for (int i = 0; i < getChildCount(); ++i) { View child = getChildAt(i); if (child instanceof ListView) { if (mListView != null) { throw new IllegalArgumentException("only one ListView child allowed"); } mListView = (ListView) child; mListView.setNestedScrollingEnabled(true); mObserver = mListView.getViewTreeObserver(); mObserver.addOnScrollChangedListener(this); } else { int gravity = (((LayoutParams) child.getLayoutParams()).gravity & Gravity.VERTICAL_GRAVITY_MASK); if (gravity == Gravity.TOP && mTopPanel == null) { mTopPanel = child; } else if (gravity == Gravity.BOTTOM && mBottomPanel == null) { mBottomPanel = child; } } } } @Override public void onDetachedFromWindow() { mListView = null; mBottomPanel = null; mTopPanel = null; if (mObserver != null) { if (mObserver.isAlive()) { mObserver.removeOnScrollChangedListener(this); } mObserver = null; } } private void applyMeasureToChild(View child, int widthMeasureSpec, int heightMeasureSpec) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); final int childWidthMeasureSpec; if (lp.width == LayoutParams.MATCH_PARENT) { final int width = Math.max(0, getMeasuredWidth() - getPaddingLeftWithForeground() - getPaddingRightWithForeground() - lp.leftMargin - lp.rightMargin); childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( width, MeasureSpec.EXACTLY); } else { childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, getPaddingLeftWithForeground() + getPaddingRightWithForeground() + lp.leftMargin + lp.rightMargin, lp.width); } final int childHeightMeasureSpec; if (lp.height == LayoutParams.MATCH_PARENT) { final int height = Math.max(0, getMeasuredHeight() - getPaddingTopWithForeground() - getPaddingBottomWithForeground() - lp.topMargin - lp.bottomMargin); childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( height, MeasureSpec.EXACTLY); } else { childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTopWithForeground() + getPaddingBottomWithForeground() + lp.topMargin + lp.bottomMargin, lp.height); } child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } private int measureAndGetHeight(View child, int widthMeasureSpec, int heightMeasureSpec) { if (child != null) { if (child.getVisibility() != GONE) { applyMeasureToChild(mBottomPanel, widthMeasureSpec, heightMeasureSpec); return child.getMeasuredHeight(); } else if (getMeasureAllChildren()) { applyMeasureToChild(mBottomPanel, widthMeasureSpec, heightMeasureSpec); } } return 0; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int count = getChildCount(); final boolean measureMatchParentChildren = MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY || MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY; mMatchParentChildren.clear(); int maxHeight = 0; int maxWidth = 0; int childState = 0; for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (getMeasureAllChildren() || child.getVisibility() != GONE) { measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); childState = combineMeasuredStates(childState, child.getMeasuredState()); if (measureMatchParentChildren) { if (lp.width == LayoutParams.MATCH_PARENT || lp.height == LayoutParams.MATCH_PARENT) { mMatchParentChildren.add(child); } } } } // Account for padding too maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground(); maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground(); // Check against our minimum height and width maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); // Check against our foreground's minimum height and width final Drawable drawable = getForeground(); if (drawable != null) { maxHeight = Math.max(maxHeight, drawable.getMinimumHeight()); maxWidth = Math.max(maxWidth, drawable.getMinimumWidth()); } setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT)); if (mListView != null) { if (mPendingScroll != 0) { mListView.scrollListBy(mPendingScroll); mPendingScroll = 0; } int paddingTop = Math.max(mListView.getPaddingTop(), measureAndGetHeight(mTopPanel, widthMeasureSpec, heightMeasureSpec)); int paddingBottom = Math.max(mListView.getPaddingBottom(), measureAndGetHeight(mBottomPanel, widthMeasureSpec, heightMeasureSpec)); if (paddingTop != mListView.getPaddingTop() || paddingBottom != mListView.getPaddingBottom()) { mPendingScroll += mListView.getPaddingTop() - paddingTop; mListView.setPadding( mListView.getPaddingLeft(), paddingTop, mListView.getPaddingRight(), paddingBottom); } } count = mMatchParentChildren.size(); if (count > 1) { for (int i = 0; i < count; i++) { final View child = mMatchParentChildren.get(i); if (mListView == null || (child != mTopPanel && child != mBottomPanel)) { applyMeasureToChild(child, widthMeasureSpec, heightMeasureSpec); } } } } @Override public void setForegroundGravity(int foregroundGravity) { if (getForegroundGravity() != foregroundGravity) { super.setForegroundGravity(foregroundGravity); // calling get* again here because the set above may apply default constraints final Drawable foreground = getForeground(); if (getForegroundGravity() == Gravity.FILL && foreground != null) { Rect padding = new Rect(); if (foreground.getPadding(padding)) { mForegroundPaddingLeft = padding.left; mForegroundPaddingTop = padding.top; mForegroundPaddingRight = padding.right; mForegroundPaddingBottom = padding.bottom; } } else { mForegroundPaddingLeft = 0; mForegroundPaddingTop = 0; mForegroundPaddingRight = 0; mForegroundPaddingBottom = 0; } } } private int getPaddingLeftWithForeground() { return isForegroundInsidePadding() ? Math.max(mPaddingLeft, mForegroundPaddingLeft) : mPaddingLeft + mForegroundPaddingLeft; } private int getPaddingRightWithForeground() { return isForegroundInsidePadding() ? Math.max(mPaddingRight, mForegroundPaddingRight) : mPaddingRight + mForegroundPaddingRight; } private int getPaddingTopWithForeground() { return isForegroundInsidePadding() ? Math.max(mPaddingTop, mForegroundPaddingTop) : mPaddingTop + mForegroundPaddingTop; } private int getPaddingBottomWithForeground() { return isForegroundInsidePadding() ? Math.max(mPaddingBottom, mForegroundPaddingBottom) : mPaddingBottom + mForegroundPaddingBottom; } @Override public void onScrollChanged() { if (mListView == null) { return; } if (mTopPanel != null) { if (mListView.getChildCount() > 0) { if (mListView.getFirstVisiblePosition() == 0) { View firstChild = mListView.getChildAt(0); setScrolling(mTopPanel, firstChild.getY() - mTopPanel.getHeight() - mTopPanel.getTop()); } else { // shift to hide the frame, last child is not the last position setScrolling(mTopPanel, -mTopPanel.getHeight()); } } else { setScrolling(mTopPanel, 0); // no visible child, fallback to default behaviour } } if (mBottomPanel != null) { if (mListView.getChildCount() > 0) { if (mListView.getLastVisiblePosition() >= mListView.getCount() - 1) { View lastChild = mListView.getChildAt(mListView.getChildCount() - 1); setScrolling(mBottomPanel, lastChild.getY() + lastChild.getHeight() - mBottomPanel.getTop()); } else { // shift to hide the frame, last child is not the last position setScrolling(mBottomPanel, mBottomPanel.getHeight()); } } else { setScrolling(mBottomPanel, 0); // no visible child, fallback to default behaviour } } } /** Only set scrolling for the panel if there is a change in its translationY. */ private void setScrolling(View panel, float translationY) { if (panel.getTranslationY() != translationY) { panel.setTranslationY(translationY); } } }