前言
继续完善,希望这个控件可以变成轮子被更多的人使用。
效果图
改进
- 改变使用方式,现在可以直接继承SwipeDialog使用,更加方便。并且和系统Dialog特性保持一致,比如设置Dialog的显示、取消、关闭监听器,设置可取消、可点击窗口以外区域取消等等。
- 增强适用性,SwipeDialog的布局支持ListView、ScrollView、设置了MovementMethod的长TextView等可滑动的视图,并与Dialog本身的滑动不冲突。解决办法是,当判定出用户有滑动行为的时候,去判断ListView、ScrollView、长TextView等可滑动的视图是不是存在并可滑动,如果是则把整个手势交给它们处理,如果不是则截获整个手势。
- 优化体验,解决在我只是想点击Dialog上面某个按钮的时候,Dialog也出现了移动的问题。解决办法很简单,只有在用户手指移动距离超过touchSlop的时候,我才移动Dialog,并且将此时的Y坐标作为downY。
TODO
- 解决调用高版本API问题,比如public void setTranslationY (float translationY) (Added in API level 11)
Demo
源码
public class SwipeDialog extends Dialog {
private DialogContainer container;
private boolean cancel;
public SwipeDialog(Context context) {
super(context);
init();
}
public SwipeDialog(Context context, int theme) {
super(context, theme);
init();
}
protected SwipeDialog(Context context, boolean cancelable, OnCancelListener cancelListener) {
super(context, cancelable, cancelListener);
init();
}
private void init() {
final Window window = getWindow();
window.requestFeature(Window.FEATURE_NO_TITLE);
window.addFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
window.getAttributes().windowAnimations = 0;
container = new DialogContainer(getContext());
container.setSwipeListener(new DialogContainer.SwipeListener() {
@Override
public void onFallIn() {
}
@Override
public void onRiseOut() {
if (cancel) {
SwipeDialog.super.cancel();
cancel = false;
} else {
SwipeDialog.super.dismiss();
}
}
@Override
public void onFallOut() {
SwipeDialog.super.dismiss();
}
@Override
public void onRecover() {
}
@Override
public void onTouchOutside() {
cancel = true;
}
});
}
@Override
public void setContentView(int layoutResID) {
View dialogView = LayoutInflater.from(getContext()).inflate(layoutResID, container, false);
// container.setDialogView(dialogView);
// ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(getWidth(), getHeight());
// super.setContentView(container, layoutParams);
setContentView(dialogView, null);
}
@Override
public void setContentView(View view) {
setContentView(view, null);
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
container.removeAllViews();
container.addDialogView(view);
ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(getWidth(), getHeight());
super.setContentView(container, layoutParams);
}
//
// /**
// * Sets whether this dialog is canceled when touched outside the window's
// * bounds. If setting to true, the dialog is set to be cancelable if not
// * already set.
// *
// * @param cancel Whether the dialog should be canceled when touched outside
// * the window.
// */
// public void setCanceledOnTouchOutside(boolean cancel) {
// if (cancel && !mCancelable) {
// mCancelable = true;
// }
//
// mWindow.setCloseOnTouchOutside(cancel);
// }
//
@Override
public void setCanceledOnTouchOutside(boolean cancel) {
super.setCanceledOnTouchOutside(cancel);
container.setCanceledOnTouchOutside(cancel);
}
@Override
public void show() {
super.show();
container.show();
}
@Override
public void dismiss() {
container.riseOut();
}
@Override
public void cancel() {
cancel = true;
container.riseOut();
}
public void setChangeDimEnabled(boolean changeDimEnabled) {
container.setChangeDimEnabled(changeDimEnabled);
}
public int getWidth() {
return getContext().getResources().getDisplayMetrics().widthPixels;
}
public int getHeight() {
return getContext().getResources().getDisplayMetrics().heightPixels - getStatusBarHeight();
}
public int getStatusBarHeight() {
int result = 0;
int resourceId = getContext().getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
result = getContext().getResources().getDimensionPixelSize(resourceId);
}
return result;
}
}
public class DialogContainer extends FrameLayout {
private boolean handleByChild;
private View targetView;
private enum AnimateType {FALL_IN, RECOVER, RISE_OUT, FALL_OUT}
private static final float MAX_DIM = 0.5F;
private float currDim;
private boolean changeDimEnabled = true;
private boolean canceledOnTouchOutside;
private boolean touchOutside;
private View dialogView;
private float downY;
private float lastY;
private int translationYTopBoundary;
private int translationYBottomBoundary;
private int translationYMax;
private boolean animating;
private boolean handleBySelf;
private int touchSlop;
private int flingVelocityThreshold;
private VelocityTracker velocityTracker;
private SwipeListener swipeListener;
public DialogContainer(Context context) {
super(context);
init();
}
public DialogContainer(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public DialogContainer(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
ViewConfiguration viewConfiguration = ViewConfiguration.get(getContext());
int minFlingVelocity = viewConfiguration.getScaledMinimumFlingVelocity();
int maxFlingVelocity = viewConfiguration.getScaledMaximumFlingVelocity();
flingVelocityThreshold = (minFlingVelocity + maxFlingVelocity) / 5;
touchSlop = viewConfiguration.getScaledTouchSlop();
setVisibility(INVISIBLE);
}
public void addDialogView(View dialogView) {
this.dialogView = dialogView;
FrameLayout.LayoutParams params = (LayoutParams) dialogView.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams();
}
if (params.width == LayoutParams.MATCH_PARENT) {
params.width = LayoutParams.WRAP_CONTENT;
}
if (params.height == LayoutParams.MATCH_PARENT) {
params.height = LayoutParams.WRAP_CONTENT;
}
params.gravity = Gravity.CENTER;
addView(dialogView, params);
}
public void show() {
getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
translationYTopBoundary = -(DialogContainer.this.dialogView.getTop() + DialogContainer.this.dialogView.getBottom()) / 2;
translationYBottomBoundary = -translationYTopBoundary;
translationYMax = DialogContainer.this.dialogView.getBottom();
dialogView.setTranslationY(-dialogView.getBottom());
if (!changeDimEnabled) {
setDim(MAX_DIM);
}
setVisibility(VISIBLE);
fallIn();
getViewTreeObserver().removeOnPreDrawListener(this);
return true;
}
});
invalidate();
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
velocityTracker = VelocityTracker.obtain();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
velocityTracker.clear();
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (animating) {
return false;
}
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
return onDown(ev);
case MotionEvent.ACTION_MOVE:
return onMove(ev);
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
return onEnd(ev);
}
return super.dispatchTouchEvent(ev);
}
private boolean onDown(MotionEvent ev) {
velocityTracker.clear();
velocityTracker.addMovement(ev);
Rect dialogHitRect = new Rect();
dialogView.getHitRect(dialogHitRect);
touchOutside = !dialogHitRect.contains(((int) ev.getX()), ((int) ev.getY()));
targetView = getTargetView(this, MotionEvent.obtain(ev));
downY = ev.getY();
lastY = ev.getY();
super.dispatchTouchEvent(ev);
return true;
}
private boolean onMove(MotionEvent ev) {
velocityTracker.addMovement(ev);
if (downY == -1) {
downY = ev.getY();
}
if (lastY == -1) {
lastY = ev.getY();
}
final float dy = ev.getY() - downY;
if (handleBySelf) {
dialogView.setTranslationY(dy);
if (changeDimEnabled) {
float newDim = MAX_DIM * (1 - Math.min(1, Math.abs(dy / translationYMax)));
setDim(newDim);
}
return true;
}
if (handleByChild) {
super.dispatchTouchEvent(ev);
return true;
}
if (Math.abs(dy) >= touchSlop) {
if (touchOutside || !targetViewCanScroll(ev.getY() - lastY)) {
handleBySelf = true;
handleByChild = false;
downY = ev.getY();
dispatchCancelEventToChild(ev);
return true;
} else {
handleByChild = true;
handleBySelf = false;
super.dispatchTouchEvent(ev);
return true;
}
}
lastY = ev.getY();
super.dispatchTouchEvent(ev);
return true;
}
private boolean onEnd(MotionEvent ev) {
velocityTracker.addMovement(ev);
velocityTracker.computeCurrentVelocity(1000);
float velocityY = velocityTracker.getYVelocity();
if (handleBySelf) {
if (Math.abs(velocityY) < flingVelocityThreshold) {
if (dialogView.getTranslationY() < translationYTopBoundary) {//[-infinite, -dialogTop)
riseOut();
} else if (dialogView.getTranslationY() < translationYBottomBoundary) {//[-dialog, bottom)
recover();
} else {
fallOut();
}
} else {
if (velocityY < 0) {
riseOut();
} else {
fallOut();
}
}
}
reset();
return super.dispatchTouchEvent(ev);
}
private void reset() {
downY = -1;
lastY = -1;
handleBySelf = false;
handleByChild = false;
}
private boolean targetViewCanScroll(float dy) {
if (targetView instanceof AdapterView<?>) {
AdapterView<?> adapterView = (AdapterView<?>) targetView;
if (adapterView.getCount() == 0
|| (adapterView.getLastVisiblePosition() == adapterView.getCount() - 1
&& adapterView.getChildAt(adapterView.getChildCount() - 1).getBottom() <= adapterView.getHeight()
&& dy < 0)
|| (adapterView.getFirstVisiblePosition() == 0
&& adapterView.getChildAt(0).getTop() >= 0)
&& dy > 0) {
return false;
} else {
return true;
}
}
if ((targetView instanceof ScrollView)
&& ((ScrollView) targetView).getChildCount() > 0
&& ((ScrollView) targetView).getChildAt(0).getMeasuredHeight() > targetView.getHeight()
&& !((targetView.getScrollY() <= 0 && dy > 0) || (targetView.getScrollY() >= ((ScrollView) targetView).getChildAt(0).getMeasuredHeight() - targetView.getHeight() && dy < 0))) {
return true;
}
if (targetView instanceof TextView
&& ((TextView) targetView).getMovementMethod() != null
&& ((TextView) targetView).getLayout().getHeight() > targetView.getHeight()
&& !((targetView.getScrollY() <= 0 && dy > 0) || (targetView.getScrollY() >= ((TextView) targetView).getLayout().getHeight() - targetView.getHeight() && dy < 0))) {
return true;
}
return false;
}
private View getTargetView(View view, MotionEvent event) {
if (view instanceof AdapterView<?> || view instanceof ScrollView) {
event.recycle();
return view;
} else if (view instanceof ViewGroup) {
event.offsetLocation(-view.getLeft(), -view.getTop());//do not swap them, tears
ViewGroup viewGroup = (ViewGroup) view;
for (int i = 0; i < viewGroup.getChildCount(); i++) {
View child = viewGroup.getChildAt(i);
Rect childHitRect = new Rect();
child.getHitRect(childHitRect);
if (childHitRect.contains(((int) event.getX()), ((int) event.getY()))) {
// event.offsetLocation(-child.getTop(), -child.getLeft());
// event.transform(child.getInverseMatrix());
return getTargetView(child, event);
}
}
event.recycle();
return view;
} else {
event.recycle();
return view;
}
}
private void dispatchDownEventToChild(MotionEvent ev) {
MotionEvent cancelEvent = MotionEvent.obtain(ev);
cancelEvent.setAction(MotionEvent.ACTION_DOWN);
super.dispatchTouchEvent(cancelEvent);
cancelEvent.recycle();
}
private void dispatchCancelEventToChild(MotionEvent ev) {
MotionEvent cancelEvent = MotionEvent.obtain(ev);
cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
super.dispatchTouchEvent(cancelEvent);
cancelEvent.recycle();
}
public void fallIn() {
animate(0, MAX_DIM, AnimateType.FALL_IN);
}
private void recover() {
animate(0, MAX_DIM, AnimateType.RECOVER);
}
public void riseOut() {
animate(-dialogView.getBottom(), 0, AnimateType.RISE_OUT);
}
private void fallOut() {
animate(getHeight() - dialogView.getTop(), 0, AnimateType.FALL_OUT);
}
private void animate(float endTranslationY, float endDim, final AnimateType animateType) {
float translationYDelta = endTranslationY - dialogView.getTranslationY();
long duration = Math.min(Math.max((long) (Math.abs(translationYDelta) * 0.7F), 400), 600);
final float startDim = currDim;
final float dimDelta = endDim - startDim;
ValueAnimator animator = ValueAnimator.ofFloat(dialogView.getTranslationY(), endTranslationY);
animator.setDuration(duration);
animator.setInterpolator(new DecelerateInterpolator(1.6F));
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
dialogView.setTranslationY((Float) valueAnimator.getAnimatedValue());
if (changeDimEnabled) {
setDim(startDim + dimDelta * valueAnimator.getAnimatedFraction());
}
}
});
animator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
animating = true;
}
@Override
public void onAnimationEnd(Animator animator) {
animating = false;
if (swipeListener != null) {
switch (animateType) {
case FALL_IN:
swipeListener.onFallIn();
break;
case RECOVER:
swipeListener.onRecover();
break;
case RISE_OUT:
swipeListener.onRiseOut();
break;
case FALL_OUT:
swipeListener.onFallOut();
break;
}
}
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
animator.start();
}
private void setDim(float dim) {
setBackgroundColor(Color.argb((int) (255 * dim), 0, 0, 0));
currDim = dim;
}
public void setDialogView(View dialogView) {
this.dialogView = dialogView;
}
public void setSwipeListener(SwipeListener swipeListener) {
this.swipeListener = swipeListener;
}
public interface SwipeListener {
void onFallIn();
void onRiseOut();
void onFallOut();
void onRecover();
void onTouchOutside();
}
public void setChangeDimEnabled(boolean changeDimEnabled) {
this.changeDimEnabled = changeDimEnabled;
}
public void setCanceledOnTouchOutside(boolean cancel) {
canceledOnTouchOutside = cancel;
if (canceledOnTouchOutside) {
setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (touchOutside) {
if (swipeListener != null) {
swipeListener.onTouchOutside();
}
riseOut();
}
}
});
}
}
}