blob: 0face9632b578d11e7172e80ae93530801718a3f [file] [log] [blame]
package com.android.ex.photo;
import android.app.Activity;
import android.app.ActivityManager;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Process;
import androidx.annotation.IdRes;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import androidx.viewpager.widget.ViewPager.OnPageChangeListener;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewPropertyAnimator;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityManager;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.AnimationSet;
import android.view.animation.ScaleAnimation;
import android.view.animation.TranslateAnimation;
import android.widget.ImageView;
import com.android.ex.photo.ActionBarInterface.OnMenuVisibilityListener;
import com.android.ex.photo.PhotoViewPager.InterceptType;
import com.android.ex.photo.PhotoViewPager.OnInterceptTouchListener;
import com.android.ex.photo.adapters.PhotoPagerAdapter;
import com.android.ex.photo.fragments.PhotoViewFragment;
import com.android.ex.photo.loaders.PhotoBitmapLoader;
import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult;
import com.android.ex.photo.loaders.PhotoPagerLoader;
import com.android.ex.photo.provider.PhotoContract;
import com.android.ex.photo.util.ImageUtils;
import com.android.ex.photo.util.Util;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* This class implements all the logic of the photo view activity. An activity should use this class
* calling through from relevant activity methods to the methods of the same name here.
*
* To customize the photo viewer activity, you should subclass this and implement your
* customizations here. Then subclass {@link PhotoViewActivity} and override just
* {@link PhotoViewActivity#createController createController} to instantiate your controller
* subclass.
*/
public class PhotoViewController implements
LoaderManager.LoaderCallbacks<Cursor>, OnPageChangeListener, OnInterceptTouchListener,
OnMenuVisibilityListener, PhotoViewCallbacks {
/**
* Defines the interface between the Activity and this class.
*
* The activity itself must delegate all appropriate method calls into this class, to the
* methods of the same name.
*/
public interface ActivityInterface {
public Context getContext();
public Context getApplicationContext();
public Intent getIntent();
public void setContentView(int resId);
public <T extends View> T findViewById(int id);
public Resources getResources();
public FragmentManager getSupportFragmentManager();
public LoaderManager getSupportLoaderManager();
public ActionBarInterface getActionBarInterface();
public boolean onOptionsItemSelected(MenuItem item);
public void finish();
public void overridePendingTransition(int enterAnim, int exitAnim);
public PhotoViewController getController();
}
private final static String TAG = "PhotoViewController";
private final static String STATE_INITIAL_URI_KEY =
"com.android.ex.PhotoViewFragment.INITIAL_URI";
private final static String STATE_CURRENT_URI_KEY =
"com.android.ex.PhotoViewFragment.CURRENT_URI";
private final static String STATE_CURRENT_INDEX_KEY =
"com.android.ex.PhotoViewFragment.CURRENT_INDEX";
private final static String STATE_FULLSCREEN_KEY =
"com.android.ex.PhotoViewFragment.FULLSCREEN";
private final static String STATE_ACTIONBARTITLE_KEY =
"com.android.ex.PhotoViewFragment.ACTIONBARTITLE";
private final static String STATE_ACTIONBARSUBTITLE_KEY =
"com.android.ex.PhotoViewFragment.ACTIONBARSUBTITLE";
private final static String STATE_ENTERANIMATIONFINISHED_KEY =
"com.android.ex.PhotoViewFragment.SCALEANIMATIONFINISHED";
protected final static String ARG_IMAGE_URI = "image_uri";
public static final int LOADER_PHOTO_LIST = 100;
/** Count used when the real photo count is unknown [but, may be determined] */
public static final int ALBUM_COUNT_UNKNOWN = -1;
public static final int ENTER_ANIMATION_DURATION_MS = 250;
public static final int EXIT_ANIMATION_DURATION_MS = 250;
/** Argument key for the dialog message */
public static final String KEY_MESSAGE = "dialog_message";
public static int sMemoryClass;
public static int sMaxPhotoSize; // The maximum size (either width or height)
private final ActivityInterface mActivity;
private int mLastFlags;
private final View.OnSystemUiVisibilityChangeListener mSystemUiVisibilityChangeListener;
/** The URI of the photos we're viewing; may be {@code null} */
private String mPhotosUri;
/** The uri of the initial photo */
private String mInitialPhotoUri;
/** The index of the currently viewed photo */
private int mCurrentPhotoIndex;
/** The uri of the currently viewed photo */
private String mCurrentPhotoUri;
/** The query projection to use; may be {@code null} */
private String[] mProjection;
/** The total number of photos; only valid if {@link #mIsEmpty} is {@code false}. */
protected int mAlbumCount = ALBUM_COUNT_UNKNOWN;
/** {@code true} if the view is empty. Otherwise, {@code false}. */
protected boolean mIsEmpty;
/** the main root view */
protected View mRootView;
/** Background image that contains nothing, so it can be alpha faded from
* transparent to black without affecting any other views. */
@Nullable
protected View mBackground;
/** The main pager; provides left/right swipe between photos */
protected PhotoViewPager mViewPager;
/** The temporary image so that we can quickly scale up the fullscreen thumbnail */
@Nullable
protected ImageView mTemporaryImage;
/** Adapter to create pager views */
protected PhotoPagerAdapter mAdapter;
/** Whether or not we're in "full screen" mode */
protected boolean mFullScreen;
/** The listeners wanting full screen state for each screen position */
private final Map<Integer, OnScreenListener>
mScreenListeners = new HashMap<Integer, OnScreenListener>();
/** The set of listeners wanting full screen state */
private final Set<CursorChangedListener> mCursorListeners = new HashSet<CursorChangedListener>();
/** When {@code true}, restart the loader when the activity becomes active */
private boolean mKickLoader;
/** Don't attempt operations that may trigger a fragment transaction when the activity is
* destroyed */
private boolean mIsDestroyedCompat;
/** Whether or not this activity is paused */
protected boolean mIsPaused = true;
/** The maximum scale factor applied to images when they are initially displayed */
protected float mMaxInitialScale;
/** The title in the actionbar */
protected String mActionBarTitle;
/** The subtitle in the actionbar */
protected String mActionBarSubtitle;
private boolean mEnterAnimationFinished;
protected boolean mScaleAnimationEnabled;
protected int mAnimationStartX;
protected int mAnimationStartY;
protected int mAnimationStartWidth;
protected int mAnimationStartHeight;
/** Whether lights out should invoked based on timer */
protected boolean mIsTimerLightsOutEnabled;
protected boolean mActionBarHiddenInitially;
protected boolean mDisplayThumbsFullScreen;
private final AccessibilityManager mAccessibilityManager;
protected BitmapCallback mBitmapCallback;
protected final Handler mHandler = new Handler();
// TODO Find a better way to do this. We basically want the activity to display the
// "loading..." progress until the fragment takes over and shows it's own "loading..."
// progress [located in photo_header_view.xml]. We could potentially have all status displayed
// by the activity, but, that gets tricky when it comes to screen rotation. For now, we
// track the loading by this variable which is fragile and may cause phantom "loading..."
// text.
private long mEnterFullScreenDelayTime;
private int lastAnnouncedTitle = -1;
public PhotoViewController(ActivityInterface activity) {
mActivity = activity;
// View.OnSystemUiVisibilityChangeListener is an API that was introduced in API level 11.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
mSystemUiVisibilityChangeListener = null;
} else {
mSystemUiVisibilityChangeListener = new View.OnSystemUiVisibilityChangeListener() {
@Override
public void onSystemUiVisibilityChange(int visibility) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT &&
visibility == 0 && mLastFlags == 3846) {
setFullScreen(false /* fullscreen */, true /* setDelayedRunnable */);
}
}
};
}
mAccessibilityManager = (AccessibilityManager)
activity.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
}
public PhotoPagerAdapter createPhotoPagerAdapter(Context context,
androidx.fragment.app.FragmentManager fm, Cursor c, float maxScale) {
return new PhotoPagerAdapter(context, fm, c, maxScale, mDisplayThumbsFullScreen);
}
public PhotoViewController.ActivityInterface getActivity() {
return mActivity;
}
public void onCreate(Bundle savedInstanceState) {
initMaxPhotoSize();
final ActivityManager mgr = (ActivityManager) mActivity.getApplicationContext().
getSystemService(Activity.ACTIVITY_SERVICE);
sMemoryClass = mgr.getMemoryClass();
final Intent intent = mActivity.getIntent();
// uri of the photos to view; optional
if (intent.hasExtra(Intents.EXTRA_PHOTOS_URI)) {
mPhotosUri = intent.getStringExtra(Intents.EXTRA_PHOTOS_URI);
}
mIsTimerLightsOutEnabled = intent.getBooleanExtra(
Intents.EXTRA_ENABLE_TIMER_LIGHTS_OUT, true);
if (intent.getBooleanExtra(Intents.EXTRA_SCALE_UP_ANIMATION, false)) {
mScaleAnimationEnabled = true;
mAnimationStartX = intent.getIntExtra(Intents.EXTRA_ANIMATION_START_X, 0);
mAnimationStartY = intent.getIntExtra(Intents.EXTRA_ANIMATION_START_Y, 0);
mAnimationStartWidth = intent.getIntExtra(Intents.EXTRA_ANIMATION_START_WIDTH, 0);
mAnimationStartHeight = intent.getIntExtra(Intents.EXTRA_ANIMATION_START_HEIGHT, 0);
}
mActionBarHiddenInitially = intent.getBooleanExtra(
Intents.EXTRA_ACTION_BAR_HIDDEN_INITIALLY, false)
&& !Util.isTouchExplorationEnabled(mAccessibilityManager);
mDisplayThumbsFullScreen = intent.getBooleanExtra(
Intents.EXTRA_DISPLAY_THUMBS_FULLSCREEN, false);
// projection for the query; optional
// If not set, the default projection is used.
// This projection must include the columns from the default projection.
if (intent.hasExtra(Intents.EXTRA_PROJECTION)) {
mProjection = intent.getStringArrayExtra(Intents.EXTRA_PROJECTION);
} else {
mProjection = null;
}
// Set the max initial scale, defaulting to 1x
mMaxInitialScale = intent.getFloatExtra(Intents.EXTRA_MAX_INITIAL_SCALE, 1.0f);
mCurrentPhotoUri = null;
mCurrentPhotoIndex = -1;
// We allow specifying the current photo by either index or uri.
// This is because some users may have live datasets that can change,
// adding new items to either the beginning or end of the set. For clients
// that do not need that capability, ability to specify the current photo
// by index is offered as a convenience.
if (intent.hasExtra(Intents.EXTRA_PHOTO_INDEX)) {
mCurrentPhotoIndex = intent.getIntExtra(Intents.EXTRA_PHOTO_INDEX, -1);
}
if (intent.hasExtra(Intents.EXTRA_INITIAL_PHOTO_URI)) {
mInitialPhotoUri = intent.getStringExtra(Intents.EXTRA_INITIAL_PHOTO_URI);
mCurrentPhotoUri = mInitialPhotoUri;
}
mIsEmpty = true;
if (savedInstanceState != null) {
mInitialPhotoUri = savedInstanceState.getString(STATE_INITIAL_URI_KEY);
mCurrentPhotoUri = savedInstanceState.getString(STATE_CURRENT_URI_KEY);
mCurrentPhotoIndex = savedInstanceState.getInt(STATE_CURRENT_INDEX_KEY);
mFullScreen = savedInstanceState.getBoolean(STATE_FULLSCREEN_KEY, false)
&& !Util.isTouchExplorationEnabled(mAccessibilityManager);
mActionBarTitle = savedInstanceState.getString(STATE_ACTIONBARTITLE_KEY);
mActionBarSubtitle = savedInstanceState.getString(STATE_ACTIONBARSUBTITLE_KEY);
mEnterAnimationFinished = savedInstanceState.getBoolean(
STATE_ENTERANIMATIONFINISHED_KEY, false);
} else {
mFullScreen = mActionBarHiddenInitially;
}
mActivity.setContentView(getContentViewId());
// Create the adapter and add the view pager
mAdapter = createPhotoPagerAdapter(mActivity.getContext(),
mActivity.getSupportFragmentManager(), null, mMaxInitialScale);
final Resources resources = mActivity.getResources();
mRootView = findViewById(getRootViewId());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
mRootView.setOnSystemUiVisibilityChangeListener(getSystemUiVisibilityChangeListener());
}
mBackground = getBackground();
mTemporaryImage = getTemporaryImage();
mViewPager = (PhotoViewPager) findViewById(R.id.photo_view_pager);
mViewPager.setAdapter(mAdapter);
mViewPager.setOnPageChangeListener(this);
mViewPager.setOnInterceptTouchListener(this);
mViewPager.setPageMargin(resources.getDimensionPixelSize(R.dimen.photo_page_margin));
mBitmapCallback = new BitmapCallback();
if (!mScaleAnimationEnabled || mEnterAnimationFinished) {
// We are not running the scale up animation. Just let the fragments
// display and handle the animation.
mActivity.getSupportLoaderManager().initLoader(LOADER_PHOTO_LIST, null, this);
// Make the background opaque immediately so that we don't see the activity
// behind this one.
if (hasBackground()) {
mBackground.setVisibility(View.VISIBLE);
}
} else {
// Attempt to load the initial image thumbnail. Once we have the
// image, animate it up. Once the animation is complete, we can kick off
// loading the ViewPager. After the primary fullres image is loaded, we will
// make our temporary image invisible and display the ViewPager.
mViewPager.setVisibility(View.GONE);
Bundle args = new Bundle();
args.putString(ARG_IMAGE_URI, mInitialPhotoUri);
mActivity.getSupportLoaderManager().initLoader(
BITMAP_LOADER_THUMBNAIL, args, mBitmapCallback);
}
mEnterFullScreenDelayTime =
resources.getInteger(R.integer.reenter_fullscreen_delay_time_in_millis);
final ActionBarInterface actionBar = mActivity.getActionBarInterface();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.addOnMenuVisibilityListener(this);
actionBar.setDisplayOptionsShowTitle();
// Set the title and subtitle immediately here, rather than waiting
// for the fragment to be initialized.
setActionBarTitles(actionBar);
}
if (!mScaleAnimationEnabled) {
setLightsOutMode(mFullScreen);
} else {
// Keep lights out mode as false. This is to prevent jank cause by concurrent
// animations during the enter animation.
setLightsOutMode(false);
}
}
private void initMaxPhotoSize() {
if (sMaxPhotoSize == 0) {
final DisplayMetrics metrics = new DisplayMetrics();
final WindowManager wm = (WindowManager)
mActivity.getContext().getSystemService(Context.WINDOW_SERVICE);
final ImageUtils.ImageSize imageSize = ImageUtils.sUseImageSize;
wm.getDefaultDisplay().getMetrics(metrics);
switch (imageSize) {
case EXTRA_SMALL:
// Use a photo that's 80% of the "small" size
sMaxPhotoSize = (Math.min(metrics.heightPixels, metrics.widthPixels) * 800) / 1000;
break;
case SMALL:
// Fall through.
case NORMAL:
// Fall through.
default:
sMaxPhotoSize = Math.min(metrics.heightPixels, metrics.widthPixels);
break;
}
}
}
public boolean onCreateOptionsMenu(Menu menu) {
return true;
}
public boolean onPrepareOptionsMenu(Menu menu) {
return true;
}
public void onActivityResult(int requestCode, int resultCode, Intent data) {}
protected View findViewById(int id) {
return mActivity.findViewById(id);
}
/**
* Returns the android id of the viewer's root view. Subclasses should override this method if
* they provide their own layout.
*/
@IdRes
protected int getRootViewId() {
return R.id.photo_activity_root_view;
}
/**
* Returns the android layout id of the root layout that should be inflated for the viewer.
* Subclasses should override this method if they provide their own layout.
*/
@IdRes
protected int getContentViewId() {
return R.layout.photo_activity_view;
}
/**
* Returns the android view for the viewer's background view, if it has one. Subclasses should
* override this if they have a different (or no) background view.
*/
@Nullable
protected View getBackground() {
return findViewById(R.id.photo_activity_background);
}
/**
* Returns whether or not the view has a background object. Subclasses should override this if
* they do not contain a background object.
*/
protected boolean hasBackground() {
return mBackground != null;
}
/**
* Returns the android view for the viewer's temporary image, if it has one. Subclasses should
* override this if they have a different (or no) temporary image view.
*/
@Nullable
protected ImageView getTemporaryImage() {
return (ImageView) findViewById(R.id.photo_activity_temporary_image);
}
/**
* Returns whether or not the view has a temporary image view. Subclasses should override this
* if they do not use a temporary image.
*/
protected boolean hasTemporaryImage() {
return mTemporaryImage != null;
}
public void onStart() {}
public void onResume() {
setFullScreen(mFullScreen, false);
mIsPaused = false;
if (mKickLoader) {
mKickLoader = false;
mActivity.getSupportLoaderManager().initLoader(LOADER_PHOTO_LIST, null, this);
}
}
public void onPause() {
mIsPaused = true;
}
public void onStop() {}
public void onDestroy() {
mIsDestroyedCompat = true;
}
private boolean isDestroyedCompat() {
return mIsDestroyedCompat;
}
public boolean onBackPressed() {
// If we are in fullscreen mode, and the default is not full screen, then
// switch back to actionBar display mode.
if (mFullScreen && !mActionBarHiddenInitially) {
toggleFullScreen();
} else {
if (mScaleAnimationEnabled) {
runExitAnimation();
} else {
return false;
}
}
return true;
}
public void onSaveInstanceState(Bundle outState) {
outState.putString(STATE_INITIAL_URI_KEY, mInitialPhotoUri);
outState.putString(STATE_CURRENT_URI_KEY, mCurrentPhotoUri);
outState.putInt(STATE_CURRENT_INDEX_KEY, mCurrentPhotoIndex);
outState.putBoolean(STATE_FULLSCREEN_KEY, mFullScreen);
outState.putString(STATE_ACTIONBARTITLE_KEY, mActionBarTitle);
outState.putString(STATE_ACTIONBARSUBTITLE_KEY, mActionBarSubtitle);
outState.putBoolean(STATE_ENTERANIMATIONFINISHED_KEY, mEnterAnimationFinished);
}
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
mActivity.finish();
return true;
default:
return false;
}
}
@Override
public void addScreenListener(int position, OnScreenListener listener) {
mScreenListeners.put(position, listener);
}
@Override
public void removeScreenListener(int position) {
mScreenListeners.remove(position);
}
@Override
public synchronized void addCursorListener(CursorChangedListener listener) {
mCursorListeners.add(listener);
}
@Override
public synchronized void removeCursorListener(CursorChangedListener listener) {
mCursorListeners.remove(listener);
}
@Override
public boolean isFragmentFullScreen(Fragment fragment) {
if (mViewPager == null || mAdapter == null || mAdapter.getCount() == 0) {
return mFullScreen;
}
return mFullScreen || (mViewPager.getCurrentItem() != mAdapter.getItemPosition(fragment));
}
@Override
public void toggleFullScreen() {
setFullScreen(!mFullScreen, true);
}
public void onPhotoRemoved(long photoId) {
final Cursor data = mAdapter.getCursor();
if (data == null) {
// Huh?! How would this happen?
return;
}
final int dataCount = data.getCount();
if (dataCount <= 1) {
mActivity.finish();
return;
}
mActivity.getSupportLoaderManager().restartLoader(LOADER_PHOTO_LIST, null, this);
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
if (id == LOADER_PHOTO_LIST) {
return new PhotoPagerLoader(mActivity.getContext(), Uri.parse(mPhotosUri), mProjection);
}
return null;
}
@Override
public Loader<BitmapResult> onCreateBitmapLoader(int id, Bundle args, String uri) {
switch (id) {
case BITMAP_LOADER_AVATAR:
case BITMAP_LOADER_THUMBNAIL:
case BITMAP_LOADER_PHOTO:
return new PhotoBitmapLoader(mActivity.getContext(), uri);
default:
return null;
}
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
final int id = loader.getId();
if (id == LOADER_PHOTO_LIST) {
if (data == null || data.getCount() == 0) {
mIsEmpty = true;
mAdapter.swapCursor(null);
} else {
mAlbumCount = data.getCount();
if (mCurrentPhotoUri != null) {
int index = 0;
// Clear query params. Compare only the path.
final int uriIndex = data.getColumnIndex(PhotoContract.PhotoViewColumns.URI);
final Uri currentPhotoUri;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
currentPhotoUri = Uri.parse(mCurrentPhotoUri).buildUpon()
.clearQuery().build();
} else {
currentPhotoUri = Uri.parse(mCurrentPhotoUri).buildUpon()
.query(null).build();
}
// Rewind data cursor to the start if it has already advanced.
data.moveToPosition(-1);
while (data.moveToNext()) {
final String uriString = data.getString(uriIndex);
final Uri uri;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
uri = Uri.parse(uriString).buildUpon().clearQuery().build();
} else {
uri = Uri.parse(uriString).buildUpon().query(null).build();
}
if (currentPhotoUri != null && currentPhotoUri.equals(uri)) {
mCurrentPhotoIndex = index;
break;
}
index++;
}
}
// We're paused; don't do anything now, we'll get re-invoked
// when the activity becomes active again
if (mIsPaused) {
mKickLoader = true;
mAdapter.swapCursor(null);
return;
}
boolean wasEmpty = mIsEmpty;
mIsEmpty = false;
mAdapter.swapCursor(data);
if (mViewPager.getAdapter() == null) {
mViewPager.setAdapter(mAdapter);
}
notifyCursorListeners(data);
// Use an index of 0 if the index wasn't specified or couldn't be found
if (mCurrentPhotoIndex < 0) {
mCurrentPhotoIndex = 0;
}
mViewPager.setCurrentItem(mCurrentPhotoIndex, false);
if (wasEmpty) {
setViewActivated(mCurrentPhotoIndex);
}
}
// Update the any action items
updateActionItems();
}
}
@Override
public void onLoaderReset(androidx.loader.content.Loader<Cursor> loader) {
// If the loader is reset, remove the reference in the adapter to this cursor
if (!isDestroyedCompat()) {
// This will cause a fragment transaction which can't happen if we're destroyed,
// but we don't care in that case because we're destroyed anyways.
mAdapter.swapCursor(null);
}
}
public void updateActionItems() {
// Do nothing, but allow extending classes to do work
}
private synchronized void notifyCursorListeners(Cursor data) {
// tell all of the objects listening for cursor changes
// that the cursor has changed
for (CursorChangedListener listener : mCursorListeners) {
listener.onCursorChanged(data);
}
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
if (positionOffset < 0.0001) {
OnScreenListener before = mScreenListeners.get(position - 1);
if (before != null) {
before.onViewUpNext();
}
OnScreenListener after = mScreenListeners.get(position + 1);
if (after != null) {
after.onViewUpNext();
}
}
}
@Override
public void onPageSelected(int position) {
mCurrentPhotoIndex = position;
setViewActivated(position);
}
@Override
public void onPageScrollStateChanged(int state) {
}
@Override
public boolean isFragmentActive(Fragment fragment) {
if (mViewPager == null || mAdapter == null) {
return false;
}
return mViewPager.getCurrentItem() == mAdapter.getItemPosition(fragment);
}
@Override
public void onFragmentVisible(PhotoViewFragment fragment) {
// Do nothing, we handle this in setViewActivated
}
@Override
public InterceptType onTouchIntercept(float origX, float origY) {
boolean interceptLeft = false;
boolean interceptRight = false;
for (OnScreenListener listener : mScreenListeners.values()) {
if (!interceptLeft) {
interceptLeft = listener.onInterceptMoveLeft(origX, origY);
}
if (!interceptRight) {
interceptRight = listener.onInterceptMoveRight(origX, origY);
}
}
if (interceptLeft) {
if (interceptRight) {
return InterceptType.BOTH;
}
return InterceptType.LEFT;
} else if (interceptRight) {
return InterceptType.RIGHT;
}
return InterceptType.NONE;
}
/**
* Updates the title bar according to the value of {@link #mFullScreen}.
*/
protected void setFullScreen(boolean fullScreen, boolean setDelayedRunnable) {
if (Util.isTouchExplorationEnabled(mAccessibilityManager)) {
// Disallow full screen mode when accessibility is enabled so that the action bar
// stays accessible.
fullScreen = false;
setDelayedRunnable = false;
}
final boolean fullScreenChanged = (fullScreen != mFullScreen);
mFullScreen = fullScreen;
if (mFullScreen) {
setLightsOutMode(true);
cancelEnterFullScreenRunnable();
} else {
setLightsOutMode(false);
if (setDelayedRunnable) {
postEnterFullScreenRunnableWithDelay();
}
}
if (fullScreenChanged) {
for (OnScreenListener listener : mScreenListeners.values()) {
listener.onFullScreenChanged(mFullScreen);
}
}
}
/**
* Posts a runnable to enter full screen after mEnterFullScreenDelayTime. This method is a
* no-op if mIsTimerLightsOutEnabled is set to false.
*/
private void postEnterFullScreenRunnableWithDelay() {
if (mIsTimerLightsOutEnabled) {
mHandler.postDelayed(mEnterFullScreenRunnable, mEnterFullScreenDelayTime);
}
}
private void cancelEnterFullScreenRunnable() {
mHandler.removeCallbacks(mEnterFullScreenRunnable);
}
protected void setLightsOutMode(boolean enabled) {
setImmersiveMode(enabled);
}
private final Runnable mEnterFullScreenRunnable = new Runnable() {
@Override
public void run() {
setFullScreen(true, true);
}
};
@Override
public void setViewActivated(int position) {
OnScreenListener listener = mScreenListeners.get(position);
if (listener != null) {
listener.onViewActivated();
}
final Cursor cursor = getCursorAtProperPosition();
mCurrentPhotoIndex = position;
// FLAG: get the column indexes once in onLoadFinished().
// That would make this more efficient, instead of looking these up
// repeatedly whenever we want them.
int uriIndex = cursor.getColumnIndex(PhotoContract.PhotoViewColumns.URI);
mCurrentPhotoUri = cursor.getString(uriIndex);
updateActionBar();
if (mAccessibilityManager.isEnabled() && lastAnnouncedTitle != position) {
String announcement = getPhotoAccessibilityAnnouncement(position);
if (announcement != null) {
Util.announceForAccessibility(mRootView, mAccessibilityManager, announcement);
lastAnnouncedTitle = position;
}
}
// Restart the timer to return to fullscreen.
cancelEnterFullScreenRunnable();
postEnterFullScreenRunnableWithDelay();
}
/**
* Adjusts the activity title and subtitle to reflect the photo name and count.
*/
public void updateActionBar() {
final int position = mViewPager.getCurrentItem() + 1;
final boolean hasAlbumCount = mAlbumCount >= 0;
final Cursor cursor = getCursorAtProperPosition();
if (cursor != null) {
// FLAG: We should grab the indexes when we first get the cursor
// and store them so we don't need to do it each time.
final int photoNameIndex = cursor.getColumnIndex(PhotoContract.PhotoViewColumns.NAME);
mActionBarTitle = cursor.getString(photoNameIndex);
} else {
mActionBarTitle = null;
}
if (mIsEmpty || !hasAlbumCount || position <= 0) {
mActionBarSubtitle = null;
} else {
mActionBarSubtitle = mActivity.getResources().getString(
R.string.photo_view_count, position, mAlbumCount);
}
setActionBarTitles(mActivity.getActionBarInterface());
}
/**
* Returns a string used as an announcement for accessibility after the user moves to a new
* photo. It will be called after {@link #updateActionBar} has been called.
* @param position the index in the album of the currently active photo
* @return announcement for accessibility
*/
protected String getPhotoAccessibilityAnnouncement(int position) {
String announcement = mActionBarTitle;
if (mActionBarSubtitle != null) {
announcement = mActivity.getContext().getResources().getString(
R.string.titles, mActionBarTitle, mActionBarSubtitle);
}
return announcement;
}
/**
* Sets the Action Bar title to {@link #mActionBarTitle} and the subtitle to
* {@link #mActionBarSubtitle}
*/
protected final void setActionBarTitles(ActionBarInterface actionBar) {
if (actionBar == null) {
return;
}
actionBar.setTitle(getInputOrEmpty(mActionBarTitle));
actionBar.setSubtitle(getInputOrEmpty(mActionBarSubtitle));
}
/**
* If the input string is non-null, it is returned, otherwise an empty string is returned;
* @param in
* @return
*/
private static final String getInputOrEmpty(String in) {
if (in == null) {
return "";
}
return in;
}
/**
* Utility method that will return the cursor that contains the data
* at the current position so that it refers to the current image on screen.
* @return the cursor at the current position or
* null if no cursor exists or if the {@link PhotoViewPager} is null.
*/
public Cursor getCursorAtProperPosition() {
if (mViewPager == null) {
return null;
}
final int position = mViewPager.getCurrentItem();
final Cursor cursor = mAdapter.getCursor();
if (cursor == null) {
return null;
}
cursor.moveToPosition(position);
return cursor;
}
public Cursor getCursor() {
return (mAdapter == null) ? null : mAdapter.getCursor();
}
@Override
public void onMenuVisibilityChanged(boolean isVisible) {
if (isVisible) {
cancelEnterFullScreenRunnable();
} else {
postEnterFullScreenRunnableWithDelay();
}
}
@Override
public void onNewPhotoLoaded(int position) {
// do nothing
}
protected void setPhotoIndex(int index) {
mCurrentPhotoIndex = index;
}
@Override
public void onFragmentPhotoLoadComplete(PhotoViewFragment fragment, boolean success) {
if (hasTemporaryImage() && mTemporaryImage.getVisibility() != View.GONE &&
TextUtils.equals(fragment.getPhotoUri(), mCurrentPhotoUri)) {
if (success) {
// The fragment for the current image is now ready for display.
if (hasTemporaryImage()) {
mTemporaryImage.setVisibility(View.GONE);
}
mViewPager.setVisibility(View.VISIBLE);
} else {
// This means that we are unable to load the fragment's photo.
// I'm not sure what the best thing to do here is, but at least if
// we display the viewPager, the fragment itself can decide how to
// display the failure of its own image.
Log.w(TAG, "Failed to load fragment image");
if (hasTemporaryImage()) {
mTemporaryImage.setVisibility(View.GONE);
}
mViewPager.setVisibility(View.VISIBLE);
}
mActivity.getSupportLoaderManager().destroyLoader(
PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL);
}
}
protected boolean isFullScreen() {
return mFullScreen;
}
@Override
public void onCursorChanged(PhotoViewFragment fragment, Cursor cursor) {
// do nothing
}
@Override
public PhotoPagerAdapter getAdapter() {
return mAdapter;
}
public void onEnterAnimationComplete() {
mEnterAnimationFinished = true;
mViewPager.setVisibility(View.VISIBLE);
setLightsOutMode(mFullScreen);
}
private void onExitAnimationComplete() {
mActivity.finish();
mActivity.overridePendingTransition(0, 0);
}
private void runEnterAnimation() {
final int totalWidth = mRootView.getMeasuredWidth();
final int totalHeight = mRootView.getMeasuredHeight();
// FLAG: Need to handle the aspect ratio of the bitmap. If it's a portrait
// bitmap, then we need to position the view higher so that the middle
// pixels line up.
if (hasTemporaryImage()) {
mTemporaryImage.setVisibility(View.VISIBLE);
}
// We need to take a full screen image, and scale/translate it so that
// it appears at exactly the same location onscreen as it is in the
// prior activity.
// The final image will take either the full screen width or height (or both).
final float scaleW = (float) mAnimationStartWidth / totalWidth;
final float scaleY = (float) mAnimationStartHeight / totalHeight;
final float scale = Math.max(scaleW, scaleY);
final int translateX = calculateTranslate(mAnimationStartX, mAnimationStartWidth,
totalWidth, scale);
final int translateY = calculateTranslate(mAnimationStartY, mAnimationStartHeight,
totalHeight, scale);
final int version = android.os.Build.VERSION.SDK_INT;
if (version >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
if (hasBackground()) {
mBackground.setAlpha(0f);
mBackground.animate().alpha(1f).setDuration(ENTER_ANIMATION_DURATION_MS).start();
mBackground.setVisibility(View.VISIBLE);
}
if (hasTemporaryImage()) {
mTemporaryImage.setScaleX(scale);
mTemporaryImage.setScaleY(scale);
mTemporaryImage.setTranslationX(translateX);
mTemporaryImage.setTranslationY(translateY);
Runnable endRunnable = new Runnable() {
@Override
public void run() {
PhotoViewController.this.onEnterAnimationComplete();
}
};
ViewPropertyAnimator animator = mTemporaryImage.animate().scaleX(1f).scaleY(1f)
.translationX(0).translationY(0).setDuration(ENTER_ANIMATION_DURATION_MS);
if (version >= Build.VERSION_CODES.JELLY_BEAN) {
animator.withEndAction(endRunnable);
} else {
mHandler.postDelayed(endRunnable, ENTER_ANIMATION_DURATION_MS);
}
animator.start();
}
} else {
if (hasBackground()) {
final Animation alphaAnimation = new AlphaAnimation(0f, 1f);
alphaAnimation.setDuration(ENTER_ANIMATION_DURATION_MS);
mBackground.startAnimation(alphaAnimation);
mBackground.setVisibility(View.VISIBLE);
}
if (hasTemporaryImage()) {
final Animation translateAnimation = new TranslateAnimation(translateX,
translateY, 0, 0);
translateAnimation.setDuration(ENTER_ANIMATION_DURATION_MS);
Animation scaleAnimation = new ScaleAnimation(scale, scale, 0, 0);
scaleAnimation.setDuration(ENTER_ANIMATION_DURATION_MS);
AnimationSet animationSet = new AnimationSet(true);
animationSet.addAnimation(translateAnimation);
animationSet.addAnimation(scaleAnimation);
AnimationListener listener = new AnimationListener() {
@Override
public void onAnimationEnd(Animation arg0) {
PhotoViewController.this.onEnterAnimationComplete();
}
@Override
public void onAnimationRepeat(Animation arg0) {
}
@Override
public void onAnimationStart(Animation arg0) {
}
};
animationSet.setAnimationListener(listener);
mTemporaryImage.startAnimation(animationSet);
}
}
}
private void runExitAnimation() {
Intent intent = mActivity.getIntent();
// FLAG: should just fall back to a standard animation if either:
// 1. images have been added or removed since we've been here, or
// 2. we are currently looking at some image other than the one we
// started on.
final int totalWidth = mRootView.getMeasuredWidth();
final int totalHeight = mRootView.getMeasuredHeight();
// We need to take a full screen image, and scale/translate it so that
// it appears at exactly the same location onscreen as it is in the
// prior activity.
// The final image will take either the full screen width or height (or both).
final float scaleW = (float) mAnimationStartWidth / totalWidth;
final float scaleY = (float) mAnimationStartHeight / totalHeight;
final float scale = Math.max(scaleW, scaleY);
final int translateX = calculateTranslate(mAnimationStartX, mAnimationStartWidth,
totalWidth, scale);
final int translateY = calculateTranslate(mAnimationStartY, mAnimationStartHeight,
totalHeight, scale);
final int version = android.os.Build.VERSION.SDK_INT;
if (version >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
if (hasBackground()) {
mBackground.animate().alpha(0f).setDuration(EXIT_ANIMATION_DURATION_MS).start();
mBackground.setVisibility(View.VISIBLE);
}
Runnable endRunnable = new Runnable() {
@Override
public void run() {
PhotoViewController.this.onExitAnimationComplete();
}
};
// If the temporary image is still visible it means that we have
// not yet loaded the fullres image, so we need to animate
// the temporary image out.
ViewPropertyAnimator animator = null;
if (hasTemporaryImage() && mTemporaryImage.getVisibility() == View.VISIBLE) {
animator = mTemporaryImage.animate().scaleX(scale).scaleY(scale)
.translationX(translateX).translationY(translateY)
.setDuration(EXIT_ANIMATION_DURATION_MS);
} else {
animator = mViewPager.animate().scaleX(scale).scaleY(scale)
.translationX(translateX).translationY(translateY)
.setDuration(EXIT_ANIMATION_DURATION_MS);
}
// If the user has swiped to a different photo, fade out the current photo
// along with the scale animation.
if (!mInitialPhotoUri.equals(mCurrentPhotoUri)) {
animator.alpha(0f);
}
if (version >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
animator.withEndAction(endRunnable);
} else {
mHandler.postDelayed(endRunnable, EXIT_ANIMATION_DURATION_MS);
}
animator.start();
} else {
if (hasBackground()) {
final Animation alphaAnimation = new AlphaAnimation(1f, 0f);
alphaAnimation.setDuration(EXIT_ANIMATION_DURATION_MS);
mBackground.startAnimation(alphaAnimation);
mBackground.setVisibility(View.VISIBLE);
}
final Animation scaleAnimation = new ScaleAnimation(1f, 1f, scale, scale);
scaleAnimation.setDuration(EXIT_ANIMATION_DURATION_MS);
AnimationListener listener = new AnimationListener() {
@Override
public void onAnimationEnd(Animation arg0) {
PhotoViewController.this.onExitAnimationComplete();
}
@Override
public void onAnimationRepeat(Animation arg0) {
}
@Override
public void onAnimationStart(Animation arg0) {
}
};
scaleAnimation.setAnimationListener(listener);
// If the temporary image is still visible it means that we have
// not yet loaded the fullres image, so we need to animate
// the temporary image out.
if (hasTemporaryImage() && mTemporaryImage.getVisibility() == View.VISIBLE) {
mTemporaryImage.startAnimation(scaleAnimation);
} else {
mViewPager.startAnimation(scaleAnimation);
}
}
}
private int calculateTranslate(int start, int startSize, int totalSize, float scale) {
// Translation takes precedence over scale. What this means is that if
// we want an view's upper left corner to be a particular spot on screen,
// but that view is scaled to something other than 1, we need to take into
// account the pixels lost to scaling.
// So if we have a view that is 200x300, and we want it's upper left corner
// to be at 50x50, but it's scaled by 50%, we can't just translate it to 50x50.
// If we were to do that, the view's *visible* upper left corner would be at
// 100x200. We need to take into account the difference between the outside
// size of the view (i.e. the size prior to scaling) and the scaled size.
// scaleFromEdge is the difference between the visible left edge and the
// actual left edge, due to scaling.
// scaleFromTop is the difference between the visible top edge, and the
// actual top edge, due to scaling.
int scaleFromEdge = Math.round((totalSize - totalSize * scale) / 2);
// The imageView is fullscreen, regardless of the aspect ratio of the actual image.
// This means that some portion of the imageView will be blank. We need to
// take into account the size of the blank area so that the actual image
// lines up with the starting image.
int blankSize = Math.round((totalSize * scale - startSize) / 2);
return start - scaleFromEdge - blankSize;
}
private void initTemporaryImage(Drawable drawable) {
if (mEnterAnimationFinished) {
// Forget this, we've already run the animation.
return;
}
if (hasTemporaryImage()) {
mTemporaryImage.setImageDrawable(drawable);
}
if (drawable != null) {
// We have not yet run the enter animation. Start it now.
int totalWidth = mRootView.getMeasuredWidth();
if (totalWidth == 0) {
// the measure pass has not yet finished. We can't properly
// run out animation until that is done. Listen for the layout
// to occur, then fire the animation.
final View base = mRootView;
base.getViewTreeObserver().addOnGlobalLayoutListener(
new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
int version = android.os.Build.VERSION.SDK_INT;
if (version >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
base.getViewTreeObserver().removeOnGlobalLayoutListener(this);
} else {
base.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
runEnterAnimation();
}
});
} else {
// initiate the animation
runEnterAnimation();
}
}
// Kick off the photo list loader
mActivity.getSupportLoaderManager().initLoader(LOADER_PHOTO_LIST, null, this);
}
public void showActionBar() {
mActivity.getActionBarInterface().show();
}
public void hideActionBar() {
mActivity.getActionBarInterface().hide();
}
public boolean isScaleAnimationEnabled() {
return mScaleAnimationEnabled;
}
public boolean isEnterAnimationFinished() {
return mEnterAnimationFinished;
}
public View getRootView() {
return mRootView;
}
private class BitmapCallback implements LoaderManager.LoaderCallbacks<BitmapResult> {
@Override
public Loader<BitmapResult> onCreateLoader(int id, Bundle args) {
String uri = args.getString(ARG_IMAGE_URI);
switch (id) {
case PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL:
return onCreateBitmapLoader(PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL,
args, uri);
case PhotoViewCallbacks.BITMAP_LOADER_AVATAR:
return onCreateBitmapLoader(PhotoViewCallbacks.BITMAP_LOADER_AVATAR,
args, uri);
}
return null;
}
@Override
public void onLoadFinished(Loader<BitmapResult> loader, BitmapResult result) {
Drawable drawable = result.getDrawable(mActivity.getResources());
final ActionBarInterface actionBar = mActivity.getActionBarInterface();
switch (loader.getId()) {
case PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL:
// We just loaded the initial thumbnail that we can display
// while waiting for the full viewPager to get initialized.
initTemporaryImage(drawable);
break;
case PhotoViewCallbacks.BITMAP_LOADER_AVATAR:
if (drawable == null) {
actionBar.setLogo(null);
} else {
actionBar.setLogo(drawable);
}
break;
}
}
@Override
public void onLoaderReset(Loader<BitmapResult> loader) {
// Do nothing
}
}
public void setImmersiveMode(boolean enabled) {
int flags = 0;
final int version = Build.VERSION.SDK_INT;
final boolean manuallyUpdateActionBar = version < Build.VERSION_CODES.JELLY_BEAN;
if (enabled &&
(!isScaleAnimationEnabled() || isEnterAnimationFinished())) {
// Turning on immersive mode causes an animation. If the scale animation is enabled and
// the enter animation isn't yet complete, then an immersive mode animation should not
// occur, since two concurrent animations are very janky.
// Disable immersive mode for seconary users to prevent b/12015090 (freezing crash)
// This is fixed in KK_MR2 but there is no way to differentiate between KK and KK_MR2.
if (version > Build.VERSION_CODES.KITKAT ||
version == Build.VERSION_CODES.KITKAT && !kitkatIsSecondaryUser()) {
flags = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_IMMERSIVE;
} else if (version >= Build.VERSION_CODES.JELLY_BEAN) {
// Clients that use the scale animation should set the following system UI flags to
// prevent janky animations on exit when the status bar is hidden:
// View.SYSTEM_UI_FLAG_VISIBLE | View.SYSTEM_UI_FLAG_STABLE
// As well, client should ensure `android:fitsSystemWindows` is set on the root
// content view.
flags = View.SYSTEM_UI_FLAG_LOW_PROFILE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_FULLSCREEN;
} else if (version >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
flags = View.SYSTEM_UI_FLAG_LOW_PROFILE;
} else if (version >= Build.VERSION_CODES.HONEYCOMB) {
flags = View.STATUS_BAR_HIDDEN;
}
if (manuallyUpdateActionBar) {
hideActionBar();
}
} else {
if (version >= Build.VERSION_CODES.KITKAT) {
flags = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
} else if (version >= Build.VERSION_CODES.JELLY_BEAN) {
flags = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
} else if (version >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
flags = View.SYSTEM_UI_FLAG_VISIBLE;
} else if (version >= Build.VERSION_CODES.HONEYCOMB) {
flags = View.STATUS_BAR_VISIBLE;
}
if (manuallyUpdateActionBar) {
showActionBar();
}
}
if (version >= Build.VERSION_CODES.HONEYCOMB) {
mLastFlags = flags;
getRootView().setSystemUiVisibility(flags);
}
}
/**
* Return true iff the app is being run as a secondary user on kitkat.
*
* This is a hack which we only know to work on kitkat.
*/
private boolean kitkatIsSecondaryUser() {
if (Build.VERSION.SDK_INT != Build.VERSION_CODES.KITKAT) {
throw new IllegalStateException("kitkatIsSecondary user is only callable on KitKat");
}
return Process.myUid() > 100000;
}
/**
* Note: This should only be called when API level is 11 or above.
*/
public View.OnSystemUiVisibilityChangeListener getSystemUiVisibilityChangeListener() {
return mSystemUiVisibilityChangeListener;
}
}