ContactDetailLayoutController.java revision 4b25da79091157935042d2942a8961ceba92166f
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 com.android.contacts.ContactLoader; 20import com.android.contacts.R; 21import com.android.contacts.activities.ContactDetailActivity.FragmentKeyListener; 22 23import android.animation.ObjectAnimator; 24import android.app.FragmentManager; 25import android.app.FragmentTransaction; 26import android.content.Context; 27import android.os.Bundle; 28import android.support.v4.view.ViewPager; 29import android.support.v4.view.ViewPager.OnPageChangeListener; 30import android.view.LayoutInflater; 31import android.view.View; 32import android.view.animation.AnimationUtils; 33import android.widget.AbsListView; 34import android.widget.AbsListView.OnScrollListener; 35 36/** 37 * Determines the layout of the contact card. 38 */ 39public class ContactDetailLayoutController { 40 41 private static final String KEY_CONTACT_HAS_UPDATES = "contactHasUpdates"; 42 private static final String KEY_CURRENT_PAGE_INDEX = "currentPageIndex"; 43 44 private static final int TAB_INDEX_DETAIL = 0; 45 private static final int TAB_INDEX_UPDATES = 1; 46 47 /** 48 * There are 3 possible layouts for the contact detail screen: 49 * 1. TWO_COLUMN - Tall and wide screen so the 2 pages can be shown side-by-side 50 * 2. VIEW_PAGER_AND_TAB_CAROUSEL - Tall and narrow screen to allow swipe between the 2 pages 51 * 3. FRAGMENT_CAROUSEL- Short and wide screen to allow half of the other page to show at a time 52 */ 53 private enum LayoutMode { 54 TWO_COLUMN, VIEW_PAGER_AND_TAB_CAROUSEL, FRAGMENT_CAROUSEL, 55 } 56 57 private final Context mContext; 58 private final LayoutInflater mLayoutInflater; 59 private final FragmentManager mFragmentManager; 60 61 private ContactDetailFragment mDetailFragment; 62 private ContactDetailUpdatesFragment mUpdatesFragment; 63 64 private View mDetailFragmentView; 65 private View mUpdatesFragmentView; 66 67 private final ViewPager mViewPager; 68 private ContactDetailViewPagerAdapter mViewPagerAdapter; 69 private int mViewPagerState; 70 71 private final ContactDetailTabCarousel mTabCarousel; 72 private final ContactDetailFragmentCarousel mFragmentCarousel; 73 74 private ContactDetailFragment.Listener mContactDetailFragmentListener; 75 76 private ContactLoader.Result mContactData; 77 78 private boolean mContactHasUpdates; 79 80 private LayoutMode mLayoutMode; 81 82 public ContactDetailLayoutController(Context context, Bundle savedState, 83 FragmentManager fragmentManager, View viewContainer, ContactDetailFragment.Listener 84 contactDetailFragmentListener) { 85 86 if (fragmentManager == null) { 87 throw new IllegalStateException("Cannot initialize a ContactDetailLayoutController " 88 + "without a non-null FragmentManager"); 89 } 90 91 mContext = context; 92 mLayoutInflater = (LayoutInflater) context.getSystemService( 93 Context.LAYOUT_INFLATER_SERVICE); 94 mFragmentManager = fragmentManager; 95 mContactDetailFragmentListener = contactDetailFragmentListener; 96 97 // Retrieve views in case this is view pager and carousel mode 98 mViewPager = (ViewPager) viewContainer.findViewById(R.id.pager); 99 mTabCarousel = (ContactDetailTabCarousel) viewContainer.findViewById(R.id.tab_carousel); 100 101 // Retrieve view in case this is in fragment carousel mode 102 mFragmentCarousel = (ContactDetailFragmentCarousel) viewContainer.findViewById( 103 R.id.fragment_carousel); 104 105 // Retrieve container views in case they are already in the XML layout 106 mDetailFragmentView = viewContainer.findViewById(R.id.about_fragment_container); 107 mUpdatesFragmentView = viewContainer.findViewById(R.id.updates_fragment_container); 108 109 // Determine the layout mode based on the presence of certain views in the layout XML. 110 if (mViewPager != null) { 111 mLayoutMode = LayoutMode.VIEW_PAGER_AND_TAB_CAROUSEL; 112 } else { 113 mLayoutMode = (mFragmentCarousel != null) ? LayoutMode.FRAGMENT_CAROUSEL : 114 LayoutMode.TWO_COLUMN; 115 } 116 117 initialize(savedState); 118 } 119 120 private void initialize(Bundle savedState) { 121 boolean fragmentsAddedToFragmentManager = true; 122 mDetailFragment = (ContactDetailFragment) mFragmentManager.findFragmentByTag( 123 ContactDetailViewPagerAdapter.ABOUT_FRAGMENT_TAG); 124 mUpdatesFragment = (ContactDetailUpdatesFragment) mFragmentManager.findFragmentByTag( 125 ContactDetailViewPagerAdapter.UPDTES_FRAGMENT_TAG); 126 127 // If the detail fragment was found in the {@link FragmentManager} then we don't need to add 128 // it again. Otherwise, create the fragments dynamically and remember to add them to the 129 // {@link FragmentManager}. 130 if (mDetailFragment == null) { 131 mDetailFragment = new ContactDetailFragment(); 132 mUpdatesFragment = new ContactDetailUpdatesFragment(); 133 fragmentsAddedToFragmentManager = false; 134 } 135 136 mDetailFragment.setListener(mContactDetailFragmentListener); 137 138 // Read from savedState if possible 139 int currentPageIndex = 0; 140 if (savedState != null) { 141 mContactHasUpdates = savedState.getBoolean(KEY_CONTACT_HAS_UPDATES); 142 currentPageIndex = savedState.getInt(KEY_CURRENT_PAGE_INDEX, 0); 143 } 144 145 switch (mLayoutMode) { 146 case VIEW_PAGER_AND_TAB_CAROUSEL: { 147 // Inflate 2 view containers to pass in as children to the {@link ViewPager}, 148 // which will in turn be the parents to the mDetailFragment and mUpdatesFragment 149 // since the fragments must have the same parent view IDs in both landscape and 150 // portrait layouts. 151 mDetailFragmentView = mLayoutInflater.inflate( 152 R.layout.contact_detail_about_fragment_container, mViewPager, false); 153 mUpdatesFragmentView = mLayoutInflater.inflate( 154 R.layout.contact_detail_updates_fragment_container, mViewPager, false); 155 156 mViewPagerAdapter = new ContactDetailViewPagerAdapter(); 157 mViewPagerAdapter.setAboutFragmentView(mDetailFragmentView); 158 mViewPagerAdapter.setUpdatesFragmentView(mUpdatesFragmentView); 159 160 mViewPager.addView(mDetailFragmentView); 161 mViewPager.addView(mUpdatesFragmentView); 162 mViewPager.setAdapter(mViewPagerAdapter); 163 mViewPager.setOnPageChangeListener(mOnPageChangeListener); 164 165 if (!fragmentsAddedToFragmentManager) { 166 FragmentTransaction transaction = mFragmentManager.beginTransaction(); 167 transaction.add(R.id.about_fragment_container, mDetailFragment, 168 ContactDetailViewPagerAdapter.ABOUT_FRAGMENT_TAG); 169 transaction.add(R.id.updates_fragment_container, mUpdatesFragment, 170 ContactDetailViewPagerAdapter.UPDTES_FRAGMENT_TAG); 171 transaction.commitAllowingStateLoss(); 172 mFragmentManager.executePendingTransactions(); 173 } 174 175 mTabCarousel.setListener(mTabCarouselListener); 176 mDetailFragment.setVerticalScrollListener( 177 new VerticalScrollListener(TAB_INDEX_DETAIL)); 178 mUpdatesFragment.setVerticalScrollListener( 179 new VerticalScrollListener(TAB_INDEX_UPDATES)); 180 mViewPager.setCurrentItem(currentPageIndex); 181 break; 182 } 183 case TWO_COLUMN: { 184 if (!fragmentsAddedToFragmentManager) { 185 FragmentTransaction transaction = mFragmentManager.beginTransaction(); 186 transaction.add(R.id.about_fragment_container, mDetailFragment, 187 ContactDetailViewPagerAdapter.ABOUT_FRAGMENT_TAG); 188 transaction.add(R.id.updates_fragment_container, mUpdatesFragment, 189 ContactDetailViewPagerAdapter.UPDTES_FRAGMENT_TAG); 190 transaction.commitAllowingStateLoss(); 191 mFragmentManager.executePendingTransactions(); 192 } 193 break; 194 } 195 case FRAGMENT_CAROUSEL: { 196 // Add the fragments to the fragment containers in the carousel using a 197 // {@link FragmentTransaction} if they haven't already been added to the 198 // {@link FragmentManager}. 199 if (!fragmentsAddedToFragmentManager) { 200 FragmentTransaction transaction = mFragmentManager.beginTransaction(); 201 transaction.add(R.id.about_fragment_container, mDetailFragment, 202 ContactDetailViewPagerAdapter.ABOUT_FRAGMENT_TAG); 203 transaction.add(R.id.updates_fragment_container, mUpdatesFragment, 204 ContactDetailViewPagerAdapter.UPDTES_FRAGMENT_TAG); 205 transaction.commitAllowingStateLoss(); 206 mFragmentManager.executePendingTransactions(); 207 } 208 209 mFragmentCarousel.setFragmentViews(mDetailFragmentView, mUpdatesFragmentView); 210 mFragmentCarousel.setFragments(mDetailFragment, mUpdatesFragment); 211 mFragmentCarousel.setCurrentPage(currentPageIndex); 212 break; 213 } 214 } 215 216 // Setup the layout if we already have a saved state 217 if (savedState != null) { 218 if (mContactHasUpdates) { 219 showContactWithUpdates(); 220 } else { 221 showContactWithoutUpdates(); 222 } 223 } 224 } 225 226 public void setContactData(ContactLoader.Result data) { 227 mContactData = data; 228 mContactHasUpdates = !data.getStreamItems().isEmpty(); 229 if (mContactHasUpdates) { 230 showContactWithUpdates(); 231 } else { 232 showContactWithoutUpdates(); 233 } 234 } 235 236 public void showEmptyState() { 237 switch (mLayoutMode) { 238 case FRAGMENT_CAROUSEL: { 239 mFragmentCarousel.enableSwipe(false); 240 mDetailFragment.showEmptyState(); 241 break; 242 } 243 case TWO_COLUMN: { 244 mDetailFragment.setShowStaticPhoto(false); 245 mUpdatesFragmentView.setVisibility(View.GONE); 246 mDetailFragment.showEmptyState(); 247 break; 248 } 249 case VIEW_PAGER_AND_TAB_CAROUSEL: { 250 mDetailFragment.setShowStaticPhoto(false); 251 mDetailFragment.showEmptyState(); 252 mTabCarousel.loadData(null); 253 mTabCarousel.setVisibility(View.GONE); 254 mViewPagerAdapter.enableSwipe(false); 255 mViewPager.setCurrentItem(0); 256 break; 257 } 258 default: 259 throw new IllegalStateException("Invalid LayoutMode " + mLayoutMode); 260 } 261 } 262 263 /** 264 * Setup the layout for the contact with updates. Pass in the index of the current page to 265 * select or null if the current selection should be left as is. 266 */ 267 private void showContactWithUpdates() { 268 if (mContactData == null) { 269 return; 270 } 271 switch (mLayoutMode) { 272 case TWO_COLUMN: { 273 // Set the contact data (hide the static photo because the photo will already be in 274 // the header that scrolls with contact details). 275 mDetailFragment.setShowStaticPhoto(false); 276 // Show the updates fragment 277 mUpdatesFragmentView.setVisibility(View.VISIBLE); 278 break; 279 } 280 case VIEW_PAGER_AND_TAB_CAROUSEL: { 281 // Update and show the tab carousel (also restore its last saved position) 282 mTabCarousel.loadData(mContactData); 283 mTabCarousel.restoreYCoordinate(); 284 mTabCarousel.setVisibility(View.VISIBLE); 285 // Update ViewPager to allow swipe between all the fragments (to see updates) 286 mViewPagerAdapter.enableSwipe(true); 287 break; 288 } 289 case FRAGMENT_CAROUSEL: { 290 // Allow swiping between all fragments 291 mFragmentCarousel.enableSwipe(true); 292 break; 293 } 294 default: 295 throw new IllegalStateException("Invalid LayoutMode " + mLayoutMode); 296 } 297 298 mDetailFragment.setData(mContactData.getLookupUri(), mContactData); 299 mUpdatesFragment.setData(mContactData.getLookupUri(), mContactData); 300 } 301 302 private void showContactWithoutUpdates() { 303 if (mContactData == null) { 304 return; 305 } 306 switch (mLayoutMode) { 307 case TWO_COLUMN: 308 // Show the static photo which is next to the list of scrolling contact details 309 mDetailFragment.setShowStaticPhoto(true); 310 // Hide the updates fragment 311 mUpdatesFragmentView.setVisibility(View.GONE); 312 break; 313 case VIEW_PAGER_AND_TAB_CAROUSEL: 314 // Hide the tab carousel 315 mTabCarousel.setVisibility(View.GONE); 316 // Update ViewPager to disable swipe so that it only shows the detail fragment 317 // and switch to the detail fragment 318 mViewPagerAdapter.enableSwipe(false); 319 mViewPager.setCurrentItem(0); 320 break; 321 case FRAGMENT_CAROUSEL: { 322 // Disable swipe so only the detail fragment shows 323 mFragmentCarousel.enableSwipe(false); 324 break; 325 } 326 default: 327 throw new IllegalStateException("Invalid LayoutMode " + mLayoutMode); 328 } 329 330 mDetailFragment.setData(mContactData.getLookupUri(), mContactData); 331 } 332 333 public FragmentKeyListener getCurrentPage() { 334 switch (getCurrentPageIndex()) { 335 case 0: 336 return mDetailFragment; 337 case 1: 338 return mUpdatesFragment; 339 default: 340 throw new IllegalStateException("Invalid current item for ViewPager"); 341 } 342 } 343 344 private int getCurrentPageIndex() { 345 // If the contact has social updates, then retrieve the current page based on the 346 // {@link ViewPager} or fragment carousel. 347 if (mContactHasUpdates) { 348 if (mViewPager != null) { 349 return mViewPager.getCurrentItem(); 350 } else if (mFragmentCarousel != null) { 351 return mFragmentCarousel.getCurrentPage(); 352 } 353 } 354 // Otherwise return the default page (detail fragment). 355 return 0; 356 } 357 358 public void onSaveInstanceState(Bundle outState) { 359 outState.putBoolean(KEY_CONTACT_HAS_UPDATES, mContactHasUpdates); 360 outState.putInt(KEY_CURRENT_PAGE_INDEX, getCurrentPageIndex()); 361 } 362 363 private final OnPageChangeListener mOnPageChangeListener = new OnPageChangeListener() { 364 365 private ObjectAnimator mTabCarouselAnimator; 366 367 @Override 368 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 369 // The user is horizontally dragging the {@link ViewPager}, so send 370 // these scroll changes to the tab carousel. Ignore these events though if the carousel 371 // is actually controlling the {@link ViewPager} scrolls because it will already be 372 // in the correct position. 373 if (mViewPager.isFakeDragging()) { 374 return; 375 } 376 int x = (int) ((position + positionOffset) * 377 mTabCarousel.getAllowedHorizontalScrollLength()); 378 mTabCarousel.scrollTo(x, 0); 379 } 380 381 @Override 382 public void onPageSelected(int position) { 383 // Since the {@link ViewPager} has committed to a new page now (but may not have 384 // finished scrolling yet), update the tab selection in the carousel. 385 mTabCarousel.setCurrentTab(position); 386 } 387 388 @Override 389 public void onPageScrollStateChanged(int state) { 390 if (mViewPagerState == ViewPager.SCROLL_STATE_IDLE) { 391 392 // If we are leaving the IDLE state, we are starting a swipe. 393 // First cancel any pending animations on the tab carousel. 394 cancelTabCarouselAnimator(); 395 396 // Sync the two lists because the list on the other page will start to show as 397 // we swipe over more. 398 syncScrollStateBetweenLists(mViewPager.getCurrentItem()); 399 400 } else if (state == ViewPager.SCROLL_STATE_IDLE) { 401 402 // Otherwise if the {@link ViewPager} is idle now, a page has been selected and 403 // scrolled into place. Perform an animation of the tab carousel is needed. 404 int currentPageIndex = mViewPager.getCurrentItem(); 405 int tabCarouselOffset = (int) mTabCarousel.getY(); 406 boolean shouldAnimateTabCarousel; 407 408 // Find the offset position of the first item in the list of the current page. 409 int listOffset = getOffsetOfFirstItemInList(currentPageIndex); 410 411 // If the list was able to successfully offset by the tab carousel amount, then 412 // log this as the new Y coordinate for that page, and no animation is needed. 413 if (listOffset == tabCarouselOffset) { 414 mTabCarousel.storeYCoordinate(currentPageIndex, tabCarouselOffset); 415 shouldAnimateTabCarousel = false; 416 } else if (listOffset == Integer.MIN_VALUE) { 417 // If the offset of the first item in the list is unknown (i.e. the item 418 // is no longer visible on screen) then just animate the tab carousel to the 419 // previously logged position. 420 shouldAnimateTabCarousel = true; 421 } else if (Math.abs(listOffset) < Math.abs(tabCarouselOffset)) { 422 // If the list could not offset the full amount of the tab carousel offset (i.e. 423 // the list can only be scrolled a tiny amount), then animate the carousel down 424 // to compensate. 425 mTabCarousel.storeYCoordinate(currentPageIndex, listOffset); 426 shouldAnimateTabCarousel = true; 427 } else { 428 // By default, animate back to the Y coordinate of the tab carousel the last 429 // time the other page was selected. 430 shouldAnimateTabCarousel = true; 431 } 432 433 if (shouldAnimateTabCarousel) { 434 float desiredOffset = mTabCarousel.getStoredYCoordinateForTab(currentPageIndex); 435 if (desiredOffset != tabCarouselOffset) { 436 createTabCarouselAnimator(desiredOffset); 437 mTabCarouselAnimator.start(); 438 } 439 } 440 } 441 mViewPagerState = state; 442 } 443 444 private void createTabCarouselAnimator(float desiredValue) { 445 mTabCarouselAnimator = ObjectAnimator.ofFloat( 446 mTabCarousel, "y", desiredValue).setDuration(75); 447 mTabCarouselAnimator.setInterpolator(AnimationUtils.loadInterpolator( 448 mContext, android.R.anim.accelerate_decelerate_interpolator)); 449 } 450 451 private void cancelTabCarouselAnimator() { 452 if (mTabCarouselAnimator != null) { 453 mTabCarouselAnimator.cancel(); 454 mTabCarouselAnimator = null; 455 } 456 } 457 }; 458 459 private void syncScrollStateBetweenLists(int currentPageIndex) { 460 // Since the user interacted with the currently visible page, we need to sync the 461 // list on the other page (i.e. if the updates page is the current page, modify the 462 // list in the details page). 463 if (currentPageIndex == TAB_INDEX_UPDATES) { 464 mDetailFragment.requestToMoveToOffset((int) mTabCarousel.getY()); 465 } else { 466 mUpdatesFragment.requestToMoveToOffset((int) mTabCarousel.getY()); 467 } 468 } 469 470 private int getOffsetOfFirstItemInList(int currentPageIndex) { 471 if (currentPageIndex == TAB_INDEX_DETAIL) { 472 return mDetailFragment.getFirstListItemOffset(); 473 } else { 474 return mUpdatesFragment.getFirstListItemOffset(); 475 } 476 } 477 478 private final ContactDetailTabCarousel.Listener mTabCarouselListener = 479 new ContactDetailTabCarousel.Listener() { 480 481 @Override 482 public void onTouchDown() { 483 // The user just started scrolling the carousel, so begin "fake dragging" the 484 // {@link ViewPager} if it's not already doing so. 485 if (mViewPager.isFakeDragging()) { 486 return; 487 } 488 mViewPager.beginFakeDrag(); 489 } 490 491 @Override 492 public void onTouchUp() { 493 // The user just stopped scrolling the carousel, so stop "fake dragging" the 494 // {@link ViewPager} if was doing so before. 495 if (mViewPager.isFakeDragging()) { 496 mViewPager.endFakeDrag(); 497 } 498 } 499 500 @Override 501 public void onScrollChanged(int l, int t, int oldl, int oldt) { 502 // The user is scrolling the carousel, so send the scroll deltas to the 503 // {@link ViewPager} so it can move in sync. 504 if (mViewPager.isFakeDragging()) { 505 mViewPager.fakeDragBy(oldl-l); 506 } 507 } 508 509 @Override 510 public void onTabSelected(int position) { 511 // The user selected a tab, so update the {@link ViewPager} 512 mViewPager.setCurrentItem(position); 513 } 514 }; 515 516 private final class VerticalScrollListener implements OnScrollListener { 517 518 private final int mPageIndex; 519 520 public VerticalScrollListener(int pageIndex) { 521 mPageIndex = pageIndex; 522 } 523 524 @Override 525 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 526 int totalItemCount) { 527 int currentPageIndex = mViewPager.getCurrentItem(); 528 // Don't move the carousel if: 1) the contact does not have social updates because then 529 // tab carousel must not be visible, 2) if the view pager is still being scrolled, or 530 // 3) if the current page being viewed is not this one. 531 if (!mContactHasUpdates || mViewPagerState != ViewPager.SCROLL_STATE_IDLE || 532 mPageIndex != currentPageIndex) { 533 return; 534 } 535 // If the FIRST item is not visible on the screen, then the carousel must be pinned 536 // at the top of the screen. 537 if (firstVisibleItem != 0) { 538 mTabCarousel.moveToYCoordinate(mPageIndex, 539 -mTabCarousel.getAllowedVerticalScrollLength()); 540 return; 541 } 542 View topView = view.getChildAt(firstVisibleItem); 543 if (topView == null) { 544 return; 545 } 546 int amtToScroll = Math.max((int) view.getChildAt(firstVisibleItem).getY(), 547 -mTabCarousel.getAllowedVerticalScrollLength()); 548 mTabCarousel.moveToYCoordinate(mPageIndex, amtToScroll); 549 } 550 551 @Override 552 public void onScrollStateChanged(AbsListView view, int scrollState) { 553 // Once the list has become IDLE, check if we need to sync the scroll position of 554 // the other list now. This will make swiping faster by doing the re-layout now 555 // (instead of at the start of a swipe). However, there will still be another check 556 // when we start swiping if the scroll positions are correct (to catch the edge case 557 // where the user flings and immediately starts a swipe so we never get the idle state). 558 if (scrollState == SCROLL_STATE_IDLE) { 559 syncScrollStateBetweenLists(mPageIndex); 560 } 561 } 562 } 563} 564