ContactDetailLayoutController.java revision 4b25da79091157935042d2942a8961ceba92166f
1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.contacts.detail;
18
19import com.android.contacts.ContactLoader;
20import com.android.contacts.R;
21import com.android.contacts.activities.ContactDetailActivity.FragmentKeyListener;
22
23import android.animation.ObjectAnimator;
24import android.app.FragmentManager;
25import android.app.FragmentTransaction;
26import android.content.Context;
27import android.os.Bundle;
28import android.support.v4.view.ViewPager;
29import android.support.v4.view.ViewPager.OnPageChangeListener;
30import android.view.LayoutInflater;
31import android.view.View;
32import android.view.animation.AnimationUtils;
33import android.widget.AbsListView;
34import android.widget.AbsListView.OnScrollListener;
35
36/**
37 * Determines the layout of the contact card.
38 */
39public class ContactDetailLayoutController {
40
41    private static final String KEY_CONTACT_HAS_UPDATES = "contactHasUpdates";
42    private static final String KEY_CURRENT_PAGE_INDEX = "currentPageIndex";
43
44    private static final int TAB_INDEX_DETAIL = 0;
45    private static final int TAB_INDEX_UPDATES = 1;
46
47    /**
48     * There are 3 possible layouts for the contact detail screen:
49     * 1. TWO_COLUMN - Tall and wide screen so the 2 pages can be shown side-by-side
50     * 2. VIEW_PAGER_AND_TAB_CAROUSEL - Tall and narrow screen to allow swipe between the 2 pages
51     * 3. FRAGMENT_CAROUSEL- Short and wide screen to allow half of the other page to show at a time
52     */
53    private enum LayoutMode {
54        TWO_COLUMN, VIEW_PAGER_AND_TAB_CAROUSEL, FRAGMENT_CAROUSEL,
55    }
56
57    private final Context mContext;
58    private final LayoutInflater mLayoutInflater;
59    private final FragmentManager mFragmentManager;
60
61    private ContactDetailFragment mDetailFragment;
62    private ContactDetailUpdatesFragment mUpdatesFragment;
63
64    private View mDetailFragmentView;
65    private View mUpdatesFragmentView;
66
67    private final ViewPager mViewPager;
68    private ContactDetailViewPagerAdapter mViewPagerAdapter;
69    private int mViewPagerState;
70
71    private final ContactDetailTabCarousel mTabCarousel;
72    private final ContactDetailFragmentCarousel mFragmentCarousel;
73
74    private ContactDetailFragment.Listener mContactDetailFragmentListener;
75
76    private ContactLoader.Result mContactData;
77
78    private boolean mContactHasUpdates;
79
80    private LayoutMode mLayoutMode;
81
82    public ContactDetailLayoutController(Context context, Bundle savedState,
83            FragmentManager fragmentManager, View viewContainer, ContactDetailFragment.Listener
84            contactDetailFragmentListener) {
85
86        if (fragmentManager == null) {
87            throw new IllegalStateException("Cannot initialize a ContactDetailLayoutController "
88                    + "without a non-null FragmentManager");
89        }
90
91        mContext = context;
92        mLayoutInflater = (LayoutInflater) context.getSystemService(
93                Context.LAYOUT_INFLATER_SERVICE);
94        mFragmentManager = fragmentManager;
95        mContactDetailFragmentListener = contactDetailFragmentListener;
96
97        // Retrieve views in case this is view pager and carousel mode
98        mViewPager = (ViewPager) viewContainer.findViewById(R.id.pager);
99        mTabCarousel = (ContactDetailTabCarousel) viewContainer.findViewById(R.id.tab_carousel);
100
101        // Retrieve view in case this is in fragment carousel mode
102        mFragmentCarousel = (ContactDetailFragmentCarousel) viewContainer.findViewById(
103                R.id.fragment_carousel);
104
105        // Retrieve container views in case they are already in the XML layout
106        mDetailFragmentView = viewContainer.findViewById(R.id.about_fragment_container);
107        mUpdatesFragmentView = viewContainer.findViewById(R.id.updates_fragment_container);
108
109        // Determine the layout mode based on the presence of certain views in the layout XML.
110        if (mViewPager != null) {
111            mLayoutMode = LayoutMode.VIEW_PAGER_AND_TAB_CAROUSEL;
112        } else {
113            mLayoutMode = (mFragmentCarousel != null) ? LayoutMode.FRAGMENT_CAROUSEL :
114                    LayoutMode.TWO_COLUMN;
115        }
116
117        initialize(savedState);
118    }
119
120    private void initialize(Bundle savedState) {
121        boolean fragmentsAddedToFragmentManager = true;
122        mDetailFragment = (ContactDetailFragment) mFragmentManager.findFragmentByTag(
123                ContactDetailViewPagerAdapter.ABOUT_FRAGMENT_TAG);
124        mUpdatesFragment = (ContactDetailUpdatesFragment) mFragmentManager.findFragmentByTag(
125                ContactDetailViewPagerAdapter.UPDTES_FRAGMENT_TAG);
126
127        // If the detail fragment was found in the {@link FragmentManager} then we don't need to add
128        // it again. Otherwise, create the fragments dynamically and remember to add them to the
129        // {@link FragmentManager}.
130        if (mDetailFragment == null) {
131            mDetailFragment = new ContactDetailFragment();
132            mUpdatesFragment = new ContactDetailUpdatesFragment();
133            fragmentsAddedToFragmentManager = false;
134        }
135
136        mDetailFragment.setListener(mContactDetailFragmentListener);
137
138        // Read from savedState if possible
139        int currentPageIndex = 0;
140        if (savedState != null) {
141            mContactHasUpdates = savedState.getBoolean(KEY_CONTACT_HAS_UPDATES);
142            currentPageIndex = savedState.getInt(KEY_CURRENT_PAGE_INDEX, 0);
143        }
144
145        switch (mLayoutMode) {
146            case VIEW_PAGER_AND_TAB_CAROUSEL: {
147                // Inflate 2 view containers to pass in as children to the {@link ViewPager},
148                // which will in turn be the parents to the mDetailFragment and mUpdatesFragment
149                // since the fragments must have the same parent view IDs in both landscape and
150                // portrait layouts.
151                mDetailFragmentView = mLayoutInflater.inflate(
152                        R.layout.contact_detail_about_fragment_container, mViewPager, false);
153                mUpdatesFragmentView = mLayoutInflater.inflate(
154                        R.layout.contact_detail_updates_fragment_container, mViewPager, false);
155
156                mViewPagerAdapter = new ContactDetailViewPagerAdapter();
157                mViewPagerAdapter.setAboutFragmentView(mDetailFragmentView);
158                mViewPagerAdapter.setUpdatesFragmentView(mUpdatesFragmentView);
159
160                mViewPager.addView(mDetailFragmentView);
161                mViewPager.addView(mUpdatesFragmentView);
162                mViewPager.setAdapter(mViewPagerAdapter);
163                mViewPager.setOnPageChangeListener(mOnPageChangeListener);
164
165                if (!fragmentsAddedToFragmentManager) {
166                    FragmentTransaction transaction = mFragmentManager.beginTransaction();
167                    transaction.add(R.id.about_fragment_container, mDetailFragment,
168                            ContactDetailViewPagerAdapter.ABOUT_FRAGMENT_TAG);
169                    transaction.add(R.id.updates_fragment_container, mUpdatesFragment,
170                            ContactDetailViewPagerAdapter.UPDTES_FRAGMENT_TAG);
171                    transaction.commitAllowingStateLoss();
172                    mFragmentManager.executePendingTransactions();
173                }
174
175                mTabCarousel.setListener(mTabCarouselListener);
176                mDetailFragment.setVerticalScrollListener(
177                        new VerticalScrollListener(TAB_INDEX_DETAIL));
178                mUpdatesFragment.setVerticalScrollListener(
179                        new VerticalScrollListener(TAB_INDEX_UPDATES));
180                mViewPager.setCurrentItem(currentPageIndex);
181                break;
182            }
183            case TWO_COLUMN: {
184                if (!fragmentsAddedToFragmentManager) {
185                    FragmentTransaction transaction = mFragmentManager.beginTransaction();
186                    transaction.add(R.id.about_fragment_container, mDetailFragment,
187                            ContactDetailViewPagerAdapter.ABOUT_FRAGMENT_TAG);
188                    transaction.add(R.id.updates_fragment_container, mUpdatesFragment,
189                            ContactDetailViewPagerAdapter.UPDTES_FRAGMENT_TAG);
190                    transaction.commitAllowingStateLoss();
191                    mFragmentManager.executePendingTransactions();
192                }
193                break;
194            }
195            case FRAGMENT_CAROUSEL: {
196                // Add the fragments to the fragment containers in the carousel using a
197                // {@link FragmentTransaction} if they haven't already been added to the
198                // {@link FragmentManager}.
199                if (!fragmentsAddedToFragmentManager) {
200                    FragmentTransaction transaction = mFragmentManager.beginTransaction();
201                    transaction.add(R.id.about_fragment_container, mDetailFragment,
202                            ContactDetailViewPagerAdapter.ABOUT_FRAGMENT_TAG);
203                    transaction.add(R.id.updates_fragment_container, mUpdatesFragment,
204                            ContactDetailViewPagerAdapter.UPDTES_FRAGMENT_TAG);
205                    transaction.commitAllowingStateLoss();
206                    mFragmentManager.executePendingTransactions();
207                }
208
209                mFragmentCarousel.setFragmentViews(mDetailFragmentView, mUpdatesFragmentView);
210                mFragmentCarousel.setFragments(mDetailFragment, mUpdatesFragment);
211                mFragmentCarousel.setCurrentPage(currentPageIndex);
212                break;
213            }
214        }
215
216        // Setup the layout if we already have a saved state
217        if (savedState != null) {
218            if (mContactHasUpdates) {
219                showContactWithUpdates();
220            } else {
221                showContactWithoutUpdates();
222            }
223        }
224    }
225
226    public void setContactData(ContactLoader.Result data) {
227        mContactData = data;
228        mContactHasUpdates = !data.getStreamItems().isEmpty();
229        if (mContactHasUpdates) {
230            showContactWithUpdates();
231        } else {
232            showContactWithoutUpdates();
233        }
234    }
235
236    public void showEmptyState() {
237        switch (mLayoutMode) {
238            case FRAGMENT_CAROUSEL: {
239                mFragmentCarousel.enableSwipe(false);
240                mDetailFragment.showEmptyState();
241                break;
242            }
243            case TWO_COLUMN: {
244                mDetailFragment.setShowStaticPhoto(false);
245                mUpdatesFragmentView.setVisibility(View.GONE);
246                mDetailFragment.showEmptyState();
247                break;
248            }
249            case VIEW_PAGER_AND_TAB_CAROUSEL: {
250                mDetailFragment.setShowStaticPhoto(false);
251                mDetailFragment.showEmptyState();
252                mTabCarousel.loadData(null);
253                mTabCarousel.setVisibility(View.GONE);
254                mViewPagerAdapter.enableSwipe(false);
255                mViewPager.setCurrentItem(0);
256                break;
257            }
258            default:
259                throw new IllegalStateException("Invalid LayoutMode " + mLayoutMode);
260        }
261    }
262
263    /**
264     * Setup the layout for the contact with updates. Pass in the index of the current page to
265     * select or null if the current selection should be left as is.
266     */
267    private void showContactWithUpdates() {
268        if (mContactData == null) {
269            return;
270        }
271        switch (mLayoutMode) {
272            case TWO_COLUMN: {
273                // Set the contact data (hide the static photo because the photo will already be in
274                // the header that scrolls with contact details).
275                mDetailFragment.setShowStaticPhoto(false);
276                // Show the updates fragment
277                mUpdatesFragmentView.setVisibility(View.VISIBLE);
278                break;
279            }
280            case VIEW_PAGER_AND_TAB_CAROUSEL: {
281                // Update and show the tab carousel (also restore its last saved position)
282                mTabCarousel.loadData(mContactData);
283                mTabCarousel.restoreYCoordinate();
284                mTabCarousel.setVisibility(View.VISIBLE);
285                // Update ViewPager to allow swipe between all the fragments (to see updates)
286                mViewPagerAdapter.enableSwipe(true);
287                break;
288            }
289            case FRAGMENT_CAROUSEL: {
290                // Allow swiping between all fragments
291                mFragmentCarousel.enableSwipe(true);
292                break;
293            }
294            default:
295                throw new IllegalStateException("Invalid LayoutMode " + mLayoutMode);
296        }
297
298        mDetailFragment.setData(mContactData.getLookupUri(), mContactData);
299        mUpdatesFragment.setData(mContactData.getLookupUri(), mContactData);
300    }
301
302    private void showContactWithoutUpdates() {
303        if (mContactData == null) {
304            return;
305        }
306        switch (mLayoutMode) {
307            case TWO_COLUMN:
308                // Show the static photo which is next to the list of scrolling contact details
309                mDetailFragment.setShowStaticPhoto(true);
310                // Hide the updates fragment
311                mUpdatesFragmentView.setVisibility(View.GONE);
312                break;
313            case VIEW_PAGER_AND_TAB_CAROUSEL:
314                // Hide the tab carousel
315                mTabCarousel.setVisibility(View.GONE);
316                // Update ViewPager to disable swipe so that it only shows the detail fragment
317                // and switch to the detail fragment
318                mViewPagerAdapter.enableSwipe(false);
319                mViewPager.setCurrentItem(0);
320                break;
321            case FRAGMENT_CAROUSEL: {
322                // Disable swipe so only the detail fragment shows
323                mFragmentCarousel.enableSwipe(false);
324                break;
325            }
326            default:
327                throw new IllegalStateException("Invalid LayoutMode " + mLayoutMode);
328        }
329
330        mDetailFragment.setData(mContactData.getLookupUri(), mContactData);
331    }
332
333    public FragmentKeyListener getCurrentPage() {
334        switch (getCurrentPageIndex()) {
335            case 0:
336                return mDetailFragment;
337            case 1:
338                return mUpdatesFragment;
339            default:
340                throw new IllegalStateException("Invalid current item for ViewPager");
341        }
342    }
343
344    private int getCurrentPageIndex() {
345        // If the contact has social updates, then retrieve the current page based on the
346        // {@link ViewPager} or fragment carousel.
347        if (mContactHasUpdates) {
348            if (mViewPager != null) {
349                return mViewPager.getCurrentItem();
350            } else if (mFragmentCarousel != null) {
351                return mFragmentCarousel.getCurrentPage();
352            }
353        }
354        // Otherwise return the default page (detail fragment).
355        return 0;
356    }
357
358    public void onSaveInstanceState(Bundle outState) {
359        outState.putBoolean(KEY_CONTACT_HAS_UPDATES, mContactHasUpdates);
360        outState.putInt(KEY_CURRENT_PAGE_INDEX, getCurrentPageIndex());
361    }
362
363    private final OnPageChangeListener mOnPageChangeListener = new OnPageChangeListener() {
364
365        private ObjectAnimator mTabCarouselAnimator;
366
367        @Override
368        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
369            // The user is horizontally dragging the {@link ViewPager}, so send
370            // these scroll changes to the tab carousel. Ignore these events though if the carousel
371            // is actually controlling the {@link ViewPager} scrolls because it will already be
372            // in the correct position.
373            if (mViewPager.isFakeDragging()) {
374                return;
375            }
376            int x = (int) ((position + positionOffset) *
377                    mTabCarousel.getAllowedHorizontalScrollLength());
378            mTabCarousel.scrollTo(x, 0);
379        }
380
381        @Override
382        public void onPageSelected(int position) {
383            // Since the {@link ViewPager} has committed to a new page now (but may not have
384            // finished scrolling yet), update the tab selection in the carousel.
385            mTabCarousel.setCurrentTab(position);
386        }
387
388        @Override
389        public void onPageScrollStateChanged(int state) {
390            if (mViewPagerState == ViewPager.SCROLL_STATE_IDLE) {
391
392                // If we are leaving the IDLE state, we are starting a swipe.
393                // First cancel any pending animations on the tab carousel.
394                cancelTabCarouselAnimator();
395
396                // Sync the two lists because the list on the other page will start to show as
397                // we swipe over more.
398                syncScrollStateBetweenLists(mViewPager.getCurrentItem());
399
400            } else if (state == ViewPager.SCROLL_STATE_IDLE) {
401
402                // Otherwise if the {@link ViewPager} is idle now, a page has been selected and
403                // scrolled into place. Perform an animation of the tab carousel is needed.
404                int currentPageIndex = mViewPager.getCurrentItem();
405                int tabCarouselOffset = (int) mTabCarousel.getY();
406                boolean shouldAnimateTabCarousel;
407
408                // Find the offset position of the first item in the list of the current page.
409                int listOffset = getOffsetOfFirstItemInList(currentPageIndex);
410
411                // If the list was able to successfully offset by the tab carousel amount, then
412                // log this as the new Y coordinate for that page, and no animation is needed.
413                if (listOffset == tabCarouselOffset) {
414                    mTabCarousel.storeYCoordinate(currentPageIndex, tabCarouselOffset);
415                    shouldAnimateTabCarousel = false;
416                } else if (listOffset == Integer.MIN_VALUE) {
417                    // If the offset of the first item in the list is unknown (i.e. the item
418                    // is no longer visible on screen) then just animate the tab carousel to the
419                    // previously logged position.
420                    shouldAnimateTabCarousel = true;
421                } else if (Math.abs(listOffset) < Math.abs(tabCarouselOffset)) {
422                    // If the list could not offset the full amount of the tab carousel offset (i.e.
423                    // the list can only be scrolled a tiny amount), then animate the carousel down
424                    // to compensate.
425                    mTabCarousel.storeYCoordinate(currentPageIndex, listOffset);
426                    shouldAnimateTabCarousel = true;
427                } else {
428                    // By default, animate back to the Y coordinate of the tab carousel the last
429                    // time the other page was selected.
430                    shouldAnimateTabCarousel = true;
431                }
432
433                if (shouldAnimateTabCarousel) {
434                    float desiredOffset = mTabCarousel.getStoredYCoordinateForTab(currentPageIndex);
435                    if (desiredOffset != tabCarouselOffset) {
436                        createTabCarouselAnimator(desiredOffset);
437                        mTabCarouselAnimator.start();
438                    }
439                }
440            }
441            mViewPagerState = state;
442        }
443
444        private void createTabCarouselAnimator(float desiredValue) {
445            mTabCarouselAnimator = ObjectAnimator.ofFloat(
446                    mTabCarousel, "y", desiredValue).setDuration(75);
447            mTabCarouselAnimator.setInterpolator(AnimationUtils.loadInterpolator(
448                    mContext, android.R.anim.accelerate_decelerate_interpolator));
449        }
450
451        private void cancelTabCarouselAnimator() {
452            if (mTabCarouselAnimator != null) {
453                mTabCarouselAnimator.cancel();
454                mTabCarouselAnimator = null;
455            }
456        }
457    };
458
459    private void syncScrollStateBetweenLists(int currentPageIndex) {
460        // Since the user interacted with the currently visible page, we need to sync the
461        // list on the other page (i.e. if the updates page is the current page, modify the
462        // list in the details page).
463        if (currentPageIndex == TAB_INDEX_UPDATES) {
464            mDetailFragment.requestToMoveToOffset((int) mTabCarousel.getY());
465        } else {
466            mUpdatesFragment.requestToMoveToOffset((int) mTabCarousel.getY());
467        }
468    }
469
470    private int getOffsetOfFirstItemInList(int currentPageIndex) {
471        if (currentPageIndex == TAB_INDEX_DETAIL) {
472            return mDetailFragment.getFirstListItemOffset();
473        } else {
474            return mUpdatesFragment.getFirstListItemOffset();
475        }
476    }
477
478    private final ContactDetailTabCarousel.Listener mTabCarouselListener =
479            new ContactDetailTabCarousel.Listener() {
480
481        @Override
482        public void onTouchDown() {
483            // The user just started scrolling the carousel, so begin "fake dragging" the
484            // {@link ViewPager} if it's not already doing so.
485            if (mViewPager.isFakeDragging()) {
486                return;
487            }
488            mViewPager.beginFakeDrag();
489        }
490
491        @Override
492        public void onTouchUp() {
493            // The user just stopped scrolling the carousel, so stop "fake dragging" the
494            // {@link ViewPager} if was doing so before.
495            if (mViewPager.isFakeDragging()) {
496                mViewPager.endFakeDrag();
497            }
498        }
499
500        @Override
501        public void onScrollChanged(int l, int t, int oldl, int oldt) {
502            // The user is scrolling the carousel, so send the scroll deltas to the
503            // {@link ViewPager} so it can move in sync.
504            if (mViewPager.isFakeDragging()) {
505                mViewPager.fakeDragBy(oldl-l);
506            }
507        }
508
509        @Override
510        public void onTabSelected(int position) {
511            // The user selected a tab, so update the {@link ViewPager}
512            mViewPager.setCurrentItem(position);
513        }
514    };
515
516    private final class VerticalScrollListener implements OnScrollListener {
517
518        private final int mPageIndex;
519
520        public VerticalScrollListener(int pageIndex) {
521            mPageIndex = pageIndex;
522        }
523
524        @Override
525        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
526                int totalItemCount) {
527            int currentPageIndex = mViewPager.getCurrentItem();
528            // Don't move the carousel if: 1) the contact does not have social updates because then
529            // tab carousel must not be visible, 2) if the view pager is still being scrolled, or
530            // 3) if the current page being viewed is not this one.
531            if (!mContactHasUpdates || mViewPagerState != ViewPager.SCROLL_STATE_IDLE ||
532                    mPageIndex != currentPageIndex) {
533                return;
534            }
535            // If the FIRST item is not visible on the screen, then the carousel must be pinned
536            // at the top of the screen.
537            if (firstVisibleItem != 0) {
538                mTabCarousel.moveToYCoordinate(mPageIndex,
539                        -mTabCarousel.getAllowedVerticalScrollLength());
540                return;
541            }
542            View topView = view.getChildAt(firstVisibleItem);
543            if (topView == null) {
544                return;
545            }
546            int amtToScroll = Math.max((int) view.getChildAt(firstVisibleItem).getY(),
547                    -mTabCarousel.getAllowedVerticalScrollLength());
548            mTabCarousel.moveToYCoordinate(mPageIndex, amtToScroll);
549        }
550
551        @Override
552        public void onScrollStateChanged(AbsListView view, int scrollState) {
553            // Once the list has become IDLE, check if we need to sync the scroll position of
554            // the other list now. This will make swiping faster by doing the re-layout now
555            // (instead of at the start of a swipe). However, there will still be another check
556            // when we start swiping if the scroll positions are correct (to catch the edge case
557            // where the user flings and immediately starts a swipe so we never get the idle state).
558            if (scrollState == SCROLL_STATE_IDLE) {
559                syncScrollStateBetweenLists(mPageIndex);
560            }
561        }
562    }
563}
564