I'm having trouble rendering a custom view insider recycler view widget. Traditionally we inflate the view inside RecylerView.Adapter like
public RowLayoutViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.sample_view, viewGroup, false);
RowLayoutViewHolder rowLayoutViewHolder = new RowLayoutViewHolder(view);
return rowLayoutViewHolder;
}
This works fine and we can bind the data to the view inside onBindViewHolder(...) method. But when I try to create subclass of ViewGroup and use that like this, I get a blank ("black") screen.
public ImageGalleryAdapter.RowLayoutViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
SampleView view = new SampleView(context);
RowLayoutViewHolder rowLayoutViewHolder = new RowLayoutViewHolder(view);
return rowLayoutViewHolder;
}
Here's my SampleView class -
public class SampleView extends ViewGroup {
public SampleView(Context context) {
super(context);
initView();
}
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
child.layout(l, t, l + 600, t + 200);
}
}
private void initView() {
inflate(getContext(), R.layout.sample_view, this);
TextView textView = (TextView) findViewById(R.id.sampleTextView);
textView.setTextColor(Color.WHITE);
textView.setText("Hello from textview");
}
}
And here is the layout -
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:background="#color/white"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="#+id/sampleTextView"
android:text="Sample TextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
Looking at the source code it seems like inflating the layout behaves exactly same as creating a view, except the inflation process adds a suitable LayoutParameter to the child view based on the rootView. I've tried adding that manually but without any result.
Any help will be appreciated.
It's pretty simple. If you see the way you add view by inflating it, you would realize that you are adding it to the parent (ViewGroup) but not attaching it. In this process, default LayoutParams are generated and set to your view. (Check LayoutInflater source)
SampleView view = new SampleView(context);
view.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
This is how it should be. Even the following seems to be working.
viewGroup.add(view, new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
I've successfully solved the issue in a roundabout way that might help
In the custom View, say SampleView, in whatever method you wish to modify the layout (I guess here onLayout()), register
getViewTreeObserver().addOnGlobalLayoutListener(new LayoutViewTreeObserver())
in the callback for LayoutViewTreeObserver:
#Override
public void onGlobalLayout() {
// Do layout stuff here
}
Also, as a side note, don't forget to remove the GlobalOnLayoutListener before the modifications so the callback doesn't get anymore necessary calls (in practice, I've had to call getViewTreeObserver() instead of keeping a reference to the observer as a lot of times a reference stops being "alive" and throws an error when I try to unregister it).
While the above worked for me, I realized that it is far from ideal since it relies on the fact that the custom view generally obtain the LayoutParams of the container class, which if used elsewhere will not be RecyclerView.LayoutParams, thus causing an error. However, I have found that using an XML layout with just the CustomView as a base tag in the XML and inflating/binding the standard way works as expected. It has something to do with how RecyclerView attached the view and the measurement(s) done before, but I have yet to do a deep dive to discover why exactly.
I'm used to the initial methodology, so I'm not sure about what's happening.
have you tried calling initView from onLayout() method?
In your onCreateViewHolder() try setting the params to the view you've created and for width set it to parent.getMeasuredWidth() see if that helps, it solved my problem.
Related
I want to implement custom ViewGroup in my case derived from FrameLayout but I want all child views added from xml to be added not directly into this view but in FrameLayout contained in this custom ViewGroup.
Let me show example to make it clear.
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:id="#+id/frame_layout_child_container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<FrameLayout
android:id="#+id/frame_layout_top"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</merge>
And I want to redirect adding all child view to FrameLayout with id frame_layout_child_container.
So of course I overrode methods addView() like this
#Override
public void addView(View child) {
this.mFrameLayoutChildViewsContainer.addView(child);
}
But for sure this doesn't work because for this time mFrameLayoutChildViewsContainer is not added to the root custom view.
My idea is always keep some view on on the top in this container frame_layout_top and all child views added into custom component should go to frame_layout_child_container
Example of using custom view
<CustomFrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"/>
</CustomFrameLayout>
So in this case TextView should be added to the frame_layout_child_container
Is it possible to delegate adding all views into child ViewGroup like I described.
I have other ideas like using bringToFront() method every time view is added to keep them in correct z-axis order or for example when view is added, save it to array and than after inflating custom view add all views to this child FrameLayout
Suggest what to do in this case in order not to hit performance with reinflating all layout every time new view is added, if it is possible to implement in other way.
Views inflated from a layout - like your example TextView - are not added to their parent ViewGroup with addView(View child), which is why overriding just that method didn't work for you. You want to override addView(View child, int index, ViewGroup.LayoutParams params), which all of the other addView() overloads end up calling.
In that method, check if the child being added is one of your two special FrameLayouts. If it is, let the super class handle the add. Otherwise, add the child to your container FrameLayout.
public class CustomFrameLayout extends FrameLayout {
private final FrameLayout topLayout;
private final FrameLayout containerLayout;
...
public CustomFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.custom, this, true);
topLayout = (FrameLayout) findViewById(R.id.frame_layout_top);
containerLayout = (FrameLayout) findViewById(R.id.frame_layout_child_container);
}
#Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
final int id = child.getId();
if (id == R.id.frame_layout_top || id == R.id.frame_layout_child_container) {
super.addView(child, index, params);
}
else {
containerLayout.addView(child, index, params);
}
}
}
I have taken an example code for implementing a RecyclerView, but trying to transpose it to work in a Child Fragment in my app.
The code is under 'Create Lists - examples'
Creating Lists and Cards
I run into trouble with my adapter..
public static class ViewHolder extends RecyclerView.ViewHolder {
// each data item is just a string in this case
public TextView mTextView;
public ViewHolder(TextView v) {
super(v);
mTextView = v;
}
}
> #Override
> public MyAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,
> int viewType) {
> View v = LayoutInflater.from(parent.getContext())
> .inflate(R.layout.my_text_view, parent, false);
> ViewHolder vh = new ViewHolder(v);
> return vh;
> }
First, it doesn't build, complaining that I am calling constructor ViewHolder with a View, when the constructor is expecting a TextView.
Looking at the code, I agree!
But this is an official example so it must be right?
So what is different to my version compared with the example?
Two things that I can think of...
1)
the layout my_text_view is not given in the example, so I made my own. Did I make it right?
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="#+id/t_title"
android:title="title"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
2)
I am calling the adapter from my child fragment, and the example was not written with fragments in mind.
I've probably bitten off more than I can understand there.
Still, working this through, as far as i understand.
The 'parent' coming into my OnCreateViewHolder, is my RecyclerView ?
'v' should be the LinearLayout of my 'my_text_view' ?
'ViewHolder' should end up being a class with property mTextView equaling the TextView from my xml.
I just don't see how I go from v=LinearLayout , to TextView??
Anyone like to help explain to this noob ??
That example is not very good. It looks like two different projects spliced together. It should be something like this:
public class MyAdapter extends RecyclerView.Adapter {
public static class ViewHolder extends RecyclerView.ViewHolder {
public TextView mTextView;
public ViewHolder(View v) {
super(v);
mTextView = (TextView) v.findViewById(R.id.t_title);
}
}
#Override
public MyAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.my_text_view, parent, false);
ViewHolder vh = new ViewHolder(v);
return vh;
}
}
To explain this segment:
onCreateViewHolder is the method that will be called any time the RecyclerView needs to create a new type of ViewHolder. This may get called only a dozen or so times to get enough Views to cover the screen or it may be called quite a few times if you have many view types.
The parameter parent in this case will be the RecyclerView that accompanies it. Why not just make it RecycerView? Because Google designers decided it should be a ViewGroup. Also it's an Adapter pattern so the only guarantee you're supposed to have is that it's a ViewGroup (i.e., may not be a RecyclerView so you shouldn't build the Adapter with that assumption). Realistically, it will pretty much always be a RecyclerView though.
The parameter int viewType is to tell you what kind of View you're building. This is determined if you override getItemViewType() method of the Adapter. You don't need to worry about this if you only have one type of View though.
For the ViewHolder, this is to basically cache the different types of Views in your layout. These can be ImageView, TextView, etc. These will be "recycled" constantly as the user scrolls so you're not always creating them. That way, you only have to update the Views with the current information. In this case, there's just a title. The ViewHolder passed in bindViewHolder will be where the actual updating happens. This is called all the time, so there's no need to initialize the Views in onCreateViewHolder. Just need to create them.
This might be a little bit hard to explain, so the best way I can think of, is providing you a Video showing up the issue.
In the Video I show myself scrolling listview, and after 5 seconds, a View is created and added inside that holder in the bottom. In that moment, listview is refreshed.
http://tinypic.com/player.php?v=vpz0k8%3E&s=8#.U0VrIvl_t8E
The issue is the following:
I've an Activity with a layout that consists of a:
Fragment (above RelativeLayout), match parent, match parent.
RelativeLayout, as wrap content.
The fragment displays a ListView with animations for every row.
If I add a View on the "RelativeLayout", it makes the fragment to readjust to the new size, as it's set above this RelativeLayout, so every Row is rebuilt again.
Do you guys think in any way to avoid this?
EDIT: Sourcecode:
https://bitbucket.org/sergicast/listview-animated-buggy
Don't start the animation if the layout process for the added footer view is running. The end of the layout process can be determined using the ViewTreeObserver (the start obviously starts with adding the footer view):
hand.postDelayed(new Runnable() {
#Override
public void run() {
ViewTreeObserver viewTreeObserver = holder.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
#SuppressWarnings("deprecation")
#Override
public void onGlobalLayout() {
holder.getViewTreeObserver().removeGlobalOnLayoutListener(this);
mIgnoreAnimation = false;
}
});
mIgnoreAnimation = true;
holder.addView(viewToAdd);
}
}, 5000);
Add this method to your Activity:
public boolean ignoreAnimation() {
return mIgnoreAnimation;
}
And check it in your Fragment:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
Context context = FragmentTest.this.getActivity();
TextView tv = new TextView(context);
tv.setText("Pos: " + position);
tv.setTextSize(35f);
if (runAnimation()) {
Animation anim = AnimationUtils.loadAnimation(context, R.anim.animation);
tv.startAnimation(anim);
}
return tv;
}
private boolean runAnimation() {
Activity activity = getActivity();
if (activity != null && activity instanceof MainActivity) {
return ! ((MainActivity)activity).ignoreAnimation();
}
return true;
}
Of course the whole Activity - Fragment communication can be improved considerably but the example gives you the idea how to solve the problem in general.
While it prevents the animation from being started, it doesn't prevent the ListView from being refreshed although the user won't notice. If you are concerned about performance you can improve the Adapter code by re-using the views:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
Context context = FragmentTest.this.getActivity();
TextView tv = null;
if (convertView != null && convertView instanceof TextView) {
tv = (TextView) convertView;
}
else {
tv = new TextView(context);
}
Yes, I can think of a possible way to solve this.
Your problem is:
You have set layout params of your holder to wrap_content. By default, when it has no content, it is "zero-sized" somewhere in the bottom and invisible to you (not invisible in terms of Android, though, sic!)
When you add a View to this holder, the framework understands, that the size of your holder container is different now. But this container is a child of another container - your root RelativeLayout, which, in turn, contains another child - your <fragment>.
Thus, framework decides, the root container alongside with its children should get laid out again. That's why your list gets invalidated and redrawn.
To fix the issue with list getting invalidated and redrawn, simply specify some fixed layout parameters to your holder. For example:
<RelativeLayout
android:id="#+id/holder"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true" >
</RelativeLayout>
That will prevent the list from being redrawn. But in that case you'll get your holder displayed from the very beginning.
Yes. This is the expected behavior of RelativeLayout
You are adding the ListView Fragment and TextView into a RelativeLayout, So whenever there is a change in the child view dimension, will affect the other child in the RelativeLayout.
So here when you add a new TexView , the other child Fragment is affected even though its height is match_parent.
You can fix this only by changing the parent layout to LinearLayout.
I deleted almost all code in my project to find a hiding bug. There was a GridView that containing a frame layout, and the layout contained CheckBox. But I couldn't check the first check box.(others worked)
Finally (I think) I found an answer. But this is so weird. When I deleted lines for recycling convertView, the bug was gone. I changed from :
if(convertView == null) {
layout = (FrameLayout)View.inflate(maincon, R.layout.taste_brand, null);
} else {
layout = (FrameLayout) convertView;
}
to FrameLayout layout = (FrameLayout)View.inflate(maincon, R.layout.taste_brand, null);.
I really have no idea of this stuation. I attach rest codes.
TasteGridAdapter.java:
public class TasteGridAdapter extends BaseAdapter {
Context maincon;
public TasteGridAdapter(Context context) {
maincon = context;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
FrameLayout layout;
if(convertView == null) {
layout = (FrameLayout)View.inflate(maincon, R.layout.taste_brand, null);
} else {
layout = (FrameLayout) convertView;
}
layout.setLayoutParams(new GridView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT));
return layout;
}
#Override
public int getCount() {
return 3;
}
#Override
public Object getItem(int position) {
return position;
}
#Override
public long getItemId(int position) {
return position;
}
}
onCreate of the activity :
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.taste);
TasteGridAdapter adapter = new TasteGridAdapter(this);
GridView grid = (GridView) findViewById(R.id.taste_grid);
grid.setAdapter(adapter);
}
taste.xml:
<?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">
<GridView
android:layout_weight="1"
android:id="#+id/taste_grid"
android:layout_width="match_parent"
android:layout_height="0dip"
android:columnWidth="87dip"
android:gravity="center"
android:horizontalSpacing="4dip"
android:numColumns="auto_fit"
android:padding="2dip"
android:stretchMode="columnWidth"
android:verticalSpacing="4dip" />
</LinearLayout>
taste_brand.xml:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="87dp"
android:layout_height="58dp">
<CheckBox
android:id="#+id/taste_brand_check"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</FrameLayout>
I encountered similar problem with first item in GridView. To resolve issue, remove 'new' keyword, and change existing views LayoutParams like that:
LayoutParams lp = layout.getLayoutParams();
lp.height = someHeight;
...do something with these LayoutParams. This hack resolves my issues. Conclusion, try to avoid creation of new LayoutParams object through "new".
layout.setLayoutParams(new GridView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT));
put this line in this condition,
if(convertView == null) {
}
I was faced same problem, but i try this and its work for me. I hope it also work for you.
What you're experiencing has to do with the way Android recycles views in ListView, GridView, etc. You mention that your first checkbox is uncheckable, while your others remain working. I think you'll notice that the others only appear to work properly, since you haven't handled the recycling properly.
The reason your line
FrameLayout layout = (FrameLayout)View.inflate(maincon, R.layout.taste_brand, null);
seems to fix the problem is because this now inflates the views again each time they are used. I'll admit, when I started with this, re-inflating the views seemed to be the best solution; it entirely defeats the purpose of recycling, however, and you lose all the performance benefits otherwise gained.
So now to fix your problem:
First, I highly recommend using the ViewHolder pattern in conjunction with your BaseAdapter. More information on that can be found here.
Second, you should probably create a boolean array to match all the items in your GridView, and use it to determine whether or not an item should be clicked. Set the value of the corresponding boolean inside your checkbox listener and use that value inside getView(..) to check or uncheck that particular box.
An overall better solution might be to use an array (or list) of models inside your adapter class, each of these containing a boolean field accessible through isChecked and setChecked(boolean). Again, you would use this inside your getView(..) to display the views properly and change the value inside your checkbox OnCheckedChangeListener.
Hope that helps.
As jonstaff says, it's to do with View recycling.
If you're using a custom Adapter class for your GridView View binding, try modifying its getView() method to always instantiate a new View like:
public View getView(int position, View convertView, ViewGroup parent){
SomeView v = new SomeView(context); // <--- here
...
return v;
}
Instead of the typical:
public View getView(int position, View convertView, ViewGroup parent){
SomeView v;
if (convertView == null)
v = new SomeView (context);
else
v= (SomeView)convertView;
...
return v;
}
This may affect performance, but it solved my problem for a small GridView of Buttons.
Problem:
I've developed a custom compound view and I'm unsure of how to display it in my listview.
What I've done:
-> My custom compound view
public class HZScrollView extends LinearLayout {
public HZScrollView(Context context) {
super(context);
initView(context);
}
private void initView(Context context) {
mContext = context;
setOrientation(LinearLayout.HORIZONTAL);
setGravity(Gravity.CENTER_VERTICAL);
ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
this.setLayoutParams(lp);
//inflate XML resource and attach
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mInflater.inflate(R.layout.hz_scroll_view, this, true);
}
}
public void addContent(String name, String age, String sex) {
//content is added to the individual widgets within this compound view
}
-> My Adapter
public View getView(int position, View convertView, ViewGroup parent) {
if(convertView == null) {
convertView = new HZScrollView(context); //<--- PROBLEM !
}
}
The major problem I'm experiencing is that the line marked with "PROBLEM" causes the exception java.lang.RuntimeException: Unable to start activity ComponentInfo{com.my.app/.MainActivity}: android.view.InflateException: <merge /> can be used only with a valid ViewGroup root and attachToRoot=true
In sample code around the 'net, the getView() usually inflates an XML layout, but in my case the compound view is completely self-contained.
Question:
How is it possible to insert/attach my custom compound view into the listview item ?
Solution:
1) in the adapter, assigning the HZScrollView to convertView is fine
2) to fix another problem with LayoutParams, the initView() needed to be updated to use AbsListView.LayoutParams instead of ViewGroup.LayoutParams (since the parent container is a listView)
3) to fix the InflateException, child views used <merge> in the XML, I refactored that to wrap the child views in LinearLayout's. Note: using <merge> in the XML for "hz_scroll_view" file is just fine.
The really interesting part, for me, was point #1 because I was unsure whether assigning a custom compound view to a listview item would work.