├── ic_launcher-web.png ├── libs ├── android-support-v4.jar └── nineoldandroids-2.4.0.jar ├── res ├── drawable-xhdpi │ ├── graphic.png │ └── ic_launcher.png ├── drawable-hdpi │ ├── ic_launcher.png │ ├── above_shadow.xml │ └── below_shadow.xml ├── drawable-mdpi │ └── ic_launcher.png ├── drawable-xxhdpi │ └── ic_launcher.png ├── menu │ └── demo.xml ├── values-v11 │ └── styles.xml ├── values-v14 │ └── styles.xml ├── values │ ├── strings.xml │ ├── attrs.xml │ └── styles.xml └── layout │ └── activity_demo.xml ├── settings.gradle ├── README.md ├── .gitattributes ├── project.properties ├── proguard-project.txt ├── AndroidManifest.xml ├── src └── com │ └── sothree │ └── slidinguppanel │ ├── ScrollerCompatIcs.java │ ├── ScrollerCompatGingerbread.java │ ├── demo │ └── DemoActivity.java │ ├── ScrollerCompat.java │ ├── SlidingUpPanelLayout.java │ └── ViewDragHelper.java └── .gitignore /ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuchen1109/AndroidSlidingUpPanel/HEAD/ic_launcher-web.png -------------------------------------------------------------------------------- /libs/android-support-v4.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuchen1109/AndroidSlidingUpPanel/HEAD/libs/android-support-v4.jar -------------------------------------------------------------------------------- /libs/nineoldandroids-2.4.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuchen1109/AndroidSlidingUpPanel/HEAD/libs/nineoldandroids-2.4.0.jar -------------------------------------------------------------------------------- /res/drawable-xhdpi/graphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuchen1109/AndroidSlidingUpPanel/HEAD/res/drawable-xhdpi/graphic.png -------------------------------------------------------------------------------- /res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuchen1109/AndroidSlidingUpPanel/HEAD/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuchen1109/AndroidSlidingUpPanel/HEAD/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuchen1109/AndroidSlidingUpPanel/HEAD/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuchen1109/AndroidSlidingUpPanel/HEAD/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':Tnt2' 2 | 3 | include 'androidslidingup' 4 | 5 | project(':androidslidingup').projectDir = new File(settingsDir, '../library/') 6 | -------------------------------------------------------------------------------- /res/menu/demo.xml: -------------------------------------------------------------------------------- 1 |
10 | -------------------------------------------------------------------------------- /res/drawable-hdpi/above_shadow.xml: -------------------------------------------------------------------------------- 1 | 2 |This class provides a platform version-independent mechanism for obeying the 28 | * current device's preferred scroll physics and fling behavior. It offers a subset of 29 | * the APIs from Scroller or OverScroller.
30 | */ 31 | public class ScrollerCompat { 32 | Object mScroller; 33 | 34 | interface ScrollerCompatImpl { 35 | Object createScroller(Context context, Interpolator interpolator); 36 | boolean isFinished(Object scroller); 37 | int getCurrX(Object scroller); 38 | int getCurrY(Object scroller); 39 | float getCurrVelocity(Object scroller); 40 | boolean computeScrollOffset(Object scroller); 41 | void startScroll(Object scroller, int startX, int startY, int dx, int dy); 42 | void startScroll(Object scroller, int startX, int startY, int dx, int dy, int duration); 43 | void fling(Object scroller, int startX, int startY, int velX, int velY, 44 | int minX, int maxX, int minY, int maxY); 45 | void fling(Object scroller, int startX, int startY, int velX, int velY, 46 | int minX, int maxX, int minY, int maxY, int overX, int overY); 47 | void abortAnimation(Object scroller); 48 | void notifyHorizontalEdgeReached(Object scroller, int startX, int finalX, int overX); 49 | void notifyVerticalEdgeReached(Object scroller, int startY, int finalY, int overY); 50 | boolean isOverScrolled(Object scroller); 51 | int getFinalX(Object scroller); 52 | int getFinalY(Object scroller); 53 | } 54 | 55 | static class ScrollerCompatImplBase implements ScrollerCompatImpl { 56 | @Override 57 | public Object createScroller(Context context, Interpolator interpolator) { 58 | return interpolator != null ? 59 | new Scroller(context, interpolator) : new Scroller(context); 60 | } 61 | 62 | @Override 63 | public boolean isFinished(Object scroller) { 64 | return ((Scroller) scroller).isFinished(); 65 | } 66 | 67 | @Override 68 | public int getCurrX(Object scroller) { 69 | return ((Scroller) scroller).getCurrX(); 70 | } 71 | 72 | @Override 73 | public int getCurrY(Object scroller) { 74 | return ((Scroller) scroller).getCurrY(); 75 | } 76 | 77 | @Override 78 | public float getCurrVelocity(Object scroller) { 79 | return 0; 80 | } 81 | 82 | @Override 83 | public boolean computeScrollOffset(Object scroller) { 84 | return ((Scroller) scroller).computeScrollOffset(); 85 | } 86 | 87 | @Override 88 | public void startScroll(Object scroller, int startX, int startY, int dx, int dy) { 89 | ((Scroller) scroller).startScroll(startX, startY, dx, dy); 90 | } 91 | 92 | @Override 93 | public void startScroll(Object scroller, int startX, int startY, int dx, int dy, 94 | int duration) { 95 | ((Scroller) scroller).startScroll(startX, startY, dx, dy, duration); 96 | } 97 | 98 | @Override 99 | public void fling(Object scroller, int startX, int startY, int velX, int velY, 100 | int minX, int maxX, int minY, int maxY) { 101 | ((Scroller) scroller).fling(startX, startY, velX, velY, minX, maxX, minY, maxY); 102 | } 103 | 104 | @Override 105 | public void fling(Object scroller, int startX, int startY, int velX, int velY, 106 | int minX, int maxX, int minY, int maxY, int overX, int overY) { 107 | ((Scroller) scroller).fling(startX, startY, velX, velY, minX, maxX, minY, maxY); 108 | } 109 | 110 | @Override 111 | public void abortAnimation(Object scroller) { 112 | ((Scroller) scroller).abortAnimation(); 113 | } 114 | 115 | @Override 116 | public void notifyHorizontalEdgeReached(Object scroller, int startX, int finalX, 117 | int overX) { 118 | // No-op 119 | } 120 | 121 | @Override 122 | public void notifyVerticalEdgeReached(Object scroller, int startY, int finalY, int overY) { 123 | // No-op 124 | } 125 | 126 | @Override 127 | public boolean isOverScrolled(Object scroller) { 128 | // Always false 129 | return false; 130 | } 131 | 132 | @Override 133 | public int getFinalX(Object scroller) { 134 | return ((Scroller) scroller).getFinalX(); 135 | } 136 | 137 | @Override 138 | public int getFinalY(Object scroller) { 139 | return ((Scroller) scroller).getFinalY(); 140 | } 141 | } 142 | 143 | static class ScrollerCompatImplGingerbread implements ScrollerCompatImpl { 144 | @Override 145 | public Object createScroller(Context context, Interpolator interpolator) { 146 | return ScrollerCompatGingerbread.createScroller(context, interpolator); 147 | } 148 | 149 | @Override 150 | public boolean isFinished(Object scroller) { 151 | return ScrollerCompatGingerbread.isFinished(scroller); 152 | } 153 | 154 | @Override 155 | public int getCurrX(Object scroller) { 156 | return ScrollerCompatGingerbread.getCurrX(scroller); 157 | } 158 | 159 | @Override 160 | public int getCurrY(Object scroller) { 161 | return ScrollerCompatGingerbread.getCurrY(scroller); 162 | } 163 | 164 | @Override 165 | public float getCurrVelocity(Object scroller) { 166 | return 0; 167 | } 168 | 169 | @Override 170 | public boolean computeScrollOffset(Object scroller) { 171 | return ScrollerCompatGingerbread.computeScrollOffset(scroller); 172 | } 173 | 174 | @Override 175 | public void startScroll(Object scroller, int startX, int startY, int dx, int dy) { 176 | ScrollerCompatGingerbread.startScroll(scroller, startX, startY, dx, dy); 177 | } 178 | 179 | @Override 180 | public void startScroll(Object scroller, int startX, int startY, int dx, int dy, 181 | int duration) { 182 | ScrollerCompatGingerbread.startScroll(scroller, startX, startY, dx, dy, duration); 183 | } 184 | 185 | @Override 186 | public void fling(Object scroller, int startX, int startY, int velX, int velY, 187 | int minX, int maxX, int minY, int maxY) { 188 | ScrollerCompatGingerbread.fling(scroller, startX, startY, velX, velY, 189 | minX, maxX, minY, maxY); 190 | } 191 | 192 | @Override 193 | public void fling(Object scroller, int startX, int startY, int velX, int velY, 194 | int minX, int maxX, int minY, int maxY, int overX, int overY) { 195 | ScrollerCompatGingerbread.fling(scroller, startX, startY, velX, velY, 196 | minX, maxX, minY, maxY, overX, overY); 197 | } 198 | 199 | @Override 200 | public void abortAnimation(Object scroller) { 201 | ScrollerCompatGingerbread.abortAnimation(scroller); 202 | } 203 | 204 | @Override 205 | public void notifyHorizontalEdgeReached(Object scroller, int startX, int finalX, 206 | int overX) { 207 | ScrollerCompatGingerbread.notifyHorizontalEdgeReached(scroller, startX, finalX, overX); 208 | } 209 | 210 | @Override 211 | public void notifyVerticalEdgeReached(Object scroller, int startY, int finalY, int overY) { 212 | ScrollerCompatGingerbread.notifyVerticalEdgeReached(scroller, startY, finalY, overY); 213 | } 214 | 215 | @Override 216 | public boolean isOverScrolled(Object scroller) { 217 | return ScrollerCompatGingerbread.isOverScrolled(scroller); 218 | } 219 | 220 | @Override 221 | public int getFinalX(Object scroller) { 222 | return ScrollerCompatGingerbread.getFinalX(scroller); 223 | } 224 | 225 | @Override 226 | public int getFinalY(Object scroller) { 227 | return ScrollerCompatGingerbread.getFinalY(scroller); 228 | } 229 | } 230 | 231 | static class ScrollerCompatImplIcs extends ScrollerCompatImplGingerbread { 232 | @Override 233 | public float getCurrVelocity(Object scroller) { 234 | return ScrollerCompatIcs.getCurrVelocity(scroller); 235 | } 236 | } 237 | 238 | static final ScrollerCompatImpl IMPL; 239 | static { 240 | final int version = Build.VERSION.SDK_INT; 241 | if (version >= 14) { // ICS 242 | IMPL = new ScrollerCompatImplIcs(); 243 | } else if (version >= 9) { // Gingerbread 244 | IMPL = new ScrollerCompatImplGingerbread(); 245 | } else { 246 | IMPL = new ScrollerCompatImplBase(); 247 | } 248 | } 249 | 250 | public static ScrollerCompat create(Context context) { 251 | return create(context, null); 252 | } 253 | 254 | public static ScrollerCompat create(Context context, Interpolator interpolator) { 255 | return new ScrollerCompat(context, interpolator); 256 | } 257 | 258 | ScrollerCompat(Context context, Interpolator interpolator) { 259 | mScroller = IMPL.createScroller(context, interpolator); 260 | } 261 | 262 | /** 263 | * Returns whether the scroller has finished scrolling. 264 | * 265 | * @return True if the scroller has finished scrolling, false otherwise. 266 | */ 267 | public boolean isFinished() { 268 | return IMPL.isFinished(mScroller); 269 | } 270 | 271 | /** 272 | * Returns the current X offset in the scroll. 273 | * 274 | * @return The new X offset as an absolute distance from the origin. 275 | */ 276 | public int getCurrX() { 277 | return IMPL.getCurrX(mScroller); 278 | } 279 | 280 | /** 281 | * Returns the current Y offset in the scroll. 282 | * 283 | * @return The new Y offset as an absolute distance from the origin. 284 | */ 285 | public int getCurrY() { 286 | return IMPL.getCurrY(mScroller); 287 | } 288 | 289 | /** 290 | * @return The final X position for the scroll in progress, if known. 291 | */ 292 | public int getFinalX() { 293 | return IMPL.getFinalX(mScroller); 294 | } 295 | 296 | /** 297 | * @return The final Y position for the scroll in progress, if known. 298 | */ 299 | public int getFinalY() { 300 | return IMPL.getFinalY(mScroller); 301 | } 302 | 303 | /** 304 | * Returns the current velocity on platform versions that support it. 305 | * 306 | *The device must support at least API level 14 (Ice Cream Sandwich). 307 | * On older platform versions this method will return 0. This method should 308 | * only be used as input for nonessential visual effects such as {@link EdgeEffectCompat}.
309 | * 310 | * @return The original velocity less the deceleration. Result may be 311 | * negative. 312 | */ 313 | public float getCurrVelocity() { 314 | return IMPL.getCurrVelocity(mScroller); 315 | } 316 | 317 | /** 318 | * Call this when you want to know the new location. If it returns true, 319 | * the animation is not yet finished. loc will be altered to provide the 320 | * new location. 321 | */ 322 | public boolean computeScrollOffset() { 323 | return IMPL.computeScrollOffset(mScroller); 324 | } 325 | 326 | /** 327 | * Start scrolling by providing a starting point and the distance to travel. 328 | * The scroll will use the default value of 250 milliseconds for the 329 | * duration. 330 | * 331 | * @param startX Starting horizontal scroll offset in pixels. Positive 332 | * numbers will scroll the content to the left. 333 | * @param startY Starting vertical scroll offset in pixels. Positive numbers 334 | * will scroll the content up. 335 | * @param dx Horizontal distance to travel. Positive numbers will scroll the 336 | * content to the left. 337 | * @param dy Vertical distance to travel. Positive numbers will scroll the 338 | * content up. 339 | */ 340 | public void startScroll(int startX, int startY, int dx, int dy) { 341 | IMPL.startScroll(mScroller, startX, startY, dx, dy); 342 | } 343 | 344 | /** 345 | * Start scrolling by providing a starting point and the distance to travel. 346 | * 347 | * @param startX Starting horizontal scroll offset in pixels. Positive 348 | * numbers will scroll the content to the left. 349 | * @param startY Starting vertical scroll offset in pixels. Positive numbers 350 | * will scroll the content up. 351 | * @param dx Horizontal distance to travel. Positive numbers will scroll the 352 | * content to the left. 353 | * @param dy Vertical distance to travel. Positive numbers will scroll the 354 | * content up. 355 | * @param duration Duration of the scroll in milliseconds. 356 | */ 357 | public void startScroll(int startX, int startY, int dx, int dy, int duration) { 358 | IMPL.startScroll(mScroller, startX, startY, dx, dy, duration); 359 | } 360 | 361 | /** 362 | * Start scrolling based on a fling gesture. The distance travelled will 363 | * depend on the initial velocity of the fling. 364 | * 365 | * @param startX Starting point of the scroll (X) 366 | * @param startY Starting point of the scroll (Y) 367 | * @param velocityX Initial velocity of the fling (X) measured in pixels per 368 | * second. 369 | * @param velocityY Initial velocity of the fling (Y) measured in pixels per 370 | * second 371 | * @param minX Minimum X value. The scroller will not scroll past this 372 | * point. 373 | * @param maxX Maximum X value. The scroller will not scroll past this 374 | * point. 375 | * @param minY Minimum Y value. The scroller will not scroll past this 376 | * point. 377 | * @param maxY Maximum Y value. The scroller will not scroll past this 378 | * point. 379 | */ 380 | public void fling(int startX, int startY, int velocityX, int velocityY, 381 | int minX, int maxX, int minY, int maxY) { 382 | IMPL.fling(mScroller, startX, startY, velocityX, velocityY, minX, maxX, minY, maxY); 383 | } 384 | 385 | /** 386 | * Start scrolling based on a fling gesture. The distance travelled will 387 | * depend on the initial velocity of the fling. 388 | * 389 | * @param startX Starting point of the scroll (X) 390 | * @param startY Starting point of the scroll (Y) 391 | * @param velocityX Initial velocity of the fling (X) measured in pixels per 392 | * second. 393 | * @param velocityY Initial velocity of the fling (Y) measured in pixels per 394 | * second 395 | * @param minX Minimum X value. The scroller will not scroll past this 396 | * point. 397 | * @param maxX Maximum X value. The scroller will not scroll past this 398 | * point. 399 | * @param minY Minimum Y value. The scroller will not scroll past this 400 | * point. 401 | * @param maxY Maximum Y value. The scroller will not scroll past this 402 | * point. 403 | * @param overX Overfling range. If > 0, horizontal overfling in either 404 | * direction will be possible. 405 | * @param overY Overfling range. If > 0, vertical overfling in either 406 | * direction will be possible. 407 | */ 408 | public void fling(int startX, int startY, int velocityX, int velocityY, 409 | int minX, int maxX, int minY, int maxY, int overX, int overY) { 410 | IMPL.fling(mScroller, startX, startY, velocityX, velocityY, 411 | minX, maxX, minY, maxY, overX, overY); 412 | } 413 | 414 | /** 415 | * Stops the animation. Aborting the animation causes the scroller to move to the final x and y 416 | * position. 417 | */ 418 | public void abortAnimation() { 419 | IMPL.abortAnimation(mScroller); 420 | } 421 | 422 | 423 | /** 424 | * Notify the scroller that we've reached a horizontal boundary. 425 | * Normally the information to handle this will already be known 426 | * when the animation is started, such as in a call to one of the 427 | * fling functions. However there are cases where this cannot be known 428 | * in advance. This function will transition the current motion and 429 | * animate from startX to finalX as appropriate. 430 | * 431 | * @param startX Starting/current X position 432 | * @param finalX Desired final X position 433 | * @param overX Magnitude of overscroll allowed. This should be the maximum 434 | * desired distance from finalX. Absolute value - must be positive. 435 | */ 436 | public void notifyHorizontalEdgeReached(int startX, int finalX, int overX) { 437 | IMPL.notifyHorizontalEdgeReached(mScroller, startX, finalX, overX); 438 | } 439 | 440 | /** 441 | * Notify the scroller that we've reached a vertical boundary. 442 | * Normally the information to handle this will already be known 443 | * when the animation is started, such as in a call to one of the 444 | * fling functions. However there are cases where this cannot be known 445 | * in advance. This function will animate a parabolic motion from 446 | * startY to finalY. 447 | * 448 | * @param startY Starting/current Y position 449 | * @param finalY Desired final Y position 450 | * @param overY Magnitude of overscroll allowed. This should be the maximum 451 | * desired distance from finalY. Absolute value - must be positive. 452 | */ 453 | public void notifyVerticalEdgeReached(int startY, int finalY, int overY) { 454 | IMPL.notifyVerticalEdgeReached(mScroller, startY, finalY, overY); 455 | } 456 | 457 | /** 458 | * Returns whether the current Scroller is currently returning to a valid position. 459 | * Valid bounds were provided by the 460 | * {@link #fling(int, int, int, int, int, int, int, int, int, int)} method. 461 | * 462 | * One should check this value before calling 463 | * {@link #startScroll(int, int, int, int)} as the interpolation currently in progress 464 | * to restore a valid position will then be stopped. The caller has to take into account 465 | * the fact that the started scroll will start from an overscrolled position. 466 | * 467 | * @return true when the current position is overscrolled and in the process of 468 | * interpolating back to a valid value. 469 | */ 470 | public boolean isOverScrolled() { 471 | return IMPL.isOverScrolled(mScroller); 472 | } 473 | } 474 | -------------------------------------------------------------------------------- /src/com/sothree/slidinguppanel/SlidingUpPanelLayout.java: -------------------------------------------------------------------------------- 1 | package com.sothree.slidinguppanel; 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.PixelFormat; 8 | import android.graphics.Rect; 9 | import android.graphics.drawable.Drawable; 10 | import android.os.Build; 11 | import android.os.Parcel; 12 | import android.os.Parcelable; 13 | import android.support.v4.view.MotionEventCompat; 14 | import android.support.v4.view.ViewCompat; 15 | import android.util.AttributeSet; 16 | import android.util.Log; 17 | import android.view.Gravity; 18 | import android.view.MotionEvent; 19 | import android.view.SoundEffectConstants; 20 | import android.view.View; 21 | import android.view.ViewConfiguration; 22 | import android.view.ViewGroup; 23 | import android.view.accessibility.AccessibilityEvent; 24 | 25 | import com.nineoldandroids.view.animation.AnimatorProxy; 26 | import com.sothree.slidinguppanel.demo.R; 27 | 28 | public class SlidingUpPanelLayout extends ViewGroup { 29 | 30 | private static final String TAG = SlidingUpPanelLayout.class.getSimpleName(); 31 | 32 | /** 33 | * 默认panel高度 34 | */ 35 | private static final int DEFAULT_PANEL_HEIGHT = 68; // dp; 36 | 37 | /** 38 | * 默认阴影的高度 39 | */ 40 | private static final int DEFAULT_SHADOW_HEIGHT = 4; // dp; 41 | 42 | /** 43 | * 默认蒙层颜色 44 | */ 45 | private static final int DEFAULT_FADE_COLOR = 0x99000000; 46 | 47 | /** 48 | * 默认最低快速滑动的阀值 49 | */ 50 | private static final int DEFAULT_MIN_FLING_VELOCITY = 400; // dips per second 51 | 52 | /** 53 | * 默认是否在mMainview上加一层蒙层 54 | */ 55 | private static final boolean DEFAULT_OVERLAY_FLAG = false; 56 | 57 | /** 58 | * 默认定义要解析的属性 59 | */ 60 | private static final int[] DEFAULT_ATTRS = new int[] { 61 | android.R.attr.gravity 62 | }; 63 | 64 | /** 65 | * fling最低速度阀值 66 | */ 67 | private int mMinFlingVelocity = DEFAULT_MIN_FLING_VELOCITY; 68 | 69 | /** 70 | * 蒙层颜色 71 | */ 72 | private int mCoveredFadeColor = DEFAULT_FADE_COLOR; 73 | 74 | /** 75 | * 默认定义在滑动时,mMainView的偏移值 76 | */ 77 | private static final int DEFAULT_PARALAX_OFFSET = 0; 78 | 79 | /** 80 | * 画蒙层的paint 81 | */ 82 | private final Paint mCoveredFadePaint = new Paint(); 83 | 84 | /** 85 | * 画阴影的drawable 86 | */ 87 | private final Drawable mShadowDrawable; 88 | 89 | /** 90 | * slideable view折叠时的高度 单位像素 91 | */ 92 | private int mPanelHeight = -1; 93 | 94 | /** 95 | * 阴影的高度 96 | */ 97 | private int mShadowHeight = -1; 98 | 99 | /** 100 | * 定义mMainView的最大偏移值 101 | */ 102 | private int mParalaxOffset = -1; 103 | 104 | /** 105 | * 若为true,定义slideable view向上滑动为展开 106 | */ 107 | private boolean mIsSlidingUp; 108 | 109 | /** 110 | * 若为true,表示panel可以滑动 111 | */ 112 | private boolean mCanSlide; 113 | 114 | /** 115 | * 若为false 表示会在mMainview上加上一层蒙层 116 | */ 117 | private boolean mOverlayContent = DEFAULT_OVERLAY_FLAG; 118 | 119 | /** 120 | * 可用来拖动的view 121 | */ 122 | private View mDragView; 123 | 124 | /** 125 | * 对应mDragView 126 | */ 127 | private int mDragViewResId = -1; 128 | 129 | /** 130 | * 可被滑动的view 131 | */ 132 | private View mSlideableView; 133 | 134 | /** 135 | * main view 一般是第一个索引的child view 136 | */ 137 | private View mMainView; 138 | 139 | /** 140 | * 定义可滑动slideable view的状态 141 | */ 142 | private enum SlideState { 143 | EXPANDED, 144 | COLLAPSED, 145 | ANCHORED,//类似锚点的功能 146 | } 147 | //记录当前slideable view的状态 148 | private SlideState mSlideState = SlideState.COLLAPSED; 149 | 150 | /** 151 | * 当前slideable view的滑动位置 是个比值 range[0,1] 0 = 展开, 1 = 收起 152 | */ 153 | private float mSlideOffset; 154 | 155 | /** 156 | * slideable view能滑动的最大距离 单位像素 157 | */ 158 | private int mSlideRange; 159 | 160 | /** 161 | * 若为true 表示不能够继续拖动 162 | */ 163 | private boolean mIsUnableToDrag; 164 | 165 | /** 166 | * 一个flag 来标示是否激活滑动功能 167 | */ 168 | private boolean mIsSlidingEnabled; 169 | 170 | /** 171 | * 若为true,此flag表示drag view想自己处理内部触摸事件,drag view可以水平滚动和处理点击事件 172 | * 默认这个值是false 173 | */ 174 | private boolean mIsUsingDragViewTouchEvents; 175 | 176 | /** 177 | * 定义最低可滑动的距离 单位像素 178 | */ 179 | private final int mScrollTouchSlop; 180 | 181 | //触摸事件down时,会记录point的x、y值 182 | private float mInitialMotionX; 183 | private float mInitialMotionY; 184 | 185 | /** 186 | * 锚点 有效值范围[0,1] 187 | */ 188 | private float mAnchorPoint = 0.f; 189 | 190 | /** 191 | * Panel滑动动作监听 192 | */ 193 | private PanelSlideListener mPanelSlideListener; 194 | 195 | /** 196 | * 辅助类 用于处理滑动的细节 197 | */ 198 | private final ViewDragHelper mDragHelper; 199 | 200 | /** 201 | * 标示是否需要重新初始化 202 | */ 203 | private boolean mFirstLayout = true; 204 | 205 | /** 206 | * 画main view和蒙层的区域大小 207 | */ 208 | private final Rect mTmpRect = new Rect(); 209 | 210 | /** 211 | * Panel滑动动作监听 212 | */ 213 | public interface PanelSlideListener { 214 | 215 | /** 216 | * 正在drag时,若有有效的滑动距离,会回调此函数 217 | * @param panel 218 | * @param slideOffset 219 | */ 220 | public void onPanelSlide(View panel, float slideOffset); 221 | 222 | /** 223 | * Panel收起时回调 224 | * @param panel 225 | */ 226 | public void onPanelCollapsed(View panel); 227 | 228 | /** 229 | * Panel展开时回调 230 | * @param panel 231 | */ 232 | public void onPanelExpanded(View panel); 233 | 234 | /** 235 | * Panel滑到锚点时,会回调 236 | * @param panel 237 | */ 238 | public void onPanelAnchored(View panel); 239 | } 240 | 241 | /** 242 | * 如果你不想实现PanelSlideListener的全部函数,可使用此 243 | */ 244 | public static class SimplePanelSlideListener implements PanelSlideListener { 245 | @Override 246 | public void onPanelSlide(View panel, float slideOffset) { 247 | } 248 | @Override 249 | public void onPanelCollapsed(View panel) { 250 | } 251 | @Override 252 | public void onPanelExpanded(View panel) { 253 | } 254 | @Override 255 | public void onPanelAnchored(View panel) { 256 | } 257 | } 258 | 259 | //构造函数 260 | public SlidingUpPanelLayout(Context context) { 261 | this(context, null); 262 | } 263 | 264 | //构造函数 265 | public SlidingUpPanelLayout(Context context, AttributeSet attrs) { 266 | this(context, attrs, 0); 267 | } 268 | 269 | //构造函数 270 | public SlidingUpPanelLayout(Context context, AttributeSet attrs, int defStyle) { 271 | super(context, attrs, defStyle); 272 | 273 | //兼容一些android提供的可视化工具做的处理 274 | if(isInEditMode()) { 275 | mShadowDrawable = null; 276 | mScrollTouchSlop = 0; 277 | mDragHelper = null; 278 | return; 279 | } 280 | 281 | //解析系统属性 282 | if (attrs != null) { 283 | TypedArray defAttrs = context.obtainStyledAttributes(attrs, DEFAULT_ATTRS); 284 | 285 | if (defAttrs != null) { 286 | int gravity = defAttrs.getInt(0, Gravity.NO_GRAVITY); 287 | if (gravity != Gravity.TOP && gravity != Gravity.BOTTOM) { 288 | throw new IllegalArgumentException("gravity must be set to either top or bottom"); 289 | } 290 | mIsSlidingUp = gravity == Gravity.BOTTOM; 291 | } 292 | 293 | defAttrs.recycle(); 294 | 295 | TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.SlidingUpPanelLayout); 296 | 297 | //解析自定义的属性 298 | if (ta != null) { 299 | mPanelHeight = ta.getDimensionPixelSize(R.styleable.SlidingUpPanelLayout_panelHeight, -1); 300 | mShadowHeight = ta.getDimensionPixelSize(R.styleable.SlidingUpPanelLayout_shadowHeight, -1); 301 | mParalaxOffset = ta.getDimensionPixelSize(R.styleable.SlidingUpPanelLayout_paralaxOffset, -1); 302 | 303 | mMinFlingVelocity = ta.getInt(R.styleable.SlidingUpPanelLayout_flingVelocity, DEFAULT_MIN_FLING_VELOCITY); 304 | mCoveredFadeColor = ta.getColor(R.styleable.SlidingUpPanelLayout_fadeColor, DEFAULT_FADE_COLOR); 305 | 306 | mDragViewResId = ta.getResourceId(R.styleable.SlidingUpPanelLayout_dragView, -1); 307 | 308 | mOverlayContent = ta.getBoolean(R.styleable.SlidingUpPanelLayout_overlay,DEFAULT_OVERLAY_FLAG); 309 | } 310 | 311 | ta.recycle(); 312 | } 313 | 314 | //若在xml为定义某些属性,会在此初始化值 315 | final float density = context.getResources().getDisplayMetrics().density; 316 | if (mPanelHeight == -1) { 317 | mPanelHeight = (int) (DEFAULT_PANEL_HEIGHT * density + 0.5f); 318 | } 319 | if (mShadowHeight == -1) { 320 | mShadowHeight = (int) (DEFAULT_SHADOW_HEIGHT * density + 0.5f); 321 | } 322 | if (mParalaxOffset == -1) { 323 | mParalaxOffset = (int) (DEFAULT_PARALAX_OFFSET * density); 324 | } 325 | // If the shadow height is zero, don't show the shadow 326 | if (mShadowHeight > 0) { 327 | if (mIsSlidingUp) { 328 | mShadowDrawable = getResources().getDrawable(R.drawable.above_shadow); 329 | } else { 330 | mShadowDrawable = getResources().getDrawable(R.drawable.below_shadow); 331 | } 332 | 333 | } else { 334 | mShadowDrawable = null; 335 | } 336 | 337 | setWillNotDraw(false); 338 | 339 | //用来处理滑动的工具类 340 | mDragHelper = ViewDragHelper.create(this, 0.5f, new DragHelperCallback()); 341 | mDragHelper.setMinVelocity(mMinFlingVelocity * density); 342 | 343 | mCanSlide = true; 344 | mIsSlidingEnabled = true; 345 | 346 | ViewConfiguration vc = ViewConfiguration.get(context); 347 | mScrollTouchSlop = vc.getScaledTouchSlop(); 348 | } 349 | 350 | /** 351 | * 在view inflate后,初始化mDragView 352 | */ 353 | @Override 354 | protected void onFinishInflate() { 355 | super.onFinishInflate(); 356 | if (mDragViewResId != -1) { 357 | mDragView = findViewById(mDragViewResId); 358 | } 359 | } 360 | 361 | /** 362 | * 设置蒙层的颜色 363 | * @param color 364 | */ 365 | public void setCoveredFadeColor(int color) { 366 | mCoveredFadeColor = color; 367 | invalidate(); 368 | } 369 | 370 | /** 371 | * 获取蒙层的颜色 372 | * @return 373 | */ 374 | public int getCoveredFadeColor() { 375 | return mCoveredFadeColor; 376 | } 377 | 378 | /** 379 | * 设置是否激活滑动功能 380 | * @param enabled 381 | */ 382 | public void setSlidingEnabled(boolean enabled) { 383 | mIsSlidingEnabled = enabled; 384 | } 385 | 386 | /** 387 | * 设置slideable view折叠时的高度 388 | * @param val 单位像素 389 | */ 390 | public void setPanelHeight(int val) { 391 | mPanelHeight = val; 392 | requestLayout(); 393 | } 394 | 395 | /** 396 | * 获取slideable view折叠时的高度 397 | */ 398 | public int getPanelHeight() { 399 | return mPanelHeight; 400 | } 401 | 402 | /** 403 | * 获取mMainView的偏移值 404 | */ 405 | public int getCurrentParalaxOffset() { 406 | int offset = (int)(mParalaxOffset * (1 - mSlideOffset)); 407 | return mIsSlidingUp ? -offset : offset; 408 | } 409 | 410 | /** 411 | * 设置回调监听函数 412 | * @param listener 413 | */ 414 | public void setPanelSlideListener(PanelSlideListener listener) { 415 | mPanelSlideListener = listener; 416 | } 417 | 418 | /** 419 | * 设置可用来拖动的view,若为NULL,表示允许整个drag view响应拖动 420 | * @param dragView 421 | */ 422 | public void setDragView(View dragView) { 423 | mDragView = dragView; 424 | } 425 | 426 | /** 427 | * 设置锚点 428 | * @param anchorPoint 有效值范围[0,1] 429 | */ 430 | public void setAnchorPoint(float anchorPoint) { 431 | if (anchorPoint > 0 && anchorPoint < 1) 432 | mAnchorPoint = anchorPoint; 433 | } 434 | 435 | /** 436 | * 若为false 表示会在mMainview上加上一层蒙层 437 | * @param overlayed 438 | */ 439 | public void setOverlayed(boolean overlayed) { 440 | mOverlayContent = overlayed; 441 | } 442 | 443 | /** 444 | * 获取是否在mMainview上加上一层蒙层 445 | * @return 446 | */ 447 | public boolean isOverlayed() { 448 | return mOverlayContent; 449 | } 450 | 451 | /** 452 | * Panel有滑动时,用于做分发 453 | * @param panel 454 | */ 455 | void dispatchOnPanelSlide(View panel) { 456 | if (mPanelSlideListener != null) { 457 | mPanelSlideListener.onPanelSlide(panel, mSlideOffset); 458 | } 459 | } 460 | 461 | /** 462 | * Panel展开时,用于做分发 463 | * @param panel 464 | */ 465 | void dispatchOnPanelExpanded(View panel) { 466 | if (mPanelSlideListener != null) { 467 | mPanelSlideListener.onPanelExpanded(panel); 468 | } 469 | sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); 470 | } 471 | 472 | /** 473 | * Panel收起时,用于做分发 474 | * @param panel 475 | */ 476 | void dispatchOnPanelCollapsed(View panel) { 477 | if (mPanelSlideListener != null) { 478 | mPanelSlideListener.onPanelCollapsed(panel); 479 | } 480 | sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); 481 | } 482 | 483 | /** 484 | * Panel滑到锚点时,用于做分发 485 | * @param panel 486 | */ 487 | void dispatchOnPanelAnchored(View panel) { 488 | if (mPanelSlideListener != null) { 489 | mPanelSlideListener.onPanelAnchored(panel); 490 | } 491 | sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); 492 | } 493 | 494 | /** 495 | * 根据当前的view的位置判断是显示还是隐藏 496 | */ 497 | void updateObscuredViewVisibility() { 498 | if (getChildCount() == 0) { 499 | return; 500 | } 501 | final int leftBound = getPaddingLeft(); 502 | final int rightBound = getWidth() - getPaddingRight(); 503 | final int topBound = getPaddingTop(); 504 | final int bottomBound = getHeight() - getPaddingBottom(); 505 | final int left; 506 | final int right; 507 | final int top; 508 | final int bottom; 509 | if (mSlideableView != null && hasOpaqueBackground(mSlideableView)) { 510 | left = mSlideableView.getLeft(); 511 | right = mSlideableView.getRight(); 512 | top = mSlideableView.getTop(); 513 | bottom = mSlideableView.getBottom(); 514 | } else { 515 | left = right = top = bottom = 0; 516 | } 517 | View child = getChildAt(0); 518 | final int clampedChildLeft = Math.max(leftBound, child.getLeft()); 519 | final int clampedChildTop = Math.max(topBound, child.getTop()); 520 | final int clampedChildRight = Math.min(rightBound, child.getRight()); 521 | final int clampedChildBottom = Math.min(bottomBound, child.getBottom()); 522 | final int vis; 523 | //计算若mMainView完全被覆盖时,就隐藏 524 | if (clampedChildLeft >= left && clampedChildTop >= top && 525 | clampedChildRight <= right && clampedChildBottom <= bottom) { 526 | vis = INVISIBLE; 527 | } else { 528 | vis = VISIBLE; 529 | } 530 | child.setVisibility(vis); 531 | } 532 | 533 | /** 534 | * 设置所有childview可见状态为VISIBLE 535 | */ 536 | void setAllChildrenVisible() { 537 | for (int i = 0, childCount = getChildCount(); i < childCount; i++) { 538 | final View child = getChildAt(i); 539 | if (child.getVisibility() == INVISIBLE) { 540 | child.setVisibility(VISIBLE); 541 | } 542 | } 543 | } 544 | 545 | /** 546 | * 给定view背景是否透明 547 | * @param v 548 | * @return 549 | */ 550 | private static boolean hasOpaqueBackground(View v) { 551 | final Drawable bg = v.getBackground(); 552 | return bg != null && bg.getOpacity() == PixelFormat.OPAQUE; 553 | } 554 | 555 | @Override 556 | protected void onAttachedToWindow() { 557 | super.onAttachedToWindow(); 558 | mFirstLayout = true; 559 | } 560 | 561 | @Override 562 | protected void onDetachedFromWindow() { 563 | super.onDetachedFromWindow(); 564 | mFirstLayout = true; 565 | } 566 | 567 | @Override 568 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 569 | final int widthMode = MeasureSpec.getMode(widthMeasureSpec); 570 | final int widthSize = MeasureSpec.getSize(widthMeasureSpec); 571 | final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 572 | final int heightSize = MeasureSpec.getSize(heightMeasureSpec); 573 | 574 | //目前只支持 MATCH_PARENT 575 | if (widthMode != MeasureSpec.EXACTLY) { 576 | throw new IllegalStateException("Width must have an exact value or MATCH_PARENT"); 577 | } else if (heightMode != MeasureSpec.EXACTLY) { 578 | throw new IllegalStateException("Height must have an exact value or MATCH_PARENT"); 579 | } 580 | 581 | int layoutHeight = heightSize - getPaddingTop() - getPaddingBottom(); 582 | int panelHeight = mPanelHeight; 583 | 584 | final int childCount = getChildCount(); 585 | 586 | if (childCount > 2) { 587 | Log.e(TAG, "onMeasure: More than two child views are not supported."); 588 | } else if (getChildAt(1).getVisibility() == GONE) { 589 | panelHeight = 0; 590 | } 591 | 592 | mSlideableView = null; 593 | mCanSlide = false; 594 | 595 | //measure 596 | for (int i = 0; i < childCount; i++) { 597 | final View child = getChildAt(i); 598 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 599 | 600 | int height = layoutHeight; 601 | if (child.getVisibility() == GONE) { 602 | lp.dimWhenOffset = false; 603 | continue; 604 | } 605 | 606 | if (i == 1) { 607 | lp.slideable = true;//设置第二个child view为可滑动的view 608 | lp.dimWhenOffset = true; 609 | mSlideableView = child; 610 | mCanSlide = true;//标示panel为可滑动状态 611 | } else { 612 | if (!mOverlayContent) { 613 | height -= panelHeight; 614 | } 615 | mMainView = child; 616 | } 617 | 618 | //子child测量 619 | int childWidthSpec; 620 | if (lp.width == LayoutParams.WRAP_CONTENT) { 621 | childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST); 622 | } else if (lp.width == LayoutParams.MATCH_PARENT) { 623 | childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); 624 | } else { 625 | childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY); 626 | } 627 | 628 | int childHeightSpec; 629 | if (lp.height == LayoutParams.WRAP_CONTENT) { 630 | childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST); 631 | } else if (lp.height == LayoutParams.MATCH_PARENT) { 632 | childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); 633 | } else { 634 | childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); 635 | } 636 | //子view measure调用 637 | child.measure(childWidthSpec, childHeightSpec); 638 | } 639 | 640 | setMeasuredDimension(widthSize, heightSize); 641 | } 642 | 643 | @Override 644 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 645 | final int paddingLeft = getPaddingLeft(); 646 | final int paddingTop = getPaddingTop(); 647 | final int slidingTop = getSlidingTop(); 648 | 649 | final int childCount = getChildCount(); 650 | 651 | //根据当前mSlideState,初始化mSlideOffset值 652 | if (mFirstLayout) { 653 | switch (mSlideState) { 654 | case EXPANDED: 655 | mSlideOffset = mCanSlide ? 0.f : 1.f; 656 | break; 657 | case ANCHORED: 658 | mSlideOffset = mCanSlide ? mAnchorPoint : 1.f; 659 | break; 660 | default: 661 | mSlideOffset = 1.f; 662 | break; 663 | } 664 | } 665 | 666 | for (int i = 0; i < childCount; i++) { 667 | final View child = getChildAt(i); 668 | 669 | if (child.getVisibility() == GONE) { 670 | continue; 671 | } 672 | 673 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 674 | final int childHeight = child.getMeasuredHeight(); 675 | 676 | //若当前的view是slideable view,则计算其滑动的最大距离值 677 | if (lp.slideable) { 678 | mSlideRange = childHeight - mPanelHeight; 679 | } 680 | 681 | int childTop; 682 | //计算top的值 ,这里mSlideOffset是可变因子 683 | if (mIsSlidingUp) { 684 | childTop = lp.slideable ? slidingTop + (int) (mSlideRange * mSlideOffset) : paddingTop; 685 | } else { 686 | childTop = lp.slideable ? slidingTop - (int) (mSlideRange * mSlideOffset) : paddingTop; 687 | if (!lp.slideable && !mOverlayContent) { 688 | childTop += mPanelHeight; 689 | } 690 | } 691 | final int childBottom = childTop + childHeight; 692 | final int childLeft = paddingLeft; 693 | final int childRight = childLeft + child.getMeasuredWidth(); 694 | 695 | //完成child view的layout 696 | child.layout(childLeft, childTop, childRight, childBottom); 697 | } 698 | 699 | if (mFirstLayout) { 700 | updateObscuredViewVisibility(); 701 | } 702 | 703 | mFirstLayout = false; 704 | } 705 | 706 | @Override 707 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 708 | super.onSizeChanged(w, h, oldw, oldh); 709 | //view size发送变化时处理 710 | if (h != oldh) { 711 | mFirstLayout = true; 712 | } 713 | } 714 | 715 | /** 716 | * 若为true,此flag表示drag view想自己处理内部触摸事件,此时drag view可以处理水平滚动和点击事件 717 | * 默认这个值是false 718 | */ 719 | public void setEnableDragViewTouchEvents(boolean enabled) { 720 | mIsUsingDragViewTouchEvents = enabled; 721 | } 722 | 723 | @Override 724 | public boolean onInterceptTouchEvent(MotionEvent ev) { 725 | final int action = MotionEventCompat.getActionMasked(ev); 726 | 727 | if (!mCanSlide || !mIsSlidingEnabled || (mIsUnableToDrag && action != MotionEvent.ACTION_DOWN)) { 728 | //滑动状态清空 729 | mDragHelper.cancel(); 730 | return super.onInterceptTouchEvent(ev); 731 | } 732 | 733 | if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { 734 | //滑动状态清空 735 | mDragHelper.cancel(); 736 | return false; 737 | } 738 | 739 | final float x = ev.getX(); 740 | final float y = ev.getY(); 741 | //若为true,表示drag view将不能处理内部的触摸事件 742 | boolean interceptTap = false; 743 | 744 | switch (action) { 745 | case MotionEvent.ACTION_DOWN: { 746 | mIsUnableToDrag = false; 747 | mInitialMotionX = x; 748 | mInitialMotionY = y; 749 | //满足此条件 表示拦截此事件 接下来会正式交给drag view的触摸事件 750 | if (isDragViewUnder((int) x, (int) y) && !mIsUsingDragViewTouchEvents) { 751 | interceptTap = true; 752 | } 753 | break; 754 | } 755 | 756 | case MotionEvent.ACTION_MOVE: { 757 | final float adx = Math.abs(x - mInitialMotionX); 758 | final float ady = Math.abs(y - mInitialMotionY); 759 | final int dragSlop = mDragHelper.getTouchSlop(); 760 | 761 | //处理可能有的横向滚动事件 762 | if (mIsUsingDragViewTouchEvents) { 763 | //满足此条件,处理横向触摸事件 764 | if (adx > mScrollTouchSlop && ady < mScrollTouchSlop) { 765 | return super.onInterceptTouchEvent(ev); 766 | } 767 | //满足此条件,表示有有效的竖向触摸事件,那么若触摸事件落在drag view上,需优先处理竖向触摸事件,忽略横向触摸事件 768 | else if (ady > mScrollTouchSlop) { 769 | interceptTap = isDragViewUnder((int) x, (int) y); 770 | } 771 | } 772 | 773 | if ((ady > dragSlop && adx > ady) || !isDragViewUnder((int) x, (int) y)) { 774 | //滑动状态清空 775 | mDragHelper.cancel(); 776 | mIsUnableToDrag = true; 777 | return false; 778 | } 779 | break; 780 | } 781 | } 782 | 783 | final boolean interceptForDrag = mDragHelper.shouldInterceptTouchEvent(ev); 784 | 785 | return interceptForDrag || interceptTap; 786 | } 787 | 788 | @Override 789 | public boolean onTouchEvent(MotionEvent ev) { 790 | if (!mCanSlide || !mIsSlidingEnabled) { 791 | return super.onTouchEvent(ev); 792 | } 793 | 794 | //具体的滑动计算处理 795 | mDragHelper.processTouchEvent(ev); 796 | 797 | final int action = ev.getAction(); 798 | boolean wantTouchEvents = true; 799 | 800 | switch (action & MotionEventCompat.ACTION_MASK) { 801 | case MotionEvent.ACTION_DOWN: { 802 | final float x = ev.getX(); 803 | final float y = ev.getY(); 804 | mInitialMotionX = x; 805 | mInitialMotionY = y; 806 | break; 807 | } 808 | 809 | case MotionEvent.ACTION_UP: { 810 | final float x = ev.getX(); 811 | final float y = ev.getY(); 812 | final float dx = x - mInitialMotionX; 813 | final float dy = y - mInitialMotionY; 814 | final int slop = mDragHelper.getTouchSlop(); 815 | View dragView = mDragView != null ? mDragView : mSlideableView; 816 | if (dx * dx + dy * dy < slop * slop && 817 | isDragViewUnder((int) x, (int) y)) { 818 | dragView.playSoundEffect(SoundEffectConstants.CLICK); 819 | //点击事件处理 展开或收起 820 | if (!isExpanded() && !isAnchored()) { 821 | expandPane(mAnchorPoint); 822 | } else { 823 | collapsePane(); 824 | } 825 | break; 826 | } 827 | break; 828 | } 829 | } 830 | 831 | return wantTouchEvents; 832 | } 833 | 834 | /** 835 | * 判断当前point是否落在dragView这个view上 836 | * @param x 837 | * @param y 838 | * @return 839 | */ 840 | private boolean isDragViewUnder(int x, int y) { 841 | View dragView = mDragView != null ? mDragView : mSlideableView; 842 | if (dragView == null) return false; 843 | int[] viewLocation = new int[2]; 844 | dragView.getLocationOnScreen(viewLocation); 845 | int[] parentLocation = new int[2]; 846 | this.getLocationOnScreen(parentLocation); 847 | int screenX = parentLocation[0] + x; 848 | int screenY = parentLocation[1] + y; 849 | return screenX >= viewLocation[0] && screenX < viewLocation[0] + dragView.getWidth() && 850 | screenY >= viewLocation[1] && screenY < viewLocation[1] + dragView.getHeight(); 851 | } 852 | 853 | /** 854 | * 若当前支持滑动,展开slideable view 855 | * @param pane 856 | * @param initialVelocity 857 | * @param mSlideOffset 858 | * @return 859 | */ 860 | private boolean expandPane(View pane, int initialVelocity, float mSlideOffset) { 861 | return mFirstLayout || smoothSlideTo(mSlideOffset, initialVelocity); 862 | } 863 | 864 | /** 865 | * 若当前支持滑动,收起slideable view 866 | * @param pane 867 | * @param initialVelocity 868 | * @return 869 | */ 870 | private boolean collapsePane(View pane, int initialVelocity) { 871 | return mFirstLayout || smoothSlideTo(1.f, initialVelocity); 872 | } 873 | 874 | /** 875 | * 若mIsSlidingUp为true,计算slideable view完全展开的top值 876 | * 若mIsSlidingUp为false,计算slideable view完全收起的top值 877 | * @return 878 | */ 879 | private int getSlidingTop() { 880 | if (mSlideableView != null) { 881 | return mIsSlidingUp 882 | ? getMeasuredHeight() - getPaddingBottom() - mSlideableView.getMeasuredHeight() 883 | : getPaddingTop(); 884 | } 885 | 886 | return getMeasuredHeight() - getPaddingBottom(); 887 | } 888 | 889 | /** 890 | * 若当前支持滑动,收起slideable view 891 | * @param pane 892 | * @param initialVelocity 893 | * @return 894 | */ 895 | public boolean collapsePane() { 896 | return collapsePane(mSlideableView, 0); 897 | } 898 | 899 | /** 900 | * 若当前支持滑动,展开slideable view 901 | * @param pane 902 | * @param initialVelocity 903 | * @param mSlideOffset 904 | * @return 905 | */ 906 | public boolean expandPane() { 907 | return expandPane(0); 908 | } 909 | 910 | /** 911 | * 展开slideable view 912 | * @param mSlideOffset 定义展开slideable view到上面位置 值范围是0-1 913 | * @return 914 | */ 915 | public boolean expandPane(float mSlideOffset) { 916 | if (mSlideState == SlideState.EXPANDED) return false; 917 | mSlideableView.setVisibility(View.VISIBLE); 918 | if (!isPaneVisible()) { 919 | showPane(); 920 | } 921 | return expandPane(mSlideableView, 0, mSlideOffset); 922 | } 923 | 924 | /** 925 | * 判断当前slideable view是否为展开状态 926 | * @return 若为true 表示状态为展开 927 | */ 928 | public boolean isExpanded() { 929 | return mSlideState == SlideState.EXPANDED; 930 | } 931 | 932 | /** 933 | * 判断当前slideable view是否为锚点状态 934 | * @return 若为true 表示slideable view在锚点位置停留 935 | */ 936 | public boolean isAnchored() { 937 | return mSlideState == SlideState.ANCHORED; 938 | } 939 | 940 | /** 941 | * 获取当前是否可以滑动 942 | * @return 943 | */ 944 | public boolean isSlideable() { 945 | return mCanSlide; 946 | } 947 | 948 | /** 949 | * slideable view的是否可见 950 | * @return 951 | */ 952 | public boolean isPaneVisible() { 953 | if (getChildCount() < 2) { 954 | return false; 955 | } 956 | View slidingPane = getChildAt(1); 957 | return slidingPane.getVisibility() == View.VISIBLE; 958 | } 959 | 960 | /** 961 | * 设置slideable view为可见 962 | */ 963 | public void showPane() { 964 | if (getChildCount() < 2) { 965 | return; 966 | } 967 | View slidingPane = getChildAt(1); 968 | slidingPane.setVisibility(View.VISIBLE); 969 | requestLayout(); 970 | } 971 | 972 | /** 973 | * 设置slideable view为不可见(gone) 974 | */ 975 | public void hidePane() { 976 | if (mSlideableView == null) { 977 | return; 978 | } 979 | mSlideableView.setVisibility(View.GONE); 980 | requestLayout(); 981 | } 982 | 983 | /** 984 | * 触摸手势在drag下,处理mMainView的滑动 985 | * @param newTop 986 | */ 987 | private void onPanelDragged(int newTop) { 988 | final int topBound = getSlidingTop(); 989 | mSlideOffset = mIsSlidingUp 990 | ? (float) (newTop - topBound) / mSlideRange 991 | : (float) (topBound - newTop) / mSlideRange; 992 | dispatchOnPanelSlide(mSlideableView); 993 | 994 | if (mParalaxOffset > 0) { 995 | //开始计算mMainView的位移 996 | int mainViewOffset = getCurrentParalaxOffset(); 997 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 998 | mMainView.setTranslationY(mainViewOffset); 999 | } else { 1000 | AnimatorProxy.wrap(mMainView).setTranslationY(mainViewOffset); 1001 | } 1002 | } 1003 | } 1004 | 1005 | @Override 1006 | protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 1007 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1008 | boolean result; 1009 | //必须需要save后,来clipRect 1010 | final int save = canvas.save(Canvas.CLIP_SAVE_FLAG); 1011 | 1012 | boolean drawScrim = false; 1013 | 1014 | if (mCanSlide && !lp.slideable && mSlideableView != null) { 1015 | // Clip against the slider; no sense drawing what will immediately be covered, 1016 | // Unless the panel is set to overlay content 1017 | if (!mOverlayContent) { 1018 | canvas.getClipBounds(mTmpRect); 1019 | if (mIsSlidingUp) { 1020 | mTmpRect.bottom = Math.min(mTmpRect.bottom, mSlideableView.getTop()); 1021 | } else { 1022 | mTmpRect.top = Math.max(mTmpRect.top, mSlideableView.getBottom()); 1023 | } 1024 | canvas.clipRect(mTmpRect); 1025 | } 1026 | // <1表示非完全收起,需要画蒙层 1027 | if (mSlideOffset < 1) { 1028 | drawScrim = true; 1029 | } 1030 | } 1031 | 1032 | result = super.drawChild(canvas, child, drawingTime); 1033 | canvas.restoreToCount(save); 1034 | 1035 | //非完全收起情况下,需要画一个半透明的蒙层 1036 | if (drawScrim && mCoveredFadeColor != 0) { 1037 | final int baseAlpha = (mCoveredFadeColor & 0xff000000) >>> 24;//取alpha值 1038 | final int imag = (int) (baseAlpha * (1 - mSlideOffset));//根据滑动的距离越大,蒙层透明度越低 1039 | final int color = imag << 24 | (mCoveredFadeColor & 0xffffff); 1040 | mCoveredFadePaint.setColor(color); 1041 | canvas.drawRect(mTmpRect, mCoveredFadePaint); 1042 | } 1043 | 1044 | return result; 1045 | } 1046 | 1047 | /** 1048 | *mSlideableView滑动到指定位置,有动画效果
1049 | * @param slideOffset
1050 | * @param velocity
1051 | * @return
1052 | */
1053 | boolean smoothSlideTo(float slideOffset, int velocity) {
1054 | //条件判断是否能滑动
1055 | if (!mCanSlide) {
1056 | // Nothing to do.
1057 | return false;
1058 | }
1059 |
1060 | final int topBound = getSlidingTop();
1061 | //计算滑动到最终坐标的y值
1062 | int y = mIsSlidingUp
1063 | ? (int) (topBound + slideOffset * mSlideRange)
1064 | : (int) (topBound - slideOffset * mSlideRange);
1065 |
1066 | //开始准备滑动mSlideableView到指定位置
1067 | if (mDragHelper.smoothSlideViewTo(mSlideableView, mSlideableView.getLeft(), y)) {
1068 | setAllChildrenVisible();
1069 | //刷新view
1070 | ViewCompat.postInvalidateOnAnimation(this);
1071 | return true;
1072 | }
1073 | return false;
1074 | }
1075 |
1076 | @Override
1077 | public void computeScroll() {
1078 | //在滑动中,若此时是非move事件触发的,DragHelper会把当前的mDragState设置为STATE_SETTLING。此时会进入此分支,来处理接下来的位移动画
1079 | if (mDragHelper.continueSettling(true)) {
1080 | if (!mCanSlide) {
1081 | mDragHelper.abort();
1082 | return;
1083 | }
1084 |
1085 | ViewCompat.postInvalidateOnAnimation(this);
1086 | }
1087 | }
1088 |
1089 | @Override
1090 | public void draw(Canvas c) {
1091 | super.draw(c);
1092 |
1093 | if (mSlideableView == null) {
1094 | // No need to draw a shadow if we don't have one.
1095 | return;
1096 | }
1097 |
1098 | //计算阴影的范围
1099 | final int right = mSlideableView.getRight();
1100 | final int top;
1101 | final int bottom;
1102 | if (mIsSlidingUp) {
1103 | top = mSlideableView.getTop() - mShadowHeight;
1104 | bottom = mSlideableView.getTop();
1105 | } else {
1106 | top = mSlideableView.getBottom();
1107 | bottom = mSlideableView.getBottom() + mShadowHeight;
1108 | }
1109 | final int left = mSlideableView.getLeft();
1110 |
1111 | //画阴影
1112 | if (mShadowDrawable != null) {
1113 | mShadowDrawable.setBounds(left, top, right, bottom);
1114 | mShadowDrawable.draw(c);
1115 | }
1116 | }
1117 |
1118 | protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
1119 | if (v instanceof ViewGroup) {
1120 | final ViewGroup group = (ViewGroup) v;
1121 | final int scrollX = v.getScrollX();
1122 | final int scrollY = v.getScrollY();
1123 | final int count = group.getChildCount();
1124 | // Count backwards - let topmost views consume scroll distance first.
1125 | for (int i = count - 1; i >= 0; i--) {
1126 | final View child = group.getChildAt(i);
1127 | if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
1128 | y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
1129 | canScroll(child, true, dx, x + scrollX - child.getLeft(),
1130 | y + scrollY - child.getTop())) {
1131 | return true;
1132 | }
1133 | }
1134 | }
1135 | return checkV && ViewCompat.canScrollHorizontally(v, -dx);
1136 | }
1137 |
1138 |
1139 | @Override
1140 | protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
1141 | return new LayoutParams();
1142 | }
1143 |
1144 | @Override
1145 | protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
1146 | return p instanceof MarginLayoutParams
1147 | ? new LayoutParams((MarginLayoutParams) p)
1148 | : new LayoutParams(p);
1149 | }
1150 |
1151 | @Override
1152 | protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
1153 | return p instanceof LayoutParams && super.checkLayoutParams(p);
1154 | }
1155 |
1156 | @Override
1157 | public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
1158 | return new LayoutParams(getContext(), attrs);
1159 | }
1160 |
1161 | @Override
1162 | public Parcelable onSaveInstanceState() {
1163 | //保存值
1164 | Parcelable superState = super.onSaveInstanceState();
1165 |
1166 | SavedState ss = new SavedState(superState);
1167 | ss.mSlideState = mSlideState;
1168 |
1169 | return ss;
1170 | }
1171 |
1172 | @Override
1173 | public void onRestoreInstanceState(Parcelable state) {
1174 | //恢复值
1175 | SavedState ss = (SavedState) state;
1176 | super.onRestoreInstanceState(ss.getSuperState());
1177 | mSlideState = ss.mSlideState;
1178 | }
1179 |
1180 | private class DragHelperCallback extends ViewDragHelper.Callback {
1181 |
1182 | //这是一个开关,若返回false,表示不可以滑动。
1183 | //若为true表示可以滑动,并且ViewDragHelper会把state设置为STATE_DRAGGING
1184 | @Override
1185 | public boolean tryCaptureView(View child, int pointerId) {
1186 | if (mIsUnableToDrag) {
1187 | return false;
1188 | }
1189 |
1190 | return ((LayoutParams) child.getLayoutParams()).slideable;
1191 | }
1192 |
1193 | //ViewDragHelper维护的状态发生变化时,会回调此函数
1194 | @Override
1195 | public void onViewDragStateChanged(int state) {
1196 | int anchoredTop = (int)(mAnchorPoint*mSlideRange);
1197 |
1198 | //在STATE_IDLE状态下判断,切换mSlideState的值。在ViewDragHelper的其他状态判断没有意义
1199 | if (mDragHelper.getViewDragState() == ViewDragHelper.STATE_IDLE) {
1200 | if (mSlideOffset == 0) {
1201 | if (mSlideState != SlideState.EXPANDED) {
1202 | updateObscuredViewVisibility();
1203 | mSlideState = SlideState.EXPANDED;
1204 | dispatchOnPanelExpanded(mSlideableView);
1205 | }
1206 | } else if (mSlideOffset == (float)anchoredTop/(float)mSlideRange) {
1207 | if (mSlideState != SlideState.ANCHORED) {
1208 | updateObscuredViewVisibility();
1209 | mSlideState = SlideState.ANCHORED;
1210 | dispatchOnPanelAnchored(mSlideableView);
1211 | }
1212 | } else if (mSlideState != SlideState.COLLAPSED) {
1213 | mSlideState = SlideState.COLLAPSED;
1214 | dispatchOnPanelCollapsed(mSlideableView);
1215 | }
1216 | }
1217 | }
1218 |
1219 | //在tryCaptureView返回true后,会回调此函数
1220 | @Override
1221 | public void onViewCaptured(View capturedChild, int activePointerId) {
1222 | // Make all child views visible in preparation for sliding things around
1223 | setAllChildrenVisible();
1224 | }
1225 |
1226 | //当panel位置有偏移时,会回调此函数
1227 | @Override
1228 | public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
1229 | onPanelDragged(top);
1230 | invalidate();
1231 | }
1232 |
1233 | //当cancel或up事件触发时,会回调此函数,后二个参数记录触发时的事件轨迹速度
1234 | @Override
1235 | public void onViewReleased(View releasedChild, float xvel, float yvel) {
1236 | //保存滑动的最终位置y值
1237 | int top = mIsSlidingUp
1238 | ? getSlidingTop()
1239 | : getSlidingTop() - mSlideRange;
1240 |
1241 | //若设置了锚点,那么计算滑动最终位置时,考虑锚点的位置
1242 | if (mAnchorPoint != 0) {
1243 | int anchoredTop;
1244 | float anchorOffset;
1245 | if (mIsSlidingUp) {
1246 | anchoredTop = (int)(mAnchorPoint*mSlideRange);
1247 | anchorOffset = (float)anchoredTop/(float)mSlideRange;
1248 | } else {
1249 | anchoredTop = mPanelHeight - (int)(mAnchorPoint*mSlideRange);
1250 | anchorOffset = (float)(mPanelHeight - anchoredTop)/(float)mSlideRange;
1251 | }
1252 |
1253 | if (yvel > 0 || (yvel == 0 && mSlideOffset >= (1f+anchorOffset)/2)) {
1254 | top += mSlideRange;
1255 | } else if (yvel == 0 && mSlideOffset < (1f+anchorOffset)/2
1256 | && mSlideOffset >= anchorOffset/2) {
1257 | top += mSlideRange * mAnchorPoint;
1258 | }
1259 |
1260 | } else if (yvel > 0 || (yvel == 0 && mSlideOffset > 0.5f)) {
1261 | top += mSlideRange;
1262 | }
1263 |
1264 | //计算好滑动的最终位置后,开始滑动view
1265 | mDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top);
1266 | invalidate();
1267 | }
1268 |
1269 | //这个很重要,要实现竖向滑动,这个必须要重写
1270 | @Override
1271 | public int getViewVerticalDragRange(View child) {
1272 | return mSlideRange;
1273 | }
1274 |
1275 | //对top值进行修正,限制值范围在(topBound,bottomBound)之间
1276 | @Override
1277 | public int clampViewPositionVertical(View child, int top, int dy) {
1278 | final int topBound;
1279 | final int bottomBound;
1280 | if (mIsSlidingUp) {
1281 | topBound = getSlidingTop();
1282 | bottomBound = topBound + mSlideRange;
1283 | } else {
1284 | bottomBound = getPaddingTop();
1285 | topBound = bottomBound - mSlideRange;
1286 | }
1287 |
1288 | return Math.min(Math.max(top, topBound), bottomBound);
1289 | }
1290 | }
1291 |
1292 | public static class LayoutParams extends ViewGroup.MarginLayoutParams {
1293 | private static final int[] ATTRS = new int[] {
1294 | android.R.attr.layout_weight
1295 | };
1296 |
1297 | /**
1298 | * 若为true,表示panel可滑动
1299 | */
1300 | boolean slideable;
1301 |
1302 | /**
1303 | * True if this view should be drawn dimmed
1304 | * when it's been offset from its default position.
1305 | */
1306 | boolean dimWhenOffset;
1307 |
1308 | Paint dimPaint;
1309 |
1310 | public LayoutParams() {
1311 | super(MATCH_PARENT, MATCH_PARENT);
1312 | }
1313 |
1314 | public LayoutParams(int width, int height) {
1315 | super(width, height);
1316 | }
1317 |
1318 | public LayoutParams(android.view.ViewGroup.LayoutParams source) {
1319 | super(source);
1320 | }
1321 |
1322 | public LayoutParams(MarginLayoutParams source) {
1323 | super(source);
1324 | }
1325 |
1326 | public LayoutParams(LayoutParams source) {
1327 | super(source);
1328 | }
1329 |
1330 | public LayoutParams(Context c, AttributeSet attrs) {
1331 | super(c, attrs);
1332 |
1333 | final TypedArray a = c.obtainStyledAttributes(attrs, ATTRS);
1334 | a.recycle();
1335 | }
1336 |
1337 | }
1338 |
1339 | static class SavedState extends BaseSavedState {
1340 | //实例化需要保存的参数
1341 | SlideState mSlideState;
1342 |
1343 | SavedState(Parcelable superState) {
1344 | super(superState);
1345 | }
1346 |
1347 | private SavedState(Parcel in) {
1348 | super(in);
1349 | try {
1350 | mSlideState = Enum.valueOf(SlideState.class, in.readString());
1351 | } catch (IllegalArgumentException e) {
1352 | mSlideState = SlideState.COLLAPSED;
1353 | }
1354 | }
1355 |
1356 | @Override
1357 | public void writeToParcel(Parcel out, int flags) {
1358 | super.writeToParcel(out, flags);
1359 | out.writeString(mSlideState.toString());
1360 | }
1361 |
1362 | public static final Parcelable.Creatoron*methods are invoked on siginficant events and several
144 | * accessor methods are expected to provide the ViewDragHelper with more information
145 | * about the state of the parent view upon request. The callback also makes decisions
146 | * governing the range and draggability of child views.
147 | */
148 | public static abstract class Callback {
149 | /**
150 | * Called when the drag state changes. See the STATE_* constants
151 | * for more information.
152 | *
153 | * @param state The new drag state
154 | *
155 | * @see #STATE_IDLE
156 | * @see #STATE_DRAGGING
157 | * @see #STATE_SETTLING
158 | */
159 | public void onViewDragStateChanged(int state) {}
160 |
161 | /**
162 | * Called when the captured view's position changes as the result of a drag or settle.
163 | *
164 | * @param changedView View whose position changed
165 | * @param left New X coordinate of the left edge of the view
166 | * @param top New Y coordinate of the top edge of the view
167 | * @param dx Change in X position from the last call
168 | * @param dy Change in Y position from the last call
169 | */
170 | public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {}
171 |
172 | /**
173 | * Called when a child view is captured for dragging or settling. The ID of the pointer
174 | * currently dragging the captured view is supplied. If activePointerId is
175 | * identified as {@link #INVALID_POINTER} the capture is programmatic instead of
176 | * pointer-initiated.
177 | *
178 | * @param capturedChild Child view that was captured
179 | * @param activePointerId Pointer id tracking the child capture
180 | */
181 | public void onViewCaptured(View capturedChild, int activePointerId) {}
182 |
183 | /**
184 | * Called when the child view is no longer being actively dragged.
185 | * The fling velocity is also supplied, if relevant. The velocity values may
186 | * be clamped to system minimums or maximums.
187 | *
188 | * Calling code may decide to fling or otherwise release the view to let it
189 | * settle into place. It should do so using {@link #settleCapturedViewAt(int, int)}
190 | * or {@link #flingCapturedView(int, int, int, int)}. If the Callback invokes
191 | * one of these methods, the ViewDragHelper will enter {@link #STATE_SETTLING}
192 | * and the view capture will not fully end until it comes to a complete stop.
193 | * If neither of these methods is invoked before onViewReleased returns,
194 | * the view will stop in place and the ViewDragHelper will return to
195 | * {@link #STATE_IDLE}.
index
247 | */
248 | public int getOrderedChildIndex(int index) {
249 | return index;
250 | }
251 |
252 | /**
253 | * Return the magnitude of a draggable child view's horizontal range of motion in pixels.
254 | * This method should return 0 for views that cannot move horizontally.
255 | *
256 | * @param child Child view to check
257 | * @return range of horizontal motion in pixels
258 | */
259 | public int getViewHorizontalDragRange(View child) {
260 | return 0;
261 | }
262 |
263 | /**
264 | * Return the magnitude of a draggable child view's vertical range of motion in pixels.
265 | * This method should return 0 for views that cannot move vertically.
266 | *
267 | * @param child Child view to check
268 | * @return range of vertical motion in pixels
269 | */
270 | public int getViewVerticalDragRange(View child) {
271 | return 0;
272 | }
273 |
274 | /**
275 | * Called when the user's input indicates that they want to capture the given child view
276 | * with the pointer indicated by pointerId. The callback should return true if the user
277 | * is permitted to drag the given view with the indicated pointer.
278 | *
279 | * ViewDragHelper may call this method multiple times for the same view even if 280 | * the view is already captured; this indicates that a new pointer is trying to take 281 | * control of the view.
282 | * 283 | *If this method returns true, a call to {@link #onViewCaptured(android.view.View, int)} 284 | * will follow if the capture is successful.
285 | * 286 | * @param child Child the user is attempting to capture 287 | * @param pointerId ID of the pointer attempting the capture 288 | * @return true if capture should be allowed, false otherwise 289 | */ 290 | public abstract boolean tryCaptureView(View child, int pointerId); 291 | 292 | /** 293 | * Restrict the motion of the dragged child view along the horizontal axis. 294 | * The default implementation does not allow horizontal motion; the extending 295 | * class must override this method and provide the desired clamping. 296 | * 297 | * 298 | * @param child Child view being dragged 299 | * @param left Attempted motion along the X axis 300 | * @param dx Proposed change in position for left 301 | * @return The new clamped position for left 302 | */ 303 | public int clampViewPositionHorizontal(View child, int left, int dx) { 304 | return 0; 305 | } 306 | 307 | /** 308 | * Restrict the motion of the dragged child view along the vertical axis. 309 | * The default implementation does not allow vertical motion; the extending 310 | * class must override this method and provide the desired clamping. 311 | * 312 | * 313 | * @param child Child view being dragged 314 | * @param top Attempted motion along the Y axis 315 | * @param dy Proposed change in position for top 316 | * @return The new clamped position for top 317 | */ 318 | public int clampViewPositionVertical(View child, int top, int dy) { 319 | return 0; 320 | } 321 | } 322 | 323 | /** 324 | * Interpolator defining the animation curve for mScroller 325 | */ 326 | private static final Interpolator sInterpolator = new Interpolator() { 327 | public float getInterpolation(float t) { 328 | t -= 1.0f; 329 | return t * t * t * t * t + 1.0f; 330 | } 331 | }; 332 | 333 | private final Runnable mSetIdleRunnable = new Runnable() { 334 | public void run() { 335 | setDragState(STATE_IDLE); 336 | } 337 | }; 338 | 339 | /** 340 | * Factory method to create a new ViewDragHelper. 341 | * 342 | * @param forParent Parent view to monitor 343 | * @param cb Callback to provide information and receive events 344 | * @return a new ViewDragHelper instance 345 | */ 346 | public static ViewDragHelper create(ViewGroup forParent, Callback cb) { 347 | return new ViewDragHelper(forParent.getContext(), forParent, cb); 348 | } 349 | 350 | /** 351 | * Factory method to create a new ViewDragHelper. 352 | * 353 | * @param forParent Parent view to monitor 354 | * @param sensitivity Multiplier for how sensitive the helper should be about detecting 355 | * the start of a drag. Larger values are more sensitive. 1.0f is normal. 356 | * @param cb Callback to provide information and receive events 357 | * @return a new ViewDragHelper instance 358 | */ 359 | public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) { 360 | final ViewDragHelper helper = create(forParent, cb); 361 | helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity)); 362 | return helper; 363 | } 364 | 365 | /** 366 | * Apps should use ViewDragHelper.create() to get a new instance. 367 | * This will allow VDH to use internal compatibility implementations for different 368 | * platform versions. 369 | * 370 | * @param context Context to initialize config-dependent params from 371 | * @param forParent Parent view to monitor 372 | */ 373 | private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) { 374 | if (forParent == null) { 375 | throw new IllegalArgumentException("Parent view may not be null"); 376 | } 377 | if (cb == null) { 378 | throw new IllegalArgumentException("Callback may not be null"); 379 | } 380 | 381 | mParentView = forParent; 382 | mCallback = cb; 383 | 384 | final ViewConfiguration vc = ViewConfiguration.get(context); 385 | final float density = context.getResources().getDisplayMetrics().density; 386 | mEdgeSize = (int) (EDGE_SIZE * density + 0.5f); 387 | 388 | mTouchSlop = vc.getScaledTouchSlop(); 389 | mMaxVelocity = vc.getScaledMaximumFlingVelocity(); 390 | mMinVelocity = vc.getScaledMinimumFlingVelocity(); 391 | mScroller = ScrollerCompat.create(context, sInterpolator); 392 | } 393 | 394 | /** 395 | * Set the minimum velocity that will be detected as having a magnitude greater than zero 396 | * in pixels per second. Callback methods accepting a velocity will be clamped appropriately. 397 | * 398 | * @param minVel Minimum velocity to detect 399 | */ 400 | public void setMinVelocity(float minVel) { 401 | mMinVelocity = minVel; 402 | } 403 | 404 | /** 405 | * Return the currently configured minimum velocity. Any flings with a magnitude less 406 | * than this value in pixels per second. Callback methods accepting a velocity will receive 407 | * zero as a velocity value if the real detected velocity was below this threshold. 408 | * 409 | * @return the minimum velocity that will be detected 410 | */ 411 | public float getMinVelocity() { 412 | return mMinVelocity; 413 | } 414 | 415 | /** 416 | * Retrieve the current drag state of this helper. This will return one of 417 | * {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}. 418 | * @return The current drag state 419 | */ 420 | public int getViewDragState() { 421 | return mDragState; 422 | } 423 | 424 | /** 425 | * Enable edge tracking for the selected edges of the parent view. 426 | * The callback's {@link Callback#onEdgeTouched(int, int)} and 427 | * {@link Callback#onEdgeDragStarted(int, int)} methods will only be invoked 428 | * for edges for which edge tracking has been enabled. 429 | * 430 | * @param edgeFlags Combination of edge flags describing the edges to watch 431 | * @see #EDGE_LEFT 432 | * @see #EDGE_TOP 433 | * @see #EDGE_RIGHT 434 | * @see #EDGE_BOTTOM 435 | */ 436 | public void setEdgeTrackingEnabled(int edgeFlags) { 437 | mTrackingEdges = edgeFlags; 438 | } 439 | 440 | /** 441 | * Return the size of an edge. This is the range in pixels along the edges of this view 442 | * that will actively detect edge touches or drags if edge tracking is enabled. 443 | * 444 | * @return The size of an edge in pixels 445 | * @see #setEdgeTrackingEnabled(int) 446 | */ 447 | public int getEdgeSize() { 448 | return mEdgeSize; 449 | } 450 | 451 | /** 452 | * Capture a specific child view for dragging within the parent. The callback will be notified 453 | * but {@link Callback#tryCaptureView(android.view.View, int)} will not be asked permission to 454 | * capture this view. 455 | * 456 | * @param childView Child view to capture 457 | * @param activePointerId ID of the pointer that is dragging the captured child view 458 | */ 459 | public void captureChildView(View childView, int activePointerId) { 460 | if (childView.getParent() != mParentView) { 461 | throw new IllegalArgumentException("captureChildView: parameter must be a descendant " + 462 | "of the ViewDragHelper's tracked parent view (" + mParentView + ")"); 463 | } 464 | 465 | mCapturedView = childView; 466 | mActivePointerId = activePointerId; 467 | mCallback.onViewCaptured(childView, activePointerId); 468 | setDragState(STATE_DRAGGING); 469 | } 470 | 471 | /** 472 | * @return The currently captured view, or null if no view has been captured. 473 | */ 474 | public View getCapturedView() { 475 | return mCapturedView; 476 | } 477 | 478 | /** 479 | * @return The ID of the pointer currently dragging the captured view, 480 | * or {@link #INVALID_POINTER}. 481 | */ 482 | public int getActivePointerId() { 483 | return mActivePointerId; 484 | } 485 | 486 | /** 487 | * @return The minimum distance in pixels that the user must travel to initiate a drag 488 | */ 489 | public int getTouchSlop() { 490 | return mTouchSlop; 491 | } 492 | 493 | /** 494 | * The result of a call to this method is equivalent to 495 | * {@link #processTouchEvent(android.view.MotionEvent)} receiving an ACTION_CANCEL event. 496 | */ 497 | public void cancel() { 498 | mActivePointerId = INVALID_POINTER; 499 | clearMotionHistory(); 500 | 501 | if (mVelocityTracker != null) { 502 | mVelocityTracker.recycle(); 503 | mVelocityTracker = null; 504 | } 505 | } 506 | 507 | /** 508 | * {@link #cancel()}, but also abort all motion in progress and snap to the end of any 509 | * animation. 510 | */ 511 | public void abort() { 512 | cancel(); 513 | if (mDragState == STATE_SETTLING) { 514 | final int oldX = mScroller.getCurrX(); 515 | final int oldY = mScroller.getCurrY(); 516 | mScroller.abortAnimation(); 517 | final int newX = mScroller.getCurrX(); 518 | final int newY = mScroller.getCurrY(); 519 | mCallback.onViewPositionChanged(mCapturedView, newX, newY, newX - oldX, newY - oldY); 520 | } 521 | setDragState(STATE_IDLE); 522 | } 523 | 524 | /** 525 | * Animate the viewchild to the given (left, top) position.
526 | * If this method returns true, the caller should invoke {@link #continueSettling(boolean)}
527 | * on each subsequent frame to continue the motion until it returns false. If this method
528 | * returns false there is no further work to do to complete the movement.
529 | *
530 | * This operation does not count as a capture event, though {@link #getCapturedView()} 531 | * will still report the sliding view while the slide is in progress.
532 | * 533 | * @param child Child view to capture and animate 534 | * @param finalLeft Final left position of child 535 | * @param finalTop Final top position of child 536 | * @return true if animation should continue through {@link #continueSettling(boolean)} calls 537 | */ 538 | public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) { 539 | mCapturedView = child; 540 | mActivePointerId = INVALID_POINTER; 541 | 542 | return forceSettleCapturedViewAt(finalLeft, finalTop, 0, 0); 543 | } 544 | 545 | /** 546 | * Settle the captured view at the given (left, top) position. 547 | * The appropriate velocity from prior motion will be taken into account. 548 | * If this method returns true, the caller should invoke {@link #continueSettling(boolean)} 549 | * on each subsequent frame to continue the motion until it returns false. If this method 550 | * returns false there is no further work to do to complete the movement. 551 | * 552 | * @param finalLeft Settled left edge position for the captured view 553 | * @param finalTop Settled top edge position for the captured view 554 | * @return true if animation should continue through {@link #continueSettling(boolean)} calls 555 | */ 556 | public boolean settleCapturedViewAt(int finalLeft, int finalTop) { 557 | if (!mReleaseInProgress) { 558 | throw new IllegalStateException("Cannot settleCapturedViewAt outside of a call to " + 559 | "Callback#onViewReleased"); 560 | } 561 | 562 | return forceSettleCapturedViewAt(finalLeft, finalTop, 563 | (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId), 564 | (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId)); 565 | } 566 | 567 | /** 568 | * Settle the captured view at the given (left, top) position. 569 | * 570 | * @param finalLeft Target left position for the captured view 571 | * @param finalTop Target top position for the captured view 572 | * @param xvel Horizontal velocity 573 | * @param yvel Vertical velocity 574 | * @return true if animation should continue through {@link #continueSettling(boolean)} calls 575 | */ 576 | private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) { 577 | final int startLeft = mCapturedView.getLeft(); 578 | final int startTop = mCapturedView.getTop(); 579 | final int dx = finalLeft - startLeft; 580 | final int dy = finalTop - startTop; 581 | 582 | if (dx == 0 && dy == 0) { 583 | // Nothing to do. Send callbacks, be done. 584 | mScroller.abortAnimation(); 585 | setDragState(STATE_IDLE); 586 | return false; 587 | } 588 | 589 | final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel); 590 | mScroller.startScroll(startLeft, startTop, dx, dy, duration); 591 | 592 | setDragState(STATE_SETTLING); 593 | return true; 594 | } 595 | 596 | private int computeSettleDuration(View child, int dx, int dy, int xvel, int yvel) { 597 | xvel = clampMag(xvel, (int) mMinVelocity, (int) mMaxVelocity); 598 | yvel = clampMag(yvel, (int) mMinVelocity, (int) mMaxVelocity); 599 | final int absDx = Math.abs(dx); 600 | final int absDy = Math.abs(dy); 601 | final int absXVel = Math.abs(xvel); 602 | final int absYVel = Math.abs(yvel); 603 | final int addedVel = absXVel + absYVel; 604 | final int addedDistance = absDx + absDy; 605 | 606 | final float xweight = xvel != 0 ? (float) absXVel / addedVel : 607 | (float) absDx / addedDistance; 608 | final float yweight = yvel != 0 ? (float) absYVel / addedVel : 609 | (float) absDy / addedDistance; 610 | 611 | int xduration = computeAxisDuration(dx, xvel, mCallback.getViewHorizontalDragRange(child)); 612 | int yduration = computeAxisDuration(dy, yvel, mCallback.getViewVerticalDragRange(child)); 613 | 614 | return (int) (xduration * xweight + yduration * yweight); 615 | } 616 | 617 | private int computeAxisDuration(int delta, int velocity, int motionRange) { 618 | if (delta == 0) { 619 | return 0; 620 | } 621 | 622 | final int width = mParentView.getWidth(); 623 | final int halfWidth = width / 2; 624 | final float distanceRatio = Math.min(1f, (float) Math.abs(delta) / width); 625 | final float distance = halfWidth + halfWidth * 626 | distanceInfluenceForSnapDuration(distanceRatio); 627 | 628 | int duration; 629 | velocity = Math.abs(velocity); 630 | if (velocity > 0) { 631 | duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); 632 | } else { 633 | final float range = (float) Math.abs(delta) / motionRange; 634 | duration = (int) ((range + 1) * BASE_SETTLE_DURATION); 635 | } 636 | return Math.min(duration, MAX_SETTLE_DURATION); 637 | } 638 | 639 | /** 640 | * Clamp the magnitude of value for absMin and absMax. 641 | * If the value is below the minimum, it will be clamped to zero. 642 | * If the value is above the maximum, it will be clamped to the maximum. 643 | * 644 | * @param value Value to clamp 645 | * @param absMin Absolute value of the minimum significant value to return 646 | * @param absMax Absolute value of the maximum value to return 647 | * @return The clamped value with the same sign asvalue
648 | */
649 | private int clampMag(int value, int absMin, int absMax) {
650 | final int absValue = Math.abs(value);
651 | if (absValue < absMin) return 0;
652 | if (absValue > absMax) return value > 0 ? absMax : -absMax;
653 | return value;
654 | }
655 |
656 | /**
657 | * Clamp the magnitude of value for absMin and absMax.
658 | * If the value is below the minimum, it will be clamped to zero.
659 | * If the value is above the maximum, it will be clamped to the maximum.
660 | *
661 | * @param value Value to clamp
662 | * @param absMin Absolute value of the minimum significant value to return
663 | * @param absMax Absolute value of the maximum value to return
664 | * @return The clamped value with the same sign as value
665 | */
666 | private float clampMag(float value, float absMin, float absMax) {
667 | final float absValue = Math.abs(value);
668 | if (absValue < absMin) return 0;
669 | if (absValue > absMax) return value > 0 ? absMax : -absMax;
670 | return value;
671 | }
672 |
673 | private float distanceInfluenceForSnapDuration(float f) {
674 | f -= 0.5f; // center the values about 0.
675 | f *= 0.3f * Math.PI / 2.0f;
676 | return (float) Math.sin(f);
677 | }
678 |
679 | /**
680 | * Settle the captured view based on standard free-moving fling behavior.
681 | * The caller should invoke {@link #continueSettling(boolean)} on each subsequent frame
682 | * to continue the motion until it returns false.
683 | *
684 | * @param minLeft Minimum X position for the view's left edge
685 | * @param minTop Minimum Y position for the view's top edge
686 | * @param maxLeft Maximum X position for the view's left edge
687 | * @param maxTop Maximum Y position for the view's top edge
688 | */
689 | public void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) {
690 | if (!mReleaseInProgress) {
691 | throw new IllegalStateException("Cannot flingCapturedView outside of a call to " +
692 | "Callback#onViewReleased");
693 | }
694 |
695 | mScroller.fling(mCapturedView.getLeft(), mCapturedView.getTop(),
696 | (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
697 | (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId),
698 | minLeft, maxLeft, minTop, maxTop);
699 |
700 | setDragState(STATE_SETTLING);
701 | }
702 |
703 | /**
704 | * Move the captured settling view by the appropriate amount for the current time.
705 | * If continueSettling returns true, the caller should call it again
706 | * on the next frame to continue.
707 | *
708 | * @param deferCallbacks true if state callbacks should be deferred via posted message.
709 | * Set this to true if you are calling this method from
710 | * {@link android.view.View#computeScroll()} or similar methods
711 | * invoked as part of layout or drawing.
712 | * @return true if settle is still in progress
713 | */
714 | public boolean continueSettling(boolean deferCallbacks) {
715 | if (mDragState == STATE_SETTLING) {
716 | boolean keepGoing = mScroller.computeScrollOffset();
717 | final int x = mScroller.getCurrX();
718 | final int y = mScroller.getCurrY();
719 | final int dx = x - mCapturedView.getLeft();
720 | final int dy = y - mCapturedView.getTop();
721 |
722 | if (dx != 0) {
723 | mCapturedView.offsetLeftAndRight(dx);
724 | }
725 | if (dy != 0) {
726 | mCapturedView.offsetTopAndBottom(dy);
727 | }
728 |
729 | if (dx != 0 || dy != 0) {
730 | mCallback.onViewPositionChanged(mCapturedView, x, y, dx, dy);
731 | }
732 |
733 | if (keepGoing && x == mScroller.getFinalX() && y == mScroller.getFinalY()) {
734 | // Close enough. The interpolator/scroller might think we're still moving
735 | // but the user sure doesn't.
736 | mScroller.abortAnimation();
737 | keepGoing = mScroller.isFinished();
738 | }
739 |
740 | if (!keepGoing) {
741 | if (deferCallbacks) {
742 | mParentView.post(mSetIdleRunnable);
743 | } else {
744 | setDragState(STATE_IDLE);
745 | }
746 | }
747 | }
748 |
749 | return mDragState == STATE_SETTLING;
750 | }
751 |
752 | /**
753 | * Like all callback events this must happen on the UI thread, but release
754 | * involves some extra semantics. During a release (mReleaseInProgress)
755 | * is the only time it is valid to call {@link #settleCapturedViewAt(int, int)}
756 | * or {@link #flingCapturedView(int, int, int, int)}.
757 | */
758 | private void dispatchViewReleased(float xvel, float yvel) {
759 | mReleaseInProgress = true;
760 | mCallback.onViewReleased(mCapturedView, xvel, yvel);
761 | mReleaseInProgress = false;
762 |
763 | if (mDragState == STATE_DRAGGING) {
764 | // onViewReleased didn't call a method that would have changed this. Go idle.
765 | setDragState(STATE_IDLE);
766 | }
767 | }
768 |
769 | private void clearMotionHistory() {
770 | if (mInitialMotionX == null) {
771 | return;
772 | }
773 | Arrays.fill(mInitialMotionX, 0);
774 | Arrays.fill(mInitialMotionY, 0);
775 | Arrays.fill(mLastMotionX, 0);
776 | Arrays.fill(mLastMotionY, 0);
777 | Arrays.fill(mInitialEdgesTouched, 0);
778 | Arrays.fill(mEdgeDragsInProgress, 0);
779 | Arrays.fill(mEdgeDragsLocked, 0);
780 | mPointersDown = 0;
781 | }
782 |
783 | private void clearMotionHistory(int pointerId) {
784 | if (mInitialMotionX == null) {
785 | return;
786 | }
787 | mInitialMotionX[pointerId] = 0;
788 | mInitialMotionY[pointerId] = 0;
789 | mLastMotionX[pointerId] = 0;
790 | mLastMotionY[pointerId] = 0;
791 | mInitialEdgesTouched[pointerId] = 0;
792 | mEdgeDragsInProgress[pointerId] = 0;
793 | mEdgeDragsLocked[pointerId] = 0;
794 | mPointersDown &= ~(1 << pointerId);
795 | }
796 |
797 | private void ensureMotionHistorySizeForId(int pointerId) {
798 | if (mInitialMotionX == null || mInitialMotionX.length <= pointerId) {
799 | float[] imx = new float[pointerId + 1];
800 | float[] imy = new float[pointerId + 1];
801 | float[] lmx = new float[pointerId + 1];
802 | float[] lmy = new float[pointerId + 1];
803 | int[] iit = new int[pointerId + 1];
804 | int[] edip = new int[pointerId + 1];
805 | int[] edl = new int[pointerId + 1];
806 |
807 | if (mInitialMotionX != null) {
808 | System.arraycopy(mInitialMotionX, 0, imx, 0, mInitialMotionX.length);
809 | System.arraycopy(mInitialMotionY, 0, imy, 0, mInitialMotionY.length);
810 | System.arraycopy(mLastMotionX, 0, lmx, 0, mLastMotionX.length);
811 | System.arraycopy(mLastMotionY, 0, lmy, 0, mLastMotionY.length);
812 | System.arraycopy(mInitialEdgesTouched, 0, iit, 0, mInitialEdgesTouched.length);
813 | System.arraycopy(mEdgeDragsInProgress, 0, edip, 0, mEdgeDragsInProgress.length);
814 | System.arraycopy(mEdgeDragsLocked, 0, edl, 0, mEdgeDragsLocked.length);
815 | }
816 |
817 | mInitialMotionX = imx;
818 | mInitialMotionY = imy;
819 | mLastMotionX = lmx;
820 | mLastMotionY = lmy;
821 | mInitialEdgesTouched = iit;
822 | mEdgeDragsInProgress = edip;
823 | mEdgeDragsLocked = edl;
824 | }
825 | }
826 |
827 | private void saveInitialMotion(float x, float y, int pointerId) {
828 | ensureMotionHistorySizeForId(pointerId);
829 | mInitialMotionX[pointerId] = mLastMotionX[pointerId] = x;
830 | mInitialMotionY[pointerId] = mLastMotionY[pointerId] = y;
831 | mInitialEdgesTouched[pointerId] = getEdgesTouched((int) x, (int) y);
832 | mPointersDown |= 1 << pointerId;
833 | }
834 |
835 | private void saveLastMotion(MotionEvent ev) {
836 | final int pointerCount = MotionEventCompat.getPointerCount(ev);
837 | for (int i = 0; i < pointerCount; i++) {
838 | final int pointerId = MotionEventCompat.getPointerId(ev, i);
839 | final float x = MotionEventCompat.getX(ev, i);
840 | final float y = MotionEventCompat.getY(ev, i);
841 | mLastMotionX[pointerId] = x;
842 | mLastMotionY[pointerId] = y;
843 | }
844 | }
845 |
846 | /**
847 | * Check if the given pointer ID represents a pointer that is currently down (to the best
848 | * of the ViewDragHelper's knowledge).
849 | *
850 | * The state used to report this information is populated by the methods 851 | * {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or 852 | * {@link #processTouchEvent(android.view.MotionEvent)}. If one of these methods has not 853 | * been called for all relevant MotionEvents to track, the information reported 854 | * by this method may be stale or incorrect.
855 | * 856 | * @param pointerId pointer ID to check; corresponds to IDs provided by MotionEvent 857 | * @return true if the pointer with the given ID is still down 858 | */ 859 | public boolean isPointerDown(int pointerId) { 860 | return (mPointersDown & 1 << pointerId) != 0; 861 | } 862 | 863 | void setDragState(int state) { 864 | if (mDragState != state) { 865 | mDragState = state; 866 | mCallback.onViewDragStateChanged(state); 867 | if (state == STATE_IDLE) { 868 | mCapturedView = null; 869 | } 870 | } 871 | } 872 | 873 | /** 874 | * Attempt to capture the view with the given pointer ID. The callback will be involved. 875 | * This will put us into the "dragging" state. If we've already captured this view with 876 | * this pointer this method will immediately return true without consulting the callback. 877 | * 878 | * @param toCapture View to capture 879 | * @param pointerId Pointer to capture with 880 | * @return true if capture was successful 881 | */ 882 | boolean tryCaptureViewForDrag(View toCapture, int pointerId) { 883 | if (toCapture == mCapturedView && mActivePointerId == pointerId) { 884 | // Already done! 885 | return true; 886 | } 887 | if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) { 888 | mActivePointerId = pointerId; 889 | captureChildView(toCapture, pointerId); 890 | return true; 891 | } 892 | return false; 893 | } 894 | 895 | /** 896 | * Tests scrollability within child views of v given a delta of dx. 897 | * 898 | * @param v View to test for horizontal scrollability 899 | * @param checkV Whether the view v passed should itself be checked for scrollability (true), 900 | * or just its children (false). 901 | * @param dx Delta scrolled in pixels along the X axis 902 | * @param dy Delta scrolled in pixels along the Y axis 903 | * @param x X coordinate of the active touch point 904 | * @param y Y coordinate of the active touch point 905 | * @return true if child views of v can be scrolled by delta of dx. 906 | */ 907 | protected boolean canScroll(View v, boolean checkV, int dx, int dy, int x, int y) { 908 | if (v instanceof ViewGroup) { 909 | final ViewGroup group = (ViewGroup) v; 910 | final int scrollX = v.getScrollX(); 911 | final int scrollY = v.getScrollY(); 912 | final int count = group.getChildCount(); 913 | // Count backwards - let topmost views consume scroll distance first. 914 | for (int i = count - 1; i >= 0; i--) { 915 | // TODO: Add versioned support here for transformed views. 916 | // This will not work for transformed views in Honeycomb+ 917 | final View child = group.getChildAt(i); 918 | if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && 919 | y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && 920 | canScroll(child, true, dx, dy, x + scrollX - child.getLeft(), 921 | y + scrollY - child.getTop())) { 922 | return true; 923 | } 924 | } 925 | } 926 | 927 | return checkV && (ViewCompat.canScrollHorizontally(v, -dx) || 928 | ViewCompat.canScrollVertically(v, -dy)); 929 | } 930 | 931 | /** 932 | * Check if this event as provided to the parent view's onInterceptTouchEvent should 933 | * cause the parent to intercept the touch event stream. 934 | * 935 | * @param ev MotionEvent provided to onInterceptTouchEvent 936 | * @return true if the parent view should return true from onInterceptTouchEvent 937 | */ 938 | public boolean shouldInterceptTouchEvent(MotionEvent ev) { 939 | final int action = MotionEventCompat.getActionMasked(ev); 940 | final int actionIndex = MotionEventCompat.getActionIndex(ev); 941 | 942 | if (action == MotionEvent.ACTION_DOWN) { 943 | // Reset things for a new event stream, just in case we didn't get 944 | // the whole previous stream. 945 | cancel(); 946 | } 947 | 948 | if (mVelocityTracker == null) { 949 | mVelocityTracker = VelocityTracker.obtain(); 950 | } 951 | mVelocityTracker.addMovement(ev); 952 | 953 | switch (action) { 954 | case MotionEvent.ACTION_DOWN: { 955 | final float x = ev.getX(); 956 | final float y = ev.getY(); 957 | final int pointerId = MotionEventCompat.getPointerId(ev, 0); 958 | saveInitialMotion(x, y, pointerId); 959 | 960 | final View toCapture = findTopChildUnder((int) x, (int) y); 961 | 962 | // Catch a settling view if possible. 963 | if (toCapture == mCapturedView && mDragState == STATE_SETTLING) { 964 | tryCaptureViewForDrag(toCapture, pointerId); 965 | } 966 | 967 | final int edgesTouched = mInitialEdgesTouched[pointerId]; 968 | if ((edgesTouched & mTrackingEdges) != 0) { 969 | mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); 970 | } 971 | break; 972 | } 973 | 974 | case MotionEventCompat.ACTION_POINTER_DOWN: { 975 | final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); 976 | final float x = MotionEventCompat.getX(ev, actionIndex); 977 | final float y = MotionEventCompat.getY(ev, actionIndex); 978 | 979 | saveInitialMotion(x, y, pointerId); 980 | 981 | // A ViewDragHelper can only manipulate one view at a time. 982 | if (mDragState == STATE_IDLE) { 983 | final int edgesTouched = mInitialEdgesTouched[pointerId]; 984 | if ((edgesTouched & mTrackingEdges) != 0) { 985 | mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); 986 | } 987 | } else if (mDragState == STATE_SETTLING) { 988 | // Catch a settling view if possible. 989 | final View toCapture = findTopChildUnder((int) x, (int) y); 990 | if (toCapture == mCapturedView) { 991 | tryCaptureViewForDrag(toCapture, pointerId); 992 | } 993 | } 994 | break; 995 | } 996 | 997 | case MotionEvent.ACTION_MOVE: { 998 | // First to cross a touch slop over a draggable view wins. Also report edge drags. 999 | final int pointerCount = MotionEventCompat.getPointerCount(ev); 1000 | for (int i = 0; i < pointerCount; i++) { 1001 | final int pointerId = MotionEventCompat.getPointerId(ev, i); 1002 | final float x = MotionEventCompat.getX(ev, i); 1003 | final float y = MotionEventCompat.getY(ev, i); 1004 | final float dx = x - mInitialMotionX[pointerId]; 1005 | final float dy = y - mInitialMotionY[pointerId]; 1006 | 1007 | reportNewEdgeDrags(dx, dy, pointerId); 1008 | if (mDragState == STATE_DRAGGING) { 1009 | // Callback might have started an edge drag 1010 | break; 1011 | } 1012 | 1013 | final View toCapture = findTopChildUnder((int) x, (int) y); 1014 | if (toCapture != null && checkTouchSlop(toCapture, dx, dy) && 1015 | tryCaptureViewForDrag(toCapture, pointerId)) { 1016 | break; 1017 | } 1018 | } 1019 | saveLastMotion(ev); 1020 | break; 1021 | } 1022 | 1023 | case MotionEventCompat.ACTION_POINTER_UP: { 1024 | final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); 1025 | clearMotionHistory(pointerId); 1026 | break; 1027 | } 1028 | 1029 | case MotionEvent.ACTION_UP: 1030 | case MotionEvent.ACTION_CANCEL: { 1031 | cancel(); 1032 | break; 1033 | } 1034 | } 1035 | 1036 | return mDragState == STATE_DRAGGING; 1037 | } 1038 | 1039 | /** 1040 | * Process a touch event received by the parent view. This method will dispatch callback events 1041 | * as needed before returning. The parent view's onTouchEvent implementation should call this. 1042 | * 1043 | * @param ev The touch event received by the parent view 1044 | */ 1045 | public void processTouchEvent(MotionEvent ev) { 1046 | final int action = MotionEventCompat.getActionMasked(ev); 1047 | final int actionIndex = MotionEventCompat.getActionIndex(ev); 1048 | 1049 | if (action == MotionEvent.ACTION_DOWN) { 1050 | // Reset things for a new event stream, just in case we didn't get 1051 | // the whole previous stream. 1052 | cancel(); 1053 | } 1054 | 1055 | if (mVelocityTracker == null) { 1056 | mVelocityTracker = VelocityTracker.obtain(); 1057 | } 1058 | mVelocityTracker.addMovement(ev); 1059 | 1060 | switch (action) { 1061 | case MotionEvent.ACTION_DOWN: { 1062 | final float x = ev.getX(); 1063 | final float y = ev.getY(); 1064 | final int pointerId = MotionEventCompat.getPointerId(ev, 0); 1065 | final View toCapture = findTopChildUnder((int) x, (int) y); 1066 | 1067 | saveInitialMotion(x, y, pointerId); 1068 | 1069 | // Since the parent is already directly processing this touch event, 1070 | // there is no reason to delay for a slop before dragging. 1071 | // Start immediately if possible. 1072 | tryCaptureViewForDrag(toCapture, pointerId); 1073 | 1074 | final int edgesTouched = mInitialEdgesTouched[pointerId]; 1075 | if ((edgesTouched & mTrackingEdges) != 0) { 1076 | mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); 1077 | } 1078 | break; 1079 | } 1080 | 1081 | case MotionEventCompat.ACTION_POINTER_DOWN: { 1082 | final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); 1083 | final float x = MotionEventCompat.getX(ev, actionIndex); 1084 | final float y = MotionEventCompat.getY(ev, actionIndex); 1085 | 1086 | saveInitialMotion(x, y, pointerId); 1087 | 1088 | // A ViewDragHelper can only manipulate one view at a time. 1089 | if (mDragState == STATE_IDLE) { 1090 | // If we're idle we can do anything! Treat it like a normal down event. 1091 | 1092 | final View toCapture = findTopChildUnder((int) x, (int) y); 1093 | tryCaptureViewForDrag(toCapture, pointerId); 1094 | 1095 | final int edgesTouched = mInitialEdgesTouched[pointerId]; 1096 | if ((edgesTouched & mTrackingEdges) != 0) { 1097 | mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); 1098 | } 1099 | } else if (isCapturedViewUnder((int) x, (int) y)) { 1100 | // We're still tracking a captured view. If the same view is under this 1101 | // point, we'll swap to controlling it with this pointer instead. 1102 | // (This will still work if we're "catching" a settling view.) 1103 | 1104 | tryCaptureViewForDrag(mCapturedView, pointerId); 1105 | } 1106 | break; 1107 | } 1108 | 1109 | case MotionEvent.ACTION_MOVE: { 1110 | if (mDragState == STATE_DRAGGING) { 1111 | final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId); 1112 | final float x = MotionEventCompat.getX(ev, index); 1113 | final float y = MotionEventCompat.getY(ev, index); 1114 | final int idx = (int) (x - mLastMotionX[mActivePointerId]); 1115 | final int idy = (int) (y - mLastMotionY[mActivePointerId]); 1116 | 1117 | dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy); 1118 | 1119 | saveLastMotion(ev); 1120 | } else { 1121 | // Check to see if any pointer is now over a draggable view. 1122 | final int pointerCount = MotionEventCompat.getPointerCount(ev); 1123 | for (int i = 0; i < pointerCount; i++) { 1124 | final int pointerId = MotionEventCompat.getPointerId(ev, i); 1125 | final float x = MotionEventCompat.getX(ev, i); 1126 | final float y = MotionEventCompat.getY(ev, i); 1127 | final float dx = x - mInitialMotionX[pointerId]; 1128 | final float dy = y - mInitialMotionY[pointerId]; 1129 | 1130 | reportNewEdgeDrags(dx, dy, pointerId); 1131 | if (mDragState == STATE_DRAGGING) { 1132 | // Callback might have started an edge drag. 1133 | break; 1134 | } 1135 | 1136 | final View toCapture = findTopChildUnder((int) x, (int) y); 1137 | if (checkTouchSlop(toCapture, dx, dy) && 1138 | tryCaptureViewForDrag(toCapture, pointerId)) { 1139 | break; 1140 | } 1141 | } 1142 | saveLastMotion(ev); 1143 | } 1144 | break; 1145 | } 1146 | 1147 | case MotionEventCompat.ACTION_POINTER_UP: { 1148 | final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); 1149 | if (mDragState == STATE_DRAGGING && pointerId == mActivePointerId) { 1150 | // Try to find another pointer that's still holding on to the captured view. 1151 | int newActivePointer = INVALID_POINTER; 1152 | final int pointerCount = MotionEventCompat.getPointerCount(ev); 1153 | for (int i = 0; i < pointerCount; i++) { 1154 | final int id = MotionEventCompat.getPointerId(ev, i); 1155 | if (id == mActivePointerId) { 1156 | // This one's going away, skip. 1157 | continue; 1158 | } 1159 | 1160 | final float x = MotionEventCompat.getX(ev, i); 1161 | final float y = MotionEventCompat.getY(ev, i); 1162 | if (findTopChildUnder((int) x, (int) y) == mCapturedView && 1163 | tryCaptureViewForDrag(mCapturedView, id)) { 1164 | newActivePointer = mActivePointerId; 1165 | break; 1166 | } 1167 | } 1168 | 1169 | if (newActivePointer == INVALID_POINTER) { 1170 | // We didn't find another pointer still touching the view, release it. 1171 | releaseViewForPointerUp(); 1172 | } 1173 | } 1174 | clearMotionHistory(pointerId); 1175 | break; 1176 | } 1177 | 1178 | case MotionEvent.ACTION_UP: { 1179 | if (mDragState == STATE_DRAGGING) { 1180 | releaseViewForPointerUp(); 1181 | } 1182 | cancel(); 1183 | break; 1184 | } 1185 | 1186 | case MotionEvent.ACTION_CANCEL: { 1187 | if (mDragState == STATE_DRAGGING) { 1188 | dispatchViewReleased(0, 0); 1189 | } 1190 | cancel(); 1191 | break; 1192 | } 1193 | } 1194 | } 1195 | 1196 | private void reportNewEdgeDrags(float dx, float dy, int pointerId) { 1197 | int dragsStarted = 0; 1198 | if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_LEFT)) { 1199 | dragsStarted |= EDGE_LEFT; 1200 | } 1201 | if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_TOP)) { 1202 | dragsStarted |= EDGE_TOP; 1203 | } 1204 | if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_RIGHT)) { 1205 | dragsStarted |= EDGE_RIGHT; 1206 | } 1207 | if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_BOTTOM)) { 1208 | dragsStarted |= EDGE_BOTTOM; 1209 | } 1210 | 1211 | if (dragsStarted != 0) { 1212 | mEdgeDragsInProgress[pointerId] |= dragsStarted; 1213 | mCallback.onEdgeDragStarted(dragsStarted, pointerId); 1214 | } 1215 | } 1216 | 1217 | private boolean checkNewEdgeDrag(float delta, float odelta, int pointerId, int edge) { 1218 | final float absDelta = Math.abs(delta); 1219 | final float absODelta = Math.abs(odelta); 1220 | 1221 | if ((mInitialEdgesTouched[pointerId] & edge) != edge || (mTrackingEdges & edge) == 0 || 1222 | (mEdgeDragsLocked[pointerId] & edge) == edge || 1223 | (mEdgeDragsInProgress[pointerId] & edge) == edge || 1224 | (absDelta <= mTouchSlop && absODelta <= mTouchSlop)) { 1225 | return false; 1226 | } 1227 | if (absDelta < absODelta * 0.5f && mCallback.onEdgeLock(edge)) { 1228 | mEdgeDragsLocked[pointerId] |= edge; 1229 | return false; 1230 | } 1231 | return (mEdgeDragsInProgress[pointerId] & edge) == 0 && absDelta > mTouchSlop; 1232 | } 1233 | 1234 | /** 1235 | * Check if we've crossed a reasonable touch slop for the given child view. 1236 | * If the child cannot be dragged along the horizontal or vertical axis, motion 1237 | * along that axis will not count toward the slop check. 1238 | * 1239 | * @param child Child to check 1240 | * @param dx Motion since initial position along X axis 1241 | * @param dy Motion since initial position along Y axis 1242 | * @return true if the touch slop has been crossed 1243 | */ 1244 | private boolean checkTouchSlop(View child, float dx, float dy) { 1245 | if (child == null) { 1246 | return false; 1247 | } 1248 | final boolean checkHorizontal = mCallback.getViewHorizontalDragRange(child) > 0; 1249 | final boolean checkVertical = mCallback.getViewVerticalDragRange(child) > 0; 1250 | 1251 | if (checkHorizontal && checkVertical) { 1252 | return dx * dx + dy * dy > mTouchSlop * mTouchSlop; 1253 | } else if (checkHorizontal) { 1254 | return Math.abs(dx) > mTouchSlop; 1255 | } else if (checkVertical) { 1256 | return Math.abs(dy) > mTouchSlop; 1257 | } 1258 | return false; 1259 | } 1260 | 1261 | /** 1262 | * Check if any pointer tracked in the current gesture has crossed 1263 | * the required slop threshold. 1264 | * 1265 | *This depends on internal state populated by 1266 | * {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or 1267 | * {@link #processTouchEvent(android.view.MotionEvent)}. You should only rely on 1268 | * the results of this method after all currently available touch data 1269 | * has been provided to one of these two methods.
1270 | * 1271 | * @param directions Combination of direction flags, see {@link #DIRECTION_HORIZONTAL}, 1272 | * {@link #DIRECTION_VERTICAL}, {@link #DIRECTION_ALL} 1273 | * @return true if the slop threshold has been crossed, false otherwise 1274 | */ 1275 | public boolean checkTouchSlop(int directions) { 1276 | final int count = mInitialMotionX.length; 1277 | for (int i = 0; i < count; i++) { 1278 | if (checkTouchSlop(directions, i)) { 1279 | return true; 1280 | } 1281 | } 1282 | return false; 1283 | } 1284 | 1285 | /** 1286 | * Check if the specified pointer tracked in the current gesture has crossed 1287 | * the required slop threshold. 1288 | * 1289 | *This depends on internal state populated by 1290 | * {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or 1291 | * {@link #processTouchEvent(android.view.MotionEvent)}. You should only rely on 1292 | * the results of this method after all currently available touch data 1293 | * has been provided to one of these two methods.
1294 | * 1295 | * @param directions Combination of direction flags, see {@link #DIRECTION_HORIZONTAL}, 1296 | * {@link #DIRECTION_VERTICAL}, {@link #DIRECTION_ALL} 1297 | * @param pointerId ID of the pointer to slop check as specified by MotionEvent 1298 | * @return true if the slop threshold has been crossed, false otherwise 1299 | */ 1300 | public boolean checkTouchSlop(int directions, int pointerId) { 1301 | if (!isPointerDown(pointerId)) { 1302 | return false; 1303 | } 1304 | 1305 | final boolean checkHorizontal = (directions & DIRECTION_HORIZONTAL) == DIRECTION_HORIZONTAL; 1306 | final boolean checkVertical = (directions & DIRECTION_VERTICAL) == DIRECTION_VERTICAL; 1307 | 1308 | final float dx = mLastMotionX[pointerId] - mInitialMotionX[pointerId]; 1309 | final float dy = mLastMotionY[pointerId] - mInitialMotionY[pointerId]; 1310 | 1311 | if (checkHorizontal && checkVertical) { 1312 | return dx * dx + dy * dy > mTouchSlop * mTouchSlop; 1313 | } else if (checkHorizontal) { 1314 | return Math.abs(dx) > mTouchSlop; 1315 | } else if (checkVertical) { 1316 | return Math.abs(dy) > mTouchSlop; 1317 | } 1318 | return false; 1319 | } 1320 | 1321 | /** 1322 | * Check if any of the edges specified were initially touched in the currently active gesture. 1323 | * If there is no currently active gesture this method will return false. 1324 | * 1325 | * @param edges Edges to check for an initial edge touch. See {@link #EDGE_LEFT}, 1326 | * {@link #EDGE_TOP}, {@link #EDGE_RIGHT}, {@link #EDGE_BOTTOM} and 1327 | * {@link #EDGE_ALL} 1328 | * @return true if any of the edges specified were initially touched in the current gesture 1329 | */ 1330 | public boolean isEdgeTouched(int edges) { 1331 | final int count = mInitialEdgesTouched.length; 1332 | for (int i = 0; i < count; i++) { 1333 | if (isEdgeTouched(edges, i)) { 1334 | return true; 1335 | } 1336 | } 1337 | return false; 1338 | } 1339 | 1340 | /** 1341 | * Check if any of the edges specified were initially touched by the pointer with 1342 | * the specified ID. If there is no currently active gesture or if there is no pointer with 1343 | * the given ID currently down this method will return false. 1344 | * 1345 | * @param edges Edges to check for an initial edge touch. See {@link #EDGE_LEFT}, 1346 | * {@link #EDGE_TOP}, {@link #EDGE_RIGHT}, {@link #EDGE_BOTTOM} and 1347 | * {@link #EDGE_ALL} 1348 | * @return true if any of the edges specified were initially touched in the current gesture 1349 | */ 1350 | public boolean isEdgeTouched(int edges, int pointerId) { 1351 | return isPointerDown(pointerId) && (mInitialEdgesTouched[pointerId] & edges) != 0; 1352 | } 1353 | 1354 | private void releaseViewForPointerUp() { 1355 | mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity); 1356 | final float xvel = clampMag( 1357 | VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId), 1358 | mMinVelocity, mMaxVelocity); 1359 | final float yvel = clampMag( 1360 | VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId), 1361 | mMinVelocity, mMaxVelocity); 1362 | dispatchViewReleased(xvel, yvel); 1363 | } 1364 | 1365 | private void dragTo(int left, int top, int dx, int dy) { 1366 | int clampedX = left; 1367 | int clampedY = top; 1368 | final int oldLeft = mCapturedView.getLeft(); 1369 | final int oldTop = mCapturedView.getTop(); 1370 | if (dx != 0) { 1371 | clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx); 1372 | mCapturedView.offsetLeftAndRight(clampedX - oldLeft); 1373 | } 1374 | if (dy != 0) { 1375 | clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy); 1376 | mCapturedView.offsetTopAndBottom(clampedY - oldTop); 1377 | } 1378 | 1379 | if (dx != 0 || dy != 0) { 1380 | final int clampedDx = clampedX - oldLeft; 1381 | final int clampedDy = clampedY - oldTop; 1382 | mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY, 1383 | clampedDx, clampedDy); 1384 | } 1385 | } 1386 | 1387 | /** 1388 | * Determine if the currently captured view is under the given point in the 1389 | * parent view's coordinate system. If there is no captured view this method 1390 | * will return false. 1391 | * 1392 | * @param x X position to test in the parent's coordinate system 1393 | * @param y Y position to test in the parent's coordinate system 1394 | * @return true if the captured view is under the given point, false otherwise 1395 | */ 1396 | public boolean isCapturedViewUnder(int x, int y) { 1397 | return isViewUnder(mCapturedView, x, y); 1398 | } 1399 | 1400 | /** 1401 | * Determine if the supplied view is under the given point in the 1402 | * parent view's coordinate system. 1403 | * 1404 | * @param view Child view of the parent to hit test 1405 | * @param x X position to test in the parent's coordinate system 1406 | * @param y Y position to test in the parent's coordinate system 1407 | * @return true if the supplied view is under the given point, false otherwise 1408 | */ 1409 | public boolean isViewUnder(View view, int x, int y) { 1410 | if (view == null) { 1411 | return false; 1412 | } 1413 | return x >= view.getLeft() && 1414 | x < view.getRight() && 1415 | y >= view.getTop() && 1416 | y < view.getBottom(); 1417 | } 1418 | 1419 | /** 1420 | * Find the topmost child under the given point within the parent view's coordinate system. 1421 | * The child order is determined using {@link Callback#getOrderedChildIndex(int)}. 1422 | * 1423 | * @param x X position to test in the parent's coordinate system 1424 | * @param y Y position to test in the parent's coordinate system 1425 | * @return The topmost child view under (x, y) or null if none found. 1426 | */ 1427 | public View findTopChildUnder(int x, int y) { 1428 | final int childCount = mParentView.getChildCount(); 1429 | for (int i = childCount - 1; i >= 0; i--) { 1430 | final View child = mParentView.getChildAt(mCallback.getOrderedChildIndex(i)); 1431 | if (x >= child.getLeft() && x < child.getRight() && 1432 | y >= child.getTop() && y < child.getBottom()) { 1433 | return child; 1434 | } 1435 | } 1436 | return null; 1437 | } 1438 | 1439 | private int getEdgesTouched(int x, int y) { 1440 | int result = 0; 1441 | 1442 | if (x < mParentView.getLeft() + mEdgeSize) result |= EDGE_LEFT; 1443 | if (y < mParentView.getTop() + mEdgeSize) result |= EDGE_TOP; 1444 | if (x > mParentView.getRight() - mEdgeSize) result |= EDGE_RIGHT; 1445 | if (y > mParentView.getBottom() - mEdgeSize) result |= EDGE_BOTTOM; 1446 | 1447 | return result; 1448 | } 1449 | } 1450 | --------------------------------------------------------------------------------