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 android.content.Context;
20import android.util.AttributeSet;
21import android.view.LayoutInflater;
22import android.view.MotionEvent;
23import android.view.View;
24import android.view.View.OnTouchListener;
25import android.view.ViewPropertyAnimator;
26import android.widget.HorizontalScrollView;
27
28import com.android.contacts.R;
29import 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 */
36public 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