blob: c4cb3343f5c6a26e9e66d88cba13d12139f09dfc [file] [log] [blame]
Alexandre Lisiona8b78722013-12-13 10:18:33 -05001/*
Alexandre Lisionc1024c02014-01-06 11:12:53 -05002 * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
Alexandre Lisiona8b78722013-12-13 10:18:33 -05003 *
4 * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
19 *
20 * Additional permission under GNU GPL version 3 section 7:
21 *
22 * If you modify this program, or any covered work, by linking or
23 * combining it with the OpenSSL project's OpenSSL library (or a
24 * modified version of that library), containing parts covered by the
25 * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
26 * grants you additional permission to convey the resulting work.
27 * Corresponding Source for a non-source form of such a combination
28 * shall include the source code for the parts of OpenSSL used as well
29 * as that of the covered work.
30 */
31
Alexandre Lision450458a2013-11-22 11:33:12 -050032package org.sflphone.views;
33
34import android.content.Context;
35import android.content.res.TypedArray;
36import android.graphics.Canvas;
37import android.graphics.Paint;
38import android.graphics.PixelFormat;
39import android.graphics.Rect;
40import android.graphics.drawable.Drawable;
41import android.os.Parcel;
42import android.os.Parcelable;
43import android.support.v4.view.MotionEventCompat;
44import android.support.v4.view.ViewCompat;
45import android.support.v4.widget.ViewDragHelper;
46import android.util.AttributeSet;
47import android.util.Log;
48import android.view.MotionEvent;
49import android.view.SoundEffectConstants;
50import android.view.View;
51import android.view.ViewConfiguration;
52import android.view.ViewGroup;
53import android.view.accessibility.AccessibilityEvent;
54
55public class SlidingUpPanelLayout extends ViewGroup {
56
57 private static final String TAG = SlidingUpPanelLayout.class.getSimpleName();
58
59 /**
60 * Default peeking out panel height
61 */
62 private static final int DEFAULT_PANEL_HEIGHT = 68; // dp;
63
64 /**
65 * Default height of the shadow above the peeking out panel
66 */
67 private static final int DEFAULT_SHADOW_HEIGHT = 4; // dp;
68
69 /**
70 * If no fade color is given by default it will fade to 80% gray.
71 */
72 private static final int DEFAULT_FADE_COLOR = 0x99000000;
73
74 /**
75 * Minimum velocity that will be detected as a fling
76 */
77 private static final int MIN_FLING_VELOCITY = 400; // dips per second
78
79 /**
80 * The fade color used for the panel covered by the slider. 0 = no fading.
81 */
82 private int mCoveredFadeColor = DEFAULT_FADE_COLOR;
83
84 /**
85 * The paint used to dim the main layout when sliding
86 */
87 private final Paint mCoveredFadePaint = new Paint();
88
89 /**
90 * Drawable used to draw the shadow between panes.
91 */
92 private Drawable mShadowDrawable;
93
94 /**
95 * The size of the overhang in pixels.
96 */
97 private int mPanelHeight;
98
99 /**
100 * The size of the shadow in pixels.
101 */
102 private final int mShadowHeight;
103
104 /**
105 * True if a panel can slide with the current measurements
106 */
107 private boolean mCanSlide;
108
109 /**
110 * If provided, the panel can be dragged by only this view. Otherwise, the entire panel can be used for dragging.
111 */
112 private View mDragView;
113
114 /**
115 * The child view that can slide, if any.
116 */
117 private View mSlideableView;
118
119 /**
120 * How far the panel is offset from its expanded position. range [0, 1] where 0 = expanded, 1 = collapsed.
121 */
122 private float mSlideOffset;
123
124 /**
125 * How far in pixels the slideable panel may move.
126 */
127 private int mSlideRange;
128
129 /**
130 * A panel view is locked into internal scrolling or another condition that is preventing a drag.
131 */
132 private boolean mIsUnableToDrag;
133
134 /**
135 * Flag indicating that sliding feature is enabled\disabled
136 */
137 private boolean mIsSlidingEnabled;
138
139 /**
140 * Flag indicating if a drag view can have its own touch events. If set to true, a drag view can scroll horizontally and have its own click
141 * listener.
142 *
143 * Default is set to false.
144 */
145 private boolean mIsUsingDragViewTouchEvents;
146
147 /**
148 * Threshold to tell if there was a scroll touch event.
149 */
150 private int mScrollTouchSlop;
151
152 private float mInitialMotionX;
153 private float mInitialMotionY;
154 private boolean mDragViewHit;
155 private float mAnchorPoint = 0.f;
156
157 private PanelSlideListener mPanelSlideListener;
158
159 private final ViewDragHelper mDragHelper;
160
161 /**
162 * Stores whether or not the pane was expanded the last time it was slideable. If expand/collapse operations are invoked this state is modified.
163 * Used by instance state save/restore.
164 */
165 private boolean mPreservedExpandedState;
166 private boolean mFirstLayout = true;
167
168 private final Rect mTmpRect = new Rect();
169
170 /**
171 * Listener for monitoring events about sliding panes.
172 */
173 public interface PanelSlideListener {
174 /**
175 * Called when a sliding pane's position changes.
176 *
177 * @param panel
178 * The child view that was moved
179 * @param slideOffset
180 * The new offset of this sliding pane within its range, from 0-1
181 */
182 public void onPanelSlide(View panel, float slideOffset);
183
184 /**
185 * Called when a sliding pane becomes slid completely collapsed. The pane may or may not be interactive at this point depending on if it's
186 * shown or hidden
187 *
188 * @param panel
189 * The child view that was slid to an collapsed position, revealing other panes
190 */
191 public void onPanelCollapsed(View panel);
192
193 /**
194 * Called when a sliding pane becomes slid completely expanded. The pane is now guaranteed to be interactive. It may now obscure other views
195 * in the layout.
196 *
197 * @param panel
198 * The child view that was slid to a expanded position
199 */
200 public void onPanelExpanded(View panel);
201
202 public void onPanelAnchored(View panel);
203 }
204
205 /**
206 * No-op stubs for {@link PanelSlideListener}. If you only want to implement a subset of the listener methods you can extend this instead of
207 * implement the full interface.
208 */
209 public static class SimplePanelSlideListener implements PanelSlideListener {
210 @Override
211 public void onPanelSlide(View panel, float slideOffset) {
212 }
213
214 @Override
215 public void onPanelCollapsed(View panel) {
216 }
217
218 @Override
219 public void onPanelExpanded(View panel) {
220 }
221
222 @Override
223 public void onPanelAnchored(View panel) {
224 }
225 }
226
227 public SlidingUpPanelLayout(Context context) {
228 this(context, null);
229 }
230
231 public SlidingUpPanelLayout(Context context, AttributeSet attrs) {
232 this(context, attrs, 0);
233 }
234
235 public SlidingUpPanelLayout(Context context, AttributeSet attrs, int defStyle) {
236 super(context, attrs, defStyle);
237
238 final float density = context.getResources().getDisplayMetrics().density;
239 mPanelHeight = (int) (DEFAULT_PANEL_HEIGHT * density + 0.5f);
240 mShadowHeight = (int) (DEFAULT_SHADOW_HEIGHT * density + 0.5f);
241
242 setWillNotDraw(false);
243
244 mDragHelper = ViewDragHelper.create(this, 0.5f, new DragHelperCallback());
245 mDragHelper.setMinVelocity(MIN_FLING_VELOCITY * density);
246
247 mCanSlide = true;
248 mIsSlidingEnabled = true;
249
250 setCoveredFadeColor(DEFAULT_FADE_COLOR);
251
252 ViewConfiguration vc = ViewConfiguration.get(context);
253 mScrollTouchSlop = vc.getScaledTouchSlop();
254 }
255
256 /**
257 * Set the color used to fade the pane covered by the sliding pane out when the pane will become fully covered in the expanded state.
258 *
259 * @param color
260 * An ARGB-packed color value
261 */
262 public void setCoveredFadeColor(int color) {
263 mCoveredFadeColor = color;
264 invalidate();
265 }
266
267 /**
268 * @return The ARGB-packed color value used to fade the fixed pane
269 */
270 public int getCoveredFadeColor() {
271 return mCoveredFadeColor;
272 }
273
274 /**
275 * Set the collapsed panel height in pixels
276 *
277 * @param val
278 * A height in pixels
279 */
280 public void setPanelHeight(int val) {
281 mPanelHeight = val;
282 requestLayout();
283 }
284
285 /**
286 * @return The current collapsed panel height
287 */
288 public int getPanelHeight() {
289 return mPanelHeight;
290 }
291
292 public void setPanelSlideListener(PanelSlideListener listener) {
293 mPanelSlideListener = listener;
294 }
295
296 /**
297 * Set the draggable view portion. Use to null, to allow the whole panel to be draggable
298 *
299 * @param dragView
300 * A view that will be used to drag the panel.
301 */
302 public void setDragView(View dragView) {
303 mDragView = dragView;
304 }
305
306 /**
307 * Set an anchor point where the panel can stop during sliding
308 *
309 * @param anchorPoint
310 * A value between 0 and 1, determining the position of the anchor point starting from the top of the layout.
311 */
312 public void setAnchorPoint(float anchorPoint) {
313 if (anchorPoint > 0 && anchorPoint < 1)
314 mAnchorPoint = anchorPoint;
315 }
316
317 /**
318 * Set the shadow for the sliding panel
319 *
320 */
321 public void setShadowDrawable(Drawable drawable) {
322 mShadowDrawable = drawable;
323 }
324
325 void dispatchOnPanelSlide(View panel) {
326 if (mPanelSlideListener != null) {
327 mPanelSlideListener.onPanelSlide(panel, mSlideOffset);
328 }
329 }
330
331 void dispatchOnPanelExpanded(View panel) {
332 if (mPanelSlideListener != null) {
333 mPanelSlideListener.onPanelExpanded(panel);
334 }
335 sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
336 }
337
338 void dispatchOnPanelCollapsed(View panel) {
339 if (mPanelSlideListener != null) {
340 mPanelSlideListener.onPanelCollapsed(panel);
341 }
342 sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
343 }
344
345 void dispatchOnPanelAnchored(View panel) {
346 if (mPanelSlideListener != null) {
347 mPanelSlideListener.onPanelAnchored(panel);
348 }
349 sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
350 }
351
352 void updateObscuredViewVisibility() {
353 if (getChildCount() == 0) {
354 return;
355 }
356 final int leftBound = getPaddingLeft();
357 final int rightBound = getWidth() - getPaddingRight();
358 final int topBound = getPaddingTop();
359 final int bottomBound = getHeight() - getPaddingBottom();
360 final int left;
361 final int right;
362 final int top;
363 final int bottom;
364 if (mSlideableView != null && hasOpaqueBackground(mSlideableView)) {
365 left = mSlideableView.getLeft();
366 right = mSlideableView.getRight();
367 top = mSlideableView.getTop();
368 bottom = mSlideableView.getBottom();
369 } else {
370 left = right = top = bottom = 0;
371 }
372 View child = getChildAt(0);
373 final int clampedChildLeft = Math.max(leftBound, child.getLeft());
374 final int clampedChildTop = Math.max(topBound, child.getTop());
375 final int clampedChildRight = Math.min(rightBound, child.getRight());
376 final int clampedChildBottom = Math.min(bottomBound, child.getBottom());
377 final int vis;
378 if (clampedChildLeft >= left && clampedChildTop >= top && clampedChildRight <= right && clampedChildBottom <= bottom) {
379 vis = INVISIBLE;
380 } else {
381 vis = VISIBLE;
382 }
383 child.setVisibility(vis);
384 }
385
386 void setAllChildrenVisible() {
387 for (int i = 0, childCount = getChildCount(); i < childCount; i++) {
388 final View child = getChildAt(i);
389 if (child.getVisibility() == INVISIBLE) {
390 child.setVisibility(VISIBLE);
391 }
392 }
393 }
394
395 private static boolean hasOpaqueBackground(View v) {
396 final Drawable bg = v.getBackground();
397 if (bg != null) {
398 return bg.getOpacity() == PixelFormat.OPAQUE;
399 }
400 return false;
401 }
402
403 @Override
404 protected void onAttachedToWindow() {
405 super.onAttachedToWindow();
406 mFirstLayout = true;
407 }
408
409 @Override
410 protected void onDetachedFromWindow() {
411 super.onDetachedFromWindow();
412 mFirstLayout = true;
413 }
414
415 @Override
416 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
417 final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
418 final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
419 final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
420 final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
421
422 if (widthMode != MeasureSpec.EXACTLY) {
423 throw new IllegalStateException("Width must have an exact value or MATCH_PARENT");
424 } else if (heightMode != MeasureSpec.EXACTLY) {
425 throw new IllegalStateException("Height must have an exact value or MATCH_PARENT");
426 }
427
428 int layoutHeight = heightSize - getPaddingTop() - getPaddingBottom();
429 int panelHeight = mPanelHeight;
430
431 final int childCount = getChildCount();
432
433 if (childCount > 2) {
434 Log.e(TAG, "onMeasure: More than two child views are not supported.");
435 } else if (getChildAt(1).getVisibility() == GONE) {
436 panelHeight = 0;
437 }
438
439 // We'll find the current one below.
440 mSlideableView = null;
441 mCanSlide = false;
442
443 // First pass. Measure based on child LayoutParams width/height.
444 for (int i = 0; i < childCount; i++) {
445 final View child = getChildAt(i);
446 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
447
448 int height = layoutHeight;
449 if (child.getVisibility() == GONE) {
450 lp.dimWhenOffset = false;
451 continue;
452 }
453
454 if (i == 1) {
455 lp.slideable = true;
456 lp.dimWhenOffset = true;
457 mSlideableView = child;
458 mCanSlide = true;
459 } else {
460 height -= panelHeight;
461 }
462
463 int childWidthSpec;
464 if (lp.width == LayoutParams.WRAP_CONTENT) {
465 childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST);
466 } else if (lp.width == LayoutParams.MATCH_PARENT) {
467 childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
468 } else {
469 childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
470 }
471
472 int childHeightSpec;
473 if (lp.height == LayoutParams.WRAP_CONTENT) {
474 childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
475 } else if (lp.height == LayoutParams.MATCH_PARENT) {
476 childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
477 } else {
478 childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
479 }
480
481 child.measure(childWidthSpec, childHeightSpec);
482 }
483
484 setMeasuredDimension(widthSize, heightSize);
485 }
486
487 @Override
488 protected void onLayout(boolean changed, int l, int t, int r, int b) {
489 final int paddingLeft = getPaddingLeft();
490 final int paddingTop = getPaddingTop();
491
492 final int childCount = getChildCount();
493 int yStart = paddingTop;
494 int nextYStart = yStart;
495
496 if (mFirstLayout) {
497 mSlideOffset = mCanSlide && mPreservedExpandedState ? 0.f : 1.f;
498 }
499
500 for (int i = 0; i < childCount; i++) {
501 final View child = getChildAt(i);
502
503 if (child.getVisibility() == GONE) {
504 continue;
505 }
506
507 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
508
509 int childHeight = child.getMeasuredHeight();
510
511 if (lp.slideable) {
512 mSlideRange = childHeight - mPanelHeight;
513 yStart += (int) (mSlideRange * mSlideOffset);
514 } else {
515 yStart = nextYStart;
516 }
517
518 final int childTop = yStart;
519 final int childBottom = childTop + childHeight;
520 final int childLeft = paddingLeft;
521 final int childRight = childLeft + child.getMeasuredWidth();
522 child.layout(childLeft, childTop, childRight, childBottom);
523
524 nextYStart += child.getHeight();
525 }
526
527 if (mFirstLayout) {
528 updateObscuredViewVisibility();
529 }
530
531 mFirstLayout = false;
532 }
533
534 @Override
535 protected void onSizeChanged(int w, int h, int oldw, int oldh) {
536 super.onSizeChanged(w, h, oldw, oldh);
537 // Recalculate sliding panes and their details
538 if (h != oldh) {
539 mFirstLayout = true;
540 }
541 }
542
543 /**
544 * Set sliding enabled flag
545 *
546 * @param enabled
547 * flag value
548 */
549 public void setSlidingEnabled(boolean enabled) {
550 mIsSlidingEnabled = enabled;
551 }
552
553 /**
554 * Set if the drag view can have its own touch events. If set to true, a drag view can scroll horizontally and have its own click listener.
555 *
556 * Default is set to false.
557 */
558 public void setEnableDragViewTouchEvents(boolean enabled) {
559 mIsUsingDragViewTouchEvents = enabled;
560 }
561
562 private boolean isDragViewHit(int x, int y) {
563 View v = mDragView != null ? mDragView : mSlideableView;
564 if (v == null)
565 return false;
566 int[] viewLocation = new int[2];
567 v.getLocationOnScreen(viewLocation);
568 int[] parentLocation = new int[2];
569 this.getLocationOnScreen(parentLocation);
570 int screenX = parentLocation[0] + x;
571 int screenY = parentLocation[1] + y;
572 return screenX >= viewLocation[0] && screenX < viewLocation[0] + v.getWidth() && screenY >= viewLocation[1]
573 && screenY < viewLocation[1] + v.getHeight();
574 }
575
576 @Override
577 public void requestChildFocus(View child, View focused) {
578 super.requestChildFocus(child, focused);
579 if (!isInTouchMode() && !mCanSlide) {
580 mPreservedExpandedState = child == mSlideableView;
581 }
582 }
583
584 @Override
585 public boolean onInterceptTouchEvent(MotionEvent ev) {
586 final int action = MotionEventCompat.getActionMasked(ev);
587
588 if (!mCanSlide || !mIsSlidingEnabled || (mIsUnableToDrag && action != MotionEvent.ACTION_DOWN)) {
589 mDragHelper.cancel();
590 return super.onInterceptTouchEvent(ev);
591 }
592
593 if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
594 mDragHelper.cancel();
595 return false;
596 }
597
598 final float x = ev.getX();
599 final float y = ev.getY();
600 boolean interceptTap = false;
601
602 switch (action) {
603 case MotionEvent.ACTION_DOWN: {
604 mIsUnableToDrag = false;
605 mInitialMotionX = x;
606 mInitialMotionY = y;
607 mDragViewHit = isDragViewHit((int) x, (int) y);
608
609 if (mDragViewHit && !mIsUsingDragViewTouchEvents) {
610 interceptTap = true;
611 }
612 break;
613 }
614
615 case MotionEvent.ACTION_MOVE: {
616 final float adx = Math.abs(x - mInitialMotionX);
617 final float ady = Math.abs(y - mInitialMotionY);
618 final int dragSlop = mDragHelper.getTouchSlop();
619
620 // Handle any horizontal scrolling on the drag view.
621 if (mIsUsingDragViewTouchEvents) {
622 if (adx > mScrollTouchSlop && ady < mScrollTouchSlop) {
623 return super.onInterceptTouchEvent(ev);
624 }
625 // Intercept the touch if the drag view has any vertical scroll.
626 // onTouchEvent will determine if the view should drag vertically.
627 else if (ady > mScrollTouchSlop) {
628 interceptTap = mDragViewHit;
629 }
630 }
631
632 if (ady > dragSlop && adx > ady) {
633 mDragHelper.cancel();
634 mIsUnableToDrag = true;
635 return false;
636 }
637 break;
638 }
639 }
640
641 final boolean interceptForDrag = mDragViewHit && mDragHelper.shouldInterceptTouchEvent(ev);
642
643 return interceptForDrag || interceptTap;
644 }
645
646 @Override
647 public boolean onTouchEvent(MotionEvent ev) {
648 if (!mCanSlide || !mIsSlidingEnabled) {
649 return super.onTouchEvent(ev);
650 }
651
652 mDragHelper.processTouchEvent(ev);
653
654 final int action = ev.getAction();
655 boolean wantTouchEvents = true;
656
657 switch (action & MotionEventCompat.ACTION_MASK) {
658 case MotionEvent.ACTION_DOWN: {
659 final float x = ev.getX();
660 final float y = ev.getY();
661 mInitialMotionX = x;
662 mInitialMotionY = y;
663 break;
664 }
665
666 case MotionEvent.ACTION_UP: {
667 final float x = ev.getX();
668 final float y = ev.getY();
669 final float dx = x - mInitialMotionX;
670 final float dy = y - mInitialMotionY;
671 final int slop = mDragHelper.getTouchSlop();
672 if (dx * dx + dy * dy < slop * slop && isDragViewHit((int) x, (int) y)) {
673 View v = mDragView != null ? mDragView : mSlideableView;
674 v.playSoundEffect(SoundEffectConstants.CLICK);
675 if (!isExpanded() && !isAnchored()) {
676 expandPane(mSlideableView, 0, mAnchorPoint);
677 } else {
678 collapsePane();
679 }
680 break;
681 }
682 break;
683 }
684 }
685
686 return wantTouchEvents;
687 }
688
689 private boolean expandPane(View pane, int initialVelocity, float mSlideOffset) {
690 if (mFirstLayout || smoothSlideTo(mSlideOffset, initialVelocity)) {
691 mPreservedExpandedState = true;
692 return true;
693 }
694 return false;
695 }
696
697 private boolean collapsePane(View pane, int initialVelocity) {
698 if (mFirstLayout || smoothSlideTo(1.f, initialVelocity)) {
699 mPreservedExpandedState = false;
700 return true;
701 }
702 return false;
703 }
704
705 /**
706 * Collapse the sliding pane if it is currently slideable. If first layout has already completed this will animate.
707 *
708 * @return true if the pane was slideable and is now collapsed/in the process of collapsing
709 */
710 public boolean collapsePane() {
711 return collapsePane(mSlideableView, 0);
712 }
713
714 /**
715 * Expand the sliding pane if it is currently slideable. If first layout has already completed this will animate.
716 *
717 * @return true if the pane was slideable and is now expanded/in the process of expading
718 */
719 public boolean expandPane() {
720 return expandPane(0);
721 }
722
723 /**
724 * Partially expand the sliding pane up to a specific offset
725 *
726 * @param mSlideOffset
727 * Value between 0 and 1, where 0 is completely expanded.
728 * @return true if the pane was slideable and is now expanded/in the process of expading
729 */
730 public boolean expandPane(float mSlideOffset) {
731 if (!isPaneVisible()) {
732 showPane();
733 }
734 return expandPane(mSlideableView, 0, mSlideOffset);
735 }
736
737 /**
738 * Check if the layout is completely expanded.
739 *
740 * @return true if sliding panels are completely expanded
741 */
742 public boolean isExpanded() {
743 return mFirstLayout && mPreservedExpandedState || !mFirstLayout && mCanSlide && mSlideOffset == 0;
744 }
745
746 /**
747 * Check if the layout is anchored in an intermediate point.
748 *
749 * @return true if sliding panels are anchored
750 */
751 public boolean isAnchored() {
752 int anchoredTop = (int) (mAnchorPoint * mSlideRange);
753 return !mFirstLayout && mCanSlide && mSlideOffset == (float) anchoredTop / (float) mSlideRange;
754 }
755
756 /**
757 * Check if the content in this layout cannot fully fit side by side and therefore the content pane can be slid back and forth.
758 *
759 * @return true if content in this layout can be expanded
760 */
761 public boolean isSlideable() {
762 return mCanSlide;
763 }
764
765 public boolean isPaneVisible() {
766 if (getChildCount() < 2) {
767 return false;
768 }
769 View slidingPane = getChildAt(1);
770 return slidingPane.getVisibility() == View.VISIBLE;
771 }
772
773 public void showPane() {
774 if (getChildCount() < 2) {
775 return;
776 }
777 View slidingPane = getChildAt(1);
778 slidingPane.setVisibility(View.VISIBLE);
779 requestLayout();
780 }
781
782 public void hidePane() {
783 if (mSlideableView == null) {
784 return;
785 }
786 mSlideableView.setVisibility(View.GONE);
787 requestLayout();
788 }
789
790 private void onPanelDragged(int newTop) {
791 final int topBound = getPaddingTop();
792 mSlideOffset = (float) (newTop - topBound) / mSlideRange;
793 dispatchOnPanelSlide(mSlideableView);
794 }
795
796 @Override
797 protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
798 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
799 boolean result;
800 final int save = canvas.save(Canvas.CLIP_SAVE_FLAG);
801
802 boolean drawScrim = false;
803
804 if (mCanSlide && !lp.slideable && mSlideableView != null) {
805 // Clip against the slider; no sense drawing what will immediately be covered.
806 canvas.getClipBounds(mTmpRect);
Alexandre Lision0936ba62013-11-25 10:04:56 -0500807 mTmpRect.bottom = (int) Math.min(mTmpRect.bottom, mSlideableView.getTop() + getResources().getDisplayMetrics().density * 68); // + 60
808 // cause of
809 // the
810 // rounded
811 // shape
812 // handle
Alexandre Lision450458a2013-11-22 11:33:12 -0500813 canvas.clipRect(mTmpRect);
814 if (mSlideOffset < 1) {
815 drawScrim = true;
816 }
817 }
818
819 result = super.drawChild(canvas, child, drawingTime);
820 canvas.restoreToCount(save);
821
822 if (drawScrim) {
823 final int baseAlpha = (mCoveredFadeColor & 0xff000000) >>> 24;
824 final int imag = (int) (baseAlpha * (1 - mSlideOffset));
825 final int color = imag << 24 | (mCoveredFadeColor & 0xffffff);
826 mCoveredFadePaint.setColor(color);
827 canvas.drawRect(mTmpRect, mCoveredFadePaint);
828 }
829
830 return result;
831 }
832
833 /**
834 * Smoothly animate mDraggingPane to the target X position within its range.
835 *
836 * @param slideOffset
837 * position to animate to
838 * @param velocity
839 * initial velocity in case of fling, or 0.
840 */
841 boolean smoothSlideTo(float slideOffset, int velocity) {
842 if (!mCanSlide) {
843 // Nothing to do.
844 return false;
845 }
846
847 final int topBound = getPaddingTop();
848 int y = (int) (topBound + slideOffset * mSlideRange);
849
850 if (mDragHelper.smoothSlideViewTo(mSlideableView, mSlideableView.getLeft(), y)) {
851 setAllChildrenVisible();
852 ViewCompat.postInvalidateOnAnimation(this);
853 return true;
854 }
855 return false;
856 }
857
858 @Override
859 public void computeScroll() {
860 if (mDragHelper.continueSettling(true)) {
861 if (!mCanSlide) {
862 mDragHelper.abort();
863 return;
864 }
865
866 ViewCompat.postInvalidateOnAnimation(this);
867 }
868 }
869
870 @Override
871 public void draw(Canvas c) {
872 super.draw(c);
873
874 if (mSlideableView == null) {
875 // No need to draw a shadow if we don't have one.
876 return;
877 }
878
879 final int right = mSlideableView.getRight();
880 final int top = mSlideableView.getTop() - mShadowHeight;
881 final int bottom = mSlideableView.getTop();
882 final int left = mSlideableView.getLeft();
883
884 if (mShadowDrawable != null) {
885 mShadowDrawable.setBounds(left, top, right, bottom);
886 mShadowDrawable.draw(c);
887 }
888 }
889
890 /**
891 * Tests scrollability within child views of v given a delta of dx.
892 *
893 * @param v
894 * View to test for horizontal scrollability
895 * @param checkV
896 * Whether the view v passed should itself be checked for scrollability (true), or just its children (false).
897 * @param dx
898 * Delta scrolled in pixels
899 * @param x
900 * X coordinate of the active touch point
901 * @param y
902 * Y coordinate of the active touch point
903 * @return true if child views of v can be scrolled by delta of dx.
904 */
905 protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
906 if (v instanceof ViewGroup) {
907 final ViewGroup group = (ViewGroup) v;
908 final int scrollX = v.getScrollX();
909 final int scrollY = v.getScrollY();
910 final int count = group.getChildCount();
911 // Count backwards - let topmost views consume scroll distance first.
912 for (int i = count - 1; i >= 0; i--) {
913 final View child = group.getChildAt(i);
914 if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && y + scrollY >= child.getTop()
915 && y + scrollY < child.getBottom() && canScroll(child, true, dx, x + scrollX - child.getLeft(), y + scrollY - child.getTop())) {
916 return true;
917 }
918 }
919 }
920 return checkV && ViewCompat.canScrollHorizontally(v, -dx);
921 }
922
923 @Override
924 protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
925 return new LayoutParams();
926 }
927
928 @Override
929 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
930 return p instanceof MarginLayoutParams ? new LayoutParams((MarginLayoutParams) p) : new LayoutParams(p);
931 }
932
933 @Override
934 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
935 return p instanceof LayoutParams && super.checkLayoutParams(p);
936 }
937
938 @Override
939 public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
940 return new LayoutParams(getContext(), attrs);
941 }
942
943 @Override
944 protected Parcelable onSaveInstanceState() {
945 Parcelable superState = super.onSaveInstanceState();
946
947 SavedState ss = new SavedState(superState);
948 ss.isExpanded = isSlideable() ? isExpanded() : mPreservedExpandedState;
949
950 return ss;
951 }
952
953 @Override
954 protected void onRestoreInstanceState(Parcelable state) {
955 SavedState ss = (SavedState) state;
956 super.onRestoreInstanceState(ss.getSuperState());
957
958 if (ss.isExpanded) {
959 expandPane();
960 } else {
961 collapsePane();
962 }
963 mPreservedExpandedState = ss.isExpanded;
964 }
965
966 private class DragHelperCallback extends ViewDragHelper.Callback {
967
968 @Override
969 public boolean tryCaptureView(View child, int pointerId) {
970 if (mIsUnableToDrag) {
971 return false;
972 }
973
974 return ((LayoutParams) child.getLayoutParams()).slideable;
975 }
976
977 @Override
978 public void onViewDragStateChanged(int state) {
979 if (mDragHelper.getViewDragState() == ViewDragHelper.STATE_IDLE) {
980 if (mSlideOffset == 0) {
981 updateObscuredViewVisibility();
982 dispatchOnPanelExpanded(mSlideableView);
983 mPreservedExpandedState = true;
984 } else if (isAnchored()) {
985 updateObscuredViewVisibility();
986 dispatchOnPanelAnchored(mSlideableView);
987 mPreservedExpandedState = true;
988 } else {
989 dispatchOnPanelCollapsed(mSlideableView);
990 mPreservedExpandedState = false;
991 }
992 }
993 }
994
995 @Override
996 public void onViewCaptured(View capturedChild, int activePointerId) {
997 // Make all child views visible in preparation for sliding things around
998 setAllChildrenVisible();
999 }
1000
1001 @Override
1002 public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
1003 onPanelDragged(top);
1004 invalidate();
1005 }
1006
1007 @Override
1008 public void onViewReleased(View releasedChild, float xvel, float yvel) {
1009 int top = getPaddingTop();
1010
1011 if (mAnchorPoint != 0) {
1012 int anchoredTop = (int) (mAnchorPoint * mSlideRange);
1013 float anchorOffset = (float) anchoredTop / (float) mSlideRange;
1014
1015 if (yvel > 0 || (yvel == 0 && mSlideOffset >= (1f + anchorOffset) / 2)) {
1016 top += mSlideRange;
1017 } else if (yvel == 0 && mSlideOffset < (1f + anchorOffset) / 2 && mSlideOffset >= anchorOffset / 2) {
1018 top += mSlideRange * mAnchorPoint;
1019 }
1020
1021 } else if (yvel > 0 || (yvel == 0 && mSlideOffset > 0.5f)) {
1022 top += mSlideRange;
1023 }
1024
1025 mDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top);
1026 invalidate();
1027 }
1028
1029 @Override
1030 public int getViewVerticalDragRange(View child) {
1031 return mSlideRange;
1032 }
1033
1034 @Override
1035 public int clampViewPositionVertical(View child, int top, int dy) {
1036 final int topBound = getPaddingTop();
1037 final int bottomBound = topBound + mSlideRange;
1038
1039 final int newLeft = Math.min(Math.max(top, topBound), bottomBound);
1040
1041 return newLeft;
1042 }
1043
1044 }
1045
1046 public static class LayoutParams extends ViewGroup.MarginLayoutParams {
1047 private static final int[] ATTRS = new int[] { android.R.attr.layout_weight };
1048
1049 /**
1050 * True if this pane is the slideable pane in the layout.
1051 */
1052 boolean slideable;
1053
1054 /**
1055 * True if this view should be drawn dimmed when it's been offset from its default position.
1056 */
1057 boolean dimWhenOffset;
1058
1059 Paint dimPaint;
1060
1061 public LayoutParams() {
1062 super(MATCH_PARENT, MATCH_PARENT);
1063 }
1064
1065 public LayoutParams(int width, int height) {
1066 super(width, height);
1067 }
1068
1069 public LayoutParams(android.view.ViewGroup.LayoutParams source) {
1070 super(source);
1071 }
1072
1073 public LayoutParams(MarginLayoutParams source) {
1074 super(source);
1075 }
1076
1077 public LayoutParams(LayoutParams source) {
1078 super(source);
1079 }
1080
1081 public LayoutParams(Context c, AttributeSet attrs) {
1082 super(c, attrs);
1083
1084 final TypedArray a = c.obtainStyledAttributes(attrs, ATTRS);
1085 a.recycle();
1086 }
1087
1088 }
1089
1090 static class SavedState extends BaseSavedState {
1091 boolean isExpanded;
1092
1093 SavedState(Parcelable superState) {
1094 super(superState);
1095 }
1096
1097 private SavedState(Parcel in) {
1098 super(in);
1099 isExpanded = in.readInt() != 0;
1100 }
1101
1102 @Override
1103 public void writeToParcel(Parcel out, int flags) {
1104 super.writeToParcel(out, flags);
1105 out.writeInt(isExpanded ? 1 : 0);
1106 }
1107
1108 public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
1109 @Override
1110 public SavedState createFromParcel(Parcel in) {
1111 return new SavedState(in);
1112 }
1113
1114 @Override
1115 public SavedState[] newArray(int size) {
1116 return new SavedState[size];
1117 }
1118 };
1119 }
1120}