• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
onTouchDown()99         public void onTouchDown();
onTouchUp()100         public void onTouchUp();
101 
onScrollChanged(int l, int t, int oldl, int oldt)102         public void onScrollChanged(int l, int t, int oldl, int oldt);
onTabSelected(int position)103         public void onTabSelected(int position);
104     }
105 
ContactDetailTabCarousel(Context context, AttributeSet attrs)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
onFinishInflate()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
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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
onLayout(boolean changed, int l, int t, int r, int b)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 
TabClickListener(int tab)226         public TabClickListener(int tab) {
227             super();
228             mTab = tab;
229         }
230 
231         @Override
onClick(View v)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      */
animateAppear(int width, int scrollOffset)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 
updateAlphaLayers()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
onScrollChanged(int x, int y, int oldX, int oldY)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      */
reset()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      */
restoreCurrentTab(int position)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      */
restoreYCoordinate()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      */
moveToYCoordinate(int tabIndex, float y)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      */
storeYCoordinate(int tabIndex, float y)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      */
getStoredYCoordinateForTab(int tabIndex)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      */
getAllowedHorizontalScrollLength()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      */
getAllowedVerticalScrollLength()437     public int getAllowedVerticalScrollLength() {
438         return mAllowedVerticalScrollLength;
439     }
440 
441     /**
442      * Updates the tab selection.
443      */
setCurrentTab(int position)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      */
loadData(ContactLoader.Result contactData)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      */
setListener(Listener listener)494     public void setListener(Listener listener) {
495         mListener = listener;
496     }
497 
498     @Override
onTouch(View v, MotionEvent event)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
onInterceptTouchEvent(MotionEvent ev)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