I would like to have the listview in a ListActivity be displayed with the header and footer visible all the time even if the list data is empty.
An empty list causes the empty view to appear and the header and footer to disappear. However my header has filtering UI and should therefore always be visible.
The only way I can make it happen at the moment is if I take the header and footer out of the listview and implement them as static views outside in the activity layout. However then these are always visible and only the data scrolls.
I would prefer for both to just be on top and bottom of the scrolling list. Wrapping it all in a scroll view does not work since then there are two nested scroll views( the list view outside and the wrapping one).
Is there a way to do this nicely apart from a hack like adding a fake record?
Make your empty view include your header.
i.e.
<ListView />
<RelativeLayout
android:id="#android:id/empty"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
// Add whatever you want here including your header
</RelativeLayout>
This answer is a bit late, but you can simply override BaseAdapter.isEmpty() to return false.
public boolean isEmpty() {
return false;
}
You could use my MergeAdapter for this. Add the header view, add the data adapter, and add the footer view. They will all scroll in unison, and the header and footer will persist even if the adapter in the middle has no rows.
Ok, this workaround is a hack. But, without having to create a fake item.
For an ArrayAdapter you can add a null item... i.e.
myAdapter.add(null);
And, in the MyAdapter getView(..) you do something like:
myItem = getItem(position);
if (myItem == null) {
return getLayoutInflater().inflate(R.layout.my_empty_row, null);
} else {
...
}
I found that header/footer will always be visible if you don't call ListView.setEmptyView.
Instead, manually show/hide your empty view:
emptyView.setVisibility(list.size() == 0 ? View.VISIBLE : View.GONE);
I meet the same issue, if ListView has no item, you could ListView.setAdapter(new SomeAdapter(new ArrayList())); and the header is also visible.
In my case I had both header and footer views.
I placed my mEmptyView in footer view.
First, as user1034295 mentioned, I overrided adapter's isEmpty() method:
#Override
public boolean isEmpty() {
// If not overridden, then this will return `true` if your list is empty
// Thus, you won't see header/footer views
return false;
}
ExpandableListView adapter's getGroupCount(). Override appropriate method for your adapter.
#Override
public int getGroupCount() {
int size = null != mData ? mData.size() : 0;
if (0 == size) mHostFragment.toggleEmptyView(true);
else mHostFragment.toggleEmptyView(false);
return size;
}
In you activity/fragment
public void toggleEmptyView(boolean show) {
if(show && mEmptyView.getVisibility() != View.VISIBLE)
mEmptyView.setVisibility(View.VISIBLE);
else if (!show && mEmptyView.getVisibility() != View.GONE)
mEmptyView.setVisibility(View.GONE);
}
mEmptyView is a child in ListView's footer view.
So, when your list becomes 0 sized you'll get your header/footer views remained + mEmptyView will become visible.
You can put the listView and the empty layout together in a FrameLayout, and put
the empty layout margin in the size of the header, then, you can change the visibility of the empty view when an item is added to listview.
layout will be as similar to this:
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1">
<ListView
android:id="#+id/myListView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listheader="#layout/myHeader"/>
<LinearLayout
android:layout_marginTop="#dimen/headerHeight"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/white"
android:id="#+id/emptyLayout">
//insert your layout here
</LinearLayout>
</FrameLayout>
Related
I have a recycler view and a relative layout below recycler view. My relative layout consists of three text views.My recycler view consists of three text views and a button. My problem is recycler view is scrolling separately and textviews in relative layout are fixed. But I want both to be scrolled, which means while scrolling the screen scroll should be done for both recycler view and relative layout but not seperately. While scrolling my relative layout should be attached to the end of recycler view. I have searched a lot for doing that but there is no results for my search. So, ended up here please anybody help me out.
You have 2 options, first one (the better one) is to create a footer ViewHolder and add it to RecyclerView as a last item in adapter.
Or you can simply wrap your views in vertical LinearLayout and then wrap it in NestedScrollView like this:
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:id="#+id/image"
android:layout_width="match_parent"
android:layout_height="200dp"
android:scaleType="centerCrop" />
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false" />
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
In order to get your RelativeLayout to scroll with the content of your RecyclerView, you'd need to add the RelativeLayout with static content to the end of the list your Adapter iterates. You'd then override getItemViewType in the adapter and return one type ID for the data in your RecyclerView and another for the footer RelativeLayout. Then in onCreateViewHolder you'd use the view type to inflate the right kind of view (one that binds your data or another that displays your RelativeLayout).
This process can be pretty labor intensive. You might also consider using a library like Epoxy to help create a footer view in your RecyclerView.
You can add view with text views in different layout and add to your recycler as last element. Then check posotion in getItemViewType and if it last return footer type inside RecyclerAdapter.
private static final int FOOTER = 1;
private static final int CHILD = 2;
// inflates the row layout from xml when needed
#Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if(viewType == CHILD){
View view = mInflater.inflate(R.layout.list_item, parent, false);
return new ViewHolder(view);
}
else if(viewType == FOOTER ){
View view = mInflater.inflate(R.layout.relative, parent, false);
return new ViewHolder(view);
}
return null;
}
#Override
public int getItemViewType(int position) {
if (position == getItemCount() - 1) {
return FOOTER;
} else {
return CHILD;
}
}
I have a pretty standard RecyclerView with a vertical LinearLayoutManager. I keep inserting new items at the top and I'm calling notifyItemInserted(0).
I want the list to stay scrolled to the top; to always display the 0th position.
From my requirement's point of view, the LayoutManager behaves differently based on the number of items.
While all items fit on the screen, it looks and behaves as I expect: The new item always appears on top and shifts everything below it.
However, as soon as the no. of items exceeds the RecyclerView's bounds, new items are added above the currently visible one, but the visible items stay in view. The user has to scroll to see the newest item.
This behavior is totally understandable and fine for many applications, but not for a "live feed", where seeing the most recent thing is more important than "not distracting" the user with auto-scrolls.
I know this question is almost a duplicate of Adding new item to the top of the RecyclerView... but all of the proposed answers are mere workarounds (most of them quite good, admittedly).
I'm looking for a way to actually change this behavior. I want the LayoutManager to act exactly the same, no matter the number of items. I want it to always shift all of the items (just like it does for the first few additions), not to stop shifting items at some point, and compensate by smooth-scrolling the list to the top.
Basically, no smoothScrollToPosition, no RecyclerView.SmoothScroller. Subclassing LinearLayoutManager is fine. I'm already digging through its code, but without any luck so far, so I decided to ask in case someone already dealt with this. Thanks for any ideas!
EDIT: To clarify why I'm dismissing answers from the linked question: Mostly I'm concerned about animation smoothness.
Notice in the first GIF where ItemAnimator is moving other items while adding the new one, both fade-in and move animations have the same duration. But when I'm "moving" the items by smooth scrolling, I cannot easily control the speed of the scroll. Even with default ItemAnimator durations, this doesn't look as good, but in my particular case, I even needed to slow down the ItemAnimator durations, which makes it even worse:
Although I wrote this answer and this is the accepted solution, I suggest a look at the other later answers to see if they work for you before attempting this.
When an item is added to the top of the RecyclerView and the item can fit onto the screen, the item is attached to a view holder and RecyclerView undergoes an animation phase to move items down to display the new item at the top.
If the new item cannot be displayed without scrolling, a view holder is not created so there is nothing to animate. The only way to get the new item onto the screen when this happens is to scroll which causes the view holder to be created so the view can be laid out on the screen. (There does seem to be an edge case where the view is partially displayed and a view holder is created, but I will ignore this particular instance since it is not germane.)
So, the issue is that two different actions, animation of an added view and scrolling of an added view, must be made to look the same to the user. We could dive into the underlying code and figure out exactly what is going on in terms of view holder creation, animation timing, etc. But, even if we can duplicate the actions, it can break if the underlying code changes. This is what you are resisting.
An alternative is to add a header at position zero of the RecyclerView. You will always see the animation when this header is displayed and new items are added to position 1. If you don't want a header, you can make it zero height and it will not display. The following video shows this technique:
This is the code for the demo. It simply adds a dummy entry at position 0 of the items. If a dummy entry is not to your liking, there are other ways to approach this. You can search for ways to add headers to RecyclerView.
(If you do use a scrollbar, it will misbehave as you can probably tell from the demo. To fix this 100%, you will have to take over a lot of the scrollbar height and placement computation. The custom computeVerticalScrollOffset() for the LinearLayoutManager takes care of placing the scrollbar at the top when appropriate. (Code was introduced after video taken.) The scrollbar, however, jumps when scrolling down. A better placement computation would take care of this problem. See this Stack Overflow question for more information on scrollbars in the context of varying height items.)
MainActivity.java
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private TheAdapter mAdapter;
private final ArrayList<String> mItems = new ArrayList<>();
private int mItemCount = 0;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
LinearLayoutManager layoutManager =
new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) {
#Override
public int computeVerticalScrollOffset(RecyclerView.State state) {
if (findFirstCompletelyVisibleItemPosition() == 0) {
// Force scrollbar to top of range. When scrolling down, the scrollbar
// will jump since RecyclerView seems to assume the same height for
// all items.
return 0;
} else {
return super.computeVerticalScrollOffset(state);
}
}
};
recyclerView.setLayoutManager(layoutManager);
for (mItemCount = 0; mItemCount < 6; mItemCount++) {
mItems.add(0, "Item # " + mItemCount);
}
// Create a dummy entry that is just a placeholder.
mItems.add(0, "Dummy item that won't display");
mAdapter = new TheAdapter(mItems);
recyclerView.setAdapter(mAdapter);
}
#Override
public void onClick(View view) {
// Always at to position #1 to let animation occur.
mItems.add(1, "Item # " + mItemCount++);
mAdapter.notifyItemInserted(1);
}
}
TheAdapter.java
class TheAdapter extends RecyclerView.Adapter<TheAdapter.ItemHolder> {
private ArrayList<String> mData;
public TheAdapter(ArrayList<String> data) {
mData = data;
}
#Override
public ItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view;
if (viewType == 0) {
// Create a zero-height view that will sit at the top of the RecyclerView to force
// animations when items are added below it.
view = new Space(parent.getContext());
view.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0));
} else {
view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.list_item, parent, false);
}
return new ItemHolder(view);
}
#Override
public void onBindViewHolder(final ItemHolder holder, int position) {
if (position == 0) {
return;
}
holder.mTextView.setText(mData.get(position));
}
#Override
public int getItemViewType(int position) {
return (position == 0) ? 0 : 1;
}
#Override
public int getItemCount() {
return mData.size();
}
public static class ItemHolder extends RecyclerView.ViewHolder {
private TextView mTextView;
public ItemHolder(View itemView) {
super(itemView);
mTextView = (TextView) itemView.findViewById(R.id.textView);
}
}
}
activity_main.xml
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:scrollbars="vertical"
app:layout_constraintBottom_toTopOf="#+id/button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="#+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:text="Button"
android:onClick="onClick"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</android.support.constraint.ConstraintLayout>
list_item.xml
<LinearLayout
android:id="#+id/list_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal">
<View
android:id="#+id/box"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginStart="16dp"
android:background="#android:color/holo_green_light"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:textSize="24sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="#id/box"
app:layout_constraintTop_toTopOf="parent"
tools:text="TextView" />
</LinearLayout>
I'm also displaying a live feed of items, and when a new item is added, it's before the first item. But in the recycler view, I need to scroll up to see the new item.
To solve the problem, add to the Adapter a RecyclerView.AdapterDataObserver() and override the onItemRangeInserted(). When new data is added, check if the data is on at position 0, and the recycler view was on top (You don't want to autoscroll to first position if you were scrolling in the list).
Exemple :
adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
#Override
public void onItemRangeInserted(int positionStart, int itemCount) {
super.onItemRangeInserted(positionStart, itemCount);
if (positionStart == 0 && positionStart == layoutManager.findFirstCompletelyVisibleItemPosition()) {
layoutManager.scrollToPosition(0);
}
}
});
This solution worked fine for me, with different type of Adapter, like ListAdapter and PagedListAdapter.
I firstly wanted to use a similar implementation of the accepted solution, where you add a dumb first item, but in PagedListAdapter it's impossible as list are immutable.
This worked for me:
val atTop = !recycler.canScrollVertically(-1)
adapter.addToFront(item)
adapter.notifyItemInserted(0)
if (atTop) {
recycler.scrollToPosition(0)
}
The only solution that worked for me was to reverse the recycler's layout by calling setReverseLayout() and setStackFromEnd() on its LinearLayoutManager.
This might sound stupid, but the way RecyclerView handles adding items at the end of the list is what you need at the top. The only downsize of this is that you'd have to reverse your list and start adding items to the end instead.
Use adapter.notifyDataSetChanged() instead of adater.notifyItemInserted(0). This will scroll recylerView to zero position if current scroll position is one(old zero).
I have a recylcer view having grid images and I have a button at the bottom of the recycler view. This button should be at the bottom of the recyclerview, not the bottom of the parent.
I tried code for recyclerView. But the button stays as parent bottom not scrolling along with the recycler view.
<android.support.v7.widget.RecyclerView
android:id="#+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:text="#string/hello_blank_fragment"/>
I have found the solution.
I can create a separate view with Button and can add in the recyclerView Adapter.But there will be one problem - Since My Layout manager is GridLayout manager and having span of 2, I have to change the span to 1 when the current view is Button.
The changing of span when the view is Button is below:
final GridLayoutManager mGridManager = new GridLayoutManager(getActivity(),2);
mGridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
#Override
public int getSpanSize(int position) {
return adapter.isPositionHeader(position) ? mGridManager.getSpanCount() : 1;
}
});
recyclerView.setLayoutManager(mGridManager);
recyclerView.setAdapter(adapter);
This code I had to add in recyclerView Adapter, this below code means when button is the view return true, otherwise return false
public boolean isPositionHeader(int position) {
if(ADD_SOURCE_BUTTON == getItemViewType(position)){
return true;
} else {
return false;
}
}
Am not sure this is a good method, but it worked for me.
Try putting RecyclerView inside a ScrollView and place the button below RecyclerView. It will work fine now. Earlier it had some issues of scrolling but now its fixed by google I think and we can place RecyclerView inside scroll view, but am not sure its a good method, but it will work.
I am trying to create an item in a ListView that has multiple options; view and edit. I would like to create it in exactly the same way as android's contact system - see below:
I have added the red boxes to illustrate the behaviour I want. If you press within the left red-box, you call the contact. If you press within the right red-box, you send a text message to the contact. I have already created a similar layout in XML, but I am having trouble implementing this functionality in code.
I have tried to create custom android:onClick function calls for the separate layouts within the item, but calling an onClick method only allows you to pass in the View as a parameter, but not the position. Needing the position to use listview.getItemAtPosition function, I tried to use listview.getPositionForView to return the position but found this was extremely unstable and was very easy to return incorrect positioning due to recycling of views.
I then tried to set the item's position as the 'tag' in the getView method of my adapter, like so: convertView.setTag(position). But on the onClick method of my activity, I try and use getTag and cast it back to an integer, and it always returns null, which I find puzzling.
What is the best way of implementing a list populated by items with multiple buttons/layouts on each item?
You can create an onClick event on each views in your row like this :
<?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="horizontal" >
<TextView
android:id="#+id/text_id"
android:layout_width="0sp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center_vertical"
android:onClick="textOnClickEvent"/>
<ImageButton
android:id="#+id/button_id"
android:layout_width="#dimen/width_button"
android:layout_height="match_parent"
android:onClick="imageOnClickEvent"
android:src="#android:drawable/ic_menu_delete" />
</LinearLayout>
Or even, add onClick listeners on each views in the getView method...
more info on this here.
In the list view when you define getview method, this is where you provide all the details of the single list item. There you can mention onlick event of each of the views.
in adapter class, add View.OnClickListener to the getView method:
#Override
public View getView(int i, View view, ViewGroup viewGroup) {
if(view == null) {
final LayoutInflater layoutInflater = LayoutInflater.from(mContext);
view = layoutInflater.inflate(R.layout.grid_vendor_item, null);
}
final TextView textName = (TextView) view.findViewById(R.id.text_id);
final ImageButton imageProfil = (TextView) view.findViewById(R.id.button_id);
textName.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
// add your edit codes
}
});
imageProfil.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
// add your open prodil codes
}
});
return view;
}
I have a GridView whose elements are based on a FrameLayout containing an ImageView and a CheckedTextView. The xml file for the FrameLayout is as follows:
<FrameLayout
xmlns:android = "http://schemas.android.com/apk/res/android"
android:id="#+id/gridImage"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
android:gravity="center_horizontal">
<ImageView
android:id="#+id/image"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:adjustViewBounds="false">
</ImageView>
<CheckedTextView
android:id="#+id/imageTick"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:layout_gravity="center_horizontal|bottom"
android:checkMark="#drawable/icon"
android:checked="false"
android:visibility="invisible"
>
</CheckedTextView>
</FrameLayout>
EDIT: This is the adapter's getView() method i use for each element:
public View getView(int position, View convertView, ViewGroup parent) {
Log.d("getView()", position+"");
View v;
ImageView imageView;
if(convertView == null) {
v = LayoutInflater.from(mContext).inflate(R.layout.photo_text_view, null);
v.setLayoutParams(new GridView.LayoutParams(100,100));
}
else
{
v = convertView;
}
imageView = (ImageView) v.findViewById(R.id.image);
imageView.setImageBitmap(mThumbsIds.get().get(position));
v.setPadding(8, 8, 8, 8);
return v;
}
In my activity class, I load a context menu and once i select an option i make the CheckedTextView visible for that GridView element like so :
GridView gridView = (GridView) findViewById(R.id.gridview);
View selected =gridView.getChildAt(position);
CheckedTextView selectedCheck = (CheckedTextView)selected.findViewById(R.id.imageTick);
selectedCheck.setChecked(true);
selectedCheck.setVisibility(View.VISIBLE);
So two things can potentially happen from this point:
1) Lets say I picked the element at position 0, the CheckedTextView becomes visible, but when i scroll down my GridView, another element (e.g. position 17) has its CheckedTextView visible aswell. This continues on random elements as i scroll down towards the bottom.
2) If i pick an element towards the bottom, scroll back up to the top, and run one of methods to make all CheckedTextView's invisible, a NullPointerException is thrown for the element at the bottom. This happens at this point: View selected =gridView.getChildAt(position); where selected becomes null.
Whats going on here? Why do these random CheckedTextView's become visible and why do i get exceptions?
The problems lie in the way you track checked items:
GridView gridView = (GridView) findViewById(R.id.gridview);
View selected =gridView.getChildAt(position);
CheckedTextView selectedCheck = (CheckedTextView)selected.findViewById(R.id.imageTick);
selectedCheck.setChecked(true);
selectedCheck.setVisibility(View.VISIBLE);
1) getChildAt will not give you the correct view if you've scrolled into content and you're indexing by adapter position. When referencing an active view in a GridView or ListView you want to do something like this:
final int index = position - gridView.getFirstVisiblePosition();
View selected = gridView.getChildAt(index);
The reason is that your GridView only keeps child views for adapter items that it is currently displaying. It may only have child views for elements 4 to 23 if elements 0 to 3 were scrolled off of the top earlier.
This is why you're getting exceptions when you do View selected =gridView.getChildAt(position); a view for that position does not actually exist when it's off-screen, so getChildAt returns null.
2) When a ListView or GridView has its content change or it otherwise needs to re-layout its child views, it does so by re-binding existing views using the convertView parameter to your adapter. Your adapter never adjusts the checked state of your item views, so a checked view can be reused for an item that should be unchecked later.
To solve this you should have your data model that your adapter presents track the checked state of your items rather than relying on the item views to do it. This means that your adapter should always verify/set the checked state of the item in getView. It may look something like this:
imageView = (ImageView) v.findViewById(R.id.image);
imageView.setImageBitmap(mThumbsIds.get().get(position));
CheckedTextView checkView = (CheckedTextView) v.findViewById(R.id.imageTick);
if (mData.isItemChecked(position)) {
checkView.setChecked(true);
checkView.setVisibility(View.VISIBLE);
} else {
checkView.setVisibility(View.GONE);
}
Then when your option is selected that should change the checked state of an item, instead of the code snippet above do something like:
data.setItemChecked(position, true);
adapter.notifyDataSetChanged();
notifyDataSetChanged() will tell the GridView that it should re-bind the item views, which will have your getView method fix up the checked states properly.