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