Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2016 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 
     17 import android.animation.ValueAnimator;
     18 import android.content.Context;
     19 import android.graphics.Color;
     20 import android.graphics.Rect;
     21 import android.util.TypedValue;
     22 import android.view.ContextThemeWrapper;
     23 import android.view.LayoutInflater;
     24 import android.view.View;
     25 import android.view.ViewGroup;
     26 import android.view.animation.DecelerateInterpolator;
     27 import android.widget.TextView;
     28 import android.widget.ViewFlipper;
     29 
     30 import androidx.core.view.ViewCompat;
     31 import androidx.leanback.R;
     32 
     33 import java.util.ArrayList;
     34 import java.util.List;
     35 
     36 /**
     37  * Abstract {@link Presenter} class for rendering media items in a playlist format.
     38  * Media item data provided for this presenter can implement the interface
     39  * {@link MultiActionsProvider}, if the media rows wish to contain custom actions.
     40  * Media items in the playlist are arranged as a vertical list with each row holding each media
     41  * item's details provided by the user of this class and a set of optional custom actions.
     42  * Each media item's details and actions are separately focusable.
     43  * The appearance of each one of the media row components can be controlled through setting
     44  * theme's attributes.
     45  * Each media item row provides a view flipper for switching between different views depending on
     46  * the playback state.
     47  * A default layout is provided by this presenter for rendering different playback states, or a
     48  * custom layout can be provided by the user by overriding the
     49  * playbackMediaItemNumberViewFlipperLayout attribute in the currently specified theme.
     50  * Subclasses should also override {@link #getMediaPlayState(Object)} to provide the current play
     51  * state of their media item model in case they wish to use different views depending on the
     52  * playback state.
     53  * The presenter can optionally provide line separators between media rows by setting
     54  * {@link #setHasMediaRowSeparator(boolean)} to true.
     55  * <p>
     56  *     Subclasses must override {@link #onBindMediaDetails} to implement their media item model
     57  *     data binding to each row view.
     58  * </p>
     59  * <p>
     60  *     The {@link OnItemViewClickedListener} and {@link OnItemViewSelectedListener}
     61  *     can be used in the same fashion to handle selection or click events on either of
     62  *     media details or each individual action views.
     63  * </p>
     64  * <p>
     65  *     {@link AbstractMediaListHeaderPresenter} can be used in conjunction with this presenter in
     66  *     order to display a playlist with a header view.
     67  * </p>
     68  */
     69 public abstract class AbstractMediaItemPresenter extends RowPresenter {
     70 
     71     /**
     72      * Different playback states of a media item
     73      */
     74 
     75     /**
     76      * Indicating that the media item is currently neither playing nor paused.
     77      */
     78     public static final int PLAY_STATE_INITIAL = 0;
     79     /**
     80      * Indicating that the media item is currently paused.
     81      */
     82     public static final int PLAY_STATE_PAUSED = 1;
     83     /**
     84      * Indicating that the media item is currently playing
     85      */
     86     public static final int PLAY_STATE_PLAYING = 2;
     87 
     88     final static Rect sTempRect = new Rect();
     89     private int mBackgroundColor = Color.TRANSPARENT;
     90     private boolean mBackgroundColorSet;
     91     private boolean mMediaRowSeparator;
     92     private int mThemeId;
     93 
     94     private Presenter mMediaItemActionPresenter = new MediaItemActionPresenter();
     95 
     96     /**
     97      * Constructor used for creating an abstract media item presenter.
     98      */
     99     public AbstractMediaItemPresenter() {
    100         this(0);
    101     }
    102 
    103     /**
    104      * Constructor used for creating an abstract media item presenter.
    105      * @param themeId The resource id of the theme that defines attributes controlling the
    106      *                appearance of different widgets in a media item row.
    107      */
    108     public AbstractMediaItemPresenter(int themeId) {
    109         mThemeId = themeId;
    110         setHeaderPresenter(null);
    111     }
    112 
    113     /**
    114      * Sets the theme used to style a media item row components.
    115      * @param themeId The resource id of the theme that defines attributes controlling the
    116      *                appearance of different widgets in a media item row.
    117      */
    118     public void setThemeId(int themeId) {
    119         mThemeId = themeId;
    120     }
    121 
    122     /**
    123      * Return The resource id of the theme that defines attributes controlling the appearance of
    124      * different widgets in a media item row.
    125      *
    126      * @return The resource id of the theme that defines attributes controlling the appearance of
    127      * different widgets in a media item row.
    128      */
    129     public int getThemeId() {
    130         return mThemeId;
    131     }
    132 
    133     /**
    134      * Sets the action presenter rendering each optional custom action within each media item row.
    135      * @param actionPresenter the presenter to be used for rendering a media item row actions.
    136      */
    137     public void setActionPresenter(Presenter actionPresenter) {
    138         mMediaItemActionPresenter = actionPresenter;
    139     }
    140 
    141     /**
    142      * Return the presenter used to render a media item row actions.
    143      *
    144      * @return the presenter used to render a media item row actions.
    145      */
    146     public Presenter getActionPresenter() {
    147         return mMediaItemActionPresenter;
    148     }
    149 
    150     /**
    151      * The ViewHolder for the {@link AbstractMediaItemPresenter}. It references different views
    152      * that place different meta-data corresponding to a media item details, actions, selector,
    153      * listeners, and presenters,
    154      */
    155     public static class ViewHolder extends RowPresenter.ViewHolder {
    156 
    157         final View mMediaRowView;
    158         final View mSelectorView;
    159         private final View mMediaItemDetailsView;
    160         final ViewFlipper mMediaItemNumberViewFlipper;
    161         final TextView mMediaItemNumberView;
    162         final View mMediaItemPausedView;
    163 
    164         final View mMediaItemPlayingView;
    165         private final TextView mMediaItemNameView;
    166         private final TextView mMediaItemDurationView;
    167         private final View mMediaItemRowSeparator;
    168         private final ViewGroup mMediaItemActionsContainer;
    169         private final List<Presenter.ViewHolder> mActionViewHolders;
    170         MultiActionsProvider.MultiAction[] mMediaItemRowActions;
    171         AbstractMediaItemPresenter mRowPresenter;
    172         ValueAnimator mFocusViewAnimator;
    173 
    174         public ViewHolder(View view) {
    175             super(view);
    176             mSelectorView = view.findViewById(R.id.mediaRowSelector);
    177             mMediaRowView  = view.findViewById(R.id.mediaItemRow);
    178             mMediaItemDetailsView = view.findViewById(R.id.mediaItemDetails);
    179             mMediaItemNameView = (TextView) view.findViewById(R.id.mediaItemName);
    180             mMediaItemDurationView = (TextView) view.findViewById(R.id.mediaItemDuration);
    181             mMediaItemRowSeparator = view.findViewById(R.id.mediaRowSeparator);
    182             mMediaItemActionsContainer = (ViewGroup) view.findViewById(
    183                     R.id.mediaItemActionsContainer);
    184             mActionViewHolders = new ArrayList<Presenter.ViewHolder>();
    185             getMediaItemDetailsView().setOnClickListener(new View.OnClickListener(){
    186                 @Override
    187                 public void onClick(View view) {
    188                     if (getOnItemViewClickedListener() != null) {
    189                         getOnItemViewClickedListener().onItemClicked(null, null,
    190                                 ViewHolder.this, getRowObject());
    191                     }
    192                 }
    193             });
    194             getMediaItemDetailsView().setOnFocusChangeListener(new View.OnFocusChangeListener() {
    195                 @Override
    196                 public void onFocusChange(View view, boolean hasFocus) {
    197                     mFocusViewAnimator = updateSelector(mSelectorView, view, mFocusViewAnimator,
    198                             true);
    199                 }
    200             });
    201             mMediaItemNumberViewFlipper =
    202                     (ViewFlipper) view.findViewById(R.id.mediaItemNumberViewFlipper);
    203 
    204             TypedValue typedValue = new TypedValue();
    205             boolean found = view.getContext().getTheme().resolveAttribute(
    206                     R.attr.playbackMediaItemNumberViewFlipperLayout, typedValue, true);
    207             View mergeView = LayoutInflater.from(view.getContext())
    208                     .inflate(found
    209                             ? typedValue.resourceId
    210                             : R.layout.lb_media_item_number_view_flipper,
    211                             mMediaItemNumberViewFlipper, true);
    212 
    213             mMediaItemNumberView = (TextView) mergeView.findViewById(R.id.initial);
    214             mMediaItemPausedView = mergeView.findViewById(R.id.paused);
    215             mMediaItemPlayingView = mergeView.findViewById(R.id.playing);
    216         }
    217 
    218         /**
    219          * Binds the actions in a media item row object to their views. This consists of creating
    220          * (or reusing the existing) action view holders, and populating them with the actions'
    221          * icons.
    222          */
    223         public void onBindRowActions() {
    224             for (int i = getMediaItemActionsContainer().getChildCount() - 1;
    225                  i >= mActionViewHolders.size(); i--) {
    226                 getMediaItemActionsContainer().removeViewAt(i);
    227                 mActionViewHolders.remove(i);
    228             }
    229             mMediaItemRowActions = null;
    230 
    231             Object rowObject = getRowObject();
    232             final MultiActionsProvider.MultiAction[] actionList;
    233             if (rowObject instanceof MultiActionsProvider) {
    234                 actionList = ((MultiActionsProvider) rowObject).getActions();
    235             } else {
    236                 return;
    237             }
    238             Presenter actionPresenter = mRowPresenter.getActionPresenter();
    239             if (actionPresenter == null) {
    240                 return;
    241             }
    242 
    243             mMediaItemRowActions = actionList;
    244             for (int i = mActionViewHolders.size(); i < actionList.length; i++) {
    245                 final int actionIndex = i;
    246                 final Presenter.ViewHolder actionViewHolder =
    247                         actionPresenter.onCreateViewHolder(getMediaItemActionsContainer());
    248                 getMediaItemActionsContainer().addView(actionViewHolder.view);
    249                 mActionViewHolders.add(actionViewHolder);
    250                 actionViewHolder.view.setOnFocusChangeListener(new View.OnFocusChangeListener() {
    251                     @Override
    252                     public void onFocusChange(View view, boolean hasFocus) {
    253                         mFocusViewAnimator = updateSelector(mSelectorView, view,
    254                                 mFocusViewAnimator, false);
    255                     }
    256                 });
    257                 actionViewHolder.view.setOnClickListener(
    258                         new View.OnClickListener() {
    259                             @Override
    260                             public void onClick(View view) {
    261                                 if (getOnItemViewClickedListener() != null) {
    262                                     getOnItemViewClickedListener().onItemClicked(
    263                                             actionViewHolder, mMediaItemRowActions[actionIndex],
    264                                             ViewHolder.this, getRowObject());
    265                                 }
    266                             }
    267                         });
    268             }
    269 
    270             if (mMediaItemActionsContainer != null) {
    271                 for (int i = 0; i < actionList.length; i++) {
    272                     Presenter.ViewHolder avh = mActionViewHolders.get(i);
    273                     actionPresenter.onUnbindViewHolder(avh);
    274                     actionPresenter.onBindViewHolder(avh, mMediaItemRowActions[i]);
    275                 }
    276             }
    277 
    278         }
    279 
    280         int findActionIndex(MultiActionsProvider.MultiAction action) {
    281             if (mMediaItemRowActions != null) {
    282                 for (int i = 0; i < mMediaItemRowActions.length; i++) {
    283                     if (mMediaItemRowActions[i] == action) {
    284                         return i;
    285                     }
    286                 }
    287             }
    288             return -1;
    289         }
    290 
    291         /**
    292          * Notifies an action has changed in this media row and the UI needs to be updated
    293          * @param action The action whose state has changed
    294          */
    295         public void notifyActionChanged(MultiActionsProvider.MultiAction action) {
    296             Presenter actionPresenter = mRowPresenter.getActionPresenter();
    297             if (actionPresenter == null) {
    298                 return;
    299             }
    300             int actionIndex = findActionIndex(action);
    301             if (actionIndex >= 0) {
    302                 Presenter.ViewHolder actionViewHolder = mActionViewHolders.get(actionIndex);
    303                 actionPresenter.onUnbindViewHolder(actionViewHolder);
    304                 actionPresenter.onBindViewHolder(actionViewHolder, action);
    305             }
    306         }
    307 
    308         /**
    309          * Notifies the content of the media item details in a row has changed and triggers updating
    310          * the UI. This causes {@link #onBindMediaDetails(ViewHolder, Object)}
    311          * on the user's provided presenter to be called back, allowing them to update UI
    312          * accordingly.
    313          */
    314         public void notifyDetailsChanged() {
    315             mRowPresenter.onUnbindMediaDetails(this);
    316             mRowPresenter.onBindMediaDetails(this, getRowObject());
    317         }
    318 
    319         /**
    320          * Notifies the playback state of the media item row has changed. This in turn triggers
    321          * updating of the UI for that media item row if corresponding views are specified for each
    322          * playback state.
    323          * By default, 3 views are provided for each playback state, or these views can be provided
    324          * by the user.
    325          */
    326         public void notifyPlayStateChanged() {
    327             mRowPresenter.onBindMediaPlayState(this);
    328         }
    329 
    330         /**
    331          * @return The SelectorView responsible for highlighting the in-focus view within each
    332          * media item row
    333          */
    334         public View getSelectorView() {
    335             return mSelectorView;
    336         }
    337 
    338         /**
    339          * @return The FlipperView responsible for flipping between different media item number
    340          * views depending on the playback state
    341          */
    342         public ViewFlipper getMediaItemNumberViewFlipper() {
    343             return mMediaItemNumberViewFlipper;
    344         }
    345 
    346         /**
    347          * @return The TextView responsible for rendering the media item number.
    348          * This view is rendered when the media item row is neither playing nor paused.
    349          */
    350         public TextView getMediaItemNumberView() {
    351             return mMediaItemNumberView;
    352         }
    353 
    354         /**
    355          * @return The view rendered when the media item row is paused.
    356          */
    357         public View getMediaItemPausedView() {
    358             return mMediaItemPausedView;
    359         }
    360 
    361         /**
    362          * @return The view rendered when the media item row is playing.
    363          */
    364         public View getMediaItemPlayingView() {
    365             return mMediaItemPlayingView;
    366         }
    367 
    368 
    369         /**
    370          * Flips to the view at index 'position'. This position corresponds to the index of a
    371          * particular view within the ViewFlipper layout specified for the MediaItemNumberView
    372          * (see playbackMediaItemNumberViewFlipperLayout attribute).
    373          * @param position The index of the child view to display.
    374          */
    375         public void setSelectedMediaItemNumberView(int position) {
    376             if (position >= 0 && position < mMediaItemNumberViewFlipper.getChildCount()) {
    377                 mMediaItemNumberViewFlipper.setDisplayedChild(position);
    378             }
    379         }
    380         /**
    381          * Returns the view displayed when the media item is neither playing nor paused,
    382          * corresponding to the playback state of PLAY_STATE_INITIAL.
    383          * @return The TextView responsible for rendering the media item name.
    384          */
    385         public TextView getMediaItemNameView() {
    386             return mMediaItemNameView;
    387         }
    388 
    389         /**
    390          * @return The TextView responsible for rendering the media item duration
    391          */
    392         public TextView getMediaItemDurationView() {
    393             return mMediaItemDurationView;
    394         }
    395 
    396         /**
    397          * @return The view container of media item details
    398          */
    399         public View getMediaItemDetailsView() {
    400             return mMediaItemDetailsView;
    401         }
    402 
    403         /**
    404          * @return The view responsible for rendering the separator line between media rows
    405          */
    406         public View getMediaItemRowSeparator() {
    407             return mMediaItemRowSeparator;
    408         }
    409 
    410         /**
    411          * @return The view containing the set of custom actions
    412          */
    413         public ViewGroup getMediaItemActionsContainer() {
    414             return mMediaItemActionsContainer;
    415         }
    416 
    417         /**
    418          * @return Array of MultiActions displayed for this media item row
    419          */
    420         public MultiActionsProvider.MultiAction[] getMediaItemRowActions() {
    421             return mMediaItemRowActions;
    422         }
    423     }
    424 
    425     @Override
    426     protected RowPresenter.ViewHolder createRowViewHolder(ViewGroup parent) {
    427         Context context = parent.getContext();
    428         if (mThemeId != 0) {
    429             context = new ContextThemeWrapper(context, mThemeId);
    430         }
    431         View view =
    432                 LayoutInflater.from(context).inflate(R.layout.lb_row_media_item, parent, false);
    433         final ViewHolder vh = new ViewHolder(view);
    434         vh.mRowPresenter = this;
    435         if (mBackgroundColorSet) {
    436             vh.mMediaRowView.setBackgroundColor(mBackgroundColor);
    437         }
    438         return vh;
    439     }
    440 
    441     @Override
    442     public boolean isUsingDefaultSelectEffect() {
    443         return false;
    444     }
    445 
    446     @Override
    447     protected boolean isClippingChildren() {
    448         return true;
    449     }
    450 
    451     @Override
    452     protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) {
    453         super.onBindRowViewHolder(vh, item);
    454 
    455         final ViewHolder mvh = (ViewHolder) vh;
    456 
    457         onBindRowActions(mvh);
    458 
    459         mvh.getMediaItemRowSeparator().setVisibility(hasMediaRowSeparator() ? View.VISIBLE :
    460                 View.GONE);
    461 
    462         onBindMediaPlayState(mvh);
    463         onBindMediaDetails((ViewHolder) vh, item);
    464     }
    465 
    466     /**
    467      * Binds the given media item object action to the given ViewHolder's action views.
    468      * @param vh ViewHolder for the media item.
    469      */
    470     protected void onBindRowActions(ViewHolder vh) {
    471         vh.onBindRowActions();
    472     }
    473 
    474     /**
    475      * Sets the background color for the row views within the playlist.
    476      * If this is not set, a default color, defaultBrandColor, from theme is used.
    477      * This defaultBrandColor defaults to android:attr/colorPrimary on v21, if it's specified.
    478      * @param color The ARGB color used to set as the media list background color.
    479      */
    480     public void setBackgroundColor(int color) {
    481         mBackgroundColorSet = true;
    482         mBackgroundColor = color;
    483     }
    484 
    485     /**
    486      * Specifies whether a line separator should be used between media item rows.
    487      * @param hasSeparator true if a separator should be displayed, false otherwise.
    488      */
    489     public void setHasMediaRowSeparator(boolean hasSeparator) {
    490         mMediaRowSeparator = hasSeparator;
    491     }
    492 
    493     public boolean hasMediaRowSeparator() {
    494         return mMediaRowSeparator;
    495     }
    496     /**
    497      * Binds the media item details to their views provided by the
    498      * {@link AbstractMediaItemPresenter}.
    499      * This method is to be overridden by the users of this presenter.
    500      * The subclasses of this presenter can access and bind individual views for either of the
    501      * media item number, name, or duration (depending on whichever views are visible according to
    502      * the providing theme attributes), by calling {@link ViewHolder#getMediaItemNumberView()},
    503      * {@link ViewHolder#getMediaItemNameView()}, and {@link ViewHolder#getMediaItemDurationView()},
    504      * on the {@link ViewHolder} provided as the argument {@code vh} of this presenter.
    505      *
    506      * @param vh The ViewHolder for this {@link AbstractMediaItemPresenter}.
    507      * @param item The media item row object being presented.
    508      */
    509     protected abstract void onBindMediaDetails(ViewHolder vh, Object item);
    510 
    511     /**
    512      * Unbinds the media item details from their views provided by the
    513      * {@link AbstractMediaItemPresenter}.
    514      * This method can be overridden by the subclasses of this presenter if required.
    515      * @param vh ViewHolder to unbind from.
    516      */
    517     protected void onUnbindMediaDetails(ViewHolder vh) {
    518     }
    519 
    520     /**
    521      * Binds the media item number view to the appropriate play state view of the media item.
    522      * The play state of the media item is extracted by calling {@link #getMediaPlayState(Object)} for
    523      * the media item embedded within this view.
    524      * This method triggers updating of the playback state UI if corresponding views are specified
    525      * for the current playback state.
    526      * By default, 3 views are provided for each playback state, or these views can be provided
    527      * by the user.
    528      */
    529     public void onBindMediaPlayState(ViewHolder vh) {
    530         int childIndex = calculateMediaItemNumberFlipperIndex(vh);
    531         if (childIndex != -1 && vh.mMediaItemNumberViewFlipper.getDisplayedChild() != childIndex) {
    532             vh.mMediaItemNumberViewFlipper.setDisplayedChild(childIndex);
    533         }
    534     }
    535 
    536     static int calculateMediaItemNumberFlipperIndex(ViewHolder vh) {
    537         int childIndex = -1;
    538         int newPlayState = vh.mRowPresenter.getMediaPlayState(vh.getRowObject());
    539         switch (newPlayState) {
    540             case PLAY_STATE_INITIAL:
    541                 childIndex = (vh.mMediaItemNumberView == null) ? -1 :
    542                         vh.mMediaItemNumberViewFlipper.indexOfChild(vh.mMediaItemNumberView);
    543                 break;
    544             case PLAY_STATE_PAUSED:
    545                 childIndex = (vh.mMediaItemPausedView == null) ? -1 :
    546                         vh.mMediaItemNumberViewFlipper.indexOfChild(vh.mMediaItemPausedView);
    547                 break;
    548             case PLAY_STATE_PLAYING:
    549                 childIndex = (vh.mMediaItemPlayingView == null) ? -1 :
    550                         vh.mMediaItemNumberViewFlipper.indexOfChild(vh.mMediaItemPlayingView);
    551         }
    552         return childIndex;
    553     }
    554 
    555     /**
    556      * Called when the given ViewHolder wants to unbind the play state view.
    557      * @param vh The ViewHolder to unbind from.
    558      */
    559     public void onUnbindMediaPlayState(ViewHolder vh) {
    560     }
    561 
    562     /**
    563      * Returns the current play state of the given media item. By default, this method returns
    564      * PLAY_STATE_INITIAL which causes the media item number
    565      * {@link ViewHolder#getMediaItemNameView()} to be displayed for different
    566      * playback states. Users of this class should override this method in order to provide the
    567      * play state of their custom media item data model.
    568      * @param item The media item
    569      * @return The current play state of this media item
    570      */
    571     protected int getMediaPlayState(Object item) {
    572         return PLAY_STATE_INITIAL;
    573     }
    574     /**
    575      * Each media item row can have multiple focusable elements; the details on the left and a set
    576      * of optional custom actions on the right.
    577      * The selector is a highlight that moves to highlight to cover whichever views is in focus.
    578      *
    579      * @param selectorView the selector view used to highlight an individual element within a row.
    580      * @param focusChangedView The component within the media row whose focus got changed.
    581      * @param layoutAnimator the ValueAnimator producing animation frames for the selector's width
    582      *                       and x-translation, generated by this method and stored for the each
    583      *                       {@link ViewHolder}.
    584      * @param isDetails Whether the changed-focused view is for a media item details (true) or
    585      *                  an action (false).
    586      */
    587     static ValueAnimator updateSelector(final View selectorView,
    588             View focusChangedView, ValueAnimator layoutAnimator, boolean isDetails) {
    589         int animationDuration = focusChangedView.getContext().getResources()
    590                 .getInteger(android.R.integer.config_shortAnimTime);
    591         DecelerateInterpolator interpolator = new DecelerateInterpolator();
    592 
    593         int layoutDirection = ViewCompat.getLayoutDirection(selectorView);
    594         if (!focusChangedView.hasFocus()) {
    595             // if neither of the details or action views are in focus (ie. another row is in focus),
    596             // animate the selector out.
    597             selectorView.animate().cancel();
    598             selectorView.animate().alpha(0f).setDuration(animationDuration)
    599                     .setInterpolator(interpolator).start();
    600             // keep existing layout animator
    601             return layoutAnimator;
    602         } else {
    603             // cancel existing layout animator
    604             if (layoutAnimator != null) {
    605                 layoutAnimator.cancel();
    606                 layoutAnimator = null;
    607             }
    608             float currentAlpha = selectorView.getAlpha();
    609             selectorView.animate().alpha(1f).setDuration(animationDuration)
    610                     .setInterpolator(interpolator).start();
    611 
    612             final ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams)
    613                     selectorView.getLayoutParams();
    614             ViewGroup rootView = (ViewGroup) selectorView.getParent();
    615             sTempRect.set(0, 0, focusChangedView.getWidth(), focusChangedView.getHeight());
    616             rootView.offsetDescendantRectToMyCoords(focusChangedView, sTempRect);
    617             if (isDetails) {
    618                 if (layoutDirection == View.LAYOUT_DIRECTION_RTL ) {
    619                     sTempRect.right += rootView.getHeight();
    620                     sTempRect.left -= rootView.getHeight() / 2;
    621                 } else {
    622                     sTempRect.left -= rootView.getHeight();
    623                     sTempRect.right += rootView.getHeight() / 2;
    624                 }
    625             }
    626             final int targetLeft = sTempRect.left;
    627             final int targetWidth = sTempRect.width();
    628             final float deltaWidth = lp.width - targetWidth;
    629             final float deltaLeft = lp.leftMargin - targetLeft;
    630 
    631             if (deltaLeft == 0f && deltaWidth == 0f)
    632             {
    633                 // no change needed
    634             } else if (currentAlpha == 0f) {
    635                 // change selector to the proper width and marginLeft without animation.
    636                 lp.width = targetWidth;
    637                 lp.leftMargin = targetLeft;
    638                 selectorView.requestLayout();
    639             } else {
    640                 // animate the selector to the proper width and marginLeft.
    641                 layoutAnimator = ValueAnimator.ofFloat(0f, 1f);
    642                 layoutAnimator.setDuration(animationDuration);
    643                 layoutAnimator.setInterpolator(interpolator);
    644 
    645                 layoutAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    646                     @Override
    647                     public void onAnimationUpdate(ValueAnimator valueAnimator) {
    648                         // Set width to the proper width for this animation step.
    649                         float fractionToEnd = 1f - valueAnimator.getAnimatedFraction();
    650                         lp.leftMargin = Math.round(targetLeft + deltaLeft * fractionToEnd);
    651                         lp.width = Math.round(targetWidth + deltaWidth * fractionToEnd);
    652                         selectorView.requestLayout();
    653                     }
    654                 });
    655                 layoutAnimator.start();
    656             }
    657             return layoutAnimator;
    658 
    659         }
    660     }
    661 }
    662