1 /*
2  * Copyright (C) 2012 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.dialer.widget;
18 
19 import android.content.Context;
20 import android.content.res.TypedArray;
21 import android.graphics.Canvas;
22 import android.graphics.PixelFormat;
23 import android.graphics.Rect;
24 import android.graphics.drawable.Drawable;
25 import android.os.Build;
26 import android.os.Parcel;
27 import android.os.Parcelable;
28 import android.support.v4.view.AccessibilityDelegateCompat;
29 import android.support.v4.view.MotionEventCompat;
30 import android.support.v4.view.ViewCompat;
31 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
32 import android.util.AttributeSet;
33 import android.util.Log;
34 import android.view.MotionEvent;
35 import android.view.View;
36 import android.view.ViewConfiguration;
37 import android.view.ViewGroup;
38 import android.view.ViewParent;
39 import android.view.accessibility.AccessibilityEvent;
40 
41 /**
42  * A custom layout that aligns its child views vertically as two panes, and allows for the bottom
43  * pane to be dragged upwards to overlap and hide the top pane. This layout is adapted from
44  * {@link android.support.v4.widget.SlidingPaneLayout}.
45  */
46 public class OverlappingPaneLayout extends ViewGroup {
47     private static final String TAG = "SlidingPaneLayout";
48     private static final boolean DEBUG = false;
49 
50     /**
51      * Default size of the overhang for a pane in the open state.
52      * At least this much of a sliding pane will remain visible.
53      * This indicates that there is more content available and provides
54      * a "physical" edge to grab to pull it closed.
55      */
56     private static final int DEFAULT_OVERHANG_SIZE = 32; // dp;
57 
58     /**
59      * If no fade color is given by default it will fade to 80% gray.
60      */
61     private static final int DEFAULT_FADE_COLOR = 0xcccccccc;
62 
63     /**
64      * Minimum velocity that will be detected as a fling
65      */
66     private static final int MIN_FLING_VELOCITY = 400; // dips per second
67 
68     /**
69      * The size of the overhang in pixels.
70      * This is the minimum section of the sliding panel that will
71      * be visible in the open state to allow for a closing drag.
72      */
73     private final int mOverhangSize;
74 
75     /**
76      * True if a panel can slide with the current measurements
77      */
78     private boolean mCanSlide;
79 
80     /**
81      * The child view that can slide, if any.
82      */
83     private View mSlideableView;
84 
85     /**
86      * The view that can be used to start the drag with.
87      */
88     private View mCapturableView;
89 
90     /**
91      * How far the panel is offset from its closed position.
92      * range [0, 1] where 0 = closed, 1 = open.
93      */
94     private float mSlideOffset;
95 
96     /**
97      * How far the panel is offset from its closed position, in pixels.
98      * range [0, {@link #mSlideRange}] where 0 is completely closed.
99      */
100     private int mSlideOffsetPx;
101 
102     /**
103      * How far in pixels the slideable panel may move.
104      */
105     private int mSlideRange;
106 
107     /**
108      * A panel view is locked into internal scrolling or another condition that
109      * is preventing a drag.
110      */
111     private boolean mIsUnableToDrag;
112 
113     /**
114      * Tracks whether or not a child view is in the process of a nested scroll.
115      */
116     private boolean mIsInNestedScroll;
117 
118     /**
119      * Indicates that the layout is currently in the process of a nested pre-scroll operation where
120      * the child scrolling view is being dragged downwards.
121      */
122     private boolean mInNestedPreScrollDownwards;
123 
124     /**
125      * Indicates that the layout is currently in the process of a nested pre-scroll operation where
126      * the child scrolling view is being dragged upwards.
127      */
128     private boolean mInNestedPreScrollUpwards;
129 
130     /**
131      * Indicates that the layout is currently in the process of a fling initiated by a pre-fling
132      * from the child scrolling view.
133      */
134     private boolean mIsInNestedFling;
135 
136     /**
137      * Indicates the direction of the pre fling. We need to store this information since
138      * OverScoller doesn't expose the direction of its velocity.
139      */
140     private boolean mInUpwardsPreFling;
141 
142     /**
143      * Stores an offset used to represent a point somewhere in between the panel's fully closed
144      * state and fully opened state where the panel can be temporarily pinned or opened up to
145      * during scrolling.
146      */
147     private int mIntermediateOffset = 0;
148 
149     private float mInitialMotionX;
150     private float mInitialMotionY;
151 
152     private PanelSlideCallbacks mPanelSlideCallbacks;
153 
154     private final ViewDragHelper mDragHelper;
155 
156     /**
157      * Stores whether or not the pane was open the last time it was slideable.
158      * If open/close operations are invoked this state is modified. Used by
159      * instance state save/restore.
160      */
161     private boolean mPreservedOpenState;
162     private boolean mFirstLayout = true;
163 
164     private final Rect mTmpRect = new Rect();
165 
166     /**
167      * How many dips we need to scroll past a position before we can snap to the next position
168      * on release. Using this prevents accidentally snapping to positions.
169      *
170      * This is needed since vertical nested scrolling can be passed to this class even if the
171      * vertical scroll is less than the the nested list's touch slop.
172      */
173     private final int mReleaseScrollSlop;
174 
175     /**
176      * Callbacks for interacting with sliding panes.
177      */
178     public interface PanelSlideCallbacks {
179         /**
180          * Called when a sliding pane's position changes.
181          * @param panel The child view that was moved
182          * @param slideOffset The new offset of this sliding pane within its range, from 0-1
183          */
onPanelSlide(View panel, float slideOffset)184         public void onPanelSlide(View panel, float slideOffset);
185         /**
186          * Called when a sliding pane becomes slid completely open. The pane may or may not
187          * be interactive at this point depending on how much of the pane is visible.
188          * @param panel The child view that was slid to an open position, revealing other panes
189          */
onPanelOpened(View panel)190         public void onPanelOpened(View panel);
191 
192         /**
193          * Called when a sliding pane becomes slid completely closed. The pane is now guaranteed
194          * to be interactive. It may now obscure other views in the layout.
195          * @param panel The child view that was slid to a closed position
196          */
onPanelClosed(View panel)197         public void onPanelClosed(View panel);
198 
199         /**
200          * Called when a sliding pane is flung as far open/closed as it can be.
201          * @param velocityY Velocity of the panel once its fling goes as far as it can.
202          */
onPanelFlingReachesEdge(int velocityY)203         public void onPanelFlingReachesEdge(int velocityY);
204 
205         /**
206          * Returns true if the second panel's contents haven't been scrolled at all. This value is
207          * used to determine whether or not we can fully expand the header on downwards scrolls.
208          *
209          * Instead of using this callback, it would be preferable to instead fully expand the header
210          * on a View#onNestedFlingOver() callback. The behavior would be nicer. Unfortunately,
211          * no such callback exists yet (b/17547693).
212          */
isScrollableChildUnscrolled()213         public boolean isScrollableChildUnscrolled();
214     }
215 
OverlappingPaneLayout(Context context)216     public OverlappingPaneLayout(Context context) {
217         this(context, null);
218     }
219 
OverlappingPaneLayout(Context context, AttributeSet attrs)220     public OverlappingPaneLayout(Context context, AttributeSet attrs) {
221         this(context, attrs, 0);
222     }
223 
OverlappingPaneLayout(Context context, AttributeSet attrs, int defStyle)224     public OverlappingPaneLayout(Context context, AttributeSet attrs, int defStyle) {
225         super(context, attrs, defStyle);
226 
227         final float density = context.getResources().getDisplayMetrics().density;
228         mOverhangSize = (int) (DEFAULT_OVERHANG_SIZE * density + 0.5f);
229 
230         setWillNotDraw(false);
231 
232         ViewCompat.setAccessibilityDelegate(this, new AccessibilityDelegate());
233 
234         mDragHelper = ViewDragHelper.create(this, 0.5f, new DragHelperCallback());
235         mDragHelper.setMinVelocity(MIN_FLING_VELOCITY * density);
236 
237         mReleaseScrollSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
238     }
239 
240     /**
241      * Set an offset, somewhere in between the panel's fully closed state and fully opened state,
242      * where the panel can be temporarily pinned or opened up to.
243      *
244      * @param offset Offset in pixels
245      */
setIntermediatePinnedOffset(int offset)246     public void setIntermediatePinnedOffset(int offset) {
247         mIntermediateOffset = offset;
248     }
249 
250     /**
251      * Set the view that can be used to start dragging the sliding pane.
252      */
setCapturableView(View capturableView)253     public void setCapturableView(View capturableView) {
254         mCapturableView = capturableView;
255     }
256 
setPanelSlideCallbacks(PanelSlideCallbacks listener)257     public void setPanelSlideCallbacks(PanelSlideCallbacks listener) {
258         mPanelSlideCallbacks = listener;
259     }
260 
dispatchOnPanelSlide(View panel)261     void dispatchOnPanelSlide(View panel) {
262         mPanelSlideCallbacks.onPanelSlide(panel, mSlideOffset);
263     }
264 
dispatchOnPanelOpened(View panel)265     void dispatchOnPanelOpened(View panel) {
266         mPanelSlideCallbacks.onPanelOpened(panel);
267         sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
268     }
269 
dispatchOnPanelClosed(View panel)270     void dispatchOnPanelClosed(View panel) {
271         mPanelSlideCallbacks.onPanelClosed(panel);
272         sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
273     }
274 
updateObscuredViewsVisibility(View panel)275     void updateObscuredViewsVisibility(View panel) {
276         final int startBound = getPaddingTop();
277         final int endBound = getHeight() - getPaddingBottom();
278 
279         final int leftBound = getPaddingLeft();
280         final int rightBound = getWidth() - getPaddingRight();
281         final int left;
282         final int right;
283         final int top;
284         final int bottom;
285         if (panel != null && viewIsOpaque(panel)) {
286             left = panel.getLeft();
287             right = panel.getRight();
288             top = panel.getTop();
289             bottom = panel.getBottom();
290         } else {
291             left = right = top = bottom = 0;
292         }
293 
294         for (int i = 0, childCount = getChildCount(); i < childCount; i++) {
295             final View child = getChildAt(i);
296 
297             if (child == panel) {
298                 // There are still more children above the panel but they won't be affected.
299                 break;
300             }
301 
302             final int clampedChildLeft = Math.max(leftBound, child.getLeft());
303             final int clampedChildRight = Math.min(rightBound, child.getRight());
304             final int clampedChildTop = Math.max(startBound, child.getTop());
305             final int clampedChildBottom = Math.min(endBound, child.getBottom());
306 
307             final int vis;
308             if (clampedChildLeft >= left && clampedChildTop >= top &&
309                     clampedChildRight <= right && clampedChildBottom <= bottom) {
310                 vis = INVISIBLE;
311             } else {
312                 vis = VISIBLE;
313             }
314             child.setVisibility(vis);
315         }
316     }
317 
setAllChildrenVisible()318     void setAllChildrenVisible() {
319         for (int i = 0, childCount = getChildCount(); i < childCount; i++) {
320             final View child = getChildAt(i);
321             if (child.getVisibility() == INVISIBLE) {
322                 child.setVisibility(VISIBLE);
323             }
324         }
325     }
326 
viewIsOpaque(View v)327     private static boolean viewIsOpaque(View v) {
328         if (ViewCompat.isOpaque(v)) return true;
329 
330         final Drawable bg = v.getBackground();
331         if (bg != null) {
332             return bg.getOpacity() == PixelFormat.OPAQUE;
333         }
334         return false;
335     }
336 
337     @Override
onAttachedToWindow()338     protected void onAttachedToWindow() {
339         super.onAttachedToWindow();
340         mFirstLayout = true;
341     }
342 
343     @Override
onDetachedFromWindow()344     protected void onDetachedFromWindow() {
345         super.onDetachedFromWindow();
346         mFirstLayout = true;
347     }
348 
349     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)350     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
351 
352         int widthMode = MeasureSpec.getMode(widthMeasureSpec);
353         int widthSize = MeasureSpec.getSize(widthMeasureSpec);
354         int heightMode = MeasureSpec.getMode(heightMeasureSpec);
355         int heightSize = MeasureSpec.getSize(heightMeasureSpec);
356 
357         if (widthMode != MeasureSpec.EXACTLY) {
358             if (isInEditMode()) {
359                 // Don't crash the layout editor. Consume all of the space if specified
360                 // or pick a magic number from thin air otherwise.
361                 // TODO Better communication with tools of this bogus state.
362                 // It will crash on a real device.
363                 if (widthMode == MeasureSpec.AT_MOST) {
364                     widthMode = MeasureSpec.EXACTLY;
365                 } else if (widthMode == MeasureSpec.UNSPECIFIED) {
366                     widthMode = MeasureSpec.EXACTLY;
367                     widthSize = 300;
368                 }
369             } else {
370                 throw new IllegalStateException("Width must have an exact value or MATCH_PARENT");
371             }
372         } else if (heightMode == MeasureSpec.UNSPECIFIED) {
373             if (isInEditMode()) {
374                 // Don't crash the layout editor. Pick a magic number from thin air instead.
375                 // TODO Better communication with tools of this bogus state.
376                 // It will crash on a real device.
377                 if (heightMode == MeasureSpec.UNSPECIFIED) {
378                     heightMode = MeasureSpec.AT_MOST;
379                     heightSize = 300;
380                 }
381             } else {
382                 throw new IllegalStateException("Height must not be UNSPECIFIED");
383             }
384         }
385 
386         int layoutWidth = 0;
387         int maxLayoutWidth = -1;
388         switch (widthMode) {
389             case MeasureSpec.EXACTLY:
390                 layoutWidth = maxLayoutWidth = widthSize - getPaddingLeft() - getPaddingRight();
391                 break;
392             case MeasureSpec.AT_MOST:
393                 maxLayoutWidth = widthSize - getPaddingLeft() - getPaddingRight();
394                 break;
395         }
396 
397         float weightSum = 0;
398         boolean canSlide = false;
399         final int heightAvailable = heightSize - getPaddingTop() - getPaddingBottom();
400         int heightRemaining = heightAvailable;
401         final int childCount = getChildCount();
402 
403         if (childCount > 2) {
404             Log.e(TAG, "onMeasure: More than two child views are not supported.");
405         }
406 
407         // We'll find the current one below.
408         mSlideableView = null;
409 
410         // First pass. Measure based on child LayoutParams width/height.
411         // Weight will incur a second pass.
412         for (int i = 0; i < childCount; i++) {
413             final View child = getChildAt(i);
414             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
415 
416             if (child.getVisibility() == GONE) {
417                 continue;
418             }
419 
420             if (lp.weight > 0) {
421                 weightSum += lp.weight;
422 
423                 // If we have no height, weight is the only contributor to the final size.
424                 // Measure this view on the weight pass only.
425                 if (lp.height == 0) continue;
426             }
427 
428             int childHeightSpec;
429             final int verticalMargin = lp.topMargin + lp.bottomMargin;
430             if (lp.height == LayoutParams.WRAP_CONTENT) {
431                 childHeightSpec = MeasureSpec.makeMeasureSpec(heightAvailable - verticalMargin,
432                         MeasureSpec.AT_MOST);
433             } else if (lp.height == LayoutParams.MATCH_PARENT) {
434                 childHeightSpec = MeasureSpec.makeMeasureSpec(heightAvailable - verticalMargin,
435                         MeasureSpec.EXACTLY);
436             } else {
437                 childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
438             }
439 
440             int childWidthSpec;
441             if (lp.width == LayoutParams.WRAP_CONTENT) {
442                 childWidthSpec = MeasureSpec.makeMeasureSpec(maxLayoutWidth, MeasureSpec.AT_MOST);
443             } else if (lp.width == LayoutParams.MATCH_PARENT) {
444                 childWidthSpec = MeasureSpec.makeMeasureSpec(maxLayoutWidth, MeasureSpec.EXACTLY);
445             } else {
446                 childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
447             }
448 
449             child.measure(childWidthSpec, childHeightSpec);
450             final int childWidth = child.getMeasuredWidth();
451             final int childHeight = child.getMeasuredHeight();
452 
453             if (widthMode == MeasureSpec.AT_MOST && childWidth > layoutWidth) {
454                 layoutWidth = Math.min(childWidth, maxLayoutWidth);
455             }
456 
457             heightRemaining -= childHeight;
458             canSlide |= lp.slideable = heightRemaining < 0;
459             if (lp.slideable) {
460                 mSlideableView = child;
461             }
462         }
463 
464         // Resolve weight and make sure non-sliding panels are smaller than the full screen.
465         if (canSlide || weightSum > 0) {
466             final int fixedPanelHeightLimit = heightAvailable - mOverhangSize;
467 
468             for (int i = 0; i < childCount; i++) {
469                 final View child = getChildAt(i);
470 
471                 if (child.getVisibility() == GONE) {
472                     continue;
473                 }
474 
475                 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
476 
477                 if (child.getVisibility() == GONE) {
478                     continue;
479                 }
480 
481                 final boolean skippedFirstPass = lp.height == 0 && lp.weight > 0;
482                 final int measuredHeight = skippedFirstPass ? 0 : child.getMeasuredHeight();
483                 if (canSlide && child != mSlideableView) {
484                     if (lp.height < 0 && (measuredHeight > fixedPanelHeightLimit || lp.weight > 0)) {
485                         // Fixed panels in a sliding configuration should
486                         // be clamped to the fixed panel limit.
487                         final int childWidthSpec;
488                         if (skippedFirstPass) {
489                             // Do initial width measurement if we skipped measuring this view
490                             // the first time around.
491                             if (lp.width == LayoutParams.WRAP_CONTENT) {
492                                 childWidthSpec = MeasureSpec.makeMeasureSpec(maxLayoutWidth,
493                                         MeasureSpec.AT_MOST);
494                             } else if (lp.height == LayoutParams.MATCH_PARENT) {
495                                 childWidthSpec = MeasureSpec.makeMeasureSpec(maxLayoutWidth,
496                                         MeasureSpec.EXACTLY);
497                             } else {
498                                 childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width,
499                                         MeasureSpec.EXACTLY);
500                             }
501                         } else {
502                             childWidthSpec = MeasureSpec.makeMeasureSpec(
503                                     child.getMeasuredWidth(), MeasureSpec.EXACTLY);
504                         }
505                         final int childHeightSpec = MeasureSpec.makeMeasureSpec(
506                                 fixedPanelHeightLimit, MeasureSpec.EXACTLY);
507                         child.measure(childWidthSpec, childHeightSpec);
508                     }
509                 } else if (lp.weight > 0) {
510                     int childWidthSpec;
511                     if (lp.height == 0) {
512                         // This was skipped the first time; figure out a real width spec.
513                         if (lp.width == LayoutParams.WRAP_CONTENT) {
514                             childWidthSpec = MeasureSpec.makeMeasureSpec(maxLayoutWidth,
515                                     MeasureSpec.AT_MOST);
516                         } else if (lp.width == LayoutParams.MATCH_PARENT) {
517                             childWidthSpec = MeasureSpec.makeMeasureSpec(maxLayoutWidth,
518                                     MeasureSpec.EXACTLY);
519                         } else {
520                             childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width,
521                                     MeasureSpec.EXACTLY);
522                         }
523                     } else {
524                         childWidthSpec = MeasureSpec.makeMeasureSpec(
525                                 child.getMeasuredWidth(), MeasureSpec.EXACTLY);
526                     }
527 
528                     if (canSlide) {
529                         // Consume available space
530                         final int verticalMargin = lp.topMargin + lp.bottomMargin;
531                         final int newHeight = heightAvailable - verticalMargin;
532                         final int childHeightSpec = MeasureSpec.makeMeasureSpec(
533                                 newHeight, MeasureSpec.EXACTLY);
534                         if (measuredHeight != newHeight) {
535                             child.measure(childWidthSpec, childHeightSpec);
536                         }
537                     } else {
538                         // Distribute the extra width proportionally similar to LinearLayout
539                         final int heightToDistribute = Math.max(0, heightRemaining);
540                         final int addedHeight = (int) (lp.weight * heightToDistribute / weightSum);
541                         final int childHeightSpec = MeasureSpec.makeMeasureSpec(
542                                 measuredHeight + addedHeight, MeasureSpec.EXACTLY);
543                         child.measure(childWidthSpec, childHeightSpec);
544                     }
545                 }
546             }
547         }
548 
549         final int measuredHeight = heightSize;
550         final int measuredWidth = layoutWidth + getPaddingLeft() + getPaddingRight();
551 
552         setMeasuredDimension(measuredWidth, measuredHeight);
553         mCanSlide = canSlide;
554 
555         if (mDragHelper.getViewDragState() != ViewDragHelper.STATE_IDLE && !canSlide) {
556             // Cancel scrolling in progress, it's no longer relevant.
557             mDragHelper.abort();
558         }
559     }
560 
561     @Override
onLayout(boolean changed, int l, int t, int r, int b)562     protected void onLayout(boolean changed, int l, int t, int r, int b) {
563         mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_TOP);
564 
565         final int height = b - t;
566         final int paddingTop = getPaddingTop();
567         final int paddingBottom = getPaddingBottom();
568         final int paddingLeft = getPaddingLeft();
569 
570         final int childCount = getChildCount();
571         int yStart = paddingTop;
572         int nextYStart = yStart;
573 
574         if (mFirstLayout) {
575             mSlideOffset = mCanSlide && mPreservedOpenState ? 1.f : 0.f;
576         }
577 
578         for (int i = 0; i < childCount; i++) {
579             final View child = getChildAt(i);
580 
581             if (child.getVisibility() == GONE) {
582                 continue;
583             }
584 
585             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
586 
587             final int childHeight = child.getMeasuredHeight();
588 
589             if (lp.slideable) {
590                 final int margin = lp.topMargin + lp.bottomMargin;
591                 final int range = Math.min(nextYStart,
592                         height - paddingBottom - mOverhangSize) - yStart - margin;
593                 mSlideRange = range;
594                 final int lpMargin = lp.topMargin;
595                 final int pos = (int) (range * mSlideOffset);
596                 yStart += pos + lpMargin;
597                 updateSlideOffset(pos);
598             } else {
599                 yStart = nextYStart;
600             }
601 
602             final int childTop = yStart;
603             final int childBottom = childTop + childHeight;
604             final int childLeft = paddingLeft;
605             final int childRight = childLeft + child.getMeasuredWidth();
606 
607             child.layout(childLeft, childTop, childRight, childBottom);
608 
609             nextYStart += child.getHeight();
610         }
611 
612         if (mFirstLayout) {
613             updateObscuredViewsVisibility(mSlideableView);
614         }
615 
616         mFirstLayout = false;
617     }
618 
619     @Override
onSizeChanged(int w, int h, int oldw, int oldh)620     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
621         super.onSizeChanged(w, h, oldw, oldh);
622         // Recalculate sliding panes and their details
623         if (h != oldh) {
624             mFirstLayout = true;
625         }
626     }
627 
628     @Override
requestChildFocus(View child, View focused)629     public void requestChildFocus(View child, View focused) {
630         super.requestChildFocus(child, focused);
631         if (!isInTouchMode() && !mCanSlide) {
632             mPreservedOpenState = child == mSlideableView;
633         }
634     }
635 
636     @Override
onInterceptTouchEvent(MotionEvent ev)637     public boolean onInterceptTouchEvent(MotionEvent ev) {
638         final int action = MotionEventCompat.getActionMasked(ev);
639 
640         // Preserve the open state based on the last view that was touched.
641         if (!mCanSlide && action == MotionEvent.ACTION_DOWN && getChildCount() > 1) {
642             // After the first things will be slideable.
643             final View secondChild = getChildAt(1);
644             if (secondChild != null) {
645                 mPreservedOpenState = !mDragHelper.isViewUnder(secondChild,
646                         (int) ev.getX(), (int) ev.getY());
647             }
648         }
649 
650         if (!mCanSlide || (mIsUnableToDrag && action != MotionEvent.ACTION_DOWN)) {
651             if (!mIsInNestedScroll) {
652                 mDragHelper.cancel();
653             }
654             return super.onInterceptTouchEvent(ev);
655         }
656 
657         if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
658             if (!mIsInNestedScroll) {
659                 mDragHelper.cancel();
660             }
661             return false;
662         }
663 
664         switch (action) {
665             case MotionEvent.ACTION_DOWN: {
666                 mIsUnableToDrag = false;
667                 final float x = ev.getX();
668                 final float y = ev.getY();
669                 mInitialMotionX = x;
670                 mInitialMotionY = y;
671 
672                 break;
673             }
674 
675             case MotionEvent.ACTION_MOVE: {
676                 final float x = ev.getX();
677                 final float y = ev.getY();
678                 final float adx = Math.abs(x - mInitialMotionX);
679                 final float ady = Math.abs(y - mInitialMotionY);
680                 final int slop = mDragHelper.getTouchSlop();
681                 if (ady > slop && adx > ady || !isCapturableViewUnder((int) x, (int) y)) {
682                     if (!mIsInNestedScroll) {
683                         mDragHelper.cancel();
684                     }
685                     mIsUnableToDrag = true;
686                     return false;
687                 }
688             }
689         }
690 
691         final boolean interceptForDrag = mDragHelper.shouldInterceptTouchEvent(ev);
692 
693         return interceptForDrag;
694     }
695 
696     @Override
onTouchEvent(MotionEvent ev)697     public boolean onTouchEvent(MotionEvent ev) {
698         if (!mCanSlide) {
699             return super.onTouchEvent(ev);
700         }
701 
702         mDragHelper.processTouchEvent(ev);
703 
704         final int action = ev.getAction();
705         boolean wantTouchEvents = true;
706 
707         switch (action & MotionEventCompat.ACTION_MASK) {
708             case MotionEvent.ACTION_DOWN: {
709                 final float x = ev.getX();
710                 final float y = ev.getY();
711                 mInitialMotionX = x;
712                 mInitialMotionY = y;
713                 break;
714             }
715         }
716 
717         return wantTouchEvents;
718     }
719 
720     /**
721      * Refreshes the {@link OverlappingPaneLayout} be attempting to re-open or re-close the pane.
722      * This ensures that the overlapping pane is repositioned based on any changes to the view
723      * which is being overlapped.
724      * <p>
725      * The {@link #openPane()} and {@link #closePane()} methods do not perform any animation if the
726      * pane has already been positioned appropriately.
727      */
refresh()728     public void refresh() {
729         if (isOpen()) {
730             openPane();
731         } else {
732             closePane();
733         }
734     }
735 
closePane(View pane, int initialVelocity)736     private boolean closePane(View pane, int initialVelocity) {
737         if (mFirstLayout || smoothSlideTo(0.f, initialVelocity)) {
738             mPreservedOpenState = false;
739             return true;
740         }
741         return false;
742     }
743 
openPane(View pane, int initialVelocity)744     private boolean openPane(View pane, int initialVelocity) {
745         if (mFirstLayout || smoothSlideTo(1.f, initialVelocity)) {
746             mPreservedOpenState = true;
747             return true;
748         }
749         return false;
750     }
751 
updateSlideOffset(int offsetPx)752     private void updateSlideOffset(int offsetPx) {
753         mSlideOffsetPx = offsetPx;
754         mSlideOffset = (float) mSlideOffsetPx / mSlideRange;
755     }
756 
757     /**
758      * Open the sliding pane if it is currently slideable. If first layout
759      * has already completed this will animate.
760      *
761      * @return true if the pane was slideable and is now open/in the process of opening
762      */
openPane()763     public boolean openPane() {
764         return openPane(mSlideableView, 0);
765     }
766 
767     /**
768      * Close the sliding pane if it is currently slideable. If first layout
769      * has already completed this will animate.
770      *
771      * @return true if the pane was slideable and is now closed/in the process of closing
772      */
closePane()773     public boolean closePane() {
774         return closePane(mSlideableView, 0);
775     }
776 
777     /**
778      * Check if the layout is open. It can be open either because the slider
779      * itself is open revealing the left pane, or if all content fits without sliding.
780      *
781      * @return true if sliding panels are open
782      */
isOpen()783     public boolean isOpen() {
784         return !mCanSlide || mSlideOffset > 0;
785     }
786 
787     /**
788      * Check if the content in this layout cannot fully fit side by side and therefore
789      * the content pane can be slid back and forth.
790      *
791      * @return true if content in this layout can be slid open and closed
792      */
isSlideable()793     public boolean isSlideable() {
794         return mCanSlide;
795     }
796 
onPanelDragged(int newTop)797     private void onPanelDragged(int newTop) {
798         if (mSlideableView == null) {
799             // This can happen if we're aborting motion during layout because everything now fits.
800             mSlideOffset = 0;
801             return;
802         }
803         final LayoutParams lp = (LayoutParams) mSlideableView.getLayoutParams();
804 
805         final int lpMargin = lp.topMargin;
806         final int topBound = getPaddingTop() + lpMargin;
807 
808         updateSlideOffset(newTop - topBound);
809 
810         dispatchOnPanelSlide(mSlideableView);
811     }
812 
813     @Override
drawChild(Canvas canvas, View child, long drawingTime)814     protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
815         final LayoutParams lp = (LayoutParams) child.getLayoutParams();
816         boolean result;
817         final int save = canvas.save(Canvas.CLIP_SAVE_FLAG);
818 
819         if (mCanSlide && !lp.slideable && mSlideableView != null) {
820             // Clip against the slider; no sense drawing what will immediately be covered.
821             canvas.getClipBounds(mTmpRect);
822 
823             mTmpRect.bottom = Math.min(mTmpRect.bottom, mSlideableView.getTop());
824             canvas.clipRect(mTmpRect);
825         }
826 
827         if (Build.VERSION.SDK_INT >= 11) { // HC
828             result = super.drawChild(canvas, child, drawingTime);
829         } else {
830             if (child.isDrawingCacheEnabled()) {
831                 child.setDrawingCacheEnabled(false);
832             }
833             result = super.drawChild(canvas, child, drawingTime);
834         }
835 
836         canvas.restoreToCount(save);
837 
838         return result;
839     }
840 
841     /**
842      * Smoothly animate mDraggingPane to the target X position within its range.
843      *
844      * @param slideOffset position to animate to
845      * @param velocity initial velocity in case of fling, or 0.
846      */
smoothSlideTo(float slideOffset, int velocity)847     boolean smoothSlideTo(float slideOffset, int velocity) {
848         if (!mCanSlide) {
849             // Nothing to do.
850             return false;
851         }
852 
853         final LayoutParams lp = (LayoutParams) mSlideableView.getLayoutParams();
854 
855         int y;
856         int topBound = getPaddingTop() + lp.topMargin;
857         y = (int) (topBound + slideOffset * mSlideRange);
858 
859         if (mDragHelper.smoothSlideViewTo(mSlideableView, mSlideableView.getLeft(), y)) {
860             setAllChildrenVisible();
861             ViewCompat.postInvalidateOnAnimation(this);
862             return true;
863         }
864         return false;
865     }
866 
867     @Override
computeScroll()868     public void computeScroll() {
869         if (mDragHelper.continueSettling(/* deferCallbacks = */ false)) {
870             if (!mCanSlide) {
871                 mDragHelper.abort();
872                 return;
873             }
874 
875             ViewCompat.postInvalidateOnAnimation(this);
876         }
877     }
878 
isCapturableViewUnder(int x, int y)879     private boolean isCapturableViewUnder(int x, int y) {
880         View capturableView = mCapturableView != null ? mCapturableView : mSlideableView;
881         if (capturableView == null) {
882             return false;
883         }
884         int[] viewLocation = new int[2];
885         capturableView.getLocationOnScreen(viewLocation);
886         int[] parentLocation = new int[2];
887         this.getLocationOnScreen(parentLocation);
888         int screenX = parentLocation[0] + x;
889         int screenY = parentLocation[1] + y;
890         return screenX >= viewLocation[0]
891                 && screenX < viewLocation[0] + capturableView.getWidth()
892                 && screenY >= viewLocation[1]
893                 && screenY < viewLocation[1] + capturableView.getHeight();
894     }
895 
896     @Override
generateDefaultLayoutParams()897     protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
898         return new LayoutParams();
899     }
900 
901     @Override
generateLayoutParams(ViewGroup.LayoutParams p)902     protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
903         return p instanceof MarginLayoutParams
904                 ? new LayoutParams((MarginLayoutParams) p)
905                 : new LayoutParams(p);
906     }
907 
908     @Override
checkLayoutParams(ViewGroup.LayoutParams p)909     protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
910         return p instanceof LayoutParams && super.checkLayoutParams(p);
911     }
912 
913     @Override
generateLayoutParams(AttributeSet attrs)914     public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
915         return new LayoutParams(getContext(), attrs);
916     }
917 
918     @Override
onSaveInstanceState()919     protected Parcelable onSaveInstanceState() {
920         Parcelable superState = super.onSaveInstanceState();
921 
922         SavedState ss = new SavedState(superState);
923         ss.isOpen = isSlideable() ? isOpen() : mPreservedOpenState;
924 
925         return ss;
926     }
927 
928     @Override
onRestoreInstanceState(Parcelable state)929     protected void onRestoreInstanceState(Parcelable state) {
930         SavedState ss = (SavedState) state;
931         super.onRestoreInstanceState(ss.getSuperState());
932 
933         if (ss.isOpen) {
934             openPane();
935         } else {
936             closePane();
937         }
938         mPreservedOpenState = ss.isOpen;
939     }
940 
941     @Override
onStartNestedScroll(View child, View target, int nestedScrollAxes)942     public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
943         final boolean startNestedScroll = (nestedScrollAxes & SCROLL_AXIS_VERTICAL) != 0;
944         if (startNestedScroll) {
945             mIsInNestedScroll = true;
946             mDragHelper.startNestedScroll(mSlideableView);
947         }
948         if (DEBUG) {
949             Log.d(TAG, "onStartNestedScroll: " + startNestedScroll);
950         }
951         return startNestedScroll;
952     }
953 
954     @Override
onNestedPreScroll(View target, int dx, int dy, int[] consumed)955     public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
956         if (dy == 0) {
957             // Nothing to do
958             return;
959         }
960         if (DEBUG) {
961             Log.d(TAG, "onNestedPreScroll: " + dy);
962         }
963 
964         mInNestedPreScrollDownwards = dy < 0;
965         mInNestedPreScrollUpwards = dy > 0;
966         mIsInNestedFling = false;
967         mDragHelper.processNestedScroll(mSlideableView, 0, -dy, consumed);
968     }
969 
970     @Override
onNestedPreFling(View target, float velocityX, float velocityY)971     public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
972         if (!(velocityY > 0 && mSlideOffsetPx != 0
973                 || velocityY < 0 && mSlideOffsetPx < mIntermediateOffset
974                 || velocityY < 0 && mSlideOffsetPx < mSlideRange
975                 && mPanelSlideCallbacks.isScrollableChildUnscrolled())) {
976             // No need to consume the fling if the fling won't collapse or expand the header.
977             // How far we are willing to expand the header depends on isScrollableChildUnscrolled().
978             return false;
979         }
980 
981         if (DEBUG) {
982             Log.d(TAG, "onNestedPreFling: " + velocityY);
983         }
984         mInUpwardsPreFling = velocityY > 0;
985         mIsInNestedFling = true;
986         mIsInNestedScroll = false;
987         mDragHelper.processNestedFling(mSlideableView, (int) -velocityY);
988         return true;
989     }
990 
991     @Override
onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed)992     public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
993             int dyUnconsumed) {
994         if (DEBUG) {
995             Log.d(TAG, "onNestedScroll: " + dyUnconsumed);
996         }
997         mIsInNestedFling = false;
998         mDragHelper.processNestedScroll(mSlideableView, 0, -dyUnconsumed, null);
999     }
1000 
1001     @Override
onStopNestedScroll(View child)1002     public void onStopNestedScroll(View child) {
1003         if (DEBUG) {
1004             Log.d(TAG, "onStopNestedScroll");
1005         }
1006         if (mIsInNestedScroll && !mIsInNestedFling) {
1007             mDragHelper.stopNestedScroll(mSlideableView);
1008             mInNestedPreScrollDownwards = false;
1009             mInNestedPreScrollUpwards = false;
1010             mIsInNestedScroll = false;
1011         }
1012     }
1013 
1014     private class DragHelperCallback extends ViewDragHelper.Callback {
1015 
1016         @Override
tryCaptureView(View child, int pointerId)1017         public boolean tryCaptureView(View child, int pointerId) {
1018             if (mIsUnableToDrag) {
1019                 return false;
1020             }
1021 
1022             return ((LayoutParams) child.getLayoutParams()).slideable;
1023         }
1024 
1025         @Override
onViewDragStateChanged(int state)1026         public void onViewDragStateChanged(int state) {
1027             if (DEBUG) {
1028                 Log.d(TAG, "onViewDragStateChanged: " + state);
1029             }
1030 
1031             if (mDragHelper.getViewDragState() == ViewDragHelper.STATE_IDLE) {
1032                 if (mSlideOffset == 0) {
1033                     updateObscuredViewsVisibility(mSlideableView);
1034                     dispatchOnPanelClosed(mSlideableView);
1035                     mPreservedOpenState = false;
1036                 } else {
1037                     dispatchOnPanelOpened(mSlideableView);
1038                     mPreservedOpenState = true;
1039                 }
1040             }
1041 
1042             if (state == ViewDragHelper.STATE_IDLE
1043                     && mDragHelper.getVelocityMagnitude() > 0
1044                     && mIsInNestedFling) {
1045                 mIsInNestedFling = false;
1046                 final int flingVelocity = !mInUpwardsPreFling ?
1047                         -mDragHelper.getVelocityMagnitude() : mDragHelper.getVelocityMagnitude();
1048                 mPanelSlideCallbacks.onPanelFlingReachesEdge(flingVelocity);
1049             }
1050         }
1051 
1052         @Override
onViewCaptured(View capturedChild, int activePointerId)1053         public void onViewCaptured(View capturedChild, int activePointerId) {
1054             // Make all child views visible in preparation for sliding things around
1055             setAllChildrenVisible();
1056         }
1057 
1058         @Override
onViewPositionChanged(View changedView, int left, int top, int dx, int dy)1059         public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
1060             onPanelDragged(top);
1061             invalidate();
1062         }
1063 
1064         @Override
onViewFling(View releasedChild, float xVelocity, float yVelocity)1065         public void onViewFling(View releasedChild, float xVelocity, float yVelocity) {
1066             if (releasedChild == null) {
1067                 return;
1068             }
1069             if (DEBUG) {
1070                 Log.d(TAG, "onViewFling: " + yVelocity);
1071             }
1072 
1073             // Flings won't always fully expand or collapse the header. Instead of performing the
1074             // fling and then waiting for the fling to end before snapping into place, we
1075             // immediately snap into place if we predict the fling won't fully expand or collapse
1076             // the header.
1077             int yOffsetPx = mDragHelper.predictFlingYOffset((int) yVelocity);
1078             if (yVelocity < 0) {
1079                 // Only perform a fling if we know the fling will fully compress the header.
1080                 if (-yOffsetPx > mSlideOffsetPx) {
1081                     mDragHelper.flingCapturedView(releasedChild.getLeft(), /* minTop = */ 0,
1082                             mSlideRange, Integer.MAX_VALUE, (int) yVelocity);
1083                 } else {
1084                     mIsInNestedFling = false;
1085                     onViewReleased(releasedChild, xVelocity, yVelocity);
1086                 }
1087             } else {
1088                 // Only perform a fling if we know the fling will expand the header as far
1089                 // as it can possible be expanded, given the isScrollableChildUnscrolled() value.
1090                 if (yOffsetPx + mSlideOffsetPx >= mSlideRange
1091                         && mPanelSlideCallbacks.isScrollableChildUnscrolled()) {
1092                     mDragHelper.flingCapturedView(releasedChild.getLeft(), /* minTop = */ 0,
1093                             Integer.MAX_VALUE, mSlideRange, (int) yVelocity);
1094                 } else if (yOffsetPx + mSlideOffsetPx >= mIntermediateOffset
1095                         && mSlideOffsetPx <= mIntermediateOffset
1096                         && !mPanelSlideCallbacks.isScrollableChildUnscrolled()) {
1097                     mDragHelper.flingCapturedView(releasedChild.getLeft(), /* minTop = */ 0,
1098                             Integer.MAX_VALUE, mIntermediateOffset, (int) yVelocity);
1099                 } else {
1100                     mIsInNestedFling = false;
1101                     onViewReleased(releasedChild, xVelocity, yVelocity);
1102                 }
1103             }
1104 
1105             mInNestedPreScrollDownwards = false;
1106             mInNestedPreScrollUpwards = false;
1107 
1108             // Without this invalidate, some calls to flingCapturedView can have no affect.
1109             invalidate();
1110         }
1111 
1112         @Override
onViewReleased(View releasedChild, float xvel, float yvel)1113         public void onViewReleased(View releasedChild, float xvel, float yvel) {
1114             if (DEBUG) {
1115                 Log.d(TAG, "onViewReleased: "
1116                         + " mIsInNestedFling=" + mIsInNestedFling
1117                         + " unscrolled=" + mPanelSlideCallbacks.isScrollableChildUnscrolled()
1118                         + ", mInNestedPreScrollDownwards = " + mInNestedPreScrollDownwards
1119                         + ", mInNestedPreScrollUpwards = " + mInNestedPreScrollUpwards
1120                         + ", yvel=" + yvel);
1121             }
1122             if (releasedChild == null) {
1123                 return;
1124             }
1125 
1126             final LayoutParams lp = (LayoutParams) releasedChild.getLayoutParams();
1127             int top = getPaddingTop() + lp.topMargin;
1128 
1129             // Decide where to snap to according to the current direction of motion and the current
1130             // position. The velocity's magnitude has no bearing on this.
1131             if (mInNestedPreScrollDownwards || yvel > 0) {
1132                 // Scrolling downwards
1133                 if (mSlideOffsetPx > mIntermediateOffset + mReleaseScrollSlop) {
1134                     top += mSlideRange;
1135                 } else if (mSlideOffsetPx > mReleaseScrollSlop) {
1136                     top += mIntermediateOffset;
1137                 } else {
1138                     // Offset is very close to 0
1139                 }
1140             } else if (mInNestedPreScrollUpwards || yvel < 0) {
1141                 // Scrolling upwards
1142                 if (mSlideOffsetPx > mSlideRange - mReleaseScrollSlop) {
1143                     // Offset is very close to mSlideRange
1144                     top += mSlideRange;
1145                 } else if (mSlideOffsetPx > mIntermediateOffset - mReleaseScrollSlop) {
1146                     // Offset is between mIntermediateOffset and mSlideRange.
1147                     top += mIntermediateOffset;
1148                 } else {
1149                     // Offset is between 0 and mIntermediateOffset.
1150                 }
1151             } else {
1152                 // Not moving upwards or downwards. This case can only be triggered when directly
1153                 // dragging the tabs. We don't bother to remember previous scroll direction
1154                 // when directly dragging the tabs.
1155                 if (0 <= mSlideOffsetPx && mSlideOffsetPx <= mIntermediateOffset / 2) {
1156                     // Offset is between 0 and mIntermediateOffset, but closer to 0
1157                     // Leave top unchanged
1158                 } else if (mIntermediateOffset / 2 <= mSlideOffsetPx
1159                         && mSlideOffsetPx <= (mIntermediateOffset + mSlideRange) / 2) {
1160                     // Offset is closest to mIntermediateOffset
1161                     top += mIntermediateOffset;
1162                 } else {
1163                     // Offset is between mIntermediateOffset and mSlideRange, but closer to
1164                     // mSlideRange
1165                     top += mSlideRange;
1166                 }
1167             }
1168 
1169             mDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top);
1170             invalidate();
1171         }
1172 
1173         @Override
getViewVerticalDragRange(View child)1174         public int getViewVerticalDragRange(View child) {
1175             return mSlideRange;
1176         }
1177 
1178         @Override
clampViewPositionHorizontal(View child, int left, int dx)1179         public int clampViewPositionHorizontal(View child, int left, int dx) {
1180             // Make sure we never move views horizontally.
1181             return child.getLeft();
1182         }
1183 
1184         @Override
clampViewPositionVertical(View child, int top, int dy)1185         public int clampViewPositionVertical(View child, int top, int dy) {
1186             final LayoutParams lp = (LayoutParams) mSlideableView.getLayoutParams();
1187 
1188             final int newTop;
1189             int previousTop = top - dy;
1190             int topBound = getPaddingTop() + lp.topMargin;
1191             int bottomBound = topBound + (mPanelSlideCallbacks.isScrollableChildUnscrolled()
1192                     || !mIsInNestedScroll ? mSlideRange : mIntermediateOffset);
1193             if (previousTop > bottomBound) {
1194                 // We were previously below the bottomBound, so loosen the bottomBound so that this
1195                 // makes sense. This can occur after the view was directly dragged by the tabs.
1196                 bottomBound = Math.max(bottomBound, mSlideRange);
1197             }
1198             newTop = Math.min(Math.max(top, topBound), bottomBound);
1199 
1200             return newTop;
1201         }
1202 
1203         @Override
onEdgeDragStarted(int edgeFlags, int pointerId)1204         public void onEdgeDragStarted(int edgeFlags, int pointerId) {
1205             mDragHelper.captureChildView(mSlideableView, pointerId);
1206         }
1207     }
1208 
1209     public static class LayoutParams extends ViewGroup.MarginLayoutParams {
1210         private static final int[] ATTRS = new int[] {
1211             android.R.attr.layout_weight
1212         };
1213 
1214         /**
1215          * The weighted proportion of how much of the leftover space
1216          * this child should consume after measurement.
1217          */
1218         public float weight = 0;
1219 
1220         /**
1221          * True if this pane is the slideable pane in the layout.
1222          */
1223         boolean slideable;
1224 
LayoutParams()1225         public LayoutParams() {
1226             super(FILL_PARENT, FILL_PARENT);
1227         }
1228 
LayoutParams(int width, int height)1229         public LayoutParams(int width, int height) {
1230             super(width, height);
1231         }
1232 
LayoutParams(android.view.ViewGroup.LayoutParams source)1233         public LayoutParams(android.view.ViewGroup.LayoutParams source) {
1234             super(source);
1235         }
1236 
LayoutParams(MarginLayoutParams source)1237         public LayoutParams(MarginLayoutParams source) {
1238             super(source);
1239         }
1240 
LayoutParams(LayoutParams source)1241         public LayoutParams(LayoutParams source) {
1242             super(source);
1243             this.weight = source.weight;
1244         }
1245 
LayoutParams(Context c, AttributeSet attrs)1246         public LayoutParams(Context c, AttributeSet attrs) {
1247             super(c, attrs);
1248 
1249             final TypedArray a = c.obtainStyledAttributes(attrs, ATTRS);
1250             this.weight = a.getFloat(0, 0);
1251             a.recycle();
1252         }
1253 
1254     }
1255 
1256     static class SavedState extends BaseSavedState {
1257         boolean isOpen;
1258 
SavedState(Parcelable superState)1259         SavedState(Parcelable superState) {
1260             super(superState);
1261         }
1262 
SavedState(Parcel in)1263         private SavedState(Parcel in) {
1264             super(in);
1265             isOpen = in.readInt() != 0;
1266         }
1267 
1268         @Override
writeToParcel(Parcel out, int flags)1269         public void writeToParcel(Parcel out, int flags) {
1270             super.writeToParcel(out, flags);
1271             out.writeInt(isOpen ? 1 : 0);
1272         }
1273 
1274         public static final Parcelable.Creator<SavedState> CREATOR =
1275                 new Parcelable.Creator<SavedState>() {
1276             public SavedState createFromParcel(Parcel in) {
1277                 return new SavedState(in);
1278             }
1279 
1280             public SavedState[] newArray(int size) {
1281                 return new SavedState[size];
1282             }
1283         };
1284     }
1285 
1286     class AccessibilityDelegate extends AccessibilityDelegateCompat {
1287         private final Rect mTmpRect = new Rect();
1288 
1289         @Override
onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info)1290         public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
1291             final AccessibilityNodeInfoCompat superNode = AccessibilityNodeInfoCompat.obtain(info);
1292             super.onInitializeAccessibilityNodeInfo(host, superNode);
1293             copyNodeInfoNoChildren(info, superNode);
1294             superNode.recycle();
1295 
1296             info.setClassName(OverlappingPaneLayout.class.getName());
1297             info.setSource(host);
1298 
1299             final ViewParent parent = ViewCompat.getParentForAccessibility(host);
1300             if (parent instanceof View) {
1301                 info.setParent((View) parent);
1302             }
1303 
1304             // This is a best-approximation of addChildrenForAccessibility()
1305             // that accounts for filtering.
1306             final int childCount = getChildCount();
1307             for (int i = 0; i < childCount; i++) {
1308                 final View child = getChildAt(i);
1309                 if (child.getVisibility() == View.VISIBLE) {
1310                     // Force importance to "yes" since we can't read the value.
1311                     ViewCompat.setImportantForAccessibility(
1312                             child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
1313                     info.addChild(child);
1314                 }
1315             }
1316         }
1317 
1318         @Override
onInitializeAccessibilityEvent(View host, AccessibilityEvent event)1319         public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
1320             super.onInitializeAccessibilityEvent(host, event);
1321 
1322             event.setClassName(OverlappingPaneLayout.class.getName());
1323         }
1324 
1325         /**
1326          * This should really be in AccessibilityNodeInfoCompat, but there unfortunately
1327          * seem to be a few elements that are not easily cloneable using the underlying API.
1328          * Leave it private here as it's not general-purpose useful.
1329          */
copyNodeInfoNoChildren(AccessibilityNodeInfoCompat dest, AccessibilityNodeInfoCompat src)1330         private void copyNodeInfoNoChildren(AccessibilityNodeInfoCompat dest,
1331                 AccessibilityNodeInfoCompat src) {
1332             final Rect rect = mTmpRect;
1333 
1334             src.getBoundsInParent(rect);
1335             dest.setBoundsInParent(rect);
1336 
1337             src.getBoundsInScreen(rect);
1338             dest.setBoundsInScreen(rect);
1339 
1340             dest.setVisibleToUser(src.isVisibleToUser());
1341             dest.setPackageName(src.getPackageName());
1342             dest.setClassName(src.getClassName());
1343             dest.setContentDescription(src.getContentDescription());
1344 
1345             dest.setEnabled(src.isEnabled());
1346             dest.setClickable(src.isClickable());
1347             dest.setFocusable(src.isFocusable());
1348             dest.setFocused(src.isFocused());
1349             dest.setAccessibilityFocused(src.isAccessibilityFocused());
1350             dest.setSelected(src.isSelected());
1351             dest.setLongClickable(src.isLongClickable());
1352 
1353             dest.addAction(src.getActions());
1354 
1355             dest.setMovementGranularities(src.getMovementGranularities());
1356         }
1357     }
1358 }
1359