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