Good day. I make a grid grid view to display levels.
GridAdapter.java

 public class GridAdapter extends BaseAdapter { private Context mContext; private ArrayList<Button> b = new ArrayList<>(); private SharedPreferences sharedPref; SharedPreferences.Editor editor; private String level_lock; public GridAdapter(Context c, ArrayList<Button> b, SharedPreferences sharedPref) { mContext = c; this.b = b; this.sharedPref = sharedPref; } public interface Callback{ void onClick(int position); } private Callback callback; public void setListener(Callback callback){ this.callback = callback; } public int getCount() { return b.size(); } public Object getItem(int position) { return null; } public long getItemId(int position) { return 0; } // create a new ImageView for each item referenced by the Adapter public View getView(final int position, View convertView, ViewGroup parent) { Button button; if (convertView == null) { // if it's not recycled, initialize some attributes button = new Button(mContext); button.setLayoutParams(new GridView.LayoutParams(150, 200)); button.setPadding(0, 40, 25, 0); //button.setScaleType(ImageButton.ScaleType.CENTER_CROP); } else { button = (Button) convertView; } button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if (callback != null) { callback.onClick(position); } } }); level_lock = sharedPref.getString("level_" + (position), "lock"); if(level_lock.equals("unlock")) { button.setBackgroundResource(R.drawable.button_select); button.setEnabled(true); button.setTextColor(mContext.getResources().getColor(R.color.colorTextSelect)); Typeface typeface = Typeface.createFromAsset(mContext.getAssets(), "BuxtonSketch.ttf"); button.setTypeface(typeface); button.setTextSize(40); button.setText(String.valueOf(position)); }else { button.setBackgroundResource(R.drawable.icon_lock); button.setEnabled(false); } return button; } } 

I fill this grid with this adapter. At the bottom of the code in the if-else I check the status of the level in SharedPreferences and depending on the state I install a button: either simple with a digit of the level, if not, then a lock without a digit and block the button. So if the level is unlock , then everything is fine, but if it works out else then misunderstand begins: locks on all buttons are set normally, BUT on these buttons, then the level figures skip and some rare buttons with locks are not blocked . "Unclear" numbers are set from a range of unlocked levels (for example, 1, 2, 3, 4, 5 are unlocked, then only these numbers are scattered around the locks). Moreover, if I flush the GridView down and then up again, then the numbers on the locks randomly change!

Can you please tell what can be caused by such anomalies? Thank.

enter image description here

  • You pass an ArrayList<Button> b to the adapter, but why do you never use it in getView(...) ? - post_zeew
  • one
    And: the old View has been returned to you in the form of convertView , if it is not null , then it is already configured. For example, a blocked button is returned to you, and then you draw numbers on it. That is, its initial position corresponded to the locked level, and everything was drawn correctly, but when you need to redraw the list, this (already configured button) returns, and you draw a number on top of it (since the current position corresponds to the locked level). - post_zeew
  • @post_zeew, unfortunately, I am not strong in adapters. Perhaps the collection there is not needed. Apparently, only .size() is taken from it. Setting just a number in getCount() does the same thing. - Pollux
  • @post_zeew, please tell me how it will modify the code correctly? - Pollux
  • I can distribute something approximate, but for RecyclerView . Replaced by ListView and GridView came RecyclerView , which, depending on the LayoutManager can display items as a list (both in ListView ) and a table (as in GridView ). I do not use these obsolete components. So if you are satisfied with the solution with RecyclerView - write something approximate then I will outline. - post_zeew

1 answer 1

The author’s problem is this:

And: the old View has been returned to you in the form of convertView , if it is not null , then it is already configured. For example, a blocked button is returned to you, and then you draw numbers on it. That is, its initial position corresponded to the locked level, and everything was drawn correctly, but when you need to redraw the list, this (already configured button) returns, and you draw a number on top of it (since the current position corresponds to the locked level).

but from the comments it turned out that the author would be satisfied with the decision from scratch , so I publish it as an answer.

The result will be something like this:

enter image description here

First, we determine the data model.

What data is the adapter logic tied to? - on the data on the availability / inaccessibility of the level, therefore, as a data model, we are completely satisfied with a set of objects with one of two possible values ​​of true / false - ArrayList<Boolean> .

Now we think how it all should look visually.

The visual component will be pretty primitive, just a button:

  1. If the level is available, then the button will say Unlocked ; if unavailable, Locked ;
  2. If the level is available, the button will be green; if it is unavailable, blue.

Functional? And what about without him

Add a little bit of some logic:

  1. If you click on the available level, then:
    • if it has an even number , then add another available level to the table (at the end);
    • if it has an odd number , then add another unavailable level to the table (at the end);
  2. When you click on an unavailable level, we will display a notification with the text “ Level locked! ".

I add these actions just for example to explain how to handle clicks on the corresponding table cells.

It's time to start acting

cell_item.xml - layout (or sketch) of a single table cell:

 <?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:padding="4dp"> <Button android:id="@+id/cell_item_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="1dp" android:textSize="10sp"/> </LinearLayout> 

Everything is simple - the usual button wrapped in LinearLayout .

main_activity.xml - layout main view (the table will be displayed in it):

 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin"> <android.support.v7.widget.RecyclerView android:id="@+id/grid_recycler_view" android:layout_width="match_parent" android:layout_height="match_parent"> </android.support.v7.widget.RecyclerView> </LinearLayout> 

Here, too, everything is simple - RecyclerView , wrapped in LinearLayout .

Adapter that will manage our RecyclerView :

 public class GridAdapter extends RecyclerView.Adapter<GridAdapter.CellViewHolder> { ArrayList<Boolean> mLevels; public GridAdapter(ArrayList<Boolean> levels) { mLevels = new ArrayList<>(levels); } @Override public CellViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View layoutView = LayoutInflater.from(parent.getContext()).inflate(R.layout.cell_item, null); CellViewHolder cellViewHolder = new CellViewHolder(layoutView); return cellViewHolder; } @Override public void onBindViewHolder(CellViewHolder holder, int position) { if (mLevels.get(position)) { holder.mButton.setText("Unlocked"); holder.mButton.setBackgroundColor(Color.parseColor("#4AF764")); } else { holder.mButton.setText("Locked"); holder.mButton.setBackgroundColor(Color.parseColor("#7CC8F8")); } } @Override public int getItemCount() { return mLevels.size(); } public void addLevel(boolean isUnlocked) { mLevels.add(isUnlocked); } public class CellViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener{ public Button mButton; public CellViewHolder(View itemView) { super(itemView); mButton = (Button) itemView.findViewById(R.id.cell_item_button); mButton.setOnClickListener(this); } @Override public void onClick(View view) { if (!mLevels.get(getAdapterPosition())) { Toast.makeText(view.getContext(), "Level locked!", Toast.LENGTH_SHORT).show(); } else { if (getAdapterPosition() % 2 == 0) { addLevel(true); } else { addLevel(false); } notifyItemChanged(getItemCount() - 1); } } } } 

When creating an adapter instance, we will pass it a list of ArrayList<Boolean> levels . The position number of the item in the list will be equal to the level number, true if the level is available and false otherwise.

Well, MainActivity , in which I add 10 levels available and 15 inaccessible:

 public class MainActivity extends AppCompatActivity { RecyclerView mRecyclerView; GridAdapter mAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mRecyclerView = (RecyclerView) findViewById(R.id.grid_recycler_view); mRecyclerView.setLayoutManager(new GridLayoutManager(this, 4)); ArrayList<Boolean> levels = new ArrayList<>(); for (int i=0; i<10; i++) { levels.add(true); } for (int i=0; i<15; i++) { levels.add(false); } mAdapter = new GridAdapter(levels); mRecyclerView.setAdapter(mAdapter); } } 

In your case, you will need to fill the levels collection depending on the data in SharedPreferences .

UPD . About ViewHolder :

When you start the application, on the screen you see 40 table cells (that is, 40 View objects). At this stage, 40 + several more onCreateViewHolder(...) created, that is, the onCreateViewHolder(...) method is called for each cell, which creates a View , and the onBindViewHolder(...) method is called for each cell, which fills the cell with specific data.

After that, you scroll down the list, while the upper cells are no longer visible, but new cells appear from below. So, in order not to create new View for cells that appear on the bottom of the screen, use the old View , which left the screen (which were on top). That is, when displaying cells that appear from below, the onCreateViewHolder(...) method is no longer called, but only onBindViewHolder(...) is called, which receives one of the already rendered View , which is no longer visible, redraws this View and displays it from below.

The essence of the ViewHolder pattern is that it holds the link to the View , which will later be redrawn.

And you need this to improve performance and save memory, as if every time you created a new View , each time you would create a new object, the creation of which would run the findViewById(...) method, which is relatively resource-intensive.

  • Thank you for such a beautiful and detailed answer! Immediately, of course, it looks extremely incomprehensible ... I will try to bet tomorrow. The truth is not yet sure how it will work with the boolean collection. It was thought immediately read from SharedPreferences . - Pollux
  • one
    @Pollux, Well, you can read from the SharedPreferences in the adapter. If something is not clear - write. - post_zeew
  • I would have finished correcting a bit, but I really like detailed and well-formed answers. more than + - Shwarz Andrei
  • @ShwarzAndrei, What's wrong? - post_zeew
  • one
    @Pollux; See UPD , I wrote about ViewHolder . - post_zeew