1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5  * in compliance with the License. You may obtain a copy of the License at
6  *
7  * http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the License
10  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11  * or implied. See the License for the specific language governing permissions and limitations under
12  * the License.
13  */
14 package androidx.leanback.widget;
15 
16 import static androidx.leanback.widget.FocusHighlight.ZOOM_FACTOR_LARGE;
17 import static androidx.leanback.widget.FocusHighlight.ZOOM_FACTOR_MEDIUM;
18 import static androidx.leanback.widget.FocusHighlight.ZOOM_FACTOR_NONE;
19 import static androidx.leanback.widget.FocusHighlight.ZOOM_FACTOR_SMALL;
20 import static androidx.leanback.widget.FocusHighlight.ZOOM_FACTOR_XSMALL;
21 
22 import android.animation.TimeAnimator;
23 import android.content.res.Resources;
24 import android.util.TypedValue;
25 import android.view.View;
26 import android.view.ViewParent;
27 import android.view.animation.AccelerateDecelerateInterpolator;
28 import android.view.animation.Interpolator;
29 
30 import androidx.leanback.R;
31 import androidx.leanback.app.HeadersFragment;
32 import androidx.leanback.graphics.ColorOverlayDimmer;
33 import androidx.recyclerview.widget.RecyclerView;
34 
35 /**
36  * Sets up the highlighting behavior when an item gains focus.
37  */
38 public class FocusHighlightHelper {
39 
isValidZoomIndex(int zoomIndex)40     static boolean isValidZoomIndex(int zoomIndex) {
41         return zoomIndex == ZOOM_FACTOR_NONE || getResId(zoomIndex) > 0;
42     }
43 
getResId(int zoomIndex)44     static int getResId(int zoomIndex) {
45         switch (zoomIndex) {
46             case ZOOM_FACTOR_SMALL:
47                 return R.fraction.lb_focus_zoom_factor_small;
48             case ZOOM_FACTOR_XSMALL:
49                 return R.fraction.lb_focus_zoom_factor_xsmall;
50             case ZOOM_FACTOR_MEDIUM:
51                 return R.fraction.lb_focus_zoom_factor_medium;
52             case ZOOM_FACTOR_LARGE:
53                 return R.fraction.lb_focus_zoom_factor_large;
54             default:
55                 return 0;
56         }
57     }
58 
59 
60     static class FocusAnimator implements TimeAnimator.TimeListener {
61         private final View mView;
62         private final int mDuration;
63         private final ShadowOverlayContainer mWrapper;
64         private final float mScaleDiff;
65         private float mFocusLevel = 0f;
66         private float mFocusLevelStart;
67         private float mFocusLevelDelta;
68         private final TimeAnimator mAnimator = new TimeAnimator();
69         private final Interpolator mInterpolator = new AccelerateDecelerateInterpolator();
70         private final ColorOverlayDimmer mDimmer;
71 
animateFocus(boolean select, boolean immediate)72         void animateFocus(boolean select, boolean immediate) {
73             endAnimation();
74             final float end = select ? 1 : 0;
75             if (immediate) {
76                 setFocusLevel(end);
77             } else if (mFocusLevel != end) {
78                 mFocusLevelStart = mFocusLevel;
79                 mFocusLevelDelta = end - mFocusLevelStart;
80                 mAnimator.start();
81             }
82         }
83 
FocusAnimator(View view, float scale, boolean useDimmer, int duration)84         FocusAnimator(View view, float scale, boolean useDimmer, int duration) {
85             mView = view;
86             mDuration = duration;
87             mScaleDiff = scale - 1f;
88             if (view instanceof ShadowOverlayContainer) {
89                 mWrapper = (ShadowOverlayContainer) view;
90             } else {
91                 mWrapper = null;
92             }
93             mAnimator.setTimeListener(this);
94             if (useDimmer) {
95                 mDimmer = ColorOverlayDimmer.createDefault(view.getContext());
96             } else {
97                 mDimmer = null;
98             }
99         }
100 
setFocusLevel(float level)101         void setFocusLevel(float level) {
102             mFocusLevel = level;
103             float scale = 1f + mScaleDiff * level;
104             mView.setScaleX(scale);
105             mView.setScaleY(scale);
106             if (mWrapper != null) {
107                 mWrapper.setShadowFocusLevel(level);
108             } else {
109                 ShadowOverlayHelper.setNoneWrapperShadowFocusLevel(mView, level);
110             }
111             if (mDimmer != null) {
112                 mDimmer.setActiveLevel(level);
113                 int color = mDimmer.getPaint().getColor();
114                 if (mWrapper != null) {
115                     mWrapper.setOverlayColor(color);
116                 } else {
117                     ShadowOverlayHelper.setNoneWrapperOverlayColor(mView, color);
118                 }
119             }
120         }
121 
getFocusLevel()122         float getFocusLevel() {
123             return mFocusLevel;
124         }
125 
endAnimation()126         void endAnimation() {
127             mAnimator.end();
128         }
129 
130         @Override
onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime)131         public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) {
132             float fraction;
133             if (totalTime >= mDuration) {
134                 fraction = 1;
135                 mAnimator.end();
136             } else {
137                 fraction = (float) (totalTime / (double) mDuration);
138             }
139             if (mInterpolator != null) {
140                 fraction = mInterpolator.getInterpolation(fraction);
141             }
142             setFocusLevel(mFocusLevelStart + fraction * mFocusLevelDelta);
143         }
144     }
145 
146     static class BrowseItemFocusHighlight implements FocusHighlightHandler {
147         private static final int DURATION_MS = 150;
148 
149         private int mScaleIndex;
150         private final boolean mUseDimmer;
151 
BrowseItemFocusHighlight(int zoomIndex, boolean useDimmer)152         BrowseItemFocusHighlight(int zoomIndex, boolean useDimmer) {
153             if (!isValidZoomIndex(zoomIndex)) {
154                 throw new IllegalArgumentException("Unhandled zoom index");
155             }
156             mScaleIndex = zoomIndex;
157             mUseDimmer = useDimmer;
158         }
159 
getScale(Resources res)160         private float getScale(Resources res) {
161             return mScaleIndex == ZOOM_FACTOR_NONE ? 1f :
162                     res.getFraction(getResId(mScaleIndex), 1, 1);
163         }
164 
165         @Override
onItemFocused(View view, boolean hasFocus)166         public void onItemFocused(View view, boolean hasFocus) {
167             view.setSelected(hasFocus);
168             getOrCreateAnimator(view).animateFocus(hasFocus, false);
169         }
170 
171         @Override
onInitializeView(View view)172         public void onInitializeView(View view) {
173             getOrCreateAnimator(view).animateFocus(false, true);
174         }
175 
getOrCreateAnimator(View view)176         private FocusAnimator getOrCreateAnimator(View view) {
177             FocusAnimator animator = (FocusAnimator) view.getTag(R.id.lb_focus_animator);
178             if (animator == null) {
179                 animator = new FocusAnimator(
180                         view, getScale(view.getResources()), mUseDimmer, DURATION_MS);
181                 view.setTag(R.id.lb_focus_animator, animator);
182             }
183             return animator;
184         }
185 
186     }
187 
188     /**
189      * Sets up the focus highlight behavior of a focused item in browse list row. App usually does
190      * not call this method, it uses {@link ListRowPresenter#ListRowPresenter(int, boolean)}.
191      *
192      * @param zoomIndex One of {@link FocusHighlight#ZOOM_FACTOR_SMALL}
193      * {@link FocusHighlight#ZOOM_FACTOR_XSMALL}
194      * {@link FocusHighlight#ZOOM_FACTOR_MEDIUM}
195      * {@link FocusHighlight#ZOOM_FACTOR_LARGE}
196      * {@link FocusHighlight#ZOOM_FACTOR_NONE}.
197      * @param useDimmer Allow dimming browse item when unselected.
198      * @param adapter  adapter of the list row.
199      */
setupBrowseItemFocusHighlight(ItemBridgeAdapter adapter, int zoomIndex, boolean useDimmer)200     public static void setupBrowseItemFocusHighlight(ItemBridgeAdapter adapter, int zoomIndex,
201             boolean useDimmer) {
202         adapter.setFocusHighlight(new BrowseItemFocusHighlight(zoomIndex, useDimmer));
203     }
204 
205     /**
206      * Sets up default focus highlight behavior of a focused item in header list. It would scale
207      * the focused item and update
208      * {@link RowHeaderPresenter#onSelectLevelChanged(RowHeaderPresenter.ViewHolder)}.
209      * Equivalent to call setupHeaderItemFocusHighlight(gridView, true).
210      *
211      * @param gridView  The header list.
212      * @deprecated Use {@link #setupHeaderItemFocusHighlight(ItemBridgeAdapter)}
213      */
214     @Deprecated
setupHeaderItemFocusHighlight(VerticalGridView gridView)215     public static void setupHeaderItemFocusHighlight(VerticalGridView gridView) {
216         setupHeaderItemFocusHighlight(gridView, true);
217     }
218 
219     /**
220      * Sets up the focus highlight behavior of a focused item in header list.
221      *
222      * @param gridView  The header list.
223      * @param scaleEnabled True if scale the item when focused, false otherwise. Note that
224      * {@link RowHeaderPresenter#onSelectLevelChanged(RowHeaderPresenter.ViewHolder)}
225      * will always be called regardless value of scaleEnabled.
226      * @deprecated Use {@link #setupHeaderItemFocusHighlight(ItemBridgeAdapter, boolean)}
227      */
228     @Deprecated
setupHeaderItemFocusHighlight(VerticalGridView gridView, boolean scaleEnabled)229     public static void setupHeaderItemFocusHighlight(VerticalGridView gridView,
230                                                      boolean scaleEnabled) {
231         if (gridView != null && gridView.getAdapter() instanceof ItemBridgeAdapter) {
232             ((ItemBridgeAdapter) gridView.getAdapter())
233                     .setFocusHighlight(new HeaderItemFocusHighlight(scaleEnabled));
234         }
235     }
236 
237     /**
238      * Sets up default focus highlight behavior of a focused item in header list. It would scale
239      * the focused item and update
240      * {@link RowHeaderPresenter#onSelectLevelChanged(RowHeaderPresenter.ViewHolder)}.
241      * Equivalent to call setupHeaderItemFocusHighlight(itemBridgeAdapter, true).
242      *
243      * @param adapter  The adapter of HeadersFragment.
244      * @see {@link HeadersFragment#getBridgeAdapter()}
245      */
setupHeaderItemFocusHighlight(ItemBridgeAdapter adapter)246     public static void setupHeaderItemFocusHighlight(ItemBridgeAdapter adapter) {
247         setupHeaderItemFocusHighlight(adapter, true);
248     }
249 
250     /**
251      * Sets up the focus highlight behavior of a focused item in header list.
252      *
253      * @param adapter  The adapter of HeadersFragment.
254      * @param scaleEnabled True if scale the item when focused, false otherwise. Note that
255      * {@link RowHeaderPresenter#onSelectLevelChanged(RowHeaderPresenter.ViewHolder)}
256      * will always be called regardless value of scaleEnabled.
257      * @see {@link HeadersFragment#getBridgeAdapter()}
258      */
setupHeaderItemFocusHighlight(ItemBridgeAdapter adapter, boolean scaleEnabled)259     public static void setupHeaderItemFocusHighlight(ItemBridgeAdapter adapter,
260             boolean scaleEnabled) {
261         adapter.setFocusHighlight(new HeaderItemFocusHighlight(scaleEnabled));
262     }
263 
264     static class HeaderItemFocusHighlight implements FocusHighlightHandler {
265         private boolean mInitialized;
266         private float mSelectScale;
267         private int mDuration;
268         boolean mScaleEnabled;
269 
HeaderItemFocusHighlight(boolean scaleEnabled)270         HeaderItemFocusHighlight(boolean scaleEnabled) {
271             mScaleEnabled = scaleEnabled;
272         }
273 
lazyInit(View view)274         void lazyInit(View view) {
275             if (!mInitialized) {
276                 Resources res = view.getResources();
277                 TypedValue value = new TypedValue();
278                 if (mScaleEnabled) {
279                     res.getValue(R.dimen.lb_browse_header_select_scale, value, true);
280                     mSelectScale = value.getFloat();
281                 } else {
282                     mSelectScale = 1f;
283                 }
284                 res.getValue(R.dimen.lb_browse_header_select_duration, value, true);
285                 mDuration = value.data;
286                 mInitialized = true;
287             }
288         }
289 
290         static class HeaderFocusAnimator extends FocusAnimator {
291 
292             ItemBridgeAdapter.ViewHolder mViewHolder;
HeaderFocusAnimator(View view, float scale, int duration)293             HeaderFocusAnimator(View view, float scale, int duration) {
294                 super(view, scale, false, duration);
295 
296                 ViewParent parent = view.getParent();
297                 while (parent != null) {
298                     if (parent instanceof RecyclerView) {
299                         break;
300                     }
301                     parent = parent.getParent();
302                 }
303                 if (parent != null) {
304                     mViewHolder = (ItemBridgeAdapter.ViewHolder) ((RecyclerView) parent)
305                             .getChildViewHolder(view);
306                 }
307             }
308 
309             @Override
setFocusLevel(float level)310             void setFocusLevel(float level) {
311                 Presenter presenter = mViewHolder.getPresenter();
312                 if (presenter instanceof RowHeaderPresenter) {
313                     ((RowHeaderPresenter) presenter).setSelectLevel(
314                             ((RowHeaderPresenter.ViewHolder) mViewHolder.getViewHolder()), level);
315                 }
316                 super.setFocusLevel(level);
317             }
318 
319         }
320 
viewFocused(View view, boolean hasFocus)321         private void viewFocused(View view, boolean hasFocus) {
322             lazyInit(view);
323             view.setSelected(hasFocus);
324             FocusAnimator animator = (FocusAnimator) view.getTag(R.id.lb_focus_animator);
325             if (animator == null) {
326                 animator = new HeaderFocusAnimator(view, mSelectScale, mDuration);
327                 view.setTag(R.id.lb_focus_animator, animator);
328             }
329             animator.animateFocus(hasFocus, false);
330         }
331 
332         @Override
onItemFocused(View view, boolean hasFocus)333         public void onItemFocused(View view, boolean hasFocus) {
334             viewFocused(view, hasFocus);
335         }
336 
337         @Override
onInitializeView(View view)338         public void onInitializeView(View view) {
339         }
340 
341     }
342 
343     /** @deprecated This type should not be instantiated as it contains only static methods. */
344     @Deprecated
345     @SuppressWarnings("PrivateConstructorForUtilityClass")
FocusHighlightHelper()346     public FocusHighlightHelper() {
347     }
348 }
349