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