mattpic mattpic - 6 months ago 11
Android Question

Using loop index in an Android OnClickListener anonymous class to create a grid of ImageButtons

I am creating an Android application for a card game. Basically, I have it so there are four rows of three card on the screen at a time, each card being represented by a pressable

ImageButton
. My problem is that I want to use a for loop so I can create
OnClickListeners
for these twelve cards, while having a way to keep track of what number card we are on so I can properly keep track of which button is pushed. In the code below, I use counter to keep track of how many cards are pushed, and
pressed_index
to keep track of the three cards that are pushed. If counter doesn't equal three, I change the color filter on the
ImageButton
to make it appear pushed. However, when I set anonymous class, I cannot use the x variable because it is not allowed in the anonymous class. I need to be able to set the
pressed_index
to the value of the current card, otherwise I'll have no way of knowing which of the 12 cards is being pushed.

ImageButton[] hand = new ImageButton[12];
int[] pressed_index = new int[3];
int counter = 0;
for (int x = 0; x < 12; x++) {
hand[x] = (ImageButton)findViewById(R.id.//card_1, card_2, etc.);
hand[x].setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
pressed_index[counter] = x;
counter++;
if (counter != 3) {
((ImageView) v).setColorFilter(Color.argb(150, 155, 155, 155));
}
}
});
}


The alternative, which is how I have it now and the only way I can think to do it otherwise is to hard-code all twelve methods and physically putting in each number. This is very inefficient though, and something that makes it very hard to update the code. The problem of setting the ID correctly isn't bad, I simply get the ID from a string constructed from the x, like so...

int ID = getResources().getIdentifier(("card_"+(x+1)),"id","com.example.project");
hand[x] = (ImageButton)findViewById(ID);


...similar to the solution found here: Android: Using findViewById() with a string / in a loop
However, the problem of using the x is unsolvable by me up to this point, even after much research. Through posts like Accessing variables from onclicklistener and Android: Accessing a global Variable inside an onClickListener, it seems only
final
variables can be passed, and one common fix is to create a separate class instead of using an anonymous class. This, however, didn't lead me very far, as when I began writing the separate class for the
OnClickListener
, I ran into the same problem, as I couldn't pass the variable to create the
OnClickListener
to match it up to the correct card. Please let me know if you have any ideas as to how I can fix this.

Answer

The regular Java/Android the idiom would be

ImageButton[] hand = new ImageButton[12];
int[] pressed_index = new int[3];
int counter = 0;
for (int x = 0; x < 12; x++) {
    final int x_ = x;
    hand[x] = (ImageButton)findViewById(R.id.//card_1, card_2, etc.);
    hand[x].setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
             pressed_index[counter] = x_;
             counter++;
             if (counter != 3) {
                 ((ImageView) v).setColorFilter(Color.argb(150, 155, 155, 155));
             }
        }
    });
}

that is, you introduce a helper final variable, assign the iterator to it and then reference the final helper in the anonymous inner class; you may say you're "capturing" the variable this way. While it's quite similar to how you close over variables in lambda syntax, blame Java design for such explicit code flavouring sigh.

Alternatively, note that while the references of variables used in anonymous class has to be final final, that doesn't mean you can't change the referenced object; e.g.

final Foo foo = new Foo();
bar.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
            foo.doBaz();
        }
});

is completely valid code - you can then change the state of foo in your doBaz() method freely. Hell, you even can (serious code smell - don't try this at home!)

ImageButton[] hand = new ImageButton[12];
int[] pressed_index = new int[3];
int counter = 0;
for (int[] x = new int[]{0}; x[0] < 12; x[0]++) {
    hand[x[0]] = (ImageButton)findViewById(R.id.//card_1, card_2, etc.);
    hand[x[0]].setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
                pressed_index[counter] = x[0];
                counter++;
                if (counter != 3) {
                    ((ImageView) v).setColorFilter(Color.argb(150, 155, 155, 155));
                }
        }
    });
}