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 import com.android.contacts.ContactLoader;
     21 import com.android.contacts.detail.ContactDetailPhotoSetter;
     22 import com.android.contacts.util.MoreMath;
     23 import com.android.contacts.util.PhoneCapabilityTester;
     24 import com.android.contacts.util.SchedulingUtils;
     25 
     26 import android.content.Context;
     27 import android.content.res.Resources;
     28 import android.util.AttributeSet;
     29 import android.util.TypedValue;
     30 import android.view.MotionEvent;
     31 import android.view.View;
     32 import android.view.View.OnTouchListener;
     33 import android.view.ViewPropertyAnimator;
     34 import android.widget.HorizontalScrollView;
     35 import android.widget.ImageView;
     36 import android.widget.TextView;
     37 
     38 import android.util.Log;
     39 
     40 /**
     41  * This is a horizontally scrolling carousel with 2 tabs: one to see info about the contact and
     42  * one to see updates from the contact.
     43  */
     44 public class ContactDetailTabCarousel extends HorizontalScrollView implements OnTouchListener {
     45 
     46     private static final String TAG = ContactDetailTabCarousel.class.getSimpleName();
     47 
     48     private static final int TRANSITION_TIME = 200;
     49     private static final int TRANSITION_MOVE_IN_TIME = 150;
     50 
     51     private static final int TAB_INDEX_ABOUT = 0;
     52     private static final int TAB_INDEX_UPDATES = 1;
     53     private static final int TAB_COUNT = 2;
     54 
     55     /** Tab width as defined as a fraction of the screen width */
     56     private float mTabWidthScreenWidthFraction;
     57 
     58     /** Tab height as defined as a fraction of the screen width */
     59     private float mTabHeightScreenWidthFraction;
     60 
     61     /** Height in pixels of the shadow under the tab carousel */
     62     private int mTabShadowHeight;
     63 
     64     private ImageView mPhotoView;
     65     private View mPhotoViewOverlay;
     66     private TextView mStatusView;
     67     private ImageView mStatusPhotoView;
     68     private final ContactDetailPhotoSetter mPhotoSetter = new ContactDetailPhotoSetter();
     69 
     70     private Listener mListener;
     71 
     72     private int mCurrentTab = TAB_INDEX_ABOUT;
     73 
     74     private View mTabAndShadowContainer;
     75     private View mShadow;
     76     private CarouselTab mAboutTab;
     77     private View mTabDivider;
     78     private CarouselTab mUpdatesTab;
     79 
     80     /** Last Y coordinate of the carousel when the tab at the given index was selected */
     81     private final float[] mYCoordinateArray = new float[TAB_COUNT];
     82 
     83     private int mTabDisplayLabelHeight;
     84 
     85     private boolean mScrollToCurrentTab = false;
     86     private int mLastScrollPosition = Integer.MIN_VALUE;
     87     private int mAllowedHorizontalScrollLength = Integer.MIN_VALUE;
     88     private int mAllowedVerticalScrollLength = Integer.MIN_VALUE;
     89 
     90     /** Factor to scale scroll-amount sent to listeners. */
     91     private float mScrollScaleFactor = 1.0f;
     92 
     93     private static final float MAX_ALPHA = 0.5f;
     94 
     95     /**
     96      * Interface for callbacks invoked when the user interacts with the carousel.
     97      */
     98     public interface Listener {
     99         public void onTouchDown();
    100         public void onTouchUp();
    101 
    102         public void onScrollChanged(int l, int t, int oldl, int oldt);
    103         public void onTabSelected(int position);
    104     }
    105 
    106     public ContactDetailTabCarousel(Context context, AttributeSet attrs) {
    107         super(context, attrs);
    108 
    109         setOnTouchListener(this);
    110 
    111         Resources resources = mContext.getResources();
    112         mTabDisplayLabelHeight = resources.getDimensionPixelSize(
    113                 R.dimen.detail_tab_carousel_tab_label_height);
    114         mTabShadowHeight = resources.getDimensionPixelSize(
    115                 R.dimen.detail_contact_photo_shadow_height);
    116         mTabWidthScreenWidthFraction = resources.getFraction(
    117                 R.fraction.tab_width_screen_width_percentage, 1, 1);
    118         mTabHeightScreenWidthFraction = resources.getFraction(
    119                 R.fraction.tab_height_screen_width_percentage, 1, 1);
    120     }
    121 
    122     @Override
    123     protected void onFinishInflate() {
    124         super.onFinishInflate();
    125         mTabAndShadowContainer = findViewById(R.id.tab_and_shadow_container);
    126         mAboutTab = (CarouselTab) findViewById(R.id.tab_about);
    127         mAboutTab.setLabel(mContext.getString(R.string.contactDetailAbout));
    128         mAboutTab.setOverlayOnClickListener(mAboutTabTouchInterceptListener);
    129 
    130         mTabDivider = findViewById(R.id.tab_divider);
    131 
    132         mUpdatesTab = (CarouselTab) findViewById(R.id.tab_update);
    133         mUpdatesTab.setLabel(mContext.getString(R.string.contactDetailUpdates));
    134         mUpdatesTab.setOverlayOnClickListener(mUpdatesTabTouchInterceptListener);
    135 
    136         mShadow = findViewById(R.id.shadow);
    137 
    138         // Retrieve the photo view for the "about" tab
    139         // TODO: This should be moved down to mAboutTab, so that it hosts its own controls
    140         mPhotoView = (ImageView) mAboutTab.findViewById(R.id.photo);
    141         mPhotoViewOverlay = mAboutTab.findViewById(R.id.photo_overlay);
    142 
    143         // Retrieve the social update views for the "updates" tab
    144         // TODO: This should be moved down to mUpdatesTab, so that it hosts its own controls
    145         mStatusView = (TextView) mUpdatesTab.findViewById(R.id.status);
    146         mStatusPhotoView = (ImageView) mUpdatesTab.findViewById(R.id.status_photo);
    147 
    148         // Workaround for framework issue... it shouldn't be necessary to have a
    149         // clickable object in the hierarchy, but if not the horizontal scroll
    150         // behavior doesn't work. Note: the "About" tab doesn't need this
    151         // because we set a real click-handler elsewhere.
    152         mStatusView.setClickable(true);
    153         mStatusPhotoView.setClickable(true);
    154     }
    155 
    156     @Override
    157     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    158         int screenWidth = MeasureSpec.getSize(widthMeasureSpec);
    159         // Compute the width of a tab as a fraction of the screen width
    160         int tabWidth = Math.round(mTabWidthScreenWidthFraction * screenWidth);
    161 
    162         // Find the allowed scrolling length by subtracting the current visible screen width
    163         // from the total length of the tabs.
    164         mAllowedHorizontalScrollLength = tabWidth * TAB_COUNT - screenWidth;
    165 
    166         // Scrolling by mAllowedHorizontalScrollLength causes listeners to
    167         // scroll by the entire screen amount; compute the scale-factor
    168         // necessary to make this so.
    169         if (mAllowedHorizontalScrollLength == 0) {
    170             // Guard against divide-by-zero.
    171             // Note: this hard-coded value prevents a crash, but won't result in the
    172             // desired scrolling behavior.  We rely on the framework calling onMeasure()
    173             // again with a non-zero screen width.
    174             mScrollScaleFactor = 1.0f;
    175             Log.w(TAG, "set scale-factor to 1.0 to avoid divide-by-zero");
    176         } else {
    177             mScrollScaleFactor = screenWidth / mAllowedHorizontalScrollLength;
    178         }
    179 
    180         int tabHeight = Math.round(screenWidth * mTabHeightScreenWidthFraction) + mTabShadowHeight;
    181         // Set the child {@link LinearLayout} to be TAB_COUNT * the computed tab width so that the
    182         // {@link LinearLayout}'s children (which are the tabs) will evenly split that width.
    183         if (getChildCount() > 0) {
    184             View child = getChildAt(0);
    185 
    186             // add 1 dip of separation between the tabs
    187             final int seperatorPixels =
    188                     (int)(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1,
    189                     getResources().getDisplayMetrics()) + 0.5f);
    190 
    191             child.measure(
    192                     MeasureSpec.makeMeasureSpec(
    193                             TAB_COUNT * tabWidth +
    194                             (TAB_COUNT - 1) * seperatorPixels, MeasureSpec.EXACTLY),
    195                     MeasureSpec.makeMeasureSpec(tabHeight, MeasureSpec.EXACTLY));
    196         }
    197 
    198         mAllowedVerticalScrollLength = tabHeight - mTabDisplayLabelHeight - mTabShadowHeight;
    199         setMeasuredDimension(
    200                 resolveSize(screenWidth, widthMeasureSpec),
    201                 resolveSize(tabHeight, heightMeasureSpec));
    202     }
    203 
    204     @Override
    205     protected void onLayout(boolean changed, int l, int t, int r, int b) {
    206         super.onLayout(changed, l, t, r, b);
    207 
    208         // Defer this stuff until after the layout has finished.  This is because
    209         // updateAlphaLayers() ultimately results in another layout request, and
    210         // the framework currently can't handle this safely.
    211         if (!mScrollToCurrentTab) return;
    212         mScrollToCurrentTab = false;
    213         SchedulingUtils.doAfterLayout(this, new Runnable() {
    214             @Override
    215             public void run() {
    216                 scrollTo(mCurrentTab == TAB_INDEX_ABOUT ? 0 : mAllowedHorizontalScrollLength, 0);
    217                 updateAlphaLayers();
    218             }
    219         });
    220     }
    221 
    222     /** When clicked, selects the corresponding tab. */
    223     private class TabClickListener implements OnClickListener {
    224         private final int mTab;
    225 
    226         public TabClickListener(int tab) {
    227             super();
    228             mTab = tab;
    229         }
    230 
    231         @Override
    232         public void onClick(View v) {
    233             mListener.onTabSelected(mTab);
    234         }
    235     }
    236 
    237     private final TabClickListener mAboutTabTouchInterceptListener =
    238             new TabClickListener(TAB_INDEX_ABOUT);
    239 
    240     private final TabClickListener mUpdatesTabTouchInterceptListener =
    241             new TabClickListener(TAB_INDEX_UPDATES);
    242 
    243     /**
    244      * Does in "appear" animation to allow a seamless transition from
    245      * the "No updates" mode.
    246      * @param width Width of the container. As we haven't been layed out yet, we can't know
    247      * @param scrollOffset The offset by how far we scrolled, where 0=not scrolled, -x=scrolled by
    248      * x pixels, Integer.MIN_VALUE=scrolled so far that the image is not visible in "no updates"
    249      * mode of this screen
    250      */
    251     public void animateAppear(int width, int scrollOffset) {
    252         final float photoHeight = mTabHeightScreenWidthFraction * width;
    253         final boolean animateZoomAndFade;
    254         int pixelsToScrollVertically = 0;
    255 
    256         // Depending on how far we are scrolled down, there is one of three animations:
    257         //   - Zoom and fade the picture (if it is still visible)
    258         //   - Scroll, zoom and fade (if the picture is mostly invisible and we now have a
    259         //     bigger visible region due to the pinning)
    260         //   - Just scroll if the picture is completely invisible. This time, no zoom is needed
    261         if (scrollOffset == Integer.MIN_VALUE) {
    262             // animate in completely by scrolling. no need for zooming here
    263             pixelsToScrollVertically = mTabDisplayLabelHeight;
    264             animateZoomAndFade = false;
    265         } else {
    266             final int pixelsOfPhotoLeft = Math.round(photoHeight) + scrollOffset;
    267             if (pixelsOfPhotoLeft > mTabDisplayLabelHeight) {
    268                 // nothing to scroll
    269                 pixelsToScrollVertically = 0;
    270             } else {
    271                 pixelsToScrollVertically = mTabDisplayLabelHeight - pixelsOfPhotoLeft;
    272             }
    273             animateZoomAndFade = true;
    274         }
    275 
    276         if (pixelsToScrollVertically != 0) {
    277             // We can't animate ourselves here, because our own translation is needed for the user's
    278             // scrolling. Instead, we use our only child. As we are transparent, that is just as
    279             // good
    280             mTabAndShadowContainer.setTranslationY(-pixelsToScrollVertically);
    281             final ViewPropertyAnimator animator = mTabAndShadowContainer.animate();
    282             animator.translationY(0.0f);
    283             animator.setDuration(TRANSITION_MOVE_IN_TIME);
    284         }
    285 
    286         if (animateZoomAndFade) {
    287             // Hack: We have two types of possible layouts:
    288             //   If the picture is square, it is square in both "with updates" and "without updates"
    289             //     --> no need for scale animation here
    290             //     example: 10inch tablet portrait
    291             //   If the picture is non-square, it is full-width in "without updates" and something
    292             //     arbitrary in "with updates"
    293             //     --> do animation with container
    294             //     example: 4.6inch phone portrait
    295             final boolean squarePicture =
    296                     mTabWidthScreenWidthFraction == mTabHeightScreenWidthFraction;
    297             final int firstTransitionTime;
    298             if (squarePicture) {
    299                 firstTransitionTime = 0;
    300             } else {
    301                 // For x, we need to scale our container so we'll animate the whole tab
    302                 // (unfortunately, we need to have the text invisible during this transition as it
    303                 // would also be stretched)
    304                 float revScale = 1.0f/mTabWidthScreenWidthFraction;
    305                 mAboutTab.setScaleX(revScale);
    306                 mAboutTab.setPivotX(0.0f);
    307                 final ViewPropertyAnimator aboutAnimator = mAboutTab.animate();
    308                 aboutAnimator.setDuration(TRANSITION_TIME);
    309                 aboutAnimator.scaleX(1.0f);
    310 
    311                 // For y, we need to scale only the picture itself because we want it to be cropped
    312                 mPhotoView.setScaleY(revScale);
    313                 mPhotoView.setPivotY(photoHeight * 0.5f);
    314                 final ViewPropertyAnimator photoAnimator = mPhotoView.animate();
    315                 photoAnimator.setDuration(TRANSITION_TIME);
    316                 photoAnimator.scaleY(1.0f);
    317                 firstTransitionTime = TRANSITION_TIME;
    318             }
    319 
    320             // Animate in the labels after the above transition is finished
    321             mAboutTab.fadeInLabelViewAnimator(firstTransitionTime, true);
    322             mUpdatesTab.fadeInLabelViewAnimator(firstTransitionTime, false);
    323 
    324             final float pixelsToTranslate = (1.0f - mTabWidthScreenWidthFraction) * width;
    325             // Views to translate
    326             for (View view : new View[] { mUpdatesTab, mTabDivider }) {
    327                 view.setTranslationX(pixelsToTranslate);
    328                 final ViewPropertyAnimator translateAnimator = view.animate();
    329                 translateAnimator.translationX(0.0f);
    330                 translateAnimator.setDuration(TRANSITION_TIME);
    331             }
    332 
    333             // Another hack: If the picture is square, there is no shadow in "Without updates"
    334             //    --> fade it in after the translations are done
    335             if (squarePicture) {
    336                 mShadow.setAlpha(0.0f);
    337                 mShadow.animate().setStartDelay(TRANSITION_TIME).alpha(1.0f);
    338             }
    339         }
    340     }
    341 
    342     private void updateAlphaLayers() {
    343         float alpha = mLastScrollPosition * MAX_ALPHA / mAllowedHorizontalScrollLength;
    344         alpha = MoreMath.clamp(alpha, 0.0f, 1.0f);
    345         mAboutTab.setAlphaLayerValue(alpha);
    346         mUpdatesTab.setAlphaLayerValue(MAX_ALPHA - alpha);
    347     }
    348 
    349     @Override
    350     protected void onScrollChanged(int x, int y, int oldX, int oldY) {
    351         super.onScrollChanged(x, y, oldX, oldY);
    352 
    353         // Guard against framework issue where onScrollChanged() is called twice
    354         // for each touch-move event.  This wreaked havoc on the tab-carousel: the
    355         // view-pager moved twice as fast as it should because we called fakeDragBy()
    356         // twice with the same value.
    357         if (mLastScrollPosition == x) return;
    358 
    359         // Since we never completely scroll the about/updates tabs off-screen,
    360         // the draggable range is less than the width of the carousel. Our
    361         // listeners don't care about this... if we scroll 75% percent of our
    362         // draggable range, they want to scroll 75% of the entire carousel
    363         // width, not the same number of pixels that we scrolled.
    364         int scaledL = (int) (x * mScrollScaleFactor);
    365         int oldScaledL = (int) (oldX * mScrollScaleFactor);
    366         mListener.onScrollChanged(scaledL, y, oldScaledL, oldY);
    367 
    368         mLastScrollPosition = x;
    369         updateAlphaLayers();
    370     }
    371 
    372     /**
    373      * Reset the carousel to the start position (i.e. because new data will be loaded in for a
    374      * different contact).
    375      */
    376     public void reset() {
    377         scrollTo(0, 0);
    378         setCurrentTab(0);
    379         moveToYCoordinate(0, 0);
    380     }
    381 
    382     /**
    383      * Set the current tab that should be restored when the view is first laid out.
    384      */
    385     public void restoreCurrentTab(int position) {
    386         setCurrentTab(position);
    387         // It is only possible to scroll the view after onMeasure() has been called (where the
    388         // allowed horizontal scroll length is determined). Hence, set a flag that will be read
    389         // in onLayout() after the children and this view have finished being laid out.
    390         mScrollToCurrentTab = true;
    391     }
    392 
    393     /**
    394      * Restore the Y position of this view to the last manually requested value. This can be done
    395      * after the parent has been re-laid out again, where this view's position could have been
    396      * lost if the view laid outside its parent's bounds.
    397      */
    398     public void restoreYCoordinate() {
    399         setY(getStoredYCoordinateForTab(mCurrentTab));
    400     }
    401 
    402     /**
    403      * Request that the view move to the given Y coordinate. Also store the Y coordinate as the
    404      * last requested Y coordinate for the given tabIndex.
    405      */
    406     public void moveToYCoordinate(int tabIndex, float y) {
    407         setY(y);
    408         storeYCoordinate(tabIndex, y);
    409     }
    410 
    411     /**
    412      * Store this information as the last requested Y coordinate for the given tabIndex.
    413      */
    414     public void storeYCoordinate(int tabIndex, float y) {
    415         mYCoordinateArray[tabIndex] = y;
    416     }
    417 
    418     /**
    419      * Returns the stored Y coordinate of this view the last time the user was on the selected
    420      * tab given by tabIndex.
    421      */
    422     public float getStoredYCoordinateForTab(int tabIndex) {
    423         return mYCoordinateArray[tabIndex];
    424     }
    425 
    426     /**
    427      * Returns the number of pixels that this view can be scrolled horizontally.
    428      */
    429     public int getAllowedHorizontalScrollLength() {
    430         return mAllowedHorizontalScrollLength;
    431     }
    432 
    433     /**
    434      * Returns the number of pixels that this view can be scrolled vertically while still allowing
    435      * the tab labels to still show.
    436      */
    437     public int getAllowedVerticalScrollLength() {
    438         return mAllowedVerticalScrollLength;
    439     }
    440 
    441     /**
    442      * Updates the tab selection.
    443      */
    444     public void setCurrentTab(int position) {
    445         final CarouselTab selected, deselected;
    446 
    447         switch (position) {
    448             case TAB_INDEX_ABOUT:
    449                 selected = mAboutTab;
    450                 deselected = mUpdatesTab;
    451                 break;
    452             case TAB_INDEX_UPDATES:
    453                 selected = mUpdatesTab;
    454                 deselected = mAboutTab;
    455                 break;
    456             default:
    457                 throw new IllegalStateException("Invalid tab position " + position);
    458         }
    459         selected.showSelectedState();
    460         selected.setOverlayClickable(false);
    461         deselected.showDeselectedState();
    462         deselected.setOverlayClickable(true);
    463         mCurrentTab = position;
    464     }
    465 
    466     /**
    467      * Loads the data from the Loader-Result. This is the only function that has to be called
    468      * from the outside to fully setup the View
    469      */
    470     public void loadData(ContactLoader.Result contactData) {
    471         if (contactData == null) return;
    472 
    473         // TODO: Move this into the {@link CarouselTab} class when the updates
    474         // fragment code is more finalized.
    475         final boolean expandOnClick = contactData.getPhotoUri() != null;
    476         final OnClickListener listener = mPhotoSetter.setupContactPhotoForClick(
    477                 mContext, contactData, mPhotoView, expandOnClick);
    478 
    479         if (expandOnClick || contactData.isWritableContact(mContext)) {
    480             mPhotoViewOverlay.setOnClickListener(listener);
    481         } else {
    482             // Work around framework issue... if we instead use
    483             // setClickable(false), then we can't swipe horizontally.
    484             mPhotoViewOverlay.setOnClickListener(null);
    485         }
    486 
    487         ContactDetailDisplayUtils.setSocialSnippet(
    488                 mContext, contactData, mStatusView, mStatusPhotoView);
    489     }
    490 
    491     /**
    492      * Set the given {@link Listener} to handle carousel events.
    493      */
    494     public void setListener(Listener listener) {
    495         mListener = listener;
    496     }
    497 
    498     @Override
    499     public boolean onTouch(View v, MotionEvent event) {
    500         switch (event.getAction()) {
    501             case MotionEvent.ACTION_DOWN:
    502                 mListener.onTouchDown();
    503                 return true;
    504             case MotionEvent.ACTION_UP:
    505                 mListener.onTouchUp();
    506                 return true;
    507         }
    508         return super.onTouchEvent(event);
    509     }
    510 
    511     @Override
    512     public boolean onInterceptTouchEvent(MotionEvent ev) {
    513         boolean interceptTouch = super.onInterceptTouchEvent(ev);
    514         if (interceptTouch) {
    515             mListener.onTouchDown();
    516         }
    517         return interceptTouch;
    518     }
    519 }
    520