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