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