PhotoViewActivity.java revision 81458718db7c4054ed5e6a377ca34ff9b3e8ebb3
1/* 2 * Copyright (C) 2011 Google Inc. 3 * Licensed to The Android Open Source Project. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18package com.android.ex.photo; 19 20import android.app.Activity; 21import android.app.ActivityManager; 22import android.content.Context; 23import android.content.Intent; 24import android.content.res.Resources; 25import android.database.Cursor; 26import android.graphics.Bitmap; 27import android.graphics.drawable.BitmapDrawable; 28import android.graphics.drawable.Drawable; 29import android.net.Uri; 30import android.os.Build; 31import android.os.Bundle; 32import android.os.Handler; 33import android.support.v4.app.Fragment; 34import android.support.v4.app.LoaderManager; 35import android.support.v4.content.Loader; 36import android.support.v4.view.ViewPager.OnPageChangeListener; 37import android.view.MenuItem; 38import android.support.v7.app.ActionBar; 39import android.support.v7.app.ActionBar.OnMenuVisibilityListener; 40import android.support.v7.app.ActionBarActivity; 41import android.text.TextUtils; 42import android.util.Log; 43import android.view.View; 44import android.view.ViewPropertyAnimator; 45import android.view.ViewTreeObserver.OnGlobalLayoutListener; 46import android.view.animation.AlphaAnimation; 47import android.view.animation.Animation; 48import android.view.animation.AnimationSet; 49import android.view.animation.ScaleAnimation; 50import android.view.animation.TranslateAnimation; 51import android.view.animation.Animation.AnimationListener; 52import android.widget.ImageView; 53 54import com.android.ex.photo.PhotoViewPager.InterceptType; 55import com.android.ex.photo.PhotoViewPager.OnInterceptTouchListener; 56import com.android.ex.photo.adapters.PhotoPagerAdapter; 57import com.android.ex.photo.fragments.PhotoViewFragment; 58import com.android.ex.photo.loaders.PhotoBitmapLoader; 59import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult; 60import com.android.ex.photo.loaders.PhotoPagerLoader; 61import com.android.ex.photo.provider.PhotoContract; 62 63import java.util.HashMap; 64import java.util.HashSet; 65import java.util.Map; 66import java.util.Set; 67 68/** 69 * Activity to view the contents of an album. 70 */ 71public class PhotoViewActivity extends ActionBarActivity implements 72 LoaderManager.LoaderCallbacks<Cursor>, OnPageChangeListener, OnInterceptTouchListener, 73 OnMenuVisibilityListener, PhotoViewCallbacks, 74 PhotoViewController.PhotoViewControllerCallbacks { 75 76 private final static String TAG = "PhotoViewActivity"; 77 78 private final static String STATE_CURRENT_URI_KEY = 79 "com.google.android.apps.plus.PhotoViewFragment.CURRENT_URI"; 80 private final static String STATE_CURRENT_INDEX_KEY = 81 "com.google.android.apps.plus.PhotoViewFragment.CURRENT_INDEX"; 82 private final static String STATE_FULLSCREEN_KEY = 83 "com.google.android.apps.plus.PhotoViewFragment.FULLSCREEN"; 84 private final static String STATE_ACTIONBARTITLE_KEY = 85 "com.google.android.apps.plus.PhotoViewFragment.ACTIONBARTITLE"; 86 private final static String STATE_ACTIONBARSUBTITLE_KEY = 87 "com.google.android.apps.plus.PhotoViewFragment.ACTIONBARTITLE"; 88 private final static String STATE_ENTERANIMATIONFINISHED_KEY = 89 "com.google.android.apps.plus.PhotoViewFragment.SCALEANIMATIONFINISHED"; 90 91 protected final static String ARG_IMAGE_URI = "image_uri"; 92 93 private static final int LOADER_PHOTO_LIST = 100; 94 95 /** Count used when the real photo count is unknown [but, may be determined] */ 96 public static final int ALBUM_COUNT_UNKNOWN = -1; 97 98 public static final int ENTER_ANIMATION_DURATION_MS = 250; 99 public static final int EXIT_ANIMATION_DURATION_MS = 250; 100 101 /** Argument key for the dialog message */ 102 public static final String KEY_MESSAGE = "dialog_message"; 103 104 public static int sMemoryClass; 105 106 /** The URI of the photos we're viewing; may be {@code null} */ 107 private String mPhotosUri; 108 /** The index of the currently viewed photo */ 109 private int mCurrentPhotoIndex; 110 /** The uri of the currently viewed photo */ 111 private String mCurrentPhotoUri; 112 /** The query projection to use; may be {@code null} */ 113 private String[] mProjection; 114 /** The total number of photos; only valid if {@link #mIsEmpty} is {@code false}. */ 115 protected int mAlbumCount = ALBUM_COUNT_UNKNOWN; 116 /** {@code true} if the view is empty. Otherwise, {@code false}. */ 117 protected boolean mIsEmpty; 118 /** the main root view */ 119 protected View mRootView; 120 /** Background image that contains nothing, so it can be alpha faded from 121 * transparent to black without affecting any other views. */ 122 protected View mBackground; 123 /** The main pager; provides left/right swipe between photos */ 124 protected PhotoViewPager mViewPager; 125 /** The temporary image so that we can quickly scale up the fullscreen thumbnail */ 126 protected ImageView mTemporaryImage; 127 /** Adapter to create pager views */ 128 protected PhotoPagerAdapter mAdapter; 129 /** Whether or not we're in "full screen" mode */ 130 protected boolean mFullScreen; 131 /** The listeners wanting full screen state for each screen position */ 132 private final Map<Integer, OnScreenListener> 133 mScreenListeners = new HashMap<Integer, OnScreenListener>(); 134 /** The set of listeners wanting full screen state */ 135 private final Set<CursorChangedListener> mCursorListeners = new HashSet<CursorChangedListener>(); 136 /** When {@code true}, restart the loader when the activity becomes active */ 137 private boolean mKickLoader; 138 /** Whether or not this activity is paused */ 139 protected boolean mIsPaused = true; 140 /** The maximum scale factor applied to images when they are initially displayed */ 141 protected float mMaxInitialScale; 142 /** The title in the actionbar */ 143 protected String mActionBarTitle; 144 /** The subtitle in the actionbar */ 145 protected String mActionBarSubtitle; 146 147 private boolean mEnterAnimationFinished; 148 protected boolean mScaleAnimationEnabled; 149 protected int mAnimationStartX; 150 protected int mAnimationStartY; 151 protected int mAnimationStartWidth; 152 protected int mAnimationStartHeight; 153 154 protected boolean mActionBarHiddenInitially; 155 protected boolean mDisplayThumbsFullScreen; 156 157 protected BitmapCallback mBitmapCallback; 158 protected final Handler mHandler = new Handler(); 159 160 // TODO Find a better way to do this. We basically want the activity to display the 161 // "loading..." progress until the fragment takes over and shows it's own "loading..." 162 // progress [located in photo_header_view.xml]. We could potentially have all status displayed 163 // by the activity, but, that gets tricky when it comes to screen rotation. For now, we 164 // track the loading by this variable which is fragile and may cause phantom "loading..." 165 // text. 166 private long mEnterFullScreenDelayTime; 167 168 private PhotoViewController mController; 169 170 protected PhotoPagerAdapter createPhotoPagerAdapter(Context context, 171 android.support.v4.app.FragmentManager fm, Cursor c, float maxScale) { 172 PhotoPagerAdapter adapter = new PhotoPagerAdapter(context, fm, c, maxScale, 173 mDisplayThumbsFullScreen); 174 return adapter; 175 } 176 177 @Override 178 protected void onCreate(Bundle savedInstanceState) { 179 super.onCreate(savedInstanceState); 180 181 final ActivityManager mgr = (ActivityManager) getApplicationContext(). 182 getSystemService(Activity.ACTIVITY_SERVICE); 183 sMemoryClass = mgr.getMemoryClass(); 184 185 mController = new PhotoViewController(this); 186 187 final Intent intent = getIntent(); 188 // uri of the photos to view; optional 189 if (intent.hasExtra(Intents.EXTRA_PHOTOS_URI)) { 190 mPhotosUri = intent.getStringExtra(Intents.EXTRA_PHOTOS_URI); 191 } 192 if (intent.getBooleanExtra(Intents.EXTRA_SCALE_UP_ANIMATION, false)) { 193 mScaleAnimationEnabled = true; 194 mAnimationStartX = intent.getIntExtra(Intents.EXTRA_ANIMATION_START_X, 0); 195 mAnimationStartY = intent.getIntExtra(Intents.EXTRA_ANIMATION_START_Y, 0); 196 mAnimationStartWidth = intent.getIntExtra(Intents.EXTRA_ANIMATION_START_WIDTH, 0); 197 mAnimationStartHeight = intent.getIntExtra(Intents.EXTRA_ANIMATION_START_HEIGHT, 0); 198 } 199 mActionBarHiddenInitially = intent.getBooleanExtra( 200 Intents.EXTRA_ACTION_BAR_HIDDEN_INITIALLY, false); 201 mDisplayThumbsFullScreen = intent.getBooleanExtra( 202 Intents.EXTRA_DISPLAY_THUMBS_FULLSCREEN, false); 203 204 // projection for the query; optional 205 // If not set, the default projection is used. 206 // This projection must include the columns from the default projection. 207 if (intent.hasExtra(Intents.EXTRA_PROJECTION)) { 208 mProjection = intent.getStringArrayExtra(Intents.EXTRA_PROJECTION); 209 } else { 210 mProjection = null; 211 } 212 213 // Set the max initial scale, defaulting to 1x 214 mMaxInitialScale = intent.getFloatExtra(Intents.EXTRA_MAX_INITIAL_SCALE, 1.0f); 215 mCurrentPhotoUri = null; 216 mCurrentPhotoIndex = -1; 217 218 // We allow specifying the current photo by either index or uri. 219 // This is because some users may have live datasets that can change, 220 // adding new items to either the beginning or end of the set. For clients 221 // that do not need that capability, ability to specify the current photo 222 // by index is offered as a convenience. 223 if (intent.hasExtra(Intents.EXTRA_PHOTO_INDEX)) { 224 mCurrentPhotoIndex = intent.getIntExtra(Intents.EXTRA_PHOTO_INDEX, -1); 225 } 226 if (intent.hasExtra(Intents.EXTRA_INITIAL_PHOTO_URI)) { 227 mCurrentPhotoUri = intent.getStringExtra(Intents.EXTRA_INITIAL_PHOTO_URI); 228 } 229 mIsEmpty = true; 230 231 if (savedInstanceState != null) { 232 mCurrentPhotoUri = savedInstanceState.getString(STATE_CURRENT_URI_KEY); 233 mCurrentPhotoIndex = savedInstanceState.getInt(STATE_CURRENT_INDEX_KEY); 234 mFullScreen = savedInstanceState.getBoolean(STATE_FULLSCREEN_KEY, false); 235 mActionBarTitle = savedInstanceState.getString(STATE_ACTIONBARTITLE_KEY); 236 mActionBarSubtitle = savedInstanceState.getString(STATE_ACTIONBARSUBTITLE_KEY); 237 mEnterAnimationFinished = savedInstanceState.getBoolean( 238 STATE_ENTERANIMATIONFINISHED_KEY, false); 239 } else { 240 mFullScreen = mActionBarHiddenInitially; 241 } 242 243 setContentView(R.layout.photo_activity_view); 244 245 // Create the adapter and add the view pager 246 mAdapter = 247 createPhotoPagerAdapter(this, getSupportFragmentManager(), null, mMaxInitialScale); 248 final Resources resources = getResources(); 249 mRootView = findViewById(R.id.photo_activity_root_view); 250 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 251 mRootView.setOnSystemUiVisibilityChangeListener( 252 mController.getSystemUiVisibilityChangeListener()); 253 } 254 mBackground = findViewById(R.id.photo_activity_background); 255 mTemporaryImage = (ImageView) findViewById(R.id.photo_activity_temporary_image); 256 mViewPager = (PhotoViewPager) findViewById(R.id.photo_view_pager); 257 mViewPager.setAdapter(mAdapter); 258 mViewPager.setOnPageChangeListener(this); 259 mViewPager.setOnInterceptTouchListener(this); 260 mViewPager.setPageMargin(resources.getDimensionPixelSize(R.dimen.photo_page_margin)); 261 262 mBitmapCallback = new BitmapCallback(); 263 if (!mScaleAnimationEnabled || mEnterAnimationFinished) { 264 // We are not running the scale up animation. Just let the fragments 265 // display and handle the animation. 266 getSupportLoaderManager().initLoader(LOADER_PHOTO_LIST, null, this); 267 // Make the background opaque immediately so that we don't see the activity 268 // behind this one. 269 mBackground.setVisibility(View.VISIBLE); 270 } else { 271 // Attempt to load the initial image thumbnail. Once we have the 272 // image, animate it up. Once the animation is complete, we can kick off 273 // loading the ViewPager. After the primary fullres image is loaded, we will 274 // make our temporary image invisible and display the ViewPager. 275 mViewPager.setVisibility(View.GONE); 276 Bundle args = new Bundle(); 277 args.putString(ARG_IMAGE_URI, mCurrentPhotoUri); 278 getSupportLoaderManager().initLoader(BITMAP_LOADER_THUMBNAIL, args, mBitmapCallback); 279 } 280 281 mEnterFullScreenDelayTime = 282 resources.getInteger(R.integer.reenter_fullscreen_delay_time_in_millis); 283 284 final ActionBar actionBar = getSupportActionBar(); 285 if (actionBar != null) { 286 actionBar.setDisplayHomeAsUpEnabled(true); 287 actionBar.addOnMenuVisibilityListener(this); 288 final int showTitle = ActionBar.DISPLAY_SHOW_TITLE; 289 actionBar.setDisplayOptions(showTitle, showTitle); 290 // Set the title and subtitle immediately here, rather than waiting 291 // for the fragment to be initialized. 292 setActionBarTitles(actionBar); 293 } 294 295 if (!mScaleAnimationEnabled) { 296 setLightsOutMode(mFullScreen); 297 } else { 298 // Keep lights out mode as false. This is to prevent jank cause by concurrent 299 // animations during the enter animation. 300 setLightsOutMode(false); 301 } 302 } 303 304 @Override 305 protected void onResume() { 306 super.onResume(); 307 setFullScreen(mFullScreen, false); 308 309 mIsPaused = false; 310 if (mKickLoader) { 311 mKickLoader = false; 312 getSupportLoaderManager().initLoader(LOADER_PHOTO_LIST, null, this); 313 } 314 } 315 316 @Override 317 protected void onPause() { 318 mIsPaused = true; 319 super.onPause(); 320 } 321 322 @Override 323 public void onBackPressed() { 324 // If we are in fullscreen mode, and the default is not full screen, then 325 // switch back to actionBar display mode. 326 if (mFullScreen && !mActionBarHiddenInitially) { 327 toggleFullScreen(); 328 } else { 329 if (mScaleAnimationEnabled) { 330 runExitAnimation(); 331 } else { 332 super.onBackPressed(); 333 } 334 } 335 } 336 337 @Override 338 public void onSaveInstanceState(Bundle outState) { 339 super.onSaveInstanceState(outState); 340 341 outState.putString(STATE_CURRENT_URI_KEY, mCurrentPhotoUri); 342 outState.putInt(STATE_CURRENT_INDEX_KEY, mCurrentPhotoIndex); 343 outState.putBoolean(STATE_FULLSCREEN_KEY, mFullScreen); 344 outState.putString(STATE_ACTIONBARTITLE_KEY, mActionBarTitle); 345 outState.putString(STATE_ACTIONBARSUBTITLE_KEY, mActionBarSubtitle); 346 outState.putBoolean(STATE_ENTERANIMATIONFINISHED_KEY, mEnterAnimationFinished); 347 } 348 349 @Override 350 public boolean onOptionsItemSelected(MenuItem item) { 351 switch (item.getItemId()) { 352 case android.R.id.home: 353 finish(); 354 return true; 355 default: 356 return super.onOptionsItemSelected(item); 357 } 358 } 359 360 @Override 361 public void addScreenListener(int position, OnScreenListener listener) { 362 mScreenListeners.put(position, listener); 363 } 364 365 @Override 366 public void removeScreenListener(int position) { 367 mScreenListeners.remove(position); 368 } 369 370 @Override 371 public synchronized void addCursorListener(CursorChangedListener listener) { 372 mCursorListeners.add(listener); 373 } 374 375 @Override 376 public synchronized void removeCursorListener(CursorChangedListener listener) { 377 mCursorListeners.remove(listener); 378 } 379 380 @Override 381 public boolean isFragmentFullScreen(Fragment fragment) { 382 if (mViewPager == null || mAdapter == null || mAdapter.getCount() == 0) { 383 return mFullScreen; 384 } 385 return mFullScreen || (mViewPager.getCurrentItem() != mAdapter.getItemPosition(fragment)); 386 } 387 388 @Override 389 public void toggleFullScreen() { 390 setFullScreen(!mFullScreen, true); 391 } 392 393 public void onPhotoRemoved(long photoId) { 394 final Cursor data = mAdapter.getCursor(); 395 if (data == null) { 396 // Huh?! How would this happen? 397 return; 398 } 399 400 final int dataCount = data.getCount(); 401 if (dataCount <= 1) { 402 finish(); 403 return; 404 } 405 406 getSupportLoaderManager().restartLoader(LOADER_PHOTO_LIST, null, this); 407 } 408 409 @Override 410 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 411 if (id == LOADER_PHOTO_LIST) { 412 return new PhotoPagerLoader(this, Uri.parse(mPhotosUri), mProjection); 413 } 414 return null; 415 } 416 417 @Override 418 public Loader<BitmapResult> onCreateBitmapLoader(int id, Bundle args, String uri) { 419 switch (id) { 420 case BITMAP_LOADER_AVATAR: 421 case BITMAP_LOADER_THUMBNAIL: 422 case BITMAP_LOADER_PHOTO: 423 return new PhotoBitmapLoader(this, uri); 424 default: 425 return null; 426 } 427 } 428 429 @Override 430 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 431 432 final int id = loader.getId(); 433 if (id == LOADER_PHOTO_LIST) { 434 if (data == null || data.getCount() == 0) { 435 mIsEmpty = true; 436 mAdapter.swapCursor(null); 437 } else { 438 mAlbumCount = data.getCount(); 439 if (mCurrentPhotoUri != null) { 440 int index = 0; 441 // Clear query params. Compare only the path. 442 final int uriIndex = data.getColumnIndex(PhotoContract.PhotoViewColumns.URI); 443 final Uri currentPhotoUri; 444 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 445 currentPhotoUri = Uri.parse(mCurrentPhotoUri).buildUpon() 446 .clearQuery().build(); 447 } else { 448 currentPhotoUri = Uri.parse(mCurrentPhotoUri).buildUpon() 449 .query(null).build(); 450 } 451 while (data.moveToNext()) { 452 final String uriString = data.getString(uriIndex); 453 final Uri uri; 454 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 455 uri = Uri.parse(uriString).buildUpon().clearQuery().build(); 456 } else { 457 uri = Uri.parse(uriString).buildUpon().query(null).build(); 458 } 459 if (currentPhotoUri != null && currentPhotoUri.equals(uri)) { 460 mCurrentPhotoIndex = index; 461 break; 462 } 463 index++; 464 } 465 } 466 467 // We're paused; don't do anything now, we'll get re-invoked 468 // when the activity becomes active again 469 // TODO(pwestbro): This shouldn't be necessary, as the loader manager should 470 // restart the loader 471 if (mIsPaused) { 472 mKickLoader = true; 473 mAdapter.swapCursor(null); 474 return; 475 } 476 boolean wasEmpty = mIsEmpty; 477 mIsEmpty = false; 478 479 mAdapter.swapCursor(data); 480 if (mViewPager.getAdapter() == null) { 481 mViewPager.setAdapter(mAdapter); 482 } 483 notifyCursorListeners(data); 484 485 // Use an index of 0 if the index wasn't specified or couldn't be found 486 if (mCurrentPhotoIndex < 0) { 487 mCurrentPhotoIndex = 0; 488 } 489 490 mViewPager.setCurrentItem(mCurrentPhotoIndex, false); 491 if (wasEmpty) { 492 setViewActivated(mCurrentPhotoIndex); 493 } 494 } 495 // Update the any action items 496 updateActionItems(); 497 } 498 } 499 500 @Override 501 public void onLoaderReset(android.support.v4.content.Loader<Cursor> loader) { 502 // If the loader is reset, remove the reference in the adapter to this cursor 503 if (!isDestroyed()) { 504 // This will cause a fragment transaction which can't happen if we're destroyed, 505 // but we don't care in that case because we're destroyed anyways. 506 mAdapter.swapCursor(null); 507 } 508 } 509 510 protected void updateActionItems() { 511 // Do nothing, but allow extending classes to do work 512 } 513 514 private synchronized void notifyCursorListeners(Cursor data) { 515 // tell all of the objects listening for cursor changes 516 // that the cursor has changed 517 for (CursorChangedListener listener : mCursorListeners) { 518 listener.onCursorChanged(data); 519 } 520 } 521 522 @Override 523 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 524 } 525 526 @Override 527 public void onPageSelected(int position) { 528 mCurrentPhotoIndex = position; 529 setViewActivated(position); 530 } 531 532 @Override 533 public void onPageScrollStateChanged(int state) { 534 } 535 536 @Override 537 public boolean isFragmentActive(Fragment fragment) { 538 if (mViewPager == null || mAdapter == null) { 539 return false; 540 } 541 return mViewPager.getCurrentItem() == mAdapter.getItemPosition(fragment); 542 } 543 544 @Override 545 public void onFragmentVisible(PhotoViewFragment fragment) { 546 // Do nothing, we handle this in setViewActivated 547 } 548 549 @Override 550 public InterceptType onTouchIntercept(float origX, float origY) { 551 boolean interceptLeft = false; 552 boolean interceptRight = false; 553 554 for (OnScreenListener listener : mScreenListeners.values()) { 555 if (!interceptLeft) { 556 interceptLeft = listener.onInterceptMoveLeft(origX, origY); 557 } 558 if (!interceptRight) { 559 interceptRight = listener.onInterceptMoveRight(origX, origY); 560 } 561 } 562 563 if (interceptLeft) { 564 if (interceptRight) { 565 return InterceptType.BOTH; 566 } 567 return InterceptType.LEFT; 568 } else if (interceptRight) { 569 return InterceptType.RIGHT; 570 } 571 return InterceptType.NONE; 572 } 573 574 /** 575 * Updates the title bar according to the value of {@link #mFullScreen}. 576 */ 577 protected void setFullScreen(boolean fullScreen, boolean setDelayedRunnable) { 578 final boolean fullScreenChanged = (fullScreen != mFullScreen); 579 mFullScreen = fullScreen; 580 581 if (mFullScreen) { 582 setLightsOutMode(true); 583 cancelEnterFullScreenRunnable(); 584 } else { 585 setLightsOutMode(false); 586 if (setDelayedRunnable) { 587 postEnterFullScreenRunnableWithDelay(); 588 } 589 } 590 591 if (fullScreenChanged) { 592 for (OnScreenListener listener : mScreenListeners.values()) { 593 listener.onFullScreenChanged(mFullScreen); 594 } 595 } 596 } 597 598 private void postEnterFullScreenRunnableWithDelay() { 599 mHandler.postDelayed(mEnterFullScreenRunnable, mEnterFullScreenDelayTime); 600 } 601 602 private void cancelEnterFullScreenRunnable() { 603 mHandler.removeCallbacks(mEnterFullScreenRunnable); 604 } 605 606 protected void setLightsOutMode(boolean enabled) { 607 mController.setImmersiveMode(enabled); 608 } 609 610 private final Runnable mEnterFullScreenRunnable = new Runnable() { 611 @Override 612 public void run() { 613 setFullScreen(true, true); 614 } 615 }; 616 617 @Override 618 public void setViewActivated(int position) { 619 OnScreenListener listener = mScreenListeners.get(position); 620 if (listener != null) { 621 listener.onViewActivated(); 622 } 623 final Cursor cursor = getCursorAtProperPosition(); 624 mCurrentPhotoIndex = position; 625 // FLAG: get the column indexes once in onLoadFinished(). 626 // That would make this more efficient, instead of looking these up 627 // repeatedly whenever we want them. 628 int uriIndex = cursor.getColumnIndex(PhotoContract.PhotoViewColumns.URI); 629 mCurrentPhotoUri = cursor.getString(uriIndex); 630 updateActionBar(); 631 632 // Restart the timer to return to fullscreen. 633 cancelEnterFullScreenRunnable(); 634 postEnterFullScreenRunnableWithDelay(); 635 } 636 637 /** 638 * Adjusts the activity title and subtitle to reflect the photo name and count. 639 */ 640 protected void updateActionBar() { 641 final int position = mViewPager.getCurrentItem() + 1; 642 final boolean hasAlbumCount = mAlbumCount >= 0; 643 644 final Cursor cursor = getCursorAtProperPosition(); 645 if (cursor != null) { 646 // FLAG: We should grab the indexes when we first get the cursor 647 // and store them so we don't need to do it each time. 648 final int photoNameIndex = cursor.getColumnIndex(PhotoContract.PhotoViewColumns.NAME); 649 mActionBarTitle = cursor.getString(photoNameIndex); 650 } else { 651 mActionBarTitle = null; 652 } 653 654 if (mIsEmpty || !hasAlbumCount || position <= 0) { 655 mActionBarSubtitle = null; 656 } else { 657 mActionBarSubtitle = 658 getResources().getString(R.string.photo_view_count, position, mAlbumCount); 659 } 660 661 setActionBarTitles(getSupportActionBar()); 662 } 663 664 /** 665 * Sets the Action Bar title to {@link #mActionBarTitle} and the subtitle to 666 * {@link #mActionBarSubtitle} 667 */ 668 protected final void setActionBarTitles(ActionBar actionBar) { 669 if (actionBar == null) { 670 return; 671 } 672 actionBar.setTitle(getInputOrEmpty(mActionBarTitle)); 673 actionBar.setSubtitle(getInputOrEmpty(mActionBarSubtitle)); 674 } 675 676 /** 677 * If the input string is non-null, it is returned, otherwise an empty string is returned; 678 * @param in 679 * @return 680 */ 681 private static final String getInputOrEmpty(String in) { 682 if (in == null) { 683 return ""; 684 } 685 return in; 686 } 687 688 /** 689 * Utility method that will return the cursor that contains the data 690 * at the current position so that it refers to the current image on screen. 691 * @return the cursor at the current position or 692 * null if no cursor exists or if the {@link PhotoViewPager} is null. 693 */ 694 public Cursor getCursorAtProperPosition() { 695 if (mViewPager == null) { 696 return null; 697 } 698 699 final int position = mViewPager.getCurrentItem(); 700 final Cursor cursor = mAdapter.getCursor(); 701 702 if (cursor == null) { 703 return null; 704 } 705 706 cursor.moveToPosition(position); 707 708 return cursor; 709 } 710 711 public Cursor getCursor() { 712 return (mAdapter == null) ? null : mAdapter.getCursor(); 713 } 714 715 @Override 716 public void onMenuVisibilityChanged(boolean isVisible) { 717 if (isVisible) { 718 cancelEnterFullScreenRunnable(); 719 } else { 720 postEnterFullScreenRunnableWithDelay(); 721 } 722 } 723 724 @Override 725 public void onNewPhotoLoaded(int position) { 726 // do nothing 727 } 728 729 protected void setPhotoIndex(int index) { 730 mCurrentPhotoIndex = index; 731 } 732 733 @Override 734 public void onFragmentPhotoLoadComplete(PhotoViewFragment fragment, boolean success) { 735 if (mTemporaryImage.getVisibility() != View.GONE && 736 TextUtils.equals(fragment.getPhotoUri(), mCurrentPhotoUri)) { 737 if (success) { 738 // The fragment for the current image is now ready for display. 739 mTemporaryImage.setVisibility(View.GONE); 740 mViewPager.setVisibility(View.VISIBLE); 741 } else { 742 // This means that we are unable to load the fragment's photo. 743 // I'm not sure what the best thing to do here is, but at least if 744 // we display the viewPager, the fragment itself can decide how to 745 // display the failure of its own image. 746 Log.w(TAG, "Failed to load fragment image"); 747 mTemporaryImage.setVisibility(View.GONE); 748 mViewPager.setVisibility(View.VISIBLE); 749 } 750 } 751 } 752 753 protected boolean isFullScreen() { 754 return mFullScreen; 755 } 756 757 @Override 758 public void onCursorChanged(PhotoViewFragment fragment, Cursor cursor) { 759 // do nothing 760 } 761 762 @Override 763 public PhotoPagerAdapter getAdapter() { 764 return mAdapter; 765 } 766 767 public void onEnterAnimationComplete() { 768 mEnterAnimationFinished = true; 769 mViewPager.setVisibility(View.VISIBLE); 770 setLightsOutMode(mFullScreen); 771 } 772 773 private void onExitAnimationComplete() { 774 finish(); 775 overridePendingTransition(0, 0); 776 } 777 778 private void runEnterAnimation() { 779 final int totalWidth = mRootView.getMeasuredWidth(); 780 final int totalHeight = mRootView.getMeasuredHeight(); 781 782 // FLAG: Need to handle the aspect ratio of the bitmap. If it's a portrait 783 // bitmap, then we need to position the view higher so that the middle 784 // pixels line up. 785 mTemporaryImage.setVisibility(View.VISIBLE); 786 // We need to take a full screen image, and scale/translate it so that 787 // it appears at exactly the same location onscreen as it is in the 788 // prior activity. 789 // The final image will take either the full screen width or height (or both). 790 791 final float scaleW = (float) mAnimationStartWidth / totalWidth; 792 final float scaleY = (float) mAnimationStartHeight / totalHeight; 793 final float scale = Math.max(scaleW, scaleY); 794 795 final int translateX = calculateTranslate(mAnimationStartX, mAnimationStartWidth, 796 totalWidth, scale); 797 final int translateY = calculateTranslate(mAnimationStartY, mAnimationStartHeight, 798 totalHeight, scale); 799 800 final int version = android.os.Build.VERSION.SDK_INT; 801 if (version >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 802 mBackground.setAlpha(0f); 803 mBackground.animate().alpha(1f).setDuration(ENTER_ANIMATION_DURATION_MS).start(); 804 mBackground.setVisibility(View.VISIBLE); 805 806 mTemporaryImage.setScaleX(scale); 807 mTemporaryImage.setScaleY(scale); 808 mTemporaryImage.setTranslationX(translateX); 809 mTemporaryImage.setTranslationY(translateY); 810 811 Runnable endRunnable = new Runnable() { 812 @Override 813 public void run() { 814 PhotoViewActivity.this.onEnterAnimationComplete(); 815 } 816 }; 817 ViewPropertyAnimator animator = mTemporaryImage.animate().scaleX(1f).scaleY(1f) 818 .translationX(0).translationY(0).setDuration(ENTER_ANIMATION_DURATION_MS); 819 if (version >= Build.VERSION_CODES.JELLY_BEAN) { 820 animator.withEndAction(endRunnable); 821 } else { 822 mHandler.postDelayed(endRunnable, ENTER_ANIMATION_DURATION_MS); 823 } 824 animator.start(); 825 } else { 826 final Animation alphaAnimation = new AlphaAnimation(0f, 1f); 827 alphaAnimation.setDuration(ENTER_ANIMATION_DURATION_MS); 828 mBackground.startAnimation(alphaAnimation); 829 mBackground.setVisibility(View.VISIBLE); 830 831 final Animation translateAnimation = new TranslateAnimation(translateX, 832 translateY, 0, 0); 833 translateAnimation.setDuration(ENTER_ANIMATION_DURATION_MS); 834 Animation scaleAnimation = new ScaleAnimation(scale, scale, 0, 0); 835 scaleAnimation.setDuration(ENTER_ANIMATION_DURATION_MS); 836 837 AnimationSet animationSet = new AnimationSet(true); 838 animationSet.addAnimation(translateAnimation); 839 animationSet.addAnimation(scaleAnimation); 840 AnimationListener listener = new AnimationListener() { 841 @Override 842 public void onAnimationEnd(Animation arg0) { 843 PhotoViewActivity.this.onEnterAnimationComplete(); 844 } 845 846 @Override 847 public void onAnimationRepeat(Animation arg0) { 848 } 849 850 @Override 851 public void onAnimationStart(Animation arg0) { 852 } 853 }; 854 animationSet.setAnimationListener(listener); 855 mTemporaryImage.startAnimation(animationSet); 856 } 857 } 858 859 private void runExitAnimation() { 860 Intent intent = getIntent(); 861 // FLAG: should just fall back to a standard animation if either: 862 // 1. images have been added or removed since we've been here, or 863 // 2. we are currently looking at some image other than the one we 864 // started on. 865 866 final int totalWidth = mRootView.getMeasuredWidth(); 867 final int totalHeight = mRootView.getMeasuredHeight(); 868 869 // We need to take a full screen image, and scale/translate it so that 870 // it appears at exactly the same location onscreen as it is in the 871 // prior activity. 872 // The final image will take either the full screen width or height (or both). 873 final float scaleW = (float) mAnimationStartWidth / totalWidth; 874 final float scaleY = (float) mAnimationStartHeight / totalHeight; 875 final float scale = Math.max(scaleW, scaleY); 876 877 final int translateX = calculateTranslate(mAnimationStartX, mAnimationStartWidth, 878 totalWidth, scale); 879 final int translateY = calculateTranslate(mAnimationStartY, mAnimationStartHeight, 880 totalHeight, scale); 881 final int version = android.os.Build.VERSION.SDK_INT; 882 if (version >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 883 mBackground.animate().alpha(0f).setDuration(EXIT_ANIMATION_DURATION_MS).start(); 884 mBackground.setVisibility(View.VISIBLE); 885 886 Runnable endRunnable = new Runnable() { 887 @Override 888 public void run() { 889 PhotoViewActivity.this.onExitAnimationComplete(); 890 } 891 }; 892 // If the temporary image is still visible it means that we have 893 // not yet loaded the fullres image, so we need to animate 894 // the temporary image out. 895 ViewPropertyAnimator animator = null; 896 if (mTemporaryImage.getVisibility() == View.VISIBLE) { 897 animator = mTemporaryImage.animate().scaleX(scale).scaleY(scale) 898 .translationX(translateX).translationY(translateY) 899 .setDuration(EXIT_ANIMATION_DURATION_MS); 900 } else { 901 animator = mViewPager.animate().scaleX(scale).scaleY(scale) 902 .translationX(translateX).translationY(translateY) 903 .setDuration(EXIT_ANIMATION_DURATION_MS); 904 } 905 if (version >= android.os.Build.VERSION_CODES.JELLY_BEAN) { 906 animator.withEndAction(endRunnable); 907 } else { 908 mHandler.postDelayed(endRunnable, EXIT_ANIMATION_DURATION_MS); 909 } 910 animator.start(); 911 } else { 912 final Animation alphaAnimation = new AlphaAnimation(1f, 0f); 913 alphaAnimation.setDuration(EXIT_ANIMATION_DURATION_MS); 914 mBackground.startAnimation(alphaAnimation); 915 mBackground.setVisibility(View.VISIBLE); 916 917 final Animation scaleAnimation = new ScaleAnimation(1f, 1f, scale, scale); 918 scaleAnimation.setDuration(EXIT_ANIMATION_DURATION_MS); 919 AnimationListener listener = new AnimationListener() { 920 @Override 921 public void onAnimationEnd(Animation arg0) { 922 PhotoViewActivity.this.onExitAnimationComplete(); 923 } 924 925 @Override 926 public void onAnimationRepeat(Animation arg0) { 927 } 928 929 @Override 930 public void onAnimationStart(Animation arg0) { 931 } 932 }; 933 scaleAnimation.setAnimationListener(listener); 934 // If the temporary image is still visible it means that we have 935 // not yet loaded the fullres image, so we need to animate 936 // the temporary image out. 937 if (mTemporaryImage.getVisibility() == View.VISIBLE) { 938 mTemporaryImage.startAnimation(scaleAnimation); 939 } else { 940 mViewPager.startAnimation(scaleAnimation); 941 } 942 } 943 } 944 945 private int calculateTranslate(int start, int startSize, int totalSize, float scale) { 946 // Translation takes precedence over scale. What this means is that if 947 // we want an view's upper left corner to be a particular spot on screen, 948 // but that view is scaled to something other than 1, we need to take into 949 // account the pixels lost to scaling. 950 // So if we have a view that is 200x300, and we want it's upper left corner 951 // to be at 50x50, but it's scaled by 50%, we can't just translate it to 50x50. 952 // If we were to do that, the view's *visible* upper left corner would be at 953 // 100x200. We need to take into account the difference between the outside 954 // size of the view (i.e. the size prior to scaling) and the scaled size. 955 // scaleFromEdge is the difference between the visible left edge and the 956 // actual left edge, due to scaling. 957 // scaleFromTop is the difference between the visible top edge, and the 958 // actual top edge, due to scaling. 959 int scaleFromEdge = Math.round((totalSize - totalSize * scale) / 2); 960 961 // The imageView is fullscreen, regardless of the aspect ratio of the actual image. 962 // This means that some portion of the imageView will be blank. We need to 963 // take into account the size of the blank area so that the actual image 964 // lines up with the starting image. 965 int blankSize = Math.round((totalSize * scale - startSize) / 2); 966 967 return start - scaleFromEdge - blankSize; 968 } 969 970 private void initTemporaryImage(Drawable drawable) { 971 if (mEnterAnimationFinished) { 972 // Forget this, we've already run the animation. 973 return; 974 } 975 mTemporaryImage.setImageDrawable(drawable); 976 if (drawable != null) { 977 // We have not yet run the enter animation. Start it now. 978 int totalWidth = mRootView.getMeasuredWidth(); 979 if (totalWidth == 0) { 980 // the measure pass has not yet finished. We can't properly 981 // run out animation until that is done. Listen for the layout 982 // to occur, then fire the animation. 983 final View base = mRootView; 984 base.getViewTreeObserver().addOnGlobalLayoutListener( 985 new OnGlobalLayoutListener() { 986 @Override 987 public void onGlobalLayout() { 988 int version = android.os.Build.VERSION.SDK_INT; 989 if (version >= android.os.Build.VERSION_CODES.JELLY_BEAN) { 990 base.getViewTreeObserver().removeOnGlobalLayoutListener(this); 991 } else { 992 base.getViewTreeObserver().removeGlobalOnLayoutListener(this); 993 } 994 runEnterAnimation(); 995 } 996 }); 997 } else { 998 // initiate the animation 999 runEnterAnimation(); 1000 } 1001 } 1002 // Kick off the photo list loader 1003 getSupportLoaderManager().initLoader(LOADER_PHOTO_LIST, null, this); 1004 } 1005 1006 // START PhotoViewControllerCallbacks 1007 1008 @Override 1009 public void showActionBar() { 1010 getSupportActionBar().show(); 1011 } 1012 1013 @Override 1014 public void hideActionBar() { 1015 getSupportActionBar().hide(); 1016 } 1017 1018 @Override 1019 public boolean isScaleAnimationEnabled() { 1020 return mScaleAnimationEnabled; 1021 } 1022 1023 @Override 1024 public boolean isEnterAnimationFinished() { 1025 return mEnterAnimationFinished; 1026 } 1027 1028 @Override 1029 public View getRootView() { 1030 return mRootView; 1031 } 1032 1033 @Override 1034 public void setNotFullscreenCallbackDoNotUseThisFunction() { 1035 setFullScreen(false /* fullscreen */, true /* setDelayedRunnable */); 1036 } 1037 1038 // END PhotoViewControllerCallbacks 1039 1040 private class BitmapCallback implements LoaderManager.LoaderCallbacks<BitmapResult> { 1041 1042 @Override 1043 public Loader<BitmapResult> onCreateLoader(int id, Bundle args) { 1044 String uri = args.getString(ARG_IMAGE_URI); 1045 switch (id) { 1046 case PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL: 1047 return onCreateBitmapLoader(PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL, 1048 args, uri); 1049 case PhotoViewCallbacks.BITMAP_LOADER_AVATAR: 1050 return onCreateBitmapLoader(PhotoViewCallbacks.BITMAP_LOADER_AVATAR, 1051 args, uri); 1052 } 1053 return null; 1054 } 1055 1056 @Override 1057 public void onLoadFinished(Loader<BitmapResult> loader, BitmapResult result) { 1058 Drawable drawable = result.getDrawable(getResources()); 1059 final ActionBar actionBar = getSupportActionBar(); 1060 switch (loader.getId()) { 1061 case PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL: 1062 // We just loaded the initial thumbnail that we can display 1063 // while waiting for the full viewPager to get initialized. 1064 initTemporaryImage(drawable); 1065 // Destroy the loader so we don't attempt to load the thumbnail 1066 // again on screen rotations. 1067 getSupportLoaderManager().destroyLoader( 1068 PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL); 1069 break; 1070 case PhotoViewCallbacks.BITMAP_LOADER_AVATAR: 1071 if (drawable == null) { 1072 actionBar.setLogo(null); 1073 } else { 1074 actionBar.setLogo(drawable); 1075 } 1076 break; 1077 } 1078 } 1079 1080 @Override 1081 public void onLoaderReset(Loader<BitmapResult> loader) { 1082 // Do nothing 1083 } 1084 } 1085} 1086