I need to move the listview up when I show a Snackbar in the bottom of the screen.
The snackbar works perfectly, but hide the content of the bottom of the listview.
This is the layout:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:elevation="4dp"
android:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar" />
<TextView
android:id="#+id/lblInformacion"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="#color/md_green_500"
android:padding="3dip"
android:textColor="#android:color/white"
android:textStyle="bold" />
<android.support.design.widget.CoordinatorLayout
android:id="#+id/clClientes"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<ListView
android:id="#+id/lvClientes"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
<TextView
android:id="#+id/lblEmpty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="No Result"
android:textSize="20sp"
android:visibility="gone" />
</RelativeLayout>
</android.support.design.widget.CoordinatorLayout>
class ListSnackbarBehavior extends AppBarLayout.ScrollingViewBehavior {
#Override
public boolean layoutDependsOn(CoordinatorLayout parent,
View child, View dependency) {
return super.layoutDependsOn(parent, child, dependency) && dependency instanceof Snackbar.SnackbarLayout;
}
#Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child,
View dependency) {
if (dependency instanceof Snackbar.SnackbarLayout) {
updateListTranslationForSnackbar(parent, child, dependency);
}
return false;
}
private void updateListTranslationForSnackbar(CoordinatorLayout parent, View child, View dependency) {
int translationY = (int) getListTranslationYForSnackbar(parent, child);
if (translationY > -1) {
ViewCompat.animate(child).translationY(-translationY).setDuration(100)
.setInterpolator(new LinearOutSlowInInterpolator()).start();
}
}
private float getListTranslationYForSnackbar(CoordinatorLayout parent,
View listView) {
View lastVisibleView = listView instanceof RecyclerView ?
getLastViewFromRecyclerView(listView) : listView instanceof ListView ? getLastViewFromListView(listView) : null;
float minOffset = 0;
if (lastVisibleView != null) {
final List<View> dependencies = parent.getDependencies(lastVisibleView);
for (int i = 0, z = dependencies.size(); i < z; i++) {
final View view = dependencies.get(i);
if (view instanceof Snackbar.SnackbarLayout && parent.doViewsOverlap(lastVisibleView, view)) {
minOffset = Math.min(minOffset,
ViewCompat.getTranslationY(view) - view.getHeight());
}
}
}
return -1;
}
private View getLastViewFromListView(View view) {
ListView listView = (ListView) view;
if (listView.getAdapter() != null && listView.getAdapter().getCount() > 0) {
int lastVisiblePosition = listView.getLastVisiblePosition();
if (lastVisiblePosition == listView.getAdapter().getCount() - 1) {
listView.smoothScrollToPosition(lastVisiblePosition);
return listView.getChildAt(listView.getChildCount() - 1);
}
}
return null;
}
private View getLastViewFromRecyclerView(View listView) {
RecyclerView recyclerView = (RecyclerView) listView;
if (recyclerView.getAdapter() != null && recyclerView.getAdapter().getItemCount() > 0) {
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
int lastPartiallyVisiblePosition = linearLayoutManager.findLastVisibleItemPosition();
if (lastPartiallyVisiblePosition == recyclerView.getAdapter().getItemCount() - 1) {
linearLayoutManager.scrollToPosition(lastPartiallyVisiblePosition);
return linearLayoutManager.getChildAt(lastPartiallyVisiblePosition);
}
}
return null;
}
}
I'm not sure if I got the translation y correct, but here is the deal:
You react only when there are changes on SnackbarLayout
Find if we are near the last item in the list
If the view overlaps, scroll to make it completely visible.
Lift the list to minus the offset when the Snackbar shows up and 0 when hides.
Related
Hi I've been following this link to accomplish the opacity effect on the toolbar
My Layout File :
<?xml version="1.0" encoding="utf-8"?>
<layout>
<android.support.design.widget.CoordinatorLayout
android:id="#+id/coordinator_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".ProductDetailActivity">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#android:color/transparent"
android:theme="#style/AppTheme.AppBarOverlay"
app:elevation="0dp" />
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
.
.
.
</android.support.v4.widget.NestedScrollView>
<android.support.v7.widget.Toolbar
android:id="#+id/prod_detail_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_behavior="util.ToolbarAlphaScrollBehavior"
/>
</android.support.design.widget.CoordinatorLayout>
</layout>
And The Class :
public class ToolbarAlphaScrollBehavior extends CoordinatorLayout.Behavior<android.support.v7.widget.Toolbar> {
private static final String TAG = ToolbarAlphaScrollBehavior.class.getSimpleName();
private ColorDrawable mStatusBarColorDrawable;
private int mStatusBarColor;
private TextView mTitleView;
private boolean searchedForTitleView = false;
private Context context;
public ToolbarAlphaScrollBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
mStatusBarColor = ContextCompat.getColor(context, R.color.colorPrimaryDark);
mStatusBarColor = getColorWithAlpha(0, mStatusBarColor);
mStatusBarColorDrawable = new ColorDrawable(mStatusBarColor);
this.context=context;
}
public static int getColorWithAlpha(float alpha, int baseColor) {
int a = Math.min(255, Math.max(0, (int) (alpha * 255))) << 24;
int rgb = 0x00ffffff & baseColor;
return a + rgb;
}
public ToolbarAlphaScrollBehavior() {
}
#Override
public boolean layoutDependsOn(CoordinatorLayout parent, Toolbar child, View dependency) {
return dependency instanceof AppBarLayout;
}
#Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, Toolbar child, MotionEvent ev) {
return ev == null || super.onInterceptTouchEvent(parent, child, ev);
}
#Override
public boolean onDependentViewChanged(CoordinatorLayout parent, Toolbar child, View dependency) {
Log.e(TAG, "child "+child.getTitle() );
if (dependency instanceof AppBarLayout) {
float ratio = (float) getCurrentScrollValue(child, dependency) / getTotalScrollRange(child, dependency);
float alpha = 1f - Math.min(1f, Math.max(0f, ratio));
int drawableAlpha = (int) (alpha * 255);
// Log.i("ToolbarAlphaScrollBehavior", "Alpha: " + alpha);
if (Build.VERSION.SDK_INT==Build.VERSION_CODES.LOLLIPOP ||
Build.VERSION.SDK_INT==Build.VERSION_CODES.LOLLIPOP_MR1) {
child.getBackground().setAlpha(drawableAlpha);
} else if (Build.VERSION.SDK_INT==Build.VERSION_CODES.KITKAT) {
ViewGroup toolbarParent = (ViewGroup) child.getParent();
if (toolbarParent.getChildCount() == 2) {
int count = toolbarParent.getChildCount();
for (int i = count - 1; i >= 0; i--) {
toolbarParent.getChildAt(i).getBackground().setAlpha(drawableAlpha);
}
}
} else {
child.getBackground().setAlpha(drawableAlpha);
}
setStatusBarColor(parent, drawableAlpha);
if (mTitleView != null) {
ViewCompat.setAlpha(mTitleView, alpha);
return false;
}
if (!searchedForTitleView) {
TextView textView= new TextView(context);
textView.setText(child.getTitle());
mTitleView = textView;
searchedForTitleView = true;
}
}
return false;
}
private void setStatusBarColor(CoordinatorLayout parent, int alpha) {
ColorDrawable statusBarBackground = (ColorDrawable) parent.getStatusBarBackground();
statusBarBackground.setColor(getColorWithAlpha(alpha, statusBarBackground.getColor()));
parent.setStatusBarBackground(statusBarBackground);
}
private int getCurrentScrollValue(Toolbar child, View dependency) {
return dependency.getBottom() - child.getTop();
}
private float getTotalScrollRange(Toolbar child, View dependency) {
return Math.max(dependency.getHeight(), ((AppBarLayout) dependency).getTotalScrollRange()) - child.getTop();
}
}
But i am getting the following error
Attempt to invoke virtual method 'void android.graphics.drawable.Drawable.setAlpha(int)' on a null object reference
The child in onDependentViewChanged is null
The behavior i am trying to accomplish is this one : Youtube video.
The toolbar must be transparent in the beginning and on scroll the color should
change to the primary color
Thanks in Advance...
Attempt to invoke virtual method 'void android.graphics.drawable.Drawable.setAlpha(int)' on a null object reference
<android.support.v7.widget.Toolbar
android:id="#+id/prod_detail_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background = "#color/colorPrimary"
app:layout_behavior="util.ToolbarAlphaScrollBehavior"
/>
I did everything I needed and got this - but the toolbar is always transparent on KitKat .
link: Transparent toolbar
you can set...
mToolbar.setBackgroundColor(ContextCompat.getColor(getActivity(),android.R.color.transparent));
or set background in xml of toolbar!
i have created a circular recyclerview by making adapter count into Integer.MAX.Now, i need to highlight the center recycler item like in the image.Kindly help me!!
By using center indicator(textview) in the layout and addOnScrollListner we can achieve this
please refer the following example
In xml:
<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"
>
<TextView
android:layout_width="wrap_content"
android:text="↓"
android:id="#+id/centerIndicator"
android:textSize="24sp"
android:textStyle="bold"
android:visibility="visible"
android:textColor="#color/theme_yellow"
android:layout_centerHorizontal="true"
android:layout_height="wrap_content"
android:layout_marginTop="27dp"
android:background="#android:color/transparent"
/>
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:id="#+id/list"
android:clipToPadding="false"
android:divider="#android:color/transparent"
android:layout_height="wrap_content"/>
</RelativeLayout>
In Activity/Fragment:
public class Sample extends Fragment {
RecyclerView listView;
ArrayList<String>mWeekDaysList=new ArrayList<>();
LinearLayoutManager mlinearLayoutManagerForDateList;
DateAdapter mDateAdapter;
TimeListAdapter mtimeAdapter;
private int mCenterPivot;
private boolean mAutoSet = true;
Activity mactivity;
public NigaichiNiralFrag() {
// Required empty public constructor
}
#Override
public View onCreateView(final LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View view= inflater.inflate(R.layout.fragment_nigaichi_niral, container, false);
mactivity=getActivity();
mWeekDaysList.add("Sunday");
mWeekDaysList.add("Monday");
mWeekDaysList.add("Tuesday");
mWeekDaysList.add("Wednesday");
mWeekDaysList.add("Thursday");
mWeekDaysList.add("Friday");
mWeekDaysList.add("Saturday");
listView = (RecyclerView) view.findViewById(R.id.list);
mlinearLayoutManagerForDateList = new LinearLayoutManager(mactivity);
mlinearLayoutManagerForDateList.setOrientation(LinearLayoutManager.HORIZONTAL);
listView.setLayoutManager(mlinearLayoutManagerForDateList);
final TextView mCenterIndicator = (TextView) view.findViewById(R.id.centerIndicator);
final int itemWidth = (int) getResources().getDimension(R.dimen.flexible_space_image_height) ;
mlinearLayoutManagerForDateList.scrollToPosition(Integer.MAX_VALUE / 2);
mDateAdapter=new DateAdapter(mWeekDaysList);
listView.setAdapter(mDateAdapter);
mCenterIndicator.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
int center = ( mCenterIndicator.getLeft() + mCenterIndicator.getRight() ) / 2 ;
int padding = center - itemWidth / 2; //Assuming both left and right padding needed are the same
listView.setPadding(5,0,5,0);
mCenterPivot = center;
listView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
LinearLayoutManager lm = (LinearLayoutManager) recyclerView.getLayoutManager();
if( mCenterPivot == 0 ) {
// Default pivot , Its a bit inaccurate .
// Better pass the center pivot as your Center Indicator view's
// calculated center on it OnGlobalLayoutListener event
mCenterPivot = lm.getOrientation() == LinearLayoutManager.HORIZONTAL ? ( recyclerView.getLeft() + recyclerView.getRight() ) : ( recyclerView.getTop() + recyclerView.getBottom() );
}
if( !mAutoSet ) {
if( newState == RecyclerView.SCROLL_STATE_IDLE ) {
//ScrollStoppped
View view = findCenterView(lm);//get the view nearest to center
//view.setBackgroundColor(Color.RED);
int position = recyclerView.getChildAdapterPosition(view) % mWeekDaysList.size();
Log.d("isideScroll",mWeekDaysList.get(position));
mDateAdapter.setSelecteditem(position);
int viewCenter = lm.getOrientation() == LinearLayoutManager.HORIZONTAL ? ( view.getLeft() + view.getRight() )/2 :( view.getTop() + view.getBottom() )/2;
//compute scroll from center
int scrollNeeded = viewCenter - mCenterPivot; // Add or subtract any offsets you need here
if( lm.getOrientation() == LinearLayoutManager.HORIZONTAL ) {
recyclerView.smoothScrollBy(scrollNeeded, 0);
}
else
{
recyclerView.smoothScrollBy(0, (int) (scrollNeeded));
}
mAutoSet =true;
}
}
if( newState == RecyclerView.SCROLL_STATE_DRAGGING || newState == RecyclerView.SCROLL_STATE_SETTLING ){
mAutoSet =false;
}
}
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
}
});
}
});
return returnView;
}
private void scrollToCenter(View v) {
int itemToScroll = listView.getChildAdapterPosition(v);
int centerOfScreen = listView.getWidth() / 2 - v.getWidth() / 2;
//v.setBackgroundColor(Color.RED);
mlinearLayoutManagerForDateList.scrollToPositionWithOffset(itemToScroll, centerOfScreen);
}
private View findCenterView(LinearLayoutManager lm) {
int minDistance = 0;
View view = null;
View returnView = null;
boolean notFound = true;
for(int i = lm.findFirstVisibleItemPosition(); i <= lm.findLastVisibleItemPosition() && notFound ; i++ ) {
view=lm.findViewByPosition(i);
int center = lm.getOrientation() == LinearLayoutManager.HORIZONTAL ? ( view.getLeft() + view.getRight() )/ 2 : ( view.getTop() + view.getBottom() )/ 2;
int leastDifference = Math.abs(mCenterPivot - center);
if( leastDifference <= minDistance || i == lm.findFirstVisibleItemPosition())
{
minDistance = leastDifference;
returnView=view;
}
else
{
notFound=false;
}
}
return returnView;
}
}
Adapter:
public class DateAdapter extends RecyclerView.Adapter<DateAdapter.ReviewHolder> {
ArrayList<String> mData;
private int selectedItem = -1;
int pos=0;
public DateAdapter(ArrayList<String> data){
mData=data;
}
#Override
public ReviewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
Context context = parent.getContext();
View v= LayoutInflater.from(context).inflate(R.layout.item_horz,parent,false);
return new DateAdapter.ReviewHolder(v);
}
#Override
public void onBindViewHolder(ReviewHolder holder, int position) {
pos=position;
position = position % mData.size();
holder.tvName.setText(mData.get(position));
holder.tvName.setGravity(Gravity.CENTER);
if (position == selectedItem) {
Log.d("CenterPosition", "center" + position);
holder.tvName.setTextColor(Color.RED);
holder.tvName.setTextSize(20);
holder.tvName.setBackgroundColor(Color.parseColor("#fccd00"));
} else {
holder.tvName.setTextColor(Color.WHITE);
holder.tvName.setTextSize(16);
holder.tvName.setBackgroundColor(Color.BLACK);
}
}
#Override
public int getItemCount() {
// return mData.size();
return Integer.MAX_VALUE;
}
public class ReviewHolder extends RecyclerView.ViewHolder {
protected TextView tvName;
View container;
public ReviewHolder(View itemView) {
super(itemView);
container=itemView;
tvName= (TextView) itemView.findViewById(R.id.text);
}
}
public void setSelecteditem(int selecteditem) {
Log.d("POSITION",String.valueOf(selecteditem));
this.selectedItem = selecteditem;
notifyDataSetChanged();
}
}
item_horz.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="151dp"
android:id="#+id/wrapper"
android:background="#color/white"
android:orientation="horizontal"
android:layout_height="50dp">
<LinearLayout
android:layout_width="150dp"
android:layout_height="50dp"
android:background="#color/black">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textSize="20sp"
android:id="#+id/text"
android:textColor="#color/white"
android:text="21"
android:gravity="center"
/>
</LinearLayout>
</LinearLayout>
Hope this will help you guys..
if you want to use horizontalscrollview (create items dynamically)instead recyclerview.
create your parent layout
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true">
<LinearLayout
android:layout_alignParentBottom="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"/>
</HorizontalScrollView>
inflate your childs items to parent layout.
LayoutInflater inflater = (LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
for (int i = 0; i < list.size(); i++) {
// inflate child element
final LinearLayout titleRow = (LinearLayout) inflater.inflate(R.layout.item_gallery, fragmentGalleryScrollLl, false);
final CircleImageView shapeImageView = (CircleImageView) titleRow.findViewById(R.id.item_gallery_iv);
shapeImageView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
scrollItem(titleRow);
}
});
fragmentGalleryScrollLl.addView(titleRow);
}
use this method when your click the items.
private void scrollItem(LinearLayout titleRow) {
int scrollX = (titleRow.getLeft() - (fragmentGalleryScrollSv.getWidth() / 2)) + (titleRow.getWidth() / 2);
fragmentGalleryScrollSv.smoothScrollTo(scrollX, 0);
}
Inside BindView, you can check middle position of the adapter and apply logic for image for that specific holder.
onBindViewHolder(View holder, int postion){
if(position == getItemCount() / 2)
{ //Write image logic for holder
}}
Callback to get center item position when scrolling stopped
import android.content.Context
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
class RecyclerCenterItemFinder(
private val context: Context,
private val layoutManager: LinearLayoutManager,
private val callback: (Int) -> Unit,
private val controlState: Int = RecyclerView.SCROLL_STATE_IDLE
) :
RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (controlState == ALL_STATES || newState == controlState) {
val firstVisible = layoutManager.findFirstVisibleItemPosition()
val lastVisible = layoutManager.findLastVisibleItemPosition()
val itemsCount = lastVisible - firstVisible + 1
val screenCenter: Int = context.resources.displayMetrics.widthPixels / 2
var minCenterOffset = Int.MAX_VALUE
var middleItemIndex = 0
for (index in 0 until itemsCount) {
val listItem = layoutManager.getChildAt(index) ?: return
val topOffset = listItem.top
val bottomOffset = listItem.bottom
val centerOffset =
Math.abs(topOffset - screenCenter) + Math.abs(bottomOffset - screenCenter)
if (minCenterOffset > centerOffset) {
minCenterOffset = centerOffset
middleItemIndex = index + firstVisible
}
}
callback(middleItemIndex)
}
}
companion object {
const val ALL_STATES = 10
}
}
recycler.addOnScrollListener(RecyclerCenterItemFinder(requireContext(),
recycler.layoutManager,
{ centerItemPosition ->
// do something
}
))
i have a BottomNavigationView activity . in that i placed a webview as fragment , but the problem is ,user can not click on the web contents on the bottom side, because of my BottomNavigationView , is there any one to suggest me a good solution
this is my activity_main.xml
<?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:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.hackerinside.jaisonjoseph.polysocial.MainActivity">
<FrameLayout
android:id="#+id/content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="#android:color/holo_blue_dark">
<TextView
android:id="#+id/message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="#dimen/activity_vertical_margin"
android:layout_marginLeft="#dimen/activity_horizontal_margin"
android:layout_marginRight="#dimen/activity_horizontal_margin"
android:layout_marginTop="#dimen/activity_vertical_margin"
/>
</FrameLayout>
<android.support.design.widget.BottomNavigationView
android:id="#+id/navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="?android:attr/windowBackground"
android:layout_alignParentBottom="true"
app:menu="#menu/navigation" />
this is my webview fragment
<LinearLayout 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:orientation="vertical"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context="com.hackerinside.jaisonjoseph.polysocial.tab1">
<FrameLayout
android:id="#+id/frame1"
android:layout_width="match_parent"
android:layout_height="3dp"
android:background="#android:color/transparent">
<ProgressBar
android:id="#+id/progressBar1"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="fill_parent"
android:layout_height="3dp"
android:background="#android:color/transparent"
android:foregroundGravity="top"
android:progressDrawable="#drawable/custom_progress"
android:progress="20"/>
</FrameLayout>
<android.support.v4.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/swiperefresh1"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.hackerinside.jaisonjoseph.polysocial.EulaWebView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/webview"
android:focusable="true"
android:focusableInTouchMode="true" />
</android.support.v4.widget.SwipeRefreshLayout>
Try a Custom scrolling behaviour for the BottomNavigationView which allow you to hide it during scroll.
From Link
public final class BottomNavigationBehavior<V extends View> extends VerticalScrollingBehavior<V> {
private static final Interpolator INTERPOLATOR = new LinearOutSlowInInterpolator();
private final BottomNavigationWithSnackbar mWithSnackBarImpl = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? new LollipopBottomNavWithSnackBarImpl() : new PreLollipopBottomNavWithSnackBarImpl();
private boolean isTablet;
private int mTabLayoutId;
private boolean hidden = false;
private ViewPropertyAnimatorCompat mOffsetValueAnimator;
private ViewGroup mTabLayout;
private View mTabsHolder;
private int mSnackbarHeight = -1;
private boolean scrollingEnabled = true;
private boolean hideAlongSnackbar = false;
int[] attrsArray = new int[] {
android.R.attr.id };
public BottomNavigationBehavior() {
super();
}
public BottomNavigationBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs,
attrsArray);
mTabLayoutId = a.getResourceId(0, View.NO_ID);
a.recycle();
}
public static <V extends View> BottomNavigationBehavior<V> from(#NonNull V view) {
ViewGroup.LayoutParams params = view.getLayoutParams();
if (!(params instanceof CoordinatorLayout.LayoutParams)) {
throw new IllegalArgumentException("The view is not a child of CoordinatorLayout");
}
CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) params)
.getBehavior();
if (!(behavior instanceof BottomNavigationBehavior)) {
throw new IllegalArgumentException(
"The view is not associated with BottomNavigationBehavior");
}
return (BottomNavigationBehavior<V>) behavior;
}
#Override
public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
mWithSnackBarImpl.updateSnackbar(parent, dependency, child);
return dependency instanceof Snackbar.SnackbarLayout;
}
#Override
public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {
updateScrollingForSnackbar(dependency, child, true);
super.onDependentViewRemoved(parent, child, dependency);
}
private void updateScrollingForSnackbar(View dependency, V child, boolean enabled) {
if (!isTablet && dependency instanceof Snackbar.SnackbarLayout) {
scrollingEnabled = enabled;
if (!hideAlongSnackbar && ViewCompat.getTranslationY(child) != 0) {
ViewCompat.setTranslationY(child, 0);
hidden = false;
hideAlongSnackbar = true;
}else if(hideAlongSnackbar){
hidden = true;
animateOffset(child, -child.getHeight());
}
}
}
#Override
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
updateScrollingForSnackbar(dependency, child, false);
return super.onDependentViewChanged(parent, child, dependency);
}
#Override
public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
boolean layoutChild = super.onLayoutChild(parent, child, layoutDirection);
if (mTabLayout == null && mTabLayoutId != View.NO_ID) {
mTabLayout = findTabLayout(child);
getTabsHolder();
}
return layoutChild;
}
#Nullable
private ViewGroup findTabLayout(#NonNull View child) {
if (mTabLayoutId == 0) return null;
return (ViewGroup) child.findViewById(mTabLayoutId);
}
#Override
public void onNestedVerticalOverScroll(CoordinatorLayout coordinatorLayout, V child, #ScrollDirection int direction, int currentOverScroll, int totalOverScroll) {
}
#Override
public void onDirectionNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dx, int dy, int[] consumed, #ScrollDirection int scrollDirection) {
handleDirection(child, scrollDirection);
}
private void handleDirection(V child, #ScrollDirection int scrollDirection) {
if (!scrollingEnabled) return;
if (scrollDirection == ScrollDirection.SCROLL_DIRECTION_DOWN && hidden) {
hidden = false;
animateOffset(child, 0);
} else if (scrollDirection == ScrollDirection.SCROLL_DIRECTION_UP && !hidden) {
hidden = true;
animateOffset(child, child.getHeight());
}
}
#Override
protected boolean onNestedDirectionFling(CoordinatorLayout coordinatorLayout, V child, View target, float velocityX, float velocityY, #ScrollDirection int scrollDirection) {
handleDirection(child, scrollDirection);
return true;
}
private void animateOffset(final V child, final int offset) {
ensureOrCancelAnimator(child);
mOffsetValueAnimator.translationY(offset).start();
animateTabsHolder(offset);
}
private void animateTabsHolder(int offset) {
if (mTabsHolder != null) {
offset = offset > 0 ? 0 : 1;
ViewCompat.animate(mTabsHolder).alpha(offset).setDuration(200).start();
}
}
private void ensureOrCancelAnimator(V child) {
if (mOffsetValueAnimator == null) {
mOffsetValueAnimator = ViewCompat.animate(child);
mOffsetValueAnimator.setDuration(100);
mOffsetValueAnimator.setInterpolator(INTERPOLATOR);
} else {
mOffsetValueAnimator.cancel();
}
}
private void getTabsHolder() {
if (mTabLayout != null) {
mTabsHolder = mTabLayout.getChildAt(0);
}
}
public boolean isScrollingEnabled() {
return scrollingEnabled;
}
public void setScrollingEnabled(boolean scrollingEnabled) {
this.scrollingEnabled = scrollingEnabled;
}
public void setHidden(V view, boolean bottomLayoutHidden) {
if (!bottomLayoutHidden && hidden) {
animateOffset(view, 0);
} else if (bottomLayoutHidden && !hidden) {
animateOffset(view, -view.getHeight());
}
hidden = bottomLayoutHidden;
}
private interface BottomNavigationWithSnackbar {
void updateSnackbar(CoordinatorLayout parent, View dependency, View child);
}
private class PreLollipopBottomNavWithSnackBarImpl implements BottomNavigationWithSnackbar {
#Override
public void updateSnackbar(CoordinatorLayout parent, View dependency, View child) {
if (!isTablet && dependency instanceof Snackbar.SnackbarLayout) {
if (mSnackbarHeight == -1) {
mSnackbarHeight = dependency.getHeight();
}
int targetPadding = child.getMeasuredHeight();
int shadow = (int) ViewCompat.getElevation(child);
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) dependency.getLayoutParams();
layoutParams.bottomMargin = targetPadding - shadow;
child.bringToFront();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
child.getParent().requestLayout();
((View) child.getParent()).invalidate();
}
}
}
}
private class LollipopBottomNavWithSnackBarImpl implements BottomNavigationWithSnackbar {
#Override
public void updateSnackbar(CoordinatorLayout parent, View dependency, View child) {
if (!isTablet && dependency instanceof Snackbar.SnackbarLayout) {
if (mSnackbarHeight == -1) {
mSnackbarHeight = dependency.getHeight();
}
int targetPadding = (mSnackbarHeight +
child.getMeasuredHeight());
dependency.setPadding(dependency.getPaddingLeft(),
dependency.getPaddingTop(), dependency.getPaddingRight(), targetPadding
);
}
}
}
}
apply it in your view:
<android.support.design.widget.BottomNavigationView
android:id="#+id/bottom_navigation"
android:layout_gravity="bottom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
app:itemBackground="#color/colorPrimary"
app:itemIconTint="#color/white"
app:itemTextColor="#color/white"
app:layout_behavior=".BottomNavigationBehavior" //apply the behaviour
app:menu="#menu/bottom_navigation_main" />
output:
So easy, use CoordinatorLayout.
1. Change your root View to CoordinatorLayout
Remember to add the dependency in gradle file:
implementation 'com.google.android.material:material:1.1.0'
2. Wrapping Content View into NestedScrollView
<androidx.core.widget.NestedScrollView //or use ScrollView with "android:nestedScrollingEnabled="true"", see below
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<WebView
android:id="#+id/webView_webView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.core.widget.NestedScrollView>
3. Adding "layout_behavior" to your Bottom View
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior">
This is what I'm trying to achieve:
I've noticed the layoutDependsOn() and onDependentViewChanged() are never called at all.
For the sake of testing, I tried using "scaling" instead of "set XY" for the custom behavior:
public class CustomBehavior extends CoordinatorLayout.Behavior<LinearLayout>
{
public CustomBehavior(Context context, AttributeSet attrs)
{
super(context, attrs);
}
#Override
public boolean layoutDependsOn(CoordinatorLayout parent, LinearLayout child, View dependency)
{
return dependency instanceof LinearLayout;
}
#Override
public boolean onDependentViewChanged(CoordinatorLayout parent, LinearLayout child, View dependency)
{
float translationY = getTranslationY(parent, child);
float percentComplete = -translationY / dependency.getHeight();
float scaleFactor = 1 - percentComplete;
child.setScaleX(scaleFactor);
child.setScaleY(scaleFactor);
return false;
}
private float getTranslationY(CoordinatorLayout parent, LinearLayout child)
{
float minOffset = 0;
final List<View> dependencies = parent.getDependencies(child);
for (int i = 0, z = dependencies.size(); i < z; i++)
{
final View view = dependencies.get(i);
if (view instanceof LinearLayout && parent.doViewsOverlap(child, view))
{
minOffset = Math.min(minOffset, ViewCompat.getTranslationY(view) - view.getHeight());
}
}
return minOffset;
}
}
And this is the fragment's xml:
<android.support.design.widget.CoordinatorLayout 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="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
android:clickable="true"
android:orientation="vertical">
<LinearLayout
android:id="#+id/myLinearLayout"
android:layout_width="330dp"
android:layout_marginTop="30dp"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:orientation="vertical"
app:layout_behavior="com.test.CustomBehavior">
...
</LinearLayout>
</RelativeLayout>
<LinearLayout
android:id="#+id/bottomSheetLayout"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:behavior_peekHeight="100dp"
app:layout_behavior="android.support.design.widget.BottomSheetBehavior">
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>
Your LinearLayout is a child of RelativeLayout and thus app:layout_behavior doesn't have any effect.
You can only assign app:layout_behavior to direct children of the CoordinatorLayout.
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>