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