/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.contacts.detail;
import com.android.contacts.R;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.widget.HorizontalScrollView;
/**
* This is a horizontally scrolling carousel with 2 fragments: one to see info about the contact and
* one to see updates from the contact. Depending on the scroll position and user selection of which
* fragment to currently view, the alpha values and touch interceptors over each fragment are
* configured accordingly.
*/
public class ContactDetailFragmentCarousel extends HorizontalScrollView implements OnTouchListener {
private static final String TAG = ContactDetailFragmentCarousel.class.getSimpleName();
/**
* Number of pixels that this view can be scrolled horizontally.
*/
private int mAllowedHorizontalScrollLength = Integer.MIN_VALUE;
/**
* Minimum X scroll position that must be surpassed (if the user is on the "about" page of the
* contact card), in order for this view to automatically snap to the "updates" page.
*/
private int mLowerThreshold = Integer.MIN_VALUE;
/**
* Maximum X scroll position (if the user is on the "updates" page of the contact card), below
* which this view will automatically snap to the "about" page.
*/
private int mUpperThreshold = Integer.MIN_VALUE;
/**
* Minimum width of a fragment (if there is more than 1 fragment in the carousel, then this is
* the width of one of the fragments).
*/
private int mMinFragmentWidth = Integer.MIN_VALUE;
/**
* Maximum alpha value of the overlay on the fragment that is not currently selected
* (if there are 1+ fragments in the carousel).
*/
private static final float MAX_ALPHA = 0.5f;
/**
* Fragment width (if there are 1+ fragments in the carousel) as defined as a fraction of the
* screen width.
*/
private static final float FRAGMENT_WIDTH_SCREEN_WIDTH_FRACTION = 0.85f;
private static final int ABOUT_PAGE = 0;
private static final int UPDATES_PAGE = 1;
private static final int MAX_FRAGMENT_VIEW_COUNT = 2;
private boolean mEnableSwipe;
private int mCurrentPage = ABOUT_PAGE;
private int mLastScrollPosition;
private ViewOverlay mAboutFragment;
private ViewOverlay mUpdatesFragment;
private View mDetailFragmentView;
private View mUpdatesFragmentView;
public ContactDetailFragmentCarousel(Context context) {
this(context, null);
}
public ContactDetailFragmentCarousel(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ContactDetailFragmentCarousel(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
final LayoutInflater inflater =
(LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.contact_detail_fragment_carousel, this);
setOnTouchListener(this);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int screenWidth = MeasureSpec.getSize(widthMeasureSpec);
int screenHeight = MeasureSpec.getSize(heightMeasureSpec);
// Take the width of this view as the width of the screen and compute necessary thresholds.
// Only do this computation 1x.
if (mAllowedHorizontalScrollLength == Integer.MIN_VALUE) {
mMinFragmentWidth = (int) (FRAGMENT_WIDTH_SCREEN_WIDTH_FRACTION * screenWidth);
mAllowedHorizontalScrollLength = (MAX_FRAGMENT_VIEW_COUNT * mMinFragmentWidth) -
screenWidth;
mLowerThreshold = (screenWidth - mMinFragmentWidth) / MAX_FRAGMENT_VIEW_COUNT;
mUpperThreshold = mAllowedHorizontalScrollLength - mLowerThreshold;
}
if (getChildCount() > 0) {
View child = getChildAt(0);
// If we enable swipe, then the {@link LinearLayout} child width must be the sum of the
// width of all its children fragments.
if (mEnableSwipe) {
child.measure(MeasureSpec.makeMeasureSpec(
mMinFragmentWidth * MAX_FRAGMENT_VIEW_COUNT, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(screenHeight, MeasureSpec.EXACTLY));
} else {
// Otherwise, the {@link LinearLayout} child width will just be the screen width
// because it will only have 1 child fragment.
child.measure(MeasureSpec.makeMeasureSpec(screenWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(screenHeight, MeasureSpec.EXACTLY));
}
}
setMeasuredDimension(
resolveSize(screenWidth, widthMeasureSpec),
resolveSize(screenHeight, heightMeasureSpec));
}
/**
* Set the current page. This dims out the non-selected page but doesn't do any scrolling of
* the carousel.
*/
public void setCurrentPage(int pageIndex) {
mCurrentPage = pageIndex;
if (mAboutFragment != null && mUpdatesFragment != null) {
mAboutFragment.setAlphaLayerValue(mCurrentPage == ABOUT_PAGE ? 0 : MAX_ALPHA);
mUpdatesFragment.setAlphaLayerValue(mCurrentPage == UPDATES_PAGE ? 0 : MAX_ALPHA);
}
}
/**
* Set the view containers for the detail and updates fragment.
*/
public void setFragmentViews(View detailFragmentView, View updatesFragmentView) {
mDetailFragmentView = detailFragmentView;
mUpdatesFragmentView = updatesFragmentView;
}
/**
* Set the detail and updates fragment.
*/
public void setFragments(ViewOverlay aboutFragment, ViewOverlay updatesFragment) {
mAboutFragment = aboutFragment;
mUpdatesFragment = updatesFragment;
}
/**
* Enable swiping if the detail and update fragments should be showing. Otherwise disable
* swiping if only the detail fragment should be showing.
*/
public void enableSwipe(boolean enable) {
if (mEnableSwipe != enable) {
mEnableSwipe = enable;
if (mUpdatesFragmentView != null) {
mUpdatesFragmentView.setVisibility(enable ? View.VISIBLE : View.GONE);
if (mCurrentPage == ABOUT_PAGE) {
mDetailFragmentView.requestFocus();
} else {
mUpdatesFragmentView.requestFocus();
}
updateTouchInterceptors();
}
}
}
public int getCurrentPage() {
return mCurrentPage;
}
private final OnClickListener mAboutFragTouchInterceptListener = new OnClickListener() {
@Override
public void onClick(View v) {
mCurrentPage = ABOUT_PAGE;
snapToEdge();
}
};
private final OnClickListener mUpdatesFragTouchInterceptListener = new OnClickListener() {
@Override
public void onClick(View v) {
mCurrentPage = UPDATES_PAGE;
snapToEdge();
}
};
private void updateTouchInterceptors() {
switch (mCurrentPage) {
case ABOUT_PAGE:
// The "about this contact" page has been selected, so disable the touch interceptor
// on this page and enable it for the "updates" page.
mAboutFragment.disableTouchInterceptor();
mUpdatesFragment.enableTouchInterceptor(mUpdatesFragTouchInterceptListener);
break;
case UPDATES_PAGE:
mUpdatesFragment.disableTouchInterceptor();
mAboutFragment.enableTouchInterceptor(mAboutFragTouchInterceptListener);
break;
}
}
private void updateAlphaLayers() {
mAboutFragment.setAlphaLayerValue(mLastScrollPosition * MAX_ALPHA /
mAllowedHorizontalScrollLength);
mUpdatesFragment.setAlphaLayerValue(MAX_ALPHA - mLastScrollPosition * MAX_ALPHA /
mAllowedHorizontalScrollLength);
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if (!mEnableSwipe) {
return;
}
mLastScrollPosition= l;
updateAlphaLayers();
}
private void snapToEdge() {
switch (mCurrentPage) {
case ABOUT_PAGE:
smoothScrollTo(0, 0);
break;
case UPDATES_PAGE:
smoothScrollTo(mAllowedHorizontalScrollLength, 0);
break;
}
updateTouchInterceptors();
}
/**
* Returns the desired page we should scroll to based on the current X scroll position and the
* current page.
*/
private int getDesiredPage() {
switch (mCurrentPage) {
case ABOUT_PAGE:
// If the user is on the "about" page, and the scroll position exceeds the lower
// threshold, then we should switch to the "updates" page.
return (mLastScrollPosition > mLowerThreshold) ? UPDATES_PAGE : ABOUT_PAGE;
case UPDATES_PAGE:
// If the user is on the "updates" page, and the scroll position goes below the
// upper threshold, then we should switch to the "about" page.
return (mLastScrollPosition < mUpperThreshold) ? ABOUT_PAGE : UPDATES_PAGE;
}
throw new IllegalStateException("Invalid current page " + mCurrentPage);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
if (!mEnableSwipe) {
return false;
}
if (event.getAction() == MotionEvent.ACTION_UP) {
mCurrentPage = getDesiredPage();
snapToEdge();
return true;
}
return false;
}
}