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

前言

Uber大家都用过,有时候它的对话框是从顶部落下来,你可以把它推上去关闭,或者把它拽下去关闭。我觉得这种交互方式很好。符合认知,也更加便捷。用在一些非关键信息的展示很合适,比如广告。

效果图

这里写图片描述

原理

并没有去继承Dialog,而是直接将dialog视图通过WindowManager.addView方法添加到窗口中。当然,我在dialog视图外层包了一层FrameLayout用来获取并处理触摸事件,并实现dialog视图的移动。至于获取触摸事件,移动dialog视图就是老生长谈了。值得注意的是,在自己处理触摸事件的时候,要注意不影响别的控件的正常工作。比如,如何中途你要夺取手势的处理权,记得给手势本来的处理者发送cancel事件;如果你中途想把一个手势交给别的控件处理记得给该控件发个down事件,让它可以进行一些必要的初始化。

剩下的就是移动dialog过程中一些状态的判断。比如如果移动距离大于touchSlop截获触摸事件,这样不会触发dialog视图上面的各种Listener。在抬手的时候,判断速度决定是否关闭对话框。

Demo

Github

小点

注意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);
    }
}

猜你喜欢

转载自blog.csdn.net/fly1183989782/article/details/47208549