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