RecyclerView in ConstraintLayout. Show only complety visible items on a screen - android

I need to create a horizontal list of items that only displays fully visible items.
But as you can see, my recycler view show a particular element. I use a horizontal LinearLayoutManager.
I add 10 elements, but recycler view has room only for 3. I need to show only 3, but it always show me 3 and particular element.
<androidx.constraintlayout.widget.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">
<TextView
android:id="#+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recycler"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintEnd_toStartOf="#+id/textView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
My item layout:
<LinearLayout
android:id="#+id/itemLayout"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="10dp">
<TextView
android:id="#+id/tvAnimalName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ASDAS"
android:background="#color/colorAccent"
android:padding="10dp"
android:textSize="17sp"/>
</LinearLayout>
Adapter and activity are plain.
How can I show only visible 3 items?
Edit.
I must to disable scroll. So i am using:
layoutManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) {
#Override
public boolean canScrollHorizontally() {
return false;
}
};
Edit 2. These methods show -1 always:
int findLastVisibleItemPosition();
int findLastCompletelyVisibleItemPosition();

class HideLastDecorator() : RecyclerView.ItemDecoration() {
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDraw(c, parent, state)
val count = parent.childCount
for (i in 0 until count) {
parent.getChildAt(i).visibility = if (count == i - 1) View.INVISIBLE else View.VISIBLE
}
}
}
and add it to your recyclerView Decorations
appsRecyclerView.addItemDecoration(HideLastDecorator())
Sorry for Kotlin :)
View.INVISIBLE is important, because if the View becomes GONE, it will be removed from the measuring of the RecyclerView's content and the new ViewHolder would be added.
I prefer to work careful with OnClickListener if any is set for the ViewHolder's content.

Below code will work for you. A little explanation: Extend RecyclerView and override onLayout() method. Once RecyclerView is ready iterate through all visible (on-screen) children of RecyclerView and apply your logic. In our case we'll draw BounddingBox for every nth child and RecyclerView. If child's bounds lie inside RecyclerView's bounds then show that child otherwise set visibility to GONE/INVISIBLE.
public class CustomRecyclerView extends RecyclerView {
Rect recyclerViewBounds = new Rect();
Rect currentChildViewBounds = new Rect();
public CustomRecyclerView(#NonNull Context context) {
super(context);
}
public CustomRecyclerView(#NonNull Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
}
public CustomRecyclerView(#NonNull Context context, #Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
recyclerViewBounds.set(l, t, r, b);
for (int i = 0; i < getChildCount(); i++) {
View currentChild = getChildAt(i);
currentChildViewBounds.set(currentChild.getLeft(), currentChild.getTop(), currentChild.getRight(), currentChild.getBottom());
currentChild.setVisibility(recyclerViewBounds.contains(currentChildViewBounds) ? VISIBLE : GONE); // or INVISBLE instead of GONE
}
}
}
And most importantly: In your xml file use com.your.packagename.CustomRecyclerView instead of androidx.recyclerview.widget.RecyclerView.
NOTE: Please refrain from any object initialization inside onLayout(). What I mean is don't move the object initializations inside onLayout() to make it "fancier".

None of the proposed answer worked as expected, so there ItemDecoration that i made, it checks if view completely visible in layout manager, and hide rest of views
class HideNotFullyVisibleDecorator : RecyclerView.ItemDecoration() {
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDraw(c, parent, state)
val count = parent.childCount
for (i in 0 until count) {
val currentChild = parent.getChildAt(i)
currentChild.visibility =
if (parent.layoutManager?.isViewPartiallyVisible(currentChild, true, false) == true)
View.VISIBLE
else
View.GONE
}
}
}
Usage: recycler.addItemDecoration(HideNotFullyVisibleDecorator())
Also in my case I disabled scrolling for recycler

Recycler view is a Scrollable container which holds viewholders and recycles on scroll-up and down,
So it will display as much data as possible on screen, and that fourth half-visible item that you've shown in screenshot is just default behaviour of every scrollable view in android.
You have to customize your viewholders to adjust accordingly on runtime so that only fully visible items should be rendered.
You can do something like:
val availableWidth = screenWidth - (textViewWidth)
val itemWidth = (availableWidth / 3)

Related

Android ViewPager2 not dynamically wrapping height of different sized fragments

I have a ViewPager2 that has different height fragments as children, the height of the children change after data is loaded
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:expandedTitleGravity="top"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.tabs.TabLayout
android:id="#+id/tabs_header"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="#string/prospect_detail_tab_prospect" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="#string/prospect_detail_tab_influencers" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="#string/prospect_detail_tab_sales" />
</com.google.android.material.tabs.TabLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="#+id/pager_header"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</com.google.android.material.appbar.CollapsingToolbarLayout>
I've tried a few different solutions but nothing has worked for resizing the viewpager2 if it goes back to a smaller fragment, it just stays the same height as the tallest fragment
What happen is the ViewPager2 doesn't refresh its own height when your fragment's height changes (if you are using a RecyclerView inside for instance).
In order to solve this, we need to calculate the height when the user select the tab. This can be done using registerOnPageChangeCallback and onPageSelected
registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
// Here we need to calculate height
}
})
Now, we need to calculate the height of the fragment. To do so, we need to access the fragment and its root view. This can be done using the fragmentManager provided to your ViewPagerAdapter, when creating it, e.g. here we are using childFragmentManager:
PagerViewAdapter(childFragmentManager, lifecycle)
registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
// Because the fragment might or might not be created yet,
// we need to check for the size of the fragmentManager
// before accessing it.
if (childFragmentManager.fragments.size > position) {
val fragment = childFragmentManager.fragments.get(position)
fragment.view?.let {
// Now we've got access to the fragment Root View
// we will use it to calculate the height and
// apply it to the ViewPager2
updatePagerHeightForChild(it, binding.viewPager)
}
}
}
})
// This function can sit in an Helper file, so it can be shared across your project.
fun updatePagerHeightForChild(view: View, pager: ViewPager2) {
view.post {
val wMeasureSpec =
View.MeasureSpec.makeMeasureSpec(view.width, View.MeasureSpec.EXACTLY)
val hMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
view.measure(wMeasureSpec, hMeasureSpec)
if (pager.layoutParams.height != view.measuredHeight) {
pager.layoutParams = (pager.layoutParams)
.also { lp ->
// applying Fragment Root View Height to
// the pager LayoutParams, so they match
lp.height = view.measuredHeight
}
}
}
}
Notice: We are accessing the viewPager using binding, if you do not use binding, provide the viewPager your own way, maybe with findViewById()
Limitation: Async Loading Content
if you've got some dynamic loading content, you might need to couple with solution with ViewTreeObserver.OnGlobalLayoutListener, I haven't tested that yet.
Reference: https://issuetracker.google.com/u/0/issues/143095219
You need to custom ViewPager a little bit:
public class WrapHeightViewPager extends ViewPager {
private int currentPosition = 0;
public WrapHeightViewPager(#NonNull Context context) {
super(context);
}
public WrapHeightViewPager(#NonNull Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (currentPosition < getChildCount()) {
View child = getChildAt(currentPosition);
child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
int height = child.getMeasuredHeight();
if (height != 0) {
heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
}
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
public void measureViewPager(int currentPosition) {
this.currentPosition = currentPosition;
requestLayout();
}
}
After that, when init tabLayout, just call measureViewPager(currentPosition) in onTabSelected function
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
#Override
public void onTabSelected(TabLayout.Tab tab) {
viewPager.measureViewPager(tab.getPosition());
}
#Override
public void onTabUnselected(TabLayout.Tab tab) {
}
#Override
public void onTabReselected(TabLayout.Tab tab) {
}
});

Motion Layout with swipe gesture + SwipeRefreshLayout + RecyclerView bug wrong behavior scrolling up

I'm using MotionLayout to build UI with 2 parts - top one with some view and bottom one with SwipeRefresh and RecyclerView inside. Also I have a single gesture for MotionLayout - SwipeRefresh moves up above top view on swipe up. The problem is when I scroll RecyclerView to the bottom (top view "collapses") and then to the top - MotionLayout starts to reverse my transition at once ("expand") - when RecyclerView is not fully scrolled to the top instead of scrolling RecyclerView first. While my SwipeRefresh is updating or refreshing it works as should. Disabling it causes refresh layout progressbar disappearing without animation - it's not a good solution. Any workarounds?
Layout xml gist
Layout scene gist
I had the same issue and came up with a solution while browsing the official bugfix history of MotionLayout. You have to override the onNestedPreScroll method of MotionLayout like this:
/**
* The current version of motionLayout (2.0.0-beta04) does not honor the position
* of the RecyclerView, if it is wrapped in a SwipeRefreshLayout.
* This is the case for the PullRequest screen: When scrolling back to top, the motionLayout transition
* would be triggered immediately instead of only as soon as the RecyclerView scrolled back to top.
*
* This workaround checks if the SwipeRefresh layout can still scroll back up. If so, it does not trigger the motionLayout transition.
*/
class SwipeRefreshMotionLayout : MotionLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
if (!isInteractionEnabled) {
return
}
if (target !is SwipeRefreshLayout) {
return super.onNestedPreScroll(target, dx, dy, consumed, type)
}
val recyclerView = target.getChildAt(0)
if (recyclerView !is RecyclerView) {
return super.onNestedPreScroll(target, dx, dy, consumed, type)
}
val canScrollVertically = recyclerView.canScrollVertically(-1)
if (dy < 0 && canScrollVertically) {
// don't start motionLayout transition
return;
}
super.onNestedPreScroll(target, dx, dy, consumed, type)
}
}
Using this MotionLayout in conjunction with SwipeRefreshLayout works nicely for me.
I also posted this here, in case you want to keep track of the bugfix by Google.
I can't post comment to #muetzenflo answer due to lack of reputation, but I was struggling for several hours trying to disable animation in my MotionLayout. I set isInteractionEnabled to "false", but it didn't work. Finally I realized that I use custom MotionLayout and probably should check it. Only when I added
if (!isInteractionEnabled) {
return
}
as a first check in onNestedPreScroll() disabling animation work as intended.
After I set SwipeRefreshLayout as touchAnchorId the bug was gone
<OnSwipe
motion:dragDirection="dragDown"
motion:touchAnchorId="#+id/swipeContainer"
motion:touchAnchorSide="top" />
androidx.constraintlayout:constraintlayout:2.0.2
Adding more to #muetzenflo's answer (as I also don't have enough reputation to comment):
The SwipeRefreshLayout in target will also hold a CircleImageView (which I assume is the refresh icon shown when pulling down). This will sometimes be the first child of the layout (this seems to happen if the fragment the layout is in has been removed and then added back later), so target.getChildAt(0) will return the CircleImageView instead of the RecyclerView. Going through all the children of target and checking if one of them is a RecyclerView provides the expected result.
Code (in Java):
SwipeRefreshLayout swipeLayout = (SwipeRefreshLayout) target;
// Check that the SwipeRefreshLayout has a RecyclerView child
View recyclerView = null;
for (int i = 0; i < swipeLayout.getChildCount(); i++) {
View child = swipeLayout.getChildAt(i);
if (child instanceof RecyclerView) {
recyclerView = child;
break;
}
}
if (recyclerView == null) {
super.onNestedPreScroll(target, dx, dy, consumed, type);
return;
}
This solution worked for me. This is based on these two answers in java :
https://stackoverflow.com/a/59827320/11962518
https://stackoverflow.com/a/65202877/11962518
binding.motionLayout.setTransitionListener(new MotionLayout.TransitionListener() {
#Override
public void onTransitionStarted(MotionLayout motionLayout, int startId, int endId) {
binding.swipeLayout.setEnabled(false);
}
#Override
public void onTransitionChange(MotionLayout motionLayout, int startId, int endId, float progress) {
if (progress != 0f) {
binding.swipeLayout.setEnabled(false);
binding.swipeLayout.setRefreshing(false);
}
}
#Override
public void onTransitionCompleted(MotionLayout motionLayout, int currentId) {
}
#Override
public void onTransitionTrigger(MotionLayout motionLayout, int triggerId, boolean positive, float progress) {
}
});
binding.recyclerView.setOnTouchListener((view, motionEvent) -> {
binding.motionLayout.onTouchEvent(motionEvent);
return false;
});
Then override onScrolled method for the RecyclerView like this :
gridLayoutManager = new RtlGridLayoutManager(context, 1);
adapterRowList = new RecyclerAdapterRowItemCommodity(rowList.getRowItemHome(), context);
binding.recyclerView.setAdapter(adapterRowList);
binding.recyclerView.setLayoutManager(gridLayoutManager);
binding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(#NotNull RecyclerView recyclerView, int dx, int dy) {
if (!recyclerView.canScrollVertically(-1)) { //Detects start of RecyclerView
binding.swipeLayout.setEnabled(true); // now enable swipe refresh layout
}
});
XML :
Note : You should put your recyclerView inside another layout like this :
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/main_lay"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="#id/view_bottom"
app:layout_constraintTop_toBottomOf="#id/top_buttons">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="#dimen/_8cdp"
android:paddingEnd="#dimen/_8cdp"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
motion_scene :
Define Transaction like this :
<Transition
motion:constraintSetStart="#id/start"
motion:constraintSetEnd="#+id/end"
motion:duration="1000">
<OnSwipe motion:touchAnchorId="#id/main_lay"
motion:touchAnchorSide="top"
motion:dragDirection="dragUp"/>
</Transition>
If RecyclerView or ListView is not the direct child of SwipeRefreshLayout then this issue occurs.
Simplest solution is to provide OnChildScrollUpCallback implementation and return the results appropriately. In Kotlin code below, refreshLayout is SwipeRefreshLayout and recyclerView is RecyclerView as can be seen in xml layout code as well.
refreshLayout.setOnChildScrollUpCallback(object : SwipeRefreshLayout.OnChildScrollUpCallback {
override fun canChildScrollUp(parent: SwipeRefreshLayout, child: View?): Boolean {
if (recyclerView != null) {
return recyclerView.canScrollVertically(-1)
}
return false
}
})
While xml layout is something like this,
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="#+id/swipeRefresh".../>
...
lots of other views i.e TextView, ImageView, MotionLayout
...
...
...
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerView".../>
...
...
...
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
This is also answered here.

Get child view height in RecyclerView ItemDecoration.getItemOffsets

I am attempting to create an ItemDecoration that will maintain a minimum height for a RecyclerView by dynamically adding padding to the last item.
To calculate the amount of padding I need to know the total height of all children. I am approximating this by getting the height of a single view and multiplying it by the number of items in the Adapter.
The problem I am experiencing is that view.getHeight() doesn't always return the correct height. Usually the first time getItemOffsets() is called the height is much smaller than it should be (e.g. 30px vs 300px). This happens even if I give the child views a fixed height in the layout XML. I am guessing this has something to do with the measure/layout cycle by I am unsure of how to get the correct view dimensions in the ItemDecoration.
What is the correct way to get the child views height programmatically in getItemOffsets()?
ItemDecoration:
public class MinHeightPaddingItemDecoration extends RecyclerView.ItemDecoration {
private static final String LOG_TAG = MinHeightPaddingItemDecoration.class.getSimpleName();
private int mMinHeight;
public MinHeightPaddingItemDecoration(int minHeight) {
super();
mMinHeight = minHeight;
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView recyclerView, RecyclerView.State state) {
int itemCount = state.getItemCount();
int lastPosition = itemCount - 1;
int itemPosition = recyclerView.getChildAdapterPosition(view);
int layoutPosition = recyclerView.getChildLayoutPosition(view);
// If this view isnt on screen then do nothing
if (layoutPosition != RecyclerView.NO_POSITION && itemPosition == lastPosition) {
// NOTE: view.getHeight() doesn't always return the correct height, even if the layout is given a fixed height in the XML
int childHeight = view.getHeight();
int totalChildHeight = childHeight * itemCount;
int minHeight = getMinHeight();
if (totalChildHeight < minHeight) {
outRect.bottom = minHeight - totalChildHeight;
}
}
}
private int getMinHeight() {
return mMinHeight;
}
}
recycler_view_item.xml
<?xml version="1.0" encoding="utf-8"?>
<layout
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">
<data>
<variable name="controller" type="my.ViewController"/>
</data>
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_gravity="center"
android:clickable="true"
android:onClick="#{() -> controller.doSomething()}">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Child layout"
/>
</RelativeLayout>
</android.support.v7.widget.CardView>
</layout>
I found this workaround for fixed sized child views. I am still not sure how to handle dynamically sized layouts.
// NOTE: view.getHeight() doesn't always return the correct height
// NOTE: even if the layout is given a fixed height in the XML.
// NOTE: Instead directly access the LayoutParams height value
int childHeight = view.getLayoutParams().height;
I found a kind-of hack, after reading https://yoda.entelect.co.za/view/9627/how-to-android-recyclerview-item-decorations.
Before calculating padding based on child.width, I manually measure the View, if needed:
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
if(view.width == 0) fixLayoutSize(view, parent)
calculatePadding(outRect, view, parent, state)
}
private fun fixLayoutSize(view: View, parent: ViewGroup) {
if (view.layoutParams == null) {
view.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT
)
}
val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY)
val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED)
val childWidth = ViewGroup.getChildMeasureSpec(
widthSpec, parent.paddingLeft + parent.paddingRight, view.layoutParams.width
)
val childHeight = ViewGroup.getChildMeasureSpec(
heightSpec, parent.paddingTop + parent.paddingBottom, view.layoutParams.height
)
view.measure(childWidth, childHeight)
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
}

Why my custom Behavior doesn't work well in Android

I have an ImageView in the top of the layout and a RecyclerView below it; That's all I need in my custom Behavior step 1; But, it doesn't seem to work well.
Here is the code:
Behavior.java
public class Depency extends CoordinatorLayout.Behavior<RecyclerView> {
private String TAG = "tag";
public Depency() {
}
public Depency(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean onDependentViewChanged(CoordinatorLayout parent, RecyclerView child, View
dependency) {
int delta = (int) (dependency.getTranslationY() + dependency.getBottom());
delta -= child.getTop();
child.offsetTopAndBottom(delta);
Log.i(TAG,
"onDependentViewChanged: " + delta + "," + child.getTop() + dependency.getClass().getSimpleName());
return true;
}
#Override
public boolean layoutDependsOn(CoordinatorLayout parent, RecyclerView child, View dependency) {
return dependency instanceof AppCompatImageView;
}
}
layout.xml
<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"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
tools:context="com.example.kenchan.fullypjo.MainActivity"
tools:showIn="#layout/activity_main">
<android.support.v7.widget.AppCompatImageView
android:id="#+id/imageView"
android:layout_width="match_parent"
android:layout_height="150dp"
android:scaleType="centerCrop"
android:src="#drawable/ic_girl"/>
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.example.kenchan.fullypjo.view.Depency"/>
</android.support.design.widget.CoordinatorLayout>
The preview in xml:
seems work fine.
But when I run the project on my cell phone , I get wrong:
Could somebody help me or advise me with my code ?
The problem has bothered me for a while;
I tried to read source code in android.support.design.widget.AppBarLayout$ScrollingViewBehavior
and I found the code logic just at different nothing:
#Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child,
View dependency) {
offsetChildAsNeeded(parent, child, dependency);
return false;
}
private void offsetChildAsNeeded(CoordinatorLayout parent, View child, View dependency) {
final CoordinatorLayout.Behavior behavior =
((CoordinatorLayout.LayoutParams) dependency.getLayoutParams()).getBehavior();
if (behavior instanceof Behavior) {
// Offset the child, pinning it to the bottom the header-dependency, maintaining
// any vertical gap and overlap
final Behavior ablBehavior = (Behavior) behavior;
ViewCompat.offsetTopAndBottom(child, (dependency.getBottom() - child.getTop())
+ ablBehavior.mOffsetDelta
+ getVerticalLayoutGap()
- getOverlapPixelsForOffset(dependency));
}
}
RecyclerView is at top z-index, that's why you are seeing it as overlayed over the ImageView. In the preview you see it with only see 9 items.
You need to change RecyclerView top when inside your Behavior's onLayoutChild() method, to the bottom of the ImageView. This method gives you a chance to lay out the child yourself, instead of allowing CoordinatorLayout to do it deliberately. If you return true, CoordinatorLayout will not attempt to layout that view.

how to display footer view to the end of the screen in the case when the list has very few items?

I want to add a footer to the listview. When the number of list items are more,the footer works fine.
But when listview has very few items,the footer gets displayed in the middle of the screen,just below the listview .which looks shabby.In such case i want the footer to align parent bottom.
Thankyou in anticipation.
it is a simplest example of what you want. you can customize it:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/RelativeLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<ListView
android:id="#+id/listView1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="#+id/footer"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true" >
</ListView>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="90dp"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true"
android:id="#+id/footer" >
</RelativeLayout>
</RelativeLayout>
if you want do that you said in comment you must set layout param's of footer in code, you must get the size of your list, then get the number of row that shows in screen, then
if (listSize < numRow)
//set footer to bottom of your list
else
// android:layout_alignParentBottom="true"
Maybe your listview's height is set to wrap_content?
As far as I know the footer is added at the bottom of the listview. If you set the listview's height to match_parent it should be aligned to the bottom and also the footer should be displayed there.
(If you use a relative layout simply set listview's attribute alignParentBottom="true" in your .xml file)
You can use RecyclerView with RecyclerView.ItemDecoration to implement this behavior.
public class StickyFooterItemDecoration extends RecyclerView.ItemDecoration {
/**
* Top offset to completely hide footer from the screen and therefore avoid noticeable blink during changing position of the footer.
*/
private static final int OFF_SCREEN_OFFSET = 5000;
#Override
public void getItemOffsets(Rect outRect, final View view, final RecyclerView parent, RecyclerView.State state) {
int adapterItemCount = parent.getAdapter().getItemCount();
if (isFooter(parent, view, adapterItemCount)) {
//For the first time, each view doesn't contain any parameters related to its size,
//hence we can't calculate the appropriate offset.
//In this case, set a big top offset and notify adapter to update footer one more time.
//Also, we shouldn't do it if footer became visible after scrolling.
if (view.getHeight() == 0 && state.didStructureChange()) {
hideFooterAndUpdate(outRect, view, parent);
} else {
outRect.set(0, calculateTopOffset(parent, view, adapterItemCount), 0, 0);
}
}
}
private void hideFooterAndUpdate(Rect outRect, final View footerView, final RecyclerView parent) {
outRect.set(0, OFF_SCREEN_OFFSET, 0, 0);
footerView.post(new Runnable() {
#Override
public void run() {
parent.getAdapter().notifyDataSetChanged();
}
});
}
private int calculateTopOffset(RecyclerView parent, View footerView, int itemCount) {
int topOffset = parent.getHeight() - visibleChildsHeightWithFooter(parent, footerView, itemCount);
return topOffset < 0 ? 0 : topOffset;
}
private int visibleChildsHeightWithFooter(RecyclerView parent, View footerView, int itemCount) {
int totalHeight = 0;
//In the case of dynamic content when adding or removing are possible itemCount from the adapter is reliable,
//but when the screen can fit fewer items than in adapter, getChildCount() from RecyclerView should be used.
int onScreenItemCount = Math.min(parent.getChildCount(), itemCount);
for (int i = 0; i < onScreenItemCount - 1; i++) {
totalHeight += parent.getChildAt(i).getHeight();
}
return totalHeight + footerView.getHeight();
}
private boolean isFooter(RecyclerView parent, View view, int itemCount) {
return parent.getChildAdapterPosition(view) == itemCount - 1;
}
}
Make sure to set match_parent for the RecyclerView height.
Please have a look at the sample application https://github.com/JohnKuper/recyclerview-sticky-footer and how it works http://sendvid.com/nbpj0806
A Huge drawback of this solution is it works correctly only after notifyDataSetChanged() throughout an application(not inside decoration). With more specific notifications it won't work properly and to support them, it requires a way more logic. Also, you can get insights from the library recyclerview-stickyheaders by eowise and improve this solution.

Categories

Resources