Currently I'm at the end of my ideas on following issue with LinearLayoutManagers and RecyclerViews on Android:
What scenario I wanted to achieve
A horizontal RecyclerView on which the user can swipe very fast without any limitations on fling. The items being fullscreen sized making them as big as the recyclerview itself. When the fling has stopped or the user stops manually, the recycler should scroll to one item (mimicing a viewPager a bit)
(I'm using support revision 25.1.0)
code snippets
The Pager-class itself
public class VelocityPager extends RecyclerView {
private int mCurrentItem = 0;
#NonNull
private LinearLayoutManager mLayoutManager;
#Nullable
private OnPageChangeListener mOnPageChangeListener = null;
#NonNull
private Rect mViewRect = new Rect();
#NonNull
private OnScrollListener mOnScrollListener = new OnScrollListener() {
private int mLastItem = 0;
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (mOnPageChangeListener == null) return;
mCurrentItem = mLayoutManager.findFirstVisibleItemPosition();
final View view = mLayoutManager.findViewByPosition(mCurrentItem);
view.getLocalVisibleRect(mViewRect);
final float offset = (float) mViewRect.left / ((View) view.getParent()).getWidth();
mOnPageChangeListener.onPageScrolled(mCurrentItem, offset, 0);
if (mCurrentItem != mLastItem) {
mOnPageChangeListener.onPageSelected(mCurrentItem);
mLastItem = mCurrentItem;
}
}
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
if (mOnPageChangeListener == null) return;
mOnPageChangeListener.onPageScrollStateChanged(newState);
}
};
public VelocityPager(#NonNull Context context) {
this(context, null);
}
public VelocityPager(#NonNull Context context, #Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public VelocityPager(#NonNull Context context, #Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mLayoutManager = createLayoutManager();
init();
}
#NonNull
private LinearLayoutManager createLayoutManager() {
return new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false);
}
#Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
addOnScrollListener(mOnScrollListener);
}
#Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
removeOnScrollListener(mOnScrollListener);
}
#Override
public void onScrollStateChanged(int state) {
// If you tap on the phone while the RecyclerView is scrolling it will stop in the middle.
// This code fixes this. This code is not strictly necessary but it improves the behaviour.
if (state == SCROLL_STATE_IDLE) {
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) getLayoutManager();
int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;
// views on the screen
int lastVisibleItemPosition = linearLayoutManager.findLastVisibleItemPosition();
View lastView = linearLayoutManager.findViewByPosition(lastVisibleItemPosition);
int firstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition();
View firstView = linearLayoutManager.findViewByPosition(firstVisibleItemPosition);
// distance we need to scroll
int leftMargin = (screenWidth - lastView.getWidth()) / 2;
int rightMargin = (screenWidth - firstView.getWidth()) / 2 + firstView.getWidth();
int leftEdge = lastView.getLeft();
int rightEdge = firstView.getRight();
int scrollDistanceLeft = leftEdge - leftMargin;
int scrollDistanceRight = rightMargin - rightEdge;
if (leftEdge > screenWidth / 2) {
smoothScrollBy(-scrollDistanceRight, 0);
} else if (rightEdge < screenWidth / 2) {
smoothScrollBy(scrollDistanceLeft, 0);
}
}
}
private void init() {
setLayoutManager(mLayoutManager);
setItemAnimator(new DefaultItemAnimator());
setHasFixedSize(true);
}
public void setCurrentItem(int index, boolean smoothScroll) {
if (mOnPageChangeListener != null) {
mOnPageChangeListener.onPageSelected(index);
}
if (smoothScroll) smoothScrollToPosition(index);
if (!smoothScroll) scrollToPosition(index);
}
public int getCurrentItem() {
return mCurrentItem;
}
public void setOnPageChangeListener(#Nullable OnPageChangeListener onPageChangeListener) {
mOnPageChangeListener = onPageChangeListener;
}
public interface OnPageChangeListener {
/**
* This method will be invoked when the current page is scrolled, either as part
* of a programmatically initiated smooth scroll or a user initiated touch scroll.
*
* #param position Position index of the first page currently being displayed.
* Page position+1 will be visible if positionOffset is nonzero.
* #param positionOffset Value from [0, 1) indicating the offset from the page at position.
* #param positionOffsetPixels Value in pixels indicating the offset from position.
*/
void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);
/**
* This method will be invoked when a new page becomes selected. Animation is not
* necessarily complete.
*
* #param position Position index of the new selected page.
*/
void onPageSelected(int position);
/**
* Called when the scroll state changes. Useful for discovering when the user
* begins dragging, when the pager is automatically settling to the current page,
* or when it is fully stopped/idle.
*
* #param state The new scroll state.
* #see VelocityPager#SCROLL_STATE_IDLE
* #see VelocityPager#SCROLL_STATE_DRAGGING
* #see VelocityPager#SCROLL_STATE_SETTLING
*/
void onPageScrollStateChanged(int state);
}
}
The item's xml layout
(Note: the root view has to be clickable for other purposes inside the app)
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true">
<LinearLayout
android:id="#+id/icon_container_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_gravity="top|end"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="16dp"
android:alpha="0"
android:background="#drawable/info_background"
android:orientation="horizontal"
android:padding="4dp"
tools:alpha="1">
<ImageView
android:id="#+id/delete"
style="#style/SelectableItemBackground"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:contentDescription="#string/desc_delete"
android:padding="12dp"
android:src="#drawable/ic_delete_white_24dp"
android:tint="#color/icons" />
</LinearLayout>
<LinearLayout
android:id="#+id/icon_container_bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginBottom="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:alpha="0"
android:background="#drawable/info_background"
android:orientation="vertical"
android:padding="4dp"
tools:alpha="1">
<ImageView
android:id="#+id/size"
style="#style/SelectableItemBackground"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:contentDescription="#string/desc_size"
android:padding="12dp"
android:src="#drawable/ic_straighten_white_24dp"
android:tint="#color/icons" />
<ImageView
android:id="#+id/palette"
style="#style/SelectableItemBackground"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:contentDescription="#string/desc_palette"
android:padding="12dp"
android:src="#drawable/ic_palette_white_24dp"
android:tint="#color/icons" />
</LinearLayout>
</RelativeLayout>
The xml layout with the pager itself
(Quite nested? Might be a cause of the problem? I don't know... )
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/drawer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:openDrawer="end">
<SwipeRefreshLayout
android:id="#+id/refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.CoordinatorLayout
android:id="#+id/coordinator"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="false">
<FrameLayout
android:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.my.example.OptionalViewPager
android:id="#+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="horizontal"
app:layout_behavior="com.my.example.MoveUpBehavior" />
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#android:color/transparent"
android:clickable="false"
android:fitsSystemWindows="false"
app:contentInsetLeft="0dp"
app:contentInsetStart="0dp"
app:contentInsetStartWithNavigation="0dp"
app:layout_collapseMode="pin"
app:navigationIcon="#drawable/ic_menu_white_24dp" />
</android.support.design.widget.CoordinatorLayout>
</SwipeRefreshLayout>
<include layout="#layout/layout_drawer" />
</android.support.v4.widget.DrawerLayout>
part of my adapter that is relevant for ViewHolders
#Override
public int getItemCount() {
return dataset.size();
}
#Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
Log.v("Adapter", "CreateViewHolder");
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final View rootView = layoutInflater.inflate(R.layout.page, parent, false);
return new MyViewHolder(rootView);
}
#Override
public void onBindViewHolder(MyViewHolder page, int position) {
Log.v("Adapter", String.format("BindViewHolder(%d)", position));
final ViewData viewData = dataset.get(position);
page.bind(viewData);
listener.onViewAdded(position, viewData.getData());
}
#Override
public void onViewRecycled(MyViewHolder page) {
if (page.getData() == null) return;
listener.onViewRemoved(page.getData().id);
}
#Override
public int getItemViewType(int position) {
return 0;
}
The ViewHolder
public class MyViewHolder extends RecyclerView.ViewHolder implements MyListener {
#BindView(R.id.info_container)
ViewGroup mInfoContainer;
#BindView(R.id.icon_container_top)
ViewGroup mIconContainerTop;
#BindView(R.id.icon_container_bottom)
ViewGroup mIconContainerBottom;
#BindView(R.id.info_rows)
ViewGroup mInfoRows;
#BindView(R.id.loading)
View mIcLoading;
#BindView(R.id.sync_status)
View mIcSyncStatus;
#BindView(R.id.delete)
View mIcDelete;
#BindView(R.id.ic_fav)
View mIcFavorite;
#BindView(R.id.size)
View mIcSize;
#BindView(R.id.palette)
View mIcPalette;
#BindView(R.id.name)
TextView mName;
#BindView(R.id.length)
TextView mLength;
#BindView(R.id.threads)
TextView mThreads;
#BindView(R.id.price)
TextView mPrice;
#Nullable
private MyModel mModel = null;
#Nullable
private Activity mActivity;
public MyViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
mActivity= (Activity) itemView.getContext();
if (mActivity!= null) mActivity.addMyListener(this);
}
#OnClick(R.id.delete)
protected void clickDeleteBtn() {
if (mActivity == null || mActivity.getMode() != Mode.EDIT) return;
if (mModel == null) return;
Animations.pop(mIcDelete);
final int modelId = mModel.id;
if (mModel.delete()) {
mActivity.delete(modelId);
}
}
#OnClick(R.id.size)
protected void clickSizeBtn() {
if (mActivity== null) return;
mActivity.setUIMode(Mode.EDIT_SIZE);
Animations.pop(mIcSize);
}
#OnClick(R.id.palette)
protected void clickPaletteBtn() {
if (mActivity== null) return;
mActivity.setUIMode(Mode.EDIT_LENGTH);
Animations.pop(mIcPalette);
}
private void initModelViews() {
if (mData == null) return;
final Locale locale = Locale.getDefault();
mName.setValue(String.format(locale, "Model#%d", mModel.id));
mLength.setValue(Html.fromHtml(String.format(locale, itemView.getContext().getString(R.string.template_length), mModel.meters)));
}
/**
* set the icon container to be off screen at the beginning
*/
private void prepareViews() {
new ExpectAnim().expect(mIconContainerTop).toBe(outOfScreen(Gravity.END), visible())
.toAnimation()
.setNow();
new ExpectAnim().expect(mIconContainerBottom).toBe(outOfScreen(Gravity.END), visible())
.toAnimation()
.setNow();
}
#Nullable
public MyModel getData() {
return mModel;
}
private void enableEdit() {
new ExpectAnim()
.expect(mIconContainerBottom)
.toBe(atItsOriginalPosition())
.toAnimation()
.start();
}
private void disableEdit() {
new ExpectAnim()
.expect(mIconContainerBottom)
.toBe(outOfScreen(Gravity.END))
.toAnimation()
.start();
}
private void enableInfo() {
new ExpectAnim()
.expect(mInfoContainer)
.toBe(atItsOriginalPosition())
.toAnimation()
.start();
}
private void disableInfo() {
new ExpectAnim()
.expect(mInfoContainer)
.toBe(outOfScreen(Gravity.BOTTOM))
.toAnimation()
.start();
}
private void enableDelete() {
if (mIconContainerTop == null) return;
new ExpectAnim()
.expect(mIconContainerTop)
.toBe(atItsOriginalPosition(), visible())
.toAnimation()
.start();
}
private void disableDelete() {
if (mIconContainerTop == null) return;
new ExpectAnim()
.expect(mIconContainerTop)
.toBe(outOfScreen(Gravity.END), invisible())
.toAnimation()
.start();
}
public void bind(#NonNull final ViewData viewData) {
mModel = viewData.getData();
prepareViews();
initModelViews();
}
}
So, here's my issue with these!
When intializing the adapter I insert about 15 to 17 items via an observable. This seems to be correct:
but when swiping horizontally the recyclerView's callbacks seem to be totally messed up and produce weird results:
Do you see that the recycler does not try to recycle old viewHolders at all? The image just shows a small portion of the "spamming" that is going on. Sometimes it will create a new viewHolder even more than two times for the same position while I scroll the recycler slowly!
Another side problem is: The listener currently should allow me to pass the bind / recycle events to an underlying game engine which will create destroy entities on the screen. Due the excessive spamming of the events it will currently create those entities also excessively!
I excpected the Recycler to create a new ViewHolder for the first (let's say in my example 17) times and then just reuse the items how it should.
Please help, I'm stuck on this problem for 2 days now and I'm frustrated after searching people with same issues but without luck.
Thank you!
There's obviously a problem with ViewHolder recycling. I'm guessing the animations you're running inside MyViewHolder might prevent RecyclerView from recycling holders properly. Make sure you cancel animations at some point, e.g. in RecyclerView.Adapter#onViewDetachedFromWindow().
After you've fixed this, I suggest you follow #EugenPechanec's suggestion to reduce the amount of custom calculations done in the OnScrollListeners. It's better to rely on support library classes and tweak the behavior a little.
When the fling has stopped or the user stops manually, the recycler should scroll to one item (mimicing a viewPager a bit)
Use the official LinearSnapHelper which snaps center of child view to center of RecyclerView.
Use a GravitySnapHelper library which can also snap to start of or end of RecyclerView, just like Google Play store does.
Both of these solutions are applied similarly:
new LinearSnapHelper().attachToRecyclerView(recyclerView);
A horizontal RecyclerView on which the user can swipe very fast without any limitations on fling.
"Without limitations" translates to "infinite speed" meaning a fling would instantly jump to target position. That's probably not what you want.
After going through SnapHelper source I found out that there is a rule: one inch takes 100 milliseconds to scroll. You can override this behavior.
final SnapHelper snapHelper = new LinearSnapHelper() {
#Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
};
snapHelper.attachToRecyclerView(recyclerView);
That's the default speed (where MILLISECONDS_PER_INCH = 100). Experiment and find out what fits your needs, start with "one inch takes 50 ms to scroll" and so on.
Related
I am having 15 to 30 items in my recyclerview. At the End of the recyclerview I want to show the Image/Layout at bottom. This image will slowly come to top while scroll the recylerview to top. When the list end the image/layout will fully shown. If we scroll down the recyclerview the image/layout should go down. If I stop the scroll at middle the image/layout will show partially. For example the Image/Layout height will be 100 dp. it will be placed in the bottom. It will not visible at first time. When we scroll the Recyclerview that view will be slowly appear. Please give me any idea to achieve this. Sorry for my bad English.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="#+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical" />
<RelativeLayout
android:id="#+id/bottomView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Will show while Scroll"
android:textSize="30sp"
/>
</RelativeLayout>
</RelativeLayout>
Scrolling Recyclerview
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dy > 0) {
footerHeight = +10;
bottomView.setTranslationY(footerHeight);
Log.i("Test","...Scrolling up");
} else {
footerHeight = -10;
bottomView.setTranslationY(footerHeight);
Log.i("Test","...Scrolling down");
}
}
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
switch (newState) {
case RecyclerView.SCROLL_STATE_IDLE:
Log.i("Test","...The RecyclerView is not scrolling");
break;
case RecyclerView.SCROLL_STATE_DRAGGING:
Log.i("Test","...Scrolling now");
break;
case RecyclerView.SCROLL_STATE_SETTLING:
Log.i("Test","...Scroll Settling");
break;
}
}
});
Here I just increase/decrease the bottomX view while scrolling. But still I am missing something.
OP:
In this image bottom view is showing always. But initially it want view should be hidden state. While scroll up Bottom view slowly come up. If I scroll down Bottom view should slowly goes down.
Start a new project and try this:
MainActivity.java:
public class MainActivity extends AppCompatActivity {
private static final int DATA_LIST_SIZE = 50;
RecyclerView recyclerView;
TextView footer;
ArrayList<SampleData> dataArrayList;
LinearLayoutManager linearLayoutManager;
int totalHeight = -1;
int invisibleHeight = -1;
int scrolledHeight = -1;
int childHeight = -1;
int footerHeight = -1;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
recyclerView = findViewById(R.id.recycler_view);
footer = findViewById(R.id.text_view_footer);
dataArrayList = genSampleDataList();
CustomRecyclerViewAdapter adapter = new CustomRecyclerViewAdapter(dataArrayList);
linearLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
recyclerView.setLayoutManager(linearLayoutManager);
recyclerView.setAdapter(adapter);
footer.measure( View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
footerHeight = footer.getMeasuredHeight();
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(#NonNull RecyclerView recyclerView, int dx, int dy) {
View firstVisibleView = recyclerView.getChildAt(0);
if (invisibleHeight == -1) {
childHeight = linearLayoutManager.getDecoratedMeasuredHeight(firstVisibleView);
totalHeight = childHeight * DATA_LIST_SIZE;
invisibleHeight = totalHeight - recyclerView.getHeight() + footerHeight;
}
scrolledHeight = linearLayoutManager.findFirstVisibleItemPosition() * childHeight +
recyclerView.getTop() - firstVisibleView.getTop();
int newRecyclerViewHeight = totalHeight - invisibleHeight + footerHeight -
scrolledHeight * footerHeight / invisibleHeight;
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, newRecyclerViewHeight);
recyclerView.setLayoutParams(params);
footer.setBackgroundColor(Color.rgb(255 * (invisibleHeight - scrolledHeight) / invisibleHeight,
255 * scrolledHeight / invisibleHeight, 0));
}
#Override
public void onScrollStateChanged(#NonNull RecyclerView recyclerView, int newState) {
}
});
}
private ArrayList<SampleData> genSampleDataList() {
ArrayList<SampleData> tmpList = new ArrayList<>();
for (int i = 0; i < DATA_LIST_SIZE; i++) {
tmpList.add(new SampleData("Item " + (i + 1), "Description " + (i + 1)));
}
return tmpList;
}
}
SampleData.java:
public class SampleData {
String name;
String description;
public SampleData(String name, String description) {
this.name = name;
this.description = description;
}
}
CustomRecyclerViewAdapter.java:
public class CustomRecyclerViewAdapter extends RecyclerView.Adapter<CustomRecyclerViewAdapter.ViewHolder> {
ArrayList<SampleData> dataList;
public CustomRecyclerViewAdapter(ArrayList<SampleData> dataList) {
this.dataList = dataList;
}
#NonNull
#Override
public ViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_view, null);
return new ViewHolder(view);
}
#Override
public void onBindViewHolder(#NonNull ViewHolder holder, int position) {
SampleData sampleData = dataList.get(position);
holder.textViewName.setText(sampleData.name);
holder.textViewDescription.setText(sampleData.description);
}
#Override
public int getItemCount() {
return dataList.size();
}
class ViewHolder extends RecyclerView.ViewHolder {
TextView textViewName;
TextView textViewDescription;
public ViewHolder(#NonNull View itemView) {
super(itemView);
textViewName = itemView.findViewById(R.id.text_view_name);
textViewDescription = itemView.findViewById(R.id.text_view_description);
}
}
}
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentTop="true"
android:scrollbars="vertical" />
<TextView
android:id="#+id/text_view_footer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#id/recycler_view"
android:gravity="center"
android:text="Will show while Scroll"
android:textSize="30sp" />
</RelativeLayout>
item_view.xml:
<TextView
android:id="#+id/text_view_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="25sp"
android:textStyle="bold" />
<TextView
android:id="#+id/text_view_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp" />
</LinearLayout>
One solution if I've read your question correctly is in your model class to include link or Uri of ImageView in a String.
Then in your RecyclerView adapter do some boolean checking to see if item added has a link to it and if it has load it with library called Picasso for example. Picasso is simple just one line of code. If you are using image from phone you might just add uri to image.
And when items are added on last item add link to image or set it yourself.
I try to do something like this :
I managed to do my cardViewAdapter but I block to enlarge my cards. I resumed the code of this response (Here the name of the class is : CardsAnimationHelper) to do the animation but it's superimposed.
Before expand:
After expand:
I solved the problem above but if on my cardView I display 10 elements at the same time for a list of 50. If I expand the first, the numbers 11,21,31,41 will also expand. Do you have a trick for this not to happen?
I have reflected, it makes no sense to me. Just before my OnClick method I display a textview where the text is the position. But when I click id are correct so that would mean that when I click it detects the click on several cards. I think I may have a problem with a view in my OnClickListener
My CardView
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
app:cardBackgroundColor="#android:color/white"
app:cardCornerRadius="2dp"
app:cardElevation="2dp">
<!-- Les CardView possèdent des attributs supplémentaires dont
- cardBackgroundColor
- cardElevation pour l'élévation (donc aussi l'ombre)
- cardCornerRadius pour arrondir les angles
-->
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- Les CardView agissent comme des FrameLayout,
pour avoir une organisation verticale nous devons
donc rajouter un LinearLayout -->
<TextView
android:id="#+id/text_cards"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:selectableItemBackground"
android:padding="20dp"
tools:text="Paris"
android:fontFamily="sans-serif"
android:textColor="#333"
android:textSize="18sp" />
<ImageView
android:id="#+id/item_description_game_more"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|end"
android:transitionName="#string/transition_cards_view"
app:srcCompat="#drawable/ic_expand_more_black_24dp"/>
<include layout="#layout/cards_resume_game_expand"/>
</android.support.design.widget.CoordinatorLayout>
</android.support.v7.widget.CardView>
</LinearLayout>
My New Adapter
public class CardsViewAdapter extends RecyclerView.Adapter<CardsViewAdapter.ViewHolder> {
private Game[] mDataset;
private boolean isPopupVisible = false;
int rotationAngle = 0;
// Provide a reference to the views for each data item
// Complex data items may need more than one view per item, and
// you provide access to all the views for a data item in a view holder
public static class ViewHolder extends RecyclerView.ViewHolder {
// each data item is just a string in this case
public TextView mTextView;
public ImageView imageView;
public LinearLayout test2;
public ViewHolder(View v) {
super(v);
mTextView = (TextView) v.findViewById(R.id.text_cards);
imageView = (ImageView) v.findViewById(R.id.item_description_game_more);
test2 = (LinearLayout) v.findViewById(R.id.popup_layout);
}
}
// Provide a suitable constructor (depends on the kind of dataset)
public CardsViewAdapter(Game[] myDataset) {
mDataset = myDataset;
}
// Create new views (invoked by the layout manager)
#Override
public CardsViewAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
// create a new view
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.cards_resume_game, parent, false);
// set the view's size, margins, paddings and layout parameters
//...
ViewHolder vh = new ViewHolder(v);
return vh;
}
// Replace the contents of a view (invoked by the layout manager)
#Override
public void onBindViewHolder(final ViewHolder holder, int position) {
// - get element from your dataset at this position
// - replace the contents of the view with that element
holder.mTextView.setText(String.valueOf(mDataset[position].getId_game()));
holder.imageView.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
// Perform action on click
if (isPopupVisible) {
isPopupVisible = false;
ObjectAnimator anim = ObjectAnimator.ofFloat(v, "rotation",rotationAngle, rotationAngle + 180);
anim.setDuration(500);
anim.start();
rotationAngle += 180;
rotationAngle = rotationAngle%360;
// CardsAnimationHelper.changeIconAnim((TextView) v, getString(R.string.icon_chevron_up));
CardsAnimationHelper.collapse(holder.test2);
} else {
isPopupVisible = true;
ObjectAnimator anim = ObjectAnimator.ofFloat(v, "rotation",rotationAngle, rotationAngle + 180);
anim.setDuration(500);
anim.start();
rotationAngle += 180;
rotationAngle = rotationAngle%360;
// CardsAnimationHelper.changeIconAnim((TextView) v, getString(R.string.icon_chevron_down));
CardsAnimationHelper.expand(holder.test2);
}
}
});
}
// Return the size of your dataset (invoked by the layout manager)
#Override
public int getItemCount() {
return mDataset.length;
}
}
I did not understand what you meant by displaying 10 elements out of 50. However, you can achieve the expand/collapse simply by showing/hiding the views and providing android:animateLayoutChanges="true" into the child layout of the CardView. Here is an example:
<android.support.v7.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:padding="16dp"
android:orientation="vertical">
<TextView
android:id="#+id/hello"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"/>
<TextView
android:id="#+id/hello2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
android:visibility="gone"/>
<TextView
android:id="#+id/hello3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
android:visibility="gone"/>
</LinearLayout>
</android.support.v7.widget.CardView>
And corresponding controller:
TextView t1 = (TextView) findViewById(R.id.hello);
final TextView t2 = (TextView) findViewById(R.id.hello2);
final TextView t3 = (TextView) findViewById(R.id.hello3);
t1.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
if (t2.getVisibility() == View.GONE) {
t2.setVisibility(View.VISIBLE);
t3.setVisibility(View.VISIBLE);
} else {
t2.setVisibility(View.GONE);
t3.setVisibility(View.GONE);
}
}
});
Tapping on the first TextView will collapse and expand the CardView along with the animation.
You'll need to create a custom class that extends CardView. Inside that class put the following methods:
public void expand() {
int initialHeight = getHeight();
measure(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
int targetHeight = getMeasuredHeight();
int distanceToExpand = targetHeight - initialHeight;
Animation a = new Animation() {
#Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
if (interpolatedTime == 1){
// Do this after expanded
}
getLayoutParams().height = (int) (initialHeight + (distanceToExpand * interpolatedTime));
requestLayout();
}
#Override
public boolean willChangeBounds() {
return true;
}
};
a.setDuration((long) distanceToExpand);
startAnimation(a);
}
public void collapse(int collapsedHeight) {
int initialHeight = getMeasuredHeight();
int distanceToCollapse = (int) (initialHeight - collapsedHeight);
Animation a = new Animation() {
#Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
if (interpolatedTime == 1){
// Do this after collapsed
}
Log.i(TAG, "Collapse | InterpolatedTime = " + interpolatedTime);
getLayoutParams().height = (int) (initialHeight - (distanceToCollapse * interpolatedTime));
requestLayout();
}
#Override
public boolean willChangeBounds() {
return true;
}
};
a.setDuration((long) distanceToCollapse);
startAnimation(a);
}
Note that when you collapse it, you'll need to pass along the height you want it to be when collapsed. The height when expanded is set to WRAP_CONTENT.
I've also added if/else statements that will run when the animation has completed.
Good luck!
RecyclerView by default, does come with a nice deletion animation, as long as you setHasStableIds(true) and provide correct implementation on getItemId.
Recently, I had added divider into RecyclerView via https://stackoverflow.com/a/27037230/72437
The outcome looks as following
https://www.youtube.com/watch?v=u-2kPZwF_0w
https://youtu.be/c81OsFAL3zY (To make the dividers more visible when delete animation played, I temporary change the RecyclerView background to red)
The dividers are still visible, when deletion animation being played.
However, if I look at GMail example, when deletion animation being played, divider lines are no longer visible. They are being covered a solid color area.
https://www.youtube.com/watch?v=cLs7paU-BIg
May I know, how can I achieve the same effect as GMail, by not showing divider lines, when deletion animation played?
The solution is fairly easy. To animate a decoration, you can and should use view.getTranslation_() and view.getAlpha(). I wrote a blog post some time ago on this exact issue, you can read it here.
Translation and fading off
The default layout manager will fade views out (alpha) and translate them, when they get added or removed. You have to account for this in your decoration.
The idea is simple:
However you draw your decoration, apply the same alpha and translation to your drawing by using view.getAlpha() and view.getTranslationY().
Following your linked answer, it would have to be adapted like the following:
// translate
int top = child.getBottom() + params.bottomMargin + view.getTranslationY();
int bottom = top + mDivider.getIntrinsicHeight();
// apply alpha
mDivider.setAlpha((int) child.getAlpha() * 255f);
mDivider.setBounds(left + view.getTranslationX(), top,
right + view.getTranslationX(), bottom);
mDivider.draw(c);
A complete sample
I like to draw things myself, since I think drawing a line is less overhead than layouting a drawable, this would look like the following:
public class SeparatorDecoration extends RecyclerView.ItemDecoration {
private final Paint mPaint;
private final int mAlpha;
public SeparatorDecoration(#ColorInt int color, float width) {
mPaint = new Paint();
mPaint.setColor(color);
mPaint.setStrokeWidth(width);
mAlpha = mPaint.getAlpha();
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
// we retrieve the position in the list
final int position = params.getViewAdapterPosition();
// add space for the separator to the bottom of every view but the last one
if (position < state.getItemCount()) {
outRect.set(0, 0, 0, (int) mPaint.getStrokeWidth()); // left, top, right, bottom
} else {
outRect.setEmpty(); // 0, 0, 0, 0
}
}
#Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
// a line will draw half its size to top and bottom,
// hence the offset to place it correctly
final int offset = (int) (mPaint.getStrokeWidth() / 2);
// this will iterate over every visible view
for (int i = 0; i < parent.getChildCount(); i++) {
final View view = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
// get the position
final int position = params.getViewAdapterPosition();
// and finally draw the separator
if (position < state.getItemCount()) {
// apply alpha to support animations
mPaint.setAlpha((int) (view.getAlpha() * mAlpha));
float positionY = view.getBottom() + offset + view.getTranslationY();
// do the drawing
c.drawLine(view.getLeft() + view.getTranslationX(),
positionY,
view.getRight() + view.getTranslationX(),
positionY,
mPaint);
}
}
}
}
Firstly, sorry for the massive answer size. However, I felt it necessary to include my entire test Activity so that you can see what I have done.
The issue
The issue that you have, is that the DividerItemDecoration has no idea of the state of your row. It does not know whether the item is being deleted.
For this reason, I made a POJO that we can use to contain an integer (that we use as both an itemId and a visual representation and a boolean indicating that this row is being deleted or not.
When you decide to delete entries (in this example adapter.notifyItemRangeRemoved(3, 8);), you must also set the associated Pojo to being deleted (in this example pojo.beingDeleted = true;).
The position of the divider when beingDeleted, is reset to the colour of the parent view. In order to cover up the divider.
I am not very fond of using the dataset itself to manage the state of its parent list. There is perhaps a better way.
The result visualized
The Activity:
public class MainActivity extends AppCompatActivity {
private static final int VERTICAL_ITEM_SPACE = 8;
private List<Pojo> mDataset = new ArrayList<Pojo>();
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
for(int i = 0; i < 30; i++) {
mDataset.add(new Pojo(i));
}
final RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(layoutManager);
recyclerView.addItemDecoration(new VerticalSpaceItemDecoration(VERTICAL_ITEM_SPACE));
recyclerView.addItemDecoration(new DividerItemDecoration(this));
RecyclerView.ItemAnimator ia = recyclerView.getItemAnimator();
ia.setRemoveDuration(4000);
final Adapter adapter = new Adapter(mDataset);
recyclerView.setAdapter(adapter);
(new Handler(Looper.getMainLooper())).postDelayed(new Runnable() {
#Override
public void run() {
int index = 0;
Iterator<Pojo> it = mDataset.iterator();
while(it.hasNext()) {
Pojo pojo = it.next();
if(index >= 3 && index <= 10) {
pojo.beingDeleted = true;
it.remove();
}
index++;
}
adapter.notifyItemRangeRemoved(3, 8);
}
}, 2000);
}
public class Adapter extends RecyclerView.Adapter<Holder> {
private List<Pojo> mDataset;
public Adapter(#NonNull final List<Pojo> dataset) {
setHasStableIds(true);
mDataset = dataset;
}
#Override
public Holder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.adapter_cell, parent, false);
return new Holder(view);
}
#Override
public void onBindViewHolder(final Holder holder, final int position) {
final Pojo data = mDataset.get(position);
holder.itemView.setTag(data);
holder.textView.setText("Test "+data.dataItem);
}
#Override
public long getItemId(int position) {
return mDataset.get(position).dataItem;
}
#Override
public int getItemCount() {
return mDataset.size();
}
}
public class Holder extends RecyclerView.ViewHolder {
public TextView textView;
public Holder(View itemView) {
super(itemView);
textView = (TextView) itemView.findViewById(R.id.text);
}
}
public class Pojo {
public int dataItem;
public boolean beingDeleted = false;
public Pojo(int dataItem) {
this.dataItem = dataItem;
}
}
public class DividerItemDecoration extends RecyclerView.ItemDecoration {
private final int[] ATTRS = new int[]{android.R.attr.listDivider};
private Paint mOverwritePaint;
private Drawable mDivider;
/**
* Default divider will be used
*/
public DividerItemDecoration(Context context) {
final TypedArray styledAttributes = context.obtainStyledAttributes(ATTRS);
mDivider = styledAttributes.getDrawable(0);
styledAttributes.recycle();
initializePaint();
}
/**
* Custom divider will be used
*/
public DividerItemDecoration(Context context, int resId) {
mDivider = ContextCompat.getDrawable(context, resId);
initializePaint();
}
private void initializePaint() {
mOverwritePaint = new Paint();
mOverwritePaint.setColor(ContextCompat.getColor(MainActivity.this, android.R.color.background_light));
}
#Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = parent.getChildAt(i);
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
int top = child.getBottom() + params.bottomMargin;
int bottom = top + mDivider.getIntrinsicHeight();
Pojo item = (Pojo) child.getTag();
if(item.beingDeleted) {
c.drawRect(left, top, right, bottom, mOverwritePaint);
} else {
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
}
}
public class VerticalSpaceItemDecoration extends RecyclerView.ItemDecoration {
private final int mVerticalSpaceHeight;
public VerticalSpaceItemDecoration(int mVerticalSpaceHeight) {
this.mVerticalSpaceHeight = mVerticalSpaceHeight;
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
outRect.bottom = mVerticalSpaceHeight;
}
}
}
The Activity Layout
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="#dimen/activity_horizontal_margin"
android:paddingRight="#dimen/activity_horizontal_margin"
android:paddingTop="#dimen/activity_vertical_margin"
android:paddingBottom="#dimen/activity_vertical_margin"
android:background="#android:color/background_light"
tools:context="test.dae.myapplication.MainActivity">
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
The RecyclerView "row" Layout
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/text"
android:padding="8dp">
</TextView>
I think the ItemDecorator you use to draw a divider after every row is messing things up when swipe to delete is performed.
Instead of Using ItemDecorator to draw a Divider in a recyclerview, add a view at the end of your RecyclerView child layout design.which will draw a divider line like ItemDecorator.
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<!-- child layout Design !-->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#android:color/darker_gray"
android:layout_gravity="bottom"
/>
</Linearlayout>
I implemented a collapsingtoolbar layout with a recyclerview as shown in the sample code attached. My issue is that, when I fling the list downward, it does not go all the way to the top.
What happens is that, the scrolling stops right at the point where the AppBarLayout is supposed to end.
The effect that I want is upon flinging the list downward, the list will go all the way to the top AND reveal/expand the AppBarLayout
My minSdk is 14. Any help or suggestion is greatly appreciated.
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.AppBarLayout>
<android.support.design.widget.CollapsingToolbarLayout
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<LinearLayout
app:layout_collapseMode="parallax">
//some elements
</LinearLayout>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
app:layout_behavior="#string/appbar_scrolling_view_behavior"/> //value android.support.design.widget.AppBarLayout$ScrollingViewBehavior
<android.support.v7.widget.Toolbar
app:popupTheme="#style/AppTheme.PopupOverlay"
app:layout_collapseMode="parallax" />
I had similar problem and I used a simple trick to expand AppBarLayout when RecyclerView fling to top (you need to have support library >= 23.x.x)
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
int firstVisiblePosition = linearLayoutManager.findFirstCompletelyVisibleItemPosition();
if (firstVisiblePosition == 0) {
mAppBarLayout.setExpanded(true, true);
}
}
}
});
You can fully expand or collapse the App Bar with the setExpanded() method. One implementation could involve overriding dispatchTouchEvent() in your Activity class, and auto-collapsing/expanding your App Bar based on whether it is collapsed past the halfway point:
#Override
public boolean dispatchTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
float per = Math.abs(mAppBarLayout.getY()) / mAppBarLayout.getTotalScrollRange();
boolean setExpanded = (per <= 0.5F);
mAppBarLayout.setExpanded(setExpanded, true);
}
return super.dispatchTouchEvent(event);
}
In respect to automatically scrolling to the last position on a fling, I have put some code on GitHub that shows how to programmatically smooth scroll to a specific location that may help. Calling a scroll to list.size() - 1 on a fling for instance could replicate the behaviour. Parts of this code by the way are adapted from the StylingAndroid and Novoda blogs:
public class RecyclerLayoutManager extends LinearLayoutManager {
private AppBarManager mAppBarManager;
private int visibleHeightForRecyclerView;
public RecyclerLayoutManager(Context context) {
super(context);
}
#Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
View firstVisibleChild = recyclerView.getChildAt(0);
final int childHeight = firstVisibleChild.getHeight();
int distanceInPixels = ((findFirstVisibleItemPosition() - position) * childHeight);
if (distanceInPixels == 0) {
distanceInPixels = (int) Math.abs(firstVisibleChild.getY());
}
//Called Once
if (visibleHeightForRecyclerView == 0) {
visibleHeightForRecyclerView = mAppBarManager.getVisibleHeightForRecyclerViewInPx();
}
//Subtract one as adapter position 0 based
final int visibleChildCount = visibleHeightForRecyclerView/childHeight - 1;
if (position <= visibleChildCount) {
//Scroll to the very top and expand the app bar
position = 0;
mAppBarManager.expandAppBar();
} else {
mAppBarManager.collapseAppBar();
}
SmoothScroller smoothScroller = new SmoothScroller(recyclerView.getContext(), Math.abs(distanceInPixels), 1000);
smoothScroller.setTargetPosition(position);
startSmoothScroll(smoothScroller);
}
public void setAppBarManager(AppBarManager appBarManager) {
mAppBarManager = appBarManager;
}
private class SmoothScroller extends LinearSmoothScroller {
private static final int TARGET_SEEK_SCROLL_DISTANCE_PX = 10000;
private final float distanceInPixels;
private final float duration;
public SmoothScroller(Context context, int distanceInPixels, int duration) {
super(context);
this.distanceInPixels = distanceInPixels;
float millisecondsPerPx = calculateSpeedPerPixel(context.getResources().getDisplayMetrics());
this.duration = distanceInPixels < TARGET_SEEK_SCROLL_DISTANCE_PX ?
(int) (Math.abs(distanceInPixels) * millisecondsPerPx) : duration;
}
#Override
public PointF computeScrollVectorForPosition(int targetPosition) {
return RecyclerLayoutManager.this
.computeScrollVectorForPosition(targetPosition);
}
#Override
protected int calculateTimeForScrolling(int dx) {
float proportion = (float) dx / distanceInPixels;
return (int) (duration * proportion);
}
}
}
Edit:
AppBarManager in the above code snippet refers to an interface used to communicate with the AppBarLayout in an Activity. Collapse/expand app bar methods do just that, with animations. The final method is used to calculate the number of RecyclerView rows visible on screen:
AppBarManager.java
public interface AppBarManager {
void collapseAppBar();
void expandAppBar();
int getVisibleHeightForRecyclerViewInPx();
}
MainActivity.java
public class MainActivity extends AppCompatActivity implements AppBarManager{
#Override
public void collapseAppBar() {
mAppBarLayout.setExpanded(false, true);
}
#Override
public void expandAppBar() {
mAppBarLayout.setExpanded(true, true);
}
#Override
public int getVisibleHeightForRecyclerViewInPx() {
if (mRecyclerFragment == null) mRecyclerFragment =
(RecyclerFragment) getSupportFragmentManager().findFragmentByTag(RecyclerFragment.TAG);
int windowHeight, appBarHeight, headerViewHeight;
windowHeight = getWindow().getDecorView().getHeight();
appBarHeight = mAppBarLayout.getHeight();
headerViewHeight = mRecyclerFragment.getHeaderView().getHeight();
return windowHeight - (appBarHeight + headerViewHeight);
}
My specific question is: How I can achieve an effect like this: http://youtu.be/EJm7subFbQI
The bounce effect is not important, but i need the "sticky" effect for the headers. Where do I start?, In what can I base me? I need something that I can implement on API 8 to up.
Thanks.
There are a few solutions that already exist for this problem. What you're describing are section headers and have come to be referred to as sticky section headers in Android.
Sticky List Headers
Sticky Scroll Views
HeaderListView
EDIT: Had some free time to add the code of fully working example. Edited the answer accordingly.
For those who don't want to use 3rd party code (or cannot use it directly, e.g. in Xamarin), this could be done fairly easily by hand.
The idea is to use another ListView for the header. This list view contains only the header items. It will not be scrollable by the user (setEnabled(false)), but will be scrolled from code based on main lists' scrolling. So you will have two lists - headerListview and mainListview, and two corresponding adapters headerAdapter and mainAdapter. headerAdapter only returns section views, while mainAdapter supports two view types (section and item). You will need a method that takes a position in the main list and returns a corresponding position in the sections list.
Main activity
public class MainActivity extends AppCompatActivity {
public static final int TYPE_SECTION = 0;
public static final int TYPE_ITEM = 1;
ListView mainListView;
ListView headerListView;
MainAdapter mainAdapter;
HeaderAdapter headerAdapter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mainListView = (ListView)findViewById(R.id.list);
headerListView = (ListView)findViewById(R.id.header);
mainAdapter = new MainAdapter();
headerAdapter = new HeaderAdapter();
headerListView.setEnabled(false);
headerListView.setAdapter(headerAdapter);
mainListView.setAdapter(mainAdapter);
mainListView.setOnScrollListener(new AbsListView.OnScrollListener(){
#Override
public void onScrollStateChanged(AbsListView view, int scrollState){
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
// this should return an index in the headers list, based one the index in the main list. The logic for this is highly dependent on your data.
int pos = mainAdapter.getSectionIndexForPosition(firstVisibleItem);
// this makes sure our headerListview shows the proper section (the one on the top of the mainListview)
headerListView.setSelection(pos);
// this makes sure that headerListview is scrolled exactly the same amount as the mainListview
if(mainAdapter.getItemViewType(firstVisibleItem + 1) == TYPE_SECTION){
headerListView.setSelectionFromTop(pos, mainListView.getChildAt(0).getTop());
}
}
});
}
public class MainAdapter extends BaseAdapter{
int count = 30;
#Override
public int getItemViewType(int position){
if((float)position / 10 == (int)((float)position/10)){
return TYPE_SECTION;
}else{
return TYPE_ITEM;
}
}
#Override
public int getViewTypeCount(){ return 2; }
#Override
public int getCount() { return count - 1; }
#Override
public Object getItem(int position) { return null; }
#Override
public long getItemId(int position) { return position; }
public int getSectionIndexForPosition(int position){ return position / 10; }
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = getLayoutInflater().inflate(R.layout.item, parent, false);
position++;
if(getItemViewType(position) == TYPE_SECTION){
((TextView)v.findViewById(R.id.text)).setText("SECTION "+position);
}else{
((TextView)v.findViewById(R.id.text)).setText("Item "+position);
}
return v;
}
}
public class HeaderAdapter extends BaseAdapter{
int count = 5;
#Override
public int getCount() { return count; }
#Override
public Object getItem(int position) { return null; }
#Override
public long getItemId(int position) { return position; }
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = getLayoutInflater().inflate(R.layout.item, parent, false);
((TextView)v.findViewById(R.id.text)).setText("SECTION "+position*10);
return v;
}
}
}
A couple of things to note here. We do not want to show the very first section in the main view list, because it would produce a duplicate (it's already shown in the header). To avoid that, in your mainAdapter.getCount():
return actualCount - 1;
and make sure the first line in your getView() method is
position++;
This way your main list will be rendering all cells but the first one.
Another thing is that you want to make sure your headerListview's height matches the height of the list item. In this example the height is fixed, but it could be tricky if your items height is not set to an exact value in dp. Please refer to this answer for how to address this: https://stackoverflow.com/a/41577017/291688
Main layout
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="#dimen/activity_vertical_margin"
android:paddingLeft="#dimen/activity_horizontal_margin"
android:paddingRight="#dimen/activity_horizontal_margin"
android:paddingTop="#dimen/activity_vertical_margin">
<ListView
android:id="#+id/header"
android:layout_width="match_parent"
android:layout_height="48dp"/>
<ListView
android:id="#+id/list"
android:layout_below="#+id/header"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>
Item / header layout
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="48dp">
<TextView
android:id="#+id/text"
android:gravity="center_vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</LinearLayout>
Add this in your app.gradle file
compile 'se.emilsjolander:StickyScrollViewItems:1.1.0'
then my layout, where I have added android:tag ="sticky" to specific views like textview or edittext not LinearLayout, looks like this. It also uses databinding, ignore that.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="temp"
type="com.lendingkart.prakhar.lendingkartdemo.databindingmodel.BusinessDetailFragmentModel" />
<variable
name="presenter"
type="com.lendingkart.prakhar.lendingkartdemo.presenters.BusinessDetailsPresenter" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.lendingkart.prakhar.lendingkartdemo.customview.StickyScrollView
android:id="#+id/sticky_scroll"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- scroll view child goes here -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
card_view:cardCornerRadius="5dp"
card_view:cardUseCompatPadding="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
style="#style/group_view_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#drawable/businessdetailtitletextviewbackground"
android:padding="#dimen/activity_horizontal_margin"
android:tag="sticky"
android:text="#string/business_contact_detail" />
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="7dp">
<android.support.design.widget.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/comapnyLabel"
android:textSize="16sp" />
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp">
<android.support.design.widget.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/contactLabel"
android:textSize="16sp" />
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp">
<android.support.design.widget.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/emailLabel"
android:textSize="16sp" />
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp">
<android.support.design.widget.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/NumberOfEmployee"
android:textSize="16sp" />
</android.support.design.widget.TextInputLayout>
</LinearLayout>
</android.support.v7.widget.CardView>
<android.support.v7.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
card_view:cardCornerRadius="5dp"
card_view:cardUseCompatPadding="true">
<TextView
style="#style/group_view_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#drawable/businessdetailtitletextviewbackground"
android:padding="#dimen/activity_horizontal_margin"
android:tag="sticky"
android:text="#string/nature_of_business" />
</android.support.v7.widget.CardView>
<android.support.v7.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
card_view:cardCornerRadius="5dp"
card_view:cardUseCompatPadding="true">
<TextView
style="#style/group_view_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#drawable/businessdetailtitletextviewbackground"
android:padding="#dimen/activity_horizontal_margin"
android:tag="sticky"
android:text="#string/taxation" />
</android.support.v7.widget.CardView>
</LinearLayout>
</com.lendingkart.prakhar.lendingkartdemo.customview.StickyScrollView>
</LinearLayout>
</layout>
style group for the textview looks this
<style name="group_view_text" parent="#android:style/TextAppearance.Medium">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:textColor">#color/edit_text_color</item>
<item name="android:textSize">16dp</item>
<item name="android:layout_centerVertical">true</item>
<item name="android:textStyle">bold</item>
</style>
and the background for the textview goes like this:(#drawable/businessdetailtitletextviewbackground)
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="#color/edit_text_color" />
</shape>
</item>
<item android:bottom="2dp">
<shape android:shape="rectangle">
<solid android:color="#color/White" />
</shape>
</item>
</layer-list>
For those looking for a solution in 2020, I have quickly created a solution extending the Layout Manager from ruslansharipov project (Sticky Header) and combining it whith the RecycleView Adapter from lisawray Groupie project (Expandable RecycleView).
You can see my example here
Result Here
You can reach this effect using SuperSLiM library. It provides you a LayoutManager for RecyclerView with interchangeable linear, grid, and staggered displays of views.
A good demo is located in github repository
It is simply to get such result
app:slm_headerDisplay="inline|sticky"
or
app:slm_headerDisplay="sticky"
I have used one special class to achieve listview like iPhone.
You can find example with source code here. https://demonuts.com/android-recyclerview-sticky-header-like-iphone/
This class which has updated listview is as
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.view.animation.AlphaAnimation;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
import android.widget.ListView;
import android.widget.RelativeLayout;
public class HeaderListView extends RelativeLayout {
// TODO: Handle listViews with fast scroll
// TODO: See if there are methods to dispatch to mListView
private static final int FADE_DELAY = 1000;
private static final int FADE_DURATION = 2000;
private InternalListView mListView;
private SectionAdapter mAdapter;
private RelativeLayout mHeader;
private View mHeaderConvertView;
private FrameLayout mScrollView;
private AbsListView.OnScrollListener mExternalOnScrollListener;
public HeaderListView(Context context) {
super(context);
init(context, null);
}
public HeaderListView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
mListView = new InternalListView(getContext(), attrs);
LayoutParams listParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
listParams.addRule(ALIGN_PARENT_TOP);
mListView.setLayoutParams(listParams);
mListView.setOnScrollListener(new HeaderListViewOnScrollListener());
mListView.setVerticalScrollBarEnabled(false);
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if (mAdapter != null) {
mAdapter.onItemClick(parent, view, position, id);
}
}
});
addView(mListView);
mHeader = new RelativeLayout(getContext());
LayoutParams headerParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
headerParams.addRule(ALIGN_PARENT_TOP);
mHeader.setLayoutParams(headerParams);
mHeader.setGravity(Gravity.BOTTOM);
addView(mHeader);
// The list view's scroll bar can be hidden by the header, so we display our own scroll bar instead
Drawable scrollBarDrawable = getResources().getDrawable(R.drawable.scrollbar_handle_holo_light);
mScrollView = new FrameLayout(getContext());
LayoutParams scrollParams = new LayoutParams(scrollBarDrawable.getIntrinsicWidth(), LayoutParams.MATCH_PARENT);
scrollParams.addRule(ALIGN_PARENT_RIGHT);
scrollParams.rightMargin = (int) dpToPx(2);
mScrollView.setLayoutParams(scrollParams);
ImageView scrollIndicator = new ImageView(context);
scrollIndicator.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
scrollIndicator.setImageDrawable(scrollBarDrawable);
scrollIndicator.setScaleType(ScaleType.FIT_XY);
mScrollView.addView(scrollIndicator);
mScrollView.setVisibility(INVISIBLE);
addView(mScrollView);
}
public void setAdapter(SectionAdapter adapter) {
mAdapter = adapter;
mListView.setAdapter(adapter);
}
public void setOnScrollListener(AbsListView.OnScrollListener l) {
mExternalOnScrollListener = l;
}
private class HeaderListViewOnScrollListener implements AbsListView.OnScrollListener {
private int previousFirstVisibleItem = -1;
private int direction = 0;
private int actualSection = 0;
private boolean scrollingStart = false;
private boolean doneMeasuring = false;
private int lastResetSection = -1;
private int nextH;
private int prevH;
private View previous;
private View next;
private AlphaAnimation fadeOut = new AlphaAnimation(1f, 0f);
private boolean noHeaderUpToHeader = false;
private boolean didScroll = false;
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (mExternalOnScrollListener != null) {
mExternalOnScrollListener.onScrollStateChanged(view, scrollState);
}
didScroll = true;
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (mExternalOnScrollListener != null) {
mExternalOnScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
}
if (!didScroll) {
return;
}
firstVisibleItem -= mListView.getHeaderViewsCount();
if (firstVisibleItem < 0) {
mHeader.removeAllViews();
return;
}
updateScrollBar();
if (visibleItemCount > 0 && firstVisibleItem == 0 && mHeader.getChildAt(0) == null) {
addSectionHeader(0);
lastResetSection = 0;
}
int realFirstVisibleItem = getRealFirstVisibleItem(firstVisibleItem, visibleItemCount);
if (totalItemCount > 0 && previousFirstVisibleItem != realFirstVisibleItem) {
direction = realFirstVisibleItem - previousFirstVisibleItem;
actualSection = mAdapter.getSection(realFirstVisibleItem);
boolean currIsHeader = mAdapter.isSectionHeader(realFirstVisibleItem);
boolean prevHasHeader = mAdapter.hasSectionHeaderView(actualSection - 1);
boolean nextHasHeader = mAdapter.hasSectionHeaderView(actualSection + 1);
boolean currHasHeader = mAdapter.hasSectionHeaderView(actualSection);
boolean currIsLast = mAdapter.getRowInSection(realFirstVisibleItem) == mAdapter.numberOfRows(actualSection) - 1;
boolean prevHasRows = mAdapter.numberOfRows(actualSection - 1) > 0;
boolean currIsFirst = mAdapter.getRowInSection(realFirstVisibleItem) == 0;
boolean needScrolling = currIsFirst && !currHasHeader && prevHasHeader && realFirstVisibleItem != firstVisibleItem;
boolean needNoHeaderUpToHeader = currIsLast && currHasHeader && !nextHasHeader && realFirstVisibleItem == firstVisibleItem && Math.abs(mListView.getChildAt(0).getTop()) >= mListView.getChildAt(0).getHeight() / 2;
noHeaderUpToHeader = false;
if (currIsHeader && !prevHasHeader && firstVisibleItem >= 0) {
resetHeader(direction < 0 ? actualSection - 1 : actualSection);
} else if ((currIsHeader && firstVisibleItem > 0) || needScrolling) {
if (!prevHasRows) {
resetHeader(actualSection-1);
}
startScrolling();
} else if (needNoHeaderUpToHeader) {
noHeaderUpToHeader = true;
} else if (lastResetSection != actualSection) {
resetHeader(actualSection);
}
previousFirstVisibleItem = realFirstVisibleItem;
}
if (scrollingStart) {
int scrolled = realFirstVisibleItem >= firstVisibleItem ? mListView.getChildAt(realFirstVisibleItem - firstVisibleItem).getTop() : 0;
if (!doneMeasuring) {
setMeasurements(realFirstVisibleItem, firstVisibleItem);
}
int headerH = doneMeasuring ? (prevH - nextH) * direction * Math.abs(scrolled) / (direction < 0 ? nextH : prevH) + (direction > 0 ? nextH : prevH) : 0;
mHeader.scrollTo(0, -Math.min(0, scrolled - headerH));
if (doneMeasuring && headerH != mHeader.getLayoutParams().height) {
LayoutParams p = (LayoutParams) (direction < 0 ? next.getLayoutParams() : previous.getLayoutParams());
p.topMargin = headerH - p.height;
mHeader.getLayoutParams().height = headerH;
mHeader.requestLayout();
}
}
if (noHeaderUpToHeader) {
if (lastResetSection != actualSection) {
addSectionHeader(actualSection);
lastResetSection = actualSection + 1;
}
mHeader.scrollTo(0, mHeader.getLayoutParams().height - (mListView.getChildAt(0).getHeight() + mListView.getChildAt(0).getTop()));
}
}
private void startScrolling() {
scrollingStart = true;
doneMeasuring = false;
lastResetSection = -1;
}
private void resetHeader(int section) {
scrollingStart = false;
addSectionHeader(section);
mHeader.requestLayout();
lastResetSection = section;
}
private void setMeasurements(int realFirstVisibleItem, int firstVisibleItem) {
if (direction > 0) {
nextH = realFirstVisibleItem >= firstVisibleItem ? mListView.getChildAt(realFirstVisibleItem - firstVisibleItem).getMeasuredHeight() : 0;
}
previous = mHeader.getChildAt(0);
prevH = previous != null ? previous.getMeasuredHeight() : mHeader.getHeight();
if (direction < 0) {
if (lastResetSection != actualSection - 1) {
addSectionHeader(Math.max(0, actualSection - 1));
next = mHeader.getChildAt(0);
}
nextH = mHeader.getChildCount() > 0 ? mHeader.getChildAt(0).getMeasuredHeight() : 0;
mHeader.scrollTo(0, prevH);
}
doneMeasuring = previous != null && prevH > 0 && nextH > 0;
}
private void updateScrollBar() {
if (mHeader != null && mListView != null && mScrollView != null) {
int offset = mListView.computeVerticalScrollOffset();
int range = mListView.computeVerticalScrollRange();
int extent = mListView.computeVerticalScrollExtent();
mScrollView.setVisibility(extent >= range ? View.INVISIBLE : View.VISIBLE);
if (extent >= range) {
return;
}
int top = range == 0 ? mListView.getHeight() : mListView.getHeight() * offset / range;
int bottom = range == 0 ? 0 : mListView.getHeight() - mListView.getHeight() * (offset + extent) / range;
mScrollView.setPadding(0, top, 0, bottom);
fadeOut.reset();
fadeOut.setFillBefore(true);
fadeOut.setFillAfter(true);
fadeOut.setStartOffset(FADE_DELAY);
fadeOut.setDuration(FADE_DURATION);
mScrollView.clearAnimation();
mScrollView.startAnimation(fadeOut);
}
}
private void addSectionHeader(int actualSection) {
View previousHeader = mHeader.getChildAt(0);
if (previousHeader != null) {
mHeader.removeViewAt(0);
}
if (mAdapter.hasSectionHeaderView(actualSection)) {
mHeaderConvertView = mAdapter.getSectionHeaderView(actualSection, mHeaderConvertView, mHeader);
mHeaderConvertView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
mHeaderConvertView.measure(MeasureSpec.makeMeasureSpec(mHeader.getWidth(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
mHeader.getLayoutParams().height = mHeaderConvertView.getMeasuredHeight();
mHeaderConvertView.scrollTo(0, 0);
mHeader.scrollTo(0, 0);
mHeader.addView(mHeaderConvertView, 0);
} else {
mHeader.getLayoutParams().height = 0;
mHeader.scrollTo(0, 0);
}
mScrollView.bringToFront();
}
private int getRealFirstVisibleItem(int firstVisibleItem, int visibleItemCount) {
if (visibleItemCount == 0) {
return -1;
}
int relativeIndex = 0, totalHeight = mListView.getChildAt(0).getTop();
for (relativeIndex = 0; relativeIndex < visibleItemCount && totalHeight < mHeader.getHeight(); relativeIndex++) {
totalHeight += mListView.getChildAt(relativeIndex).getHeight();
}
int realFVI = Math.max(firstVisibleItem, firstVisibleItem + relativeIndex - 1);
return realFVI;
}
}
public ListView getListView() {
return mListView;
}
public void addHeaderView(View v) {
mListView.addHeaderView(v);
}
private float dpToPx(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getContext().getResources().getDisplayMetrics());
}
protected class InternalListView extends ListView {
public InternalListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
protected int computeVerticalScrollExtent() {
return super.computeVerticalScrollExtent();
}
#Override
protected int computeVerticalScrollOffset() {
return super.computeVerticalScrollOffset();
}
#Override
protected int computeVerticalScrollRange() {
return super.computeVerticalScrollRange();
}
}
}
XML usage
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.parsaniahardik.listview_stickyheader_ios.HeaderListView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/lv">
</com.example.parsaniahardik.listview_stickyheader_ios.HeaderListView>