javaandroidandroid-adaptergrid-layoutview-hierarchy

How to get selected child in GridLayout similar to GridView


I want to achieve the following abilities:

The problem is when when registering a View.OnLongClickListener callback to child View, neither parent GridLayout nor any ancestor registered callbacks (either View.OnClickListener or View.onTouchEvent) called when clicking on them.

How can I get a selected child inside a GridLayout similar to either AdapterView.OnItemSelectedListener or AdapterView.OnItemLongClickListener and solve the above mentioned problem?


Solution

  • What about storing a "selected" view as a global variable, and removing it when its focus changes? By playing with focusable, focusableInTouchMode and onClick listeners, you could have the right results. I'm not sure that's the best solution, but it works.

    What you will need:

    • A global View variable: the GridLayout's child long clicked, as selected.
    • (optional) A custom parent container as any ViewGroup: it will set the focusable listeners on all its children [*]. In my tests, I used a LinearLayout and a RelativeLayout.

    [*] If you don't use the optional parent custom Class, you have to set android:focusable="true" and android:focusableInTouchMode="true" on all children of the parent ViewGroup. And you'll have to set OnClickListener in order to call removeViewSelected() when the parent ViewGroup is clicked.

    • Adding Click listeners for GridLayout children: which updates the selected view.
    • Implementing a Focus listener: which removes the selected view if it's losing focus.

    It will handle all focus change state on parent and child hierarchy, see the output:

    GridLayout selected view and disable view on click listeners

    I used the following pattern:

    CoordinatorLayout         --- simple root group
        ParentLayout          --- aka "parentlayout"
            Button            --- simple Button example
            GridLayout        --- aka "gridlayout"
        FloattingActionButton --- simple Button example
    

    Let's preparing the selected View and its update methods in the Activity:

    private View selectedView;
    
    ...
    private void setViewSelected(View view) {
        removeViewSelected();
    
        selectedView = view;
        if (selectedView != null) {
            // change to a selected background for example
            selectedView.setBackgroundColor(
                    ContextCompat.getColor(this, R.color.colorAccent));
        }
    }
    
    private View getViewSelected() {
        if (selectedView != null) {
            return selectedView;
        }
        return null;
    }
    
    private void removeViewSelected() {
        if (selectedView != null) {
            // reset the original background for example
            selectedView.setBackgroundResource(R.drawable.white_with_borders);
            selectedView = null;
        }
        // clear and reset the focus on the parent
        parentlayout.clearFocus();
        parentlayout.requestFocus();
    }
    

    On each GridLayout child, add the Click and LongClick listeners to update or remove the selected view. Mine were TextViews added dynamically, but you could easily create a for-loop to retrieve the children:

    TextView tv = new TextView(this);
    ...
    gridlayout.addView(tv);
    
    tv.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            removeViewSelected();
        }
    });
    
    tv.setOnLongClickListener(new View.OnLongClickListener() {
        @Override
        public boolean onLongClick(View view) {
            setViewSelected(view);
            return true;
        }
    });
    

    Set the FocusChange listener on the parent container:

    parentlayout.setOnFocusChangeListener(new View.OnFocusChangeListener() {
        @Override
        public void onFocusChange(View view, boolean hasFocus) {
            View viewSelected = getViewSelected();
            // if the selected view exists and it lost focus
            if (viewSelected != null && !viewSelected.hasFocus()) {
                // remove it
                removeViewSelected();
            }
        }
    });
    

    Then, the optional custom ViewGroup: it's optional because you could set the focusable state by XML and the clickable listener dynamically, but it seems easier to me. I used this following custom Class as parent container:

    public class ParentLayout extends RelativeLayout implements View.OnClickListener {
    
        public ParentLayout(Context context) {
            super(context);
            init();
        }
    
        public ParentLayout(Context context, AttributeSet attrs) {
            super(context, attrs);
            init();
        }
    
        public ParentLayout(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init();
        }
    
        // handle focus and click states
        public void init() {
            setFocusable(true);
            setFocusableInTouchMode(true);
            setOnClickListener(this);
        }
    
        // when positioning all children within this 
        // layout, add their focusable state
        @Override
        protected void onLayout(boolean c, int l, int t, int r, int b) {
            super.onLayout(c, l, t, r, b);
    
            final int count = getChildCount();
            for (int i = 0; i < count; i++) {
                final View child = getChildAt(i);
                child.setFocusable(true);
                child.setFocusableInTouchMode(true);
            }
            // now, even the Button has a focusable state
        }
    
        // handle the click events
        @Override
        public void onClick(View view) {
            // clear and set the focus on this viewgroup
            this.clearFocus();
            this.requestFocus();
            // now, the focus listener in Activity will handle
            // the focus change state when this layout is clicked
        }
    }
    

    For example, this is the layout I used:

    <?xml version="1.0" encoding="utf-8"?>
    <android.support.design.widget.CoordinatorLayout ...>
    
        <com.app.ParentLayout
            android:id="@+id/parent_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center_horizontal">
    
            <Button
                android:id="@+id/sample_button"
                android:layout_width="250dp"
                android:layout_height="wrap_content"
                android:layout_centerHorizontal="true"
                android:layout_alignParentBottom="true"
                android:text="A Simple Button"
                android:layout_marginTop="20dp"
                android:layout_marginBottom="20dp"/>
    
            <android.support.v7.widget.GridLayout
                android:id="@+id/grid_layout"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_centerHorizontal="true"
                android:layout_above="@id/sample_button" .../>
        </com.app.ParentLayout>
    
        <android.support.design.widget.FloatingActionButton .../>
    </android.support.design.widget.CoordinatorLayout>
    

    Hope this will be useful.