11 |
12 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SlideNestedPanelLayout
2 | 仿美团订单的拖拽面板,内部处理嵌套滑动逻辑,动效查看效果图
3 |
4 | 参考资源:https://github.com/woxingxiao/SlidingUpPanelLayout
5 |
6 | 先看效果图,客观满意再star一波
7 |
8 | 
9 |
10 | #### v.1.0.0
11 | - 1.主要针对上述库精简
12 | - 2.解决了内嵌 ScrollView 得滑动
13 | - 3.增加了状态回调
14 | - 4.加个人理解的中文注释方便学习
15 | - 5.还有不完善的地方
16 |
17 |
18 |
--------------------------------------------------------------------------------
/SlideNestedPanel/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/SlideNestedPanel/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | android {
4 | compileSdkVersion 28
5 |
6 |
7 |
8 | defaultConfig {
9 | minSdkVersion 19
10 | targetSdkVersion 28
11 | versionCode 1
12 | versionName "1.0"
13 |
14 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
15 |
16 | }
17 |
18 | buildTypes {
19 | release {
20 | minifyEnabled false
21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
22 | }
23 | }
24 |
25 | }
26 |
27 | dependencies {
28 | implementation fileTree(dir: 'libs', include: ['*.jar'])
29 |
30 | implementation 'com.android.support:appcompat-v7:28.0.0'
31 | implementation "com.android.support:recyclerview-v7:28.0.0"
32 | testImplementation 'junit:junit:4.12'
33 | androidTestImplementation 'com.android.support.test:runner:1.0.2'
34 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
35 | }
36 |
--------------------------------------------------------------------------------
/SlideNestedPanel/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/SlideNestedPanel/src/androidTest/java/com/snail/slidenested/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.snail.slidenested;
2 |
3 | import android.content.Context;
4 | import android.support.test.InstrumentationRegistry;
5 | import android.support.test.runner.AndroidJUnit4;
6 |
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | /**
13 | * Instrumented test, which will execute on an Android device.
14 | *
15 | * @see Testing documentation
16 | */
17 | @RunWith(AndroidJUnit4.class)
18 | public class ExampleInstrumentedTest {
19 | @Test
20 | public void useAppContext() {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("com.snail.slidenested.test", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/SlideNestedPanel/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/SlideNestedPanel/src/main/java/com/snail/slidenested/PanelLayoutParams.java:
--------------------------------------------------------------------------------
1 | package com.snail.slidenested;
2 |
3 | import android.content.Context;
4 | import android.content.res.TypedArray;
5 | import android.util.AttributeSet;
6 | import android.view.ViewGroup;
7 |
8 | /**
9 | * author:created by Snail.江
10 | * time: 2018/12/21 14:26
11 | * email:409962004@qq.com
12 | * TODO: 自定义布局属性
13 | */
14 | public class PanelLayoutParams extends ViewGroup.MarginLayoutParams {
15 |
16 | private static final int[] ATTRS = {android.R.attr.layout_weight};
17 |
18 | public float weight = 0;
19 |
20 | public PanelLayoutParams() {
21 | super(MATCH_PARENT, MATCH_PARENT);
22 | }
23 |
24 | public PanelLayoutParams(int width, int height) {
25 | super(width, height);
26 | }
27 |
28 | public PanelLayoutParams(int width, int height, int weight) {
29 | this(width, height);
30 | this.weight = weight;
31 | }
32 |
33 | public PanelLayoutParams(ViewGroup.LayoutParams source) {
34 | super(source);
35 | }
36 |
37 | public PanelLayoutParams(ViewGroup.MarginLayoutParams source) {
38 | super(source);
39 | }
40 |
41 | public PanelLayoutParams(PanelLayoutParams source) {
42 | super(source);
43 | }
44 |
45 |
46 | public PanelLayoutParams(Context c, AttributeSet attrs) {
47 | super(c, attrs);
48 |
49 | final TypedArray ta = c.obtainStyledAttributes(attrs, ATTRS);
50 | if (ta != null) {
51 | this.weight = ta.getFloat(0, 0);
52 | ta.recycle();
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/SlideNestedPanel/src/main/java/com/snail/slidenested/PanelSlideListener.java:
--------------------------------------------------------------------------------
1 | package com.snail.slidenested;
2 |
3 | import android.view.View;
4 |
5 | /**
6 | * author:created by Snail.江
7 | * time: 2018/12/21 11:55
8 | * email:409962004@qq.com
9 | * TODO: 面板滑动回调
10 | */
11 | public interface PanelSlideListener {
12 |
13 | //当面板滑动位置发送变化
14 | void onPanelSlide(View panel,float slideOffset);
15 |
16 |
17 | //当面板状态发送变化
18 | void onPanelStateChanged(View panel,PanelState preState,PanelState newState);
19 | }
20 |
--------------------------------------------------------------------------------
/SlideNestedPanel/src/main/java/com/snail/slidenested/PanelState.java:
--------------------------------------------------------------------------------
1 | package com.snail.slidenested;
2 |
3 | /**
4 | * author:created by Snail.江
5 | * time: 2018/12/21 11:56
6 | * email:409962004@qq.com
7 | * TODO: 面板状态
8 | */
9 | public enum PanelState {
10 |
11 | EXPANDED,
12 | COLLAPSED,
13 | ANCHORED,
14 | DRAGGING
15 | }
16 |
--------------------------------------------------------------------------------
/SlideNestedPanel/src/main/java/com/snail/slidenested/ScrollableViewHelper.java:
--------------------------------------------------------------------------------
1 | package com.snail.slidenested;
2 |
3 | import android.support.v4.widget.NestedScrollView;
4 | import android.support.v7.widget.RecyclerView;
5 | import android.view.View;
6 | import android.widget.FrameLayout;
7 | import android.widget.ListView;
8 | import android.widget.ScrollView;
9 |
10 | /**
11 | * Helper class for determining the current scroll positions for scrollable views. Currently works
12 | * for ListView, ScrollView and RecyclerView, but the library users can override it to add support
13 | * for other views.
14 | */
15 | public class ScrollableViewHelper {
16 | /**
17 | * Returns the current scroll position of the scrollable view. If this method returns zero or
18 | * less, it means at the scrollable view is in a position such as the panel should handle
19 | * scrolling. If the method returns anything above zero, then the panel will let the scrollable
20 | * view handle the scrolling
21 | *
22 | * @param view the scrollable view
23 | * @param isSlidingUp whether or not the panel is sliding up or down
24 | * @return the scroll position
25 | */
26 | public int getScrollableViewScrollPosition(View view, boolean isSlidingUp) {
27 | if (view == null) return 0;
28 | if (view instanceof ScrollView || view instanceof NestedScrollView) {
29 | if (isSlidingUp) {
30 | return view.getScrollY();
31 | } else {
32 | FrameLayout sv = (FrameLayout) view;
33 | View child = sv.getChildAt(0);
34 | return (child.getBottom() - (sv.getHeight() + sv.getScrollY()));
35 | }
36 | } else if (view instanceof ListView && ((ListView) view).getChildCount() > 0) {
37 | ListView lv = ((ListView) view);
38 | if (lv.getAdapter() == null) return 0;
39 | if (isSlidingUp) {
40 | View firstChild = lv.getChildAt(0);
41 | // Approximate the scroll position based on the top child and the first visible item
42 | return lv.getFirstVisiblePosition() * firstChild.getHeight() - firstChild.getTop();
43 | } else {
44 | View lastChild = lv.getChildAt(lv.getChildCount() - 1);
45 | // Approximate the scroll position based on the bottom child and the last visible item
46 | return (lv.getAdapter().getCount() - lv.getLastVisiblePosition() - 1) * lastChild.getHeight() + lastChild.getBottom() - lv.getBottom();
47 | }
48 | } else if (view instanceof RecyclerView && ((RecyclerView) view).getChildCount() > 0) {
49 | RecyclerView rv = ((RecyclerView) view);
50 | RecyclerView.LayoutManager lm = rv.getLayoutManager();
51 | if (rv.getAdapter() == null) return 0;
52 | if (isSlidingUp) {
53 | View firstChild = rv.getChildAt(0);
54 | // Approximate the scroll position based on the top child and the first visible item
55 | return rv.getChildLayoutPosition(firstChild) * lm.getDecoratedMeasuredHeight(firstChild) - lm.getDecoratedTop(firstChild);
56 | } else {
57 | View lastChild = rv.getChildAt(rv.getChildCount() - 1);
58 | // Approximate the scroll position based on the bottom child and the last visible item
59 | return (rv.getAdapter().getItemCount() - 1) * lm.getDecoratedMeasuredHeight(lastChild) + lm.getDecoratedBottom(lastChild) - rv.getBottom();
60 | }
61 | } else {
62 | return 0;
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/SlideNestedPanel/src/main/java/com/snail/slidenested/SlideNestedPanelLayout.java:
--------------------------------------------------------------------------------
1 | package com.snail.slidenested;
2 |
3 | import android.content.Context;
4 | import android.content.res.TypedArray;
5 | import android.graphics.Canvas;
6 | import android.graphics.Paint;
7 | import android.graphics.Rect;
8 | import android.support.v4.view.MotionEventCompat;
9 | import android.support.v4.view.ViewCompat;
10 | import android.util.AttributeSet;
11 | import android.view.MotionEvent;
12 | import android.view.View;
13 | import android.view.ViewGroup;
14 | import android.view.animation.AnimationUtils;
15 | import android.view.animation.Interpolator;
16 | import com.snail.slidenestedpanel.R;
17 |
18 | /**
19 | * author:created by Snail.江
20 | * time: 2018/12/20 11:13
21 | * email:409962004@qq.com
22 | * TODO: 仿美团订单,嵌套滑动拖拽控件
23 | */
24 | public class SlideNestedPanelLayout extends ViewGroup {
25 |
26 |
27 | //默认面板状态
28 | private static final PanelState DEFAULT_STATE = PanelState.COLLAPSED;
29 | //最少滑动速度
30 | private static final int DEFAULT_FLING_VELOCITY = 400;
31 | //默认渐变色
32 | private static final int DEFAULT_FADE_COLOR = 0x99000000;
33 | //默认覆盖标识
34 | private static final boolean DEFAULT_OVERLAY_FLAG = false;
35 | //默认裁剪标识
36 | private static final boolean DEFAULT_CLIP_FLAG = true;
37 | //默认基准点
38 | private static final float DEFAULT_ANCHOR_POINT = 1.0f;
39 | //默认面板高度
40 | private static final int DEFAULT_PANEL_HEIGHT = 68;
41 | //默认视觉差比例
42 | private static final int DEFAULT_PARALLAX_OFFSET = 0;
43 |
44 |
45 | //面板高度
46 | private int mPanelHeight;
47 |
48 |
49 | //视觉差比例
50 | private int mParallaxOffset;
51 |
52 |
53 | //最少滑动速度因子
54 | private int mFlingVelocity;
55 |
56 |
57 | //覆盖渐变颜色
58 | private int mFadeColor;
59 |
60 |
61 | //嵌套滑动view resId
62 | private int mScrollViewResId;
63 |
64 |
65 | //覆盖内容标识
66 | private boolean mOverlayFlag;
67 |
68 |
69 | //裁剪标识
70 | private boolean mClipPanelFlag;
71 |
72 |
73 | //面板停止基准点
74 | private float mAnchorPoint;
75 |
76 |
77 | //状态
78 | private PanelState mPanelState = DEFAULT_STATE;
79 |
80 |
81 | //拖拽助手helper
82 | private ViewDragHelper mDragHelper = null;
83 |
84 |
85 | //是否依附到窗口
86 | private boolean isAttachedToWindow = true;
87 |
88 |
89 | //锁定面板,不可滑动标识
90 | private boolean isUnableToDrag;
91 |
92 |
93 | //主View
94 | private View mMainView;
95 |
96 |
97 | //面板内拖拽view
98 | private View mDragView;
99 |
100 |
101 | //面板内Scroll View
102 | private View mScrollView;
103 |
104 |
105 | //滑动辅助
106 | private ScrollableViewHelper mScrollableViewHelper = new ScrollableViewHelper();
107 |
108 |
109 | //面板距离展开的位置,范围0-anchorPoint(0为折叠,anchorPoint为展开基准点)
110 | private float mSlideOffset;
111 |
112 |
113 | //记录下面板已滑动的范围
114 | private int mSlideRange;
115 |
116 |
117 | //是否自己处理事件
118 | private boolean isMyHandleTouch = false;
119 |
120 |
121 | //是否到顶(-1不确定、0底部、1顶部)
122 | private short isOnTopFlag = -1;
123 |
124 |
125 | //绘制区域
126 | private final Rect mTmpRect = new Rect();
127 |
128 |
129 | //当Main滑动时用来画渐变的画笔
130 | private final Paint mCoveredFadePaint = new Paint();
131 |
132 |
133 | //状态回调
134 | private StateCallback stateCallback;
135 |
136 |
137 | //标记触摸位置
138 | private float mPrevMotionX;
139 | private float mPrevMotionY;
140 | private float mInitialMotionX;
141 | private float mInitialMotionY;
142 |
143 |
144 | public SlideNestedPanelLayout(Context context) {
145 | this(context, null);
146 | }
147 |
148 | public SlideNestedPanelLayout(Context context, AttributeSet attrs) {
149 | this(context, attrs, 0);
150 | }
151 |
152 | public SlideNestedPanelLayout(Context context, AttributeSet attrs, int defStyleAttr) {
153 | super(context, attrs, defStyleAttr);
154 |
155 | //定义拖拽的插值器
156 | //获取自定义属性
157 | Interpolator scrollerInterpolator = null;
158 | if (attrs != null) {
159 | TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.SlideNestedPanelLayout);
160 | if (ta != null) {
161 | mPanelHeight = ta.getDimensionPixelSize(R.styleable.SlideNestedPanelLayout_panelHeight, -1);
162 | mParallaxOffset = ta.getDimensionPixelSize(R.styleable.SlideNestedPanelLayout_parallaxOffset, -1);
163 | mFlingVelocity = ta.getInt(R.styleable.SlideNestedPanelLayout_flingVelocity, DEFAULT_FLING_VELOCITY);
164 | mFadeColor = ta.getColor(R.styleable.SlideNestedPanelLayout_fadeColor, DEFAULT_FADE_COLOR);
165 | mScrollViewResId = ta.getResourceId(R.styleable.SlideNestedPanelLayout_scrollView, -1);
166 | mOverlayFlag = ta.getBoolean(R.styleable.SlideNestedPanelLayout_overlay, DEFAULT_OVERLAY_FLAG);
167 | mClipPanelFlag = ta.getBoolean(R.styleable.SlideNestedPanelLayout_clipPanel, DEFAULT_CLIP_FLAG);
168 | mAnchorPoint = ta.getFloat(R.styleable.SlideNestedPanelLayout_anchorPoint, DEFAULT_ANCHOR_POINT);
169 | mPanelState = PanelState.values()[ta.getInt(R.styleable.SlideNestedPanelLayout_initialState, DEFAULT_STATE.ordinal())];
170 |
171 | int interpolatorResId = ta.getResourceId(R.styleable.SlideNestedPanelLayout_interpolator, -1);
172 | if (interpolatorResId != -1) {
173 | scrollerInterpolator = AnimationUtils.loadInterpolator(context, interpolatorResId);
174 | }
175 | ta.recycle();
176 | }
177 | }
178 |
179 | //根据密度算默值
180 | final float density = context.getResources().getDisplayMetrics().density;
181 | if (mPanelHeight == -1)
182 | mPanelHeight = (int) (DEFAULT_PANEL_HEIGHT * density + 0.5f);
183 |
184 | if (mParallaxOffset == -1)
185 | mParallaxOffset = (int) (DEFAULT_PARALLAX_OFFSET * density);
186 |
187 | //不要执行onDraw
188 | setWillNotDraw(false);
189 |
190 | mDragHelper = ViewDragHelper.create(this, 1.0f, scrollerInterpolator, new DragHelperCallback());
191 | mDragHelper.setMinVelocity(mFlingVelocity * density);
192 | }
193 |
194 | @Override
195 | protected void onAttachedToWindow() {
196 | super.onAttachedToWindow();
197 | isAttachedToWindow = true;
198 | }
199 |
200 | @Override
201 | protected void onDetachedFromWindow() {
202 | super.onDetachedFromWindow();
203 | isAttachedToWindow = false;
204 | }
205 |
206 | @Override
207 | protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
208 | return new PanelLayoutParams();
209 | }
210 |
211 | @Override
212 | protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
213 | return p instanceof MarginLayoutParams
214 | ? new PanelLayoutParams((MarginLayoutParams) p)
215 | : new PanelLayoutParams(p);
216 | }
217 |
218 | @Override
219 | protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
220 | return p instanceof PanelLayoutParams && super.checkLayoutParams(p);
221 | }
222 |
223 | @Override
224 | public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
225 | return new PanelLayoutParams(getContext(), attrs);
226 | }
227 |
228 | @Override
229 | protected void onFinishInflate() {
230 | super.onFinishInflate();
231 | if (mScrollViewResId != -1)
232 | mScrollView = findViewById(mScrollViewResId);
233 | }
234 |
235 | @Override
236 | protected void onLayout(boolean changed, int l, int t, int r, int b) {
237 | final int paddingLeft = getPaddingLeft();
238 | final int paddingTop = getPaddingTop();
239 | final int childCount = getChildCount();
240 |
241 | if (isAttachedToWindow) {
242 | switch (mPanelState) {
243 | case EXPANDED:
244 | mSlideOffset = mAnchorPoint;
245 | break;
246 |
247 | case ANCHORED:
248 | mSlideOffset = mAnchorPoint;
249 | break;
250 |
251 | default:
252 | mSlideOffset = 0.f;
253 | break;
254 | }
255 | }
256 |
257 | for (int i = 0; i < childCount; i++) {
258 | final View child = getChildAt(i);
259 | final PanelLayoutParams params = (PanelLayoutParams) child.getLayoutParams();
260 |
261 | if (child.getVisibility() == GONE && (i == 0 || isAttachedToWindow))
262 | continue;
263 |
264 | final int childHeight = child.getMeasuredHeight();
265 | int childTop = paddingTop;
266 |
267 | if (child == mDragView)
268 | childTop = computePanelToPosition(mSlideOffset);
269 |
270 | final int childBottom = childTop + childHeight;
271 | final int childLeft = paddingLeft + params.leftMargin;
272 | final int childRight = childLeft + child.getMeasuredWidth();
273 | child.layout(childLeft,childTop,childRight,childBottom);
274 | }
275 |
276 | applyParallaxForCurrentSlideOffset();
277 | isAttachedToWindow = false;
278 | }
279 |
280 | @Override
281 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
282 | final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
283 | final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
284 | final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
285 | final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
286 |
287 | //宽高必须占满
288 | if (widthMode != MeasureSpec.EXACTLY && widthMode != MeasureSpec.AT_MOST)
289 | throw new IllegalArgumentException("Width 必须填满或者指定值");
290 | else if (heightMode != MeasureSpec.EXACTLY && heightMode != MeasureSpec.AT_MOST)
291 | throw new IllegalArgumentException("Height 必须填或者指定值");
292 |
293 | final int childCount = getChildCount();
294 |
295 | if (childCount != 2)
296 | throw new IllegalArgumentException("SlideNestedPanelLayout必须有2给子view");
297 |
298 | mMainView = getChildAt(0);
299 | mDragView = getChildAt(1);
300 |
301 | int layoutWidth = widthSize - getPaddingLeft() - getPaddingRight();
302 | int layoutHeight = heightSize - getPaddingTop() - getPaddingBottom();
303 |
304 | //第一步:首先测量子view的宽高
305 | for (int i = 0; i < childCount; i++) {
306 | final View child = getChildAt(i);
307 | final PanelLayoutParams params = (PanelLayoutParams) child.getLayoutParams();
308 |
309 | if (child.getVisibility() == GONE && i == 0)
310 | continue;
311 |
312 | int width = layoutWidth;
313 | int height = layoutHeight;
314 |
315 |
316 | //如果是主view,要记录需要overlay的高度
317 | if (child == mMainView) {
318 | if (!mOverlayFlag)
319 | height -= mPanelHeight;
320 | width -= params.leftMargin + params.rightMargin;
321 | } else if (child == mDragView) {
322 | height -= params.topMargin;
323 | }
324 |
325 | //判断width应该使用的Mode
326 | int childWidthSpec;
327 | if (params.width == LayoutParams.WRAP_CONTENT) {
328 | childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST);
329 | } else if (params.width == LayoutParams.MATCH_PARENT) {
330 | childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
331 | } else {
332 | childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
333 | }
334 |
335 | //判断height应该使用的Mode
336 | int childHeightSpec;
337 | if (params.height == LayoutParams.WRAP_CONTENT) {
338 | childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
339 | } else {
340 | //根据权重修正高度
341 | if (params.weight > 0 && params.weight < 1)
342 | height = (int) (height * params.weight);
343 | else if (params.height != LayoutParams.MATCH_PARENT)
344 | height = params.height;
345 | childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
346 | }
347 |
348 | child.measure(childWidthSpec, childHeightSpec);
349 |
350 | if (child == mDragView)
351 | mSlideRange = mDragView.getMeasuredHeight() - mPanelHeight;
352 | }
353 |
354 | setMeasuredDimension(widthSize, heightSize);
355 | }
356 |
357 | @Override
358 | public boolean onTouchEvent(MotionEvent event) {
359 | if (!isEnabled())
360 | return super.onTouchEvent(event);
361 | try {
362 | mDragHelper.processTouchEvent(event);
363 | return true;
364 | } catch (Exception e) {
365 | e.printStackTrace();
366 | return false;
367 | }
368 | }
369 |
370 | @Override
371 | public boolean dispatchTouchEvent(MotionEvent ev) {
372 | final int action = MotionEventCompat.getActionMasked(ev);
373 | //点击外部渐变层折叠收缩
374 | //松开,非折叠状态,且不在拖拽,点击落在dragView
375 | if (ev.getAction() == MotionEvent.ACTION_UP
376 | && !mDragHelper.isDragging()
377 | && mPanelState != PanelState.COLLAPSED
378 | && isViewUnder(mDragView, (int) ev.getX(), (int) ev.getY())) {
379 | mScrollView.scrollTo(0,0);
380 | mDragHelper.smoothSlideViewTo(mDragView,0,computePanelToPosition(0f));
381 | ViewCompat.postInvalidateOnAnimation(this);
382 |
383 | //点击渐变收缩后,把拖拽标识恢复
384 | setEnabled(true);
385 | return true;
386 | }
387 |
388 | final float x = ev.getX();
389 | final float y = ev.getY();
390 |
391 | switch (action) {
392 | case MotionEvent.ACTION_DOWN:{
393 | mPrevMotionX = x;
394 | mPrevMotionY = y;
395 | }
396 | break;
397 |
398 | case MotionEvent.ACTION_MOVE:{
399 | float dx = x - mPrevMotionX;
400 | float dy = y - mPrevMotionY;
401 | mPrevMotionX = x;
402 | mPrevMotionY = y;
403 |
404 | //横向滑动就不分发了
405 | if (Math.abs(dx) > Math.abs(dy)) {
406 | return true;
407 | }
408 |
409 | //滑动向上、向下
410 | if (dy > 0) { //收缩
411 |
412 | if (mScrollableViewHelper.getScrollableViewScrollPosition(mScrollView, true) > 0) {
413 | isMyHandleTouch = true;
414 | return super.dispatchTouchEvent(ev);
415 | }
416 |
417 | //之前子view处理了事件
418 | //我们就需要重新组合一下让面板得到一个合理的点击事件
419 | if (isMyHandleTouch) {
420 | MotionEvent up = MotionEvent.obtain(ev);
421 | up.setAction(MotionEvent.ACTION_CANCEL);
422 | super.dispatchTouchEvent(up);
423 | up.recycle();
424 | ev.setAction(MotionEvent.ACTION_DOWN);
425 | }
426 |
427 | isMyHandleTouch = false;
428 | return this.onTouchEvent(ev);
429 | } else { //展开
430 |
431 | //scrollY=0表示没滑动过,canScroll(1)表示可scroll up
432 | //逻辑或的意义:拖拽到顶后,要不要禁用外部拖拽
433 | if (isOnTopFlag == 1) {
434 | int offset = mDragView.getScrollY();
435 | boolean scroll = mScrollableViewHelper.getScrollableViewScrollPosition(mScrollView, true) > 0;
436 | setEnabled(offset == 0 || scroll);
437 | mDragHelper.abort();
438 | return super.dispatchTouchEvent(ev);
439 | }
440 |
441 | //面板是否全部展开
442 | if (mSlideOffset < mAnchorPoint) {
443 | isMyHandleTouch = false;
444 | return this.onTouchEvent(ev);
445 | }
446 |
447 | if (!isMyHandleTouch && mDragHelper.isDragging()) {
448 | mDragHelper.cancel();
449 | ev.setAction(MotionEvent.ACTION_DOWN);
450 | }
451 |
452 | isMyHandleTouch = true;
453 | return super.dispatchTouchEvent(ev);
454 | }
455 | }
456 |
457 | case MotionEvent.ACTION_UP:{
458 | //如果内嵌视图正在处理触摸,会接收到一个up事件、
459 | //我们想要清楚之前所有的拖拽状态,这样我们就不会意外拦截一个触摸事件
460 | if (isMyHandleTouch) {
461 | mDragHelper.setDragState(ViewDragHelper.STATE_IDLE);
462 | }
463 | }
464 | break;
465 | }
466 |
467 | return super.dispatchTouchEvent(ev);
468 | }
469 |
470 | @Override
471 | public boolean onInterceptTouchEvent(MotionEvent ev) {
472 | //如果scrollView处理事件,则不要拦截
473 | if (isMyHandleTouch) {
474 | mDragHelper.abort();
475 | return false;
476 | }
477 |
478 | int action = MotionEventCompat.getActionMasked(ev);
479 | float x = ev.getX();
480 | float y = ev.getY();
481 | float adx = Math.abs(x - mInitialMotionX);
482 | float ady = Math.abs(y - mInitialMotionY);
483 | int dragSlop = mDragHelper.getTouchSlop();
484 |
485 | switch (action) {
486 | case MotionEvent.ACTION_DOWN:{
487 | isUnableToDrag = false;
488 | mInitialMotionX = x;
489 | mInitialMotionY = y;
490 | if (isViewUnder(mDragView, (int) x, (int) y)) {
491 | mDragHelper.cancel();
492 | isUnableToDrag = true;
493 | return false;
494 | }
495 | }
496 | break;
497 |
498 | case MotionEvent.ACTION_MOVE:{
499 | if (ady > dragSlop && adx > ady) {
500 | mDragHelper.cancel();
501 | isUnableToDrag = true;
502 | return false;
503 | }
504 | }
505 | break;
506 |
507 | case MotionEvent.ACTION_CANCEL:
508 | case MotionEvent.ACTION_UP:{
509 | //如果抬手释放后drawView还在拖拽状态,需要调用processTouchEvent
510 | if (mDragHelper.isDragging()) {
511 | mDragHelper.processTouchEvent(ev);
512 | return true;
513 | }
514 | }
515 | break;
516 | }
517 | return mDragHelper.shouldInterceptTouchEvent(ev);
518 | }
519 |
520 | @Override
521 | protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
522 | boolean result;
523 | final int save = canvas.save();
524 | //mainView的遮盖渐变层
525 | if (mDragView != null && mDragView != child) {
526 | canvas.getClipBounds(mTmpRect);
527 | if (!mOverlayFlag) {
528 | mTmpRect.bottom = Math.min(mTmpRect.bottom, mDragView.getTop());
529 | }
530 | if (mClipPanelFlag) {
531 | canvas.clipRect(mTmpRect);
532 | }
533 | result = super.drawChild(canvas, child, drawingTime);
534 | if (mFadeColor != 0 && mSlideOffset > 0) {
535 | final int baseAlpha = (mFadeColor & 0xff000000) >>> 24;
536 | final int imag = (int) (baseAlpha * mSlideOffset);
537 | final int color = imag << 24 | (mFadeColor & 0xffffff);
538 | mCoveredFadePaint.setColor(color);
539 | canvas.drawRect(mTmpRect, mCoveredFadePaint);
540 | }
541 | } else {
542 | result = super.drawChild(canvas, child, drawingTime);
543 | }
544 |
545 | //没有合适的回调方法,只能另辟蹊径了
546 | //在这里判断dragView有没有到顶,然后把事件给内嵌view
547 | final int targetY = computePanelToPosition(mAnchorPoint);
548 | final int originalY = computePanelToPosition(0f);
549 | if (mDragView.getTop() == targetY) {
550 | //避免多次回调
551 | if (isOnTopFlag != 1 && stateCallback != null) {
552 | stateCallback.onExpandedState();
553 | }
554 | isOnTopFlag = 1;
555 | }else if (mDragView.getTop() == originalY){
556 | if (isOnTopFlag == -1 && stateCallback != null) {
557 | stateCallback.onCollapsedState();
558 | }
559 | isOnTopFlag = 0;
560 | }else {
561 | isOnTopFlag = -1;
562 | }
563 |
564 | canvas.restoreToCount(save);
565 | return result;
566 | }
567 |
568 | @Override
569 | public void computeScroll() {
570 | if (mDragHelper != null && mDragHelper.continueSettling(true)) {
571 | if (!isEnabled()) {
572 | mDragHelper.abort();
573 | return;
574 | }
575 | ViewCompat.postInvalidateOnAnimation(this);
576 | }
577 | }
578 |
579 |
580 | /**坐标是否落在target view*/
581 | private boolean isViewUnder(View view,int x,int y){
582 | if (view == null)
583 | return true;
584 | int[] viewLocation = new int[2];
585 | view.getLocationOnScreen(viewLocation);
586 | int[] parentLocation = new int[2];
587 | this.getLocationOnScreen(parentLocation);
588 | int screenX = parentLocation[0] + x;
589 | int screenY = parentLocation[1] + y;
590 | return screenX < viewLocation[0] || screenX >= viewLocation[0] + view.getWidth() ||
591 | screenY < viewLocation[1] || screenY >= viewLocation[1] + view.getHeight();
592 | }
593 |
594 |
595 | /**
596 | * 面板状态
597 | */
598 | private void setPanelStateInternal(PanelState state) {
599 | if (mPanelState == state)
600 | return;
601 | mPanelState = state;
602 | }
603 |
604 |
605 | /**
606 | * 计算滑动的偏移量
607 | */
608 | private int computePanelToPosition(float slideOffset) {
609 | int slidePixelOffset = (int) (slideOffset * mSlideRange);
610 | return getMeasuredHeight() - getPaddingBottom() - mPanelHeight - slidePixelOffset;
611 | }
612 |
613 |
614 | /**更新视觉差位置*/
615 | private void applyParallaxForCurrentSlideOffset(){
616 | if (mParallaxOffset > 0) {
617 | int offset = -(int)(mParallaxOffset * Math.max(mSlideOffset,0));
618 | mMainView.setTranslationY(offset);
619 | }
620 | }
621 |
622 |
623 | /**
624 | * 拖拽状态更新以及位置的更新
625 | * */
626 | private void onPanelDragged(int newTop) {
627 | setPanelStateInternal(PanelState.DRAGGING);
628 | //重新计算距离顶部偏移
629 | mSlideOffset = computeSlideOffset(newTop);
630 | //更新视觉差效果和分发事件
631 | applyParallaxForCurrentSlideOffset();
632 | //如果偏移是向上,覆盖则无效,需要增加main的高度
633 | LayoutParams lp = mMainView.getLayoutParams();
634 | int defaultHeight = getHeight() - getPaddingBottom() - getPaddingTop() - mPanelHeight;
635 | if (mSlideOffset <= 0 && !mOverlayFlag) {
636 | lp.height = (newTop - getPaddingBottom());
637 | if (lp.height == defaultHeight) {
638 | lp.height = LayoutParams.MATCH_PARENT;
639 | }
640 | } else if (lp.height != LayoutParams.MATCH_PARENT && !mOverlayFlag) {
641 | lp.height = LayoutParams.MATCH_PARENT;
642 | }
643 | mMainView.requestLayout();
644 | }
645 |
646 |
647 | /**
648 | * 计算滑动偏移量*/
649 | private float computeSlideOffset(int topPosition) {
650 | final int topBoundCollapsed = computePanelToPosition(0f);
651 | return (float) (topBoundCollapsed - topPosition) / mSlideRange;
652 | }
653 |
654 |
655 | /**状态回调*/
656 | public void setStateCallback(StateCallback callback){
657 | this.stateCallback = callback;
658 | }
659 |
660 |
661 | /**
662 | * 拖拽回调
663 | */
664 | private class DragHelperCallback extends ViewDragHelper.Callback {
665 |
666 | private boolean slideUp = false;
667 |
668 | @Override
669 | public boolean tryCaptureView(View child, int pointerId) {
670 | return !isUnableToDrag && child == mDragView;
671 | }
672 |
673 | @Override
674 | public void onViewDragStateChanged(int state) {
675 | if (mDragHelper != null && mDragHelper.getViewDragState() == ViewDragHelper.STATE_IDLE) {
676 | mSlideOffset = computeSlideOffset(mDragView.getTop());
677 | applyParallaxForCurrentSlideOffset();
678 |
679 | if (mSlideOffset == 1) {
680 | setPanelStateInternal(PanelState.EXPANDED);
681 | } else if (mSlideOffset == 0) {
682 | setPanelStateInternal(PanelState.COLLAPSED);
683 | } else {
684 | setPanelStateInternal(PanelState.ANCHORED);
685 | }
686 | }
687 | }
688 |
689 | @Override
690 | public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
691 | slideUp = dy > 0;//正为收缩,负为展开
692 | onPanelDragged(top);
693 | // invalidate();
694 | }
695 |
696 | @Override
697 | public void onViewReleased(View releasedChild, float xvel, float yvel) {
698 | int target;
699 | if (!slideUp) {
700 | if (mSlideOffset >= mAnchorPoint / 6) {
701 | target = computePanelToPosition(mAnchorPoint);
702 | } else {
703 | target = computePanelToPosition(0f);
704 | }
705 | }else {
706 | if (mSlideOffset >= mAnchorPoint / 3) {
707 | target = computePanelToPosition(0f);
708 | } else {
709 | target = computePanelToPosition(mAnchorPoint);
710 | }
711 | }
712 |
713 | if (mDragHelper != null) {
714 | mDragHelper.settleCapturedViewAt(releasedChild.getLeft(), target);
715 | }
716 |
717 | // invalidate();
718 | }
719 |
720 | @Override
721 | public int getViewVerticalDragRange(View child) {
722 | return mSlideRange;
723 | }
724 |
725 | @Override
726 | public int clampViewPositionVertical(View child, int top, int dy) {
727 | final int original = computePanelToPosition(0f);
728 | final int anchor = computePanelToPosition(mAnchorPoint);
729 | return Math.min(Math.max(top, anchor), original);
730 | }
731 | }
732 | }
733 |
--------------------------------------------------------------------------------
/SlideNestedPanel/src/main/java/com/snail/slidenested/StateCallback.java:
--------------------------------------------------------------------------------
1 | package com.snail.slidenested;
2 |
3 | /**
4 | * author:created by Snail.江
5 | * time: 2019/1/14 12:05
6 | * email:409962004@qq.com
7 | * TODO: 状态回调
8 | */
9 | public interface StateCallback {
10 |
11 | void onExpandedState();
12 |
13 | void onCollapsedState();
14 | }
15 |
--------------------------------------------------------------------------------
/SlideNestedPanel/src/main/java/com/snail/slidenested/ViewDragHelper.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013 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 |
18 | package com.snail.slidenested;
19 |
20 | import android.content.Context;
21 | import android.support.v4.view.MotionEventCompat;
22 | import android.support.v4.view.VelocityTrackerCompat;
23 | import android.support.v4.view.ViewCompat;
24 | import android.support.v4.widget.ScrollerCompat;
25 | import android.view.MotionEvent;
26 | import android.view.VelocityTracker;
27 | import android.view.View;
28 | import android.view.ViewConfiguration;
29 | import android.view.ViewGroup;
30 | import android.view.animation.Interpolator;
31 |
32 | import java.util.Arrays;
33 |
34 | /**
35 | * ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number
36 | * of useful operations and state tracking for allowing a user to drag and reposition
37 | * views within their parent ViewGroup.
38 | */
39 | public class ViewDragHelper {
40 | private static final String TAG = "ViewDragHelper";
41 |
42 | /**
43 | * A null/invalid pointer ID.
44 | */
45 | public static final int INVALID_POINTER = -1;
46 |
47 | /**
48 | * A view is not currently being dragged or animating as a result of a fling/snap.
49 | */
50 | public static final int STATE_IDLE = 0;
51 |
52 | /**
53 | * A view is currently being dragged. The position is currently changing as a result
54 | * of user input or simulated user input.
55 | */
56 | public static final int STATE_DRAGGING = 1;
57 |
58 | /**
59 | * A view is currently settling into place as a result of a fling or
60 | * predefined non-interactive motion.
61 | */
62 | public static final int STATE_SETTLING = 2;
63 |
64 | /**
65 | * Edge flag indicating that the left edge should be affected.
66 | */
67 | public static final int EDGE_LEFT = 1 << 0;
68 |
69 | /**
70 | * Edge flag indicating that the right edge should be affected.
71 | */
72 | public static final int EDGE_RIGHT = 1 << 1;
73 |
74 | /**
75 | * Edge flag indicating that the top edge should be affected.
76 | */
77 | public static final int EDGE_TOP = 1 << 2;
78 |
79 | /**
80 | * Edge flag indicating that the bottom edge should be affected.
81 | */
82 | public static final int EDGE_BOTTOM = 1 << 3;
83 |
84 | /**
85 | * Edge flag set indicating all edges should be affected.
86 | */
87 | public static final int EDGE_ALL = EDGE_LEFT | EDGE_TOP | EDGE_RIGHT | EDGE_BOTTOM;
88 |
89 | /**
90 | * Indicates that a check should occur along the horizontal axis
91 | */
92 | public static final int DIRECTION_HORIZONTAL = 1 << 0;
93 |
94 | /**
95 | * Indicates that a check should occur along the vertical axis
96 | */
97 | public static final int DIRECTION_VERTICAL = 1 << 1;
98 |
99 | /**
100 | * Indicates that a check should occur along all axes
101 | */
102 | public static final int DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL;
103 |
104 | private static final int EDGE_SIZE = 20; // dp
105 |
106 | private static final int BASE_SETTLE_DURATION = 256; // ms
107 | private static final int MAX_SETTLE_DURATION = 600; // ms
108 |
109 | // Current drag state; idle, dragging or settling
110 | private int mDragState;
111 |
112 | // Distance to travel before a drag may begin
113 | private int mTouchSlop;
114 |
115 | // Last known position/pointer tracking
116 | private int mActivePointerId = INVALID_POINTER;
117 | private float[] mInitialMotionX;
118 | private float[] mInitialMotionY;
119 | private float[] mLastMotionX;
120 | private float[] mLastMotionY;
121 | private int[] mInitialEdgesTouched;
122 | private int[] mEdgeDragsInProgress;
123 | private int[] mEdgeDragsLocked;
124 | private int mPointersDown;
125 |
126 | private VelocityTracker mVelocityTracker;
127 | private float mMaxVelocity;
128 | private float mMinVelocity;
129 |
130 | private int mEdgeSize;
131 | private int mTrackingEdges;
132 |
133 | private ScrollerCompat mScroller;
134 |
135 | private final Callback mCallback;
136 |
137 | private View mCapturedView;
138 | private boolean mReleaseInProgress;
139 |
140 | private final ViewGroup mParentView;
141 |
142 | /**
143 | * A Callback is used as a communication channel with the ViewDragHelper back to the
144 | * parent view using it. on*methods are invoked on siginficant events and several
145 | * accessor methods are expected to provide the ViewDragHelper with more information
146 | * about the state of the parent view upon request. The callback also makes decisions
147 | * governing the range and draggability of child views.
148 | */
149 | public static abstract class Callback {
150 | /**
151 | * Called when the drag state changes. See the STATE_* constants
152 | * for more information.
153 | *
154 | * @param state The new drag state
155 | *
156 | * @see #STATE_IDLE
157 | * @see #STATE_DRAGGING
158 | * @see #STATE_SETTLING
159 | */
160 | public void onViewDragStateChanged(int state) {}
161 |
162 | /**
163 | * Called when the captured view's position changes as the result of a drag or settle.
164 | *
165 | * @param changedView View whose position changed
166 | * @param left New X coordinate of the left edge of the view
167 | * @param top New Y coordinate of the top edge of the view
168 | * @param dx Change in X position from the last call
169 | * @param dy Change in Y position from the last call
170 | */
171 | public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {}
172 |
173 | /**
174 | * Called when a child view is captured for dragging or settling. The ID of the pointer
175 | * currently dragging the captured view is supplied. If activePointerId is
176 | * identified as {@link #INVALID_POINTER} the capture is programmatic instead of
177 | * pointer-initiated.
178 | *
179 | * @param capturedChild Child view that was captured
180 | * @param activePointerId Pointer id tracking the child capture
181 | */
182 | public void onViewCaptured(View capturedChild, int activePointerId) {}
183 |
184 | /**
185 | * Called when the child view is no longer being actively dragged.
186 | * The fling velocity is also supplied, if relevant. The velocity values may
187 | * be clamped to system minimums or maximums.
188 | *
189 | *
Calling code may decide to fling or otherwise release the view to let it
190 | * settle into place. It should do so using {@link #settleCapturedViewAt(int, int)}
191 | * or {@link #flingCapturedView(int, int, int, int)}. If the Callback invokes
192 | * one of these methods, the ViewDragHelper will enter {@link #STATE_SETTLING}
193 | * and the view capture will not fully end until it comes to a complete stop.
194 | * If neither of these methods is invoked before onViewReleased returns,
195 | * the view will stop in place and the ViewDragHelper will return to
196 | * {@link #STATE_IDLE}.
197 | *
198 | * @param releasedChild The captured child view now being released
199 | * @param xvel X velocity of the pointer as it left the screen in pixels per second.
200 | * @param yvel Y velocity of the pointer as it left the screen in pixels per second.
201 | */
202 | public void onViewReleased(View releasedChild, float xvel, float yvel) {}
203 |
204 | /**
205 | * Called when one of the subscribed edges in the parent view has been touched
206 | * by the user while no child view is currently captured.
207 | *
208 | * @param edgeFlags A combination of edge flags describing the edge(s) currently touched
209 | * @param pointerId ID of the pointer touching the described edge(s)
210 | * @see #EDGE_LEFT
211 | * @see #EDGE_TOP
212 | * @see #EDGE_RIGHT
213 | * @see #EDGE_BOTTOM
214 | */
215 | public void onEdgeTouched(int edgeFlags, int pointerId) {}
216 |
217 | /**
218 | * Called when the given edge may become locked. This can happen if an edge drag
219 | * was preliminarily rejected before beginning, but after {@link #onEdgeTouched(int, int)}
220 | * was called. This method should return true to lock this edge or false to leave it
221 | * unlocked. The default behavior is to leave edges unlocked.
222 | *
223 | * @param edgeFlags A combination of edge flags describing the edge(s) locked
224 | * @return true to lock the edge, false to leave it unlocked
225 | */
226 | public boolean onEdgeLock(int edgeFlags) {
227 | return false;
228 | }
229 |
230 | /**
231 | * Called when the user has started a deliberate drag away from one
232 | * of the subscribed edges in the parent view while no child view is currently captured.
233 | *
234 | * @param edgeFlags A combination of edge flags describing the edge(s) dragged
235 | * @param pointerId ID of the pointer touching the described edge(s)
236 | * @see #EDGE_LEFT
237 | * @see #EDGE_TOP
238 | * @see #EDGE_RIGHT
239 | * @see #EDGE_BOTTOM
240 | */
241 | public void onEdgeDragStarted(int edgeFlags, int pointerId) {}
242 |
243 | /**
244 | * Called to determine the Z-order of child views.
245 | *
246 | * @param index the ordered position to query for
247 | * @return index of the view that should be ordered at position index
248 | */
249 | public int getOrderedChildIndex(int index) {
250 | return index;
251 | }
252 |
253 | /**
254 | * Return the magnitude of a draggable child view's horizontal range of motion in pixels.
255 | * This method should return 0 for views that cannot move horizontally.
256 | *
257 | * @param child Child view to check
258 | * @return range of horizontal motion in pixels
259 | */
260 | public int getViewHorizontalDragRange(View child) {
261 | return 0;
262 | }
263 |
264 | /**
265 | * Return the magnitude of a draggable child view's vertical range of motion in pixels.
266 | * This method should return 0 for views that cannot move vertically.
267 | *
268 | * @param child Child view to check
269 | * @return range of vertical motion in pixels
270 | */
271 | public int getViewVerticalDragRange(View child) {
272 | return 0;
273 | }
274 |
275 | /**
276 | * Called when the user's input indicates that they want to capture the given child view
277 | * with the pointer indicated by pointerId. The callback should return true if the user
278 | * is permitted to drag the given view with the indicated pointer.
279 | *
280 | *
ViewDragHelper may call this method multiple times for the same view even if
281 | * the view is already captured; this indicates that a new pointer is trying to take
282 | * control of the view.
283 | *
284 | *
If this method returns true, a call to {@link #onViewCaptured(View, int)}
285 | * will follow if the capture is successful.
286 | *
287 | * @param child Child the user is attempting to capture
288 | * @param pointerId ID of the pointer attempting the capture
289 | * @return true if capture should be allowed, false otherwise
290 | */
291 | public abstract boolean tryCaptureView(View child, int pointerId);
292 |
293 | /**
294 | * Restrict the motion of the dragged child view along the horizontal axis.
295 | * The default implementation does not allow horizontal motion; the extending
296 | * class must override this method and provide the desired clamping.
297 | *
298 | *
299 | * @param child Child view being dragged
300 | * @param left Attempted motion along the X axis
301 | * @param dx Proposed change in position for left
302 | * @return The new clamped position for left
303 | */
304 | public int clampViewPositionHorizontal(View child, int left, int dx) {
305 | return 0;
306 | }
307 |
308 | /**
309 | * Restrict the motion of the dragged child view along the vertical axis.
310 | * The default implementation does not allow vertical motion; the extending
311 | * class must override this method and provide the desired clamping.
312 | *
313 | *
314 | * @param child Child view being dragged
315 | * @param top Attempted motion along the Y axis
316 | * @param dy Proposed change in position for top
317 | * @return The new clamped position for top
318 | */
319 | public int clampViewPositionVertical(View child, int top, int dy) {
320 | return 0;
321 | }
322 | }
323 |
324 | /**
325 | * Interpolator defining the animation curve for mScroller
326 | */
327 | private static final Interpolator sInterpolator = new Interpolator() {
328 | public float getInterpolation(float t) {
329 | t -= 1.0f;
330 | return t * t * t * t * t + 1.0f;
331 | }
332 | };
333 |
334 | private final Runnable mSetIdleRunnable = new Runnable() {
335 | public void run() {
336 | setDragState(STATE_IDLE);
337 | }
338 | };
339 |
340 | /**
341 | * Factory method to create a new ViewDragHelper.
342 | *
343 | * @param forParent Parent view to monitor
344 | * @param cb Callback to provide information and receive events
345 | * @return a new ViewDragHelper instance
346 | */
347 | public static ViewDragHelper create(ViewGroup forParent, Callback cb) {
348 | return new ViewDragHelper(forParent.getContext(), forParent, null, cb);
349 | }
350 |
351 | /**
352 | * Factory method to create a new ViewDragHelper with the specified interpolator.
353 | *
354 | * @param forParent Parent view to monitor
355 | * @param interpolator interpolator for scroller
356 | * @param cb Callback to provide information and receive events
357 | * @return a new ViewDragHelper instance
358 | */
359 | public static ViewDragHelper create(ViewGroup forParent, Interpolator interpolator, Callback cb) {
360 | return new ViewDragHelper(forParent.getContext(), forParent, interpolator, cb);
361 | }
362 |
363 | /**
364 | * Factory method to create a new ViewDragHelper.
365 | *
366 | * @param forParent Parent view to monitor
367 | * @param sensitivity Multiplier for how sensitive the helper should be about detecting
368 | * the start of a drag. Larger values are more sensitive. 1.0f is normal.
369 | * @param cb Callback to provide information and receive events
370 | * @return a new ViewDragHelper instance
371 | */
372 | public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) {
373 | final ViewDragHelper helper = create(forParent, cb);
374 | helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
375 | return helper;
376 | }
377 |
378 | /**
379 | * Factory method to create a new ViewDragHelper with the specified interpolator.
380 | *
381 | * @param forParent Parent view to monitor
382 | * @param sensitivity Multiplier for how sensitive the helper should be about detecting
383 | * the start of a drag. Larger values are more sensitive. 1.0f is normal.
384 | * @param interpolator interpolator for scroller
385 | * @param cb Callback to provide information and receive events
386 | * @return a new ViewDragHelper instance
387 | */
388 | public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Interpolator interpolator, Callback cb) {
389 | final ViewDragHelper helper = create(forParent, interpolator, cb);
390 | helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
391 | return helper;
392 | }
393 |
394 | /**
395 | * Apps should use ViewDragHelper.create() to get a new instance.
396 | * This will allow VDH to use internal compatibility implementations for different
397 | * platform versions.
398 | * If the interpolator is null, the default interpolator will be used.
399 | *
400 | * @param context Context to initialize config-dependent params from
401 | * @param forParent Parent view to monitor
402 | * @param interpolator interpolator for scroller
403 | */
404 | private ViewDragHelper(Context context, ViewGroup forParent, Interpolator interpolator, Callback cb) {
405 | if (forParent == null) {
406 | throw new IllegalArgumentException("Parent view may not be null");
407 | }
408 | if (cb == null) {
409 | throw new IllegalArgumentException("Callback may not be null");
410 | }
411 |
412 | mParentView = forParent;
413 | mCallback = cb;
414 |
415 | final ViewConfiguration vc = ViewConfiguration.get(context);
416 | final float density = context.getResources().getDisplayMetrics().density;
417 | mEdgeSize = (int) (EDGE_SIZE * density + 0.5f);
418 |
419 | mTouchSlop = vc.getScaledTouchSlop();
420 | mMaxVelocity = vc.getScaledMaximumFlingVelocity();
421 | mMinVelocity = vc.getScaledMinimumFlingVelocity();
422 | mScroller = ScrollerCompat.create(context, interpolator != null ? interpolator : sInterpolator);
423 | }
424 |
425 | /**
426 | * Set the minimum velocity that will be detected as having a magnitude greater than zero
427 | * in pixels per second. Callback methods accepting a velocity will be clamped appropriately.
428 | *
429 | * @param minVel Minimum velocity to detect
430 | */
431 | public void setMinVelocity(float minVel) {
432 | mMinVelocity = minVel;
433 | }
434 |
435 | /**
436 | * Return the currently configured minimum velocity. Any flings with a magnitude less
437 | * than this value in pixels per second. Callback methods accepting a velocity will receive
438 | * zero as a velocity value if the real detected velocity was below this threshold.
439 | *
440 | * @return the minimum velocity that will be detected
441 | */
442 | public float getMinVelocity() {
443 | return mMinVelocity;
444 | }
445 |
446 | /**
447 | * Retrieve the current drag state of this helper. This will return one of
448 | * {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}.
449 | * @return The current drag state
450 | */
451 | public int getViewDragState() {
452 | return mDragState;
453 | }
454 |
455 | /**
456 | * Enable edge tracking for the selected edges of the parent view.
457 | * The callback's {@link Callback#onEdgeTouched(int, int)} and
458 | * {@link Callback#onEdgeDragStarted(int, int)} methods will only be invoked
459 | * for edges for which edge tracking has been enabled.
460 | *
461 | * @param edgeFlags Combination of edge flags describing the edges to watch
462 | * @see #EDGE_LEFT
463 | * @see #EDGE_TOP
464 | * @see #EDGE_RIGHT
465 | * @see #EDGE_BOTTOM
466 | */
467 | public void setEdgeTrackingEnabled(int edgeFlags) {
468 | mTrackingEdges = edgeFlags;
469 | }
470 |
471 | /**
472 | * Return the size of an edge. This is the range in pixels along the edges of this view
473 | * that will actively detect edge touches or drags if edge tracking is enabled.
474 | *
475 | * @return The size of an edge in pixels
476 | * @see #setEdgeTrackingEnabled(int)
477 | */
478 | public int getEdgeSize() {
479 | return mEdgeSize;
480 | }
481 |
482 | /**
483 | * Capture a specific child view for dragging within the parent. The callback will be notified
484 | * but {@link Callback#tryCaptureView(View, int)} will not be asked permission to
485 | * capture this view.
486 | *
487 | * @param childView Child view to capture
488 | * @param activePointerId ID of the pointer that is dragging the captured child view
489 | */
490 | public void captureChildView(View childView, int activePointerId) {
491 | if (childView.getParent() != mParentView) {
492 | throw new IllegalArgumentException("captureChildView: parameter must be a descendant " +
493 | "of the ViewDragHelper's tracked parent view (" + mParentView + ")");
494 | }
495 |
496 | mCapturedView = childView;
497 | mActivePointerId = activePointerId;
498 | mCallback.onViewCaptured(childView, activePointerId);
499 | setDragState(STATE_DRAGGING);
500 | }
501 |
502 | /**
503 | * @return The currently captured view, or null if no view has been captured.
504 | */
505 | public View getCapturedView() {
506 | return mCapturedView;
507 | }
508 |
509 | /**
510 | * @return The ID of the pointer currently dragging the captured view,
511 | * or {@link #INVALID_POINTER}.
512 | */
513 | public int getActivePointerId() {
514 | return mActivePointerId;
515 | }
516 |
517 | /**
518 | * @return The minimum distance in pixels that the user must travel to initiate a drag
519 | */
520 | public int getTouchSlop() {
521 | return mTouchSlop;
522 | }
523 |
524 | /**
525 | * The result of a call to this method is equivalent to
526 | * {@link #processTouchEvent(MotionEvent)} receiving an ACTION_CANCEL event.
527 | */
528 | public void cancel() {
529 | mActivePointerId = INVALID_POINTER;
530 | clearMotionHistory();
531 |
532 | if (mVelocityTracker != null) {
533 | mVelocityTracker.recycle();
534 | mVelocityTracker = null;
535 | }
536 | }
537 |
538 | /**
539 | * {@link #cancel()}, but also abort all motion in progress and snap to the end of any
540 | * animation.
541 | */
542 | public void abort() {
543 | cancel();
544 | if (mDragState == STATE_SETTLING) {
545 | final int oldX = mScroller.getCurrX();
546 | final int oldY = mScroller.getCurrY();
547 | mScroller.abortAnimation();
548 | final int newX = mScroller.getCurrX();
549 | final int newY = mScroller.getCurrY();
550 | mCallback.onViewPositionChanged(mCapturedView, newX, newY, newX - oldX, newY - oldY);
551 | }
552 | setDragState(STATE_IDLE);
553 | }
554 |
555 | /**
556 | * Animate the view child to the given (left, top) position.
557 | * If this method returns true, the caller should invoke {@link #continueSettling(boolean)}
558 | * on each subsequent frame to continue the motion until it returns false. If this method
559 | * returns false there is no further work to do to complete the movement.
560 | *
561 | *
This operation does not count as a capture event, though {@link #getCapturedView()}
562 | * will still report the sliding view while the slide is in progress.
563 | *
564 | * @param child Child view to capture and animate
565 | * @param finalLeft Final left position of child
566 | * @param finalTop Final top position of child
567 | * @return true if animation should continue through {@link #continueSettling(boolean)} calls
568 | */
569 | public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) {
570 | mCapturedView = child;
571 | mActivePointerId = INVALID_POINTER;
572 |
573 | return forceSettleCapturedViewAt(finalLeft, finalTop, 0, 0);
574 | }
575 |
576 | /**
577 | * Settle the captured view at the given (left, top) position.
578 | * The appropriate velocity from prior motion will be taken into account.
579 | * If this method returns true, the caller should invoke {@link #continueSettling(boolean)}
580 | * on each subsequent frame to continue the motion until it returns false. If this method
581 | * returns false there is no further work to do to complete the movement.
582 | *
583 | * @param finalLeft Settled left edge position for the captured view
584 | * @param finalTop Settled top edge position for the captured view
585 | * @return true if animation should continue through {@link #continueSettling(boolean)} calls
586 | */
587 | public boolean settleCapturedViewAt(int finalLeft, int finalTop) {
588 | if (!mReleaseInProgress) {
589 | throw new IllegalStateException("Cannot settleCapturedViewAt outside of a call to " +
590 | "Callback#onViewReleased");
591 | }
592 |
593 | return forceSettleCapturedViewAt(finalLeft, finalTop,
594 | (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
595 | (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId));
596 | }
597 |
598 | /**
599 | * Settle the captured view at the given (left, top) position.
600 | *
601 | * @param finalLeft Target left position for the captured view
602 | * @param finalTop Target top position for the captured view
603 | * @param xvel Horizontal velocity
604 | * @param yvel Vertical velocity
605 | * @return true if animation should continue through {@link #continueSettling(boolean)} calls
606 | */
607 | private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) {
608 | final int startLeft = mCapturedView.getLeft();
609 | final int startTop = mCapturedView.getTop();
610 | final int dx = finalLeft - startLeft;
611 | final int dy = finalTop - startTop;
612 |
613 | if (dx == 0 && dy == 0) {
614 | // Nothing to do. Send callbacks, be done.
615 | mScroller.abortAnimation();
616 | setDragState(STATE_IDLE);
617 | return false;
618 | }
619 |
620 | final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel);
621 | mScroller.startScroll(startLeft, startTop, dx, dy, duration);
622 |
623 | setDragState(STATE_SETTLING);
624 | return true;
625 | }
626 |
627 | private int computeSettleDuration(View child, int dx, int dy, int xvel, int yvel) {
628 | xvel = clampMag(xvel, (int) mMinVelocity, (int) mMaxVelocity);
629 | yvel = clampMag(yvel, (int) mMinVelocity, (int) mMaxVelocity);
630 | final int absDx = Math.abs(dx);
631 | final int absDy = Math.abs(dy);
632 | final int absXVel = Math.abs(xvel);
633 | final int absYVel = Math.abs(yvel);
634 | final int addedVel = absXVel + absYVel;
635 | final int addedDistance = absDx + absDy;
636 |
637 | final float xweight = xvel != 0 ? (float) absXVel / addedVel :
638 | (float) absDx / addedDistance;
639 | final float yweight = yvel != 0 ? (float) absYVel / addedVel :
640 | (float) absDy / addedDistance;
641 |
642 | int xduration = computeAxisDuration(dx, xvel, mCallback.getViewHorizontalDragRange(child));
643 | int yduration = computeAxisDuration(dy, yvel, mCallback.getViewVerticalDragRange(child));
644 |
645 | return (int) (xduration * xweight + yduration * yweight);
646 | }
647 |
648 | private int computeAxisDuration(int delta, int velocity, int motionRange) {
649 | if (delta == 0) {
650 | return 0;
651 | }
652 |
653 | final int width = mParentView.getWidth();
654 | final int halfWidth = width / 2;
655 | final float distanceRatio = Math.min(1f, (float) Math.abs(delta) / width);
656 | final float distance = halfWidth + halfWidth *
657 | distanceInfluenceForSnapDuration(distanceRatio);
658 |
659 | int duration;
660 | velocity = Math.abs(velocity);
661 | if (velocity > 0) {
662 | duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
663 | } else {
664 | final float range = (float) Math.abs(delta) / motionRange;
665 | duration = (int) ((range + 1) * BASE_SETTLE_DURATION);
666 | }
667 | return Math.min(duration, MAX_SETTLE_DURATION);
668 | }
669 |
670 | /**
671 | * Clamp the magnitude of value for absMin and absMax.
672 | * If the value is below the minimum, it will be clamped to zero.
673 | * If the value is above the maximum, it will be clamped to the maximum.
674 | *
675 | * @param value Value to clamp
676 | * @param absMin Absolute value of the minimum significant value to return
677 | * @param absMax Absolute value of the maximum value to return
678 | * @return The clamped value with the same sign as value
679 | */
680 | private int clampMag(int value, int absMin, int absMax) {
681 | final int absValue = Math.abs(value);
682 | if (absValue < absMin) return 0;
683 | if (absValue > absMax) return value > 0 ? absMax : -absMax;
684 | return value;
685 | }
686 |
687 | /**
688 | * Clamp the magnitude of value for absMin and absMax.
689 | * If the value is below the minimum, it will be clamped to zero.
690 | * If the value is above the maximum, it will be clamped to the maximum.
691 | *
692 | * @param value Value to clamp
693 | * @param absMin Absolute value of the minimum significant value to return
694 | * @param absMax Absolute value of the maximum value to return
695 | * @return The clamped value with the same sign as value
696 | */
697 | private float clampMag(float value, float absMin, float absMax) {
698 | final float absValue = Math.abs(value);
699 | if (absValue < absMin) return 0;
700 | if (absValue > absMax) return value > 0 ? absMax : -absMax;
701 | return value;
702 | }
703 |
704 | private float distanceInfluenceForSnapDuration(float f) {
705 | f -= 0.5f; // center the values about 0.
706 | f *= 0.3f * Math.PI / 2.0f;
707 | return (float) Math.sin(f);
708 | }
709 |
710 | /**
711 | * Settle the captured view based on standard free-moving fling behavior.
712 | * The caller should invoke {@link #continueSettling(boolean)} on each subsequent frame
713 | * to continue the motion until it returns false.
714 | *
715 | * @param minLeft Minimum X position for the view's left edge
716 | * @param minTop Minimum Y position for the view's top edge
717 | * @param maxLeft Maximum X position for the view's left edge
718 | * @param maxTop Maximum Y position for the view's top edge
719 | */
720 | public void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) {
721 | if (!mReleaseInProgress) {
722 | throw new IllegalStateException("Cannot flingCapturedView outside of a call to " +
723 | "Callback#onViewReleased");
724 | }
725 |
726 | mScroller.fling(mCapturedView.getLeft(), mCapturedView.getTop(),
727 | (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
728 | (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId),
729 | minLeft, maxLeft, minTop, maxTop);
730 |
731 | setDragState(STATE_SETTLING);
732 | }
733 |
734 | /**
735 | * Move the captured settling view by the appropriate amount for the current time.
736 | * If continueSettling returns true, the caller should call it again
737 | * on the next frame to continue.
738 | *
739 | * @param deferCallbacks true if state callbacks should be deferred via posted message.
740 | * Set this to true if you are calling this method from
741 | * {@link View#computeScroll()} or similar methods
742 | * invoked as part of layout or drawing.
743 | * @return true if settle is still in progress
744 | */
745 | public boolean continueSettling(boolean deferCallbacks) {
746 | // Make sure, there is a captured view
747 | if (mCapturedView == null) {
748 | return false;
749 | }
750 | if (mDragState == STATE_SETTLING) {
751 | boolean keepGoing = mScroller.computeScrollOffset();
752 | final int x = mScroller.getCurrX();
753 | final int y = mScroller.getCurrY();
754 | final int dx = x - mCapturedView.getLeft();
755 | final int dy = y - mCapturedView.getTop();
756 |
757 | if(!keepGoing && dy != 0) { //fix #525
758 | //Invalid drag state
759 | mCapturedView.setTop(0);
760 | return true;
761 | }
762 |
763 | if (dx != 0) {
764 | mCapturedView.offsetLeftAndRight(dx);
765 | }
766 | if (dy != 0) {
767 | mCapturedView.offsetTopAndBottom(dy);
768 | }
769 |
770 | if (dx != 0 || dy != 0) {
771 | mCallback.onViewPositionChanged(mCapturedView, x, y, dx, dy);
772 | }
773 |
774 | if (keepGoing && x == mScroller.getFinalX() && y == mScroller.getFinalY()) {
775 | // Close enough. The interpolator/scroller might think we're still moving
776 | // but the user sure doesn't.
777 | mScroller.abortAnimation();
778 | keepGoing = mScroller.isFinished();
779 | }
780 |
781 | if (!keepGoing) {
782 | if (deferCallbacks) {
783 | mParentView.post(mSetIdleRunnable);
784 | } else {
785 | setDragState(STATE_IDLE);
786 | }
787 | }
788 | }
789 |
790 | return mDragState == STATE_SETTLING;
791 | }
792 |
793 | /**
794 | * Like all callback events this must happen on the UI thread, but release
795 | * involves some extra semantics. During a release (mReleaseInProgress)
796 | * is the only time it is valid to call {@link #settleCapturedViewAt(int, int)}
797 | * or {@link #flingCapturedView(int, int, int, int)}.
798 | */
799 | private void dispatchViewReleased(float xvel, float yvel) {
800 | mReleaseInProgress = true;
801 | mCallback.onViewReleased(mCapturedView, xvel, yvel);
802 | mReleaseInProgress = false;
803 |
804 | if (mDragState == STATE_DRAGGING) {
805 | // onViewReleased didn't call a method that would have changed this. Go idle.
806 | setDragState(STATE_IDLE);
807 | }
808 | }
809 |
810 | private void clearMotionHistory() {
811 | if (mInitialMotionX == null) {
812 | return;
813 | }
814 | Arrays.fill(mInitialMotionX, 0);
815 | Arrays.fill(mInitialMotionY, 0);
816 | Arrays.fill(mLastMotionX, 0);
817 | Arrays.fill(mLastMotionY, 0);
818 | Arrays.fill(mInitialEdgesTouched, 0);
819 | Arrays.fill(mEdgeDragsInProgress, 0);
820 | Arrays.fill(mEdgeDragsLocked, 0);
821 | mPointersDown = 0;
822 | }
823 |
824 | private void clearMotionHistory(int pointerId) {
825 | if (mInitialMotionX == null || mInitialMotionX.length <= pointerId) {
826 | return;
827 | }
828 | mInitialMotionX[pointerId] = 0;
829 | mInitialMotionY[pointerId] = 0;
830 | mLastMotionX[pointerId] = 0;
831 | mLastMotionY[pointerId] = 0;
832 | mInitialEdgesTouched[pointerId] = 0;
833 | mEdgeDragsInProgress[pointerId] = 0;
834 | mEdgeDragsLocked[pointerId] = 0;
835 | mPointersDown &= ~(1 << pointerId);
836 | }
837 |
838 | private void ensureMotionHistorySizeForId(int pointerId) {
839 | if (mInitialMotionX == null || mInitialMotionX.length <= pointerId) {
840 | float[] imx = new float[pointerId + 1];
841 | float[] imy = new float[pointerId + 1];
842 | float[] lmx = new float[pointerId + 1];
843 | float[] lmy = new float[pointerId + 1];
844 | int[] iit = new int[pointerId + 1];
845 | int[] edip = new int[pointerId + 1];
846 | int[] edl = new int[pointerId + 1];
847 |
848 | if (mInitialMotionX != null) {
849 | System.arraycopy(mInitialMotionX, 0, imx, 0, mInitialMotionX.length);
850 | System.arraycopy(mInitialMotionY, 0, imy, 0, mInitialMotionY.length);
851 | System.arraycopy(mLastMotionX, 0, lmx, 0, mLastMotionX.length);
852 | System.arraycopy(mLastMotionY, 0, lmy, 0, mLastMotionY.length);
853 | System.arraycopy(mInitialEdgesTouched, 0, iit, 0, mInitialEdgesTouched.length);
854 | System.arraycopy(mEdgeDragsInProgress, 0, edip, 0, mEdgeDragsInProgress.length);
855 | System.arraycopy(mEdgeDragsLocked, 0, edl, 0, mEdgeDragsLocked.length);
856 | }
857 |
858 | mInitialMotionX = imx;
859 | mInitialMotionY = imy;
860 | mLastMotionX = lmx;
861 | mLastMotionY = lmy;
862 | mInitialEdgesTouched = iit;
863 | mEdgeDragsInProgress = edip;
864 | mEdgeDragsLocked = edl;
865 | }
866 | }
867 |
868 | private void saveInitialMotion(float x, float y, int pointerId) {
869 | ensureMotionHistorySizeForId(pointerId);
870 | mInitialMotionX[pointerId] = mLastMotionX[pointerId] = x;
871 | mInitialMotionY[pointerId] = mLastMotionY[pointerId] = y;
872 | mInitialEdgesTouched[pointerId] = getEdgesTouched((int) x, (int) y);
873 | mPointersDown |= 1 << pointerId;
874 | }
875 |
876 | private void saveLastMotion(MotionEvent ev) {
877 | final int pointerCount = MotionEventCompat.getPointerCount(ev);
878 | for (int i = 0; i < pointerCount; i++) {
879 | final int pointerId = MotionEventCompat.getPointerId(ev, i);
880 | final float x = MotionEventCompat.getX(ev, i);
881 | final float y = MotionEventCompat.getY(ev, i);
882 | // Sometimes we can try and save last motion for a pointer never recorded in initial motion. In this case we just discard it.
883 | if (mLastMotionX != null && mLastMotionY != null
884 | && mLastMotionX.length > pointerId && mLastMotionY.length > pointerId) {
885 | mLastMotionX[pointerId] = x;
886 | mLastMotionY[pointerId] = y;
887 | }
888 | }
889 | }
890 |
891 | /**
892 | * Check if the given pointer ID represents a pointer that is currently down (to the best
893 | * of the ViewDragHelper's knowledge).
894 | *
895 | *
The state used to report this information is populated by the methods
896 | * {@link #shouldInterceptTouchEvent(MotionEvent)} or
897 | * {@link #processTouchEvent(MotionEvent)}. If one of these methods has not
898 | * been called for all relevant MotionEvents to track, the information reported
899 | * by this method may be stale or incorrect.
900 | *
901 | * @param pointerId pointer ID to check; corresponds to IDs provided by MotionEvent
902 | * @return true if the pointer with the given ID is still down
903 | */
904 | public boolean isPointerDown(int pointerId) {
905 | return (mPointersDown & 1 << pointerId) != 0;
906 | }
907 |
908 | void setDragState(int state) {
909 | if (mDragState != state) {
910 | mDragState = state;
911 | mCallback.onViewDragStateChanged(state);
912 | if (mDragState == STATE_IDLE) {
913 | mCapturedView = null;
914 | }
915 | }
916 | }
917 |
918 | /**
919 | * Attempt to capture the view with the given pointer ID. The callback will be involved.
920 | * This will put us into the "dragging" state. If we've already captured this view with
921 | * this pointer this method will immediately return true without consulting the callback.
922 | *
923 | * @param toCapture View to capture
924 | * @param pointerId Pointer to capture with
925 | * @return true if capture was successful
926 | */
927 | boolean tryCaptureViewForDrag(View toCapture, int pointerId) {
928 | if (toCapture == mCapturedView && mActivePointerId == pointerId) {
929 | // Already done!
930 | return true;
931 | }
932 | if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) {
933 | mActivePointerId = pointerId;
934 | captureChildView(toCapture, pointerId);
935 | return true;
936 | }
937 | return false;
938 | }
939 |
940 | /**
941 | * Tests scrollability within child views of v given a delta of dx.
942 | *
943 | * @param v View to test for horizontal scrollability
944 | * @param checkV Whether the view v passed should itself be checked for scrollability (true),
945 | * or just its children (false).
946 | * @param dx Delta scrolled in pixels along the X axis
947 | * @param dy Delta scrolled in pixels along the Y axis
948 | * @param x X coordinate of the active touch point
949 | * @param y Y coordinate of the active touch point
950 | * @return true if child views of v can be scrolled by delta of dx.
951 | */
952 | protected boolean canScroll(View v, boolean checkV, int dx, int dy, int x, int y) {
953 | if (v instanceof ViewGroup) {
954 | final ViewGroup group = (ViewGroup) v;
955 | final int scrollX = v.getScrollX();
956 | final int scrollY = v.getScrollY();
957 | final int count = group.getChildCount();
958 | // Count backwards - let topmost views consume scroll distance first.
959 | for (int i = count - 1; i >= 0; i--) {
960 | // TODO: Add versioned support here for transformed views.
961 | // This will not work for transformed views in Honeycomb+
962 | final View child = group.getChildAt(i);
963 | if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
964 | y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
965 | canScroll(child, true, dx, dy, x + scrollX - child.getLeft(),
966 | y + scrollY - child.getTop())) {
967 | return true;
968 | }
969 | }
970 | }
971 |
972 | return checkV && (ViewCompat.canScrollHorizontally(v, -dx) ||
973 | ViewCompat.canScrollVertically(v, -dy));
974 | }
975 |
976 | /**
977 | * Check if this event as provided to the parent view's onInterceptTouchEvent should
978 | * cause the parent to intercept the touch event stream.
979 | *
980 | * @param ev MotionEvent provided to onInterceptTouchEvent
981 | * @return true if the parent view should return true from onInterceptTouchEvent
982 | */
983 | public boolean shouldInterceptTouchEvent(MotionEvent ev) {
984 | final int action = MotionEventCompat.getActionMasked(ev);
985 | final int actionIndex = MotionEventCompat.getActionIndex(ev);
986 |
987 | if (action == MotionEvent.ACTION_DOWN) {
988 | // Reset things for a new event stream, just in case we didn't get
989 | // the whole previous stream.
990 | cancel();
991 | }
992 |
993 | if (mVelocityTracker == null) {
994 | mVelocityTracker = VelocityTracker.obtain();
995 | }
996 | mVelocityTracker.addMovement(ev);
997 |
998 | switch (action) {
999 | case MotionEvent.ACTION_DOWN: {
1000 | final float x = ev.getX();
1001 | final float y = ev.getY();
1002 | final int pointerId = MotionEventCompat.getPointerId(ev, 0);
1003 | saveInitialMotion(x, y, pointerId);
1004 |
1005 | final View toCapture = findTopChildUnder((int) x, (int) y);
1006 |
1007 | // Catch a settling view if possible.
1008 | if (toCapture == mCapturedView && mDragState == STATE_SETTLING) {
1009 | tryCaptureViewForDrag(toCapture, pointerId);
1010 | }
1011 |
1012 | final int edgesTouched = mInitialEdgesTouched[pointerId];
1013 | if ((edgesTouched & mTrackingEdges) != 0) {
1014 | mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
1015 | }
1016 | break;
1017 | }
1018 |
1019 | case MotionEventCompat.ACTION_POINTER_DOWN: {
1020 | final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex);
1021 | final float x = MotionEventCompat.getX(ev, actionIndex);
1022 | final float y = MotionEventCompat.getY(ev, actionIndex);
1023 |
1024 | saveInitialMotion(x, y, pointerId);
1025 |
1026 | // A ViewDragHelper can only manipulate one view at a time.
1027 | if (mDragState == STATE_IDLE) {
1028 | final int edgesTouched = mInitialEdgesTouched[pointerId];
1029 | if ((edgesTouched & mTrackingEdges) != 0) {
1030 | mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
1031 | }
1032 | } else if (mDragState == STATE_SETTLING) {
1033 | // Catch a settling view if possible.
1034 | final View toCapture = findTopChildUnder((int) x, (int) y);
1035 | if (toCapture == mCapturedView) {
1036 | tryCaptureViewForDrag(toCapture, pointerId);
1037 | }
1038 | }
1039 | break;
1040 | }
1041 |
1042 | case MotionEvent.ACTION_MOVE: {
1043 | // First to cross a touch slop over a draggable view wins. Also report edge drags.
1044 | final int pointerCount = MotionEventCompat.getPointerCount(ev);
1045 | for (int i = 0; i < pointerCount && mInitialMotionX != null && mInitialMotionY != null; i++) {
1046 | final int pointerId = MotionEventCompat.getPointerId(ev, i);
1047 | if (pointerId >= mInitialMotionX.length || pointerId >= mInitialMotionY.length) {
1048 | continue;
1049 | }
1050 | final float x = MotionEventCompat.getX(ev, i);
1051 | final float y = MotionEventCompat.getY(ev, i);
1052 | final float dx = x - mInitialMotionX[pointerId];
1053 | final float dy = y - mInitialMotionY[pointerId];
1054 |
1055 | reportNewEdgeDrags(dx, dy, pointerId);
1056 | if (mDragState == STATE_DRAGGING) {
1057 | // Callback might have started an edge drag
1058 | break;
1059 | }
1060 |
1061 | final View toCapture = findTopChildUnder((int)mInitialMotionX[pointerId], (int)mInitialMotionY[pointerId]);
1062 | if (toCapture != null && checkTouchSlop(toCapture, dx, dy) &&
1063 | tryCaptureViewForDrag(toCapture, pointerId)) {
1064 | break;
1065 | }
1066 | }
1067 | saveLastMotion(ev);
1068 | break;
1069 | }
1070 |
1071 | case MotionEventCompat.ACTION_POINTER_UP: {
1072 | final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex);
1073 | clearMotionHistory(pointerId);
1074 | break;
1075 | }
1076 |
1077 | case MotionEvent.ACTION_UP:
1078 | case MotionEvent.ACTION_CANCEL: {
1079 | cancel();
1080 | break;
1081 | }
1082 | }
1083 |
1084 | return mDragState == STATE_DRAGGING;
1085 | }
1086 |
1087 | /**
1088 | * Process a touch event received by the parent view. This method will dispatch callback events
1089 | * as needed before returning. The parent view's onTouchEvent implementation should call this.
1090 | *
1091 | * @param ev The touch event received by the parent view
1092 | */
1093 | public void processTouchEvent(MotionEvent ev) {
1094 | final int action = MotionEventCompat.getActionMasked(ev);
1095 | final int actionIndex = MotionEventCompat.getActionIndex(ev);
1096 |
1097 | if (action == MotionEvent.ACTION_DOWN) {
1098 | // Reset things for a new event stream, just in case we didn't get
1099 | // the whole previous stream.
1100 | cancel();
1101 | }
1102 |
1103 | if (mVelocityTracker == null) {
1104 | mVelocityTracker = VelocityTracker.obtain();
1105 | }
1106 | mVelocityTracker.addMovement(ev);
1107 |
1108 | switch (action) {
1109 | case MotionEvent.ACTION_DOWN: {
1110 | final float x = ev.getX();
1111 | final float y = ev.getY();
1112 | final int pointerId = MotionEventCompat.getPointerId(ev, 0);
1113 | final View toCapture = findTopChildUnder((int) x, (int) y);
1114 |
1115 | saveInitialMotion(x, y, pointerId);
1116 |
1117 | // Since the parent is already directly processing this touch event,
1118 | // there is no reason to delay for a slop before dragging.
1119 | // Start immediately if possible.
1120 | tryCaptureViewForDrag(toCapture, pointerId);
1121 |
1122 | final int edgesTouched = mInitialEdgesTouched[pointerId];
1123 | if ((edgesTouched & mTrackingEdges) != 0) {
1124 | mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
1125 | }
1126 | break;
1127 | }
1128 |
1129 | case MotionEventCompat.ACTION_POINTER_DOWN: {
1130 | final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex);
1131 | final float x = MotionEventCompat.getX(ev, actionIndex);
1132 | final float y = MotionEventCompat.getY(ev, actionIndex);
1133 |
1134 | saveInitialMotion(x, y, pointerId);
1135 |
1136 | // A ViewDragHelper can only manipulate one view at a time.
1137 | if (mDragState == STATE_IDLE) {
1138 | // If we're idle we can do anything! Treat it like a normal down event.
1139 |
1140 | final View toCapture = findTopChildUnder((int) x, (int) y);
1141 | tryCaptureViewForDrag(toCapture, pointerId);
1142 |
1143 | final int edgesTouched = mInitialEdgesTouched[pointerId];
1144 | if ((edgesTouched & mTrackingEdges) != 0) {
1145 | mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
1146 | }
1147 | } else if (isCapturedViewUnder((int) x, (int) y)) {
1148 | // We're still tracking a captured view. If the same view is under this
1149 | // point, we'll swap to controlling it with this pointer instead.
1150 | // (This will still work if we're "catching" a settling view.)
1151 |
1152 | tryCaptureViewForDrag(mCapturedView, pointerId);
1153 | }
1154 | break;
1155 | }
1156 |
1157 | case MotionEvent.ACTION_MOVE: {
1158 | if (mDragState == STATE_DRAGGING) {
1159 | final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
1160 | final float x = MotionEventCompat.getX(ev, index);
1161 | final float y = MotionEventCompat.getY(ev, index);
1162 | final int idx = (int) (x - mLastMotionX[mActivePointerId]);
1163 | final int idy = (int) (y - mLastMotionY[mActivePointerId]);
1164 |
1165 | dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);
1166 |
1167 | saveLastMotion(ev);
1168 | } else {
1169 | // Check to see if any pointer is now over a draggable view.
1170 | final int pointerCount = MotionEventCompat.getPointerCount(ev);
1171 | for (int i = 0; i < pointerCount; i++) {
1172 | final int pointerId = MotionEventCompat.getPointerId(ev, i)
1173 | ;
1174 | final float x = MotionEventCompat.getX(ev, i);
1175 | final float y = MotionEventCompat.getY(ev, i);
1176 | final float dx = x - mInitialMotionX[pointerId];
1177 | final float dy = y - mInitialMotionY[pointerId];
1178 |
1179 | reportNewEdgeDrags(dx, dy, pointerId);
1180 | if (mDragState == STATE_DRAGGING) {
1181 | // Callback might have started an edge drag.
1182 | break;
1183 | }
1184 |
1185 | final View toCapture = findTopChildUnder((int) mInitialMotionX[pointerId], (int) mInitialMotionY[pointerId]);
1186 | if (checkTouchSlop(toCapture, dx, dy) &&
1187 | tryCaptureViewForDrag(toCapture, pointerId)) {
1188 | break;
1189 | }
1190 | }
1191 | saveLastMotion(ev);
1192 | }
1193 | break;
1194 | }
1195 |
1196 | case MotionEventCompat.ACTION_POINTER_UP: {
1197 | final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex);
1198 | if (mDragState == STATE_DRAGGING && pointerId == mActivePointerId) {
1199 | // Try to find another pointer that's still holding on to the captured view.
1200 | int newActivePointer = INVALID_POINTER;
1201 | final int pointerCount = MotionEventCompat.getPointerCount(ev);
1202 | for (int i = 0; i < pointerCount; i++) {
1203 | final int id = MotionEventCompat.getPointerId(ev, i);
1204 | if (id == mActivePointerId) {
1205 | // This one's going away, skip.
1206 | continue;
1207 | }
1208 |
1209 | final float x = MotionEventCompat.getX(ev, i);
1210 | final float y = MotionEventCompat.getY(ev, i);
1211 | if (findTopChildUnder((int) x, (int) y) == mCapturedView &&
1212 | tryCaptureViewForDrag(mCapturedView, id)) {
1213 | newActivePointer = mActivePointerId;
1214 | break;
1215 | }
1216 | }
1217 |
1218 | if (newActivePointer == INVALID_POINTER) {
1219 | // We didn't find another pointer still touching the view, release it.
1220 | releaseViewForPointerUp();
1221 | }
1222 | }
1223 | clearMotionHistory(pointerId);
1224 | break;
1225 | }
1226 |
1227 | case MotionEvent.ACTION_UP: {
1228 | if (mDragState == STATE_DRAGGING) {
1229 | releaseViewForPointerUp();
1230 | }
1231 | cancel();
1232 | break;
1233 | }
1234 |
1235 | case MotionEvent.ACTION_CANCEL: {
1236 | if (mDragState == STATE_DRAGGING) {
1237 | dispatchViewReleased(0, 0);
1238 | }
1239 | cancel();
1240 | break;
1241 | }
1242 | }
1243 | }
1244 |
1245 | private void reportNewEdgeDrags(float dx, float dy, int pointerId) {
1246 | int dragsStarted = 0;
1247 | if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_LEFT)) {
1248 | dragsStarted |= EDGE_LEFT;
1249 | }
1250 | if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_TOP)) {
1251 | dragsStarted |= EDGE_TOP;
1252 | }
1253 | if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_RIGHT)) {
1254 | dragsStarted |= EDGE_RIGHT;
1255 | }
1256 | if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_BOTTOM)) {
1257 | dragsStarted |= EDGE_BOTTOM;
1258 | }
1259 |
1260 | if (dragsStarted != 0) {
1261 | mEdgeDragsInProgress[pointerId] |= dragsStarted;
1262 | mCallback.onEdgeDragStarted(dragsStarted, pointerId);
1263 | }
1264 | }
1265 |
1266 | private boolean checkNewEdgeDrag(float delta, float odelta, int pointerId, int edge) {
1267 | final float absDelta = Math.abs(delta);
1268 | final float absODelta = Math.abs(odelta);
1269 |
1270 | if ((mInitialEdgesTouched[pointerId] & edge) != edge || (mTrackingEdges & edge) == 0 ||
1271 | (mEdgeDragsLocked[pointerId] & edge) == edge ||
1272 | (mEdgeDragsInProgress[pointerId] & edge) == edge ||
1273 | (absDelta <= mTouchSlop && absODelta <= mTouchSlop)) {
1274 | return false;
1275 | }
1276 | if (absDelta < absODelta * 0.5f && mCallback.onEdgeLock(edge)) {
1277 | mEdgeDragsLocked[pointerId] |= edge;
1278 | return false;
1279 | }
1280 | return (mEdgeDragsInProgress[pointerId] & edge) == 0 && absDelta > mTouchSlop;
1281 | }
1282 |
1283 | /**
1284 | * Check if we've crossed a reasonable touch slop for the given child view.
1285 | * If the child cannot be dragged along the horizontal or vertical axis, motion
1286 | * along that axis will not count toward the slop check.
1287 | *
1288 | * @param child Child to check
1289 | * @param dx Motion since initial position along X axis
1290 | * @param dy Motion since initial position along Y axis
1291 | * @return true if the touch slop has been crossed
1292 | */
1293 | private boolean checkTouchSlop(View child, float dx, float dy) {
1294 | if (child == null) {
1295 | return false;
1296 | }
1297 | final boolean checkHorizontal = mCallback.getViewHorizontalDragRange(child) > 0;
1298 | final boolean checkVertical = mCallback.getViewVerticalDragRange(child) > 0;
1299 |
1300 | if (checkHorizontal && checkVertical) {
1301 | return dx * dx + dy * dy > mTouchSlop * mTouchSlop;
1302 | } else if (checkHorizontal) {
1303 | return Math.abs(dx) > mTouchSlop;
1304 | } else if (checkVertical) {
1305 | return Math.abs(dy) > mTouchSlop;
1306 | }
1307 | return false;
1308 | }
1309 |
1310 | /**
1311 | * Check if any pointer tracked in the current gesture has crossed
1312 | * the required slop threshold.
1313 | *
1314 | *
This depends on internal state populated by
1315 | * {@link #shouldInterceptTouchEvent(MotionEvent)} or
1316 | * {@link #processTouchEvent(MotionEvent)}. You should only rely on
1317 | * the results of this method after all currently available touch data
1318 | * has been provided to one of these two methods.
1319 | *
1320 | * @param directions Combination of direction flags, see {@link #DIRECTION_HORIZONTAL},
1321 | * {@link #DIRECTION_VERTICAL}, {@link #DIRECTION_ALL}
1322 | * @return true if the slop threshold has been crossed, false otherwise
1323 | */
1324 | public boolean checkTouchSlop(int directions) {
1325 | final int count = mInitialMotionX.length;
1326 | for (int i = 0; i < count; i++) {
1327 | if (checkTouchSlop(directions, i)) {
1328 | return true;
1329 | }
1330 | }
1331 | return false;
1332 | }
1333 |
1334 | /**
1335 | * Check if the specified pointer tracked in the current gesture has crossed
1336 | * the required slop threshold.
1337 | *
1338 | *
This depends on internal state populated by
1339 | * {@link #shouldInterceptTouchEvent(MotionEvent)} or
1340 | * {@link #processTouchEvent(MotionEvent)}. You should only rely on
1341 | * the results of this method after all currently available touch data
1342 | * has been provided to one of these two methods.
1343 | *
1344 | * @param directions Combination of direction flags, see {@link #DIRECTION_HORIZONTAL},
1345 | * {@link #DIRECTION_VERTICAL}, {@link #DIRECTION_ALL}
1346 | * @param pointerId ID of the pointer to slop check as specified by MotionEvent
1347 | * @return true if the slop threshold has been crossed, false otherwise
1348 | */
1349 | public boolean checkTouchSlop(int directions, int pointerId) {
1350 | if (!isPointerDown(pointerId)) {
1351 | return false;
1352 | }
1353 |
1354 | final boolean checkHorizontal = (directions & DIRECTION_HORIZONTAL) == DIRECTION_HORIZONTAL;
1355 | final boolean checkVertical = (directions & DIRECTION_VERTICAL) == DIRECTION_VERTICAL;
1356 |
1357 | final float dx = mLastMotionX[pointerId] - mInitialMotionX[pointerId];
1358 | final float dy = mLastMotionY[pointerId] - mInitialMotionY[pointerId];
1359 |
1360 | if (checkHorizontal && checkVertical) {
1361 | return dx * dx + dy * dy > mTouchSlop * mTouchSlop;
1362 | } else if (checkHorizontal) {
1363 | return Math.abs(dx) > mTouchSlop;
1364 | } else if (checkVertical) {
1365 | return Math.abs(dy) > mTouchSlop;
1366 | }
1367 | return false;
1368 | }
1369 |
1370 | /**
1371 | * Check if any of the edges specified were initially touched in the currently active gesture.
1372 | * If there is no currently active gesture this method will return false.
1373 | *
1374 | * @param edges Edges to check for an initial edge touch. See {@link #EDGE_LEFT},
1375 | * {@link #EDGE_TOP}, {@link #EDGE_RIGHT}, {@link #EDGE_BOTTOM} and
1376 | * {@link #EDGE_ALL}
1377 | * @return true if any of the edges specified were initially touched in the current gesture
1378 | */
1379 | public boolean isEdgeTouched(int edges) {
1380 | final int count = mInitialEdgesTouched.length;
1381 | for (int i = 0; i < count; i++) {
1382 | if (isEdgeTouched(edges, i)) {
1383 | return true;
1384 | }
1385 | }
1386 | return false;
1387 | }
1388 |
1389 | /**
1390 | * Check if any of the edges specified were initially touched by the pointer with
1391 | * the specified ID. If there is no currently active gesture or if there is no pointer with
1392 | * the given ID currently down this method will return false.
1393 | *
1394 | * @param edges Edges to check for an initial edge touch. See {@link #EDGE_LEFT},
1395 | * {@link #EDGE_TOP}, {@link #EDGE_RIGHT}, {@link #EDGE_BOTTOM} and
1396 | * {@link #EDGE_ALL}
1397 | * @return true if any of the edges specified were initially touched in the current gesture
1398 | */
1399 | public boolean isEdgeTouched(int edges, int pointerId) {
1400 | return isPointerDown(pointerId) && (mInitialEdgesTouched[pointerId] & edges) != 0;
1401 | }
1402 |
1403 | public boolean isDragging() {
1404 | return mDragState == STATE_DRAGGING;
1405 | }
1406 |
1407 | private void releaseViewForPointerUp() {
1408 | mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
1409 | final float xvel = clampMag(
1410 | VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
1411 | mMinVelocity, mMaxVelocity);
1412 | final float yvel = clampMag(
1413 | VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId),
1414 | mMinVelocity, mMaxVelocity);
1415 | dispatchViewReleased(xvel, yvel);
1416 | }
1417 |
1418 | private void dragTo(int left, int top, int dx, int dy) {
1419 | int clampedX = left;
1420 | int clampedY = top;
1421 | final int oldLeft = mCapturedView.getLeft();
1422 | final int oldTop = mCapturedView.getTop();
1423 | if (dx != 0) {
1424 | clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);
1425 | mCapturedView.offsetLeftAndRight(clampedX - oldLeft);
1426 | }
1427 | if (dy != 0) {
1428 | clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
1429 | mCapturedView.offsetTopAndBottom(clampedY - oldTop);
1430 | }
1431 |
1432 | if (dx != 0 || dy != 0) {
1433 | final int clampedDx = clampedX - oldLeft;
1434 | final int clampedDy = clampedY - oldTop;
1435 | mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY,
1436 | clampedDx, clampedDy);
1437 | }
1438 | }
1439 |
1440 | /**
1441 | * Determine if the currently captured view is under the given point in the
1442 | * parent view's coordinate system. If there is no captured view this method
1443 | * will return false.
1444 | *
1445 | * @param x X position to test in the parent's coordinate system
1446 | * @param y Y position to test in the parent's coordinate system
1447 | * @return true if the captured view is under the given point, false otherwise
1448 | */
1449 | public boolean isCapturedViewUnder(int x, int y) {
1450 | return isViewUnder(mCapturedView, x, y);
1451 | }
1452 |
1453 | /**
1454 | * Determine if the supplied view is under the given point in the
1455 | * parent view's coordinate system.
1456 | *
1457 | * @param view Child view of the parent to hit test
1458 | * @param x X position to test in the parent's coordinate system
1459 | * @param y Y position to test in the parent's coordinate system
1460 | * @return true if the supplied view is under the given point, false otherwise
1461 | */
1462 | public boolean isViewUnder(View view, int x, int y) {
1463 | if (view == null) {
1464 | return false;
1465 | }
1466 | return x >= view.getLeft() &&
1467 | x < view.getRight() &&
1468 | y >= view.getTop() &&
1469 | y < view.getBottom();
1470 | }
1471 |
1472 | /**
1473 | * Find the topmost child under the given point within the parent view's coordinate system.
1474 | * The child order is determined using {@link Callback#getOrderedChildIndex(int)}.
1475 | *
1476 | * @param x X position to test in the parent's coordinate system
1477 | * @param y Y position to test in the parent's coordinate system
1478 | * @return The topmost child view under (x, y) or null if none found.
1479 | */
1480 | public View findTopChildUnder(int x, int y) {
1481 | final int childCount = mParentView.getChildCount();
1482 | for (int i = childCount - 1; i >= 0; i--) {
1483 | final View child = mParentView.getChildAt(mCallback.getOrderedChildIndex(i));
1484 | if (x >= child.getLeft() && x < child.getRight() &&
1485 | y >= child.getTop() && y < child.getBottom()) {
1486 | return child;
1487 | }
1488 | }
1489 | return null;
1490 | }
1491 |
1492 | private int getEdgesTouched(int x, int y) {
1493 | int result = 0;
1494 |
1495 | if (x < mParentView.getLeft() + mEdgeSize) result |= EDGE_LEFT;
1496 | if (y < mParentView.getTop() + mEdgeSize) result |= EDGE_TOP;
1497 | if (x > mParentView.getRight() - mEdgeSize) result |= EDGE_RIGHT;
1498 | if (y > mParentView.getBottom() - mEdgeSize) result |= EDGE_BOTTOM;
1499 |
1500 | return result;
1501 | }
1502 | }
1503 |
--------------------------------------------------------------------------------
/SlideNestedPanel/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/SlideNestedPanel/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | slideNested
3 |
4 |
--------------------------------------------------------------------------------
/SlideNestedPanel/src/test/java/com/snail/slidenested/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.snail.slidenested;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 28
5 | defaultConfig {
6 | applicationId "com.snail.slidenested"
7 | minSdkVersion 19
8 | targetSdkVersion 28
9 | versionCode 1
10 | versionName "1.0.0"
11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
12 | }
13 | buildTypes {
14 | release {
15 | minifyEnabled false
16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
17 | }
18 | }
19 | }
20 |
21 | dependencies {
22 | implementation fileTree(dir: 'libs', include: ['*.jar'])
23 | implementation 'com.android.support.constraint:constraint-layout:1.1.3'
24 | implementation "com.android.support:appcompat-v7:28.0.0"
25 | implementation "com.android.support:design:28.0.0"
26 | testImplementation 'junit:junit:4.12'
27 | androidTestImplementation 'com.android.support.test:runner:1.0.2'
28 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
29 |
30 | implementation project(':SlideNestedPanel')
31 | }
32 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/snail/slidenested/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.snail.slidenested;
2 |
3 | import android.content.Context;
4 | import android.support.test.InstrumentationRegistry;
5 | import android.support.test.runner.AndroidJUnit4;
6 |
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | /**
13 | * Instrumented test, which will execute on an Android device.
14 | *
15 | * @see Testing documentation
16 | */
17 | @RunWith(AndroidJUnit4.class)
18 | public class ExampleInstrumentedTest {
19 | @Test
20 | public void useAppContext() {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("com.snail.slidenested", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/java/com/snail/slidenested/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.snail.slidenested;
2 |
3 | import android.graphics.Color;
4 | import android.support.design.widget.TabLayout;
5 | import android.support.v4.widget.NestedScrollView;
6 | import android.support.v7.app.AppCompatActivity;
7 | import android.os.Bundle;
8 | import android.util.Log;
9 | import android.widget.FrameLayout;
10 |
11 | public class MainActivity extends AppCompatActivity {
12 |
13 | @Override
14 | protected void onCreate(Bundle savedInstanceState) {
15 | super.onCreate(savedInstanceState);
16 | setContentView(R.layout.activity_main);
17 |
18 | final FrameLayout mFrameLayout = findViewById(R.id.frameLayout);
19 | TabLayout mTabLayout = findViewById(R.id.tabLayout);
20 | mTabLayout.setTabMode(TabLayout.MODE_SCROLLABLE);
21 | mTabLayout.addTab(mTabLayout.newTab().setText("费用说明"));
22 | mTabLayout.addTab(mTabLayout.newTab().setText("预定须知"));
23 | mTabLayout.addTab(mTabLayout.newTab().setText("退款政策"));
24 | mTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
25 | @Override
26 | public void onTabSelected(TabLayout.Tab tab) {
27 | switch (tab.getPosition()) {
28 | case 0:
29 | mFrameLayout.setBackgroundColor(Color.parseColor("#ff0000"));
30 | break;
31 |
32 | case 1:
33 | mFrameLayout.setBackgroundColor(Color.parseColor("#0000ff"));
34 | break;
35 |
36 | case 2:
37 | mFrameLayout.setBackgroundColor(Color.parseColor("#00ff00"));
38 | break;
39 | }
40 | }
41 |
42 | @Override
43 | public void onTabUnselected(TabLayout.Tab tab) {
44 |
45 | }
46 |
47 | @Override
48 | public void onTabReselected(TabLayout.Tab tab) {
49 |
50 | }
51 | });
52 |
53 | NestedScrollView mScrollView = findViewById(R.id.nestedScrollView);
54 | mScrollView.animate().translationY(-150).alpha(1.0f).setDuration(500);
55 |
56 | SlideNestedPanelLayout mPanelLayout = findViewById(R.id.slideNestedPanelLayout);
57 | mPanelLayout.setStateCallback(new StateCallback() {
58 | @Override
59 | public void onExpandedState() {
60 | Log.i("-->","onExpandedState");
61 | }
62 |
63 | @Override
64 | public void onCollapsedState() {
65 | Log.i("-->","onCollapsedState");
66 | }
67 | });
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_cardview_white_top.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/include_main_content.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
18 |
19 |
31 |
32 |
38 |
39 |
40 |
41 |
55 |
56 |
68 |
69 |
83 |
84 |
95 |
96 |
97 |
111 |
112 |
113 |
127 |
128 |
135 |
136 |
145 |
146 |
154 |
155 |
167 |
168 |
175 |
176 |
188 |
189 |
196 |
197 |
198 |
199 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/include_main_top.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
14 |
15 |
25 |
26 |
36 |
37 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BmobSnail/SlideNestedPanelLayout/76da7e08b586eb7b22bccd8174cc483e91a34881/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BmobSnail/SlideNestedPanelLayout/76da7e08b586eb7b22bccd8174cc483e91a34881/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BmobSnail/SlideNestedPanelLayout/76da7e08b586eb7b22bccd8174cc483e91a34881/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BmobSnail/SlideNestedPanelLayout/76da7e08b586eb7b22bccd8174cc483e91a34881/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BmobSnail/SlideNestedPanelLayout/76da7e08b586eb7b22bccd8174cc483e91a34881/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BmobSnail/SlideNestedPanelLayout/76da7e08b586eb7b22bccd8174cc483e91a34881/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/icon_back_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BmobSnail/SlideNestedPanelLayout/76da7e08b586eb7b22bccd8174cc483e91a34881/app/src/main/res/mipmap-xhdpi/icon_back_white.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/icon_tour_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BmobSnail/SlideNestedPanelLayout/76da7e08b586eb7b22bccd8174cc483e91a34881/app/src/main/res/mipmap-xhdpi/icon_tour_background.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/icon_tour_forward.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BmobSnail/SlideNestedPanelLayout/76da7e08b586eb7b22bccd8174cc483e91a34881/app/src/main/res/mipmap-xhdpi/icon_tour_forward.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/icon_tour_label.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BmobSnail/SlideNestedPanelLayout/76da7e08b586eb7b22bccd8174cc483e91a34881/app/src/main/res/mipmap-xhdpi/icon_tour_label.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/icon_tour_location.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BmobSnail/SlideNestedPanelLayout/76da7e08b586eb7b22bccd8174cc483e91a34881/app/src/main/res/mipmap-xhdpi/icon_tour_location.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/icon_tour_loved.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BmobSnail/SlideNestedPanelLayout/76da7e08b586eb7b22bccd8174cc483e91a34881/app/src/main/res/mipmap-xhdpi/icon_tour_loved.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/icon_tour_map.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BmobSnail/SlideNestedPanelLayout/76da7e08b586eb7b22bccd8174cc483e91a34881/app/src/main/res/mipmap-xhdpi/icon_tour_map.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BmobSnail/SlideNestedPanelLayout/76da7e08b586eb7b22bccd8174cc483e91a34881/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BmobSnail/SlideNestedPanelLayout/76da7e08b586eb7b22bccd8174cc483e91a34881/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BmobSnail/SlideNestedPanelLayout/76da7e08b586eb7b22bccd8174cc483e91a34881/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BmobSnail/SlideNestedPanelLayout/76da7e08b586eb7b22bccd8174cc483e91a34881/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #008577
4 | #00574B
5 | #D81B60
6 |
7 | #FFFFFF
8 | #666
9 | #ccc
10 | #FFD328
11 |
12 | #F2F2F2
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | SlideNestedPanelLayout
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/test/java/com/snail/slidenested/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.snail.slidenested;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 |
5 | repositories {
6 | google()
7 | jcenter()
8 | }
9 | dependencies {
10 | classpath 'com.android.tools.build:gradle:3.2.1'
11 |
12 |
13 | // NOTE: Do not place your application dependencies here; they belong
14 | // in the individual module build.gradle files
15 | }
16 | }
17 |
18 | allprojects {
19 | repositories {
20 | google()
21 | jcenter()
22 | }
23 | }
24 |
25 | task clean(type: Delete) {
26 | delete rootProject.buildDir
27 | }
28 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 |
15 |
16 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BmobSnail/SlideNestedPanelLayout/76da7e08b586eb7b22bccd8174cc483e91a34881/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/screenshot/sample.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BmobSnail/SlideNestedPanelLayout/76da7e08b586eb7b22bccd8174cc483e91a34881/screenshot/sample.gif
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':SlideNestedPanel'
2 |
--------------------------------------------------------------------------------