PlayControlsRowView.java revision 816a4be1a0f34f6a48877c8afd3dbbca19eac435
1/* 2 * Copyright (C) 2015 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 17package com.android.tv.menu; 18 19import android.content.Context; 20import android.content.res.Resources; 21import android.text.format.DateFormat; 22import android.util.AttributeSet; 23import android.view.View; 24import android.view.ViewGroup; 25import android.widget.TextView; 26 27import com.android.tv.R; 28import com.android.tv.TimeShiftManager; 29import com.android.tv.TimeShiftManager.TimeShiftActionId; 30import com.android.tv.data.Program; 31import com.android.tv.menu.MenuView.MenuShowReason; 32 33public class PlayControlsRowView extends MenuRowView { 34 // Dimensions 35 private final int mTimeIndicatorLeftMargin; 36 private final int mTimeTextLeftMargin; 37 private final int mTimelineWidth; 38 // Views 39 private View mTitleView; 40 private View mBackgroundView; 41 private View mTimeIndicator; 42 private TextView mTimeText; 43 private View mProgressEmptyBefore; 44 private View mProgressWatched; 45 private View mProgressBuffered; 46 private View mProgressEmptyAfter; 47 private View mControlBar; 48 private PlayControlsButton mJumpPreviousButton; 49 private PlayControlsButton mRewindButton; 50 private PlayControlsButton mPlayPauseButton; 51 private PlayControlsButton mFastForwardButton; 52 private PlayControlsButton mJumpNextButton; 53 private TextView mProgramStartTimeText; 54 private TextView mProgramEndTimeText; 55 private View mUnavailableMessageText; 56 private TimeShiftManager mTimeShiftManager; 57 58 private long mProgramStartTimeMs; 59 private long mProgramEndTimeMs; 60 61 public PlayControlsRowView(Context context) { 62 this(context, null); 63 } 64 65 public PlayControlsRowView(Context context, AttributeSet attrs) { 66 this(context, attrs, 0); 67 } 68 69 public PlayControlsRowView(Context context, AttributeSet attrs, int defStyleAttr) { 70 this(context, attrs, defStyleAttr, 0); 71 } 72 73 public PlayControlsRowView(Context context, AttributeSet attrs, int defStyleAttr, 74 int defStyleRes) { 75 super(context, attrs, defStyleAttr, defStyleRes); 76 Resources res = context.getResources(); 77 mTimeIndicatorLeftMargin = 78 - res.getDimensionPixelSize(R.dimen.play_controls_time_indicator_width) / 2; 79 mTimeTextLeftMargin = 80 - res.getDimensionPixelOffset(R.dimen.play_controls_time_width) / 2; 81 mTimelineWidth = res.getDimensionPixelSize(R.dimen.play_controls_width); 82 } 83 84 @Override 85 protected int getContentsViewId() { 86 return R.id.play_controls; 87 } 88 89 @Override 90 protected void onFinishInflate() { 91 super.onFinishInflate(); 92 // Clip the ViewGroup(body) to the rounded rectangle of outline. 93 findViewById(R.id.body).setClipToOutline(true); 94 mTitleView = findViewById(R.id.title); 95 mBackgroundView = findViewById(R.id.background); 96 mTimeIndicator = findViewById(R.id.time_indicator); 97 mTimeText = (TextView) findViewById(R.id.time_text); 98 mProgressEmptyBefore = findViewById(R.id.timeline_bg_start); 99 mProgressWatched = findViewById(R.id.watched); 100 mProgressBuffered = findViewById(R.id.buffered); 101 mProgressEmptyAfter = findViewById(R.id.timeline_bg_end); 102 mControlBar = findViewById(R.id.play_control_bar); 103 mJumpPreviousButton = (PlayControlsButton) findViewById(R.id.jump_previous); 104 mRewindButton = (PlayControlsButton) findViewById(R.id.rewind); 105 mPlayPauseButton = (PlayControlsButton) findViewById(R.id.play_pause); 106 mFastForwardButton = (PlayControlsButton) findViewById(R.id.fast_forward); 107 mJumpNextButton = (PlayControlsButton) findViewById(R.id.jump_next); 108 mProgramStartTimeText = (TextView) findViewById(R.id.program_start_time); 109 mProgramEndTimeText = (TextView) findViewById(R.id.program_end_time); 110 mUnavailableMessageText = findViewById(R.id.unavailable_text); 111 112 initializeButton(mJumpPreviousButton, R.drawable.lb_ic_skip_previous, 113 R.string.play_controls_description_skip_previous, new Runnable() { 114 @Override 115 public void run() { 116 if (mTimeShiftManager.isAvailable()) { 117 mTimeShiftManager.jumpToPrevious(); 118 updateAll(); 119 } 120 } 121 }); 122 initializeButton(mRewindButton, R.drawable.lb_ic_fast_rewind, 123 R.string.play_controls_description_fast_rewind, new Runnable() { 124 @Override 125 public void run() { 126 if (mTimeShiftManager.isAvailable()) { 127 mTimeShiftManager.rewind(); 128 updateButtons(); 129 } 130 } 131 }); 132 initializeButton(mPlayPauseButton, R.drawable.lb_ic_play, 133 R.string.play_controls_description_play_pause, new Runnable() { 134 @Override 135 public void run() { 136 if (mTimeShiftManager.isAvailable()) { 137 mTimeShiftManager.togglePlayPause(); 138 updateButtons(); 139 } 140 } 141 }); 142 initializeButton(mFastForwardButton, R.drawable.lb_ic_fast_forward, 143 R.string.play_controls_description_fast_forward, new Runnable() { 144 @Override 145 public void run() { 146 if (mTimeShiftManager.isAvailable()) { 147 mTimeShiftManager.fastForward(); 148 updateButtons(); 149 } 150 } 151 }); 152 initializeButton(mJumpNextButton, R.drawable.lb_ic_skip_next, 153 R.string.play_controls_description_skip_next, new Runnable() { 154 @Override 155 public void run() { 156 if (mTimeShiftManager.isAvailable()) { 157 mTimeShiftManager.jumpToNext(); 158 updateAll(); 159 } 160 } 161 }); 162 changeFocusableForDescendents(false); 163 } 164 165 private void changeFocusableForDescendents(boolean focusable) { 166 setFocusable(focusable); 167 setDescendantFocusability(focusable ? FOCUS_AFTER_DESCENDANTS : FOCUS_BLOCK_DESCENDANTS); 168 } 169 170 private void setRowEnable(boolean enable) { 171 setEnabled(enable); 172 changeFocusableForDescendents(enable); 173 mTitleView.setVisibility(enable ? View.VISIBLE : View.INVISIBLE); 174 } 175 176 private void initializeButton(PlayControlsButton button, int imageResId, 177 int descriptionId, Runnable clickAction) { 178 button.setImageResId(imageResId); 179 button.setAction(clickAction); 180 button.findViewById(R.id.button) 181 .setContentDescription(getResources().getString(descriptionId)); 182 } 183 184 @Override 185 public void onBind(MenuRow row) { 186 super.onBind(row); 187 PlayControlsRow playControlsRow = (PlayControlsRow) row; 188 mTimeShiftManager = playControlsRow.getTimeShiftManager(); 189 mTimeShiftManager.setListener(new TimeShiftManager.Listener() { 190 @Override 191 public void onAvailabilityChanged() { 192 updateMenuVisibility(); 193 PlayControlsRowView.this.onAvailabilityChanged(); 194 } 195 196 @Override 197 public void onPlayStatusChanged(int status) { 198 updateMenuVisibility(); 199 if (mTimeShiftManager.isAvailable()) { 200 updateAll(); 201 } 202 } 203 204 @Override 205 public void onRecordStartTimeChanged() { 206 if (!mTimeShiftManager.isAvailable()) { 207 return; 208 } 209 updateAll(); 210 } 211 212 @Override 213 public void onCurrentPositionChanged() { 214 if (!mTimeShiftManager.isAvailable()) { 215 return; 216 } 217 initializeTimeline(); 218 updateAll(); 219 } 220 221 @Override 222 public void onProgramInfoChanged() { 223 if (!mTimeShiftManager.isAvailable()) { 224 return; 225 } 226 initializeTimeline(); 227 updateAll(); 228 } 229 230 @Override 231 public void onActionEnabledChanged(@TimeShiftActionId int actionId, boolean enabled) { 232 // Move focus to the play/pause button when the PREVIOUS, NEXT, REWIND or 233 // FAST_FORWARD button is clicked and the button becomes disabled. 234 // No need to update the UI here because the UI will be updated by other callbacks. 235 if (!enabled && 236 ((actionId == TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS 237 && mJumpPreviousButton.hasFocus()) 238 || (actionId == TimeShiftManager.TIME_SHIFT_ACTION_ID_REWIND 239 && mRewindButton.hasFocus()) 240 || (actionId == TimeShiftManager.TIME_SHIFT_ACTION_ID_FAST_FORWARD 241 && mFastForwardButton.hasFocus()) 242 || (actionId == TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT 243 && mJumpNextButton.hasFocus()))) { 244 mPlayPauseButton.requestFocus(); 245 } 246 } 247 }); 248 onAvailabilityChanged(); 249 } 250 251 private void onAvailabilityChanged() { 252 if (mTimeShiftManager.isAvailable()) { 253 setRowEnable(true); 254 initializeTimeline(); 255 mBackgroundView.setEnabled(true); 256 } else { 257 setRowEnable(false); 258 mBackgroundView.setEnabled(false); 259 } 260 updateAll(); 261 } 262 263 private void initializeTimeline() { 264 Program program = mTimeShiftManager.getProgramAt(mTimeShiftManager.getCurrentPositionMs()); 265 mProgramStartTimeMs = program.getStartTimeUtcMillis(); 266 mProgramEndTimeMs = program.getEndTimeUtcMillis(); 267 } 268 269 private void updateMenuVisibility() { 270 boolean keepMenuVisible = 271 mTimeShiftManager.isAvailable() && !mTimeShiftManager.isNormalPlaying(); 272 getMenuView().setKeepVisible(keepMenuVisible); 273 } 274 275 @Override 276 public void updateView(boolean withAnimation) { 277 super.updateView(withAnimation); 278 updateAll(); 279 postHideRippleAnimation(); 280 } 281 282 @Override 283 protected float getTitleScaleSelected() { 284 return 1.0f; 285 } 286 287 @Override 288 protected float getTitleAlphaSelected() { 289 return 0.0f; 290 } 291 292 @Override 293 public void initialize(@MenuShowReason int reason) { 294 super.initialize(reason); 295 switch (reason) { 296 case MenuView.REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS: 297 if (mTimeShiftManager.isActionEnabled( 298 TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS)) { 299 setInitialFocusView(mJumpPreviousButton); 300 } else { 301 setInitialFocusView(mPlayPauseButton); 302 } 303 break; 304 case MenuView.REASON_PLAY_CONTROLS_REWIND: 305 if (mTimeShiftManager.isActionEnabled( 306 TimeShiftManager.TIME_SHIFT_ACTION_ID_REWIND)) { 307 setInitialFocusView(mRewindButton); 308 } else { 309 setInitialFocusView(mPlayPauseButton); 310 } 311 break; 312 case MenuView.REASON_PLAY_CONTROLS_FAST_FORWARD: 313 if (mTimeShiftManager.isActionEnabled( 314 TimeShiftManager.TIME_SHIFT_ACTION_ID_FAST_FORWARD)) { 315 setInitialFocusView(mFastForwardButton); 316 } else { 317 setInitialFocusView(mPlayPauseButton); 318 } 319 break; 320 case MenuView.REASON_PLAY_CONTROLS_JUMP_TO_NEXT: 321 if (mTimeShiftManager.isActionEnabled( 322 TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT)) { 323 setInitialFocusView(mJumpNextButton); 324 } else { 325 setInitialFocusView(mPlayPauseButton); 326 } 327 break; 328 case MenuView.REASON_PLAY_CONTROLS_PLAY_PAUSE: 329 case MenuView.REASON_PLAY_CONTROLS_PLAY: 330 case MenuView.REASON_PLAY_CONTROLS_PAUSE: 331 default: 332 setInitialFocusView(mPlayPauseButton); 333 break; 334 } 335 postHideRippleAnimation(); 336 } 337 338 private void postHideRippleAnimation() { 339 // Focus may be changed in another message if requestFocus is called in this message. 340 // After the focus is actually changed, hideRippleAnimation should run 341 // to reflect the result of the focus change. To be sure, hideRippleAnimation is posted. 342 post(new Runnable() { 343 @Override 344 public void run() { 345 mJumpPreviousButton.hideRippleAnimation(); 346 mRewindButton.hideRippleAnimation(); 347 mPlayPauseButton.hideRippleAnimation(); 348 mFastForwardButton.hideRippleAnimation(); 349 mJumpNextButton.hideRippleAnimation(); 350 } 351 }); 352 } 353 354 @Override 355 protected void onChildFocusChange(View v, boolean hasFocus) { 356 super.onChildFocusChange(v, hasFocus); 357 if ((v.getParent().equals(mRewindButton) || v.getParent().equals(mFastForwardButton)) 358 && !hasFocus) { 359 if (mTimeShiftManager.getPlayStatus() == TimeShiftManager.PLAY_STATUS_PLAYING) { 360 mTimeShiftManager.play(); 361 updateButtons(); 362 } 363 } 364 } 365 366 private void updateAll() { 367 updateTime(); 368 updateProgress(); 369 updateRecTimeText(); 370 updateButtons(); 371 } 372 373 private void updateTime() { 374 if (isEnabled()) { 375 mTimeText.setVisibility(View.VISIBLE); 376 mTimeIndicator.setVisibility(View.VISIBLE); 377 } else { 378 mTimeText.setVisibility(View.INVISIBLE); 379 mTimeIndicator.setVisibility(View.INVISIBLE); 380 return; 381 } 382 long currentPositionMs = mTimeShiftManager.getCurrentPositionMs(); 383 ViewGroup.MarginLayoutParams params = 384 (ViewGroup.MarginLayoutParams) mTimeText.getLayoutParams(); 385 int currentTimePositionPixel = 386 convertDurationToPixel(currentPositionMs - mProgramStartTimeMs); 387 params.leftMargin = currentTimePositionPixel + mTimeTextLeftMargin; 388 mTimeText.setLayoutParams(params); 389 mTimeText.setText(getTimeString(currentPositionMs)); 390 params = (ViewGroup.MarginLayoutParams) mTimeIndicator.getLayoutParams(); 391 params.leftMargin = currentTimePositionPixel + mTimeIndicatorLeftMargin; 392 mTimeIndicator.setLayoutParams(params); 393 } 394 395 private void updateProgress() { 396 if (isEnabled()) { 397 mProgressWatched.setVisibility(View.VISIBLE); 398 mProgressBuffered.setVisibility(View.VISIBLE); 399 mProgressEmptyAfter.setVisibility(View.VISIBLE); 400 } else { 401 mProgressWatched.setVisibility(View.INVISIBLE); 402 mProgressBuffered.setVisibility(View.INVISIBLE); 403 mProgressEmptyAfter.setVisibility(View.INVISIBLE); 404 if (mProgramStartTimeMs < mProgramEndTimeMs) { 405 layoutProgress(mProgressEmptyBefore, mProgramStartTimeMs, mProgramEndTimeMs); 406 } else { 407 // Not initialized yet. 408 layoutProgress(mProgressEmptyBefore, mTimelineWidth); 409 } 410 return; 411 } 412 413 long progressStartTimeMs = Math.min(mProgramEndTimeMs, 414 Math.max(mProgramStartTimeMs, mTimeShiftManager.getRecordStartTimeMs())); 415 long currentPlayingTimeMs = Math.min(mProgramEndTimeMs, 416 Math.max(mProgramStartTimeMs, mTimeShiftManager.getCurrentPositionMs())); 417 long progressEndTimeMs = Math.min(mProgramEndTimeMs, 418 Math.max(mProgramStartTimeMs, System.currentTimeMillis())); 419 420 layoutProgress(mProgressEmptyBefore, mProgramStartTimeMs, progressStartTimeMs); 421 layoutProgress(mProgressWatched, progressStartTimeMs, currentPlayingTimeMs); 422 layoutProgress(mProgressBuffered, currentPlayingTimeMs, progressEndTimeMs); 423 } 424 425 private void layoutProgress(View progress, long progressStartTimeMs, long progressEndTimeMs) { 426 layoutProgress(progress, Math.max(0, 427 convertDurationToPixel(progressEndTimeMs - progressStartTimeMs)) + 1); 428 } 429 430 private void layoutProgress(View progress, int width) { 431 ViewGroup.MarginLayoutParams params = 432 (ViewGroup.MarginLayoutParams) progress.getLayoutParams(); 433 params.width = width; 434 progress.setLayoutParams(params); 435 } 436 437 private void updateRecTimeText() { 438 if (isEnabled()) { 439 mProgramStartTimeText.setVisibility(View.VISIBLE); 440 mProgramEndTimeText.setVisibility(View.VISIBLE); 441 } else { 442 mProgramStartTimeText.setVisibility(View.INVISIBLE); 443 mProgramEndTimeText.setVisibility(View.INVISIBLE); 444 return; 445 } 446 mProgramStartTimeText.setText(getTimeString(mProgramStartTimeMs)); 447 mProgramEndTimeText.setText(getTimeString(mProgramEndTimeMs)); 448 } 449 450 private void updateButtons() { 451 if (isEnabled()) { 452 mControlBar.setVisibility(View.VISIBLE); 453 mUnavailableMessageText.setVisibility(View.GONE); 454 } else { 455 mControlBar.setVisibility(View.INVISIBLE); 456 mUnavailableMessageText.setVisibility(View.VISIBLE); 457 return; 458 } 459 460 if (mTimeShiftManager.getPlayStatus() == TimeShiftManager.PLAY_STATUS_PAUSED) { 461 mPlayPauseButton.setImageResId(R.drawable.lb_ic_play); 462 mPlayPauseButton.setEnabled(mTimeShiftManager.isActionEnabled( 463 TimeShiftManager.TIME_SHIFT_ACTION_ID_PLAY)); 464 } else { 465 mPlayPauseButton.setImageResId(R.drawable.lb_ic_pause); 466 mPlayPauseButton.setEnabled(mTimeShiftManager.isActionEnabled( 467 TimeShiftManager.TIME_SHIFT_ACTION_ID_PAUSE)); 468 } 469 mJumpPreviousButton.setEnabled(mTimeShiftManager.isActionEnabled( 470 TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS)); 471 mRewindButton.setEnabled(mTimeShiftManager.isActionEnabled( 472 TimeShiftManager.TIME_SHIFT_ACTION_ID_REWIND)); 473 mFastForwardButton.setEnabled(mTimeShiftManager.isActionEnabled( 474 TimeShiftManager.TIME_SHIFT_ACTION_ID_FAST_FORWARD)); 475 mJumpNextButton.setEnabled(mTimeShiftManager.isActionEnabled( 476 TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT)); 477 478 PlayControlsButton button; 479 if (mTimeShiftManager.getPlayDirection() == TimeShiftManager.PLAY_DIRECTION_FORWARD) { 480 mRewindButton.setLabel(null); 481 button = mFastForwardButton; 482 } else { 483 mFastForwardButton.setLabel(null); 484 button = mRewindButton; 485 } 486 if (mTimeShiftManager.getDisplayedPlaySpeed() == TimeShiftManager.PLAY_SPEED_1X) { 487 button.setLabel(null); 488 } else { 489 button.setLabel(getResources().getString(R.string.play_controls_speed, 490 mTimeShiftManager.getDisplayedPlaySpeed())); 491 } 492 } 493 494 private String getTimeString(long timeMs) { 495 return DateFormat.getTimeFormat(getContext()).format(timeMs); 496 } 497 498 private int convertDurationToPixel(long duration) { 499 if (mProgramEndTimeMs <= mProgramStartTimeMs) { 500 return 0; 501 } 502 return (int) (duration * mTimelineWidth / (mProgramEndTimeMs - mProgramStartTimeMs)); 503 } 504} 505