1

I want to achieve the following abilities:

  • Select only one child View inside a GridLayout each time by long clicking it.
  • A click on the GridLayout or any ancestor parent in the visual hierarchy will deselected selected child View if one already selected.

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?

Eido95
  • 1,313
  • 1
  • 15
  • 29

2 Answers2

4

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.

Blo
  • 11,903
  • 5
  • 45
  • 99
  • Thank you very much for your detailed answer. After implementing your solution _without_ implementing **ParentLayout** (1. Used `ViewGroup` instance and inflated an XML instead of `ParentLayout` 2. Declared settings in inflated XML instead of inside `onLayout` and `init` methods 3. Registered and handled `View.OnClickListener` with `ViewGroup` inflated instance instead of inside `ParentLayout`) it seems that in order to _deselected an already selected child using a click on the layout_ you need to **double touch** the layout instead of **single touch** it. Do you know how to solve it? – Eido95 Dec 29 '16 at 11:34
  • 1
    @Eido95, I made the changes to use directly a `LinearLayout`, write `focusable`, `focusableInTouchMode` on children in XML, also tried with `RelativeLayout`, create the `OnClickListener` on `LinearLayout` calling `removeViewSelected()`... everything works as expected on a **single touch**. Could you set some `Logs` to see if everything's called? Maybe, see this [related issue](http://stackoverflow.com/questions/16792238/onclicklistener-not-triggered-for-custom-view). If you dont find, post your code on Pastebin and link it; I could test it. I dont have any problem without using a custom Class. – Blo Dec 29 '16 at 15:36
  • I read your linked related issue but unfortunately it didn't solve my problem. I've uploaded [part of my code](http://pastebin.com/YtdqL4Zp) (6 files) that is relevant for my question so you can easily build it and reproduce the problem I've mentioned in my previous comment. Also I've highlighted relevant code lines which output logs to see which method called. – Eido95 Dec 30 '16 at 17:06
  • 1
    Well @Eido95, thanks for sharing your files, I copied/pasted into a new project and everything works fine. The simple touch in the bottom right works as expected (selection + deselection), no double touch needed. I just made three changes to build it on my computer, I didn't use the `TypedArray array` in `ScreensLayout` (style missing), same as `changeSplit` method (`ScreenSplit` missing), and built with libraries `25.0.1` (but this shouldn't be related to your issue). – Blo Dec 30 '16 at 19:20
  • Thank you very much for your time @Fllo. Unfortunately building with libraries `25.1.0` didn't solve my problem. In addition, I've updated the [linked partial code paste](http://pastebin.com/YtdqL4Zp) with `ScreenSplit` for your convenience. – Eido95 Dec 31 '16 at 00:30
  • Despite the fact that everything worked fine for you when implementing the code provided, I've uploaded a [visual reproduce](http://imgur.com/yEAfwme) of the mentioned problem (when running the code I provided) on my debugged physical device. Firstly I present when **single touch** deselection _works_ (deselecting selected screen when clicking it again), secondly I present when **single touch** _doesn't work_ (deselecting using another screen), so one more touch (results **double touch**) needed (the double touch action is pretty quick so pay attention to it). – Eido95 Dec 31 '16 at 00:34
  • May be it is either an Android version or ROM problem @Fllo? Which device at which version did you use to successfully solve the problem? – Eido95 Dec 31 '16 at 00:39
  • 1
    Finally get it @Eido95! thanks for the gif, I didn't notice the first time in my tests, but I think I had the bug too. It's not an API or ROM problem, my guess it's because of a custom widget inflated, or maybe the mediaplayer idk... I thought it was the main container which didn't get the single touch, sorry. Anyway, I had too and finally resolved by removing all `focusable` and `focusableInTouchMode` attributes in `layout_grid_screen.xml` for all `ScreenView` elements. It will work well now! :) – Blo Dec 31 '16 at 02:27
0

Use the following code :

int last_pos = -1;
GridLayout gridLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    gridLayout = (GridLayout) findViewById(R.id.gridLayout);
    int child_count = gridLayout.getChildCount();
    for(int i =0;i<child_count;i++){
        gridLayout.getChildAt(i).setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View view) {
                //Deselect previous
                if(last_pos!=-1) gridLayout.getChildAt(last_pos).setSelected(false);
                //Select the one you clicked
                view.setSelected(true);
                last_pos = gridLayout.indexOfChild(view);
                return false;
            }
        });
    }
    //Remove focus if the parent is clicked
    gridLayout.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            gridLayout.getChildAt(last_pos).setSelected(false);
        }
    });
Hristo Stoyanov
  • 1,960
  • 1
  • 12
  • 24
  • Thank you for your answer but your solution unfortunately not working. Is it worked for you? – Eido95 Dec 29 '16 at 10:23
  • 1
    You should explain why the code you provide is working and what is it doing. If you provide simply a code, this won't help the OP and the following users that could end up with a similar question. – AxelH Dec 29 '16 at 12:09