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