PopupWindow touch event transparent transmission scheme

background

Sometimes when we pop up a PopupWindow, there is a requirement:

  1. Click the clickable control on the pop-up window to execute the click logic of the control;

  2. 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

 

Keywords: Android

Added by speps on Sun, 19 Sep 2021 12:09:45 +0300