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