TvActivity.java revision cac2f3efd029ad9cd24d7a920bcef18b346c3d82
1/* 2 * Copyright (C) 2014 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; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.app.Activity; 22import android.app.DialogFragment; 23import android.app.Fragment; 24import android.app.FragmentTransaction; 25import android.content.ComponentName; 26import android.content.ContentUris; 27import android.content.Context; 28import android.content.Intent; 29import android.content.pm.PackageManager; 30import android.content.pm.ResolveInfo; 31import android.database.ContentObserver; 32import android.graphics.Point; 33import android.media.AudioManager; 34import android.net.Uri; 35import android.os.Bundle; 36import android.os.Handler; 37import android.provider.TvContract; 38import android.tv.TvInputInfo; 39import android.tv.TvInputManager; 40import android.tv.TvInputService; 41import android.tv.TvView; 42import android.util.Log; 43import android.view.Display; 44import android.view.GestureDetector; 45import android.view.InputEvent; 46import android.view.KeyEvent; 47import android.view.MotionEvent; 48import android.view.View; 49import android.view.ViewGroup; 50import android.widget.LinearLayout; 51import android.widget.TextView; 52import android.widget.Toast; 53 54import com.android.tv.menu.MenuDialogFragment; 55 56import java.util.List; 57 58/** 59 * The main activity for demonstrating TV app. 60 */ 61public class TvActivity extends Activity implements 62 InputPickerDialogFragment.InputPickerDialogListener, 63 AudioManager.OnAudioFocusChangeListener { 64 private static final String TAG = "TvActivity"; 65 66 private static final int DURATION_SHOW_CHANNEL_BANNER = 2000; 67 private static final int DURATION_SHOW_CONTROL_GUIDE = 1000; 68 private static final float AUDIO_MAX_VOLUME = 1.0f; 69 private static final float AUDIO_MIN_VOLUME = 0.0f; 70 private static final float AUDIO_DUCKING_VOLUME = 0.3f; 71 // TODO: add more KEYCODEs to the white list. 72 private static final int[] KEYCODE_WHITELIST = { 73 KeyEvent.KEYCODE_0, KeyEvent.KEYCODE_1, KeyEvent.KEYCODE_2, KeyEvent.KEYCODE_3, 74 KeyEvent.KEYCODE_4, KeyEvent.KEYCODE_5, KeyEvent.KEYCODE_5, KeyEvent.KEYCODE_7, 75 KeyEvent.KEYCODE_8, KeyEvent.KEYCODE_9, KeyEvent.KEYCODE_STAR, KeyEvent.KEYCODE_POUND, 76 KeyEvent.KEYCODE_M, 77 }; 78 // TODO: this value should be able to be toggled in menu. 79 private static final boolean USE_KEYCODE_BLACKLIST = false; 80 private static final int[] KEYCODE_BLACKLIST = { 81 KeyEvent.KEYCODE_MENU, KeyEvent.KEYCODE_CHANNEL_UP, KeyEvent.KEYCODE_CHANNEL_DOWN, 82 KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_CTRL_LEFT, KeyEvent.KEYCODE_CTRL_RIGHT 83 }; 84 // STOPSHIP: debug keys are used only for testing. 85 private static final boolean USE_DEBUG_KEYS = true; 86 87 private TvInputManager mTvInputManager; 88 private TvView mTvView; 89 private LinearLayout mControlGuide; 90 private LinearLayout mChannelBanner; 91 private Runnable mHideChannelBanner; 92 private Runnable mHideControlGuide; 93 private TextView mChannelTextView; 94 private TextView mProgramTextView; 95 private int mShortAnimationDuration; 96 private int mDisplayWidth; 97 private GestureDetector mGestureDetector; 98 private ChannelMap mChannelMap; 99 private TvInputManager.Session mTvSession; 100 private TvInputInfo mTvInputInfo; 101 private AudioManager mAudioManager; 102 private int mAudioFocusStatus; 103 private boolean mDefaultSessionRequested; 104 private boolean mTunePendding; 105 private boolean mPipShowing; 106 private boolean mDebugNonFullSizeScreen; 107 private boolean mUseKeycodeBlacklist = USE_KEYCODE_BLACKLIST; 108 109 // PIP is used for debug/verification of multiple sessions rather than real PIP feature. 110 // When PIP is enabled, the same channel as mTvView is tuned. 111 private TvView mPipView; 112 private TvInputManager.Session mPipSession; 113 private TvInputInfo mPipInputInfo; 114 115 @Override 116 protected void onCreate(Bundle savedInstanceState) { 117 super.onCreate(savedInstanceState); 118 119 setContentView(R.layout.activity_tv); 120 mTvView = (TvView) findViewById(R.id.tv_view); 121 mTvView.setOnUnhandledInputEventListener(new TvView.OnUnhandledInputEventListener() { 122 @Override 123 public boolean onUnhandledInputEvent(InputEvent event) { 124 if (event instanceof KeyEvent) { 125 KeyEvent keyEvent = (KeyEvent) event; 126 if (keyEvent.getAction() == KeyEvent.ACTION_UP) { 127 return onKeyUp(keyEvent.getKeyCode(), keyEvent); 128 } 129 } else if (event instanceof MotionEvent) { 130 MotionEvent motionEvent = (MotionEvent) event; 131 if (motionEvent.isTouchEvent()) { 132 return onTouchEvent(motionEvent); 133 } 134 } 135 return false; 136 } 137 }); 138 mPipView = (TvView) findViewById(R.id.pip_view); 139 mPipView.setZOrderMediaOverlay(true); 140 141 mControlGuide = (LinearLayout) findViewById(R.id.control_guide); 142 mChannelBanner = (LinearLayout) findViewById(R.id.channel_banner); 143 144 // Initially hide the channel banner and the control guide. 145 mChannelBanner.setVisibility(View.GONE); 146 mControlGuide.setVisibility(View.GONE); 147 148 mHideControlGuide = new HideRunnable(mControlGuide); 149 mHideChannelBanner = new HideRunnable(mChannelBanner); 150 151 mChannelTextView = (TextView) findViewById(R.id.channel_text); 152 mProgramTextView = (TextView) findViewById(R.id.program_text); 153 154 mShortAnimationDuration = getResources().getInteger(android.R.integer.config_shortAnimTime); 155 156 mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 157 mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS; 158 Display display = getWindowManager().getDefaultDisplay(); 159 Point size = new Point(); 160 display.getSize(size); 161 mDisplayWidth = size.x; 162 163 mGestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() { 164 static final float CONTROL_MARGIN = 0.2f; 165 final float mLeftMargin = mDisplayWidth * CONTROL_MARGIN; 166 final float mRightMargin = mDisplayWidth * (1 - CONTROL_MARGIN); 167 168 @Override 169 public boolean onDown(MotionEvent event) { 170 Log.d(TAG, "onDown: " + event.toString()); 171 if (mChannelMap == null) { 172 return false; 173 } 174 175 showAndHide(mControlGuide, mHideControlGuide, DURATION_SHOW_CONTROL_GUIDE); 176 177 if (event.getX() <= mLeftMargin) { 178 channelDown(); 179 return true; 180 } else if (event.getX() >= mRightMargin) { 181 channelUp(); 182 return true; 183 } 184 return false; 185 } 186 187 @Override 188 public boolean onSingleTapUp(MotionEvent event) { 189 if (mChannelMap == null) { 190 showInputPickerDialog(); 191 return true; 192 } 193 194 if (event.getX() > mLeftMargin && event.getX() < mRightMargin) { 195 showMenu(); 196 return true; 197 } 198 return false; 199 } 200 }); 201 202 getContentResolver().registerContentObserver(TvContract.Programs.CONTENT_URI, true, 203 mProgramUpdateObserver); 204 205 mTvInputManager = (TvInputManager) getSystemService(Context.TV_INPUT_SERVICE); 206 } 207 208 @Override 209 protected void onNewIntent(Intent intent) { 210 setIntent(intent); 211 } 212 213 @Override 214 protected void onResume() { 215 super.onResume(); 216 startDefaultSession(); 217 } 218 219 private void startDefaultSession() { 220 if (mTvInputManager == null) { 221 return; 222 } 223 // TODO: Remove this check after TvInputManagerService becomes system service. 224 if (mDefaultSessionRequested) { 225 return; 226 } 227 mDefaultSessionRequested = true; 228 if (mTunePendding) { 229 return; 230 } 231 232 // Check whether the system has at least one TvInputService app installed. 233 final List<ResolveInfo> services = getPackageManager().queryIntentServices( 234 new Intent(TvInputService.SERVICE_INTERFACE), PackageManager.GET_SERVICES); 235 if (services == null || services.isEmpty()) { 236 Toast.makeText(this, R.string.no_input_device_found, Toast.LENGTH_SHORT).show(); 237 // TODO: Direct the user to a Play Store landing page for TvInputService apps. 238 return; 239 } 240 241 // Figure out the initial channel to tune to. 242 long channelId = Channel.INVALID_ID; 243 Intent intent = getIntent(); 244 if (Intent.ACTION_VIEW.equals(intent.getAction())) { 245 // In case the channel is given explicitly, use it. 246 channelId = ContentUris.parseId(intent.getData()); 247 } 248 if (channelId == Channel.INVALID_ID) { 249 // Otherwise, remember the last channel the user watched. 250 Channel channel = TvInputUtils.getLastWatchedChannel(this); 251 if (channel != null) { 252 channelId = channel.getId(); 253 } 254 } 255 if (channelId == Channel.INVALID_ID) { 256 // If failed to pick a channel, try a different input. 257 showInputPickerDialog(); 258 return; 259 } 260 ComponentName inputName = TvInputUtils.getInputNameForChannel(this, channelId); 261 if (inputName == null) { 262 // If failed to determine the input for that channel, try a different input. 263 showInputPickerDialog(); 264 return; 265 } 266 // If the session is already started, simply adjust the volume without tune. 267 if (mTvSession != null) { 268 setVolumeByAudioFocusStatus(); 269 } else { 270 List<TvInputInfo> inputList = mTvInputManager.getTvInputList(); 271 for (TvInputInfo info : inputList) { 272 if (inputName.equals(info.getComponent())) { 273 startSession(info, channelId); 274 break; 275 } 276 } 277 } 278 } 279 280 @Override 281 protected void onPause() { 282 stopSession(); 283 stopPipSession(); 284 mDefaultSessionRequested = false; 285 super.onPause(); 286 } 287 288 public void showInputPickerDialog() { 289 showDialogFragment(InputPickerDialogFragment.DIALOG_TAG, new InputPickerDialogFragment()); 290 } 291 292 @Override 293 public void onInputPicked(TvInputInfo which) { 294 if (mTvSession != null && which.equals(mTvInputInfo)) { 295 // Nothing has changed thus nothing to do. 296 return; 297 } 298 299 // Start a new session with the new input. 300 stopSession(); 301 Channel channel = TvInputUtils.getLastWatchedChannel(this, which.getComponent()); 302 long channelId = channel != null ? channel.getId() : Channel.INVALID_ID; 303 startSession(which, channelId); 304 } 305 306 @Override 307 public boolean dispatchKeyEvent(KeyEvent event) { 308 int eventKeyCode = event.getKeyCode(); 309 if (mUseKeycodeBlacklist) { 310 for (int keycode : KEYCODE_BLACKLIST) { 311 if (keycode == eventKeyCode) { 312 return super.dispatchKeyEvent(event); 313 } 314 } 315 return dispatchKeyEventToSession(event); 316 } else { 317 for (int keycode : KEYCODE_WHITELIST) { 318 if (keycode == eventKeyCode) { 319 return dispatchKeyEventToSession(event); 320 } 321 } 322 return super.dispatchKeyEvent(event); 323 } 324 } 325 326 @Override 327 public void onAudioFocusChange(int focusChange) { 328 mAudioFocusStatus = focusChange; 329 setVolumeByAudioFocusStatus(); 330 } 331 332 private void setVolumeByAudioFocusStatus() { 333 if (mTvSession != null) { 334 switch (mAudioFocusStatus) { 335 case AudioManager.AUDIOFOCUS_GAIN: 336 mTvSession.setVolume(AUDIO_MAX_VOLUME); 337 break; 338 case AudioManager.AUDIOFOCUS_LOSS: 339 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: 340 mTvSession.setVolume(AUDIO_MIN_VOLUME); 341 break; 342 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: 343 mTvSession.setVolume(AUDIO_DUCKING_VOLUME); 344 break; 345 } 346 } 347 } 348 349 private void startSession(TvInputInfo inputInfo, long channelId) { 350 // TODO: recreate SurfaceView to prevent abusing from the previous session. 351 mTvInputInfo = inputInfo; 352 // Prepare a new channel map for the current input. 353 mChannelMap = new ChannelMap(this, inputInfo.getComponent(), channelId, 354 mOnChannelsLoadFinished); 355 // Create a new session and start. 356 mTvView.bindTvInput(inputInfo.getComponent(), mSessionCreated); 357 tune(); 358 } 359 360 private final TvInputManager.SessionCreateCallback mSessionCreated = 361 new TvInputManager.SessionCreateCallback() { 362 @Override 363 public void onSessionCreated(TvInputManager.Session session) { 364 if (session != null) { 365 mTvSession = session; 366 int result = mAudioManager.requestAudioFocus(TvActivity.this, 367 AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); 368 mAudioFocusStatus = 369 (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) ? 370 AudioManager.AUDIOFOCUS_GAIN 371 : AudioManager.AUDIOFOCUS_LOSS; 372 if (mTunePendding) { 373 tune(); 374 } 375 } else { 376 Log.w(TAG, "Failed to create a session"); 377 // TODO: show something to user about this error. 378 } 379 } 380 }; 381 382 private void startPipSession() { 383 if (mTvSession == null) { 384 Log.w(TAG, "TV content should be playing."); 385 return; 386 } 387 Log.d(TAG, "startPipSession"); 388 mPipInputInfo = mTvInputInfo; 389 mPipView.bindTvInput(mPipInputInfo.getComponent(), mPipSessionCreated); 390 mPipShowing = true; 391 } 392 393 private final TvInputManager.SessionCreateCallback mPipSessionCreated = 394 new TvInputManager.SessionCreateCallback() { 395 @Override 396 public void onSessionCreated(final TvInputManager.Session session) { 397 Log.d(TAG, "PIP session is created."); 398 if (mTvSession == null) { 399 Log.w(TAG, "TV content should be playing."); 400 if (session != null) { 401 mPipView.unbindTvInput(); 402 } 403 mPipShowing = false; 404 return; 405 } 406 if (session == null) { 407 Log.w(TAG, "Fail to create another session."); 408 mPipShowing = false; 409 return; 410 } 411 runOnUiThread(new Runnable() { 412 @Override 413 public void run() { 414 mPipSession = session; 415 mPipSession.setVolume(0); 416 mPipSession.tune(mChannelMap.getCurrentChannelUri()); 417 mPipView.setVisibility(View.VISIBLE); 418 } 419 }); 420 } 421 }; 422 423 private final ContentObserver mProgramUpdateObserver = new ContentObserver(new Handler()) { 424 @Override 425 public void onChange(boolean selfChange, Uri uri) { 426 if (mChannelMap == null || !mChannelMap.isLoadFinished()) { 427 return; 428 } 429 Uri channelUri = mChannelMap.getCurrentChannelUri(); 430 if (channelUri == null) { 431 return; 432 } 433 Program program = TvInputUtils.getCurrentProgram(TvActivity.this, channelUri); 434 if (program == null) { 435 return; 436 } 437 mProgramTextView.setText(program.getTitle()); 438 } 439 }; 440 441 private final Runnable mOnChannelsLoadFinished = new Runnable() { 442 @Override 443 public void run() { 444 if (mTunePendding) { 445 tune(); 446 } 447 } 448 }; 449 450 private void tune() { 451 Log.d(TAG, "tune()"); 452 // Prerequisites to be able to tune. 453 if (mChannelMap == null || !mChannelMap.isLoadFinished()) { 454 Log.d(TAG, "Channel map not ready"); 455 mTunePendding = true; 456 return; 457 } 458 if (mTvSession == null) { 459 Log.d(TAG, "Service not connected"); 460 mTunePendding = true; 461 return; 462 } 463 setVolumeByAudioFocusStatus(); 464 465 Uri currentChannelUri = mChannelMap.getCurrentChannelUri(); 466 if (currentChannelUri != null) { 467 // TODO: implement 'no signal' 468 // TODO: add result callback and show a message on failure. 469 TvInputUtils.setLastWatchedChannel(this, currentChannelUri); 470 mTvSession.tune(currentChannelUri); 471 displayChannelBanner(); 472 } 473 mTunePendding = false; 474 } 475 476 private void displayChannelBanner() { 477 runOnUiThread(new Runnable() { 478 @Override 479 public void run() { 480 // TODO: Show a beautiful channel banner instead. 481 String channelBannerString = ""; 482 String displayNumber = mChannelMap.getCurrentDisplayNumber(); 483 if (displayNumber != null) { 484 channelBannerString += displayNumber; 485 } 486 String displayName = mChannelMap.getCurrentDisplayName(); 487 if (displayName != null) { 488 channelBannerString += " " + displayName; 489 } 490 mChannelTextView.setText(channelBannerString); 491 492 Program program = TvInputUtils.getCurrentProgram(TvActivity.this, 493 mChannelMap.getCurrentChannelUri()); 494 String programTitle = program != null ? program.getTitle() : null; 495 // Program title might not be available at this point. Setting the text to null to 496 // clear the previous program title for now. It will be filled as soon as we get the 497 // updated program information. 498 mProgramTextView.setText(programTitle); 499 500 showAndHide(mChannelBanner, mHideChannelBanner, DURATION_SHOW_CHANNEL_BANNER); 501 } 502 }); 503 } 504 505 public void showRecentlyWatchedDialog() { 506 showDialogFragment(RecentlyWatchedDialogFragment.DIALOG_TAG, 507 new RecentlyWatchedDialogFragment()); 508 } 509 510 private void stopSession() { 511 if (mTvSession != null) { 512 mTvSession.setVolume(AUDIO_MIN_VOLUME); 513 mAudioManager.abandonAudioFocus(this); 514 mTvView.unbindTvInput(); 515 mTvSession = null; 516 mTvInputInfo = null; 517 } 518 if (mChannelMap != null) { 519 mChannelMap.close(); 520 mChannelMap = null; 521 } 522 } 523 524 private void stopPipSession() { 525 Log.d(TAG, "stopPipSession"); 526 if (mPipSession != null) { 527 mPipView.setVisibility(View.INVISIBLE); 528 mPipView.unbindTvInput(); 529 mPipSession = null; 530 mPipInputInfo = null; 531 } 532 mPipShowing = false; 533 } 534 535 @Override 536 protected void onDestroy() { 537 getContentResolver().unregisterContentObserver(mProgramUpdateObserver); 538 Log.d(TAG, "onDestroy()"); 539 super.onDestroy(); 540 } 541 542 @Override 543 public boolean onKeyUp(int keyCode, KeyEvent event) { 544 if (mChannelMap == null) { 545 switch (keyCode) { 546 case KeyEvent.KEYCODE_H: 547 showRecentlyWatchedDialog(); 548 return true; 549 case KeyEvent.KEYCODE_TV_INPUT: 550 case KeyEvent.KEYCODE_I: 551 case KeyEvent.KEYCODE_CHANNEL_UP: 552 case KeyEvent.KEYCODE_DPAD_UP: 553 case KeyEvent.KEYCODE_CHANNEL_DOWN: 554 case KeyEvent.KEYCODE_DPAD_DOWN: 555 case KeyEvent.KEYCODE_NUMPAD_ENTER: 556 case KeyEvent.KEYCODE_E: 557 case KeyEvent.KEYCODE_MENU: 558 showInputPickerDialog(); 559 return true; 560 } 561 } else { 562 switch (keyCode) { 563 case KeyEvent.KEYCODE_H: 564 showRecentlyWatchedDialog(); 565 return true; 566 567 case KeyEvent.KEYCODE_TV_INPUT: 568 case KeyEvent.KEYCODE_I: 569 showInputPickerDialog(); 570 return true; 571 572 case KeyEvent.KEYCODE_CHANNEL_UP: 573 case KeyEvent.KEYCODE_DPAD_UP: 574 channelUp(); 575 return true; 576 577 case KeyEvent.KEYCODE_CHANNEL_DOWN: 578 case KeyEvent.KEYCODE_DPAD_DOWN: 579 channelDown(); 580 return true; 581 582 case KeyEvent.KEYCODE_NUMPAD_ENTER: 583 case KeyEvent.KEYCODE_E: 584 displayChannelBanner(); 585 return true; 586 587 case KeyEvent.KEYCODE_MENU: 588 if (event.isCanceled()) { 589 return true; 590 } 591 showMenu(); 592 return true; 593 } 594 } 595 if (USE_DEBUG_KEYS) { 596 switch (keyCode) { 597 case KeyEvent.KEYCODE_W: { 598 mDebugNonFullSizeScreen = !mDebugNonFullSizeScreen; 599 if (mDebugNonFullSizeScreen) { 600 mTvView.layout(100, 100, 400, 300); 601 } else { 602 ViewGroup.LayoutParams params = mTvView.getLayoutParams(); 603 params.width = ViewGroup.LayoutParams.MATCH_PARENT; 604 params.height = ViewGroup.LayoutParams.MATCH_PARENT; 605 mTvView.setLayoutParams(params); 606 } 607 return true; 608 } 609 case KeyEvent.KEYCODE_P: { 610 togglePipView(); 611 return true; 612 } 613 case KeyEvent.KEYCODE_CTRL_LEFT: 614 case KeyEvent.KEYCODE_CTRL_RIGHT: { 615 mUseKeycodeBlacklist = !mUseKeycodeBlacklist; 616 return true; 617 } 618 } 619 } 620 return super.onKeyUp(keyCode, event); 621 } 622 623 @Override 624 public boolean onTouchEvent(MotionEvent event) { 625 mGestureDetector.onTouchEvent(event); 626 return super.onTouchEvent(event); 627 } 628 629 public void togglePipView() { 630 if (mPipShowing) { 631 stopPipSession(); 632 } else { 633 startPipSession(); 634 } 635 } 636 637 private boolean dispatchKeyEventToSession(final KeyEvent event) { 638 return false; 639 } 640 641 private void channelUp() { 642 if (mChannelMap != null) { 643 mChannelMap.moveToNextChannel(); 644 tune(); 645 } 646 } 647 648 private void channelDown() { 649 if (mChannelMap != null) { 650 mChannelMap.moveToPreviousChannel(); 651 tune(); 652 } 653 } 654 655 private void showMenu() { 656 MenuDialogFragment f = new MenuDialogFragment(); 657 if (mTvSession != null) { 658 Bundle arg = new Bundle(); 659 arg.putString(MenuDialogFragment.ARG_CURRENT_PACKAGE_NAME, 660 mTvInputInfo.getPackageName()); 661 arg.putString(MenuDialogFragment.ARG_CURRENT_SERVICE_NAME, 662 mTvInputInfo.getServiceName()); 663 f.setArguments(arg); 664 } 665 666 showDialogFragment(MenuDialogFragment.DIALOG_TAG, f); 667 } 668 669 private void showDialogFragment(String tag, DialogFragment dialog) { 670 FragmentTransaction ft = getFragmentManager().beginTransaction(); 671 Fragment prev = getFragmentManager().findFragmentByTag(tag); 672 if (prev != null) { 673 ft.remove(prev); 674 } 675 ft.addToBackStack(null); 676 dialog.show(ft, tag); 677 } 678 679 private final Handler mHideHandler = new Handler(); 680 681 private class HideRunnable implements Runnable { 682 private final View mView; 683 684 public HideRunnable(View view) { 685 mView = view; 686 } 687 688 @Override 689 public void run() { 690 mView.animate() 691 .alpha(0f) 692 .setDuration(mShortAnimationDuration) 693 .setListener(new AnimatorListenerAdapter() { 694 @Override 695 public void onAnimationEnd(Animator animation) { 696 mView.setVisibility(View.GONE); 697 } 698 }); 699 } 700 } 701 702 private void showAndHide(View view, Runnable hide, long duration) { 703 if (view.getVisibility() == View.VISIBLE) { 704 // Skip the show animation if the view is already visible and cancel the scheduled hide 705 // animation. 706 mHideHandler.removeCallbacks(hide); 707 } else { 708 view.setAlpha(0f); 709 view.setVisibility(View.VISIBLE); 710 view.animate() 711 .alpha(1f) 712 .setDuration(mShortAnimationDuration) 713 .setListener(null); 714 } 715 // Schedule the hide animation after a few seconds. 716 mHideHandler.postDelayed(hide, duration); 717 } 718} 719