background
Sometimes when we pop up a PopupWindow, there is a requirement:
-
Click the clickable control on the pop-up window to execute the click logic of the control;
-
When the finger touches the blank area on the pop-up window or the non clickable control, the event will be transmitted to the view under the pop-up window, that is, it will not affect the normal business logic
thinking
Set onTouchInterceptor for PopupWindow. When the touch event is down, judge whether the coordinates of the touch event are in a clickable component in the pop-up window. If yes, execute component click monitoring; Otherwise, it is left to the activity to pass the event.
code implementation
Build scenario: there is a ListView in the Activity and two buttons in the pop-up window. One is responsible for displaying the log and the other is responsible for controlling the visibility of the other Button.
Set UI layout
The following are the three corresponding layout files:
Main interface 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 list item layout 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>
Pop up window layout_ 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"/> </LinearLayout>
Set data
It mainly sets data for ListView. Our adapter class is as follows:
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; } }
Then bind ListView and adapter in MainActivity:
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) Structure pop-up window
Also in onCreate() of MainActivity, under the binding adapter:
PopupWindow window = new PopupWindow(this); View windowView = LayoutInflater.from(this).inflate(R.layout.window_layout, null); mButton = windowView.findViewById(R.id.btn_window); mButton.setOnClickListener(view -> { Log.i("MainActivity", "onCreate: Button is clicked!"); }); mHide = windowView.findViewById(R.id.btn_hide); mHide.setOnClickListener(v -> { mButton.setVisibility(mButton.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE); }); ... window.setContentView(windowView); window.setWidth(750); window.setHeight(1750); // The display pop-up window must be post textView.post(() -> { window.showAtLocation(textView, Gravity.START, 0, 0); });
Transparent transmission of touch events
First, we need to know the location of all the controls in the pop-up window to respond to the click event, so we need to save all the clickable controls:
public class MainActivity extends AppCompatActivity { private Button mButton; private Button mHide; private List<View> mWindowViews = new ArrayList<>(); // Save all view s that need to respond to click events @Override protected void onCreate(Bundle savedInstanceState) { ... mButton = windowView.findViewById(R.id.btn_window); mButton.setOnClickListener(view -> { Log.i("MainActivity", "onCreate: Button is clicked!"); }); mHide = windowView.findViewById(R.id.btn_hide); mHide.setOnClickListener(v -> { mButton.setVisibility(mButton.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE); }); // After the view is initialized, it can be saved mWindowViews.add(mButton); mWindowViews.add(mHide); ... } ... }
Then, we need to set a touch interceptor for the pop-up window. Because the click event corresponds to the press event, we only need to process the press event in the interceptor, and let the activity pass the rest:
public class MainActivity extends AppCompatActivity { private Button mButton; private Button mHide; private List<View> mWindowViews = new ArrayList<>(); @Override protected void onCreate(Bundle savedInstanceState) { ... mWindowViews.add(mButton); mWindowViews.add(mHide); window.setTouchInterceptor((view, motionEvent) -> { // Set touch interceptor if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) { // Only press events are processed View target = isDebugWindowValidTouched(motionEvent); // Try to get the pop-up view corresponding to the press event if (target != null && target.isClickable()) { // If this view can be clicked, execute its click logic and consume this event flow target.performClick(); return true; } } // In other cases, the event is transferred to the main interface without consuming the event flow MainActivity.this.dispatchTouchEvent(motionEvent); return false; }); ... } ... }
Find the control view that needs to be consumed and consume this event flow. Because an event flow includes all events from press to move and then to lift, and the click event generally has a small displacement from press to lift, so the whole event flow is handed over to the pop-up window. In other words, if the user's finger presses a clickable component in the pop-up window and does not loosen it, but moves to other blank areas to loosen it (a common action indicating wrong pressing), we will ignore this situation and the pop-up window will still enter the click logic.
Finally, the core of this article is to determine the pop-up control corresponding to the press event, and the corresponding method isdebugwindovalidtouched():
private View isDebugWindowValidTouched(MotionEvent event) { if (event == null) { return null; } final float eventX = event.getX(); final float eventY = event.getY(); // If getX() and getY() can't get the correct coordinates, try getRawX() and getRawY() final float eventRawX = event.getRawX(); final float eventRawY = event.getRawY(); for (View view : mWindowViews) { if (view.getVisibility() != View.VISIBLE) { continue; } 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(); if (rect.contains(eventX, eventY) || rect.contains(eventRawX, eventRawY))) { return view; } } return null; }
effect