package com.chenjishi.slidedemo.base; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; import android.support.v4.view.AccessibilityDelegateCompat; import android.support.v4.view.MotionEventCompat; import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewPager; import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import android.support.v4.widget.ViewDragHelper; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import android.view.accessibility.AccessibilityEvent; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; public class SlideLayout extends ViewGroup { private static final int MIN_FLING_VELOCITY = 400; // dips per second private Drawable mShadowDrawable; private boolean mCanSlide; private View mSlideableView; private float mSlideOffset; private int mSlideRange; private boolean mIsUnableToDrag; private float mInitialMotionX; private float mInitialMotionY; private float mEdgeSize; private ViewPager mViewPager; private SlideListener mSlideListener; private final ViewDragHelper mDragHelper; private boolean mPreservedOpenState; private boolean mFirstLayout = true; private final ArrayList<DisableLayerRunnable> mPostedRunnables = new ArrayList<DisableLayerRunnable>(); static final SlidingPanelLayoutImpl IMPL; static { final int deviceVersion = Build.VERSION.SDK_INT; if (deviceVersion >= 17) { IMPL = new SlidingPanelLayoutImplJBMR1(); } else if (deviceVersion >= 16) { IMPL = new SlidingPanelLayoutImplJB(); } else { IMPL = new SlidingPanelLayoutImplBase(); } } public interface SlideListener { void onPanelSlide(View panel, float slideOffset); void onViewCaptured(); } public SlideLayout(Context context) { this(context, null); } public SlideLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public SlideLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); final float density = context.getResources().getDisplayMetrics().density; setWillNotDraw(false); ViewCompat.setAccessibilityDelegate(this, new AccessibilityDelegate()); ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); mDragHelper = ViewDragHelper.create(this, 0.5f, new DragHelperCallback()); mDragHelper.setMinVelocity(MIN_FLING_VELOCITY * density); } public void setCanSlide(boolean b) { mCanSlide = b; } public void setSlidingListener(SlideListener listener) { mSlideListener = listener; } void dispatchOnPanelSlide(View panel) { if (mSlideListener != null) { mSlideListener.onPanelSlide(panel, mSlideOffset); } } public void setEdgeSize(int offset) { mEdgeSize = offset; } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); mFirstLayout = true; } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mFirstLayout = true; for (int i = 0, count = mPostedRunnables.size(); i < count; i++) { final DisableLayerRunnable dlr = mPostedRunnables.get(i); dlr.run(); } mPostedRunnables.clear(); } @Override public void addView(View child) { if (getChildCount() > 0) { throw new IllegalStateException("SlideLayout can host only one direct child"); } super.addView(child); } @Override public void addView(View child, int index) { if (getChildCount() > 0) { throw new IllegalStateException("SlideLayout can host only one direct child"); } super.addView(child, index); } @Override public void addView(View child, int width, int height) { if (getChildCount() > 0) { throw new IllegalStateException("SlideLayout can host only one direct child"); } super.addView(child, width, height); } @Override public void addView(View child, ViewGroup.LayoutParams params) { if (getChildCount() > 0) { throw new IllegalStateException("SlideLayout can host only one direct child"); } super.addView(child, params); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); if (widthMode != MeasureSpec.EXACTLY) { throw new IllegalStateException("Width must have an exact value or MATCH_PARENT"); } int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (heightMode != MeasureSpec.EXACTLY) { throw new IllegalStateException("Width must have an exact value or MATCH_PARENT"); } final int childCount = getChildCount(); // We'll find the current one below. mSlideableView = null; for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); int childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); int childHeightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY); child.measure(childWidthSpec, childHeightSpec); lp.slideable = true; mSlideableView = child; } setMeasuredDimension(widthSize, heightSize); mCanSlide = true; if (mDragHelper.getViewDragState() != ViewDragHelper.STATE_IDLE) { // Cancel scrolling in progress, it's no longer relevant. mDragHelper.abort(); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT); final int width = r - l; final int paddingStart = getPaddingLeft(); final int paddingEnd = getPaddingRight(); final int paddingTop = getPaddingTop(); final int childCount = getChildCount(); int xStart = paddingStart; int nextXStart = xStart; if (mFirstLayout) { mSlideOffset = mCanSlide && mPreservedOpenState ? 1.f : 0.f; } for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); final int childWidth = child.getMeasuredWidth(); int offset = 0; if (lp.slideable) { final int range = childWidth; mSlideRange = range; final int lpMargin = lp.leftMargin; final int pos = (int) (range * mSlideOffset); xStart += pos + lpMargin; mSlideOffset = (float) pos / mSlideRange; } else { xStart = nextXStart; } final int childLeft = xStart - offset; final int childRight = childLeft + childWidth; final int childTop = paddingTop; final int childBottom = childTop + child.getMeasuredHeight(); child.layout(childLeft, paddingTop, childRight, childBottom); nextXStart += child.getWidth(); } mFirstLayout = false; } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // Recalculate sliding panes and their details if (w != oldw) { mFirstLayout = true; } } @Override public void requestChildFocus(View child, View focused) { super.requestChildFocus(child, focused); if (!isInTouchMode() && !mCanSlide) { mPreservedOpenState = child == mSlideableView; } } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { final int action = MotionEventCompat.getActionMasked(ev); if (!mCanSlide || (mIsUnableToDrag && action != MotionEvent.ACTION_DOWN)) { mDragHelper.cancel(); return super.onInterceptTouchEvent(ev); } if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { mDragHelper.cancel(); return false; } boolean interceptTap = false; switch (action) { case MotionEvent.ACTION_DOWN: { mIsUnableToDrag = false; final float x = ev.getX(); final float y = ev.getY(); mInitialMotionX = x; mInitialMotionY = y; if (x > mEdgeSize) { mDragHelper.cancel(); mIsUnableToDrag = true; return false; } break; } case MotionEvent.ACTION_MOVE: { final float x = ev.getX(); final float y = ev.getY(); final float adx = Math.abs(x - mInitialMotionX); final float ady = Math.abs(y - mInitialMotionY); final int slop = mDragHelper.getTouchSlop(); if (adx > slop && ady > adx) { mDragHelper.cancel(); mIsUnableToDrag = true; return false; } if (null != mViewPager && mInitialMotionX > mEdgeSize) { mDragHelper.cancel(); mIsUnableToDrag = true; return false; } if (null != mViewPager && canViewPagerScroll(mViewPager, (int) adx)) { mDragHelper.cancel(); mIsUnableToDrag = true; return false; } } } final boolean interceptForDrag = mDragHelper.shouldInterceptTouchEvent(ev); return interceptForDrag || interceptTap; } @Override public boolean onTouchEvent(MotionEvent ev) { if (!mCanSlide) return super.onTouchEvent(ev); mDragHelper.processTouchEvent(ev); final int action = ev.getAction(); boolean wantTouchEvents = true; switch (action & MotionEventCompat.ACTION_MASK) { case MotionEvent.ACTION_DOWN: { final float x = ev.getX(); final float y = ev.getY(); mInitialMotionX = x; mInitialMotionY = y; break; } case MotionEvent.ACTION_UP: { break; } } return wantTouchEvents; } public void setViewPager(ViewPager viewPager) { mViewPager = viewPager; } private boolean canViewPagerScroll(ViewPager p, int dx) { if (dx == 0) return false; final int index = p.getCurrentItem(); return !(dx > 0 && index <= 0 || dx < 0 && index >= p.getAdapter().getCount() - 1); } private void onPanelDragged(int newLeft) { if (mSlideableView == null) { // This can happen if we're aborting motion during layout because everything now fits. mSlideOffset = 0; return; } final LayoutParams lp = (LayoutParams) mSlideableView.getLayoutParams(); final int paddingStart = getPaddingLeft(); final int lpMargin = lp.leftMargin; final int startBound = paddingStart + lpMargin; mSlideOffset = (float) (newLeft - startBound) / mSlideRange; dispatchOnPanelSlide(mSlideableView); } @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { boolean result; final int save = canvas.save(Canvas.CLIP_SAVE_FLAG); if (Build.VERSION.SDK_INT >= 11) { result = super.drawChild(canvas, child, drawingTime); } else { if (child.isDrawingCacheEnabled()) { child.setDrawingCacheEnabled(false); } result = super.drawChild(canvas, child, drawingTime); } canvas.restoreToCount(save); return result; } private void invalidateChildRegion(View v) { IMPL.invalidateChildRegion(this, v); } @Override public void computeScroll() { if (mDragHelper.continueSettling(true)) { if (!mCanSlide) { mDragHelper.abort(); return; } ViewCompat.postInvalidateOnAnimation(this); } } public void setShadowResource(int resId) { mShadowDrawable = getResources().getDrawable(resId); } @Override public void draw(Canvas c) { super.draw(c); final View shadowView = mSlideableView; if (shadowView == null || mShadowDrawable == null) { // No need to draw a shadow if we don't have one. return; } final int top = shadowView.getTop(); final int bottom = shadowView.getBottom(); final int shadowWidth = mShadowDrawable.getIntrinsicWidth(); final int right = shadowView.getLeft(); final int left = right - shadowWidth; mShadowDrawable.setBounds(left, top, right, bottom); mShadowDrawable.draw(c); } @Override protected ViewGroup.LayoutParams generateDefaultLayoutParams() { return new LayoutParams(); } @Override protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { return p instanceof MarginLayoutParams ? new LayoutParams((MarginLayoutParams) p) : new LayoutParams(p); } @Override protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { return p instanceof LayoutParams && super.checkLayoutParams(p); } @Override public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(), attrs); } private class DragHelperCallback extends ViewDragHelper.Callback { @Override public boolean tryCaptureView(View child, int pointerId) { if (mIsUnableToDrag) return false; return ((LayoutParams) child.getLayoutParams()).slideable; } @Override public void onViewDragStateChanged(int state) { if (mDragHelper.getViewDragState() == ViewDragHelper.STATE_IDLE) { if (mSlideOffset == 0) { mPreservedOpenState = false; } else { mPreservedOpenState = true; } } } @Override public void onViewCaptured(View capturedChild, int activePointerId) { if (null != mSlideListener) mSlideListener.onViewCaptured(); } @Override public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { onPanelDragged(left); invalidate(); } @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { final LayoutParams lp = (LayoutParams) releasedChild.getLayoutParams(); int left = getPaddingLeft() + lp.leftMargin; if (xvel > 0 || (xvel == 0 && mSlideOffset > 0.5f)) { left += mSlideRange; } mDragHelper.settleCapturedViewAt(left, releasedChild.getTop()); invalidate(); } @Override public int getViewHorizontalDragRange(View child) { return mSlideRange; } @Override public int clampViewPositionHorizontal(View child, int left, int dx) { final LayoutParams lp = (LayoutParams) mSlideableView.getLayoutParams(); int startBound = getPaddingLeft() + lp.leftMargin; int endBound = startBound + mSlideRange; final int newLeft = Math.min(Math.max(left, startBound), endBound); return newLeft; } @Override public int clampViewPositionVertical(View child, int top, int dy) { // Make sure we never move views vertically. // This could happen if the child has less height than its parent. return child.getTop(); } @Override public void onEdgeDragStarted(int edgeFlags, int pointerId) { mDragHelper.captureChildView(mSlideableView, pointerId); } } public static class LayoutParams extends MarginLayoutParams { private static final int[] ATTRS = new int[]{ android.R.attr.layout_weight }; /** * The weighted proportion of how much of the leftover space * this child should consume after measurement. */ public float weight = 0; /** * True if this pane is the slideable pane in the layout. */ boolean slideable; Paint dimPaint; public LayoutParams() { super(FILL_PARENT, FILL_PARENT); } public LayoutParams(int width, int height) { super(width, height); } public LayoutParams(ViewGroup.LayoutParams source) { super(source); } public LayoutParams(MarginLayoutParams source) { super(source); } public LayoutParams(LayoutParams source) { super(source); this.weight = source.weight; } public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); final TypedArray a = c.obtainStyledAttributes(attrs, ATTRS); this.weight = a.getFloat(0, 0); a.recycle(); } } interface SlidingPanelLayoutImpl { void invalidateChildRegion(SlideLayout parent, View child); } static class SlidingPanelLayoutImplBase implements SlidingPanelLayoutImpl { public void invalidateChildRegion(SlideLayout parent, View child) { ViewCompat.postInvalidateOnAnimation(parent, child.getLeft(), child.getTop(), child.getRight(), child.getBottom()); } } static class SlidingPanelLayoutImplJB extends SlidingPanelLayoutImplBase { /* * Private API hacks! Nasty! Bad! * * In Jellybean, some optimizations in the hardware UI renderer * prevent a changed Paint on a View using a hardware layer from having * the intended effect. This twiddles some internal bits on the view to force * it to recreate the display list. */ private Method mGetDisplayList; private Field mRecreateDisplayList; SlidingPanelLayoutImplJB() { try { mGetDisplayList = View.class.getDeclaredMethod("getDisplayList", (Class[]) null); } catch (NoSuchMethodException e) { } try { mRecreateDisplayList = View.class.getDeclaredField("mRecreateDisplayList"); mRecreateDisplayList.setAccessible(true); } catch (NoSuchFieldException e) { } } @Override public void invalidateChildRegion(SlideLayout parent, View child) { if (mGetDisplayList != null && mRecreateDisplayList != null) { try { mRecreateDisplayList.setBoolean(child, true); mGetDisplayList.invoke(child, (Object[]) null); } catch (Exception e) { } } else { // Slow path. REALLY slow path. Let's hope we don't get here. child.invalidate(); return; } super.invalidateChildRegion(parent, child); } } static class SlidingPanelLayoutImplJBMR1 extends SlidingPanelLayoutImplBase { @Override public void invalidateChildRegion(SlideLayout parent, View child) { ViewCompat.setLayerPaint(child, ((LayoutParams) child.getLayoutParams()).dimPaint); } } class AccessibilityDelegate extends AccessibilityDelegateCompat { private final Rect mTmpRect = new Rect(); @Override public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { final AccessibilityNodeInfoCompat superNode = AccessibilityNodeInfoCompat.obtain(info); super.onInitializeAccessibilityNodeInfo(host, superNode); copyNodeInfoNoChildren(info, superNode); superNode.recycle(); info.setClassName(SlideLayout.class.getName()); info.setSource(host); final ViewParent parent = ViewCompat.getParentForAccessibility(host); if (parent instanceof View) { info.setParent((View) parent); } // This is a best-approximation of addChildrenForAccessibility() // that accounts for filtering. final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); if (!filter(child) && (child.getVisibility() == View.VISIBLE)) { // Force importance to "yes" since we can't read the value. ViewCompat.setImportantForAccessibility( child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); info.addChild(child); } } } @Override public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { super.onInitializeAccessibilityEvent(host, event); event.setClassName(SlideLayout.class.getName()); } @Override public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, AccessibilityEvent event) { if (!filter(child)) { return super.onRequestSendAccessibilityEvent(host, child, event); } return false; } public boolean filter(View child) { return false; } /** * This should really be in AccessibilityNodeInfoCompat, but there unfortunately * seem to be a few elements that are not easily cloneable using the underlying API. * Leave it private here as it's not general-purpose useful. */ private void copyNodeInfoNoChildren(AccessibilityNodeInfoCompat dest, AccessibilityNodeInfoCompat src) { final Rect rect = mTmpRect; src.getBoundsInParent(rect); dest.setBoundsInParent(rect); src.getBoundsInScreen(rect); dest.setBoundsInScreen(rect); dest.setVisibleToUser(src.isVisibleToUser()); dest.setPackageName(src.getPackageName()); dest.setClassName(src.getClassName()); dest.setContentDescription(src.getContentDescription()); dest.setEnabled(src.isEnabled()); dest.setClickable(src.isClickable()); dest.setFocusable(src.isFocusable()); dest.setFocused(src.isFocused()); dest.setAccessibilityFocused(src.isAccessibilityFocused()); dest.setSelected(src.isSelected()); dest.setLongClickable(src.isLongClickable()); dest.addAction(src.getActions()); dest.setMovementGranularities(src.getMovementGranularities()); } } private class DisableLayerRunnable implements Runnable { final View mChildView; DisableLayerRunnable(View childView) { mChildView = childView; } @Override public void run() { if (mChildView.getParent() == SlideLayout.this) { ViewCompat.setLayerType(mChildView, ViewCompat.LAYER_TYPE_NONE, null); invalidateChildRegion(mChildView); } mPostedRunnables.remove(this); } } }