1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package androidx.leanback.widget; 17 18 import android.content.Context; 19 import android.graphics.Bitmap; 20 import android.graphics.Color; 21 import android.os.Build; 22 import android.util.TypedValue; 23 import android.view.KeyEvent; 24 import android.view.LayoutInflater; 25 import android.view.View; 26 import android.view.ViewGroup; 27 import android.widget.ImageView; 28 import android.widget.TextView; 29 30 import androidx.annotation.ColorInt; 31 import androidx.leanback.R; 32 import androidx.leanback.widget.ControlBarPresenter.OnControlClickedListener; 33 import androidx.leanback.widget.ControlBarPresenter.OnControlSelectedListener; 34 35 import java.util.Arrays; 36 37 /** 38 * A PlaybackTransportRowPresenter renders a {@link PlaybackControlsRow} to display a 39 * series of playback control buttons. Typically this row will be the first row in a fragment 40 * such as the {@link androidx.leanback.app.PlaybackSupportFragment}. 41 * 42 * <p>The detailed description is rendered using a {@link Presenter} passed in 43 * {@link #setDescriptionPresenter(Presenter)}. This can be an instance of 44 * {@link AbstractDetailsDescriptionPresenter}. The application can access the 45 * detailed description ViewHolder from {@link ViewHolder#getDescriptionViewHolder()}. 46 * </p> 47 */ 48 public class PlaybackTransportRowPresenter extends PlaybackRowPresenter { 49 50 static class BoundData extends PlaybackControlsPresenter.BoundData { 51 ViewHolder mRowViewHolder; 52 } 53 54 /** 55 * A ViewHolder for the PlaybackControlsRow supporting seek UI. 56 */ 57 public class ViewHolder extends PlaybackRowPresenter.ViewHolder implements PlaybackSeekUi { 58 final Presenter.ViewHolder mDescriptionViewHolder; 59 final ImageView mImageView; 60 final ViewGroup mDescriptionDock; 61 final ViewGroup mControlsDock; 62 final ViewGroup mSecondaryControlsDock; 63 final TextView mTotalTime; 64 final TextView mCurrentTime; 65 final SeekBar mProgressBar; 66 final ThumbsBar mThumbsBar; 67 long mTotalTimeInMs = Long.MIN_VALUE; 68 long mCurrentTimeInMs = Long.MIN_VALUE; 69 long mSecondaryProgressInMs; 70 final StringBuilder mTempBuilder = new StringBuilder(); 71 ControlBarPresenter.ViewHolder mControlsVh; 72 ControlBarPresenter.ViewHolder mSecondaryControlsVh; 73 BoundData mControlsBoundData = new BoundData(); 74 BoundData mSecondaryBoundData = new BoundData(); 75 Presenter.ViewHolder mSelectedViewHolder; 76 Object mSelectedItem; 77 PlaybackControlsRow.PlayPauseAction mPlayPauseAction; 78 int mThumbHeroIndex = -1; 79 80 Client mSeekClient; 81 boolean mInSeek; 82 PlaybackSeekDataProvider mSeekDataProvider; 83 long[] mPositions; 84 int mPositionsLength; 85 86 final PlaybackControlsRow.OnPlaybackProgressCallback mListener = 87 new PlaybackControlsRow.OnPlaybackProgressCallback() { 88 @Override 89 public void onCurrentPositionChanged(PlaybackControlsRow row, long ms) { 90 setCurrentPosition(ms); 91 } 92 93 @Override 94 public void onDurationChanged(PlaybackControlsRow row, long ms) { 95 setTotalTime(ms); 96 } 97 98 @Override 99 public void onBufferedPositionChanged(PlaybackControlsRow row, long ms) { 100 setBufferedPosition(ms); 101 } 102 }; 103 updateProgressInSeek(boolean forward)104 void updateProgressInSeek(boolean forward) { 105 long newPos; 106 long pos = mCurrentTimeInMs; 107 if (mPositionsLength > 0) { 108 int index = Arrays.binarySearch(mPositions, 0, mPositionsLength, pos); 109 int thumbHeroIndex; 110 if (forward) { 111 if (index >= 0) { 112 // found it, seek to neighbour key position at higher side 113 if (index < mPositionsLength - 1) { 114 newPos = mPositions[index + 1]; 115 thumbHeroIndex = index + 1; 116 } else { 117 newPos = mTotalTimeInMs; 118 thumbHeroIndex = index; 119 } 120 } else { 121 // not found, seek to neighbour key position at higher side. 122 int insertIndex = -1 - index; 123 if (insertIndex <= mPositionsLength - 1) { 124 newPos = mPositions[insertIndex]; 125 thumbHeroIndex = insertIndex; 126 } else { 127 newPos = mTotalTimeInMs; 128 thumbHeroIndex = insertIndex > 0 ? insertIndex - 1 : 0; 129 } 130 } 131 } else { 132 if (index >= 0) { 133 // found it, seek to neighbour key position at lower side. 134 if (index > 0) { 135 newPos = mPositions[index - 1]; 136 thumbHeroIndex = index - 1; 137 } else { 138 newPos = 0; 139 thumbHeroIndex = 0; 140 } 141 } else { 142 // not found, seek to neighbour key position at lower side. 143 int insertIndex = -1 - index; 144 if (insertIndex > 0) { 145 newPos = mPositions[insertIndex - 1]; 146 thumbHeroIndex = insertIndex - 1; 147 } else { 148 newPos = 0; 149 thumbHeroIndex = 0; 150 } 151 } 152 } 153 updateThumbsInSeek(thumbHeroIndex, forward); 154 } else { 155 long interval = (long) (mTotalTimeInMs * getDefaultSeekIncrement()); 156 newPos = pos + (forward ? interval : -interval); 157 if (newPos > mTotalTimeInMs) { 158 newPos = mTotalTimeInMs; 159 } else if (newPos < 0) { 160 newPos = 0; 161 } 162 } 163 double ratio = (double) newPos / mTotalTimeInMs; // Range: [0, 1] 164 mProgressBar.setProgress((int) (ratio * Integer.MAX_VALUE)); // Could safely cast to int 165 mSeekClient.onSeekPositionChanged(newPos); 166 } 167 updateThumbsInSeek(int thumbHeroIndex, boolean forward)168 void updateThumbsInSeek(int thumbHeroIndex, boolean forward) { 169 if (mThumbHeroIndex == thumbHeroIndex) { 170 return; 171 } 172 173 final int totalNum = mThumbsBar.getChildCount(); 174 if (totalNum < 0 || (totalNum & 1) == 0) { 175 throw new RuntimeException(); 176 } 177 final int heroChildIndex = totalNum / 2; 178 final int start = Math.max(thumbHeroIndex - (totalNum / 2), 0); 179 final int end = Math.min(thumbHeroIndex + (totalNum / 2), mPositionsLength - 1); 180 final int newRequestStart; 181 final int newRequestEnd; 182 183 if (mThumbHeroIndex < 0) { 184 // first time 185 newRequestStart = start; 186 newRequestEnd = end; 187 } else { 188 forward = thumbHeroIndex > mThumbHeroIndex; 189 final int oldStart = Math.max(mThumbHeroIndex - (totalNum / 2), 0); 190 final int oldEnd = Math.min(mThumbHeroIndex + (totalNum / 2), 191 mPositionsLength - 1); 192 if (forward) { 193 newRequestStart = Math.max(oldEnd + 1, start); 194 newRequestEnd = end; 195 // overlapping area directly assign bitmap from previous result 196 for (int i = start; i <= newRequestStart - 1; i++) { 197 mThumbsBar.setThumbBitmap(heroChildIndex + (i - thumbHeroIndex), 198 mThumbsBar.getThumbBitmap(heroChildIndex + (i - mThumbHeroIndex))); 199 } 200 } else { 201 newRequestEnd = Math.min(oldStart - 1, end); 202 newRequestStart = start; 203 // overlapping area directly assign bitmap from previous result in backward 204 for (int i = end; i >= newRequestEnd + 1; i--) { 205 mThumbsBar.setThumbBitmap(heroChildIndex + (i - thumbHeroIndex), 206 mThumbsBar.getThumbBitmap(heroChildIndex + (i - mThumbHeroIndex))); 207 } 208 } 209 } 210 // processing new requests with mThumbHeroIndex updated 211 mThumbHeroIndex = thumbHeroIndex; 212 if (forward) { 213 for (int i = newRequestStart; i <= newRequestEnd; i++) { 214 mSeekDataProvider.getThumbnail(i, mThumbResult); 215 } 216 } else { 217 for (int i = newRequestEnd; i >= newRequestStart; i--) { 218 mSeekDataProvider.getThumbnail(i, mThumbResult); 219 } 220 } 221 // set thumb bitmaps outside (start , end) to null 222 for (int childIndex = 0; childIndex < heroChildIndex - mThumbHeroIndex + start; 223 childIndex++) { 224 mThumbsBar.setThumbBitmap(childIndex, null); 225 } 226 for (int childIndex = heroChildIndex + end - mThumbHeroIndex + 1; 227 childIndex < totalNum; childIndex++) { 228 mThumbsBar.setThumbBitmap(childIndex, null); 229 } 230 } 231 232 PlaybackSeekDataProvider.ResultCallback mThumbResult = 233 new PlaybackSeekDataProvider.ResultCallback() { 234 @Override 235 public void onThumbnailLoaded(Bitmap bitmap, int index) { 236 int childIndex = index - (mThumbHeroIndex - mThumbsBar.getChildCount() / 2); 237 if (childIndex < 0 || childIndex >= mThumbsBar.getChildCount()) { 238 return; 239 } 240 mThumbsBar.setThumbBitmap(childIndex, bitmap); 241 } 242 }; 243 onForward()244 boolean onForward() { 245 if (!startSeek()) { 246 return false; 247 } 248 updateProgressInSeek(true); 249 return true; 250 } 251 onBackward()252 boolean onBackward() { 253 if (!startSeek()) { 254 return false; 255 } 256 updateProgressInSeek(false); 257 return true; 258 } 259 /** 260 * Constructor of ViewHolder of PlaybackTransportRowPresenter 261 * @param rootView Root view of the ViewHolder. 262 * @param descriptionPresenter The presenter that will be used to create description 263 * ViewHolder. The description view will be added into tree. 264 */ ViewHolder(View rootView, Presenter descriptionPresenter)265 public ViewHolder(View rootView, Presenter descriptionPresenter) { 266 super(rootView); 267 mImageView = (ImageView) rootView.findViewById(R.id.image); 268 mDescriptionDock = (ViewGroup) rootView.findViewById(R.id.description_dock); 269 mCurrentTime = (TextView) rootView.findViewById(R.id.current_time); 270 mTotalTime = (TextView) rootView.findViewById(R.id.total_time); 271 mProgressBar = (SeekBar) rootView.findViewById(R.id.playback_progress); 272 mProgressBar.setOnClickListener(new View.OnClickListener() { 273 @Override 274 public void onClick(View view) { 275 onProgressBarClicked(ViewHolder.this); 276 } 277 }); 278 mProgressBar.setOnKeyListener(new View.OnKeyListener() { 279 280 @Override 281 public boolean onKey(View view, int keyCode, KeyEvent keyEvent) { 282 // when in seek only allow this keys 283 switch (keyCode) { 284 case KeyEvent.KEYCODE_DPAD_UP: 285 case KeyEvent.KEYCODE_DPAD_DOWN: 286 // eat DPAD UP/DOWN in seek mode 287 return mInSeek; 288 case KeyEvent.KEYCODE_DPAD_LEFT: 289 case KeyEvent.KEYCODE_MINUS: 290 case KeyEvent.KEYCODE_MEDIA_REWIND: 291 if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) { 292 onBackward(); 293 } 294 return true; 295 case KeyEvent.KEYCODE_DPAD_RIGHT: 296 case KeyEvent.KEYCODE_PLUS: 297 case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: 298 if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) { 299 onForward(); 300 } 301 return true; 302 case KeyEvent.KEYCODE_DPAD_CENTER: 303 case KeyEvent.KEYCODE_ENTER: 304 if (!mInSeek) { 305 return false; 306 } 307 if (keyEvent.getAction() == KeyEvent.ACTION_UP) { 308 stopSeek(false); 309 } 310 return true; 311 case KeyEvent.KEYCODE_BACK: 312 case KeyEvent.KEYCODE_ESCAPE: 313 if (!mInSeek) { 314 return false; 315 } 316 if (keyEvent.getAction() == KeyEvent.ACTION_UP) { 317 // SeekBar does not support cancel in accessibility mode, so always 318 // "confirm" if accessibility is on. 319 stopSeek(Build.VERSION.SDK_INT >= 21 320 ? !mProgressBar.isAccessibilityFocused() : true); 321 } 322 return true; 323 } 324 return false; 325 } 326 }); 327 mProgressBar.setAccessibilitySeekListener(new SeekBar.AccessibilitySeekListener() { 328 @Override 329 public boolean onAccessibilitySeekForward() { 330 return onForward(); 331 } 332 333 @Override 334 public boolean onAccessibilitySeekBackward() { 335 return onBackward(); 336 } 337 }); 338 mProgressBar.setMax(Integer.MAX_VALUE); //current progress will be a fraction of this 339 mControlsDock = (ViewGroup) rootView.findViewById(R.id.controls_dock); 340 mSecondaryControlsDock = 341 (ViewGroup) rootView.findViewById(R.id.secondary_controls_dock); 342 mDescriptionViewHolder = descriptionPresenter == null ? null : 343 descriptionPresenter.onCreateViewHolder(mDescriptionDock); 344 if (mDescriptionViewHolder != null) { 345 mDescriptionDock.addView(mDescriptionViewHolder.view); 346 } 347 mThumbsBar = (ThumbsBar) rootView.findViewById(R.id.thumbs_row); 348 } 349 350 /** 351 * @return The ViewHolder for description. 352 */ getDescriptionViewHolder()353 public final Presenter.ViewHolder getDescriptionViewHolder() { 354 return mDescriptionViewHolder; 355 } 356 357 @Override setPlaybackSeekUiClient(Client client)358 public void setPlaybackSeekUiClient(Client client) { 359 mSeekClient = client; 360 } 361 startSeek()362 boolean startSeek() { 363 if (mInSeek) { 364 return true; 365 } 366 if (mSeekClient == null || !mSeekClient.isSeekEnabled() 367 || mTotalTimeInMs <= 0) { 368 return false; 369 } 370 mInSeek = true; 371 mSeekClient.onSeekStarted(); 372 mSeekDataProvider = mSeekClient.getPlaybackSeekDataProvider(); 373 mPositions = mSeekDataProvider != null ? mSeekDataProvider.getSeekPositions() : null; 374 if (mPositions != null) { 375 int pos = Arrays.binarySearch(mPositions, mTotalTimeInMs); 376 if (pos >= 0) { 377 mPositionsLength = pos + 1; 378 } else { 379 mPositionsLength = -1 - pos; 380 } 381 } else { 382 mPositionsLength = 0; 383 } 384 mControlsVh.view.setVisibility(View.GONE); 385 mSecondaryControlsVh.view.setVisibility(View.INVISIBLE); 386 mDescriptionViewHolder.view.setVisibility(View.INVISIBLE); 387 mThumbsBar.setVisibility(View.VISIBLE); 388 return true; 389 } 390 stopSeek(boolean cancelled)391 void stopSeek(boolean cancelled) { 392 if (!mInSeek) { 393 return; 394 } 395 mInSeek = false; 396 mSeekClient.onSeekFinished(cancelled); 397 if (mSeekDataProvider != null) { 398 mSeekDataProvider.reset(); 399 } 400 mThumbHeroIndex = -1; 401 mThumbsBar.clearThumbBitmaps(); 402 mSeekDataProvider = null; 403 mPositions = null; 404 mPositionsLength = 0; 405 mControlsVh.view.setVisibility(View.VISIBLE); 406 mSecondaryControlsVh.view.setVisibility(View.VISIBLE); 407 mDescriptionViewHolder.view.setVisibility(View.VISIBLE); 408 mThumbsBar.setVisibility(View.INVISIBLE); 409 } 410 dispatchItemSelection()411 void dispatchItemSelection() { 412 if (!isSelected()) { 413 return; 414 } 415 if (mSelectedViewHolder == null) { 416 if (getOnItemViewSelectedListener() != null) { 417 getOnItemViewSelectedListener().onItemSelected(null, null, 418 ViewHolder.this, getRow()); 419 } 420 } else { 421 if (getOnItemViewSelectedListener() != null) { 422 getOnItemViewSelectedListener().onItemSelected(mSelectedViewHolder, 423 mSelectedItem, ViewHolder.this, getRow()); 424 } 425 } 426 }; 427 getPresenter(boolean primary)428 Presenter getPresenter(boolean primary) { 429 ObjectAdapter adapter = primary 430 ? ((PlaybackControlsRow) getRow()).getPrimaryActionsAdapter() 431 : ((PlaybackControlsRow) getRow()).getSecondaryActionsAdapter(); 432 if (adapter == null) { 433 return null; 434 } 435 if (adapter.getPresenterSelector() instanceof ControlButtonPresenterSelector) { 436 ControlButtonPresenterSelector selector = 437 (ControlButtonPresenterSelector) adapter.getPresenterSelector(); 438 return selector.getSecondaryPresenter(); 439 } 440 return adapter.getPresenter(adapter.size() > 0 ? adapter.get(0) : null); 441 } 442 443 /** 444 * Returns the TextView that showing total time label. This method might be used in 445 * {@link #onSetDurationLabel}. 446 * @return The TextView that showing total time label. 447 */ getDurationView()448 public final TextView getDurationView() { 449 return mTotalTime; 450 } 451 452 /** 453 * Called to update total time label. Default implementation updates the TextView 454 * {@link #getDurationView()}. Subclass might override. 455 * @param totalTimeMs Total duration of the media in milliseconds. 456 */ onSetDurationLabel(long totalTimeMs)457 protected void onSetDurationLabel(long totalTimeMs) { 458 if (mTotalTime != null) { 459 formatTime(totalTimeMs, mTempBuilder); 460 mTotalTime.setText(mTempBuilder.toString()); 461 } 462 } 463 setTotalTime(long totalTimeMs)464 void setTotalTime(long totalTimeMs) { 465 if (mTotalTimeInMs != totalTimeMs) { 466 mTotalTimeInMs = totalTimeMs; 467 onSetDurationLabel(totalTimeMs); 468 } 469 } 470 471 /** 472 * Returns the TextView that showing current position label. This method might be used in 473 * {@link #onSetCurrentPositionLabel}. 474 * @return The TextView that showing current position label. 475 */ getCurrentPositionView()476 public final TextView getCurrentPositionView() { 477 return mCurrentTime; 478 } 479 480 /** 481 * Called to update current time label. Default implementation updates the TextView 482 * {@link #getCurrentPositionView}. Subclass might override. 483 * @param currentTimeMs Current playback position in milliseconds. 484 */ onSetCurrentPositionLabel(long currentTimeMs)485 protected void onSetCurrentPositionLabel(long currentTimeMs) { 486 if (mCurrentTime != null) { 487 formatTime(currentTimeMs, mTempBuilder); 488 mCurrentTime.setText(mTempBuilder.toString()); 489 } 490 } 491 setCurrentPosition(long currentTimeMs)492 void setCurrentPosition(long currentTimeMs) { 493 if (currentTimeMs != mCurrentTimeInMs) { 494 mCurrentTimeInMs = currentTimeMs; 495 onSetCurrentPositionLabel(currentTimeMs); 496 } 497 if (!mInSeek) { 498 int progressRatio = 0; 499 if (mTotalTimeInMs > 0) { 500 // Use ratio to represent current progres 501 double ratio = (double) mCurrentTimeInMs / mTotalTimeInMs; // Range: [0, 1] 502 progressRatio = (int) (ratio * Integer.MAX_VALUE); // Could safely cast to int 503 } 504 mProgressBar.setProgress((int) progressRatio); 505 } 506 } 507 setBufferedPosition(long progressMs)508 void setBufferedPosition(long progressMs) { 509 mSecondaryProgressInMs = progressMs; 510 // Solve the progress bar by using ratio 511 double ratio = (double) progressMs / mTotalTimeInMs; // Range: [0, 1] 512 double progressRatio = ratio * Integer.MAX_VALUE; // Could safely cast to int 513 mProgressBar.setSecondaryProgress((int) progressRatio); 514 } 515 } 516 formatTime(long ms, StringBuilder sb)517 static void formatTime(long ms, StringBuilder sb) { 518 sb.setLength(0); 519 if (ms < 0) { 520 sb.append("--"); 521 return; 522 } 523 long seconds = ms / 1000; 524 long minutes = seconds / 60; 525 long hours = minutes / 60; 526 seconds -= minutes * 60; 527 minutes -= hours * 60; 528 529 if (hours > 0) { 530 sb.append(hours).append(':'); 531 if (minutes < 10) { 532 sb.append('0'); 533 } 534 } 535 sb.append(minutes).append(':'); 536 if (seconds < 10) { 537 sb.append('0'); 538 } 539 sb.append(seconds); 540 } 541 542 float mDefaultSeekIncrement = 0.01f; 543 int mProgressColor = Color.TRANSPARENT; 544 boolean mProgressColorSet; 545 Presenter mDescriptionPresenter; 546 ControlBarPresenter mPlaybackControlsPresenter; 547 ControlBarPresenter mSecondaryControlsPresenter; 548 OnActionClickedListener mOnActionClickedListener; 549 550 private final OnControlSelectedListener mOnControlSelectedListener = 551 new OnControlSelectedListener() { 552 @Override 553 public void onControlSelected(Presenter.ViewHolder itemViewHolder, Object item, 554 ControlBarPresenter.BoundData data) { 555 ViewHolder vh = ((BoundData) data).mRowViewHolder; 556 if (vh.mSelectedViewHolder != itemViewHolder || vh.mSelectedItem != item) { 557 vh.mSelectedViewHolder = itemViewHolder; 558 vh.mSelectedItem = item; 559 vh.dispatchItemSelection(); 560 } 561 } 562 }; 563 564 private final OnControlClickedListener mOnControlClickedListener = 565 new OnControlClickedListener() { 566 @Override 567 public void onControlClicked(Presenter.ViewHolder itemViewHolder, Object item, 568 ControlBarPresenter.BoundData data) { 569 ViewHolder vh = ((BoundData) data).mRowViewHolder; 570 if (vh.getOnItemViewClickedListener() != null) { 571 vh.getOnItemViewClickedListener().onItemClicked(itemViewHolder, item, 572 vh, vh.getRow()); 573 } 574 if (mOnActionClickedListener != null && item instanceof Action) { 575 mOnActionClickedListener.onActionClicked((Action) item); 576 } 577 } 578 }; 579 PlaybackTransportRowPresenter()580 public PlaybackTransportRowPresenter() { 581 setHeaderPresenter(null); 582 setSelectEffectEnabled(false); 583 584 mPlaybackControlsPresenter = new ControlBarPresenter(R.layout.lb_control_bar); 585 mPlaybackControlsPresenter.setDefaultFocusToMiddle(false); 586 mSecondaryControlsPresenter = new ControlBarPresenter(R.layout.lb_control_bar); 587 mSecondaryControlsPresenter.setDefaultFocusToMiddle(false); 588 589 mPlaybackControlsPresenter.setOnControlSelectedListener(mOnControlSelectedListener); 590 mSecondaryControlsPresenter.setOnControlSelectedListener(mOnControlSelectedListener); 591 mPlaybackControlsPresenter.setOnControlClickedListener(mOnControlClickedListener); 592 mSecondaryControlsPresenter.setOnControlClickedListener(mOnControlClickedListener); 593 } 594 595 /** 596 * @param descriptionPresenter Presenter for displaying item details. 597 */ setDescriptionPresenter(Presenter descriptionPresenter)598 public void setDescriptionPresenter(Presenter descriptionPresenter) { 599 mDescriptionPresenter = descriptionPresenter; 600 } 601 602 /** 603 * Sets the listener for {@link Action} click events. 604 */ setOnActionClickedListener(OnActionClickedListener listener)605 public void setOnActionClickedListener(OnActionClickedListener listener) { 606 mOnActionClickedListener = listener; 607 } 608 609 /** 610 * Returns the listener for {@link Action} click events. 611 */ getOnActionClickedListener()612 public OnActionClickedListener getOnActionClickedListener() { 613 return mOnActionClickedListener; 614 } 615 616 /** 617 * Sets the primary color for the progress bar. If not set, a default from 618 * the theme will be used. 619 */ setProgressColor(@olorInt int color)620 public void setProgressColor(@ColorInt int color) { 621 mProgressColor = color; 622 mProgressColorSet = true; 623 } 624 625 /** 626 * Returns the primary color for the progress bar. If no color was set, transparent 627 * is returned. 628 */ 629 @ColorInt getProgressColor()630 public int getProgressColor() { 631 return mProgressColor; 632 } 633 634 @Override onReappear(RowPresenter.ViewHolder rowViewHolder)635 public void onReappear(RowPresenter.ViewHolder rowViewHolder) { 636 ViewHolder vh = (ViewHolder) rowViewHolder; 637 if (vh.view.hasFocus()) { 638 vh.mProgressBar.requestFocus(); 639 } 640 } 641 getDefaultProgressColor(Context context)642 private int getDefaultProgressColor(Context context) { 643 TypedValue outValue = new TypedValue(); 644 if (context.getTheme() 645 .resolveAttribute(R.attr.playbackProgressPrimaryColor, outValue, true)) { 646 return context.getResources().getColor(outValue.resourceId); 647 } 648 return context.getResources().getColor(R.color.lb_playback_progress_color_no_theme); 649 } 650 651 @Override createRowViewHolder(ViewGroup parent)652 protected RowPresenter.ViewHolder createRowViewHolder(ViewGroup parent) { 653 View v = LayoutInflater.from(parent.getContext()).inflate( 654 R.layout.lb_playback_transport_controls_row, parent, false); 655 ViewHolder vh = new ViewHolder(v, mDescriptionPresenter); 656 initRow(vh); 657 return vh; 658 } 659 initRow(final ViewHolder vh)660 private void initRow(final ViewHolder vh) { 661 vh.mControlsVh = (ControlBarPresenter.ViewHolder) mPlaybackControlsPresenter 662 .onCreateViewHolder(vh.mControlsDock); 663 vh.mProgressBar.setProgressColor(mProgressColorSet ? mProgressColor 664 : getDefaultProgressColor(vh.mControlsDock.getContext())); 665 vh.mControlsDock.addView(vh.mControlsVh.view); 666 667 vh.mSecondaryControlsVh = (ControlBarPresenter.ViewHolder) mSecondaryControlsPresenter 668 .onCreateViewHolder(vh.mSecondaryControlsDock); 669 vh.mSecondaryControlsDock.addView(vh.mSecondaryControlsVh.view); 670 ((PlaybackTransportRowView) vh.view.findViewById(R.id.transport_row)) 671 .setOnUnhandledKeyListener(new PlaybackTransportRowView.OnUnhandledKeyListener() { 672 @Override 673 public boolean onUnhandledKey(KeyEvent event) { 674 if (vh.getOnKeyListener() != null) { 675 if (vh.getOnKeyListener().onKey(vh.view, event.getKeyCode(), event)) { 676 return true; 677 } 678 } 679 return false; 680 } 681 }); 682 } 683 684 @Override onBindRowViewHolder(RowPresenter.ViewHolder holder, Object item)685 protected void onBindRowViewHolder(RowPresenter.ViewHolder holder, Object item) { 686 super.onBindRowViewHolder(holder, item); 687 688 ViewHolder vh = (ViewHolder) holder; 689 PlaybackControlsRow row = (PlaybackControlsRow) vh.getRow(); 690 691 if (row.getItem() == null) { 692 vh.mDescriptionDock.setVisibility(View.GONE); 693 } else { 694 vh.mDescriptionDock.setVisibility(View.VISIBLE); 695 if (vh.mDescriptionViewHolder != null) { 696 mDescriptionPresenter.onBindViewHolder(vh.mDescriptionViewHolder, row.getItem()); 697 } 698 } 699 700 if (row.getImageDrawable() == null) { 701 vh.mImageView.setVisibility(View.GONE); 702 } else { 703 vh.mImageView.setVisibility(View.VISIBLE); 704 } 705 vh.mImageView.setImageDrawable(row.getImageDrawable()); 706 707 vh.mControlsBoundData.adapter = row.getPrimaryActionsAdapter(); 708 vh.mControlsBoundData.presenter = vh.getPresenter(true); 709 vh.mControlsBoundData.mRowViewHolder = vh; 710 mPlaybackControlsPresenter.onBindViewHolder(vh.mControlsVh, vh.mControlsBoundData); 711 712 vh.mSecondaryBoundData.adapter = row.getSecondaryActionsAdapter(); 713 vh.mSecondaryBoundData.presenter = vh.getPresenter(false); 714 vh.mSecondaryBoundData.mRowViewHolder = vh; 715 mSecondaryControlsPresenter.onBindViewHolder(vh.mSecondaryControlsVh, 716 vh.mSecondaryBoundData); 717 718 vh.setTotalTime(row.getDuration()); 719 vh.setCurrentPosition(row.getCurrentPosition()); 720 vh.setBufferedPosition(row.getBufferedPosition()); 721 row.setOnPlaybackProgressChangedListener(vh.mListener); 722 } 723 724 @Override onUnbindRowViewHolder(RowPresenter.ViewHolder holder)725 protected void onUnbindRowViewHolder(RowPresenter.ViewHolder holder) { 726 ViewHolder vh = (ViewHolder) holder; 727 PlaybackControlsRow row = (PlaybackControlsRow) vh.getRow(); 728 729 if (vh.mDescriptionViewHolder != null) { 730 mDescriptionPresenter.onUnbindViewHolder(vh.mDescriptionViewHolder); 731 } 732 mPlaybackControlsPresenter.onUnbindViewHolder(vh.mControlsVh); 733 mSecondaryControlsPresenter.onUnbindViewHolder(vh.mSecondaryControlsVh); 734 row.setOnPlaybackProgressChangedListener(null); 735 736 super.onUnbindRowViewHolder(holder); 737 } 738 739 /** 740 * Client of progress bar is clicked, default implementation delegate click to 741 * PlayPauseAction. 742 * 743 * @param vh ViewHolder of PlaybackTransportRowPresenter 744 */ onProgressBarClicked(ViewHolder vh)745 protected void onProgressBarClicked(ViewHolder vh) { 746 if (vh != null) { 747 if (vh.mPlayPauseAction == null) { 748 vh.mPlayPauseAction = new PlaybackControlsRow.PlayPauseAction(vh.view.getContext()); 749 } 750 if (vh.getOnItemViewClickedListener() != null) { 751 vh.getOnItemViewClickedListener().onItemClicked(vh, vh.mPlayPauseAction, 752 vh, vh.getRow()); 753 } 754 if (mOnActionClickedListener != null) { 755 mOnActionClickedListener.onActionClicked(vh.mPlayPauseAction); 756 } 757 } 758 } 759 760 /** 761 * Set default seek increment if {@link PlaybackSeekDataProvider} is null. 762 * @param ratio float value between 0(inclusive) and 1(inclusive). 763 */ setDefaultSeekIncrement(float ratio)764 public void setDefaultSeekIncrement(float ratio) { 765 mDefaultSeekIncrement = ratio; 766 } 767 768 /** 769 * Get default seek increment if {@link PlaybackSeekDataProvider} is null. 770 * @return float value between 0(inclusive) and 1(inclusive). 771 */ getDefaultSeekIncrement()772 public float getDefaultSeekIncrement() { 773 return mDefaultSeekIncrement; 774 } 775 776 @Override onRowViewSelected(RowPresenter.ViewHolder vh, boolean selected)777 protected void onRowViewSelected(RowPresenter.ViewHolder vh, boolean selected) { 778 super.onRowViewSelected(vh, selected); 779 if (selected) { 780 ((ViewHolder) vh).dispatchItemSelection(); 781 } 782 } 783 784 @Override onRowViewAttachedToWindow(RowPresenter.ViewHolder vh)785 protected void onRowViewAttachedToWindow(RowPresenter.ViewHolder vh) { 786 super.onRowViewAttachedToWindow(vh); 787 if (mDescriptionPresenter != null) { 788 mDescriptionPresenter.onViewAttachedToWindow( 789 ((ViewHolder) vh).mDescriptionViewHolder); 790 } 791 } 792 793 @Override onRowViewDetachedFromWindow(RowPresenter.ViewHolder vh)794 protected void onRowViewDetachedFromWindow(RowPresenter.ViewHolder vh) { 795 super.onRowViewDetachedFromWindow(vh); 796 if (mDescriptionPresenter != null) { 797 mDescriptionPresenter.onViewDetachedFromWindow( 798 ((ViewHolder) vh).mDescriptionViewHolder); 799 } 800 } 801 802 } 803