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 */
14package androidx.leanback.widget;
15
16
17import android.animation.ValueAnimator;
18import android.content.Context;
19import android.graphics.Color;
20import android.graphics.Rect;
21import android.util.TypedValue;
22import android.view.ContextThemeWrapper;
23import android.view.LayoutInflater;
24import android.view.View;
25import android.view.ViewGroup;
26import android.view.animation.DecelerateInterpolator;
27import android.widget.TextView;
28import android.widget.ViewFlipper;
29
30import androidx.core.view.ViewCompat;
31import androidx.leanback.R;
32
33import java.util.ArrayList;
34import 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 */
69public 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