[关闭]
@myron-lee 2017-03-22T12:26:56.000000Z 字数 12477 阅读 1989

可滑动关闭的对话框(二)

Blog


前言

继续完善,希望这个控件可以变成轮子被更多的人使用。

效果图

效果图

改进

  1. 改变使用方式,现在可以直接继承SwipeDialog使用,更加方便。并且和系统Dialog特性保持一致,比如设置Dialog的显示、取消、关闭监听器,设置可取消、可点击窗口以外区域取消等等。
  2. 增强适用性,SwipeDialog的布局支持ListView、ScrollView、设置了MovementMethod的长TextView等可滑动的视图,并与Dialog本身的滑动不冲突。解决办法是,当判定出用户有滑动行为的时候,去判断ListView、ScrollView、长TextView等可滑动的视图是不是存在并可滑动,如果是则把整个手势交给它们处理,如果不是则截获整个手势。
  3. 优化体验,解决在我只是想点击Dialog上面某个按钮的时候,Dialog也出现了移动的问题。解决办法很简单,只有在用户手指移动距离超过touchSlop的时候,我才移动Dialog,并且将此时的Y坐标作为downY。

TODO

  1. 解决调用高版本API问题,比如public void setTranslationY (float translationY) (Added in API level 11)

Demo

Github

源码

  1. public class SwipeDialog extends Dialog {
  2. private DialogContainer container;
  3. private boolean cancel;
  4. public SwipeDialog(Context context) {
  5. super(context);
  6. init();
  7. }
  8. public SwipeDialog(Context context, int theme) {
  9. super(context, theme);
  10. init();
  11. }
  12. protected SwipeDialog(Context context, boolean cancelable, OnCancelListener cancelListener) {
  13. super(context, cancelable, cancelListener);
  14. init();
  15. }
  16. private void init() {
  17. final Window window = getWindow();
  18. window.requestFeature(Window.FEATURE_NO_TITLE);
  19. window.addFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
  20. getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
  21. window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
  22. window.getAttributes().windowAnimations = 0;
  23. container = new DialogContainer(getContext());
  24. container.setSwipeListener(new DialogContainer.SwipeListener() {
  25. @Override
  26. public void onFallIn() {
  27. }
  28. @Override
  29. public void onRiseOut() {
  30. if (cancel) {
  31. SwipeDialog.super.cancel();
  32. cancel = false;
  33. } else {
  34. SwipeDialog.super.dismiss();
  35. }
  36. }
  37. @Override
  38. public void onFallOut() {
  39. SwipeDialog.super.dismiss();
  40. }
  41. @Override
  42. public void onRecover() {
  43. }
  44. @Override
  45. public void onTouchOutside() {
  46. cancel = true;
  47. }
  48. });
  49. }
  50. @Override
  51. public void setContentView(int layoutResID) {
  52. View dialogView = LayoutInflater.from(getContext()).inflate(layoutResID, container, false);
  53. // container.setDialogView(dialogView);
  54. // ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(getWidth(), getHeight());
  55. // super.setContentView(container, layoutParams);
  56. setContentView(dialogView, null);
  57. }
  58. @Override
  59. public void setContentView(View view) {
  60. setContentView(view, null);
  61. }
  62. @Override
  63. public void setContentView(View view, ViewGroup.LayoutParams params) {
  64. container.removeAllViews();
  65. container.addDialogView(view);
  66. ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(getWidth(), getHeight());
  67. super.setContentView(container, layoutParams);
  68. }
  69. //
  70. // /**
  71. // * Sets whether this dialog is canceled when touched outside the window's
  72. // * bounds. If setting to true, the dialog is set to be cancelable if not
  73. // * already set.
  74. // *
  75. // * @param cancel Whether the dialog should be canceled when touched outside
  76. // * the window.
  77. // */
  78. // public void setCanceledOnTouchOutside(boolean cancel) {
  79. // if (cancel && !mCancelable) {
  80. // mCancelable = true;
  81. // }
  82. //
  83. // mWindow.setCloseOnTouchOutside(cancel);
  84. // }
  85. //
  86. @Override
  87. public void setCanceledOnTouchOutside(boolean cancel) {
  88. super.setCanceledOnTouchOutside(cancel);
  89. container.setCanceledOnTouchOutside(cancel);
  90. }
  91. @Override
  92. public void show() {
  93. super.show();
  94. container.show();
  95. }
  96. @Override
  97. public void dismiss() {
  98. container.riseOut();
  99. }
  100. @Override
  101. public void cancel() {
  102. cancel = true;
  103. container.riseOut();
  104. }
  105. public void setChangeDimEnabled(boolean changeDimEnabled) {
  106. container.setChangeDimEnabled(changeDimEnabled);
  107. }
  108. public int getWidth() {
  109. return getContext().getResources().getDisplayMetrics().widthPixels;
  110. }
  111. public int getHeight() {
  112. return getContext().getResources().getDisplayMetrics().heightPixels - getStatusBarHeight();
  113. }
  114. public int getStatusBarHeight() {
  115. int result = 0;
  116. int resourceId = getContext().getResources().getIdentifier("status_bar_height", "dimen", "android");
  117. if (resourceId > 0) {
  118. result = getContext().getResources().getDimensionPixelSize(resourceId);
  119. }
  120. return result;
  121. }
  122. }
  1. public class DialogContainer extends FrameLayout {
  2. private boolean handleByChild;
  3. private View targetView;
  4. private enum AnimateType {FALL_IN, RECOVER, RISE_OUT, FALL_OUT}
  5. private static final float MAX_DIM = 0.5F;
  6. private float currDim;
  7. private boolean changeDimEnabled = true;
  8. private boolean canceledOnTouchOutside;
  9. private boolean touchOutside;
  10. private View dialogView;
  11. private float downY;
  12. private float lastY;
  13. private int translationYTopBoundary;
  14. private int translationYBottomBoundary;
  15. private int translationYMax;
  16. private boolean animating;
  17. private boolean handleBySelf;
  18. private int touchSlop;
  19. private int flingVelocityThreshold;
  20. private VelocityTracker velocityTracker;
  21. private SwipeListener swipeListener;
  22. public DialogContainer(Context context) {
  23. super(context);
  24. init();
  25. }
  26. public DialogContainer(Context context, AttributeSet attrs) {
  27. super(context, attrs);
  28. init();
  29. }
  30. public DialogContainer(Context context, AttributeSet attrs, int defStyleAttr) {
  31. super(context, attrs, defStyleAttr);
  32. init();
  33. }
  34. private void init() {
  35. ViewConfiguration viewConfiguration = ViewConfiguration.get(getContext());
  36. int minFlingVelocity = viewConfiguration.getScaledMinimumFlingVelocity();
  37. int maxFlingVelocity = viewConfiguration.getScaledMaximumFlingVelocity();
  38. flingVelocityThreshold = (minFlingVelocity + maxFlingVelocity) / 5;
  39. touchSlop = viewConfiguration.getScaledTouchSlop();
  40. setVisibility(INVISIBLE);
  41. }
  42. public void addDialogView(View dialogView) {
  43. this.dialogView = dialogView;
  44. FrameLayout.LayoutParams params = (LayoutParams) dialogView.getLayoutParams();
  45. if (params == null) {
  46. params = generateDefaultLayoutParams();
  47. }
  48. if (params.width == LayoutParams.MATCH_PARENT) {
  49. params.width = LayoutParams.WRAP_CONTENT;
  50. }
  51. if (params.height == LayoutParams.MATCH_PARENT) {
  52. params.height = LayoutParams.WRAP_CONTENT;
  53. }
  54. params.gravity = Gravity.CENTER;
  55. addView(dialogView, params);
  56. }
  57. public void show() {
  58. getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
  59. @Override
  60. public boolean onPreDraw() {
  61. translationYTopBoundary = -(DialogContainer.this.dialogView.getTop() + DialogContainer.this.dialogView.getBottom()) / 2;
  62. translationYBottomBoundary = -translationYTopBoundary;
  63. translationYMax = DialogContainer.this.dialogView.getBottom();
  64. dialogView.setTranslationY(-dialogView.getBottom());
  65. if (!changeDimEnabled) {
  66. setDim(MAX_DIM);
  67. }
  68. setVisibility(VISIBLE);
  69. fallIn();
  70. getViewTreeObserver().removeOnPreDrawListener(this);
  71. return true;
  72. }
  73. });
  74. invalidate();
  75. }
  76. @Override
  77. protected void onAttachedToWindow() {
  78. super.onAttachedToWindow();
  79. velocityTracker = VelocityTracker.obtain();
  80. }
  81. @Override
  82. protected void onDetachedFromWindow() {
  83. super.onDetachedFromWindow();
  84. velocityTracker.clear();
  85. }
  86. @Override
  87. public boolean dispatchTouchEvent(MotionEvent ev) {
  88. if (animating) {
  89. return false;
  90. }
  91. switch (ev.getAction()) {
  92. case MotionEvent.ACTION_DOWN:
  93. return onDown(ev);
  94. case MotionEvent.ACTION_MOVE:
  95. return onMove(ev);
  96. case MotionEvent.ACTION_CANCEL:
  97. case MotionEvent.ACTION_UP:
  98. return onEnd(ev);
  99. }
  100. return super.dispatchTouchEvent(ev);
  101. }
  102. private boolean onDown(MotionEvent ev) {
  103. velocityTracker.clear();
  104. velocityTracker.addMovement(ev);
  105. Rect dialogHitRect = new Rect();
  106. dialogView.getHitRect(dialogHitRect);
  107. touchOutside = !dialogHitRect.contains(((int) ev.getX()), ((int) ev.getY()));
  108. targetView = getTargetView(this, MotionEvent.obtain(ev));
  109. downY = ev.getY();
  110. lastY = ev.getY();
  111. super.dispatchTouchEvent(ev);
  112. return true;
  113. }
  114. private boolean onMove(MotionEvent ev) {
  115. velocityTracker.addMovement(ev);
  116. if (downY == -1) {
  117. downY = ev.getY();
  118. }
  119. if (lastY == -1) {
  120. lastY = ev.getY();
  121. }
  122. final float dy = ev.getY() - downY;
  123. if (handleBySelf) {
  124. dialogView.setTranslationY(dy);
  125. if (changeDimEnabled) {
  126. float newDim = MAX_DIM * (1 - Math.min(1, Math.abs(dy / translationYMax)));
  127. setDim(newDim);
  128. }
  129. return true;
  130. }
  131. if (handleByChild) {
  132. super.dispatchTouchEvent(ev);
  133. return true;
  134. }
  135. if (Math.abs(dy) >= touchSlop) {
  136. if (touchOutside || !targetViewCanScroll(ev.getY() - lastY)) {
  137. handleBySelf = true;
  138. handleByChild = false;
  139. downY = ev.getY();
  140. dispatchCancelEventToChild(ev);
  141. return true;
  142. } else {
  143. handleByChild = true;
  144. handleBySelf = false;
  145. super.dispatchTouchEvent(ev);
  146. return true;
  147. }
  148. }
  149. lastY = ev.getY();
  150. super.dispatchTouchEvent(ev);
  151. return true;
  152. }
  153. private boolean onEnd(MotionEvent ev) {
  154. velocityTracker.addMovement(ev);
  155. velocityTracker.computeCurrentVelocity(1000);
  156. float velocityY = velocityTracker.getYVelocity();
  157. if (handleBySelf) {
  158. if (Math.abs(velocityY) < flingVelocityThreshold) {
  159. if (dialogView.getTranslationY() < translationYTopBoundary) {//[-infinite, -dialogTop)
  160. riseOut();
  161. } else if (dialogView.getTranslationY() < translationYBottomBoundary) {//[-dialog, bottom)
  162. recover();
  163. } else {
  164. fallOut();
  165. }
  166. } else {
  167. if (velocityY < 0) {
  168. riseOut();
  169. } else {
  170. fallOut();
  171. }
  172. }
  173. }
  174. reset();
  175. return super.dispatchTouchEvent(ev);
  176. }
  177. private void reset() {
  178. downY = -1;
  179. lastY = -1;
  180. handleBySelf = false;
  181. handleByChild = false;
  182. }
  183. private boolean targetViewCanScroll(float dy) {
  184. if (targetView instanceof AdapterView<?>) {
  185. AdapterView<?> adapterView = (AdapterView<?>) targetView;
  186. if (adapterView.getCount() == 0
  187. || (adapterView.getLastVisiblePosition() == adapterView.getCount() - 1
  188. && adapterView.getChildAt(adapterView.getChildCount() - 1).getBottom() <= adapterView.getHeight()
  189. && dy < 0)
  190. || (adapterView.getFirstVisiblePosition() == 0
  191. && adapterView.getChildAt(0).getTop() >= 0)
  192. && dy > 0) {
  193. return false;
  194. } else {
  195. return true;
  196. }
  197. }
  198. if ((targetView instanceof ScrollView)
  199. && ((ScrollView) targetView).getChildCount() > 0
  200. && ((ScrollView) targetView).getChildAt(0).getMeasuredHeight() > targetView.getHeight()
  201. && !((targetView.getScrollY() <= 0 && dy > 0) || (targetView.getScrollY() >= ((ScrollView) targetView).getChildAt(0).getMeasuredHeight() - targetView.getHeight() && dy < 0))) {
  202. return true;
  203. }
  204. if (targetView instanceof TextView
  205. && ((TextView) targetView).getMovementMethod() != null
  206. && ((TextView) targetView).getLayout().getHeight() > targetView.getHeight()
  207. && !((targetView.getScrollY() <= 0 && dy > 0) || (targetView.getScrollY() >= ((TextView) targetView).getLayout().getHeight() - targetView.getHeight() && dy < 0))) {
  208. return true;
  209. }
  210. return false;
  211. }
  212. private View getTargetView(View view, MotionEvent event) {
  213. if (view instanceof AdapterView<?> || view instanceof ScrollView) {
  214. event.recycle();
  215. return view;
  216. } else if (view instanceof ViewGroup) {
  217. event.offsetLocation(-view.getLeft(), -view.getTop());//do not swap them, tears
  218. ViewGroup viewGroup = (ViewGroup) view;
  219. for (int i = 0; i < viewGroup.getChildCount(); i++) {
  220. View child = viewGroup.getChildAt(i);
  221. Rect childHitRect = new Rect();
  222. child.getHitRect(childHitRect);
  223. if (childHitRect.contains(((int) event.getX()), ((int) event.getY()))) {
  224. // event.offsetLocation(-child.getTop(), -child.getLeft());
  225. // event.transform(child.getInverseMatrix());
  226. return getTargetView(child, event);
  227. }
  228. }
  229. event.recycle();
  230. return view;
  231. } else {
  232. event.recycle();
  233. return view;
  234. }
  235. }
  236. private void dispatchDownEventToChild(MotionEvent ev) {
  237. MotionEvent cancelEvent = MotionEvent.obtain(ev);
  238. cancelEvent.setAction(MotionEvent.ACTION_DOWN);
  239. super.dispatchTouchEvent(cancelEvent);
  240. cancelEvent.recycle();
  241. }
  242. private void dispatchCancelEventToChild(MotionEvent ev) {
  243. MotionEvent cancelEvent = MotionEvent.obtain(ev);
  244. cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
  245. super.dispatchTouchEvent(cancelEvent);
  246. cancelEvent.recycle();
  247. }
  248. public void fallIn() {
  249. animate(0, MAX_DIM, AnimateType.FALL_IN);
  250. }
  251. private void recover() {
  252. animate(0, MAX_DIM, AnimateType.RECOVER);
  253. }
  254. public void riseOut() {
  255. animate(-dialogView.getBottom(), 0, AnimateType.RISE_OUT);
  256. }
  257. private void fallOut() {
  258. animate(getHeight() - dialogView.getTop(), 0, AnimateType.FALL_OUT);
  259. }
  260. private void animate(float endTranslationY, float endDim, final AnimateType animateType) {
  261. float translationYDelta = endTranslationY - dialogView.getTranslationY();
  262. long duration = Math.min(Math.max((long) (Math.abs(translationYDelta) * 0.7F), 400), 600);
  263. final float startDim = currDim;
  264. final float dimDelta = endDim - startDim;
  265. ValueAnimator animator = ValueAnimator.ofFloat(dialogView.getTranslationY(), endTranslationY);
  266. animator.setDuration(duration);
  267. animator.setInterpolator(new DecelerateInterpolator(1.6F));
  268. animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
  269. @Override
  270. public void onAnimationUpdate(ValueAnimator valueAnimator) {
  271. dialogView.setTranslationY((Float) valueAnimator.getAnimatedValue());
  272. if (changeDimEnabled) {
  273. setDim(startDim + dimDelta * valueAnimator.getAnimatedFraction());
  274. }
  275. }
  276. });
  277. animator.addListener(new Animator.AnimatorListener() {
  278. @Override
  279. public void onAnimationStart(Animator animator) {
  280. animating = true;
  281. }
  282. @Override
  283. public void onAnimationEnd(Animator animator) {
  284. animating = false;
  285. if (swipeListener != null) {
  286. switch (animateType) {
  287. case FALL_IN:
  288. swipeListener.onFallIn();
  289. break;
  290. case RECOVER:
  291. swipeListener.onRecover();
  292. break;
  293. case RISE_OUT:
  294. swipeListener.onRiseOut();
  295. break;
  296. case FALL_OUT:
  297. swipeListener.onFallOut();
  298. break;
  299. }
  300. }
  301. }
  302. @Override
  303. public void onAnimationCancel(Animator animator) {
  304. }
  305. @Override
  306. public void onAnimationRepeat(Animator animator) {
  307. }
  308. });
  309. animator.start();
  310. }
  311. private void setDim(float dim) {
  312. setBackgroundColor(Color.argb((int) (255 * dim), 0, 0, 0));
  313. currDim = dim;
  314. }
  315. public void setDialogView(View dialogView) {
  316. this.dialogView = dialogView;
  317. }
  318. public void setSwipeListener(SwipeListener swipeListener) {
  319. this.swipeListener = swipeListener;
  320. }
  321. public interface SwipeListener {
  322. void onFallIn();
  323. void onRiseOut();
  324. void onFallOut();
  325. void onRecover();
  326. void onTouchOutside();
  327. }
  328. public void setChangeDimEnabled(boolean changeDimEnabled) {
  329. this.changeDimEnabled = changeDimEnabled;
  330. }
  331. public void setCanceledOnTouchOutside(boolean cancel) {
  332. canceledOnTouchOutside = cancel;
  333. if (canceledOnTouchOutside) {
  334. setOnClickListener(new OnClickListener() {
  335. @Override
  336. public void onClick(View v) {
  337. if (touchOutside) {
  338. if (swipeListener != null) {
  339. swipeListener.onTouchOutside();
  340. }
  341. riseOut();
  342. }
  343. }
  344. });
  345. }
  346. }
  347. }
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注