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