Loading src/com/android/dialer/list/ListsFragment.java +51 −8 Original line number Diff line number Diff line Loading @@ -4,15 +4,10 @@ import android.animation.LayoutTransition; import android.app.ActionBar; import android.app.Fragment; import android.app.FragmentManager; import android.app.LoaderManager; import android.content.Context; import android.content.CursorLoader; import android.content.Loader; import android.content.SharedPreferences; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.provider.CallLog; import android.support.v13.app.FragmentPagerAdapter; import android.support.v4.view.ViewPager; import android.support.v4.view.ViewPager.OnPageChangeListener; Loading @@ -20,6 +15,7 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.ListView; import com.android.contacts.common.GeoUtil; Loading @@ -34,7 +30,7 @@ import com.android.dialer.calllog.ContactInfoHelper; import com.android.dialer.list.ShortcutCardsAdapter.SwipeableShortcutCard; import com.android.dialer.util.DialerUtils; import com.android.dialer.widget.OverlappingPaneLayout; import com.android.dialer.widget.OverlappingPaneLayout.PanelSlideListener; import com.android.dialer.widget.OverlappingPaneLayout.PanelSlideCallbacks; import com.android.dialerbind.analytics.AnalyticsFragment; import com.android.dialerbind.ObjectFactory; Loading Loading @@ -108,7 +104,7 @@ public class ListsFragment extends AnalyticsFragment implements CallLogQueryHand */ private long mCurrentCallShortcutDate = 0; private PanelSlideListener mPanelSlideListener = new PanelSlideListener() { private PanelSlideCallbacks mPanelSlideCallbacks = new PanelSlideCallbacks() { @Override public void onPanelSlide(View panel, float slideOffset) { // For every 1 percent that the panel is slid upwards, clip 1 percent off the top Loading Loading @@ -152,8 +148,35 @@ public class ListsFragment extends AnalyticsFragment implements CallLogQueryHand } mIsPanelOpen = false; } @Override public void onPanelFlingReachesEdge(int velocityY) { if (getCurrentListView() != null) { getCurrentListView().fling(velocityY); } } @Override public boolean isScrollableChildUnscrolled() { final AbsListView listView = getCurrentListView(); return listView != null && (listView.getChildCount() == 0 || listView.getChildAt(0).getTop() == listView.getPaddingTop()); } }; private AbsListView getCurrentListView() { final int position = mViewPager.getCurrentItem(); switch (getRtlPosition(position)) { case TAB_INDEX_SPEED_DIAL: return mSpeedDialFragment == null ? null : mSpeedDialFragment.getListView(); case TAB_INDEX_RECENTS: return mRecentsFragment == null ? null : mRecentsFragment.getListView(); case TAB_INDEX_ALL_CONTACTS: return mAllContactsFragment == null ? null : mAllContactsFragment.getListView(); } throw new IllegalStateException("No fragment at position " + position); } public class ViewPagerAdapter extends FragmentPagerAdapter { public ViewPagerAdapter(FragmentManager fm) { super(fm); Loading @@ -177,6 +200,26 @@ public class ListsFragment extends AnalyticsFragment implements CallLogQueryHand throw new IllegalStateException("No fragment at position " + position); } @Override public Object instantiateItem(ViewGroup container, int position) { // On rotation the FragmentManager handles rotation. Therefore getItem() isn't called. // Copy the fragments that the FragmentManager finds so that we can store them in // instance variables for later. final Fragment fragment = (Fragment) super.instantiateItem(container, position); switch (getRtlPosition(position)) { case TAB_INDEX_SPEED_DIAL: mSpeedDialFragment = (SpeedDialFragment) fragment; return mSpeedDialFragment; case TAB_INDEX_RECENTS: mRecentsFragment = (CallLogFragment) fragment; return mRecentsFragment; case TAB_INDEX_ALL_CONTACTS: mAllContactsFragment = (AllContactsFragment) fragment; return mAllContactsFragment; } return super.instantiateItem(container, position); } @Override public int getCount() { return TAB_INDEX_COUNT; Loading Loading @@ -360,7 +403,7 @@ public class ListsFragment extends AnalyticsFragment implements CallLogQueryHand // the framework better supports nested scrolling. paneLayout.setCapturableView(mViewPagerTabs); paneLayout.openPane(); paneLayout.setPanelSlideListener(mPanelSlideListener); paneLayout.setPanelSlideCallbacks(mPanelSlideCallbacks); paneLayout.setIntermediatePinnedOffset( ((HostInterface) getActivity()).getActionBarHeight()); Loading src/com/android/dialer/list/SpeedDialFragment.java +4 −0 Original line number Diff line number Diff line Loading @@ -420,4 +420,8 @@ public class SpeedDialFragment extends AnalyticsFragment implements OnItemClickL public void cacheOffsetsForDatasetChange() { saveOffsets(0); } public AbsListView getListView() { return mListView; } } src/com/android/dialer/widget/OverlappingPaneLayout.java +187 −39 Original line number Diff line number Diff line Loading @@ -33,6 +33,7 @@ import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewParent; import android.view.accessibility.AccessibilityEvent; Loading Loading @@ -116,18 +117,27 @@ public class OverlappingPaneLayout extends ViewGroup { /** * Indicates that the layout is currently in the process of a nested pre-scroll operation where * the child scrolling view is being dragged downwards, and still has the ability to consume * scroll events itself. If so, we should open the pane up to the maximum offset defined in * {@link #mIntermediateOffset}, and no further, so that the child view can continue performing * its own scroll. * the child scrolling view is being dragged downwards. */ private boolean mInNestedPreScrollDownwards = false; private boolean mInNestedPreScrollDownwards; /** * Indicates whether or not a nested scrolling child is able to scroll internally at this point * in time. * Indicates that the layout is currently in the process of a nested pre-scroll operation where * the child scrolling view is being dragged upwards. */ private boolean mInNestedPreScrollUpwards; /** * Indicates that the layout is currently in the process of a fling initiated by a pre-fling * from the child scrolling view. */ private boolean mIsInNestedFling; /** * Indicates the direction of the pre fling. We need to store this information since * OverScoller doesn't expose the direction of its velocity. */ private boolean mChildCannotConsumeScroll; private boolean mInUpwardsPreFling; /** * Stores an offset used to represent a point somewhere in between the panel's fully closed Loading @@ -139,7 +149,7 @@ public class OverlappingPaneLayout extends ViewGroup { private float mInitialMotionX; private float mInitialMotionY; private PanelSlideListener mPanelSlideListener; private PanelSlideCallbacks mPanelSlideCallbacks; private final ViewDragHelper mDragHelper; Loading @@ -154,9 +164,18 @@ public class OverlappingPaneLayout extends ViewGroup { private final Rect mTmpRect = new Rect(); /** * Listener for monitoring events about sliding panes. * How many dips we need to scroll past a position before we can snap to the next position * on release. Using this prevents accidentally snapping to positions. * * This is needed since vertical nested scrolling can be passed to this class even if the * vertical scroll is less than the the nested list's touch slop. */ public interface PanelSlideListener { private final int mReleaseScrollSlop; /** * Callbacks for interacting with sliding panes. */ public interface PanelSlideCallbacks { /** * Called when a sliding pane's position changes. * @param panel The child view that was moved Loading @@ -176,6 +195,22 @@ public class OverlappingPaneLayout extends ViewGroup { * @param panel The child view that was slid to a closed position */ public void onPanelClosed(View panel); /** * Called when a sliding pane is flung as far open/closed as it can be. * @param velocityY Velocity of the panel once its fling goes as far as it can. */ public void onPanelFlingReachesEdge(int velocityY); /** * Returns true if the second panel's contents haven't been scrolled at all. This value is * used to determine whether or not we can fully expand the header on downwards scrolls. * * Instead of using this callback, it would be preferable to instead fully expand the header * on a View#onNestedFlingOver() callback. The behavior would be nicer. Unfortunately, * no such callback exists yet (b/17547693). */ public boolean isScrollableChildUnscrolled(); } public OverlappingPaneLayout(Context context) { Loading @@ -199,6 +234,8 @@ public class OverlappingPaneLayout extends ViewGroup { mDragHelper = ViewDragHelper.create(this, 0.5f, new DragHelperCallback()); mDragHelper.setMinVelocity(MIN_FLING_VELOCITY * density); mReleaseScrollSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); } /** Loading @@ -218,27 +255,21 @@ public class OverlappingPaneLayout extends ViewGroup { mCapturableView = capturableView; } public void setPanelSlideListener(PanelSlideListener listener) { mPanelSlideListener = listener; public void setPanelSlideCallbacks(PanelSlideCallbacks listener) { mPanelSlideCallbacks = listener; } void dispatchOnPanelSlide(View panel) { if (mPanelSlideListener != null) { mPanelSlideListener.onPanelSlide(panel, mSlideOffset); } mPanelSlideCallbacks.onPanelSlide(panel, mSlideOffset); } void dispatchOnPanelOpened(View panel) { if (mPanelSlideListener != null) { mPanelSlideListener.onPanelOpened(panel); } mPanelSlideCallbacks.onPanelOpened(panel); sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); } void dispatchOnPanelClosed(View panel) { if (mPanelSlideListener != null) { mPanelSlideListener.onPanelClosed(panel); } mPanelSlideCallbacks.onPanelClosed(panel); sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); } Loading Loading @@ -820,7 +851,7 @@ public class OverlappingPaneLayout extends ViewGroup { @Override public void computeScroll() { if (mDragHelper.continueSettling(true)) { if (mDragHelper.continueSettling(/* deferCallbacks = */ false)) { if (!mCanSlide) { mDragHelper.abort(); return; Loading Loading @@ -897,7 +928,6 @@ public class OverlappingPaneLayout extends ViewGroup { final boolean startNestedScroll = (nestedScrollAxes & SCROLL_AXIS_VERTICAL) != 0; if (startNestedScroll) { mIsInNestedScroll = true; mChildCannotConsumeScroll = true; mDragHelper.startNestedScroll(mSlideableView); } if (DEBUG) { Loading @@ -915,19 +945,41 @@ public class OverlappingPaneLayout extends ViewGroup { if (DEBUG) { Log.d(TAG, "onNestedPreScroll: " + dy); } mInNestedPreScrollDownwards = mChildCannotConsumeScroll && dy < 0 && mSlideOffsetPx <= mIntermediateOffset; mInNestedPreScrollDownwards = dy < 0; mInNestedPreScrollUpwards = dy > 0; mIsInNestedFling = false; mDragHelper.processNestedScroll(mSlideableView, 0, -dy, consumed); } @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { if (!(velocityY > 0 && mSlideOffsetPx != 0 || velocityY < 0 && mSlideOffsetPx < mIntermediateOffset || velocityY < 0 && mSlideOffsetPx < mSlideRange && mPanelSlideCallbacks.isScrollableChildUnscrolled())) { // No need to consume the fling if the fling won't collapse or expand the header. // How far we are willing to expand the header depends on isScrollableChildUnscrolled(). return false; } if (DEBUG) { Log.d(TAG, "onNestedPreFling: " + velocityY); } mInUpwardsPreFling = velocityY > 0; mIsInNestedFling = true; mIsInNestedScroll = false; mDragHelper.processNestedFling(mSlideableView, (int) -velocityY); return true; } @Override public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { if (DEBUG) { Log.d(TAG, "onNestedScroll: " + dyUnconsumed); } mChildCannotConsumeScroll = false; mInNestedPreScrollDownwards = false; mIsInNestedFling = false; mDragHelper.processNestedScroll(mSlideableView, 0, -dyUnconsumed, null); } Loading @@ -938,9 +990,11 @@ public class OverlappingPaneLayout extends ViewGroup { } if (mIsInNestedScroll) { mDragHelper.stopNestedScroll(mSlideableView); } mInNestedPreScrollDownwards = false; mInNestedPreScrollUpwards = false; mIsInNestedScroll = false; } } private class DragHelperCallback extends ViewDragHelper.Callback { Loading @@ -955,6 +1009,10 @@ public class OverlappingPaneLayout extends ViewGroup { @Override public void onViewDragStateChanged(int state) { if (DEBUG) { Log.d(TAG, "onViewDragStateChanged: " + state); } if (mDragHelper.getViewDragState() == ViewDragHelper.STATE_IDLE) { if (mSlideOffset == 0) { updateObscuredViewsVisibility(mSlideableView); Loading @@ -965,6 +1023,16 @@ public class OverlappingPaneLayout extends ViewGroup { mPreservedOpenState = true; } } if (mDragHelper.getVelocityMagnitude() > 0 && (mDragHelper.getCurrentScrollY() == 0 || mDragHelper.getCurrentScrollY() == mIntermediateOffset) && mIsInNestedFling) { mIsInNestedFling = false; final int flingVelocity = !mInUpwardsPreFling ? -mDragHelper.getVelocityMagnitude() : mDragHelper.getVelocityMagnitude(); mPanelSlideCallbacks.onPanelFlingReachesEdge(flingVelocity); } } @Override Loading @@ -979,21 +1047,97 @@ public class OverlappingPaneLayout extends ViewGroup { invalidate(); } @Override public void onViewFling(View releasedChild, float xVelocity, float yVelocity) { if (releasedChild == null) { return; } if (DEBUG) { Log.d(TAG, "onViewFling: " + yVelocity); } // Flings won't always fully expand or collapse the header. Instead of performing the // fling and then waiting for the fling to end before snapping into place, we // immediately snap into place if we predict the fling won't fully expand or collapse // the header. int yOffsetPx = mDragHelper.predictFlingYOffset((int) yVelocity); if (yVelocity < 0) { // Only perform a fling if we know the fling will fully compress the header. if (-yOffsetPx > mSlideOffsetPx) { mDragHelper.flingCapturedView(releasedChild.getLeft(), /* minTop = */ 0, mSlideRange, Integer.MAX_VALUE, (int) yVelocity); } else { mIsInNestedFling = false; onViewReleased(releasedChild, xVelocity, yVelocity); } } else { // Only perform a fling if we know the fling will expand the header as far // as it can possible be expanded, given the isScrollableChildUnscrolled() value. if (yOffsetPx + mSlideOffsetPx >= mSlideRange && mPanelSlideCallbacks.isScrollableChildUnscrolled()) { mDragHelper.flingCapturedView(releasedChild.getLeft(), /* minTop = */ 0, Integer.MAX_VALUE, mSlideRange, (int) yVelocity); } else if (yOffsetPx + mSlideOffsetPx >= mIntermediateOffset && mSlideOffsetPx <= mIntermediateOffset && !mPanelSlideCallbacks.isScrollableChildUnscrolled()) { mDragHelper.flingCapturedView(releasedChild.getLeft(), /* minTop = */ 0, Integer.MAX_VALUE, mIntermediateOffset, (int) yVelocity); } else { mIsInNestedFling = false; onViewReleased(releasedChild, xVelocity, yVelocity); } } mInNestedPreScrollDownwards = false; mInNestedPreScrollUpwards = false; // Without this invalidate, some calls to flingCapturedView can have no affect. invalidate(); } @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { if (DEBUG) { Log.d(TAG, "onViewReleased: " + " unscrolled=" + mPanelSlideCallbacks.isScrollableChildUnscrolled() + ", mInNestedPreScrollDownwards = " + mInNestedPreScrollDownwards + ", mInNestedPreScrollUpwards = " + mInNestedPreScrollUpwards + ", yvel=" + yvel); } if (releasedChild == null) { return; } final LayoutParams lp = (LayoutParams) releasedChild.getLayoutParams(); final LayoutParams lp = (LayoutParams) releasedChild.getLayoutParams(); int top = getPaddingTop() + lp.topMargin; if (mInNestedPreScrollDownwards) { // Snap to the closest pinnable position based on the current slide offset // (in pixels) [0 - mIntermediateoffset - mSlideRange] if (yvel > 0) { // Decide where to snap to according to the current direction of motion and the current // position. The velocity's magnitude has no bearing on this. if (mInNestedPreScrollDownwards || yvel > 0) { // Scrolling downwards if (mSlideOffsetPx > mIntermediateOffset + mReleaseScrollSlop) { top += mSlideRange; } else if (mSlideOffsetPx > mReleaseScrollSlop) { top += mIntermediateOffset; } else { // Offset is very close to 0 } } else if (mInNestedPreScrollUpwards || yvel < 0) { // Scrolling upwards if (mSlideOffsetPx > mSlideRange - mReleaseScrollSlop) { // Offset is very close to mSlideRange top += mSlideRange; } else if (0 <= mSlideOffsetPx && mSlideOffsetPx <= mIntermediateOffset / 2) { } else if (mSlideOffsetPx > mIntermediateOffset - mReleaseScrollSlop) { // Offset is between mIntermediateOffset and mSlideRange. top += mIntermediateOffset; } else { // Offset is between 0 and mIntermediateOffset. } } else { // Not moving upwards or downwards. This case can only be triggered when directly // dragging the tabs. We don't bother to remember previous scroll direction // when directly dragging the tabs. if (0 <= mSlideOffsetPx && mSlideOffsetPx <= mIntermediateOffset / 2) { // Offset is between 0 and mIntermediateOffset, but closer to 0 // Leave top unchanged } else if (mIntermediateOffset / 2 <= mSlideOffsetPx Loading @@ -1005,8 +1149,6 @@ public class OverlappingPaneLayout extends ViewGroup { // mSlideRange top += mSlideRange; } } else if (yvel > 0 || (yvel == 0 && mSlideOffset > 0.5f)) { top += mSlideRange; } mDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top); Loading @@ -1029,9 +1171,15 @@ public class OverlappingPaneLayout extends ViewGroup { final LayoutParams lp = (LayoutParams) mSlideableView.getLayoutParams(); final int newTop; int previousTop = top - dy; int topBound = getPaddingTop() + lp.topMargin; int bottomBound = topBound + (mInNestedPreScrollDownwards ? mIntermediateOffset : mSlideRange); int bottomBound = topBound + (mPanelSlideCallbacks.isScrollableChildUnscrolled() || !mIsInNestedScroll ? mSlideRange : mIntermediateOffset); if (previousTop > bottomBound) { // We were previously below the bottomBound, so loosen the bottomBound so that this // makes sense. This can occur after the view was directly dragged by the tabs. bottomBound = Math.max(bottomBound, mSlideRange); } newTop = Math.min(Math.max(top, topBound), bottomBound); return newTop; Loading src/com/android/dialer/widget/ViewDragHelper.java +75 −12 Original line number Diff line number Diff line Loading @@ -27,7 +27,6 @@ import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.animation.Interpolator; import java.util.Arrays; Loading Loading @@ -201,6 +200,18 @@ public class ViewDragHelper { */ public void onViewReleased(View releasedChild, float xvel, float yvel) {} /** * Called when the child view has been released with a fling. * * <p>Calling code may decide to fling or otherwise release the view to let it * settle into place.</p> * * @param releasedChild The captured child view now being released * @param xvel X velocity of the fling. * @param yvel Y velocity of the fling. */ public void onViewFling(View releasedChild, float xvel, float yvel) {} /** * Called when one of the subscribed edges in the parent view has been touched * by the user while no child view is currently captured. Loading Loading @@ -321,16 +332,6 @@ public class ViewDragHelper { } } /** * Interpolator defining the animation curve for mScroller */ private static final Interpolator sInterpolator = new Interpolator() { public float getInterpolation(float t) { t -= 1.0f; return t * t * t * t * t + 1.0f; } }; private final Runnable mSetIdleRunnable = new Runnable() { public void run() { setDragState(STATE_IDLE); Loading Loading @@ -389,7 +390,7 @@ public class ViewDragHelper { mTouchSlop = vc.getScaledTouchSlop(); mMaxVelocity = vc.getScaledMaximumFlingVelocity(); mMinVelocity = vc.getScaledMinimumFlingVelocity(); mScroller = ScrollerCompat.create(context, sInterpolator); mScroller = ScrollerCompat.create(context); } /** Loading Loading @@ -701,6 +702,46 @@ public class ViewDragHelper { setDragState(STATE_SETTLING); } /** * Settle the captured view based on standard free-moving fling behavior. * The caller should invoke {@link #continueSettling(boolean)} on each subsequent frame * to continue the motion until it returns false. * * @param minLeft Minimum X position for the view's left edge * @param minTop Minimum Y position for the view's top edge * @param maxLeft Maximum X position for the view's left edge * @param maxTop Maximum Y position for the view's top edge * @param yvel the Y velocity to fling with */ public void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop, int yvel) { if (!mReleaseInProgress) { throw new IllegalStateException("Cannot flingCapturedView outside of a call to " + "Callback#onViewReleased"); } mScroller.abortAnimation(); mScroller.fling(mCapturedView.getLeft(), mCapturedView.getTop(), 0, yvel, minLeft, maxLeft, minTop, maxTop); setDragState(STATE_SETTLING); } /** * Predict how far a fling with {@param yvel} will cause the view to travel from stand still. * @return predicted y offset */ public int predictFlingYOffset(int yvel) { mScroller.abortAnimation(); mScroller.fling(0, 0, 0, yvel, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE); final int finalY = mScroller.getFinalY(); mScroller.abortAnimation(); return finalY; } public int getCurrentScrollY() { return mScroller.getCurrY(); } /** * Move the captured settling view by the appropriate amount for the current time. * If <code>continueSettling</code> returns true, the caller should call it again Loading Loading @@ -750,6 +791,28 @@ public class ViewDragHelper { return mDragState == STATE_SETTLING; } public void processNestedFling(View target, int yvel) { mCapturedView = target; dispatchViewFling(0, yvel); } public int getVelocityMagnitude() { // Use Math.abs() to ensure this always returns an absolute value, even if the // ScrollerCompat implementation changes. return (int) Math.abs(mScroller.getCurrVelocity()); } private void dispatchViewFling(float xvel, float yvel) { mReleaseInProgress = true; mCallback.onViewFling(mCapturedView, xvel, yvel); mReleaseInProgress = false; if (mDragState == STATE_DRAGGING) { // onViewReleased didn't call a method that would have changed this. Go idle. setDragState(STATE_IDLE); } } /** * Like all callback events this must happen on the UI thread, but release * involves some extra semantics. During a release (mReleaseInProgress) Loading Loading
src/com/android/dialer/list/ListsFragment.java +51 −8 Original line number Diff line number Diff line Loading @@ -4,15 +4,10 @@ import android.animation.LayoutTransition; import android.app.ActionBar; import android.app.Fragment; import android.app.FragmentManager; import android.app.LoaderManager; import android.content.Context; import android.content.CursorLoader; import android.content.Loader; import android.content.SharedPreferences; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.provider.CallLog; import android.support.v13.app.FragmentPagerAdapter; import android.support.v4.view.ViewPager; import android.support.v4.view.ViewPager.OnPageChangeListener; Loading @@ -20,6 +15,7 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.ListView; import com.android.contacts.common.GeoUtil; Loading @@ -34,7 +30,7 @@ import com.android.dialer.calllog.ContactInfoHelper; import com.android.dialer.list.ShortcutCardsAdapter.SwipeableShortcutCard; import com.android.dialer.util.DialerUtils; import com.android.dialer.widget.OverlappingPaneLayout; import com.android.dialer.widget.OverlappingPaneLayout.PanelSlideListener; import com.android.dialer.widget.OverlappingPaneLayout.PanelSlideCallbacks; import com.android.dialerbind.analytics.AnalyticsFragment; import com.android.dialerbind.ObjectFactory; Loading Loading @@ -108,7 +104,7 @@ public class ListsFragment extends AnalyticsFragment implements CallLogQueryHand */ private long mCurrentCallShortcutDate = 0; private PanelSlideListener mPanelSlideListener = new PanelSlideListener() { private PanelSlideCallbacks mPanelSlideCallbacks = new PanelSlideCallbacks() { @Override public void onPanelSlide(View panel, float slideOffset) { // For every 1 percent that the panel is slid upwards, clip 1 percent off the top Loading Loading @@ -152,8 +148,35 @@ public class ListsFragment extends AnalyticsFragment implements CallLogQueryHand } mIsPanelOpen = false; } @Override public void onPanelFlingReachesEdge(int velocityY) { if (getCurrentListView() != null) { getCurrentListView().fling(velocityY); } } @Override public boolean isScrollableChildUnscrolled() { final AbsListView listView = getCurrentListView(); return listView != null && (listView.getChildCount() == 0 || listView.getChildAt(0).getTop() == listView.getPaddingTop()); } }; private AbsListView getCurrentListView() { final int position = mViewPager.getCurrentItem(); switch (getRtlPosition(position)) { case TAB_INDEX_SPEED_DIAL: return mSpeedDialFragment == null ? null : mSpeedDialFragment.getListView(); case TAB_INDEX_RECENTS: return mRecentsFragment == null ? null : mRecentsFragment.getListView(); case TAB_INDEX_ALL_CONTACTS: return mAllContactsFragment == null ? null : mAllContactsFragment.getListView(); } throw new IllegalStateException("No fragment at position " + position); } public class ViewPagerAdapter extends FragmentPagerAdapter { public ViewPagerAdapter(FragmentManager fm) { super(fm); Loading @@ -177,6 +200,26 @@ public class ListsFragment extends AnalyticsFragment implements CallLogQueryHand throw new IllegalStateException("No fragment at position " + position); } @Override public Object instantiateItem(ViewGroup container, int position) { // On rotation the FragmentManager handles rotation. Therefore getItem() isn't called. // Copy the fragments that the FragmentManager finds so that we can store them in // instance variables for later. final Fragment fragment = (Fragment) super.instantiateItem(container, position); switch (getRtlPosition(position)) { case TAB_INDEX_SPEED_DIAL: mSpeedDialFragment = (SpeedDialFragment) fragment; return mSpeedDialFragment; case TAB_INDEX_RECENTS: mRecentsFragment = (CallLogFragment) fragment; return mRecentsFragment; case TAB_INDEX_ALL_CONTACTS: mAllContactsFragment = (AllContactsFragment) fragment; return mAllContactsFragment; } return super.instantiateItem(container, position); } @Override public int getCount() { return TAB_INDEX_COUNT; Loading Loading @@ -360,7 +403,7 @@ public class ListsFragment extends AnalyticsFragment implements CallLogQueryHand // the framework better supports nested scrolling. paneLayout.setCapturableView(mViewPagerTabs); paneLayout.openPane(); paneLayout.setPanelSlideListener(mPanelSlideListener); paneLayout.setPanelSlideCallbacks(mPanelSlideCallbacks); paneLayout.setIntermediatePinnedOffset( ((HostInterface) getActivity()).getActionBarHeight()); Loading
src/com/android/dialer/list/SpeedDialFragment.java +4 −0 Original line number Diff line number Diff line Loading @@ -420,4 +420,8 @@ public class SpeedDialFragment extends AnalyticsFragment implements OnItemClickL public void cacheOffsetsForDatasetChange() { saveOffsets(0); } public AbsListView getListView() { return mListView; } }
src/com/android/dialer/widget/OverlappingPaneLayout.java +187 −39 Original line number Diff line number Diff line Loading @@ -33,6 +33,7 @@ import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewParent; import android.view.accessibility.AccessibilityEvent; Loading Loading @@ -116,18 +117,27 @@ public class OverlappingPaneLayout extends ViewGroup { /** * Indicates that the layout is currently in the process of a nested pre-scroll operation where * the child scrolling view is being dragged downwards, and still has the ability to consume * scroll events itself. If so, we should open the pane up to the maximum offset defined in * {@link #mIntermediateOffset}, and no further, so that the child view can continue performing * its own scroll. * the child scrolling view is being dragged downwards. */ private boolean mInNestedPreScrollDownwards = false; private boolean mInNestedPreScrollDownwards; /** * Indicates whether or not a nested scrolling child is able to scroll internally at this point * in time. * Indicates that the layout is currently in the process of a nested pre-scroll operation where * the child scrolling view is being dragged upwards. */ private boolean mInNestedPreScrollUpwards; /** * Indicates that the layout is currently in the process of a fling initiated by a pre-fling * from the child scrolling view. */ private boolean mIsInNestedFling; /** * Indicates the direction of the pre fling. We need to store this information since * OverScoller doesn't expose the direction of its velocity. */ private boolean mChildCannotConsumeScroll; private boolean mInUpwardsPreFling; /** * Stores an offset used to represent a point somewhere in between the panel's fully closed Loading @@ -139,7 +149,7 @@ public class OverlappingPaneLayout extends ViewGroup { private float mInitialMotionX; private float mInitialMotionY; private PanelSlideListener mPanelSlideListener; private PanelSlideCallbacks mPanelSlideCallbacks; private final ViewDragHelper mDragHelper; Loading @@ -154,9 +164,18 @@ public class OverlappingPaneLayout extends ViewGroup { private final Rect mTmpRect = new Rect(); /** * Listener for monitoring events about sliding panes. * How many dips we need to scroll past a position before we can snap to the next position * on release. Using this prevents accidentally snapping to positions. * * This is needed since vertical nested scrolling can be passed to this class even if the * vertical scroll is less than the the nested list's touch slop. */ public interface PanelSlideListener { private final int mReleaseScrollSlop; /** * Callbacks for interacting with sliding panes. */ public interface PanelSlideCallbacks { /** * Called when a sliding pane's position changes. * @param panel The child view that was moved Loading @@ -176,6 +195,22 @@ public class OverlappingPaneLayout extends ViewGroup { * @param panel The child view that was slid to a closed position */ public void onPanelClosed(View panel); /** * Called when a sliding pane is flung as far open/closed as it can be. * @param velocityY Velocity of the panel once its fling goes as far as it can. */ public void onPanelFlingReachesEdge(int velocityY); /** * Returns true if the second panel's contents haven't been scrolled at all. This value is * used to determine whether or not we can fully expand the header on downwards scrolls. * * Instead of using this callback, it would be preferable to instead fully expand the header * on a View#onNestedFlingOver() callback. The behavior would be nicer. Unfortunately, * no such callback exists yet (b/17547693). */ public boolean isScrollableChildUnscrolled(); } public OverlappingPaneLayout(Context context) { Loading @@ -199,6 +234,8 @@ public class OverlappingPaneLayout extends ViewGroup { mDragHelper = ViewDragHelper.create(this, 0.5f, new DragHelperCallback()); mDragHelper.setMinVelocity(MIN_FLING_VELOCITY * density); mReleaseScrollSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); } /** Loading @@ -218,27 +255,21 @@ public class OverlappingPaneLayout extends ViewGroup { mCapturableView = capturableView; } public void setPanelSlideListener(PanelSlideListener listener) { mPanelSlideListener = listener; public void setPanelSlideCallbacks(PanelSlideCallbacks listener) { mPanelSlideCallbacks = listener; } void dispatchOnPanelSlide(View panel) { if (mPanelSlideListener != null) { mPanelSlideListener.onPanelSlide(panel, mSlideOffset); } mPanelSlideCallbacks.onPanelSlide(panel, mSlideOffset); } void dispatchOnPanelOpened(View panel) { if (mPanelSlideListener != null) { mPanelSlideListener.onPanelOpened(panel); } mPanelSlideCallbacks.onPanelOpened(panel); sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); } void dispatchOnPanelClosed(View panel) { if (mPanelSlideListener != null) { mPanelSlideListener.onPanelClosed(panel); } mPanelSlideCallbacks.onPanelClosed(panel); sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); } Loading Loading @@ -820,7 +851,7 @@ public class OverlappingPaneLayout extends ViewGroup { @Override public void computeScroll() { if (mDragHelper.continueSettling(true)) { if (mDragHelper.continueSettling(/* deferCallbacks = */ false)) { if (!mCanSlide) { mDragHelper.abort(); return; Loading Loading @@ -897,7 +928,6 @@ public class OverlappingPaneLayout extends ViewGroup { final boolean startNestedScroll = (nestedScrollAxes & SCROLL_AXIS_VERTICAL) != 0; if (startNestedScroll) { mIsInNestedScroll = true; mChildCannotConsumeScroll = true; mDragHelper.startNestedScroll(mSlideableView); } if (DEBUG) { Loading @@ -915,19 +945,41 @@ public class OverlappingPaneLayout extends ViewGroup { if (DEBUG) { Log.d(TAG, "onNestedPreScroll: " + dy); } mInNestedPreScrollDownwards = mChildCannotConsumeScroll && dy < 0 && mSlideOffsetPx <= mIntermediateOffset; mInNestedPreScrollDownwards = dy < 0; mInNestedPreScrollUpwards = dy > 0; mIsInNestedFling = false; mDragHelper.processNestedScroll(mSlideableView, 0, -dy, consumed); } @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { if (!(velocityY > 0 && mSlideOffsetPx != 0 || velocityY < 0 && mSlideOffsetPx < mIntermediateOffset || velocityY < 0 && mSlideOffsetPx < mSlideRange && mPanelSlideCallbacks.isScrollableChildUnscrolled())) { // No need to consume the fling if the fling won't collapse or expand the header. // How far we are willing to expand the header depends on isScrollableChildUnscrolled(). return false; } if (DEBUG) { Log.d(TAG, "onNestedPreFling: " + velocityY); } mInUpwardsPreFling = velocityY > 0; mIsInNestedFling = true; mIsInNestedScroll = false; mDragHelper.processNestedFling(mSlideableView, (int) -velocityY); return true; } @Override public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { if (DEBUG) { Log.d(TAG, "onNestedScroll: " + dyUnconsumed); } mChildCannotConsumeScroll = false; mInNestedPreScrollDownwards = false; mIsInNestedFling = false; mDragHelper.processNestedScroll(mSlideableView, 0, -dyUnconsumed, null); } Loading @@ -938,9 +990,11 @@ public class OverlappingPaneLayout extends ViewGroup { } if (mIsInNestedScroll) { mDragHelper.stopNestedScroll(mSlideableView); } mInNestedPreScrollDownwards = false; mInNestedPreScrollUpwards = false; mIsInNestedScroll = false; } } private class DragHelperCallback extends ViewDragHelper.Callback { Loading @@ -955,6 +1009,10 @@ public class OverlappingPaneLayout extends ViewGroup { @Override public void onViewDragStateChanged(int state) { if (DEBUG) { Log.d(TAG, "onViewDragStateChanged: " + state); } if (mDragHelper.getViewDragState() == ViewDragHelper.STATE_IDLE) { if (mSlideOffset == 0) { updateObscuredViewsVisibility(mSlideableView); Loading @@ -965,6 +1023,16 @@ public class OverlappingPaneLayout extends ViewGroup { mPreservedOpenState = true; } } if (mDragHelper.getVelocityMagnitude() > 0 && (mDragHelper.getCurrentScrollY() == 0 || mDragHelper.getCurrentScrollY() == mIntermediateOffset) && mIsInNestedFling) { mIsInNestedFling = false; final int flingVelocity = !mInUpwardsPreFling ? -mDragHelper.getVelocityMagnitude() : mDragHelper.getVelocityMagnitude(); mPanelSlideCallbacks.onPanelFlingReachesEdge(flingVelocity); } } @Override Loading @@ -979,21 +1047,97 @@ public class OverlappingPaneLayout extends ViewGroup { invalidate(); } @Override public void onViewFling(View releasedChild, float xVelocity, float yVelocity) { if (releasedChild == null) { return; } if (DEBUG) { Log.d(TAG, "onViewFling: " + yVelocity); } // Flings won't always fully expand or collapse the header. Instead of performing the // fling and then waiting for the fling to end before snapping into place, we // immediately snap into place if we predict the fling won't fully expand or collapse // the header. int yOffsetPx = mDragHelper.predictFlingYOffset((int) yVelocity); if (yVelocity < 0) { // Only perform a fling if we know the fling will fully compress the header. if (-yOffsetPx > mSlideOffsetPx) { mDragHelper.flingCapturedView(releasedChild.getLeft(), /* minTop = */ 0, mSlideRange, Integer.MAX_VALUE, (int) yVelocity); } else { mIsInNestedFling = false; onViewReleased(releasedChild, xVelocity, yVelocity); } } else { // Only perform a fling if we know the fling will expand the header as far // as it can possible be expanded, given the isScrollableChildUnscrolled() value. if (yOffsetPx + mSlideOffsetPx >= mSlideRange && mPanelSlideCallbacks.isScrollableChildUnscrolled()) { mDragHelper.flingCapturedView(releasedChild.getLeft(), /* minTop = */ 0, Integer.MAX_VALUE, mSlideRange, (int) yVelocity); } else if (yOffsetPx + mSlideOffsetPx >= mIntermediateOffset && mSlideOffsetPx <= mIntermediateOffset && !mPanelSlideCallbacks.isScrollableChildUnscrolled()) { mDragHelper.flingCapturedView(releasedChild.getLeft(), /* minTop = */ 0, Integer.MAX_VALUE, mIntermediateOffset, (int) yVelocity); } else { mIsInNestedFling = false; onViewReleased(releasedChild, xVelocity, yVelocity); } } mInNestedPreScrollDownwards = false; mInNestedPreScrollUpwards = false; // Without this invalidate, some calls to flingCapturedView can have no affect. invalidate(); } @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { if (DEBUG) { Log.d(TAG, "onViewReleased: " + " unscrolled=" + mPanelSlideCallbacks.isScrollableChildUnscrolled() + ", mInNestedPreScrollDownwards = " + mInNestedPreScrollDownwards + ", mInNestedPreScrollUpwards = " + mInNestedPreScrollUpwards + ", yvel=" + yvel); } if (releasedChild == null) { return; } final LayoutParams lp = (LayoutParams) releasedChild.getLayoutParams(); final LayoutParams lp = (LayoutParams) releasedChild.getLayoutParams(); int top = getPaddingTop() + lp.topMargin; if (mInNestedPreScrollDownwards) { // Snap to the closest pinnable position based on the current slide offset // (in pixels) [0 - mIntermediateoffset - mSlideRange] if (yvel > 0) { // Decide where to snap to according to the current direction of motion and the current // position. The velocity's magnitude has no bearing on this. if (mInNestedPreScrollDownwards || yvel > 0) { // Scrolling downwards if (mSlideOffsetPx > mIntermediateOffset + mReleaseScrollSlop) { top += mSlideRange; } else if (mSlideOffsetPx > mReleaseScrollSlop) { top += mIntermediateOffset; } else { // Offset is very close to 0 } } else if (mInNestedPreScrollUpwards || yvel < 0) { // Scrolling upwards if (mSlideOffsetPx > mSlideRange - mReleaseScrollSlop) { // Offset is very close to mSlideRange top += mSlideRange; } else if (0 <= mSlideOffsetPx && mSlideOffsetPx <= mIntermediateOffset / 2) { } else if (mSlideOffsetPx > mIntermediateOffset - mReleaseScrollSlop) { // Offset is between mIntermediateOffset and mSlideRange. top += mIntermediateOffset; } else { // Offset is between 0 and mIntermediateOffset. } } else { // Not moving upwards or downwards. This case can only be triggered when directly // dragging the tabs. We don't bother to remember previous scroll direction // when directly dragging the tabs. if (0 <= mSlideOffsetPx && mSlideOffsetPx <= mIntermediateOffset / 2) { // Offset is between 0 and mIntermediateOffset, but closer to 0 // Leave top unchanged } else if (mIntermediateOffset / 2 <= mSlideOffsetPx Loading @@ -1005,8 +1149,6 @@ public class OverlappingPaneLayout extends ViewGroup { // mSlideRange top += mSlideRange; } } else if (yvel > 0 || (yvel == 0 && mSlideOffset > 0.5f)) { top += mSlideRange; } mDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top); Loading @@ -1029,9 +1171,15 @@ public class OverlappingPaneLayout extends ViewGroup { final LayoutParams lp = (LayoutParams) mSlideableView.getLayoutParams(); final int newTop; int previousTop = top - dy; int topBound = getPaddingTop() + lp.topMargin; int bottomBound = topBound + (mInNestedPreScrollDownwards ? mIntermediateOffset : mSlideRange); int bottomBound = topBound + (mPanelSlideCallbacks.isScrollableChildUnscrolled() || !mIsInNestedScroll ? mSlideRange : mIntermediateOffset); if (previousTop > bottomBound) { // We were previously below the bottomBound, so loosen the bottomBound so that this // makes sense. This can occur after the view was directly dragged by the tabs. bottomBound = Math.max(bottomBound, mSlideRange); } newTop = Math.min(Math.max(top, topBound), bottomBound); return newTop; Loading
src/com/android/dialer/widget/ViewDragHelper.java +75 −12 Original line number Diff line number Diff line Loading @@ -27,7 +27,6 @@ import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.animation.Interpolator; import java.util.Arrays; Loading Loading @@ -201,6 +200,18 @@ public class ViewDragHelper { */ public void onViewReleased(View releasedChild, float xvel, float yvel) {} /** * Called when the child view has been released with a fling. * * <p>Calling code may decide to fling or otherwise release the view to let it * settle into place.</p> * * @param releasedChild The captured child view now being released * @param xvel X velocity of the fling. * @param yvel Y velocity of the fling. */ public void onViewFling(View releasedChild, float xvel, float yvel) {} /** * Called when one of the subscribed edges in the parent view has been touched * by the user while no child view is currently captured. Loading Loading @@ -321,16 +332,6 @@ public class ViewDragHelper { } } /** * Interpolator defining the animation curve for mScroller */ private static final Interpolator sInterpolator = new Interpolator() { public float getInterpolation(float t) { t -= 1.0f; return t * t * t * t * t + 1.0f; } }; private final Runnable mSetIdleRunnable = new Runnable() { public void run() { setDragState(STATE_IDLE); Loading Loading @@ -389,7 +390,7 @@ public class ViewDragHelper { mTouchSlop = vc.getScaledTouchSlop(); mMaxVelocity = vc.getScaledMaximumFlingVelocity(); mMinVelocity = vc.getScaledMinimumFlingVelocity(); mScroller = ScrollerCompat.create(context, sInterpolator); mScroller = ScrollerCompat.create(context); } /** Loading Loading @@ -701,6 +702,46 @@ public class ViewDragHelper { setDragState(STATE_SETTLING); } /** * Settle the captured view based on standard free-moving fling behavior. * The caller should invoke {@link #continueSettling(boolean)} on each subsequent frame * to continue the motion until it returns false. * * @param minLeft Minimum X position for the view's left edge * @param minTop Minimum Y position for the view's top edge * @param maxLeft Maximum X position for the view's left edge * @param maxTop Maximum Y position for the view's top edge * @param yvel the Y velocity to fling with */ public void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop, int yvel) { if (!mReleaseInProgress) { throw new IllegalStateException("Cannot flingCapturedView outside of a call to " + "Callback#onViewReleased"); } mScroller.abortAnimation(); mScroller.fling(mCapturedView.getLeft(), mCapturedView.getTop(), 0, yvel, minLeft, maxLeft, minTop, maxTop); setDragState(STATE_SETTLING); } /** * Predict how far a fling with {@param yvel} will cause the view to travel from stand still. * @return predicted y offset */ public int predictFlingYOffset(int yvel) { mScroller.abortAnimation(); mScroller.fling(0, 0, 0, yvel, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE); final int finalY = mScroller.getFinalY(); mScroller.abortAnimation(); return finalY; } public int getCurrentScrollY() { return mScroller.getCurrY(); } /** * Move the captured settling view by the appropriate amount for the current time. * If <code>continueSettling</code> returns true, the caller should call it again Loading Loading @@ -750,6 +791,28 @@ public class ViewDragHelper { return mDragState == STATE_SETTLING; } public void processNestedFling(View target, int yvel) { mCapturedView = target; dispatchViewFling(0, yvel); } public int getVelocityMagnitude() { // Use Math.abs() to ensure this always returns an absolute value, even if the // ScrollerCompat implementation changes. return (int) Math.abs(mScroller.getCurrVelocity()); } private void dispatchViewFling(float xvel, float yvel) { mReleaseInProgress = true; mCallback.onViewFling(mCapturedView, xvel, yvel); mReleaseInProgress = false; if (mDragState == STATE_DRAGGING) { // onViewReleased didn't call a method that would have changed this. Go idle. setDragState(STATE_IDLE); } } /** * Like all callback events this must happen on the UI thread, but release * involves some extra semantics. During a release (mReleaseInProgress) Loading