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