前言
Uber大家都用过,有时候它的对话框是从顶部落下来,你可以把它推上去关闭,或者把它拽下去关闭。我觉得这种交互方式很好。符合认知,也更加便捷。用在一些非关键信息的展示很合适,比如广告。
效果图
原理
并没有去继承Dialog,而是直接将dialog视图通过WindowManager.addView方法添加到窗口中。当然,我在dialog视图外层包了一层FrameLayout用来获取并处理触摸事件,并实现dialog视图的移动。至于获取触摸事件,移动dialog视图就是老生长谈了。值得注意的是,在自己处理触摸事件的时候,要注意不影响别的控件的正常工作。比如,如何中途你要夺取手势的处理权,记得给手势本来的处理者发送cancel事件;如果你中途想把一个手势交给别的控件处理记得给该控件发个down事件,让它可以进行一些必要的初始化。
剩下的就是移动dialog过程中一些状态的判断。比如如果移动距离大于touchSlop截获触摸事件,这样不会触发dialog视图上面的各种Listener。在抬手的时候,判断速度决定是否关闭对话框。
Demo
小点
注意VelocityTracker的用法
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
velocityTracker = VelocityTracker.obtain();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
velocityTracker.clear();
}
private boolean onDown(MotionEvent ev) {
velocityTracker.clear();
velocityTracker.addMovement(ev);
...
}
private boolean onMove(MotionEvent ev) {
velocityTracker.addMovement(ev);
...
}
private boolean onEnd(MotionEvent ev) {
velocityTracker.addMovement(ev);
...
}
recycle MotionEvent
private void dispatchCancelEvent(MotionEvent ev) {
MotionEvent cancelEvent = MotionEvent.obtain(ev);
cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
super.dispatchTouchEvent(cancelEvent);
cancelEvent.recycle();
}
根据要移动的距离计算移动动画的时长
long duration = Math.min(Math.max((long) (Math.abs(translationYDelta) * 0.7F), 400), 600);
源码
public class SwipeDialogManager{
private Context context;
private WindowManager windowManager;
private EmbedDialogFrameLayout dialogContainer;
public SwipeDialogManager(Context context) {
this.context = context;
this.windowManager = (WindowManager) context.getSystemService("window");
dialogContainer = new EmbedDialogFrameLayout(context);
}
/**
* deprecated
* @param layout
*/
public void addDialogView(int layout) {
dialogContainer = new EmbedDialogFrameLayout(context);
dialogContainer.addDialogView(layout);
dialogContainer.setRemoveDialogListener(new RemoveDialogListener(){
@Override
public void removeDialog() {
windowManager.removeView(dialogContainer);
}
});
addContainerToWindowManager();
}
public void addDialogView(View view) {
addViewToContainer(view);
addContainerToWindowManager();
}
private void addViewToContainer(View view) {
dialogContainer.addDialogView(view);
dialogContainer.setRemoveDialogListener(new RemoveDialogListener() {
@Override
public void removeDialog() {
windowManager.removeView(dialogContainer);
}
});
}
private void addContainerToWindowManager() {
WindowManager.LayoutParams windowParams = new WindowManager.LayoutParams();
windowParams.x = 0;
windowParams.y = 0;
windowParams.width = WindowManager.LayoutParams.MATCH_PARENT;
windowParams.height = WindowManager.LayoutParams.MATCH_PARENT;
windowParams.flags = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
windowParams.format = PixelFormat.TRANSLUCENT;
windowManager.addView(dialogContainer, windowParams);
}
public void show(){
dialogContainer.show();
}
public interface RemoveDialogListener {
void removeDialog();
}
public class EmbedDialogFrameLayout extends FrameLayout {
private static final float DIM_RATIO = 0.8F;
private float currentDim;
private View dialogView;
private float downY;
private int translationYTopBoundary;
private int translationYBottomBoundary;
private boolean animating;
private SwipeDialogManager.RemoveDialogListener removeDialogListener;
private int minFlingVelocity;
private int touchSlop;
private boolean intercept;
private VelocityTracker velocityTracker;
private int maxFlingVelocity;
private int customFlingVelocityThrehold;
private int translationYMax;
public EmbedDialogFrameLayout(Context context) {
super(context);
init();
}
public EmbedDialogFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public EmbedDialogFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
ViewConfiguration viewConfiguration = ViewConfiguration.get(getContext());
minFlingVelocity = viewConfiguration.getScaledMinimumFlingVelocity();
maxFlingVelocity = viewConfiguration.getScaledMaximumFlingVelocity();
customFlingVelocityThrehold = (minFlingVelocity + maxFlingVelocity) / 5;
touchSlop = viewConfiguration.getScaledTouchSlop();
}
public void addDialogView(int layout) {
View dialogView = LayoutInflater.from(getContext()).inflate(layout, this, false);
dialogView.findViewById(R.id.girl).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(getContext(), "beautiful girl", Toast.LENGTH_LONG).show();
}
});
dialogView.findViewById(R.id.button).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(getContext(), "button clicked", Toast.LENGTH_LONG).show();
}
});
addDialogView(dialogView);
}
public void addDialogView(View dialogView){
this.dialogView = dialogView;
dialogView.setVisibility(INVISIBLE);
FrameLayout.LayoutParams params = (LayoutParams) dialogView.getLayoutParams();
params.gravity = Gravity.CENTER;
addView(dialogView, params);
}
public void show() {
getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
translationYTopBoundary = -(dialogView.getTop() + dialogView.getBottom()) / 2;
translationYBottomBoundary = -translationYTopBoundary;
translationYMax = dialogView.getBottom();
fallIn();
getViewTreeObserver().removeOnPreDrawListener(this);
return true;
}
});
invalidate();
}
private void fallIn() {
dialogView.setTranslationY(-dialogView.getBottom());
dialogView.setVisibility(VISIBLE);
animate(0, DIM_RATIO, false);
}
@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);
downY = ev.getY();
super.dispatchTouchEvent(ev);
return true;
}
private boolean onMove(MotionEvent ev) {
velocityTracker.addMovement(ev);
if (downY == -1) {
downY = ev.getY();
}
float dy = ev.getY() - downY;
dialogView.setTranslationY(dy);
float newDim = DIM_RATIO * (1 - Math.min(1, Math.abs(dy / translationYMax)));
setDim(newDim);
if (intercept) {
return true;
}
if (Math.abs(dy) > touchSlop) {
dispatchCancelEvent(ev);
intercept = true;
return true;
}
return super.dispatchTouchEvent(ev);
}
private void dispatchCancelEvent(MotionEvent ev) {
MotionEvent cancelEvent = MotionEvent.obtain(ev);
cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
super.dispatchTouchEvent(cancelEvent);
cancelEvent.recycle();
}
private boolean onEnd(MotionEvent ev) {
velocityTracker.addMovement(ev);
velocityTracker.computeCurrentVelocity(1000);
float velocityY = velocityTracker.getYVelocity();
if (Math.abs(velocityY) < customFlingVelocityThrehold) {
if (dialogView.getTranslationY() < translationYTopBoundary) {//[-infinite, -dialogTop)
riseOut();
} else if (dialogView.getTranslationY() < translationYBottomBoundary) {//[-dialog, bottom)
recover();
} else {
fallOut();
}
} else {
if (intercept) {
if (velocityY < 0) {
riseOut();
} else {
fallOut();
}
}
}
return super.dispatchTouchEvent(ev);
}
private void recover() {
animate(0, DIM_RATIO, false);
}
private void riseOut() {
animate(-dialogView.getBottom(), 0, true);
}
private void fallOut() {
animate(getHeight() - dialogView.getTop(), 0, true);
}
private void animate(float endTranslationY, float endDim, final boolean dismiss) {
float translationYDelta = endTranslationY - dialogView.getTranslationY();
long duration = Math.min(Math.max((long) (Math.abs(translationYDelta) * 0.7F), 400), 600);
final float startDim = currentDim;
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());
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;
downY = -1;
intercept = false;
if (dismiss) {
if (removeDialogListener != null) {
removeDialogListener.removeDialog();
}
if (onDismissListener != null) {
onDismissListener.onDismiss(null);
}
}
}
@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));
currentDim = dim;
}
private Dialog.OnDismissListener onDismissListener;
public void setOnDismissListener(Dialog.OnDismissListener onDismissListener) {
this.onDismissListener = onDismissListener;
}
public void setRemoveDialogListener(SwipeDialogManager.RemoveDialogListener removeDialogListener) {
this.removeDialogListener = removeDialogListener;
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_BACK:
if (!animating){
riseOut();
}
break;
case KeyEvent.KEYCODE_MENU:
break;
default:
break;
}
return super.dispatchKeyEvent(event);
}
}