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