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.Activity; 21 import android.app.ActivityManager; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.res.Resources; 25 import android.database.Cursor; 26 import android.graphics.Bitmap; 27 import android.graphics.drawable.BitmapDrawable; 28 import android.net.Uri; 29 import android.os.Build; 30 import android.os.Bundle; 31 import android.os.Handler; 32 import android.support.v4.app.Fragment; 33 import android.support.v4.app.LoaderManager; 34 import android.support.v4.content.Loader; 35 import android.support.v4.view.ViewPager.OnPageChangeListener; 36 import android.view.MenuItem; 37 import android.support.v7.app.ActionBar; 38 import android.support.v7.app.ActionBar.OnMenuVisibilityListener; 39 import android.support.v7.app.ActionBarActivity; 40 import android.text.TextUtils; 41 import android.util.Log; 42 import android.view.View; 43 import android.view.ViewPropertyAnimator; 44 import android.view.ViewTreeObserver.OnGlobalLayoutListener; 45 import android.view.animation.AlphaAnimation; 46 import android.view.animation.Animation; 47 import android.view.animation.AnimationSet; 48 import android.view.animation.ScaleAnimation; 49 import android.view.animation.TranslateAnimation; 50 import android.view.animation.Animation.AnimationListener; 51 import android.widget.ImageView; 52 53 import com.android.ex.photo.PhotoViewPager.InterceptType; 54 import com.android.ex.photo.PhotoViewPager.OnInterceptTouchListener; 55 import com.android.ex.photo.adapters.PhotoPagerAdapter; 56 import com.android.ex.photo.fragments.PhotoViewFragment; 57 import com.android.ex.photo.loaders.PhotoBitmapLoader; 58 import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult; 59 import com.android.ex.photo.loaders.PhotoPagerLoader; 60 import com.android.ex.photo.provider.PhotoContract; 61 62 import java.util.HashMap; 63 import java.util.HashSet; 64 import java.util.Map; 65 import java.util.Set; 66 67 /** 68 * Activity to view the contents of an album. 69 */ 70 public 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; 428 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 429 currentPhotoUri = Uri.parse(mCurrentPhotoUri).buildUpon() 430 .clearQuery().build(); 431 } else { 432 currentPhotoUri = Uri.parse(mCurrentPhotoUri).buildUpon() 433 .query(null).build(); 434 } 435 while (data.moveToNext()) { 436 final String uriString = data.getString(uriIndex); 437 final Uri uri; 438 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 439 uri = Uri.parse(uriString).buildUpon().clearQuery().build(); 440 } else { 441 uri = Uri.parse(uriString).buildUpon().query(null).build(); 442 } 443 if (currentPhotoUri != null && currentPhotoUri.equals(uri)) { 444 mCurrentPhotoIndex = index; 445 break; 446 } 447 index++; 448 } 449 } 450 451 // We're paused; don't do anything now, we'll get re-invoked 452 // when the activity becomes active again 453 // TODO(pwestbro): This shouldn't be necessary, as the loader manager should 454 // restart the loader 455 if (mIsPaused) { 456 mRestartLoader = true; 457 return; 458 } 459 boolean wasEmpty = mIsEmpty; 460 mIsEmpty = false; 461 462 mAdapter.swapCursor(data); 463 if (mViewPager.getAdapter() == null) { 464 mViewPager.setAdapter(mAdapter); 465 } 466 notifyCursorListeners(data); 467 468 // Use an index of 0 if the index wasn't specified or couldn't be found 469 if (mCurrentPhotoIndex < 0) { 470 mCurrentPhotoIndex = 0; 471 } 472 473 mViewPager.setCurrentItem(mCurrentPhotoIndex, false); 474 if (wasEmpty) { 475 setViewActivated(mCurrentPhotoIndex); 476 } 477 } 478 // Update the any action items 479 updateActionItems(); 480 } 481 } 482 483 @Override 484 public void onLoaderReset(android.support.v4.content.Loader<Cursor> loader) { 485 // If the loader is reset, remove the reference in the adapter to this cursor 486 // TODO(pwestbro): reenable this when b/7075236 is fixed 487 // mAdapter.swapCursor(null); 488 } 489 490 protected void updateActionItems() { 491 // Do nothing, but allow extending classes to do work 492 } 493 494 private synchronized void notifyCursorListeners(Cursor data) { 495 // tell all of the objects listening for cursor changes 496 // that the cursor has changed 497 for (CursorChangedListener listener : mCursorListeners) { 498 listener.onCursorChanged(data); 499 } 500 } 501 502 @Override 503 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 504 } 505 506 @Override 507 public void onPageSelected(int position) { 508 mCurrentPhotoIndex = position; 509 setViewActivated(position); 510 } 511 512 @Override 513 public void onPageScrollStateChanged(int state) { 514 } 515 516 @Override 517 public boolean isFragmentActive(Fragment fragment) { 518 if (mViewPager == null || mAdapter == null) { 519 return false; 520 } 521 return mViewPager.getCurrentItem() == mAdapter.getItemPosition(fragment); 522 } 523 524 @Override 525 public void onFragmentVisible(PhotoViewFragment fragment) { 526 // Do nothing, we handle this in setViewActivated 527 } 528 529 @Override 530 public InterceptType onTouchIntercept(float origX, float origY) { 531 boolean interceptLeft = false; 532 boolean interceptRight = false; 533 534 for (OnScreenListener listener : mScreenListeners.values()) { 535 if (!interceptLeft) { 536 interceptLeft = listener.onInterceptMoveLeft(origX, origY); 537 } 538 if (!interceptRight) { 539 interceptRight = listener.onInterceptMoveRight(origX, origY); 540 } 541 } 542 543 if (interceptLeft) { 544 if (interceptRight) { 545 return InterceptType.BOTH; 546 } 547 return InterceptType.LEFT; 548 } else if (interceptRight) { 549 return InterceptType.RIGHT; 550 } 551 return InterceptType.NONE; 552 } 553 554 /** 555 * Updates the title bar according to the value of {@link #mFullScreen}. 556 */ 557 protected void setFullScreen(boolean fullScreen, boolean setDelayedRunnable) { 558 final boolean fullScreenChanged = (fullScreen != mFullScreen); 559 mFullScreen = fullScreen; 560 561 if (mFullScreen) { 562 setLightsOutMode(true); 563 cancelEnterFullScreenRunnable(); 564 } else { 565 setLightsOutMode(false); 566 if (setDelayedRunnable) { 567 postEnterFullScreenRunnableWithDelay(); 568 } 569 } 570 571 if (fullScreenChanged) { 572 for (OnScreenListener listener : mScreenListeners.values()) { 573 listener.onFullScreenChanged(mFullScreen); 574 } 575 } 576 } 577 578 private void postEnterFullScreenRunnableWithDelay() { 579 mHandler.postDelayed(mEnterFullScreenRunnable, mEnterFullScreenDelayTime); 580 } 581 582 private void cancelEnterFullScreenRunnable() { 583 mHandler.removeCallbacks(mEnterFullScreenRunnable); 584 } 585 586 protected void setLightsOutMode(boolean enabled) { 587 int flags = 0; 588 final int version = Build.VERSION.SDK_INT; 589 final ActionBar actionBar = getSupportActionBar(); 590 if (enabled) { 591 if (version >= android.os.Build.VERSION_CODES.JELLY_BEAN) { 592 flags = View.SYSTEM_UI_FLAG_LOW_PROFILE 593 | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 594 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE; 595 if (!mScaleAnimationEnabled) { 596 // If we are using the scale animation for intro and exit, 597 // we can't go into fullscreen mode. The issue is that the 598 // activity that invoked this will not be in fullscreen, so 599 // as we transition out, the background activity will be 600 // temporarily rendered without an actionbar, and the shrinking 601 // photo will not line up properly. After that it redraws 602 // in the correct location, but it still looks janks. 603 // FLAG: there may be a better way to fix this, but I don't 604 // yet know what it is. 605 flags |= View.SYSTEM_UI_FLAG_FULLSCREEN; 606 } 607 } else if (version >= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 608 flags = View.SYSTEM_UI_FLAG_LOW_PROFILE; 609 } else if (version >= android.os.Build.VERSION_CODES.HONEYCOMB) { 610 flags = View.STATUS_BAR_HIDDEN; 611 } 612 actionBar.hide(); 613 } else { 614 if (version >= android.os.Build.VERSION_CODES.JELLY_BEAN) { 615 flags = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 616 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE; 617 } else if (version >= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 618 flags = View.SYSTEM_UI_FLAG_VISIBLE; 619 } else if (version >= android.os.Build.VERSION_CODES.HONEYCOMB) { 620 flags = View.STATUS_BAR_VISIBLE; 621 } 622 actionBar.show(); 623 } 624 625 if (version >= Build.VERSION_CODES.HONEYCOMB) { 626 mRootView.setSystemUiVisibility(flags); 627 } 628 } 629 630 private final Runnable mEnterFullScreenRunnable = new Runnable() { 631 @Override 632 public void run() { 633 setFullScreen(true, true); 634 } 635 }; 636 637 @Override 638 public void setViewActivated(int position) { 639 OnScreenListener listener = mScreenListeners.get(position); 640 if (listener != null) { 641 listener.onViewActivated(); 642 } 643 final Cursor cursor = getCursorAtProperPosition(); 644 mCurrentPhotoIndex = position; 645 // FLAG: get the column indexes once in onLoadFinished(). 646 // That would make this more efficient, instead of looking these up 647 // repeatedly whenever we want them. 648 int uriIndex = cursor.getColumnIndex(PhotoContract.PhotoViewColumns.URI); 649 mCurrentPhotoUri = cursor.getString(uriIndex); 650 updateActionBar(); 651 652 // Restart the timer to return to fullscreen. 653 cancelEnterFullScreenRunnable(); 654 postEnterFullScreenRunnableWithDelay(); 655 } 656 657 /** 658 * Adjusts the activity title and subtitle to reflect the photo name and count. 659 */ 660 protected void updateActionBar() { 661 final int position = mViewPager.getCurrentItem() + 1; 662 final boolean hasAlbumCount = mAlbumCount >= 0; 663 664 final Cursor cursor = getCursorAtProperPosition(); 665 if (cursor != null) { 666 // FLAG: We should grab the indexes when we first get the cursor 667 // and store them so we don't need to do it each time. 668 final int photoNameIndex = cursor.getColumnIndex(PhotoContract.PhotoViewColumns.NAME); 669 mActionBarTitle = cursor.getString(photoNameIndex); 670 } else { 671 mActionBarTitle = null; 672 } 673 674 if (mIsEmpty || !hasAlbumCount || position <= 0) { 675 mActionBarSubtitle = null; 676 } else { 677 mActionBarSubtitle = 678 getResources().getString(R.string.photo_view_count, position, mAlbumCount); 679 } 680 681 setActionBarTitles(getSupportActionBar()); 682 } 683 684 /** 685 * Sets the Action Bar title to {@link #mActionBarTitle} and the subtitle to 686 * {@link #mActionBarSubtitle} 687 */ 688 protected final void setActionBarTitles(ActionBar actionBar) { 689 if (actionBar == null) { 690 return; 691 } 692 actionBar.setTitle(getInputOrEmpty(mActionBarTitle)); 693 actionBar.setSubtitle(getInputOrEmpty(mActionBarSubtitle)); 694 } 695 696 /** 697 * If the input string is non-null, it is returned, otherwise an empty string is returned; 698 * @param in 699 * @return 700 */ 701 private static final String getInputOrEmpty(String in) { 702 if (in == null) { 703 return ""; 704 } 705 return in; 706 } 707 708 /** 709 * Utility method that will return the cursor that contains the data 710 * at the current position so that it refers to the current image on screen. 711 * @return the cursor at the current position or 712 * null if no cursor exists or if the {@link PhotoViewPager} is null. 713 */ 714 public Cursor getCursorAtProperPosition() { 715 if (mViewPager == null) { 716 return null; 717 } 718 719 final int position = mViewPager.getCurrentItem(); 720 final Cursor cursor = mAdapter.getCursor(); 721 722 if (cursor == null) { 723 return null; 724 } 725 726 cursor.moveToPosition(position); 727 728 return cursor; 729 } 730 731 public Cursor getCursor() { 732 return (mAdapter == null) ? null : mAdapter.getCursor(); 733 } 734 735 @Override 736 public void onMenuVisibilityChanged(boolean isVisible) { 737 if (isVisible) { 738 cancelEnterFullScreenRunnable(); 739 } else { 740 postEnterFullScreenRunnableWithDelay(); 741 } 742 } 743 744 @Override 745 public void onNewPhotoLoaded(int position) { 746 // do nothing 747 } 748 749 protected void setPhotoIndex(int index) { 750 mCurrentPhotoIndex = index; 751 } 752 753 @Override 754 public void onFragmentPhotoLoadComplete(PhotoViewFragment fragment, boolean success) { 755 if (mTemporaryImage.getVisibility() != View.GONE && 756 TextUtils.equals(fragment.getPhotoUri(), mCurrentPhotoUri)) { 757 if (success) { 758 // The fragment for the current image is now ready for display. 759 mTemporaryImage.setVisibility(View.GONE); 760 mViewPager.setVisibility(View.VISIBLE); 761 } else { 762 // This means that we are unable to load the fragment's photo. 763 // I'm not sure what the best thing to do here is, but at least if 764 // we display the viewPager, the fragment itself can decide how to 765 // display the failure of its own image. 766 Log.w(TAG, "Failed to load fragment image"); 767 mTemporaryImage.setVisibility(View.GONE); 768 mViewPager.setVisibility(View.VISIBLE); 769 } 770 } 771 } 772 773 protected boolean isFullScreen() { 774 return mFullScreen; 775 } 776 777 @Override 778 public void onCursorChanged(PhotoViewFragment fragment, Cursor cursor) { 779 // do nothing 780 } 781 782 @Override 783 public PhotoPagerAdapter getAdapter() { 784 return mAdapter; 785 } 786 787 public void onEnterAnimationComplete() { 788 mEnterAnimationFinished = true; 789 mViewPager.setVisibility(View.VISIBLE); 790 } 791 792 private void onExitAnimationComplete() { 793 finish(); 794 overridePendingTransition(0, 0); 795 } 796 797 private void runEnterAnimation() { 798 final int totalWidth = mRootView.getMeasuredWidth(); 799 final int totalHeight = mRootView.getMeasuredHeight(); 800 801 // FLAG: Need to handle the aspect ratio of the bitmap. If it's a portrait 802 // bitmap, then we need to position the view higher so that the middle 803 // pixels line up. 804 mTemporaryImage.setVisibility(View.VISIBLE); 805 // We need to take a full screen image, and scale/translate it so that 806 // it appears at exactly the same location onscreen as it is in the 807 // prior activity. 808 // The final image will take either the full screen width or height (or both). 809 810 final float scaleW = (float) mAnimationStartWidth / totalWidth; 811 final float scaleY = (float) mAnimationStartHeight / totalHeight; 812 final float scale = Math.max(scaleW, scaleY); 813 814 final int translateX = calculateTranslate(mAnimationStartX, mAnimationStartWidth, 815 totalWidth, scale); 816 final int translateY = calculateTranslate(mAnimationStartY, mAnimationStartHeight, 817 totalHeight, scale); 818 819 final int version = android.os.Build.VERSION.SDK_INT; 820 if (version >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 821 mBackground.setAlpha(0f); 822 mBackground.animate().alpha(1f).setDuration(ENTER_ANIMATION_DURATION_MS).start(); 823 mBackground.setVisibility(View.VISIBLE); 824 825 mTemporaryImage.setScaleX(scale); 826 mTemporaryImage.setScaleY(scale); 827 mTemporaryImage.setTranslationX(translateX); 828 mTemporaryImage.setTranslationY(translateY); 829 830 Runnable endRunnable = new Runnable() { 831 @Override 832 public void run() { 833 PhotoViewActivity.this.onEnterAnimationComplete(); 834 } 835 }; 836 ViewPropertyAnimator animator = mTemporaryImage.animate().scaleX(1f).scaleY(1f) 837 .translationX(0).translationY(0).setDuration(ENTER_ANIMATION_DURATION_MS); 838 if (version >= Build.VERSION_CODES.JELLY_BEAN) { 839 animator.withEndAction(endRunnable); 840 } else { 841 mHandler.postDelayed(endRunnable, ENTER_ANIMATION_DURATION_MS); 842 } 843 animator.start(); 844 } else { 845 final Animation alphaAnimation = new AlphaAnimation(0f, 1f); 846 alphaAnimation.setDuration(ENTER_ANIMATION_DURATION_MS); 847 mBackground.startAnimation(alphaAnimation); 848 mBackground.setVisibility(View.VISIBLE); 849 850 final Animation translateAnimation = new TranslateAnimation(translateX, 851 translateY, 0, 0); 852 translateAnimation.setDuration(ENTER_ANIMATION_DURATION_MS); 853 Animation scaleAnimation = new ScaleAnimation(scale, scale, 0, 0); 854 scaleAnimation.setDuration(ENTER_ANIMATION_DURATION_MS); 855 856 AnimationSet animationSet = new AnimationSet(true); 857 animationSet.addAnimation(translateAnimation); 858 animationSet.addAnimation(scaleAnimation); 859 AnimationListener listener = new AnimationListener() { 860 @Override 861 public void onAnimationEnd(Animation arg0) { 862 PhotoViewActivity.this.onEnterAnimationComplete(); 863 } 864 865 @Override 866 public void onAnimationRepeat(Animation arg0) { 867 } 868 869 @Override 870 public void onAnimationStart(Animation arg0) { 871 } 872 }; 873 animationSet.setAnimationListener(listener); 874 mTemporaryImage.startAnimation(animationSet); 875 } 876 } 877 878 private void runExitAnimation() { 879 Intent intent = getIntent(); 880 // FLAG: should just fall back to a standard animation if either: 881 // 1. images have been added or removed since we've been here, or 882 // 2. we are currently looking at some image other than the one we 883 // started on. 884 885 final int totalWidth = mRootView.getMeasuredWidth(); 886 final int totalHeight = mRootView.getMeasuredHeight(); 887 888 // We need to take a full screen image, and scale/translate it so that 889 // it appears at exactly the same location onscreen as it is in the 890 // prior activity. 891 // The final image will take either the full screen width or height (or both). 892 final float scaleW = (float) mAnimationStartWidth / totalWidth; 893 final float scaleY = (float) mAnimationStartHeight / totalHeight; 894 final float scale = Math.max(scaleW, scaleY); 895 896 final int translateX = calculateTranslate(mAnimationStartX, mAnimationStartWidth, 897 totalWidth, scale); 898 final int translateY = calculateTranslate(mAnimationStartY, mAnimationStartHeight, 899 totalHeight, scale); 900 final int version = android.os.Build.VERSION.SDK_INT; 901 if (version >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 902 mBackground.animate().alpha(0f).setDuration(EXIT_ANIMATION_DURATION_MS).start(); 903 mBackground.setVisibility(View.VISIBLE); 904 905 Runnable endRunnable = new Runnable() { 906 @Override 907 public void run() { 908 PhotoViewActivity.this.onExitAnimationComplete(); 909 } 910 }; 911 // If the temporary image is still visible it means that we have 912 // not yet loaded the fullres image, so we need to animate 913 // the temporary image out. 914 ViewPropertyAnimator animator = null; 915 if (mTemporaryImage.getVisibility() == View.VISIBLE) { 916 animator = mTemporaryImage.animate().scaleX(scale).scaleY(scale) 917 .translationX(translateX).translationY(translateY) 918 .setDuration(EXIT_ANIMATION_DURATION_MS); 919 } else { 920 animator = mViewPager.animate().scaleX(scale).scaleY(scale) 921 .translationX(translateX).translationY(translateY) 922 .setDuration(EXIT_ANIMATION_DURATION_MS); 923 } 924 if (version >= android.os.Build.VERSION_CODES.JELLY_BEAN) { 925 animator.withEndAction(endRunnable); 926 } else { 927 mHandler.postDelayed(endRunnable, EXIT_ANIMATION_DURATION_MS); 928 } 929 animator.start(); 930 } else { 931 final Animation alphaAnimation = new AlphaAnimation(1f, 0f); 932 alphaAnimation.setDuration(EXIT_ANIMATION_DURATION_MS); 933 mBackground.startAnimation(alphaAnimation); 934 mBackground.setVisibility(View.VISIBLE); 935 936 final Animation scaleAnimation = new ScaleAnimation(1f, 1f, scale, scale); 937 scaleAnimation.setDuration(EXIT_ANIMATION_DURATION_MS); 938 AnimationListener listener = new AnimationListener() { 939 @Override 940 public void onAnimationEnd(Animation arg0) { 941 PhotoViewActivity.this.onExitAnimationComplete(); 942 } 943 944 @Override 945 public void onAnimationRepeat(Animation arg0) { 946 } 947 948 @Override 949 public void onAnimationStart(Animation arg0) { 950 } 951 }; 952 scaleAnimation.setAnimationListener(listener); 953 // If the temporary image is still visible it means that we have 954 // not yet loaded the fullres image, so we need to animate 955 // the temporary image out. 956 if (mTemporaryImage.getVisibility() == View.VISIBLE) { 957 mTemporaryImage.startAnimation(scaleAnimation); 958 } else { 959 mViewPager.startAnimation(scaleAnimation); 960 } 961 } 962 } 963 964 private int calculateTranslate(int start, int startSize, int totalSize, float scale) { 965 // Translation takes precedence over scale. What this means is that if 966 // we want an view's upper left corner to be a particular spot on screen, 967 // but that view is scaled to something other than 1, we need to take into 968 // account the pixels lost to scaling. 969 // So if we have a view that is 200x300, and we want it's upper left corner 970 // to be at 50x50, but it's scaled by 50%, we can't just translate it to 50x50. 971 // If we were to do that, the view's *visible* upper left corner would be at 972 // 100x200. We need to take into account the difference between the outside 973 // size of the view (i.e. the size prior to scaling) and the scaled size. 974 // scaleFromEdge is the difference between the visible left edge and the 975 // actual left edge, due to scaling. 976 // scaleFromTop is the difference between the visible top edge, and the 977 // actual top edge, due to scaling. 978 int scaleFromEdge = Math.round((totalSize - totalSize * scale) / 2); 979 980 // The imageView is fullscreen, regardless of the aspect ratio of the actual image. 981 // This means that some portion of the imageView will be blank. We need to 982 // take into account the size of the blank area so that the actual image 983 // lines up with the starting image. 984 int blankSize = Math.round((totalSize * scale - startSize) / 2); 985 986 return start - scaleFromEdge - blankSize; 987 } 988 989 private void initTemporaryImage(Bitmap bitmap) { 990 if (mEnterAnimationFinished) { 991 // Forget this, we've already run the animation. 992 return; 993 } 994 mTemporaryImage.setImageBitmap(bitmap); 995 if (bitmap != null) { 996 // We have not yet run the enter animation. Start it now. 997 int totalWidth = mRootView.getMeasuredWidth(); 998 if (totalWidth == 0) { 999 // the measure pass has not yet finished. We can't properly 1000 // run out animation until that is done. Listen for the layout 1001 // to occur, then fire the animation. 1002 final View base = mRootView; 1003 base.getViewTreeObserver().addOnGlobalLayoutListener( 1004 new OnGlobalLayoutListener() { 1005 @Override 1006 public void onGlobalLayout() { 1007 int version = android.os.Build.VERSION.SDK_INT; 1008 if (version >= android.os.Build.VERSION_CODES.JELLY_BEAN) { 1009 base.getViewTreeObserver().removeOnGlobalLayoutListener(this); 1010 } else { 1011 base.getViewTreeObserver().removeGlobalOnLayoutListener(this); 1012 } 1013 runEnterAnimation(); 1014 } 1015 }); 1016 } else { 1017 // initiate the animation 1018 runEnterAnimation(); 1019 } 1020 } 1021 // Kick off the photo list loader 1022 getSupportLoaderManager().initLoader(LOADER_PHOTO_LIST, null, this); 1023 } 1024 1025 private class BitmapCallback implements LoaderManager.LoaderCallbacks<BitmapResult> { 1026 1027 @Override 1028 public Loader<BitmapResult> onCreateLoader(int id, Bundle args) { 1029 String uri = args.getString(ARG_IMAGE_URI); 1030 switch (id) { 1031 case PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL: 1032 return onCreateBitmapLoader(PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL, 1033 args, uri); 1034 case PhotoViewCallbacks.BITMAP_LOADER_AVATAR: 1035 return onCreateBitmapLoader(PhotoViewCallbacks.BITMAP_LOADER_AVATAR, 1036 args, uri); 1037 } 1038 return null; 1039 } 1040 1041 @Override 1042 public void onLoadFinished(Loader<BitmapResult> loader, BitmapResult result) { 1043 Bitmap bitmap = result.bitmap; 1044 final ActionBar actionBar = getSupportActionBar(); 1045 switch (loader.getId()) { 1046 case PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL: 1047 // We just loaded the initial thumbnail that we can display 1048 // while waiting for the full viewPager to get initialized. 1049 initTemporaryImage(bitmap); 1050 // Destroy the loader so we don't attempt to load the thumbnail 1051 // again on screen rotations. 1052 getSupportLoaderManager().destroyLoader( 1053 PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL); 1054 break; 1055 case PhotoViewCallbacks.BITMAP_LOADER_AVATAR: 1056 if (bitmap == null) { 1057 actionBar.setLogo(null); 1058 } else { 1059 BitmapDrawable drawable = new BitmapDrawable(getResources(), bitmap); 1060 actionBar.setLogo(drawable); 1061 } 1062 break; 1063 } 1064 } 1065 1066 @Override 1067 public void onLoaderReset(Loader<BitmapResult> loader) { 1068 // Do nothing 1069 } 1070 } 1071 } 1072