Home | History | Annotate | Download | only in detail
      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 
     17 package com.android.contacts.detail;
     18 
     19 import android.content.Context;
     20 import android.util.AttributeSet;
     21 import android.view.LayoutInflater;
     22 import android.view.MotionEvent;
     23 import android.view.View;
     24 import android.view.View.OnTouchListener;
     25 import android.view.ViewPropertyAnimator;
     26 import android.widget.HorizontalScrollView;
     27 
     28 import com.android.contacts.R;
     29 import com.android.contacts.widget.FrameLayoutWithOverlay;
     30 
     31 /**
     32  * This is a horizontally scrolling carousel with 2 fragments: one to see info about the contact and
     33  * one to see updates from the contact. Depending on the scroll position and user selection of which
     34  * fragment to currently view, the touch interceptors over each fragment are configured accordingly.
     35  */
     36 public class ContactDetailFragmentCarousel extends HorizontalScrollView implements OnTouchListener {
     37 
     38     private static final String TAG = ContactDetailFragmentCarousel.class.getSimpleName();
     39 
     40     /**
     41      * Number of pixels that this view can be scrolled horizontally.
     42      */
     43     private int mAllowedHorizontalScrollLength = Integer.MIN_VALUE;
     44 
     45     /**
     46      * Minimum X scroll position that must be surpassed (if the user is on the "about" page of the
     47      * contact card), in order for this view to automatically snap to the "updates" page.
     48      */
     49     private int mLowerThreshold = Integer.MIN_VALUE;
     50 
     51     /**
     52      * Maximum X scroll position (if the user is on the "updates" page of the contact card), below
     53      * which this view will automatically snap to the "about" page.
     54      */
     55     private int mUpperThreshold = Integer.MIN_VALUE;
     56 
     57     /**
     58      * Minimum width of a fragment (if there is more than 1 fragment in the carousel, then this is
     59      * the width of one of the fragments).
     60      */
     61     private int mMinFragmentWidth = Integer.MIN_VALUE;
     62 
     63     /**
     64      * Fragment width (if there are 1+ fragments in the carousel) as defined as a fraction of the
     65      * screen width.
     66      */
     67     private static final float FRAGMENT_WIDTH_SCREEN_WIDTH_FRACTION = 0.85f;
     68 
     69     private static final int ABOUT_PAGE = 0;
     70     private static final int UPDATES_PAGE = 1;
     71 
     72     private static final int MAX_FRAGMENT_VIEW_COUNT = 2;
     73 
     74     private boolean mEnableSwipe;
     75 
     76     private int mCurrentPage = ABOUT_PAGE;
     77     private int mLastScrollPosition;
     78 
     79     private FrameLayoutWithOverlay mAboutFragment;
     80     private FrameLayoutWithOverlay mUpdatesFragment;
     81 
     82     public ContactDetailFragmentCarousel(Context context) {
     83         this(context, null);
     84     }
     85 
     86     public ContactDetailFragmentCarousel(Context context, AttributeSet attrs) {
     87         this(context, attrs, 0);
     88     }
     89 
     90     public ContactDetailFragmentCarousel(Context context, AttributeSet attrs, int defStyle) {
     91         super(context, attrs, defStyle);
     92 
     93         final LayoutInflater inflater =
     94                 (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
     95         inflater.inflate(R.layout.contact_detail_fragment_carousel, this);
     96 
     97         setOnTouchListener(this);
     98     }
     99 
    100     @Override
    101     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    102         int screenWidth = MeasureSpec.getSize(widthMeasureSpec);
    103         int screenHeight = MeasureSpec.getSize(heightMeasureSpec);
    104 
    105         // Take the width of this view as the width of the screen and compute necessary thresholds.
    106         // Only do this computation 1x.
    107         if (mAllowedHorizontalScrollLength == Integer.MIN_VALUE) {
    108             mMinFragmentWidth = (int) (FRAGMENT_WIDTH_SCREEN_WIDTH_FRACTION * screenWidth);
    109             mAllowedHorizontalScrollLength = (MAX_FRAGMENT_VIEW_COUNT * mMinFragmentWidth) -
    110                     screenWidth;
    111             mLowerThreshold = (screenWidth - mMinFragmentWidth) / MAX_FRAGMENT_VIEW_COUNT;
    112             mUpperThreshold = mAllowedHorizontalScrollLength - mLowerThreshold;
    113         }
    114 
    115         if (getChildCount() > 0) {
    116             View child = getChildAt(0);
    117             // If we enable swipe, then the {@link LinearLayout} child width must be the sum of the
    118             // width of all its children fragments.
    119             // Or the current page may already be set to something other than the first.  If so,
    120             // it also means there are multiple child fragments.
    121             if (mEnableSwipe || mCurrentPage == 1 ||
    122                     (mCurrentPage == 0 && getLayoutDirection() == View.LAYOUT_DIRECTION_RTL)) {
    123                 child.measure(MeasureSpec.makeMeasureSpec(
    124                         mMinFragmentWidth * MAX_FRAGMENT_VIEW_COUNT, MeasureSpec.EXACTLY),
    125                         MeasureSpec.makeMeasureSpec(screenHeight, MeasureSpec.EXACTLY));
    126             } else {
    127                 // Otherwise, the {@link LinearLayout} child width will just be the screen width
    128                 // because it will only have 1 child fragment.
    129                 child.measure(MeasureSpec.makeMeasureSpec(screenWidth, MeasureSpec.EXACTLY),
    130                         MeasureSpec.makeMeasureSpec(screenHeight, MeasureSpec.EXACTLY));
    131             }
    132         }
    133 
    134         setMeasuredDimension(
    135                 resolveSize(screenWidth, widthMeasureSpec),
    136                 resolveSize(screenHeight, heightMeasureSpec));
    137     }
    138 
    139     /**
    140      * Set the current page. This dims out the non-selected page but doesn't do any scrolling of
    141      * the carousel.
    142      */
    143     public void setCurrentPage(int pageIndex) {
    144         mCurrentPage = pageIndex;
    145 
    146         updateTouchInterceptors();
    147     }
    148 
    149     /**
    150      * Set the view containers for the detail and updates fragment.
    151      */
    152     public void setFragmentViews(FrameLayoutWithOverlay about, FrameLayoutWithOverlay updates) {
    153         mAboutFragment = about;
    154         mUpdatesFragment = updates;
    155 
    156         mAboutFragment.setOverlayOnClickListener(mAboutFragTouchInterceptListener);
    157         mUpdatesFragment.setOverlayOnClickListener(mUpdatesFragTouchInterceptListener);
    158     }
    159 
    160     /**
    161      * Enable swiping if the detail and update fragments should be showing. Otherwise disable
    162      * swiping if only the detail fragment should be showing.
    163      */
    164     public void enableSwipe(boolean enable) {
    165         if (mEnableSwipe != enable) {
    166             mEnableSwipe = enable;
    167             if (mUpdatesFragment != null) {
    168                 mUpdatesFragment.setVisibility(enable ? View.VISIBLE : View.GONE);
    169                 snapToEdge();
    170                 updateTouchInterceptors();
    171             }
    172         }
    173     }
    174 
    175     /**
    176      * Reset the fragment carousel to show the about page.
    177      */
    178     public void reset() {
    179         if (mCurrentPage != ABOUT_PAGE) {
    180             mCurrentPage = ABOUT_PAGE;
    181             snapToEdgeSmooth();
    182         }
    183     }
    184 
    185     public int getCurrentPage() {
    186         return mCurrentPage;
    187     }
    188 
    189     private final OnClickListener mAboutFragTouchInterceptListener = new OnClickListener() {
    190         @Override
    191         public void onClick(View v) {
    192             mCurrentPage = ABOUT_PAGE;
    193             snapToEdgeSmooth();
    194         }
    195     };
    196 
    197     private final OnClickListener mUpdatesFragTouchInterceptListener = new OnClickListener() {
    198         @Override
    199         public void onClick(View v) {
    200             mCurrentPage = UPDATES_PAGE;
    201             snapToEdgeSmooth();
    202         }
    203     };
    204 
    205     private void updateTouchInterceptors() {
    206         // Disable the touch-interceptor on the selected page, and enable it on the other.
    207         if (mAboutFragment != null) {
    208             mAboutFragment.setOverlayClickable(mCurrentPage != ABOUT_PAGE);
    209         }
    210         if (mUpdatesFragment != null) {
    211             mUpdatesFragment.setOverlayClickable(mCurrentPage != UPDATES_PAGE);
    212         }
    213     }
    214 
    215     @Override
    216     protected void onScrollChanged(int l, int t, int oldl, int oldt) {
    217         super.onScrollChanged(l, t, oldl, oldt);
    218         if (!mEnableSwipe) {
    219             return;
    220         }
    221         mLastScrollPosition = l;
    222     }
    223 
    224     /**
    225      * Used to set initial scroll offset.  Not smooth.
    226      */
    227     private void snapToEdge() {
    228         setScrollX(calculateHorizontalOffset());
    229         updateTouchInterceptors();
    230     }
    231 
    232     /**
    233      * Smooth version of snapToEdge().
    234      */
    235     private void snapToEdgeSmooth() {
    236         smoothScrollTo(calculateHorizontalOffset(), 0);
    237         updateTouchInterceptors();
    238     }
    239 
    240     private int calculateHorizontalOffset() {
    241         int offset;
    242         if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
    243             offset = (mCurrentPage == ABOUT_PAGE) ? mAllowedHorizontalScrollLength : 0;
    244         } else {
    245             offset = (mCurrentPage == ABOUT_PAGE) ? 0 : mAllowedHorizontalScrollLength;
    246         }
    247         return offset;
    248     }
    249 
    250     /**
    251      * Returns the desired page we should scroll to based on the current X scroll position and the
    252      * current page.
    253      */
    254     private int getDesiredPage() {
    255         switch (mCurrentPage) {
    256             case ABOUT_PAGE:
    257                 if (getLayoutDirection() == View.LAYOUT_DIRECTION_LTR) {
    258                     // If the user is on the "about" page, and the scroll position exceeds the lower
    259                     // threshold, then we should switch to the "updates" page.
    260                     return (mLastScrollPosition > mLowerThreshold) ? UPDATES_PAGE : ABOUT_PAGE;
    261                 } else {
    262                     return (mLastScrollPosition < mUpperThreshold) ? UPDATES_PAGE : ABOUT_PAGE;
    263                 }
    264             case UPDATES_PAGE:
    265                 if (getLayoutDirection() == View.LAYOUT_DIRECTION_LTR) {
    266                     // If the user is on the "updates" page, and the scroll position goes below the
    267                     // upper threshold, then we should switch to the "about" page.
    268                     return (mLastScrollPosition < mUpperThreshold) ? ABOUT_PAGE : UPDATES_PAGE;
    269                 } else {
    270                     return (mLastScrollPosition > mLowerThreshold) ? ABOUT_PAGE : UPDATES_PAGE;
    271                 }
    272         }
    273         throw new IllegalStateException("Invalid current page " + mCurrentPage);
    274     }
    275 
    276     @Override
    277     public boolean onTouch(View v, MotionEvent event) {
    278         if (!mEnableSwipe) {
    279             return false;
    280         }
    281         if (event.getAction() == MotionEvent.ACTION_UP) {
    282             mCurrentPage = getDesiredPage();
    283             snapToEdgeSmooth();
    284             return true;
    285         }
    286         return false;
    287     }
    288 
    289     /**
    290      * Starts an "appear" animation by moving in the "Updates" from the right.
    291      */
    292     public void animateAppear() {
    293         final int x = Math.round((1.0f - FRAGMENT_WIDTH_SCREEN_WIDTH_FRACTION) * getWidth());
    294         mUpdatesFragment.setTranslationX(x);
    295         final ViewPropertyAnimator animator = mUpdatesFragment.animate();
    296         animator.translationX(0.0f);
    297     }
    298 }
    299