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