美文网首页
Android仿系统下拉抽屉

Android仿系统下拉抽屉

作者: 差点长成一枚帅哥 | 来源:发表于2024-10-23 11:49 被阅读0次

最近做了一个下拉抽屉效果,具体看下图


1729247475927.gif

产品实现要求
1.可以全局下拉,即使是退出到其他应用,依然可以下拉
2.可以上拉收起
要实现这个效果,站在开发者的焦虑立马回想到悬浮的WindowView,通过WindowManager.addView方法来实现,是的我也是这么实现的,但实现过程中遇到以下一个棘手的问题,就是悬浮的WindowView与我们的Activity,Fragment组件的焦点会冲突,从而导致要么Activity,Fragment不能触发点击事件,要么WindowView不能触发点击事件,我相信很多小伙伴们也遇到过我这个问题。
关键设置参数

// Activity可以获取焦点,但不能上拉下拉
int flags1 = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |    //不能获取焦点
                WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE |         //不能触摸
                WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |       //不能触摸模式
                WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW;  

//Activity获取不了焦点,可以上拉,下拉
int flags2 = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;

//初始化param
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
                WindowManager.LayoutParams.MATCH_PARENT,
                WindowManager.LayoutParams.MATCH_PARENT,
                type, flags2,
                PixelFormat.TRANSLUCENT);
        params.gravity = Gravity.TOP;
        params.alpha = 0f;
        params.height = Util.dp2px(App.getContext(), 10);
windowManager.addView(floatingView, params);

我最终用了frags2来实现这个效果,具体实现流程如下


image.png

实现这个效果分两部分代码

一、动态控制WindowView高度

    /**
     * 初始化参数
     */
    private WindowManager.LayoutParams initParams(){

        int type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;

        //Activity获取不了焦点,可以上拉,下拉
        int flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;

        WindowManager.LayoutParams params = new WindowManager.LayoutParams(
                WindowManager.LayoutParams.MATCH_PARENT,
                WindowManager.LayoutParams.MATCH_PARENT,
                type, flags,
                PixelFormat.TRANSLUCENT);
        params.gravity = Gravity.TOP;
        params.alpha = 0f;
        params.height = Util.dp2px(App.getContext(), 10);
        return params;
    }

     /**
     *  初始化下拉设置页面(在Service的onCreate里面调用该方法即可,当然Activity里面调用也行)
     */
    private void initSettingDrawerView(){
        WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
        floatingView = (VerticalDrawerLayout) LayoutInflater.from(this).inflate(R.layout.settings_hover_layout_new, null);
        params = initParams();
        // 将浮动视图添加到 WindowManager
        windowManager.addView(floatingView, params);
        floatingView.setOnDragStateChangeListener(new VerticalDrawerLayout.OnDragStateChangeListener() {
            @Override
            public void onStartDrag() {
                //开始拖拽
                if(!floatingView.isOpened() || params.height == Util.dp2px(App.getContext(), 10)) {
                    params.alpha = 1;
                    params.height = WindowManager.LayoutParams.MATCH_PARENT;
                    windowManager.updateViewLayout(floatingView, params);
                }
                Log.e(TAG, "onStartDrag");
            }

            @Override
            public void onStopDrag() {
                //停止拖拽
                Log.e(TAG, "onStopDrag");
            }

            @Override
            public void onDragToTop() {
                //收起
                Log.e(TAG, "onDragToTop");
                params.alpha = 0;
                params.height = Util.dp2px(App.getContext(), 10);
                windowManager.updateViewLayout(floatingView, params);
            }
        });
    }

二、实现下拉抽屉自定义View

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.LinearInterpolator;
import android.widget.FrameLayout;
import com.lxz.launcher.App;
import com.lxz.launcher.R;
import com.lxz.launcher.common.callback.IBaseCallback;
import com.lxz.launcher.common.utils.Util;
import com.lxz.launcher.module.setting.SettingUIView;

/**
 * 纵向抽屉
 */
public class VerticalDrawerLayout extends FrameLayout {

    private static final String TAG = "VerticalDrawerLayout";

    /**
     * 触发阈值
     */
    private static final float TRIGGER_THRESHOLD_VALUE = 0.3f;

    /**
     * 唯一内容子View
     */
    private View mContentView;
    private View mBgView;
    private SettingUIView settingUIView;

    private int contentTop = -800;
    private int marginTop = Util.dp2px(App.getContext(), 30);

    /**
     * 是否已打开
     */
    private boolean isOpened = false;

    /**
     * 拽托状态回调
     */
    private OnDragStateChangeListener mOnDragStateChangeListener;

    public VerticalDrawerLayout(Context context) {
        super(context);
    }

    public VerticalDrawerLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public VerticalDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }



    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        Log.d(TAG, "onLayout.....left:"+left + "  top:"+top+ "  contentTop:"+contentTop + "  right:" + right + "   bottom:"+bottom);
        if(contentTop != 0) {
            isOpened = false;
            mContentView.layout(0, contentTop, 1024, 0);
            setBgAlpha(0);
        }
    }

    /**
     * 设置背景透明度
     */
    private void setBgAlpha(int top){
        float max = getMeasuredHeight() - marginTop;
        float progress = Math.abs(getMeasuredHeight() + top) - marginTop;
        float alpha = progress/max;
        Log.d(TAG, "setBgAlpha.....max:"+max + "  progress:"+progress + "  alpha:"+alpha);
        mBgView.setAlpha(alpha);
    }

    @Override
    protected void onFinishInflate() {
        Log.d(TAG, "onFinishInflate.....");
        super.onFinishInflate();
        int childCount = getChildCount();
        if (childCount != 1) {
            throw new RuntimeException("子View必须只有1个,就是抽屉View");
        }
        mContentView = getChildAt(0);
        mBgView = findViewById(R.id.content_bg);

        //下拉设置控件
        settingUIView = findViewById(R.id.settint_ui_view);
        settingUIView.setDrawerLayout(this);
    }


    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d(TAG, "onInterceptTouchEvent.....");
        //将onInterceptTouchEvent委托给ViewDragHelper
        //return super.onInterceptTouchEvent(ev);
        return false;
    }

    private float[] recentSpeed = new float[2];
    private int recentIndex = 0;
    private float downY;
    private long downTime;
    private float lastY;
    private float destanceY;
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(TAG, "onLayout.....onTouchEvent.....");
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                //手指down事件
                lastY = event.getY();
                downY = lastY;
                downTime = System.currentTimeMillis();
                mOnDragStateChangeListener.onStartDrag();
                settingUIView.onShow();
                //速度归零
                recentSpeed[0] = 0;
                recentSpeed[1] = 0;

                break;
            case MotionEvent.ACTION_MOVE:
                //手指move事件
                destanceY = event.getY() - lastY;
                lastY = event.getY();
                moveContentView(destanceY);
                Log.d(TAG, "mContentView.move destance:" + destanceY);
                recentSpeed[recentIndex] = Math.abs(destanceY);
                recentIndex++;
                recentIndex %= 2;

                break;
            case MotionEvent.ACTION_UP:
                //手指up事件
                float time = (System.currentTimeMillis() - downTime);
                float speedDirect = (event.getY() - downY)/time;
                float speed = (recentSpeed[0] + recentSpeed[1])/2;
                int bottom = mContentView.getBottom();
                int height = mContentView.getMeasuredHeight();
                Log.d(TAG, "mContentView.up speed:" + speed);
                Log.d(TAG, "mContentView.up destance speed:" + speed);
                if(speedDirect <= -0.0f && speed >= 20f){
                    //向上速度很快,直接收起
                    animToClose();
                    break;
                }
                if(speedDirect >= 0.0f && speed >= 20f){
                    //向下速度很快,直接展开
                    animToOpen(false);
                    break;
                }
                if(bottom > height/2){
                    //展开
                    animToOpen(false);
                }else {
                    //收起
                    animToClose();
                }

                break;
        }
        return false;
    }

    /**
     * 展开
     */
    public void animToOpen(boolean isStrong){
        if(isStrong){
            mOnDragStateChangeListener.onStartDrag();
            settingUIView.onShow();
        }
        int topFrom = mContentView.getTop();
        int topTo = 0;
        doValueAnimatorToMove(topFrom, topTo, new IBaseCallback<Boolean>() {
            @Override
            public void onSuccess(Boolean result) {
                mOnDragStateChangeListener.onStopDrag();
            }
        });
    }

    /**
     * 收起
     */
    public void animToClose(){
        animToClose(new IBaseCallback<Boolean>() {
            @Override
            public void onSuccess(Boolean result) {
                settingUIView.onHide();
            }
        });

    }

    /**
     * 收起
     */
    public void animToClose(IBaseCallback<Boolean> callback){
        int topFrom = mContentView.getTop();
        int topTo = -800;
        doValueAnimatorToMove(topFrom, topTo, new IBaseCallback<Boolean>() {
            @Override
            public void onSuccess(Boolean result) {
                settingUIView.onHide();
                mOnDragStateChangeListener.onDragToTop();
                if(null != callback){
                    callback.onSuccess(true);
                }
            }
        });
    }

    /**
     * 移动距离
     * @param destanceY
     */
    private void moveContentView(float destanceY){
        Log.d(TAG, "move top:"+ mContentView.getTop()  + "  destanceY:"+destanceY);
        //contentView已经收齐
        int top = mContentView.getTop();
        top += Math.round(destanceY);
        moveToContentView(top);
    }

    /**
     * 移动至top位置
     * @param top
     */
    private void moveToContentView(int top){
        if(top <= -800){
            top = -800;
            isOpened = false;
        }
        if(top >= 0){
            top = 0;
            isOpened = true;
        }
        mContentView.layout(0, top, 1280, top+800);
        contentTop = top;
        setBgAlpha(top);
    }


    /**
     * 动画执行,继续移动至结束
     * @param from
     * @param to
     */
    private void doValueAnimatorToMove(int from, int to, IBaseCallback<Boolean> callback) {
        int destance = Math.abs(to - from);
        Log.d(TAG, "doValueAnimatorToMove destance:" + destance);
        // ofXxx(X.. values) 此处values是可变参数,动画过程中产生的数值与构造时传入的值类型一样
        ValueAnimator valueAnimator = ValueAnimator.ofInt(from, to);

        // 自带的监听,监听动画过程中值的实时变化
        valueAnimator.addUpdateListener(animation -> {
            // 参数animation为当前状态动画的实例
            int curValue = (Integer) animation.getAnimatedValue();
            moveToContentView(curValue);
        });
        valueAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                callback.onSuccess(true);
            }
        });
        //valueAnimator.setRepeatMode(ValueAnimator.RESTART);
        //valueAnimator.setRepeatCount(ValueAnimator.REVERSE);
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.setDuration(destance >= 500 ? 200 : 100);
        valueAnimator.start();
    }


    /**
     * 是否已打开
     */
    public boolean isOpened() {
        return isOpened;
    }

    /**
     * 结束回调
     */
    public interface OnDragStateChangeListener {
        void onStartDrag();

        void onStopDrag();

        void onDragToTop();
    }

    public void setOnDragStateChangeListener(OnDragStateChangeListener listener) {
        this.mOnDragStateChangeListener = listener;
    }
}

最后补上settings_hover_layout_new布局文件

<?xml version="1.0" encoding="utf-8"?>
<com.lxz.launcher.common.wedgit.VerticalDrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/settings_ui"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <FrameLayout
        android:id="@+id/contentView"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <View
            android:id="@+id/content_bg"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@drawable/background"
            android:alpha="1">
        </View>

        <com.lxz.launcher.module.setting.SettingUIView
            android:id="@+id/settint_ui_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_margin="@dimen/dp_44"
            android:background="@drawable/shape_settings_background"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.0"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0.0"
            android:orientation="vertical">
        </com.lxz.launcher.module.setting.SettingUIView>
    </FrameLayout>

</com.lxz.launcher.common.wedgit.VerticalDrawerLayout>

SettingUIView为另外一个自定义View,小伙伴们不用管,这里直接去实现自己的布局文件即可。
若小伙伴们有其他问题,请留言,我看到会第一时间回复!

相关文章

网友评论

      本文标题:Android仿系统下拉抽屉

      本文链接:https://www.haomeiwen.com/subject/kwtldjtx.html