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.ContactLoader; 20 import com.android.contacts.R; 21 22 import android.content.Context; 23 import android.content.res.Resources; 24 import android.util.AttributeSet; 25 import android.view.MotionEvent; 26 import android.view.View; 27 import android.view.View.OnTouchListener; 28 import android.widget.HorizontalScrollView; 29 import android.widget.ImageView; 30 import android.widget.TextView; 31 32 /** 33 * This is a horizontally scrolling carousel with 2 tabs: one to see info about the contact and 34 * one to see updates from the contact. 35 */ 36 public class ContactDetailTabCarousel extends HorizontalScrollView implements OnTouchListener { 37 38 private static final String TAG = ContactDetailTabCarousel.class.getSimpleName(); 39 40 private static final int TAB_INDEX_ABOUT = 0; 41 private static final int TAB_INDEX_UPDATES = 1; 42 private static final int TAB_COUNT = 2; 43 44 /** Tab width as defined as a fraction of the screen width */ 45 private float mTabWidthScreenWidthFraction; 46 47 /** Tab height as defined as a fraction of the screen width */ 48 private float mTabHeightScreenWidthFraction; 49 50 private ImageView mPhotoView; 51 private TextView mStatusView; 52 private ImageView mStatusPhotoView; 53 54 private Listener mListener; 55 56 private int mCurrentTab = TAB_INDEX_ABOUT; 57 58 private CarouselTab mAboutTab; 59 private CarouselTab mUpdatesTab; 60 61 /** Last Y coordinate of the carousel when the tab at the given index was selected */ 62 private final float[] mYCoordinateArray = new float[TAB_COUNT]; 63 64 private int mTabDisplayLabelHeight; 65 66 private boolean mScrollToCurrentTab = false; 67 private int mLastScrollPosition; 68 69 private int mAllowedHorizontalScrollLength = Integer.MIN_VALUE; 70 private int mAllowedVerticalScrollLength = Integer.MIN_VALUE; 71 72 private static final float MAX_ALPHA = 0.5f; 73 74 /** 75 * Interface for callbacks invoked when the user interacts with the carousel. 76 */ 77 public interface Listener { 78 public void onTouchDown(); 79 public void onTouchUp(); 80 public void onScrollChanged(int l, int t, int oldl, int oldt); 81 public void onTabSelected(int position); 82 } 83 84 public ContactDetailTabCarousel(Context context, AttributeSet attrs) { 85 super(context, attrs); 86 87 setOnTouchListener(this); 88 89 Resources resources = mContext.getResources(); 90 mTabDisplayLabelHeight = resources.getDimensionPixelSize( 91 R.dimen.detail_tab_carousel_tab_label_height); 92 mTabWidthScreenWidthFraction = resources.getFraction( 93 R.fraction.tab_width_screen_width_percentage, 1, 1); 94 mTabHeightScreenWidthFraction = resources.getFraction( 95 R.fraction.tab_height_screen_width_percentage, 1, 1); 96 } 97 98 @Override 99 protected void onFinishInflate() { 100 super.onFinishInflate(); 101 mAboutTab = (CarouselTab) findViewById(R.id.tab_about); 102 mAboutTab.setLabel(mContext.getString(R.string.contactDetailAbout)); 103 104 mUpdatesTab = (CarouselTab) findViewById(R.id.tab_update); 105 mUpdatesTab.setLabel(mContext.getString(R.string.contactDetailUpdates)); 106 107 mAboutTab.enableTouchInterceptor(mAboutTabTouchInterceptListener); 108 mUpdatesTab.enableTouchInterceptor(mUpdatesTabTouchInterceptListener); 109 110 // Retrieve the photo view for the "about" tab 111 mPhotoView = (ImageView) mAboutTab.findViewById(R.id.photo); 112 113 // Retrieve the social update views for the "updates" tab 114 mStatusView = (TextView) mUpdatesTab.findViewById(R.id.status); 115 mStatusPhotoView = (ImageView) mUpdatesTab.findViewById(R.id.status_photo); 116 } 117 118 @Override 119 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 120 int screenWidth = MeasureSpec.getSize(widthMeasureSpec); 121 // Compute the width of a tab as a fraction of the screen width 122 int tabWidth = (int) (mTabWidthScreenWidthFraction * screenWidth); 123 124 // Find the allowed scrolling length by subtracting the current visible screen width 125 // from the total length of the tabs. 126 mAllowedHorizontalScrollLength = tabWidth * TAB_COUNT - screenWidth; 127 128 int tabHeight = (int) (screenWidth * mTabHeightScreenWidthFraction); 129 // Set the child {@link LinearLayout} to be TAB_COUNT * the computed tab width so that the 130 // {@link LinearLayout}'s children (which are the tabs) will evenly split that width. 131 if (getChildCount() > 0) { 132 View child = getChildAt(0); 133 child.measure(MeasureSpec.makeMeasureSpec(TAB_COUNT * tabWidth, MeasureSpec.EXACTLY), 134 MeasureSpec.makeMeasureSpec(tabHeight, MeasureSpec.EXACTLY)); 135 } 136 137 mAllowedVerticalScrollLength = tabHeight - mTabDisplayLabelHeight; 138 setMeasuredDimension( 139 resolveSize(screenWidth, widthMeasureSpec), 140 resolveSize(tabHeight, heightMeasureSpec)); 141 } 142 143 @Override 144 protected void onLayout(boolean changed, int l, int t, int r, int b) { 145 super.onLayout(changed, l, t, r, b); 146 if (mScrollToCurrentTab) { 147 mScrollToCurrentTab = false; 148 scrollTo(mCurrentTab == TAB_INDEX_ABOUT ? 0 : mAllowedHorizontalScrollLength, 0); 149 updateAlphaLayers(); 150 } 151 } 152 153 private final OnClickListener mAboutTabTouchInterceptListener = new OnClickListener() { 154 @Override 155 public void onClick(View v) { 156 mListener.onTabSelected(TAB_INDEX_ABOUT); 157 } 158 }; 159 160 private final OnClickListener mUpdatesTabTouchInterceptListener = new OnClickListener() { 161 @Override 162 public void onClick(View v) { 163 mListener.onTabSelected(TAB_INDEX_UPDATES); 164 } 165 }; 166 167 private void updateAlphaLayers() { 168 mAboutTab.setAlphaLayerValue(mLastScrollPosition * MAX_ALPHA / 169 mAllowedHorizontalScrollLength); 170 mUpdatesTab.setAlphaLayerValue(MAX_ALPHA - mLastScrollPosition * MAX_ALPHA / 171 mAllowedHorizontalScrollLength); 172 } 173 174 @Override 175 protected void onScrollChanged(int l, int t, int oldl, int oldt) { 176 super.onScrollChanged(l, t, oldl, oldt); 177 mListener.onScrollChanged(l, t, oldl, oldt); 178 mLastScrollPosition = l; 179 updateAlphaLayers(); 180 } 181 182 /** 183 * Reset the carousel to the start position (i.e. because new data will be loaded in for a 184 * different contact). 185 */ 186 public void reset() { 187 scrollTo(0, 0); 188 setCurrentTab(0); 189 moveToYCoordinate(0, 0); 190 } 191 192 /** 193 * Set the current tab that should be restored when the view is first laid out. 194 */ 195 public void restoreCurrentTab(int position) { 196 setCurrentTab(position); 197 // It is only possible to scroll the view after onMeasure() has been called (where the 198 // allowed horizontal scroll length is determined). Hence, set a flag that will be read 199 // in onLayout() after the children and this view have finished being laid out. 200 mScrollToCurrentTab = true; 201 } 202 203 /** 204 * Restore the Y position of this view to the last manually requested value. This can be done 205 * after the parent has been re-laid out again, where this view's position could have been 206 * lost if the view laid outside its parent's bounds. 207 */ 208 public void restoreYCoordinate() { 209 setY(getStoredYCoordinateForTab(mCurrentTab)); 210 } 211 212 /** 213 * Request that the view move to the given Y coordinate. Also store the Y coordinate as the 214 * last requested Y coordinate for the given tabIndex. 215 */ 216 public void moveToYCoordinate(int tabIndex, float y) { 217 setY(y); 218 storeYCoordinate(tabIndex, y); 219 } 220 221 /** 222 * Store this information as the last requested Y coordinate for the given tabIndex. 223 */ 224 public void storeYCoordinate(int tabIndex, float y) { 225 mYCoordinateArray[tabIndex] = y; 226 } 227 228 /** 229 * Returns the stored Y coordinate of this view the last time the user was on the selected 230 * tab given by tabIndex. 231 */ 232 public float getStoredYCoordinateForTab(int tabIndex) { 233 return mYCoordinateArray[tabIndex]; 234 } 235 236 /** 237 * Returns the number of pixels that this view can be scrolled horizontally. 238 */ 239 public int getAllowedHorizontalScrollLength() { 240 return mAllowedHorizontalScrollLength; 241 } 242 243 /** 244 * Returns the number of pixels that this view can be scrolled vertically while still allowing 245 * the tab labels to still show. 246 */ 247 public int getAllowedVerticalScrollLength() { 248 return mAllowedVerticalScrollLength; 249 } 250 251 /** 252 * Updates the tab selection. 253 */ 254 public void setCurrentTab(int position) { 255 switch (position) { 256 case TAB_INDEX_ABOUT: 257 mAboutTab.showSelectedState(); 258 mUpdatesTab.showDeselectedState(); 259 break; 260 case TAB_INDEX_UPDATES: 261 mUpdatesTab.showSelectedState(); 262 mAboutTab.showDeselectedState(); 263 break; 264 default: 265 throw new IllegalStateException("Invalid tab position " + position); 266 } 267 mCurrentTab = position; 268 } 269 270 /** 271 * Loads the data from the Loader-Result. This is the only function that has to be called 272 * from the outside to fully setup the View 273 */ 274 public void loadData(ContactLoader.Result contactData) { 275 if (contactData == null) { 276 return; 277 } 278 279 // TODO: Move this into the {@link CarouselTab} class when the updates fragment code is more 280 // finalized 281 ContactDetailDisplayUtils.setPhoto(mContext, contactData, mPhotoView); 282 ContactDetailDisplayUtils.setSocialSnippet(mContext, contactData, mStatusView, 283 mStatusPhotoView); 284 } 285 286 /** 287 * Set the given {@link Listener} to handle carousel events. 288 */ 289 public void setListener(Listener listener) { 290 mListener = listener; 291 } 292 293 @Override 294 public boolean onTouch(View v, MotionEvent event) { 295 switch (event.getAction()) { 296 case MotionEvent.ACTION_DOWN: 297 mListener.onTouchDown(); 298 return true; 299 case MotionEvent.ACTION_UP: 300 mListener.onTouchUp(); 301 return true; 302 } 303 return super.onTouchEvent(event); 304 } 305 306 @Override 307 public boolean onInterceptTouchEvent(MotionEvent ev) { 308 boolean interceptTouch = super.onInterceptTouchEvent(ev); 309 if (interceptTouch) { 310 mListener.onTouchDown(); 311 } 312 return interceptTouch; 313 } 314 } 315