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