最近做了一个下拉抽屉效果,具体看下图
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,小伙伴们不用管,这里直接去实现自己的布局文件即可。
若小伙伴们有其他问题,请留言,我看到会第一时间回复!
网友评论