1 package com.android.ex.photo; 2 3 import android.app.Activity; 4 import android.app.ActivityManager; 5 import android.content.Context; 6 import android.content.Intent; 7 import android.content.res.Resources; 8 import android.database.Cursor; 9 import android.graphics.drawable.Drawable; 10 import android.net.Uri; 11 import android.os.Build; 12 import android.os.Bundle; 13 import android.os.Handler; 14 import android.os.Process; 15 import androidx.annotation.IdRes; 16 import androidx.annotation.Nullable; 17 import androidx.fragment.app.Fragment; 18 import androidx.fragment.app.FragmentManager; 19 import androidx.loader.app.LoaderManager; 20 import androidx.loader.content.Loader; 21 import androidx.viewpager.widget.ViewPager.OnPageChangeListener; 22 import android.text.TextUtils; 23 import android.util.DisplayMetrics; 24 import android.util.Log; 25 import android.view.Menu; 26 import android.view.MenuItem; 27 import android.view.View; 28 import android.view.ViewPropertyAnimator; 29 import android.view.ViewTreeObserver.OnGlobalLayoutListener; 30 import android.view.WindowManager; 31 import android.view.accessibility.AccessibilityManager; 32 import android.view.animation.AlphaAnimation; 33 import android.view.animation.Animation; 34 import android.view.animation.Animation.AnimationListener; 35 import android.view.animation.AnimationSet; 36 import android.view.animation.ScaleAnimation; 37 import android.view.animation.TranslateAnimation; 38 import android.widget.ImageView; 39 40 import com.android.ex.photo.ActionBarInterface.OnMenuVisibilityListener; 41 import com.android.ex.photo.PhotoViewPager.InterceptType; 42 import com.android.ex.photo.PhotoViewPager.OnInterceptTouchListener; 43 import com.android.ex.photo.adapters.PhotoPagerAdapter; 44 import com.android.ex.photo.fragments.PhotoViewFragment; 45 import com.android.ex.photo.loaders.PhotoBitmapLoader; 46 import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult; 47 import com.android.ex.photo.loaders.PhotoPagerLoader; 48 import com.android.ex.photo.provider.PhotoContract; 49 import com.android.ex.photo.util.ImageUtils; 50 import com.android.ex.photo.util.Util; 51 52 import java.util.HashMap; 53 import java.util.HashSet; 54 import java.util.Map; 55 import java.util.Set; 56 57 /** 58 * This class implements all the logic of the photo view activity. An activity should use this class 59 * calling through from relevant activity methods to the methods of the same name here. 60 * 61 * To customize the photo viewer activity, you should subclass this and implement your 62 * customizations here. Then subclass {@link PhotoViewActivity} and override just 63 * {@link PhotoViewActivity#createController createController} to instantiate your controller 64 * subclass. 65 */ 66 public class PhotoViewController implements 67 LoaderManager.LoaderCallbacks<Cursor>, OnPageChangeListener, OnInterceptTouchListener, 68 OnMenuVisibilityListener, PhotoViewCallbacks { 69 70 /** 71 * Defines the interface between the Activity and this class. 72 * 73 * The activity itself must delegate all appropriate method calls into this class, to the 74 * methods of the same name. 75 */ 76 public interface ActivityInterface { getContext()77 public Context getContext(); getApplicationContext()78 public Context getApplicationContext(); getIntent()79 public Intent getIntent(); setContentView(int resId)80 public void setContentView(int resId); findViewById(int id)81 public <T extends View> T findViewById(int id); getResources()82 public Resources getResources(); getSupportFragmentManager()83 public FragmentManager getSupportFragmentManager(); getSupportLoaderManager()84 public LoaderManager getSupportLoaderManager(); getActionBarInterface()85 public ActionBarInterface getActionBarInterface(); onOptionsItemSelected(MenuItem item)86 public boolean onOptionsItemSelected(MenuItem item); finish()87 public void finish(); overridePendingTransition(int enterAnim, int exitAnim)88 public void overridePendingTransition(int enterAnim, int exitAnim); getController()89 public PhotoViewController getController(); 90 } 91 92 private final static String TAG = "PhotoViewController"; 93 94 private final static String STATE_INITIAL_URI_KEY = 95 "com.android.ex.PhotoViewFragment.INITIAL_URI"; 96 private final static String STATE_CURRENT_URI_KEY = 97 "com.android.ex.PhotoViewFragment.CURRENT_URI"; 98 private final static String STATE_CURRENT_INDEX_KEY = 99 "com.android.ex.PhotoViewFragment.CURRENT_INDEX"; 100 private final static String STATE_FULLSCREEN_KEY = 101 "com.android.ex.PhotoViewFragment.FULLSCREEN"; 102 private final static String STATE_ACTIONBARTITLE_KEY = 103 "com.android.ex.PhotoViewFragment.ACTIONBARTITLE"; 104 private final static String STATE_ACTIONBARSUBTITLE_KEY = 105 "com.android.ex.PhotoViewFragment.ACTIONBARSUBTITLE"; 106 private final static String STATE_ENTERANIMATIONFINISHED_KEY = 107 "com.android.ex.PhotoViewFragment.SCALEANIMATIONFINISHED"; 108 109 protected final static String ARG_IMAGE_URI = "image_uri"; 110 111 public static final int LOADER_PHOTO_LIST = 100; 112 113 /** Count used when the real photo count is unknown [but, may be determined] */ 114 public static final int ALBUM_COUNT_UNKNOWN = -1; 115 116 public static final int ENTER_ANIMATION_DURATION_MS = 250; 117 public static final int EXIT_ANIMATION_DURATION_MS = 250; 118 119 /** Argument key for the dialog message */ 120 public static final String KEY_MESSAGE = "dialog_message"; 121 122 public static int sMemoryClass; 123 public static int sMaxPhotoSize; // The maximum size (either width or height) 124 125 private final ActivityInterface mActivity; 126 127 private int mLastFlags; 128 129 private final View.OnSystemUiVisibilityChangeListener mSystemUiVisibilityChangeListener; 130 131 /** The URI of the photos we're viewing; may be {@code null} */ 132 private String mPhotosUri; 133 /** The uri of the initial photo */ 134 private String mInitialPhotoUri; 135 /** The index of the currently viewed photo */ 136 private int mCurrentPhotoIndex; 137 /** The uri of the currently viewed photo */ 138 private String mCurrentPhotoUri; 139 /** The query projection to use; may be {@code null} */ 140 private String[] mProjection; 141 /** The total number of photos; only valid if {@link #mIsEmpty} is {@code false}. */ 142 protected int mAlbumCount = ALBUM_COUNT_UNKNOWN; 143 /** {@code true} if the view is empty. Otherwise, {@code false}. */ 144 protected boolean mIsEmpty; 145 /** the main root view */ 146 protected View mRootView; 147 /** Background image that contains nothing, so it can be alpha faded from 148 * transparent to black without affecting any other views. */ 149 @Nullable 150 protected View mBackground; 151 /** The main pager; provides left/right swipe between photos */ 152 protected PhotoViewPager mViewPager; 153 /** The temporary image so that we can quickly scale up the fullscreen thumbnail */ 154 @Nullable 155 protected ImageView mTemporaryImage; 156 /** Adapter to create pager views */ 157 protected PhotoPagerAdapter mAdapter; 158 /** Whether or not we're in "full screen" mode */ 159 protected boolean mFullScreen; 160 /** The listeners wanting full screen state for each screen position */ 161 private final Map<Integer, OnScreenListener> 162 mScreenListeners = new HashMap<Integer, OnScreenListener>(); 163 /** The set of listeners wanting full screen state */ 164 private final Set<CursorChangedListener> mCursorListeners = new HashSet<CursorChangedListener>(); 165 /** When {@code true}, restart the loader when the activity becomes active */ 166 private boolean mKickLoader; 167 /** Don't attempt operations that may trigger a fragment transaction when the activity is 168 * destroyed */ 169 private boolean mIsDestroyedCompat; 170 /** Whether or not this activity is paused */ 171 protected boolean mIsPaused = true; 172 /** The maximum scale factor applied to images when they are initially displayed */ 173 protected float mMaxInitialScale; 174 /** The title in the actionbar */ 175 protected String mActionBarTitle; 176 /** The subtitle in the actionbar */ 177 protected String mActionBarSubtitle; 178 179 private boolean mEnterAnimationFinished; 180 protected boolean mScaleAnimationEnabled; 181 protected int mAnimationStartX; 182 protected int mAnimationStartY; 183 protected int mAnimationStartWidth; 184 protected int mAnimationStartHeight; 185 186 /** Whether lights out should invoked based on timer */ 187 protected boolean mIsTimerLightsOutEnabled; 188 protected boolean mActionBarHiddenInitially; 189 protected boolean mDisplayThumbsFullScreen; 190 191 private final AccessibilityManager mAccessibilityManager; 192 193 protected BitmapCallback mBitmapCallback; 194 protected final Handler mHandler = new Handler(); 195 196 // TODO Find a better way to do this. We basically want the activity to display the 197 // "loading..." progress until the fragment takes over and shows it's own "loading..." 198 // progress [located in photo_header_view.xml]. We could potentially have all status displayed 199 // by the activity, but, that gets tricky when it comes to screen rotation. For now, we 200 // track the loading by this variable which is fragile and may cause phantom "loading..." 201 // text. 202 private long mEnterFullScreenDelayTime; 203 204 private int lastAnnouncedTitle = -1; 205 PhotoViewController(ActivityInterface activity)206 public PhotoViewController(ActivityInterface activity) { 207 mActivity = activity; 208 209 // View.OnSystemUiVisibilityChangeListener is an API that was introduced in API level 11. 210 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { 211 mSystemUiVisibilityChangeListener = null; 212 } else { 213 mSystemUiVisibilityChangeListener = new View.OnSystemUiVisibilityChangeListener() { 214 @Override 215 public void onSystemUiVisibilityChange(int visibility) { 216 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && 217 visibility == 0 && mLastFlags == 3846) { 218 setFullScreen(false /* fullscreen */, true /* setDelayedRunnable */); 219 } 220 } 221 }; 222 } 223 224 mAccessibilityManager = (AccessibilityManager) 225 activity.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); 226 } 227 createPhotoPagerAdapter(Context context, androidx.fragment.app.FragmentManager fm, Cursor c, float maxScale)228 public PhotoPagerAdapter createPhotoPagerAdapter(Context context, 229 androidx.fragment.app.FragmentManager fm, Cursor c, float maxScale) { 230 return new PhotoPagerAdapter(context, fm, c, maxScale, mDisplayThumbsFullScreen); 231 } 232 getActivity()233 public PhotoViewController.ActivityInterface getActivity() { 234 return mActivity; 235 } 236 onCreate(Bundle savedInstanceState)237 public void onCreate(Bundle savedInstanceState) { 238 initMaxPhotoSize(); 239 final ActivityManager mgr = (ActivityManager) mActivity.getApplicationContext(). 240 getSystemService(Activity.ACTIVITY_SERVICE); 241 sMemoryClass = mgr.getMemoryClass(); 242 243 final Intent intent = mActivity.getIntent(); 244 // uri of the photos to view; optional 245 if (intent.hasExtra(Intents.EXTRA_PHOTOS_URI)) { 246 mPhotosUri = intent.getStringExtra(Intents.EXTRA_PHOTOS_URI); 247 } 248 249 mIsTimerLightsOutEnabled = intent.getBooleanExtra( 250 Intents.EXTRA_ENABLE_TIMER_LIGHTS_OUT, true); 251 252 if (intent.getBooleanExtra(Intents.EXTRA_SCALE_UP_ANIMATION, false)) { 253 mScaleAnimationEnabled = true; 254 mAnimationStartX = intent.getIntExtra(Intents.EXTRA_ANIMATION_START_X, 0); 255 mAnimationStartY = intent.getIntExtra(Intents.EXTRA_ANIMATION_START_Y, 0); 256 mAnimationStartWidth = intent.getIntExtra(Intents.EXTRA_ANIMATION_START_WIDTH, 0); 257 mAnimationStartHeight = intent.getIntExtra(Intents.EXTRA_ANIMATION_START_HEIGHT, 0); 258 } 259 mActionBarHiddenInitially = intent.getBooleanExtra( 260 Intents.EXTRA_ACTION_BAR_HIDDEN_INITIALLY, false) 261 && !Util.isTouchExplorationEnabled(mAccessibilityManager); 262 mDisplayThumbsFullScreen = intent.getBooleanExtra( 263 Intents.EXTRA_DISPLAY_THUMBS_FULLSCREEN, false); 264 265 // projection for the query; optional 266 // If not set, the default projection is used. 267 // This projection must include the columns from the default projection. 268 if (intent.hasExtra(Intents.EXTRA_PROJECTION)) { 269 mProjection = intent.getStringArrayExtra(Intents.EXTRA_PROJECTION); 270 } else { 271 mProjection = null; 272 } 273 274 // Set the max initial scale, defaulting to 1x 275 mMaxInitialScale = intent.getFloatExtra(Intents.EXTRA_MAX_INITIAL_SCALE, 1.0f); 276 mCurrentPhotoUri = null; 277 mCurrentPhotoIndex = -1; 278 279 // We allow specifying the current photo by either index or uri. 280 // This is because some users may have live datasets that can change, 281 // adding new items to either the beginning or end of the set. For clients 282 // that do not need that capability, ability to specify the current photo 283 // by index is offered as a convenience. 284 if (intent.hasExtra(Intents.EXTRA_PHOTO_INDEX)) { 285 mCurrentPhotoIndex = intent.getIntExtra(Intents.EXTRA_PHOTO_INDEX, -1); 286 } 287 if (intent.hasExtra(Intents.EXTRA_INITIAL_PHOTO_URI)) { 288 mInitialPhotoUri = intent.getStringExtra(Intents.EXTRA_INITIAL_PHOTO_URI); 289 mCurrentPhotoUri = mInitialPhotoUri; 290 } 291 mIsEmpty = true; 292 293 if (savedInstanceState != null) { 294 mInitialPhotoUri = savedInstanceState.getString(STATE_INITIAL_URI_KEY); 295 mCurrentPhotoUri = savedInstanceState.getString(STATE_CURRENT_URI_KEY); 296 mCurrentPhotoIndex = savedInstanceState.getInt(STATE_CURRENT_INDEX_KEY); 297 mFullScreen = savedInstanceState.getBoolean(STATE_FULLSCREEN_KEY, false) 298 && !Util.isTouchExplorationEnabled(mAccessibilityManager); 299 mActionBarTitle = savedInstanceState.getString(STATE_ACTIONBARTITLE_KEY); 300 mActionBarSubtitle = savedInstanceState.getString(STATE_ACTIONBARSUBTITLE_KEY); 301 mEnterAnimationFinished = savedInstanceState.getBoolean( 302 STATE_ENTERANIMATIONFINISHED_KEY, false); 303 } else { 304 mFullScreen = mActionBarHiddenInitially; 305 } 306 307 mActivity.setContentView(getContentViewId()); 308 309 // Create the adapter and add the view pager 310 mAdapter = createPhotoPagerAdapter(mActivity.getContext(), 311 mActivity.getSupportFragmentManager(), null, mMaxInitialScale); 312 final Resources resources = mActivity.getResources(); 313 mRootView = findViewById(getRootViewId()); 314 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 315 mRootView.setOnSystemUiVisibilityChangeListener(getSystemUiVisibilityChangeListener()); 316 } 317 mBackground = getBackground(); 318 mTemporaryImage = getTemporaryImage(); 319 mViewPager = (PhotoViewPager) findViewById(R.id.photo_view_pager); 320 mViewPager.setAdapter(mAdapter); 321 mViewPager.setOnPageChangeListener(this); 322 mViewPager.setOnInterceptTouchListener(this); 323 mViewPager.setPageMargin(resources.getDimensionPixelSize(R.dimen.photo_page_margin)); 324 325 mBitmapCallback = new BitmapCallback(); 326 if (!mScaleAnimationEnabled || mEnterAnimationFinished) { 327 // We are not running the scale up animation. Just let the fragments 328 // display and handle the animation. 329 mActivity.getSupportLoaderManager().initLoader(LOADER_PHOTO_LIST, null, this); 330 // Make the background opaque immediately so that we don't see the activity 331 // behind this one. 332 if (hasBackground()) { 333 mBackground.setVisibility(View.VISIBLE); 334 } 335 } else { 336 // Attempt to load the initial image thumbnail. Once we have the 337 // image, animate it up. Once the animation is complete, we can kick off 338 // loading the ViewPager. After the primary fullres image is loaded, we will 339 // make our temporary image invisible and display the ViewPager. 340 mViewPager.setVisibility(View.GONE); 341 Bundle args = new Bundle(); 342 args.putString(ARG_IMAGE_URI, mInitialPhotoUri); 343 mActivity.getSupportLoaderManager().initLoader( 344 BITMAP_LOADER_THUMBNAIL, args, mBitmapCallback); 345 } 346 347 mEnterFullScreenDelayTime = 348 resources.getInteger(R.integer.reenter_fullscreen_delay_time_in_millis); 349 350 final ActionBarInterface actionBar = mActivity.getActionBarInterface(); 351 if (actionBar != null) { 352 actionBar.setDisplayHomeAsUpEnabled(true); 353 actionBar.addOnMenuVisibilityListener(this); 354 actionBar.setDisplayOptionsShowTitle(); 355 // Set the title and subtitle immediately here, rather than waiting 356 // for the fragment to be initialized. 357 setActionBarTitles(actionBar); 358 } 359 360 if (!mScaleAnimationEnabled) { 361 setLightsOutMode(mFullScreen); 362 } else { 363 // Keep lights out mode as false. This is to prevent jank cause by concurrent 364 // animations during the enter animation. 365 setLightsOutMode(false); 366 } 367 } 368 initMaxPhotoSize()369 private void initMaxPhotoSize() { 370 if (sMaxPhotoSize == 0) { 371 final DisplayMetrics metrics = new DisplayMetrics(); 372 final WindowManager wm = (WindowManager) 373 mActivity.getContext().getSystemService(Context.WINDOW_SERVICE); 374 final ImageUtils.ImageSize imageSize = ImageUtils.sUseImageSize; 375 wm.getDefaultDisplay().getMetrics(metrics); 376 switch (imageSize) { 377 case EXTRA_SMALL: 378 // Use a photo that's 80% of the "small" size 379 sMaxPhotoSize = (Math.min(metrics.heightPixels, metrics.widthPixels) * 800) / 1000; 380 break; 381 case SMALL: 382 // Fall through. 383 case NORMAL: 384 // Fall through. 385 default: 386 sMaxPhotoSize = Math.min(metrics.heightPixels, metrics.widthPixels); 387 break; 388 } 389 } 390 } 391 onCreateOptionsMenu(Menu menu)392 public boolean onCreateOptionsMenu(Menu menu) { 393 return true; 394 } 395 onPrepareOptionsMenu(Menu menu)396 public boolean onPrepareOptionsMenu(Menu menu) { 397 return true; 398 } 399 onActivityResult(int requestCode, int resultCode, Intent data)400 public void onActivityResult(int requestCode, int resultCode, Intent data) {} 401 findViewById(int id)402 protected View findViewById(int id) { 403 return mActivity.findViewById(id); 404 } 405 406 /** 407 * Returns the android id of the viewer's root view. Subclasses should override this method if 408 * they provide their own layout. 409 */ 410 @IdRes getRootViewId()411 protected int getRootViewId() { 412 return R.id.photo_activity_root_view; 413 } 414 415 /** 416 * Returns the android layout id of the root layout that should be inflated for the viewer. 417 * Subclasses should override this method if they provide their own layout. 418 */ 419 @IdRes getContentViewId()420 protected int getContentViewId() { 421 return R.layout.photo_activity_view; 422 } 423 424 /** 425 * Returns the android view for the viewer's background view, if it has one. Subclasses should 426 * override this if they have a different (or no) background view. 427 */ 428 @Nullable getBackground()429 protected View getBackground() { 430 return findViewById(R.id.photo_activity_background); 431 } 432 433 /** 434 * Returns whether or not the view has a background object. Subclasses should override this if 435 * they do not contain a background object. 436 */ hasBackground()437 protected boolean hasBackground() { 438 return mBackground != null; 439 } 440 441 /** 442 * Returns the android view for the viewer's temporary image, if it has one. Subclasses should 443 * override this if they have a different (or no) temporary image view. 444 */ 445 @Nullable getTemporaryImage()446 protected ImageView getTemporaryImage() { 447 return (ImageView) findViewById(R.id.photo_activity_temporary_image); 448 } 449 450 /** 451 * Returns whether or not the view has a temporary image view. Subclasses should override this 452 * if they do not use a temporary image. 453 */ hasTemporaryImage()454 protected boolean hasTemporaryImage() { 455 return mTemporaryImage != null; 456 } 457 onStart()458 public void onStart() {} 459 onResume()460 public void onResume() { 461 setFullScreen(mFullScreen, false); 462 463 mIsPaused = false; 464 if (mKickLoader) { 465 mKickLoader = false; 466 mActivity.getSupportLoaderManager().initLoader(LOADER_PHOTO_LIST, null, this); 467 } 468 } 469 onPause()470 public void onPause() { 471 mIsPaused = true; 472 } 473 onStop()474 public void onStop() {} 475 onDestroy()476 public void onDestroy() { 477 mIsDestroyedCompat = true; 478 } 479 isDestroyedCompat()480 private boolean isDestroyedCompat() { 481 return mIsDestroyedCompat; 482 } 483 onBackPressed()484 public boolean onBackPressed() { 485 // If we are in fullscreen mode, and the default is not full screen, then 486 // switch back to actionBar display mode. 487 if (mFullScreen && !mActionBarHiddenInitially) { 488 toggleFullScreen(); 489 } else { 490 if (mScaleAnimationEnabled) { 491 runExitAnimation(); 492 } else { 493 return false; 494 } 495 } 496 return true; 497 } 498 onSaveInstanceState(Bundle outState)499 public void onSaveInstanceState(Bundle outState) { 500 outState.putString(STATE_INITIAL_URI_KEY, mInitialPhotoUri); 501 outState.putString(STATE_CURRENT_URI_KEY, mCurrentPhotoUri); 502 outState.putInt(STATE_CURRENT_INDEX_KEY, mCurrentPhotoIndex); 503 outState.putBoolean(STATE_FULLSCREEN_KEY, mFullScreen); 504 outState.putString(STATE_ACTIONBARTITLE_KEY, mActionBarTitle); 505 outState.putString(STATE_ACTIONBARSUBTITLE_KEY, mActionBarSubtitle); 506 outState.putBoolean(STATE_ENTERANIMATIONFINISHED_KEY, mEnterAnimationFinished); 507 } 508 onOptionsItemSelected(MenuItem item)509 public boolean onOptionsItemSelected(MenuItem item) { 510 switch (item.getItemId()) { 511 case android.R.id.home: 512 mActivity.finish(); 513 return true; 514 default: 515 return false; 516 } 517 } 518 519 @Override addScreenListener(int position, OnScreenListener listener)520 public void addScreenListener(int position, OnScreenListener listener) { 521 mScreenListeners.put(position, listener); 522 } 523 524 @Override removeScreenListener(int position)525 public void removeScreenListener(int position) { 526 mScreenListeners.remove(position); 527 } 528 529 @Override addCursorListener(CursorChangedListener listener)530 public synchronized void addCursorListener(CursorChangedListener listener) { 531 mCursorListeners.add(listener); 532 } 533 534 @Override removeCursorListener(CursorChangedListener listener)535 public synchronized void removeCursorListener(CursorChangedListener listener) { 536 mCursorListeners.remove(listener); 537 } 538 539 @Override isFragmentFullScreen(Fragment fragment)540 public boolean isFragmentFullScreen(Fragment fragment) { 541 if (mViewPager == null || mAdapter == null || mAdapter.getCount() == 0) { 542 return mFullScreen; 543 } 544 return mFullScreen || (mViewPager.getCurrentItem() != mAdapter.getItemPosition(fragment)); 545 } 546 547 @Override toggleFullScreen()548 public void toggleFullScreen() { 549 setFullScreen(!mFullScreen, true); 550 } 551 onPhotoRemoved(long photoId)552 public void onPhotoRemoved(long photoId) { 553 final Cursor data = mAdapter.getCursor(); 554 if (data == null) { 555 // Huh?! How would this happen? 556 return; 557 } 558 559 final int dataCount = data.getCount(); 560 if (dataCount <= 1) { 561 mActivity.finish(); 562 return; 563 } 564 565 mActivity.getSupportLoaderManager().restartLoader(LOADER_PHOTO_LIST, null, this); 566 } 567 568 @Override onCreateLoader(int id, Bundle args)569 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 570 if (id == LOADER_PHOTO_LIST) { 571 return new PhotoPagerLoader(mActivity.getContext(), Uri.parse(mPhotosUri), mProjection); 572 } 573 return null; 574 } 575 576 @Override onCreateBitmapLoader(int id, Bundle args, String uri)577 public Loader<BitmapResult> onCreateBitmapLoader(int id, Bundle args, String uri) { 578 switch (id) { 579 case BITMAP_LOADER_AVATAR: 580 case BITMAP_LOADER_THUMBNAIL: 581 case BITMAP_LOADER_PHOTO: 582 return new PhotoBitmapLoader(mActivity.getContext(), uri); 583 default: 584 return null; 585 } 586 } 587 588 @Override onLoadFinished(Loader<Cursor> loader, Cursor data)589 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 590 final int id = loader.getId(); 591 if (id == LOADER_PHOTO_LIST) { 592 if (data == null || data.getCount() == 0) { 593 mIsEmpty = true; 594 mAdapter.swapCursor(null); 595 } else { 596 mAlbumCount = data.getCount(); 597 if (mCurrentPhotoUri != null) { 598 int index = 0; 599 // Clear query params. Compare only the path. 600 final int uriIndex = data.getColumnIndex(PhotoContract.PhotoViewColumns.URI); 601 final Uri currentPhotoUri; 602 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 603 currentPhotoUri = Uri.parse(mCurrentPhotoUri).buildUpon() 604 .clearQuery().build(); 605 } else { 606 currentPhotoUri = Uri.parse(mCurrentPhotoUri).buildUpon() 607 .query(null).build(); 608 } 609 // Rewind data cursor to the start if it has already advanced. 610 data.moveToPosition(-1); 611 while (data.moveToNext()) { 612 final String uriString = data.getString(uriIndex); 613 final Uri uri; 614 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 615 uri = Uri.parse(uriString).buildUpon().clearQuery().build(); 616 } else { 617 uri = Uri.parse(uriString).buildUpon().query(null).build(); 618 } 619 if (currentPhotoUri != null && currentPhotoUri.equals(uri)) { 620 mCurrentPhotoIndex = index; 621 break; 622 } 623 index++; 624 } 625 } 626 627 // We're paused; don't do anything now, we'll get re-invoked 628 // when the activity becomes active again 629 if (mIsPaused) { 630 mKickLoader = true; 631 mAdapter.swapCursor(null); 632 return; 633 } 634 boolean wasEmpty = mIsEmpty; 635 mIsEmpty = false; 636 637 mAdapter.swapCursor(data); 638 if (mViewPager.getAdapter() == null) { 639 mViewPager.setAdapter(mAdapter); 640 } 641 notifyCursorListeners(data); 642 643 // Use an index of 0 if the index wasn't specified or couldn't be found 644 if (mCurrentPhotoIndex < 0) { 645 mCurrentPhotoIndex = 0; 646 } 647 648 mViewPager.setCurrentItem(mCurrentPhotoIndex, false); 649 if (wasEmpty) { 650 setViewActivated(mCurrentPhotoIndex); 651 } 652 } 653 // Update the any action items 654 updateActionItems(); 655 } 656 } 657 658 @Override onLoaderReset(androidx.loader.content.Loader<Cursor> loader)659 public void onLoaderReset(androidx.loader.content.Loader<Cursor> loader) { 660 // If the loader is reset, remove the reference in the adapter to this cursor 661 if (!isDestroyedCompat()) { 662 // This will cause a fragment transaction which can't happen if we're destroyed, 663 // but we don't care in that case because we're destroyed anyways. 664 mAdapter.swapCursor(null); 665 } 666 } 667 updateActionItems()668 public void updateActionItems() { 669 // Do nothing, but allow extending classes to do work 670 } 671 notifyCursorListeners(Cursor data)672 private synchronized void notifyCursorListeners(Cursor data) { 673 // tell all of the objects listening for cursor changes 674 // that the cursor has changed 675 for (CursorChangedListener listener : mCursorListeners) { 676 listener.onCursorChanged(data); 677 } 678 } 679 680 @Override onPageScrolled(int position, float positionOffset, int positionOffsetPixels)681 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 682 if (positionOffset < 0.0001) { 683 OnScreenListener before = mScreenListeners.get(position - 1); 684 if (before != null) { 685 before.onViewUpNext(); 686 } 687 OnScreenListener after = mScreenListeners.get(position + 1); 688 if (after != null) { 689 after.onViewUpNext(); 690 } 691 } 692 } 693 694 @Override onPageSelected(int position)695 public void onPageSelected(int position) { 696 mCurrentPhotoIndex = position; 697 setViewActivated(position); 698 } 699 700 @Override onPageScrollStateChanged(int state)701 public void onPageScrollStateChanged(int state) { 702 } 703 704 @Override isFragmentActive(Fragment fragment)705 public boolean isFragmentActive(Fragment fragment) { 706 if (mViewPager == null || mAdapter == null) { 707 return false; 708 } 709 return mViewPager.getCurrentItem() == mAdapter.getItemPosition(fragment); 710 } 711 712 @Override onFragmentVisible(PhotoViewFragment fragment)713 public void onFragmentVisible(PhotoViewFragment fragment) { 714 // Do nothing, we handle this in setViewActivated 715 } 716 717 @Override onTouchIntercept(float origX, float origY)718 public InterceptType onTouchIntercept(float origX, float origY) { 719 boolean interceptLeft = false; 720 boolean interceptRight = false; 721 722 for (OnScreenListener listener : mScreenListeners.values()) { 723 if (!interceptLeft) { 724 interceptLeft = listener.onInterceptMoveLeft(origX, origY); 725 } 726 if (!interceptRight) { 727 interceptRight = listener.onInterceptMoveRight(origX, origY); 728 } 729 } 730 731 if (interceptLeft) { 732 if (interceptRight) { 733 return InterceptType.BOTH; 734 } 735 return InterceptType.LEFT; 736 } else if (interceptRight) { 737 return InterceptType.RIGHT; 738 } 739 return InterceptType.NONE; 740 } 741 742 /** 743 * Updates the title bar according to the value of {@link #mFullScreen}. 744 */ setFullScreen(boolean fullScreen, boolean setDelayedRunnable)745 protected void setFullScreen(boolean fullScreen, boolean setDelayedRunnable) { 746 if (Util.isTouchExplorationEnabled(mAccessibilityManager)) { 747 // Disallow full screen mode when accessibility is enabled so that the action bar 748 // stays accessible. 749 fullScreen = false; 750 setDelayedRunnable = false; 751 } 752 753 final boolean fullScreenChanged = (fullScreen != mFullScreen); 754 mFullScreen = fullScreen; 755 756 if (mFullScreen) { 757 setLightsOutMode(true); 758 cancelEnterFullScreenRunnable(); 759 } else { 760 setLightsOutMode(false); 761 if (setDelayedRunnable) { 762 postEnterFullScreenRunnableWithDelay(); 763 } 764 } 765 766 if (fullScreenChanged) { 767 for (OnScreenListener listener : mScreenListeners.values()) { 768 listener.onFullScreenChanged(mFullScreen); 769 } 770 } 771 } 772 773 /** 774 * Posts a runnable to enter full screen after mEnterFullScreenDelayTime. This method is a 775 * no-op if mIsTimerLightsOutEnabled is set to false. 776 */ postEnterFullScreenRunnableWithDelay()777 private void postEnterFullScreenRunnableWithDelay() { 778 if (mIsTimerLightsOutEnabled) { 779 mHandler.postDelayed(mEnterFullScreenRunnable, mEnterFullScreenDelayTime); 780 } 781 } 782 cancelEnterFullScreenRunnable()783 private void cancelEnterFullScreenRunnable() { 784 mHandler.removeCallbacks(mEnterFullScreenRunnable); 785 } 786 setLightsOutMode(boolean enabled)787 protected void setLightsOutMode(boolean enabled) { 788 setImmersiveMode(enabled); 789 } 790 791 private final Runnable mEnterFullScreenRunnable = new Runnable() { 792 @Override 793 public void run() { 794 setFullScreen(true, true); 795 } 796 }; 797 798 @Override setViewActivated(int position)799 public void setViewActivated(int position) { 800 OnScreenListener listener = mScreenListeners.get(position); 801 if (listener != null) { 802 listener.onViewActivated(); 803 } 804 final Cursor cursor = getCursorAtProperPosition(); 805 mCurrentPhotoIndex = position; 806 // FLAG: get the column indexes once in onLoadFinished(). 807 // That would make this more efficient, instead of looking these up 808 // repeatedly whenever we want them. 809 int uriIndex = cursor.getColumnIndex(PhotoContract.PhotoViewColumns.URI); 810 mCurrentPhotoUri = cursor.getString(uriIndex); 811 updateActionBar(); 812 if (mAccessibilityManager.isEnabled() && lastAnnouncedTitle != position) { 813 String announcement = getPhotoAccessibilityAnnouncement(position); 814 if (announcement != null) { 815 Util.announceForAccessibility(mRootView, mAccessibilityManager, announcement); 816 lastAnnouncedTitle = position; 817 } 818 } 819 820 // Restart the timer to return to fullscreen. 821 cancelEnterFullScreenRunnable(); 822 postEnterFullScreenRunnableWithDelay(); 823 } 824 825 /** 826 * Adjusts the activity title and subtitle to reflect the photo name and count. 827 */ updateActionBar()828 public void updateActionBar() { 829 final int position = mViewPager.getCurrentItem() + 1; 830 final boolean hasAlbumCount = mAlbumCount >= 0; 831 832 final Cursor cursor = getCursorAtProperPosition(); 833 if (cursor != null) { 834 // FLAG: We should grab the indexes when we first get the cursor 835 // and store them so we don't need to do it each time. 836 final int photoNameIndex = cursor.getColumnIndex(PhotoContract.PhotoViewColumns.NAME); 837 mActionBarTitle = cursor.getString(photoNameIndex); 838 } else { 839 mActionBarTitle = null; 840 } 841 842 if (mIsEmpty || !hasAlbumCount || position <= 0) { 843 mActionBarSubtitle = null; 844 } else { 845 mActionBarSubtitle = mActivity.getResources().getString( 846 R.string.photo_view_count, position, mAlbumCount); 847 } 848 849 setActionBarTitles(mActivity.getActionBarInterface()); 850 } 851 852 /** 853 * Returns a string used as an announcement for accessibility after the user moves to a new 854 * photo. It will be called after {@link #updateActionBar} has been called. 855 * @param position the index in the album of the currently active photo 856 * @return announcement for accessibility 857 */ getPhotoAccessibilityAnnouncement(int position)858 protected String getPhotoAccessibilityAnnouncement(int position) { 859 String announcement = mActionBarTitle; 860 if (mActionBarSubtitle != null) { 861 announcement = mActivity.getContext().getResources().getString( 862 R.string.titles, mActionBarTitle, mActionBarSubtitle); 863 } 864 return announcement; 865 } 866 867 /** 868 * Sets the Action Bar title to {@link #mActionBarTitle} and the subtitle to 869 * {@link #mActionBarSubtitle} 870 */ setActionBarTitles(ActionBarInterface actionBar)871 protected final void setActionBarTitles(ActionBarInterface actionBar) { 872 if (actionBar == null) { 873 return; 874 } 875 actionBar.setTitle(getInputOrEmpty(mActionBarTitle)); 876 actionBar.setSubtitle(getInputOrEmpty(mActionBarSubtitle)); 877 } 878 879 /** 880 * If the input string is non-null, it is returned, otherwise an empty string is returned; 881 * @param in 882 * @return 883 */ getInputOrEmpty(String in)884 private static final String getInputOrEmpty(String in) { 885 if (in == null) { 886 return ""; 887 } 888 return in; 889 } 890 891 /** 892 * Utility method that will return the cursor that contains the data 893 * at the current position so that it refers to the current image on screen. 894 * @return the cursor at the current position or 895 * null if no cursor exists or if the {@link PhotoViewPager} is null. 896 */ getCursorAtProperPosition()897 public Cursor getCursorAtProperPosition() { 898 if (mViewPager == null) { 899 return null; 900 } 901 902 final int position = mViewPager.getCurrentItem(); 903 final Cursor cursor = mAdapter.getCursor(); 904 905 if (cursor == null) { 906 return null; 907 } 908 909 cursor.moveToPosition(position); 910 911 return cursor; 912 } 913 getCursor()914 public Cursor getCursor() { 915 return (mAdapter == null) ? null : mAdapter.getCursor(); 916 } 917 918 @Override onMenuVisibilityChanged(boolean isVisible)919 public void onMenuVisibilityChanged(boolean isVisible) { 920 if (isVisible) { 921 cancelEnterFullScreenRunnable(); 922 } else { 923 postEnterFullScreenRunnableWithDelay(); 924 } 925 } 926 927 @Override onNewPhotoLoaded(int position)928 public void onNewPhotoLoaded(int position) { 929 // do nothing 930 } 931 setPhotoIndex(int index)932 protected void setPhotoIndex(int index) { 933 mCurrentPhotoIndex = index; 934 } 935 936 @Override onFragmentPhotoLoadComplete(PhotoViewFragment fragment, boolean success)937 public void onFragmentPhotoLoadComplete(PhotoViewFragment fragment, boolean success) { 938 if (hasTemporaryImage() && mTemporaryImage.getVisibility() != View.GONE && 939 TextUtils.equals(fragment.getPhotoUri(), mCurrentPhotoUri)) { 940 if (success) { 941 // The fragment for the current image is now ready for display. 942 if (hasTemporaryImage()) { 943 mTemporaryImage.setVisibility(View.GONE); 944 } 945 mViewPager.setVisibility(View.VISIBLE); 946 } else { 947 // This means that we are unable to load the fragment's photo. 948 // I'm not sure what the best thing to do here is, but at least if 949 // we display the viewPager, the fragment itself can decide how to 950 // display the failure of its own image. 951 Log.w(TAG, "Failed to load fragment image"); 952 if (hasTemporaryImage()) { 953 mTemporaryImage.setVisibility(View.GONE); 954 } 955 mViewPager.setVisibility(View.VISIBLE); 956 } 957 mActivity.getSupportLoaderManager().destroyLoader( 958 PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL); 959 } 960 } 961 isFullScreen()962 protected boolean isFullScreen() { 963 return mFullScreen; 964 } 965 966 @Override onCursorChanged(PhotoViewFragment fragment, Cursor cursor)967 public void onCursorChanged(PhotoViewFragment fragment, Cursor cursor) { 968 // do nothing 969 } 970 971 @Override getAdapter()972 public PhotoPagerAdapter getAdapter() { 973 return mAdapter; 974 } 975 onEnterAnimationComplete()976 public void onEnterAnimationComplete() { 977 mEnterAnimationFinished = true; 978 mViewPager.setVisibility(View.VISIBLE); 979 setLightsOutMode(mFullScreen); 980 } 981 onExitAnimationComplete()982 private void onExitAnimationComplete() { 983 mActivity.finish(); 984 mActivity.overridePendingTransition(0, 0); 985 } 986 runEnterAnimation()987 private void runEnterAnimation() { 988 final int totalWidth = mRootView.getMeasuredWidth(); 989 final int totalHeight = mRootView.getMeasuredHeight(); 990 991 // FLAG: Need to handle the aspect ratio of the bitmap. If it's a portrait 992 // bitmap, then we need to position the view higher so that the middle 993 // pixels line up. 994 if (hasTemporaryImage()) { 995 mTemporaryImage.setVisibility(View.VISIBLE); 996 } 997 // We need to take a full screen image, and scale/translate it so that 998 // it appears at exactly the same location onscreen as it is in the 999 // prior activity. 1000 // The final image will take either the full screen width or height (or both). 1001 1002 final float scaleW = (float) mAnimationStartWidth / totalWidth; 1003 final float scaleY = (float) mAnimationStartHeight / totalHeight; 1004 final float scale = Math.max(scaleW, scaleY); 1005 1006 final int translateX = calculateTranslate(mAnimationStartX, mAnimationStartWidth, 1007 totalWidth, scale); 1008 final int translateY = calculateTranslate(mAnimationStartY, mAnimationStartHeight, 1009 totalHeight, scale); 1010 1011 final int version = android.os.Build.VERSION.SDK_INT; 1012 if (version >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 1013 if (hasBackground()) { 1014 mBackground.setAlpha(0f); 1015 mBackground.animate().alpha(1f).setDuration(ENTER_ANIMATION_DURATION_MS).start(); 1016 mBackground.setVisibility(View.VISIBLE); 1017 } 1018 1019 if (hasTemporaryImage()) { 1020 mTemporaryImage.setScaleX(scale); 1021 mTemporaryImage.setScaleY(scale); 1022 mTemporaryImage.setTranslationX(translateX); 1023 mTemporaryImage.setTranslationY(translateY); 1024 1025 Runnable endRunnable = new Runnable() { 1026 @Override 1027 public void run() { 1028 PhotoViewController.this.onEnterAnimationComplete(); 1029 } 1030 }; 1031 ViewPropertyAnimator animator = mTemporaryImage.animate().scaleX(1f).scaleY(1f) 1032 .translationX(0).translationY(0).setDuration(ENTER_ANIMATION_DURATION_MS); 1033 if (version >= Build.VERSION_CODES.JELLY_BEAN) { 1034 animator.withEndAction(endRunnable); 1035 } else { 1036 mHandler.postDelayed(endRunnable, ENTER_ANIMATION_DURATION_MS); 1037 } 1038 animator.start(); 1039 } 1040 } else { 1041 if (hasBackground()) { 1042 final Animation alphaAnimation = new AlphaAnimation(0f, 1f); 1043 alphaAnimation.setDuration(ENTER_ANIMATION_DURATION_MS); 1044 mBackground.startAnimation(alphaAnimation); 1045 mBackground.setVisibility(View.VISIBLE); 1046 } 1047 1048 if (hasTemporaryImage()) { 1049 final Animation translateAnimation = new TranslateAnimation(translateX, 1050 translateY, 0, 0); 1051 translateAnimation.setDuration(ENTER_ANIMATION_DURATION_MS); 1052 Animation scaleAnimation = new ScaleAnimation(scale, scale, 0, 0); 1053 scaleAnimation.setDuration(ENTER_ANIMATION_DURATION_MS); 1054 1055 AnimationSet animationSet = new AnimationSet(true); 1056 animationSet.addAnimation(translateAnimation); 1057 animationSet.addAnimation(scaleAnimation); 1058 AnimationListener listener = new AnimationListener() { 1059 @Override 1060 public void onAnimationEnd(Animation arg0) { 1061 PhotoViewController.this.onEnterAnimationComplete(); 1062 } 1063 1064 @Override 1065 public void onAnimationRepeat(Animation arg0) { 1066 } 1067 1068 @Override 1069 public void onAnimationStart(Animation arg0) { 1070 } 1071 }; 1072 animationSet.setAnimationListener(listener); 1073 mTemporaryImage.startAnimation(animationSet); 1074 } 1075 } 1076 } 1077 runExitAnimation()1078 private void runExitAnimation() { 1079 Intent intent = mActivity.getIntent(); 1080 // FLAG: should just fall back to a standard animation if either: 1081 // 1. images have been added or removed since we've been here, or 1082 // 2. we are currently looking at some image other than the one we 1083 // started on. 1084 1085 final int totalWidth = mRootView.getMeasuredWidth(); 1086 final int totalHeight = mRootView.getMeasuredHeight(); 1087 1088 // We need to take a full screen image, and scale/translate it so that 1089 // it appears at exactly the same location onscreen as it is in the 1090 // prior activity. 1091 // The final image will take either the full screen width or height (or both). 1092 final float scaleW = (float) mAnimationStartWidth / totalWidth; 1093 final float scaleY = (float) mAnimationStartHeight / totalHeight; 1094 final float scale = Math.max(scaleW, scaleY); 1095 1096 final int translateX = calculateTranslate(mAnimationStartX, mAnimationStartWidth, 1097 totalWidth, scale); 1098 final int translateY = calculateTranslate(mAnimationStartY, mAnimationStartHeight, 1099 totalHeight, scale); 1100 final int version = android.os.Build.VERSION.SDK_INT; 1101 if (version >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 1102 if (hasBackground()) { 1103 mBackground.animate().alpha(0f).setDuration(EXIT_ANIMATION_DURATION_MS).start(); 1104 mBackground.setVisibility(View.VISIBLE); 1105 } 1106 1107 Runnable endRunnable = new Runnable() { 1108 @Override 1109 public void run() { 1110 PhotoViewController.this.onExitAnimationComplete(); 1111 } 1112 }; 1113 // If the temporary image is still visible it means that we have 1114 // not yet loaded the fullres image, so we need to animate 1115 // the temporary image out. 1116 ViewPropertyAnimator animator = null; 1117 if (hasTemporaryImage() && mTemporaryImage.getVisibility() == View.VISIBLE) { 1118 animator = mTemporaryImage.animate().scaleX(scale).scaleY(scale) 1119 .translationX(translateX).translationY(translateY) 1120 .setDuration(EXIT_ANIMATION_DURATION_MS); 1121 } else { 1122 animator = mViewPager.animate().scaleX(scale).scaleY(scale) 1123 .translationX(translateX).translationY(translateY) 1124 .setDuration(EXIT_ANIMATION_DURATION_MS); 1125 } 1126 // If the user has swiped to a different photo, fade out the current photo 1127 // along with the scale animation. 1128 if (!mInitialPhotoUri.equals(mCurrentPhotoUri)) { 1129 animator.alpha(0f); 1130 } 1131 if (version >= android.os.Build.VERSION_CODES.JELLY_BEAN) { 1132 animator.withEndAction(endRunnable); 1133 } else { 1134 mHandler.postDelayed(endRunnable, EXIT_ANIMATION_DURATION_MS); 1135 } 1136 animator.start(); 1137 } else { 1138 if (hasBackground()) { 1139 final Animation alphaAnimation = new AlphaAnimation(1f, 0f); 1140 alphaAnimation.setDuration(EXIT_ANIMATION_DURATION_MS); 1141 mBackground.startAnimation(alphaAnimation); 1142 mBackground.setVisibility(View.VISIBLE); 1143 } 1144 1145 final Animation scaleAnimation = new ScaleAnimation(1f, 1f, scale, scale); 1146 scaleAnimation.setDuration(EXIT_ANIMATION_DURATION_MS); 1147 AnimationListener listener = new AnimationListener() { 1148 @Override 1149 public void onAnimationEnd(Animation arg0) { 1150 PhotoViewController.this.onExitAnimationComplete(); 1151 } 1152 1153 @Override 1154 public void onAnimationRepeat(Animation arg0) { 1155 } 1156 1157 @Override 1158 public void onAnimationStart(Animation arg0) { 1159 } 1160 }; 1161 scaleAnimation.setAnimationListener(listener); 1162 // If the temporary image is still visible it means that we have 1163 // not yet loaded the fullres image, so we need to animate 1164 // the temporary image out. 1165 if (hasTemporaryImage() && mTemporaryImage.getVisibility() == View.VISIBLE) { 1166 mTemporaryImage.startAnimation(scaleAnimation); 1167 } else { 1168 mViewPager.startAnimation(scaleAnimation); 1169 } 1170 } 1171 } 1172 calculateTranslate(int start, int startSize, int totalSize, float scale)1173 private int calculateTranslate(int start, int startSize, int totalSize, float scale) { 1174 // Translation takes precedence over scale. What this means is that if 1175 // we want an view's upper left corner to be a particular spot on screen, 1176 // but that view is scaled to something other than 1, we need to take into 1177 // account the pixels lost to scaling. 1178 // So if we have a view that is 200x300, and we want it's upper left corner 1179 // to be at 50x50, but it's scaled by 50%, we can't just translate it to 50x50. 1180 // If we were to do that, the view's *visible* upper left corner would be at 1181 // 100x200. We need to take into account the difference between the outside 1182 // size of the view (i.e. the size prior to scaling) and the scaled size. 1183 // scaleFromEdge is the difference between the visible left edge and the 1184 // actual left edge, due to scaling. 1185 // scaleFromTop is the difference between the visible top edge, and the 1186 // actual top edge, due to scaling. 1187 int scaleFromEdge = Math.round((totalSize - totalSize * scale) / 2); 1188 1189 // The imageView is fullscreen, regardless of the aspect ratio of the actual image. 1190 // This means that some portion of the imageView will be blank. We need to 1191 // take into account the size of the blank area so that the actual image 1192 // lines up with the starting image. 1193 int blankSize = Math.round((totalSize * scale - startSize) / 2); 1194 1195 return start - scaleFromEdge - blankSize; 1196 } 1197 initTemporaryImage(Drawable drawable)1198 private void initTemporaryImage(Drawable drawable) { 1199 if (mEnterAnimationFinished) { 1200 // Forget this, we've already run the animation. 1201 return; 1202 } 1203 if (hasTemporaryImage()) { 1204 mTemporaryImage.setImageDrawable(drawable); 1205 } 1206 if (drawable != null) { 1207 // We have not yet run the enter animation. Start it now. 1208 int totalWidth = mRootView.getMeasuredWidth(); 1209 if (totalWidth == 0) { 1210 // the measure pass has not yet finished. We can't properly 1211 // run out animation until that is done. Listen for the layout 1212 // to occur, then fire the animation. 1213 final View base = mRootView; 1214 base.getViewTreeObserver().addOnGlobalLayoutListener( 1215 new OnGlobalLayoutListener() { 1216 @Override 1217 public void onGlobalLayout() { 1218 int version = android.os.Build.VERSION.SDK_INT; 1219 if (version >= android.os.Build.VERSION_CODES.JELLY_BEAN) { 1220 base.getViewTreeObserver().removeOnGlobalLayoutListener(this); 1221 } else { 1222 base.getViewTreeObserver().removeGlobalOnLayoutListener(this); 1223 } 1224 runEnterAnimation(); 1225 } 1226 }); 1227 } else { 1228 // initiate the animation 1229 runEnterAnimation(); 1230 } 1231 } 1232 // Kick off the photo list loader 1233 mActivity.getSupportLoaderManager().initLoader(LOADER_PHOTO_LIST, null, this); 1234 } 1235 showActionBar()1236 public void showActionBar() { 1237 mActivity.getActionBarInterface().show(); 1238 } 1239 hideActionBar()1240 public void hideActionBar() { 1241 mActivity.getActionBarInterface().hide(); 1242 } 1243 isScaleAnimationEnabled()1244 public boolean isScaleAnimationEnabled() { 1245 return mScaleAnimationEnabled; 1246 } 1247 isEnterAnimationFinished()1248 public boolean isEnterAnimationFinished() { 1249 return mEnterAnimationFinished; 1250 } 1251 getRootView()1252 public View getRootView() { 1253 return mRootView; 1254 } 1255 1256 private class BitmapCallback implements LoaderManager.LoaderCallbacks<BitmapResult> { 1257 1258 @Override onCreateLoader(int id, Bundle args)1259 public Loader<BitmapResult> onCreateLoader(int id, Bundle args) { 1260 String uri = args.getString(ARG_IMAGE_URI); 1261 switch (id) { 1262 case PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL: 1263 return onCreateBitmapLoader(PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL, 1264 args, uri); 1265 case PhotoViewCallbacks.BITMAP_LOADER_AVATAR: 1266 return onCreateBitmapLoader(PhotoViewCallbacks.BITMAP_LOADER_AVATAR, 1267 args, uri); 1268 } 1269 return null; 1270 } 1271 1272 @Override onLoadFinished(Loader<BitmapResult> loader, BitmapResult result)1273 public void onLoadFinished(Loader<BitmapResult> loader, BitmapResult result) { 1274 Drawable drawable = result.getDrawable(mActivity.getResources()); 1275 final ActionBarInterface actionBar = mActivity.getActionBarInterface(); 1276 switch (loader.getId()) { 1277 case PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL: 1278 // We just loaded the initial thumbnail that we can display 1279 // while waiting for the full viewPager to get initialized. 1280 initTemporaryImage(drawable); 1281 break; 1282 case PhotoViewCallbacks.BITMAP_LOADER_AVATAR: 1283 if (drawable == null) { 1284 actionBar.setLogo(null); 1285 } else { 1286 actionBar.setLogo(drawable); 1287 } 1288 break; 1289 } 1290 } 1291 1292 @Override onLoaderReset(Loader<BitmapResult> loader)1293 public void onLoaderReset(Loader<BitmapResult> loader) { 1294 // Do nothing 1295 } 1296 } 1297 setImmersiveMode(boolean enabled)1298 public void setImmersiveMode(boolean enabled) { 1299 int flags = 0; 1300 final int version = Build.VERSION.SDK_INT; 1301 final boolean manuallyUpdateActionBar = version < Build.VERSION_CODES.JELLY_BEAN; 1302 if (enabled && 1303 (!isScaleAnimationEnabled() || isEnterAnimationFinished())) { 1304 // Turning on immersive mode causes an animation. If the scale animation is enabled and 1305 // the enter animation isn't yet complete, then an immersive mode animation should not 1306 // occur, since two concurrent animations are very janky. 1307 1308 // Disable immersive mode for seconary users to prevent b/12015090 (freezing crash) 1309 // This is fixed in KK_MR2 but there is no way to differentiate between KK and KK_MR2. 1310 if (version > Build.VERSION_CODES.KITKAT || 1311 version == Build.VERSION_CODES.KITKAT && !kitkatIsSecondaryUser()) { 1312 flags = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 1313 | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 1314 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE 1315 | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 1316 | View.SYSTEM_UI_FLAG_FULLSCREEN 1317 | View.SYSTEM_UI_FLAG_IMMERSIVE; 1318 } else if (version >= Build.VERSION_CODES.JELLY_BEAN) { 1319 // Clients that use the scale animation should set the following system UI flags to 1320 // prevent janky animations on exit when the status bar is hidden: 1321 // View.SYSTEM_UI_FLAG_VISIBLE | View.SYSTEM_UI_FLAG_STABLE 1322 // As well, client should ensure `android:fitsSystemWindows` is set on the root 1323 // content view. 1324 flags = View.SYSTEM_UI_FLAG_LOW_PROFILE 1325 | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 1326 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE 1327 | View.SYSTEM_UI_FLAG_FULLSCREEN; 1328 } else if (version >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 1329 flags = View.SYSTEM_UI_FLAG_LOW_PROFILE; 1330 } else if (version >= Build.VERSION_CODES.HONEYCOMB) { 1331 flags = View.STATUS_BAR_HIDDEN; 1332 } 1333 1334 if (manuallyUpdateActionBar) { 1335 hideActionBar(); 1336 } 1337 } else { 1338 if (version >= Build.VERSION_CODES.KITKAT) { 1339 flags = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 1340 | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 1341 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE; 1342 } else if (version >= Build.VERSION_CODES.JELLY_BEAN) { 1343 flags = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 1344 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE; 1345 } else if (version >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 1346 flags = View.SYSTEM_UI_FLAG_VISIBLE; 1347 } else if (version >= Build.VERSION_CODES.HONEYCOMB) { 1348 flags = View.STATUS_BAR_VISIBLE; 1349 } 1350 1351 if (manuallyUpdateActionBar) { 1352 showActionBar(); 1353 } 1354 } 1355 1356 if (version >= Build.VERSION_CODES.HONEYCOMB) { 1357 mLastFlags = flags; 1358 getRootView().setSystemUiVisibility(flags); 1359 } 1360 } 1361 1362 /** 1363 * Return true iff the app is being run as a secondary user on kitkat. 1364 * 1365 * This is a hack which we only know to work on kitkat. 1366 */ 1367 private boolean kitkatIsSecondaryUser() { 1368 if (Build.VERSION.SDK_INT != Build.VERSION_CODES.KITKAT) { 1369 throw new IllegalStateException("kitkatIsSecondary user is only callable on KitKat"); 1370 } 1371 return Process.myUid() > 100000; 1372 } 1373 1374 /** 1375 * Note: This should only be called when API level is 11 or above. 1376 */ 1377 public View.OnSystemUiVisibilityChangeListener getSystemUiVisibilityChangeListener() { 1378 return mSystemUiVisibilityChangeListener; 1379 } 1380 } 1381