AbstractMediaItemPresenter.java revision 78be4412362eafffe14b60a20b7ddd4bf86a515b
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 private final View mMediaRowView; 157 private final View mSelectorView; 158 private final View mMediaItemDetailsView; 159 private final ViewFlipper mMediaItemNumberViewFlipper; 160 private final TextView mMediaItemNumberView; 161 private final View mMediaItemPausedView; 162 163 private 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 private MultiActionsProvider.MultiAction[] mMediaItemRowActions; 170 AbstractMediaItemPresenter mRowPresenter; 171 private 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 ? typedValue.resourceId : 208 R.layout.lb_media_item_number_view_flipper, 209 mMediaItemNumberViewFlipper, true); 210 211 mMediaItemNumberView = (TextView) mergeView.findViewById(R.id.initial); 212 mMediaItemPausedView = mergeView.findViewById(R.id.paused); 213 mMediaItemPlayingView = mergeView.findViewById(R.id.playing); 214 } 215 216 /** 217 * Binds the actions in a media item row object to their views. This consists of creating 218 * (or reusing the existing) action view holders, and populating them with the actions' 219 * icons. 220 */ 221 public void onBindRowActions() { 222 for (int i = getMediaItemActionsContainer().getChildCount() - 1; 223 i >= mActionViewHolders.size(); i--) { 224 getMediaItemActionsContainer().removeViewAt(i); 225 mActionViewHolders.remove(i); 226 } 227 mMediaItemRowActions = null; 228 229 Object rowObject = getRowObject(); 230 final MultiActionsProvider.MultiAction[] actionList; 231 if (rowObject instanceof MultiActionsProvider) { 232 actionList = ((MultiActionsProvider) rowObject).getActions(); 233 } else { 234 return; 235 } 236 Presenter actionPresenter = mRowPresenter.getActionPresenter(); 237 if (actionPresenter == null) { 238 return; 239 } 240 241 mMediaItemRowActions = actionList; 242 for (int i = mActionViewHolders.size(); i < actionList.length; i++) { 243 final int actionIndex = i; 244 final Presenter.ViewHolder actionViewHolder = actionPresenter. 245 onCreateViewHolder(getMediaItemActionsContainer()); 246 getMediaItemActionsContainer().addView(actionViewHolder.view); 247 mActionViewHolders.add(actionViewHolder); 248 actionViewHolder.view.setOnFocusChangeListener(new View.OnFocusChangeListener() { 249 @Override 250 public void onFocusChange(View view, boolean hasFocus) { 251 mFocusViewAnimator = updateSelector(mSelectorView, view, 252 mFocusViewAnimator, false); 253 } 254 }); 255 actionViewHolder.view.setOnClickListener( 256 new View.OnClickListener() { 257 @Override 258 public void onClick(View view) { 259 if (getOnItemViewClickedListener() != null) { 260 getOnItemViewClickedListener().onItemClicked( 261 actionViewHolder, mMediaItemRowActions[actionIndex], 262 ViewHolder.this, getRowObject()); 263 } 264 } 265 }); 266 } 267 268 if (mMediaItemActionsContainer != null) { 269 for (int i = 0; i < actionList.length; i++) { 270 Presenter.ViewHolder avh = mActionViewHolders.get(i); 271 actionPresenter.onUnbindViewHolder(avh); 272 actionPresenter.onBindViewHolder(avh, mMediaItemRowActions[i]); 273 } 274 } 275 276 } 277 278 int findActionIndex(MultiActionsProvider.MultiAction action) { 279 if (mMediaItemRowActions != null) { 280 for (int i = 0; i < mMediaItemRowActions.length; i++) { 281 if (mMediaItemRowActions[i] == action) { 282 return i; 283 } 284 } 285 } 286 return -1; 287 } 288 289 /** 290 * Notifies an action has changed in this media row and the UI needs to be updated 291 * @param action The action whose state has changed 292 */ 293 public void notifyActionChanged(MultiActionsProvider.MultiAction action) { 294 Presenter actionPresenter = mRowPresenter.getActionPresenter(); 295 if (actionPresenter == null) { 296 return; 297 } 298 int actionIndex = findActionIndex(action); 299 if (actionIndex >= 0) { 300 Presenter.ViewHolder actionViewHolder = mActionViewHolders.get(actionIndex); 301 actionPresenter.onUnbindViewHolder(actionViewHolder); 302 actionPresenter.onBindViewHolder(actionViewHolder, action); 303 } 304 } 305 306 /** 307 * Notifies the content of the media item details in a row has changed and triggers updating 308 * the UI. This causes {@link #onBindMediaDetails(ViewHolder, Object)} 309 * on the user's provided presenter to be called back, allowing them to update UI 310 * accordingly. 311 */ 312 public void notifyDetailsChanged() { 313 mRowPresenter.onUnbindMediaDetails(this); 314 mRowPresenter.onBindMediaDetails(this, getRowObject()); 315 } 316 317 /** 318 * Notifies the playback state of the media item row has changed. This in turn triggers 319 * updating of the UI for that media item row if corresponding views are specified for each 320 * playback state. 321 * By default, 3 views are provided for each playback state, or these views can be provided 322 * by the user. 323 */ 324 public void notifyPlayStateChanged() { 325 mRowPresenter.onBindMediaPlayState(this); 326 } 327 328 /** 329 * @return The SelectorView responsible for highlighting the in-focus view within each 330 * media item row 331 */ 332 public View getSelectorView() { 333 return mSelectorView; 334 } 335 336 /** 337 * @return The FlipperView responsible for flipping between different media item number 338 * views depending on the playback state 339 */ 340 public ViewFlipper getMediaItemNumberViewFlipper() { 341 return mMediaItemNumberViewFlipper; 342 } 343 344 /** 345 * @return The TextView responsible for rendering the media item number. 346 * This view is rendered when the media item row is neither playing nor paused. 347 */ 348 public TextView getMediaItemNumberView() { 349 return mMediaItemNumberView; 350 } 351 352 /** 353 * @return The view rendered when the media item row is paused. 354 */ 355 public View getMediaItemPausedView() { 356 return mMediaItemPausedView; 357 } 358 359 /** 360 * @return The view rendered when the media item row is playing. 361 */ 362 public View getMediaItemPlayingView() { 363 return mMediaItemPlayingView; 364 } 365 366 367 /** 368 * Flips to the view at index 'position'. This position corresponds to the index of a 369 * particular view within the ViewFlipper layout specified for the MediaItemNumberView 370 * (see playbackMediaItemNumberViewFlipperLayout attribute). 371 * @param position The index of the child view to display. 372 */ 373 public void setSelectedMediaItemNumberView(int position) { 374 if (position >= 0 & position < mMediaItemNumberViewFlipper.getChildCount()) { 375 mMediaItemNumberViewFlipper.setDisplayedChild(position); 376 } 377 } 378 /** 379 * Returns the view displayed when the media item is neither playing nor paused, 380 * corresponding to the playback state of PLAY_STATE_INITIAL. 381 * @return The TextView responsible for rendering the media item name. 382 */ 383 public TextView getMediaItemNameView() { 384 return mMediaItemNameView; 385 } 386 387 /** 388 * @return The TextView responsible for rendering the media item duration 389 */ 390 public TextView getMediaItemDurationView() { 391 return mMediaItemDurationView; 392 } 393 394 /** 395 * @return The view container of media item details 396 */ 397 public View getMediaItemDetailsView() { 398 return mMediaItemDetailsView; 399 } 400 401 /** 402 * @return The view responsible for rendering the separator line between media rows 403 */ 404 public View getMediaItemRowSeparator() { 405 return mMediaItemRowSeparator; 406 } 407 408 /** 409 * @return The view containing the set of custom actions 410 */ 411 public ViewGroup getMediaItemActionsContainer() { 412 return mMediaItemActionsContainer; 413 } 414 415 /** 416 * @return Array of MultiActions displayed for this media item row 417 */ 418 public MultiActionsProvider.MultiAction[] getMediaItemRowActions() { 419 return mMediaItemRowActions; 420 } 421 } 422 423 @Override 424 protected RowPresenter.ViewHolder createRowViewHolder(ViewGroup parent) { 425 Context context = parent.getContext(); 426 if (mThemeId != 0) { 427 context = new ContextThemeWrapper(context, mThemeId); 428 } 429 View view = LayoutInflater.from(context). 430 inflate(R.layout.lb_row_media_item, parent, false); 431 final ViewHolder vh = new ViewHolder(view); 432 vh.mRowPresenter = this; 433 if (mBackgroundColorSet) { 434 vh.mMediaRowView.setBackgroundColor(mBackgroundColor); 435 } 436 return vh; 437 } 438 439 @Override 440 public boolean isUsingDefaultSelectEffect() { 441 return false; 442 } 443 444 @Override 445 protected boolean isClippingChildren() { 446 return true; 447 } 448 449 @Override 450 protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) { 451 super.onBindRowViewHolder(vh, item); 452 453 final ViewHolder mvh = (ViewHolder) vh; 454 455 onBindRowActions(mvh); 456 457 mvh.getMediaItemRowSeparator().setVisibility(hasMediaRowSeparator() ? View.VISIBLE : 458 View.GONE); 459 460 onBindMediaPlayState(mvh); 461 onBindMediaDetails((ViewHolder) vh, item); 462 } 463 464 /** 465 * Binds the given media item object action to the given ViewHolder's action views. 466 * @param vh ViewHolder for the media item. 467 */ 468 protected void onBindRowActions(ViewHolder vh) { 469 vh.onBindRowActions(); 470 } 471 472 /** 473 * Sets the background color for the row views within the playlist. 474 * If this is not set, a default color, defaultBrandColor, from theme is used. 475 * This defaultBrandColor defaults to android:attr/colorPrimary on v21, if it's specified. 476 * @param color The ARGB color used to set as the media list background color. 477 */ 478 public void setBackgroundColor(int color) { 479 mBackgroundColorSet = true; 480 mBackgroundColor = color; 481 } 482 483 /** 484 * Specifies whether a line separator should be used between media item rows. 485 * @param hasSeparator true if a separator should be displayed, false otherwise. 486 */ 487 public void setHasMediaRowSeparator(boolean hasSeparator) { 488 mMediaRowSeparator = hasSeparator; 489 } 490 491 public boolean hasMediaRowSeparator() { 492 return mMediaRowSeparator; 493 } 494 /** 495 * Binds the media item details to their views provided by the 496 * {@link AbstractMediaItemPresenter}. 497 * This method is to be overridden by the users of this presenter. 498 * The subclasses of this presenter can access and bind individual views for either of the 499 * media item number, name, or duration (depending on whichever views are visible according to 500 * the providing theme attributes), by calling {@link ViewHolder#getMediaItemNumberView()}, 501 * {@link ViewHolder#getMediaItemNameView()}, and {@link ViewHolder#getMediaItemDurationView()}, 502 * on the {@link ViewHolder} provided as the argument {@code vh} of this presenter. 503 * 504 * @param vh The ViewHolder for this {@link AbstractMediaItemPresenter}. 505 * @param item The media item row object being presented. 506 */ 507 protected abstract void onBindMediaDetails(ViewHolder vh, Object item); 508 509 /** 510 * Unbinds the media item details from their views provided by the 511 * {@link AbstractMediaItemPresenter}. 512 * This method can be overridden by the subclasses of this presenter if required. 513 * @param vh ViewHolder to unbind from. 514 */ 515 protected void onUnbindMediaDetails(ViewHolder vh) { 516 } 517 518 /** 519 * Binds the media item number view to the appropriate play state view of the media item. 520 * The play state of the media item is extracted by calling {@link #getMediaPlayState(Object)} for 521 * the media item embedded within this view. 522 * This method triggers updating of the playback state UI if corresponding views are specified 523 * for the current playback state. 524 * By default, 3 views are provided for each playback state, or these views can be provided 525 * by the user. 526 */ 527 public void onBindMediaPlayState(ViewHolder vh) { 528 int childIndex = calculateMediaItemNumberFlipperIndex(vh); 529 if (childIndex != -1 && vh.mMediaItemNumberViewFlipper.getDisplayedChild() != childIndex) { 530 vh.mMediaItemNumberViewFlipper.setDisplayedChild(childIndex); 531 } 532 } 533 534 static int calculateMediaItemNumberFlipperIndex(ViewHolder vh) { 535 int childIndex = -1; 536 int newPlayState = vh.mRowPresenter.getMediaPlayState(vh.getRowObject()); 537 switch (newPlayState) { 538 case PLAY_STATE_INITIAL: 539 childIndex = (vh.mMediaItemNumberView == null) ? -1 : 540 vh.mMediaItemNumberViewFlipper.indexOfChild(vh.mMediaItemNumberView); 541 break; 542 case PLAY_STATE_PAUSED: 543 childIndex = (vh.mMediaItemPausedView == null) ? -1 : 544 vh.mMediaItemNumberViewFlipper.indexOfChild(vh.mMediaItemPausedView); 545 break; 546 case PLAY_STATE_PLAYING: 547 childIndex = (vh.mMediaItemPlayingView == null) ? -1 : 548 vh.mMediaItemNumberViewFlipper.indexOfChild(vh.mMediaItemPlayingView); 549 } 550 return childIndex; 551 } 552 553 /** 554 * Called when the given ViewHolder wants to unbind the play state view. 555 * @param vh The ViewHolder to unbind from. 556 */ 557 public void onUnbindMediaPlayState(ViewHolder vh) { 558 } 559 560 /** 561 * Returns the current play state of the given media item. By default, this method returns 562 * PLAY_STATE_INITIAL which causes the media item number 563 * {@link ViewHolder#getMediaItemNameView()} to be displayed for different 564 * playback states. Users of this class should override this method in order to provide the 565 * play state of their custom media item data model. 566 * @param item The media item 567 * @return The current play state of this media item 568 */ 569 protected int getMediaPlayState(Object item) { 570 return PLAY_STATE_INITIAL; 571 } 572 /** 573 * Each media item row can have multiple focusable elements; the details on the left and a set 574 * of optional custom actions on the right. 575 * The selector is a highlight that moves to highlight to cover whichever views is in focus. 576 * 577 * @param selectorView the selector view used to highlight an individual element within a row. 578 * @param focusChangedView The component within the media row whose focus got changed. 579 * @param layoutAnimator the ValueAnimator producing animation frames for the selector's width 580 * and x-translation, generated by this method and stored for the each 581 * {@link ViewHolder}. 582 * @param isDetails Whether the changed-focused view is for a media item details (true) or 583 * an action (false). 584 */ 585 private static ValueAnimator updateSelector(final View selectorView, 586 View focusChangedView, ValueAnimator layoutAnimator, boolean isDetails) { 587 int animationDuration = focusChangedView.getContext().getResources() 588 .getInteger(android.R.integer.config_shortAnimTime); 589 DecelerateInterpolator interpolator = new DecelerateInterpolator(); 590 591 int layoutDirection = ViewCompat.getLayoutDirection(selectorView); 592 if (!focusChangedView.hasFocus()) { 593 // if neither of the details or action views are in focus (ie. another row is in focus), 594 // animate the selector out. 595 selectorView.animate().cancel(); 596 selectorView.animate().alpha(0f).setDuration(animationDuration) 597 .setInterpolator(interpolator).start(); 598 // keep existing layout animator 599 return layoutAnimator; 600 } else { 601 // cancel existing layout animator 602 if (layoutAnimator != null) { 603 layoutAnimator.cancel(); 604 layoutAnimator = null; 605 } 606 float currentAlpha = selectorView.getAlpha(); 607 selectorView.animate().alpha(1f).setDuration(animationDuration) 608 .setInterpolator(interpolator).start(); 609 610 final ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) 611 selectorView.getLayoutParams(); 612 ViewGroup rootView = (ViewGroup) selectorView.getParent(); 613 sTempRect.set(0, 0, focusChangedView.getWidth(), focusChangedView.getHeight()); 614 rootView.offsetDescendantRectToMyCoords(focusChangedView, sTempRect); 615 if (isDetails) { 616 if (layoutDirection == View.LAYOUT_DIRECTION_RTL ) { 617 sTempRect.right += rootView.getHeight(); 618 sTempRect.left -= rootView.getHeight() / 2; 619 } else { 620 sTempRect.left -= rootView.getHeight(); 621 sTempRect.right += rootView.getHeight() / 2; 622 } 623 } 624 final int targetLeft = sTempRect.left; 625 final int targetWidth = sTempRect.width(); 626 final float deltaWidth = lp.width - targetWidth; 627 final float deltaLeft = lp.leftMargin - targetLeft; 628 629 if (deltaLeft == 0f && deltaWidth == 0f) 630 { 631 // no change needed 632 } else if (currentAlpha == 0f) { 633 // change selector to the proper width and marginLeft without animation. 634 lp.width = targetWidth; 635 lp.leftMargin = targetLeft; 636 selectorView.requestLayout(); 637 } else { 638 // animate the selector to the proper width and marginLeft. 639 layoutAnimator = ValueAnimator.ofFloat(0f, 1f); 640 layoutAnimator.setDuration(animationDuration); 641 layoutAnimator.setInterpolator(interpolator); 642 643 layoutAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 644 @Override 645 public void onAnimationUpdate(ValueAnimator valueAnimator) { 646 // Set width to the proper width for this animation step. 647 float fractionToEnd = 1f - valueAnimator.getAnimatedFraction(); 648 lp.leftMargin = Math.round(targetLeft + deltaLeft * fractionToEnd); 649 lp.width = Math.round(targetWidth + deltaWidth * fractionToEnd); 650 selectorView.requestLayout(); 651 } 652 }); 653 layoutAnimator.start(); 654 } 655 return layoutAnimator; 656 657 } 658 } 659} 660