PopupWindow触摸事件透传方案

背景

有时我们弹出一个PopupWindow弹窗时,有这么一个需求:

  1. 点击弹窗上的控件(非空白区域)时,执行控件的点击逻辑;

  2. 手指触到弹窗上空白区域时,事件透传到弹窗下的view,即不影响正常的业务逻辑

思路

给PopupWindow设置onTouchInterceptor,判断触摸事件的坐标是否在弹窗内的某个组件中,是的话触摸事件由弹窗根视图传递;否则交给activity去传递事件。

代码实现

构建场景:Activity中有一个ListView,弹窗中有两个Button,一个负责显示Toast,一个负责控制另一个Button的可见性。

设置UI布局

以下是对应的三个布局文件:

主界面activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/root"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        android:id="@+id/text_view"
        android:visibility="gone"
        />

    <ListView
        android:id="@+id/list"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

</LinearLayout>

ListView列表项布局item_layout.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/text_item"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</LinearLayout>

弹窗布局window_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="@color/purple_500">

    <Button
        android:id="@+id/btn_window"
        android:text="Button1 for test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <Button
        android:id="@+id/btn_hide"
        android:text="Hide Button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <TextView
            android:id="@+id/text_window"
            android:textIsSelectable="true"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:text="@string/many_letters"/>
</LinearLayout>

设置数据

主要是给ListView设置数据,我们的适配器类如下所示:

public class MyAdapter extends BaseAdapter {
    private List<String> mList;
    private Context mContext;

    public MyAdapter(List<String> list, Context context) {
        mList = list;
        mContext = context;
    }

    @Override
    public int getCount() {
        return mList.size();
    }

    @Override
    public String getItem(int i) {
        return mList.get(i);
    }

    @Override
    public long getItemId(int i) {
        return i;
    }

    @Override
    public View getView(int i, View view, ViewGroup viewGroup) {
        if (view == null) {
            view = LayoutInflater.from(mContext).inflate(R.layout.item_layout, null);
        }
        TextView textItem = view.findViewById(R.id.text_item);
        textItem.setText(getItem(i));
        return view;
    }
}

而后在MainActivity中绑定ListView和适配器:

public class MainActivity extends AppCompatActivity {
    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView textView = findViewById(R.id.text_view);
        ListView listView = findViewById(R.id.list);
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < 200; i++) {
            list.add("No." + i);
        }
        MyAdapter adapter = new MyAdapter(list, this);

        listView.setAdapter(adapter);

        adapter.notifyDataSetChanged();
        ...
    }
    
    ...
}

3)、构造弹窗

定义属性mWindowView,后面事件透传会用到。而后在MainActivity的onCreate()中,在绑定适配器下面加载弹窗视图,并且设置属性:

public class MainActivity extends AppCompatActivity {
    ...
    private View mWindowView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        mWindowView = LayoutInflater.from(this).inflate(R.layout.window_layout, null);
        PopupWindow window = new PopupWindow(this);

        mButton = windowView.findViewById(R.id.btn_window);
        mButton.setOnClickListener(view -> {
            Toast.makeText(MainActivity.this, "Button on window has been clicked!", Toast.LENGTH_SHORT).show();
        });

        mHide = windowView.findViewById(R.id.btn_hide);
        mHide.setOnClickListener(v -> {
            mButton.setVisibility(mButton.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE);
        });

        ...

        window.setContentView(mWindowView);
        window.setWidth(750);
        window.setHeight(1750);

        // 展示弹窗必须post
        textView.post(() -> {
            window.showAtLocation(textView, Gravity.START, 0, 0);
        });
        ...
    }
    ...
}

实现触摸事件透传

我们需要给弹窗设置一个触摸拦截器,先尝试获取触摸到的子视图,如果有,说明没有触摸到空白区域,弹窗传递并消费事件;否则,说明触摸到了空白区域,先修改事件坐标,再传给activity:

public class MainActivity extends AppCompatActivity {
    private Button mButton;
    private Button mHide;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...

        window.setTouchInterceptor((view, motionEvent) -> { // 设置触摸拦截器
            // 获取触摸到的子视图
            View target = Utils.getViewTouchedByEvent(mWindowView, motionEvent);

            // 弹窗处理事件
            if (target != null) {
                mWindowView.dispatchTouchEvent(motionEvent);
                return true;
            }

            // 没有子视图消费事件,就把事件传给activity;
            // 先对事件坐标进行变换,然后交给主界面传递,不对事件流进行消费
            int[] popupLocation = new int[2];
            mWindowView.getLocationOnScreen(popupLocation);
            event.offsetLocation(popupLocation[0], popupLocation[1]);
     
            MainActivity.this.dispatchTouchEvent(motionEvent);
            return false;
        });

        ...
    }

...
}

其中获取触摸到的子视图的方法getViewTouchedByEvent()封装到了Utils类中:

public class Utils {
    public static View getViewTouchedByEvent(View view, MotionEvent event) {
        if (view == null || event == null) {
            return null;
        }

        if (!(view instanceof ViewGroup)) {
            return isDebugWindowValidTouched(view, event) ? view : null;
        }

        ViewGroup parent = ((ViewGroup) view);
        int childrenCount = parent.getChildCount();
        for (int i = 0; i < childrenCount; i++) {
            View target = getViewTouchedByEvent(parent.getChildAt(i), event);
            if (target != null) {
                return target;
            }
        }
        return null;
    }

    // 判断event坐标是否在视图view中
    private static boolean isDebugWindowValidTouched(View view, MotionEvent event) {
        if (event == null || view == null) {
            return false;
        }

        if (view.getVisibility() != View.VISIBLE) {
            return false;
        }

        final float eventRawX = event.getRawX();
        final float eventRawY = event.getRawY();

        RectF rect = new RectF();
        int[] location = new int[2];
        view.getLocationOnScreen(location);

        float x = location[0];
        float y = location[1];
        rect.left = x;
        rect.right = x + view.getWidth();
        rect.top = y;
        rect.bottom = y + view.getHeight();

        return rect.contains(eventRawX, eventRawY) ;
    }
}

此方法的主要步骤就是判断当前view是不是ViewGroup:是的话,根据event的坐标和view的坐标判断view是不是被触摸到了;否则对view的所有子view递归调用getViewTouchedByEvent(),来获取被触摸事件触摸的最底层的view。

最后,说下触摸到空白区域时,把触摸事件传给activity前,修改事件坐标的原因:事件坐标是事件在视图中的坐标,而且底层的activity是正常的尺寸,因此需要加上弹窗根视图在屏幕上的坐标偏移量,再传给activity。

效果 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值