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