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