Supposed that I have a view structure like this:
<TableLayout>
<TableRow>
<ToggleButton />
<ToggleButton />
<ToggleButton />
</TableRow>
<TableRow>
<ToggleButton />
<ToggleButton />
<ToggleButton />
</TableRow>
</TableLayout>
My goal is to make toggle buttons act as group of radio button. There were a lots of answer on SOF saying that using RadioGroup. I tried some, they work but not as expected since they break TableRow's formats, say: column fixed width. Then I decided to use a recursive method to check & reset toggle button state as of below:
private void resetChildState(ViewGroup parent, int target) {
int count = parent.getChildCount();
for (int i = 0; i < count; i++) {
View child = parent.getChildAt(i);
if (child instanceof TableRow) {
resetChildState((ViewGroup)child, target);
} else if (child instanceof ToggleButton) {
if ((int)child.getTag() != target) {
((ToggleButton) child).setChecked(false);
}
}
}
}
This works, but another problem came. The respond time is too bad since there were too much work on main thread which resulted in some laggy behaviors.
My question is: is there any other better approach for my situation?
Keep a list of just the ToggleButtons and iterate over that to reset them. Then you won't need to iterate over the entire view hierarchy.
Related
I need to add custom button objects to each row in a ListView. Here's a simplified row layout:
<LinearLayout android:id="#+id/table_cell"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<TextView android:id="#+id/label"
android:textSize="19dp"
android:textStyle="bold"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:lines="1"
/>
<LinearLayout android:id="#+id/button_wrapper"
android:layout_width="100dp"
android:layout_height="match_parent"
/>
</LinearLayout>
In my custom ArrayAdapter, I place the button into the cell in getView():
#Override
public View getView(int position, View convertView, ViewGroup parent) {
// recycle the cell if possible
View cell = null;
if (convertView == null) {
LayoutInflater inflater = (LayoutInflater) this.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
cell = inflater.inflate(R.layout.table_cell, parent, false);
} else {
cell = convertView;
}
MyButton button = (MyButton) this.buttons.get(position);
if (button != null) {
// remove the button from the previous instance of this cell
ViewGroup parent = (ViewGroup)button.getParent();
if (parent != null) {
parent.removeView(button);
}
// add the button to the new instance of this cell
ViewGroup buttonWrapper = (ViewGroup)cell.findViewById(R.id.button_wrapper);
buttonWrapper.addView(button);
}
}
I know that getView() is called multiple times for each table row as I scroll the table or click buttons or do other things, so the code above removes the button from the previous view before adding it to the new view to avoid a "view already has a parent" exception.
The problem is that this assumes the latest view generated from getView is the one that's visible on the screen, but this is often not the case. Sometimes getView() generates new views, but an older view remains on the screen. In that situation, my button disappears because getView() moves it to a new view that is not visible. I discovered that behavior by initializing an int variable named repeatRowTest and then adding this code inside getView():
if (position == 0) {
Log.d("getView", "repeat row count: " + repeatRowCountTest);
TextView label = (TextView)cell.findViewById(R.id.label);
label.setText(String.format("%d %s", repeatRowCountTest, label.getText()));
repeatRowCountTest++;
}
This shows me how many times a given row has been generated, and which instance is currently displayed. I might see a row being generated 10 times, while only the 5th one is displayed. But my buttons will only be visible if the latest instance of the row is displayed.
So the question is, how can I tell whether a row generated in getView() is actually going to be displayed, so I know whether to move my button into it, or leave my button where it is? Or more generally, how can I add a button to a row and make sure it remains visible as getView is repeated for a given position?
I've inspected all the properties of a displayed row versus an extra, non-displayed row, and couldn't find any differences. I also tried calling notifyDataSetChanged on the array adapter after my buttons disappear, and that refreshes the list with all the latest views that contain the buttons -- but it's not clear which events trigger getView to repeat itself, so I wouldn't know when I need to call notifyDataSetChanged to make things right again. I suppose I could clone the button and add a new instance of the button to each new instance of the row, but that seems more resource-intensive than is necessary, and will create other problems since other objects have references to these buttons. I haven't found any code examples showing the best way to do this, but it seems like a common requirement, so hopefully I'm missing something simple!
UPDATE: Is there a method of the ArrayAdapter I can override that is called after the getView() methods are called? If so, I could check the parents of all the recently created rows to see if they are actually displayed in the ListView, and refresh the ListView at that point if they aren't.
You don't need to create your custom button by code, you can insert it inside the row layout xml like a normal android button. In this way you can remove the button wrapper layout and the add/remove logic from getView.
<LinearLayout android:id="#+id/table_cell"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<TextView android:id="#+id/label"
android:textSize="19dp"
android:textStyle="bold"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:lines="1"
/>
<yourpackagename.MyButton
android:id="#+id/button"
android:layout_width="100dp"
android:layout_height="match_parent"
/>
</LinearLayout>
Is simpler to understand with code, but maybe you have to adapt it.
XML:
<LinearLayout android:id="#+id/table_cell"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<TextView android:id="#+id/label"
android:textSize="19dp"
android:textStyle="bold"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:lines="1"
/>
<yourpackagename.MyButton
android:id="#+id/button1"
android:layout_width="12dp"
android:layout_height="match_parent"
/>
<yourpackagename.MyButton
android:id="#+id/button2"
android:layout_width="12dp"
android:layout_height="match_parent"
/>
<yourpackagename.MyButton
android:id="#+id/button3"
android:layout_width="12dp"
android:layout_height="match_parent"
/>
<yourpackagename.MyButton
android:id="#+id/button4"
android:layout_width="12dp"
android:layout_height="match_parent"
/>
<yourpackagename.MyButton
android:id="#+id/button5"
android:layout_width="12dp"
android:layout_height="match_parent"
/>
<yourpackagename.MyButton
android:id="#+id/button6"
android:layout_width="12dp"
android:layout_height="match_parent"
/>
<yourpackagename.MyButton
android:id="#+id/button7"
android:layout_width="12dp"
android:layout_height="match_parent"
/>
<yourpackagename.MyButton
android:id="#+id/button8"
android:layout_width="12dp"
android:layout_height="match_parent"
/>
</LinearLayout>
Model class that you pass to the Adapter:
public class MyRowModel
{
public boolean isButton1Visible;
public boolean isButton2Visible;
public boolean isButton3Visible;
public boolean isButton4Visible;
public boolean isButton5Visible;
public boolean isButton6Visible;
public boolean isButton7Visible;
public boolean isButton8Visible;
}
ViewHolder:
private class ViewHolder {
public MyButton b1;
public MyButton b2;
public MyButton b3;
public MyButton b4;
public MyButton b5;
public MyButton b6;
public MyButton b7;
public MyButton b8;
}
getView method:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
if (convertView == null) {
LayoutInflater inflater = (LayoutInflater) this.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.table_cell, parent, false);
viewHolder = new ViewHolder();
viewHolder.b1 = (MyButton)convertView.findViewById(R.id.button1);
viewHolder.b2 = (MyButton)convertView.findViewById(R.id.button2);
viewHolder.b3 = (MyButton)convertView.findViewById(R.id.button3);
viewHolder.b4 = (MyButton)convertView.findViewById(R.id.button4);
viewHolder.b5 = (MyButton)convertView.findViewById(R.id.button5);
viewHolder.b6 = (MyButton)convertView.findViewById(R.id.button6);
viewHolder.b7 = (MyButton)convertView.findViewById(R.id.button7);
viewHolder.b8 = (MyButton)convertView.findViewById(R.id.button8);
convertView.setTag(viewHolder);
} else {
viewHolder = convertView.getTag();
}
MyRowModel myRowModel = getItem(position);
if(myRowModel.isButton1Visible)
{
viewHolder.b1.setVisibility(View.VISIBLE);
}
else
{
viewHolder.b1.setVisibility(View.INVISIBLE);
}
if(myRowModel.isButton2Visible)
{
viewHolder.b2.setVisibility(View.VISIBLE);
}
else
{
viewHolder.b2.setVisibility(View.INVISIBLE);
}
//and so on
return convertView;
}
I noticed that if I scroll the ListView after the problem occurs, all the rows redraw with the buttons showing, so apparently Android intends to display the latest view for each row, but it isn't always refreshing the view.
Then I tried to figure out what is causing getView to run repeatedly for the currently visible rows (normally it would only run when new rows come into view). Unfortunately, lots of things that are happening elsewhere in this activity are triggering the ListView to regenerate its views, like a ProgressBar that moves as audio plays, an animation that shortens and lengthens the ListView to show another view next to it, and the buttons inside the table rows updating with different graphics to show the status of different things the app is tracking. I was able to eliminate some of this, for example by checking to see if a button is already in the desired state before updating its state, but I can't eliminate all of it.
Since the most frequent action that triggers getView is updating the audio ProgressBar, I added a line to call invalidateViews() on the ListView whenever I update the ProgressBar. That keeps the ListView refreshed so that the latest views always remain visible and therefore my views always remain visible. When running in the debugger, that slows the app down quite a bit, but when running on a standalone device, the performance change isn't noticeable.
Perhaps a better question to ask at this point is why a ProgressBar that isn't related to the ListView causes the ListView to constantly regenerate its views. If I have time or I run into more problems with this, I'll post that as a separate question.
I am trying to achieve something like this.
The Expandable List consists of the names of certain categories and when a parent is clicked, it shows the list of all the children in that category.
Now, suppose I want dynamically add a child to any category ?
How do I do that ?
Do I keep a button with every parent in the list clicking on which would add a new child under it ?
But looking around in different forums, I came to realize that it is not really easy to set a button click handler inside every parent. But if that is the only way, can anyone give me some sample code please ?
I found this thread but wasn't able to implement it in my code.
Android Row becomes Unclickable with Button
Adding a button to the group view shouldn't be that difficult.
I believe the below should work (although I don't have a project using an array backed ExpandableListView to test on).
I don't know your group row layout, so I'll make one up here for reference purposes.
group_layout.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/test"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<TextView
android:id="#android:id/text1"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:focusable="false"
android:focusableInTouchMode="false"
android:gravity="center_vertical"
android:paddingLeft="?android:attr/expandableListPreferredItemPaddingLeft"
android:textAppearance="?android:attr/textAppearanceLarge" />
<Button
android:id="#+id/addbutton"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:focusable="false"
android:focusableInTouchMode="false"
android:text="Add"
android:textSize="12dp" />
</LinearLayout>
Then in your getGroupView method from your adapter:
public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
if (convertView == null) {
View convertView = View.inflate(getApplicationContext(), R.layout.group_layout, null);
Button addButton = (Button)convertView.findViewById(R.id.addButton);
addButton.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View view) {
// your code to add to the child list
}
});
}
TextView textView = (TextView)convertView.findViewById(R.id.text1);
textView.setText(getGroup(groupPosition).toString());
return convertView;
}
I'm having a weird problem, in my rather complex view layout. (I will try to simplify it a bit in my explanation)
Basically I have a ListView, where each item consists of a TextView and an ImageButton. I am able to either click the list item (on the textview), or the button (I set the ImageButton to non-focusable, otherwise it wouldn't work)
Now it seems to work fine, until I open another window and return to the listview.
From that point on, I can click the ImageButton without anything happening (not even the background changes during the click). But when I click on the TextView again, all the click events from the ImageButton are dispatched at once.
Why is that?
EDIT:
The List Item:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="0px"
android:minHeight="40dp"
android:orientation="horizontal"
android:paddingLeft="2px"
android:paddingRight="2px"
>
<TextView
android:id="#+id/text1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Text"
android:textSize="19dp"
android:paddingTop="4px"
android:paddingBottom="4px"
android:layout_gravity="center_vertical"/>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/open_subtree_layout"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_margin="0px"
android:orientation="horizontal"
android:padding="0px">
<View
android:layout_width="1px"
android:layout_height="match_parent"
android:background="#drawable/separator_line" />
<com.treeviewer.leveldisplay.DontPressWithParentImageButton
android:id="#+id/btn_right"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:background="#drawable/list_selector_background"
android:focusable="false"
android:focusableInTouchMode="false"
android:padding="10dp"
android:src="#drawable/arrow_right" />
</LinearLayout>
</LinearLayout>
That's how it is inflated:
[...]
LayoutInflater inflater = (LayoutInflater)mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mView = inflater.inflate(R.layout.tree_row, null, false);
TextView textView = (TextView)mView.findViewById(R.id.text1);
LinearLayout nextNodeButtonContainer = (LinearLayout)mView.findViewById(R.id.open_subtree_layout);
if(childCount >= 0) {
titleBuilder.append(" (" + childCount + ")");
nextNodeButtonContainer.setVisibility(View.VISIBLE);
View button = nextNodeButtonContainer.findViewById(R.id.btn_right);
button.setFocusable(false);
button.setFocusableInTouchMode(false);
//button.setClickable(true);
button.setOnClickListener(clickListener);
button.setTag(tagValue);
} else {
nextNodeButtonContainer.setVisibility(View.GONE);
}
textView.setText(titleBuilder);
Let me know, if you need more code.
Ok, I finally solved this problem.
Unfortunately, in my question I didn't provide the necessary information to solve it, as the problem was somewhere I didn't expect it:
I have a ListAdapter where the getView method looks like this:
public View getView(int position, View convertView, ViewGroup parent) {
return mNodes.get(position).getView(mNodeArrowClickListener, position);
}
And the getView method of the nodes (TreeLevelElements) looked like:
public class TreeLevelElement {
private final Context mContext;
private View mView = null;
//[...] other methods
View getView(OnClickListener clickListener, final int tagValue) {
if(mView == null) {
//[...] produce a new View from XML
}
return mView;
}
}
The problem was, that I stored the Views in my elements, so I guess that conflicted somehow with android strategy to reuse old views for new items.
I don't know what exactly happened, but now that I removed mView and create a new one every time, it works.
I will also change it to reuse the convertView instead.
I am trying to use this Android: How to get a radiogroup with togglebuttons? code of the given answer
but in
static final RadioGroup.OnCheckedChangeListener ToggleListener = new RadioGroup.OnCheckedChangeListener() {
#Override
public void onCheckedChanged(final RadioGroup radioGroup, final int i) {
for (int j = 0; j < radioGroup.getChildCount(); j++) {
final ToggleButton view = (ToggleButton) radioGroup.getChildAt(j);
view.setChecked(view.getId() == i);
}
}
};
in line
final ToggleButton view = (ToggleButton) radioGroup.getChildAt(j);
it always crashes. In Logcat I don't see any message.
I tried everything I could think of , but can't fid the problem -
Many thanks!
ps this is my xml for the radiogroup:
<RadioGroup android:id="#+id/radioGroup2" android:layout_width="150sp" android:layout_height="wrap_content"
android:paddingLeft = "10sp" android:layout_alignBottom="#+id/a2" >
<RadioButton android:layout_width="wrap_content" android:id="#+id/Settings_otherSettingsT2Yes" android:layout_height="wrap_content"
android:textColor="#000000" android:textSize="18sp"
android:text="#string/Settings_otherSettingsT2Yes" android:checked="false"></RadioButton>
<RadioButton android:layout_width="wrap_content" android:id="#+id/Settings_otherSettingsT2No" android:layout_height="wrap_content"
android:textColor="#000000" android:textSize="18sp"
android:text="#string/Settings_otherSettingsT2No"></RadioButton>
</RadioGroup>
...nothing special here
I checked the number of children by logging radioGroup.getChildCount() and it gives 2 as expected
In your code you cast RadioButtons to ToggleButton. This is likely to be the cause of your crashes. I don't understand why you can't find an exception in logcat.
However, you say you want to use code like in the link, but your XML is not close to the link. You use RadioButtons where the link uses ToggleButtons.
If you just want RadioButtons, then completely disregard that link.
If you want to use ToggleButtons then that link isn't the best way anyway. Adding ToggleButtons to a RadioGroup just confuses the intent of the code.
I'm trying to do an "Unselect all" button in a ListActivity to unchecked all checkboxes in a ListView managed by a custom SimpleCursorAdapter.
As suggested here, I tried
In my ListActivity I have:
Button bt_f_unsel = (Button) findViewById(R.id.btn_f_unsel);
bt_f_unsel.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
for ( int i=0; i< getListAdapter().getCount(); i++ ) {
mListView.setItemChecked(i, false);
}
}
});
but nothing happens.
I'm wondering if this is because of my custom row:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent" android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="#+id/contact_pic"
android:layout_width="50dp"
android:layout_height="50dp" />
<TextView
android:id="#+id/contact_name"
android:textSize="10sp"
android:singleLine="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<CheckBox
android:id="#+id/checkbox"
android:button="#drawable/whipem_cb"
android:layout_alignParentRight="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
which makes mListView.setItemChecked() not find the checkbox.
How can I uncheck all cb and refresh all the rows from a button in my ListActivity?
Thanks
I'm using a dirty but easy trick:
//recursive blind checks removal for everything inside a View
private void removeAllChecks(ViewGroup vg) {
View v = null;
for(int i = 0; i < vg.getChildCount(); i++){
try {
v = vg.getChildAt(i);
((CheckBox)v).setChecked(false);
}
catch(Exception e1){ //if not checkBox, null View, etc
try {
removeAllChecks((ViewGroup)v);
}
catch(Exception e2){ //v is not a view group
continue;
}
}
}
}
Pass your list object to it. Just avoid really long and complicated lists.
Honestly I don't think the setChecked Methods will work with a custom layout. It expects the view to be a CheckedTextView with an id of text1.
And since the views are recycled I think the solution is to update whatever boolean in your objects in the list that determines if the checkbox is checked and then call adapter.notifyDataSetChanged(). You are changing the boolean state of the data (which is what really matters) and telling the Adapter to update the ListView. So the next time the views are drawn the checkbox will be checked correctly. And the current views that are shown will be redrawn.
This worked for me:
MenuViewAdapter adapter = new MenuViewAdapter(this, menuViews,this);
ListView lv = (ListView)this.findViewById(R.id.menu_list);
CheckBox cb;
for(int i=0; i<lv.getChildCount();i++)
{
cb = (CheckBox)lv.getChildAt(i).findViewById(R.id.checkBox);
cb.setChecked(false);
}
adapter.notifyDataSetChanged();
I use
checkbox.setChecked(false);
checkbox.refreshDrawableState();