Home | History | Annotate | Download | only in widget
      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 
     40     static boolean isValidZoomIndex(int zoomIndex) {
     41         return zoomIndex == ZOOM_FACTOR_NONE || getResId(zoomIndex) > 0;
     42     }
     43 
     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 
     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 
     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 
    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 
    122         float getFocusLevel() {
    123             return mFocusLevel;
    124         }
    125 
    126         void endAnimation() {
    127             mAnimator.end();
    128         }
    129 
    130         @Override
    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 
    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 
    160         private float getScale(Resources res) {
    161             return mScaleIndex == ZOOM_FACTOR_NONE ? 1f :
    162                     res.getFraction(getResId(mScaleIndex), 1, 1);
    163         }
    164 
    165         @Override
    166         public void onItemFocused(View view, boolean hasFocus) {
    167             view.setSelected(hasFocus);
    168             getOrCreateAnimator(view).animateFocus(hasFocus, false);
    169         }
    170 
    171         @Override
    172         public void onInitializeView(View view) {
    173             getOrCreateAnimator(view).animateFocus(false, true);
    174         }
    175 
    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      */
    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
    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
    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      */
    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      */
    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 
    270         HeaderItemFocusHighlight(boolean scaleEnabled) {
    271             mScaleEnabled = scaleEnabled;
    272         }
    273 
    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;
    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
    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 
    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
    333         public void onItemFocused(View view, boolean hasFocus) {
    334             viewFocused(view, hasFocus);
    335         }
    336 
    337         @Override
    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")
    346     public FocusHighlightHelper() {
    347     }
    348 }
    349