1 /* 2 * Copyright (C) 2017 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 package androidx.leanback.widget; 17 18 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; 19 20 import android.content.Context; 21 import android.graphics.Bitmap; 22 import android.util.AttributeSet; 23 import android.util.SparseArray; 24 import android.view.View; 25 import android.view.ViewGroup; 26 import android.widget.ImageView; 27 import android.widget.LinearLayout; 28 29 import androidx.annotation.RestrictTo; 30 import androidx.leanback.R; 31 32 /** 33 * @hide 34 */ 35 @RestrictTo(LIBRARY_GROUP) 36 public class ThumbsBar extends LinearLayout { 37 38 // initial value for Thumb's number before measuring the screen size 39 int mNumOfThumbs = -1; 40 int mThumbWidthInPixel; 41 int mThumbHeightInPixel; 42 int mHeroThumbWidthInPixel; 43 int mHeroThumbHeightInPixel; 44 int mMeasuredMarginInPixel; 45 final SparseArray<Bitmap> mBitmaps = new SparseArray<>(); 46 47 // flag to determine if the number of thumbs in thumbs bar is set by user through 48 // setNumberofThumbs API or auto-calculated according to android tv design spec. 49 private boolean mIsUserSets = false; 50 ThumbsBar(Context context, AttributeSet attrs)51 public ThumbsBar(Context context, AttributeSet attrs) { 52 this(context, attrs, 0); 53 } 54 ThumbsBar(Context context, AttributeSet attrs, int defStyle)55 public ThumbsBar(Context context, AttributeSet attrs, int defStyle) { 56 super(context, attrs, defStyle); 57 // According to the spec, 58 // the width of non-hero thumb should be 80% of HeroThumb's Width, i.e. 0.8 * 192dp = 154dp 59 mThumbWidthInPixel = context.getResources().getDimensionPixelSize( 60 R.dimen.lb_playback_transport_thumbs_width); 61 mThumbHeightInPixel = context.getResources().getDimensionPixelSize( 62 R.dimen.lb_playback_transport_thumbs_height); 63 // According to the spec, the width of HeroThumb should be 192dp 64 mHeroThumbHeightInPixel = context.getResources().getDimensionPixelSize( 65 R.dimen.lb_playback_transport_hero_thumbs_width); 66 mHeroThumbWidthInPixel = context.getResources().getDimensionPixelSize( 67 R.dimen.lb_playback_transport_hero_thumbs_height); 68 // According to the spec, the margin between thumbs to be 4dp 69 mMeasuredMarginInPixel = context.getResources().getDimensionPixelSize( 70 R.dimen.lb_playback_transport_thumbs_margin); 71 } 72 73 /** 74 * Get hero index which is the middle child. 75 */ getHeroIndex()76 public int getHeroIndex() { 77 return getChildCount() / 2; 78 } 79 80 /** 81 * Set size of thumb view in pixels 82 */ setThumbSize(int width, int height)83 public void setThumbSize(int width, int height) { 84 mThumbHeightInPixel = height; 85 mThumbWidthInPixel = width; 86 int heroIndex = getHeroIndex(); 87 for (int i = 0; i < getChildCount(); i++) { 88 if (heroIndex != i) { 89 View child = getChildAt(i); 90 LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); 91 boolean changed = false; 92 if (lp.height != height) { 93 lp.height = height; 94 changed = true; 95 } 96 if (lp.width != width) { 97 lp.width = width; 98 changed = true; 99 } 100 if (changed) { 101 child.setLayoutParams(lp); 102 } 103 } 104 } 105 } 106 107 /** 108 * Set size of hero thumb view in pixels, it is usually larger than other thumbs. 109 */ setHeroThumbSize(int width, int height)110 public void setHeroThumbSize(int width, int height) { 111 mHeroThumbHeightInPixel = height; 112 mHeroThumbWidthInPixel = width; 113 int heroIndex = getHeroIndex(); 114 for (int i = 0; i < getChildCount(); i++) { 115 if (heroIndex == i) { 116 View child = getChildAt(i); 117 LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); 118 boolean changed = false; 119 if (lp.height != height) { 120 lp.height = height; 121 changed = true; 122 } 123 if (lp.width != width) { 124 lp.width = width; 125 changed = true; 126 } 127 if (changed) { 128 child.setLayoutParams(lp); 129 } 130 } 131 } 132 } 133 134 /** 135 * Set the space between thumbs in pixels 136 */ setThumbSpace(int spaceInPixel)137 public void setThumbSpace(int spaceInPixel) { 138 mMeasuredMarginInPixel = spaceInPixel; 139 requestLayout(); 140 } 141 142 /** 143 * Set number of thumb views. 144 */ setNumberOfThumbs(int numOfThumbs)145 public void setNumberOfThumbs(int numOfThumbs) { 146 mIsUserSets = true; 147 mNumOfThumbs = numOfThumbs; 148 setNumberOfThumbsInternal(); 149 } 150 151 /** 152 * Helper function for setNumberOfThumbs. 153 * Will Update the layout settings in ThumbsBar based on mNumOfThumbs 154 */ setNumberOfThumbsInternal()155 private void setNumberOfThumbsInternal() { 156 while (getChildCount() > mNumOfThumbs) { 157 removeView(getChildAt(getChildCount() - 1)); 158 } 159 while (getChildCount() < mNumOfThumbs) { 160 View view = createThumbView(this); 161 LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(mThumbWidthInPixel, 162 mThumbHeightInPixel); 163 addView(view, lp); 164 } 165 int heroIndex = getHeroIndex(); 166 for (int i = 0; i < getChildCount(); i++) { 167 View child = getChildAt(i); 168 LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); 169 if (heroIndex == i) { 170 lp.width = mHeroThumbWidthInPixel; 171 lp.height = mHeroThumbHeightInPixel; 172 } else { 173 lp.width = mThumbWidthInPixel; 174 lp.height = mThumbHeightInPixel; 175 } 176 child.setLayoutParams(lp); 177 } 178 } 179 roundUp(int num, int divisor)180 private static int roundUp(int num, int divisor) { 181 return (num + divisor - 1) / divisor; 182 } 183 184 /** 185 * Helper function to compute how many thumbs should be put in the screen 186 * Assume we should put x's non-hero thumbs in the screen, the equation should be 187 * 192dp (width of hero thumbs) + 188 * 154dp (width of common thumbs) * x + 189 * 4dp (width of the margin between thumbs) * x 190 * = width 191 * So the calculated number of non-hero thumbs should be (width - 192dp) / 158dp. 192 * If the calculated number of non-hero thumbs is less than 2, it will be updated to 2 193 * or if the calculated number or non-hero thumbs is not an even number, it will be 194 * decremented by one. 195 * This processing is used to make sure the arrangement of non-hero thumbs 196 * in ThumbsBar is symmetrical. 197 * Also there should be a hero thumb in the middle of the ThumbsBar, 198 * the final result should be non-hero thumbs (after processing) + 1. 199 * 200 * @param widthInPixel measured width in pixel 201 * @return The number of thumbs 202 */ calculateNumOfThumbs(int widthInPixel)203 private int calculateNumOfThumbs(int widthInPixel) { 204 int nonHeroThumbNum = roundUp(widthInPixel - mHeroThumbWidthInPixel, 205 mThumbWidthInPixel + mMeasuredMarginInPixel); 206 if (nonHeroThumbNum < 2) { 207 // If the calculated number of non-hero thumbs is less than 2, 208 // it will be updated to 2 209 nonHeroThumbNum = 2; 210 } else if ((nonHeroThumbNum & 1) != 0) { 211 // If the calculated number or non-hero thumbs is not an even number, 212 // it will be increased by one. 213 nonHeroThumbNum++; 214 } 215 // Count Hero Thumb to the final result 216 return nonHeroThumbNum + 1; 217 } 218 219 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)220 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 221 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 222 int width = getMeasuredWidth(); 223 // If the number of thumbs in ThumbsBar is not set by user explicitly, it will be 224 // recalculated based on Android TV Design Spec 225 if (!mIsUserSets) { 226 int numOfThumbs = calculateNumOfThumbs(width); 227 // Set new number of thumbs when calculation result is different with current number 228 if (mNumOfThumbs != numOfThumbs) { 229 mNumOfThumbs = numOfThumbs; 230 setNumberOfThumbsInternal(); 231 } 232 } 233 } 234 235 @Override onLayout(boolean changed, int l, int t, int r, int b)236 protected void onLayout(boolean changed, int l, int t, int r, int b) { 237 super.onLayout(changed, l, t, r, b); 238 int heroIndex = getHeroIndex(); 239 View heroView = getChildAt(heroIndex); 240 int heroLeft = getWidth() / 2 - heroView.getMeasuredWidth() / 2; 241 int heroRight = getWidth() / 2 + heroView.getMeasuredWidth() / 2; 242 heroView.layout(heroLeft, getPaddingTop(), heroRight, 243 getPaddingTop() + heroView.getMeasuredHeight()); 244 int heroCenter = getPaddingTop() + heroView.getMeasuredHeight() / 2; 245 246 for (int i = heroIndex - 1; i >= 0; i--) { 247 heroLeft -= mMeasuredMarginInPixel; 248 View child = getChildAt(i); 249 child.layout(heroLeft - child.getMeasuredWidth(), 250 heroCenter - child.getMeasuredHeight() / 2, 251 heroLeft, 252 heroCenter + child.getMeasuredHeight() / 2); 253 heroLeft -= child.getMeasuredWidth(); 254 } 255 for (int i = heroIndex + 1; i < mNumOfThumbs; i++) { 256 heroRight += mMeasuredMarginInPixel; 257 View child = getChildAt(i); 258 child.layout(heroRight, 259 heroCenter - child.getMeasuredHeight() / 2, 260 heroRight + child.getMeasuredWidth(), 261 heroCenter + child.getMeasuredHeight() / 2); 262 heroRight += child.getMeasuredWidth(); 263 } 264 } 265 266 /** 267 * Create a thumb view, it's by default a ImageView. 268 */ createThumbView(ViewGroup parent)269 protected View createThumbView(ViewGroup parent) { 270 return new ImageView(parent.getContext()); 271 } 272 273 /** 274 * Clear all thumb bitmaps set on thumb views. 275 */ clearThumbBitmaps()276 public void clearThumbBitmaps() { 277 for (int i = 0; i < getChildCount(); i++) { 278 setThumbBitmap(i, null); 279 } 280 mBitmaps.clear(); 281 } 282 283 284 /** 285 * Get bitmap of given child index. 286 */ getThumbBitmap(int index)287 public Bitmap getThumbBitmap(int index) { 288 return mBitmaps.get(index); 289 } 290 291 /** 292 * Set thumb bitmap for a given index of child. 293 */ setThumbBitmap(int index, Bitmap bitmap)294 public void setThumbBitmap(int index, Bitmap bitmap) { 295 mBitmaps.put(index, bitmap); 296 ((ImageView) getChildAt(index)).setImageBitmap(bitmap); 297 } 298 } 299