SwipeBackLayout.java 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608
  1. package me.imid.swipebacklayout.lib;
  2. import android.app.Activity;
  3. import android.content.Context;
  4. import android.content.res.TypedArray;
  5. import android.graphics.Canvas;
  6. import android.graphics.Rect;
  7. import android.graphics.drawable.Drawable;
  8. import android.support.v4.view.ViewCompat;
  9. import android.util.AttributeSet;
  10. import android.view.MotionEvent;
  11. import android.view.View;
  12. import android.view.ViewGroup;
  13. import android.widget.FrameLayout;
  14. import java.util.ArrayList;
  15. import java.util.List;
  16. public class SwipeBackLayout extends FrameLayout {
  17. /**
  18. * Minimum velocity that will be detected as a fling
  19. */
  20. private static final int MIN_FLING_VELOCITY = 400; // dips per second
  21. private static final int DEFAULT_SCRIM_COLOR = 0x99000000;
  22. private static final int FULL_ALPHA = 255;
  23. /**
  24. * Edge flag indicating that the left edge should be affected.
  25. */
  26. public static final int EDGE_LEFT = ViewDragHelper.EDGE_LEFT;
  27. /**
  28. * Edge flag indicating that the right edge should be affected.
  29. */
  30. public static final int EDGE_RIGHT = ViewDragHelper.EDGE_RIGHT;
  31. /**
  32. * Edge flag indicating that the bottom edge should be affected.
  33. */
  34. public static final int EDGE_BOTTOM = ViewDragHelper.EDGE_BOTTOM;
  35. /**
  36. * Edge flag set indicating all edges should be affected.
  37. */
  38. public static final int EDGE_ALL = EDGE_LEFT | EDGE_RIGHT | EDGE_BOTTOM;
  39. /**
  40. * A view is not currently being dragged or animating as a result of a
  41. * fling/snap.
  42. */
  43. public static final int STATE_IDLE = ViewDragHelper.STATE_IDLE;
  44. /**
  45. * A view is currently being dragged. The position is currently changing as
  46. * a result of user input or simulated user input.
  47. */
  48. public static final int STATE_DRAGGING = ViewDragHelper.STATE_DRAGGING;
  49. /**
  50. * A view is currently settling into place as a result of a fling or
  51. * predefined non-interactive motion.
  52. */
  53. public static final int STATE_SETTLING = ViewDragHelper.STATE_SETTLING;
  54. /**
  55. * Default threshold of scroll
  56. */
  57. private static final float DEFAULT_SCROLL_THRESHOLD = 0.3f;
  58. private static final int OVERSCROLL_DISTANCE = 10;
  59. private static final int[] EDGE_FLAGS = {
  60. EDGE_LEFT, EDGE_RIGHT, EDGE_BOTTOM, EDGE_ALL
  61. };
  62. private int mEdgeFlag;
  63. /**
  64. * Threshold of scroll, we will close the activity, when scrollPercent over
  65. * this value;
  66. */
  67. private float mScrollThreshold = DEFAULT_SCROLL_THRESHOLD;
  68. private Activity mActivity;
  69. private boolean mEnable = true;
  70. private View mContentView;
  71. private ViewDragHelper mDragHelper;
  72. private float mScrollPercent;
  73. private int mContentLeft;
  74. private int mContentTop;
  75. /**
  76. * The set of listeners to be sent events through.
  77. */
  78. private List<SwipeListener> mListeners;
  79. private Drawable mShadowLeft;
  80. private Drawable mShadowRight;
  81. private Drawable mShadowBottom;
  82. private float mScrimOpacity;
  83. private int mScrimColor = DEFAULT_SCRIM_COLOR;
  84. private boolean mInLayout;
  85. private Rect mTmpRect = new Rect();
  86. /**
  87. * Edge being dragged
  88. */
  89. private int mTrackingEdge;
  90. public SwipeBackLayout(Context context) {
  91. this(context, null);
  92. }
  93. public SwipeBackLayout(Context context, AttributeSet attrs) {
  94. this(context, attrs, R.attr.SwipeBackLayoutStyle);
  95. }
  96. public SwipeBackLayout(Context context, AttributeSet attrs, int defStyle) {
  97. super(context, attrs);
  98. mDragHelper = ViewDragHelper.create(this, new ViewDragCallback());
  99. TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SwipeBackLayout, defStyle,
  100. R.style.SwipeBackLayout);
  101. int edgeSize = a.getDimensionPixelSize(R.styleable.SwipeBackLayout_edge_size, -1);
  102. if (edgeSize > 0)
  103. setEdgeSize(edgeSize);
  104. int mode = EDGE_FLAGS[a.getInt(R.styleable.SwipeBackLayout_edge_flag, 0)];
  105. setEdgeTrackingEnabled(mode);
  106. int shadowLeft = a.getResourceId(R.styleable.SwipeBackLayout_shadow_left,
  107. R.drawable.shadow_left);
  108. int shadowRight = a.getResourceId(R.styleable.SwipeBackLayout_shadow_right,
  109. R.drawable.shadow_right);
  110. int shadowBottom = a.getResourceId(R.styleable.SwipeBackLayout_shadow_bottom,
  111. R.drawable.shadow_bottom);
  112. setShadow(shadowLeft, EDGE_LEFT);
  113. setShadow(shadowRight, EDGE_RIGHT);
  114. setShadow(shadowBottom, EDGE_BOTTOM);
  115. a.recycle();
  116. final float density = getResources().getDisplayMetrics().density;
  117. final float minVel = MIN_FLING_VELOCITY * density;
  118. mDragHelper.setMinVelocity(minVel);
  119. mDragHelper.setMaxVelocity(minVel * 2f);
  120. }
  121. /**
  122. * Sets the sensitivity of the NavigationLayout.
  123. *
  124. * @param context The application context.
  125. * @param sensitivity value between 0 and 1, the final value for touchSlop =
  126. * ViewConfiguration.getScaledTouchSlop * (1 / s);
  127. */
  128. public void setSensitivity(Context context, float sensitivity) {
  129. mDragHelper.setSensitivity(context, sensitivity);
  130. }
  131. /**
  132. * Set up contentView which will be moved by user gesture
  133. *
  134. * @param view
  135. */
  136. private void setContentView(View view) {
  137. mContentView = view;
  138. }
  139. public void setEnableGesture(boolean enable) {
  140. mEnable = enable;
  141. }
  142. /**
  143. * Enable edge tracking for the selected edges of the parent view. The
  144. * callback's
  145. * {@link me.imid.swipebacklayout.lib.ViewDragHelper.Callback#onEdgeTouched(int, int)}
  146. * and
  147. * {@link me.imid.swipebacklayout.lib.ViewDragHelper.Callback#onEdgeDragStarted(int, int)}
  148. * methods will only be invoked for edges for which edge tracking has been
  149. * enabled.
  150. *
  151. * @param edgeFlags Combination of edge flags describing the edges to watch
  152. * @see #EDGE_LEFT
  153. * @see #EDGE_RIGHT
  154. * @see #EDGE_BOTTOM
  155. */
  156. public void setEdgeTrackingEnabled(int edgeFlags) {
  157. mEdgeFlag = edgeFlags;
  158. mDragHelper.setEdgeTrackingEnabled(mEdgeFlag);
  159. }
  160. /**
  161. * Set a color to use for the scrim that obscures primary content while a
  162. * drawer is open.
  163. *
  164. * @param color Color to use in 0xAARRGGBB format.
  165. */
  166. public void setScrimColor(int color) {
  167. mScrimColor = color;
  168. invalidate();
  169. }
  170. /**
  171. * Set the size of an edge. This is the range in pixels along the edges of
  172. * this view that will actively detect edge touches or drags if edge
  173. * tracking is enabled.
  174. *
  175. * @param size The size of an edge in pixels
  176. */
  177. public void setEdgeSize(int size) {
  178. mDragHelper.setEdgeSize(size);
  179. }
  180. /**
  181. * Register a callback to be invoked when a swipe event is sent to this
  182. * view.
  183. *
  184. * @param listener the swipe listener to attach to this view
  185. * @deprecated use {@link #addSwipeListener} instead
  186. */
  187. @Deprecated
  188. public void setSwipeListener(SwipeListener listener) {
  189. addSwipeListener(listener);
  190. }
  191. /**
  192. * Add a callback to be invoked when a swipe event is sent to this view.
  193. *
  194. * @param listener the swipe listener to attach to this view
  195. */
  196. public void addSwipeListener(SwipeListener listener) {
  197. if (mListeners == null) {
  198. mListeners = new ArrayList<SwipeListener>();
  199. }
  200. mListeners.add(listener);
  201. }
  202. /**
  203. * Removes a listener from the set of listeners
  204. *
  205. * @param listener
  206. */
  207. public void removeSwipeListener(SwipeListener listener) {
  208. if (mListeners == null) {
  209. return;
  210. }
  211. mListeners.remove(listener);
  212. }
  213. public static interface SwipeListener {
  214. /**
  215. * Invoke when state change
  216. *
  217. * @param state flag to describe scroll state
  218. * @param scrollPercent scroll percent of this view
  219. * @see #STATE_IDLE
  220. * @see #STATE_DRAGGING
  221. * @see #STATE_SETTLING
  222. */
  223. public void onScrollStateChange(int state, float scrollPercent);
  224. /**
  225. * Invoke when edge touched
  226. *
  227. * @param edgeFlag edge flag describing the edge being touched
  228. * @see #EDGE_LEFT
  229. * @see #EDGE_RIGHT
  230. * @see #EDGE_BOTTOM
  231. */
  232. public void onEdgeTouch(int edgeFlag);
  233. /**
  234. * Invoke when scroll percent over the threshold for the first time
  235. */
  236. public void onScrollOverThreshold();
  237. }
  238. /**
  239. * Set scroll threshold, we will close the activity, when scrollPercent over
  240. * this value
  241. *
  242. * @param threshold
  243. */
  244. public void setScrollThresHold(float threshold) {
  245. if (threshold >= 1.0f || threshold <= 0) {
  246. throw new IllegalArgumentException("Threshold value should be between 0 and 1.0");
  247. }
  248. mScrollThreshold = threshold;
  249. }
  250. /**
  251. * Set a drawable used for edge shadow.
  252. *
  253. * @param shadow Drawable to use
  254. * @param edgeFlags Combination of edge flags describing the edge to set
  255. * @see #EDGE_LEFT
  256. * @see #EDGE_RIGHT
  257. * @see #EDGE_BOTTOM
  258. */
  259. public void setShadow(Drawable shadow, int edgeFlag) {
  260. if ((edgeFlag & EDGE_LEFT) != 0) {
  261. mShadowLeft = shadow;
  262. } else if ((edgeFlag & EDGE_RIGHT) != 0) {
  263. mShadowRight = shadow;
  264. } else if ((edgeFlag & EDGE_BOTTOM) != 0) {
  265. mShadowBottom = shadow;
  266. }
  267. invalidate();
  268. }
  269. /**
  270. * Set a drawable used for edge shadow.
  271. *
  272. * @param resId Resource of drawable to use
  273. * @param edgeFlags Combination of edge flags describing the edge to set
  274. * @see #EDGE_LEFT
  275. * @see #EDGE_RIGHT
  276. * @see #EDGE_BOTTOM
  277. */
  278. public void setShadow(int resId, int edgeFlag) {
  279. setShadow(getResources().getDrawable(resId), edgeFlag);
  280. }
  281. /**
  282. * Scroll out contentView and finish the activity
  283. */
  284. public void scrollToFinishActivity() {
  285. final int childWidth = mContentView.getWidth();
  286. final int childHeight = mContentView.getHeight();
  287. int left = 0, top = 0;
  288. if ((mEdgeFlag & EDGE_LEFT) != 0) {
  289. left = childWidth + mShadowLeft.getIntrinsicWidth() + OVERSCROLL_DISTANCE;
  290. mTrackingEdge = EDGE_LEFT;
  291. } else if ((mEdgeFlag & EDGE_RIGHT) != 0) {
  292. left = -childWidth - mShadowRight.getIntrinsicWidth() - OVERSCROLL_DISTANCE;
  293. mTrackingEdge = EDGE_RIGHT;
  294. } else if ((mEdgeFlag & EDGE_BOTTOM) != 0) {
  295. top = -childHeight - mShadowBottom.getIntrinsicHeight() - OVERSCROLL_DISTANCE;
  296. mTrackingEdge = EDGE_BOTTOM;
  297. }
  298. mDragHelper.smoothSlideViewTo(mContentView, left, top);
  299. invalidate();
  300. }
  301. @Override
  302. public boolean onInterceptTouchEvent(MotionEvent event) {
  303. if (!mEnable) {
  304. return false;
  305. }
  306. try {
  307. return mDragHelper.shouldInterceptTouchEvent(event);
  308. } catch (ArrayIndexOutOfBoundsException e) {
  309. // FIXME: handle exception
  310. // issues #9
  311. return false;
  312. }
  313. }
  314. @Override
  315. public boolean onTouchEvent(MotionEvent event) {
  316. if (!mEnable) {
  317. return false;
  318. }
  319. mDragHelper.processTouchEvent(event);
  320. return true;
  321. }
  322. @Override
  323. protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
  324. mInLayout = true;
  325. if (mContentView != null)
  326. mContentView.layout(mContentLeft, mContentTop,
  327. mContentLeft + mContentView.getMeasuredWidth(),
  328. mContentTop + mContentView.getMeasuredHeight());
  329. mInLayout = false;
  330. }
  331. @Override
  332. public void requestLayout() {
  333. if (!mInLayout) {
  334. super.requestLayout();
  335. }
  336. }
  337. @Override
  338. protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
  339. final boolean drawContent = child == mContentView;
  340. boolean ret = super.drawChild(canvas, child, drawingTime);
  341. if (mScrimOpacity > 0 && drawContent
  342. && mDragHelper.getViewDragState() != ViewDragHelper.STATE_IDLE) {
  343. drawShadow(canvas, child);
  344. drawScrim(canvas, child);
  345. }
  346. return ret;
  347. }
  348. private void drawScrim(Canvas canvas, View child) {
  349. final int baseAlpha = (mScrimColor & 0xff000000) >>> 24;
  350. final int alpha = (int) (baseAlpha * mScrimOpacity);
  351. final int color = alpha << 24 | (mScrimColor & 0xffffff);
  352. if ((mTrackingEdge & EDGE_LEFT) != 0) {
  353. canvas.clipRect(0, 0, child.getLeft(), getHeight());
  354. } else if ((mTrackingEdge & EDGE_RIGHT) != 0) {
  355. canvas.clipRect(child.getRight(), 0, getRight(), getHeight());
  356. } else if ((mTrackingEdge & EDGE_BOTTOM) != 0) {
  357. canvas.clipRect(child.getLeft(), child.getBottom(), getRight(), getHeight());
  358. }
  359. canvas.drawColor(color);
  360. }
  361. private void drawShadow(Canvas canvas, View child) {
  362. final Rect childRect = mTmpRect;
  363. child.getHitRect(childRect);
  364. if ((mEdgeFlag & EDGE_LEFT) != 0) {
  365. mShadowLeft.setBounds(childRect.left - mShadowLeft.getIntrinsicWidth(), childRect.top,
  366. childRect.left, childRect.bottom);
  367. mShadowLeft.setAlpha((int) (mScrimOpacity * FULL_ALPHA));
  368. mShadowLeft.draw(canvas);
  369. }
  370. if ((mEdgeFlag & EDGE_RIGHT) != 0) {
  371. mShadowRight.setBounds(childRect.right, childRect.top,
  372. childRect.right + mShadowRight.getIntrinsicWidth(), childRect.bottom);
  373. mShadowRight.setAlpha((int) (mScrimOpacity * FULL_ALPHA));
  374. mShadowRight.draw(canvas);
  375. }
  376. if ((mEdgeFlag & EDGE_BOTTOM) != 0) {
  377. mShadowBottom.setBounds(childRect.left, childRect.bottom, childRect.right,
  378. childRect.bottom + mShadowBottom.getIntrinsicHeight());
  379. mShadowBottom.setAlpha((int) (mScrimOpacity * FULL_ALPHA));
  380. mShadowBottom.draw(canvas);
  381. }
  382. }
  383. public void attachToActivity(Activity activity) {
  384. mActivity = activity;
  385. TypedArray a = activity.getTheme().obtainStyledAttributes(new int[]{
  386. android.R.attr.windowBackground
  387. });
  388. int background = a.getResourceId(0, 0);
  389. a.recycle();
  390. ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView();
  391. ViewGroup decorChild = (ViewGroup) decor.getChildAt(0);
  392. decorChild.setBackgroundResource(background);
  393. decor.removeView(decorChild);
  394. addView(decorChild);
  395. setContentView(decorChild);
  396. decor.addView(this);
  397. }
  398. @Override
  399. public void computeScroll() {
  400. mScrimOpacity = 1 - mScrollPercent;
  401. if (mDragHelper.continueSettling(true)) {
  402. ViewCompat.postInvalidateOnAnimation(this);
  403. }
  404. }
  405. private class ViewDragCallback extends ViewDragHelper.Callback {
  406. private boolean mIsScrollOverValid;
  407. @Override
  408. public boolean tryCaptureView(View view, int i) {
  409. boolean ret = mDragHelper.isEdgeTouched(mEdgeFlag, i);
  410. if (ret) {
  411. if (mDragHelper.isEdgeTouched(EDGE_LEFT, i)) {
  412. mTrackingEdge = EDGE_LEFT;
  413. } else if (mDragHelper.isEdgeTouched(EDGE_RIGHT, i)) {
  414. mTrackingEdge = EDGE_RIGHT;
  415. } else if (mDragHelper.isEdgeTouched(EDGE_BOTTOM, i)) {
  416. mTrackingEdge = EDGE_BOTTOM;
  417. }
  418. if (mListeners != null && !mListeners.isEmpty()) {
  419. for (SwipeListener listener : mListeners) {
  420. listener.onEdgeTouch(mTrackingEdge);
  421. }
  422. }
  423. mIsScrollOverValid = true;
  424. }
  425. boolean directionCheck = false;
  426. if (mEdgeFlag == EDGE_LEFT || mEdgeFlag == EDGE_RIGHT) {
  427. directionCheck = !mDragHelper.checkTouchSlop(ViewDragHelper.DIRECTION_VERTICAL, i);
  428. } else if (mEdgeFlag == EDGE_BOTTOM) {
  429. directionCheck = !mDragHelper
  430. .checkTouchSlop(ViewDragHelper.DIRECTION_HORIZONTAL, i);
  431. } else if (mEdgeFlag == EDGE_ALL) {
  432. directionCheck = true;
  433. }
  434. return ret & directionCheck;
  435. }
  436. @Override
  437. public int getViewHorizontalDragRange(View child) {
  438. return mEdgeFlag & (EDGE_LEFT | EDGE_RIGHT);
  439. }
  440. @Override
  441. public int getViewVerticalDragRange(View child) {
  442. return mEdgeFlag & EDGE_BOTTOM;
  443. }
  444. @Override
  445. public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
  446. super.onViewPositionChanged(changedView, left, top, dx, dy);
  447. if ((mTrackingEdge & EDGE_LEFT) != 0) {
  448. mScrollPercent = Math.abs((float) left
  449. / (mContentView.getWidth() + mShadowLeft.getIntrinsicWidth()));
  450. } else if ((mTrackingEdge & EDGE_RIGHT) != 0) {
  451. mScrollPercent = Math.abs((float) left
  452. / (mContentView.getWidth() + mShadowRight.getIntrinsicWidth()));
  453. } else if ((mTrackingEdge & EDGE_BOTTOM) != 0) {
  454. mScrollPercent = Math.abs((float) top
  455. / (mContentView.getHeight() + mShadowBottom.getIntrinsicHeight()));
  456. }
  457. mContentLeft = left;
  458. mContentTop = top;
  459. invalidate();
  460. if (mScrollPercent < mScrollThreshold && !mIsScrollOverValid) {
  461. mIsScrollOverValid = true;
  462. }
  463. if (mListeners != null && !mListeners.isEmpty()
  464. && mDragHelper.getViewDragState() == STATE_DRAGGING
  465. && mScrollPercent >= mScrollThreshold && mIsScrollOverValid) {
  466. mIsScrollOverValid = false;
  467. for (SwipeListener listener : mListeners) {
  468. listener.onScrollOverThreshold();
  469. }
  470. }
  471. if (mScrollPercent >= 1) {
  472. if (!mActivity.isFinishing()) {
  473. mActivity.finish();
  474. mActivity.overridePendingTransition(0, 0);
  475. }
  476. }
  477. }
  478. @Override
  479. public void onViewReleased(View releasedChild, float xvel, float yvel) {
  480. final int childWidth = releasedChild.getWidth();
  481. final int childHeight = releasedChild.getHeight();
  482. int left = 0, top = 0;
  483. if ((mTrackingEdge & EDGE_LEFT) != 0) {
  484. left = xvel > 0 || xvel == 0 && mScrollPercent > mScrollThreshold ? childWidth
  485. + mShadowLeft.getIntrinsicWidth() + OVERSCROLL_DISTANCE : 0;
  486. } else if ((mTrackingEdge & EDGE_RIGHT) != 0) {
  487. left = xvel < 0 || xvel == 0 && mScrollPercent > mScrollThreshold ? -(childWidth
  488. + mShadowLeft.getIntrinsicWidth() + OVERSCROLL_DISTANCE) : 0;
  489. } else if ((mTrackingEdge & EDGE_BOTTOM) != 0) {
  490. top = yvel < 0 || yvel == 0 && mScrollPercent > mScrollThreshold ? -(childHeight
  491. + mShadowBottom.getIntrinsicHeight() + OVERSCROLL_DISTANCE) : 0;
  492. }
  493. mDragHelper.settleCapturedViewAt(left, top);
  494. invalidate();
  495. }
  496. @Override
  497. public int clampViewPositionHorizontal(View child, int left, int dx) {
  498. int ret = 0;
  499. if ((mTrackingEdge & EDGE_LEFT) != 0) {
  500. ret = Math.min(child.getWidth(), Math.max(left, 0));
  501. } else if ((mTrackingEdge & EDGE_RIGHT) != 0) {
  502. ret = Math.min(0, Math.max(left, -child.getWidth()));
  503. }
  504. return ret;
  505. }
  506. @Override
  507. public int clampViewPositionVertical(View child, int top, int dy) {
  508. int ret = 0;
  509. if ((mTrackingEdge & EDGE_BOTTOM) != 0) {
  510. ret = Math.min(0, Math.max(top, -child.getHeight()));
  511. }
  512. return ret;
  513. }
  514. @Override
  515. public void onViewDragStateChanged(int state) {
  516. super.onViewDragStateChanged(state);
  517. if (mListeners != null && !mListeners.isEmpty()) {
  518. for (SwipeListener listener : mListeners) {
  519. listener.onScrollStateChange(state, mScrollPercent);
  520. }
  521. }
  522. }
  523. }
  524. }