背景
有时我们弹出一个PopupWindow弹窗时,有这么一个需求:
-
点击弹窗上的控件(非空白区域)时,执行控件的点击逻辑;
-
手指触到弹窗上空白区域时,事件透传到弹窗下的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。
效果