How to overlap RecyclerView over ScrollView [duplicate] - android

I'm trying to implement a layout which contains RecyclerView and ScrollView at the same layout.
Layout template:
<RelativeLayout>
<ScrollView android:id="#+id/myScrollView">
<unrelated data>...</unrealated data>
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/my_recycler_view" />
</ScrollView>
</RelativeLayout>
Problems: I can scroll until the last element of ScrollView.
Things I tried:
Card view inside the ScrollView (now ScrollView contains RecyclerView) - can see the card up until the RecyclerView.
Initial thought was to implement this ViewGroup using RecyclerView instead of ScrollView where one of it's views type is the CardView, but I got the exact same results as with the ScrollView.

use NestedScrollView instead of ScrollView
Please go through NestedScrollView reference document for more information.
and add recyclerView.setNestedScrollingEnabled(false); to your RecyclerView

I know I am late it the game, but the issue still exists even after google has made fix on the android.support.v7.widget.RecyclerView
The issue I get now is RecyclerView with layout_height=wrap_content not taking height of all the items issue inside ScrollView that only happens on Marshmallow and Nougat+ (API 23, 24, 25) versions.
(UPDATE: Replacing ScrollView with android.support.v4.widget.NestedScrollView works on all versions. I somehow missed testing accepted solution. Added this in my github project as demo.)
After trying different things, I have found workaround that fixes this issue.
Here is my layout structure in a nutshell:
<ScrollView>
<LinearLayout> (vertical - this is the only child of scrollview)
<SomeViews>
<RecyclerView> (layout_height=wrap_content)
<SomeOtherViews>
The workaround is the wrap the RecyclerView with RelativeLayout. Don't ask me how I found this workaround!!! ¯\_(ツ)_/¯
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:descendantFocusability="blocksDescendants">
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</RelativeLayout>
Complete example is available on GitHub project - https://github.com/amardeshbd/android-recycler-view-wrap-content
Here is a demo screencast showing the fix in action:

Although the recommendation that
you should never put a scrollable view inside another scrollable view
Is a sound advice, however if you set a fixed height on the recycler view it should work fine.
If you know the height of the adapter item layout you could just calculate the height of the RecyclerView.
int viewHeight = adapterItemSize * adapterData.size();
recyclerView.getLayoutParams().height = viewHeight;

In case setting fixed height for the RecyclerView didn't work for someone (like me), here is what I've added to the fixed height solution:
mRecyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
#Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
int action = e.getAction();
switch (action) {
case MotionEvent.ACTION_MOVE:
rv.getParent().requestDisallowInterceptTouchEvent(true);
break;
}
return false;
}
#Override
public void onTouchEvent(RecyclerView rv, MotionEvent e) {
}
#Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
}
});

The new Android Support Library 23.2 solves that problem, you can now set wrap_content as the height of your RecyclerView and works correctly.
Android Support Library 23.2

RecyclerViews are fine to put in ScrollViews so long as they aren't scrolling themselves. In this case, it makes sense to make it a fixed height.
The proper solution is to use wrap_content on the RecyclerView height and then implement a custom LinearLayoutManager that can properly handle the wrapping.
Copy this LinearLayoutManager into your project: link
Then wrap the RecyclerView:
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
And set it up like so:
RecyclerView list = (RecyclerView)findViewById(R.id.list);
list.setHasFixedSize(true);
list.setLayoutManager(new com.example.myapp.LinearLayoutManager(list.getContext()));
list.setAdapter(new MyViewAdapter(data));
Edit: This can cause complications with scrolling because the RecyclerView can steal the ScrollView's touch events. My solution was just to ditch the RecyclerView in all and go with a LinearLayout, programmatically inflate subviews, and add them to the layout.

For ScrollView, you could use fillViewport=true and make layout_height="match_parent" as below and put RecyclerView inside:
<ScrollView
android:fillViewport="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="#+id/llOptions">
<android.support.v7.widget.RecyclerView
android:id="#+id/rvList"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</ScrollView>
No further height adjustment needed through code.

Try this. Very late answer, but surely helps anyone in future.
Change your ScrollView to NestedScrollView:
<android.support.v4.widget.NestedScrollView>
<android.support.v7.widget.RecyclerView
...
... />
</android.support.v4.widget.NestedScrollView>
In your UI code, update it for Recyclerview:
recyclerView.setNestedScrollingEnabled(false);
recyclerView.setHasFixedSize(false);

Calculating RecyclerView's height manually is not good, better is to use a custom LayoutManager.
The reason for above issue is any view which has it's scroll (ListView, GridView, RecyclerView) failed to calculate it's height when add as a child in another view has scroll. So overriding its onMeasure method will solve the issue.
Please replace the default layout manager with the below:
public class MyLinearLayoutManager extends android.support.v7.widget.LinearLayoutManager {
private static boolean canMakeInsetsDirty = true;
private static Field insetsDirtyField = null;
private static final int CHILD_WIDTH = 0;
private static final int CHILD_HEIGHT = 1;
private static final int DEFAULT_CHILD_SIZE = 100;
private final int[] childDimensions = new int[2];
private final RecyclerView view;
private int childSize = DEFAULT_CHILD_SIZE;
private boolean hasChildSize;
private int overScrollMode = ViewCompat.OVER_SCROLL_ALWAYS;
private final Rect tmpRect = new Rect();
#SuppressWarnings("UnusedDeclaration")
public MyLinearLayoutManager(Context context) {
super(context);
this.view = null;
}
#SuppressWarnings("UnusedDeclaration")
public MyLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
this.view = null;
}
#SuppressWarnings("UnusedDeclaration")
public MyLinearLayoutManager(RecyclerView view) {
super(view.getContext());
this.view = view;
this.overScrollMode = ViewCompat.getOverScrollMode(view);
}
#SuppressWarnings("UnusedDeclaration")
public MyLinearLayoutManager(RecyclerView view, int orientation, boolean reverseLayout) {
super(view.getContext(), orientation, reverseLayout);
this.view = view;
this.overScrollMode = ViewCompat.getOverScrollMode(view);
}
public void setOverScrollMode(int overScrollMode) {
if (overScrollMode < ViewCompat.OVER_SCROLL_ALWAYS || overScrollMode > ViewCompat.OVER_SCROLL_NEVER) {
throw new IllegalArgumentException("Unknown overscroll mode: " + overScrollMode);
}
if (this.view == null) throw new IllegalStateException("view == null");
this.overScrollMode = overScrollMode;
ViewCompat.setOverScrollMode(view, overScrollMode);
}
public static int makeUnspecifiedSpec() {
return View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
}
#Override
public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec) {
final int widthMode = View.MeasureSpec.getMode(widthSpec);
final int heightMode = View.MeasureSpec.getMode(heightSpec);
final int widthSize = View.MeasureSpec.getSize(widthSpec);
final int heightSize = View.MeasureSpec.getSize(heightSpec);
final boolean hasWidthSize = widthMode != View.MeasureSpec.UNSPECIFIED;
final boolean hasHeightSize = heightMode != View.MeasureSpec.UNSPECIFIED;
final boolean exactWidth = widthMode == View.MeasureSpec.EXACTLY;
final boolean exactHeight = heightMode == View.MeasureSpec.EXACTLY;
final int unspecified = makeUnspecifiedSpec();
/**
* In case of exact calculations for both dimensions let's
* use default "onMeasure" implementation.
*/
if (exactWidth && exactHeight) {
super.onMeasure(recycler, state, widthSpec, heightSpec);
return;
}
final boolean vertical = getOrientation() == VERTICAL;
initChildDimensions(widthSize, heightSize, vertical);
int width = 0;
int height = 0;
/**
* It's possible to get scrap views in recycler which are bound to old (invalid)
* adapter entities. This happens because their invalidation happens after "onMeasure"
* method. As a workaround let's clear the recycler now (it should not cause
* any performance issues while scrolling as "onMeasure" is never called whiles scrolling).
*/
recycler.clear();
final int stateItemCount = state.getItemCount();
final int adapterItemCount = getItemCount();
/**
* Adapter always contains actual data while state might contain old data
* (f.e. data before the animation is done). As we want to measure the view
* with actual data we must use data from the adapter and not from the state.
*/
for (int i = 0; i < adapterItemCount; i++) {
if (vertical) {
if (!hasChildSize) {
if (i < stateItemCount) {
/**
* We should not exceed state count, otherwise we'll get IndexOutOfBoundsException.
* For such items we will use previously calculated dimensions.
*/
measureChild(recycler, i, widthSize, unspecified, childDimensions);
} else {
logMeasureWarning(i);
}
}
height += childDimensions[CHILD_HEIGHT];
if (i == 0) {
width = childDimensions[CHILD_WIDTH];
}
if (hasHeightSize && height >= heightSize) {
break;
}
} else {
if (!hasChildSize) {
if (i < stateItemCount) {
/**
* We should not exceed state count, otherwise we'll get IndexOutOfBoundsException.
* For such items we will use previously calculated dimensions.
*/
measureChild(recycler, i, unspecified, heightSize, childDimensions);
} else {
logMeasureWarning(i);
}
}
width += childDimensions[CHILD_WIDTH];
if (i == 0) {
height = childDimensions[CHILD_HEIGHT];
}
if (hasWidthSize && width >= widthSize) {
break;
}
}
}
if (exactWidth) {
width = widthSize;
} else {
width += getPaddingLeft() + getPaddingRight();
if (hasWidthSize) {
width = Math.min(width, widthSize);
}
}
if (exactHeight) {
height = heightSize;
} else {
height += getPaddingTop() + getPaddingBottom();
if (hasHeightSize) {
height = Math.min(height, heightSize);
}
}
setMeasuredDimension(width, height);
if (view != null && overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS) {
final boolean fit = (vertical && (!hasHeightSize || height < heightSize))
|| (!vertical && (!hasWidthSize || width < widthSize));
ViewCompat.setOverScrollMode(view, fit ? ViewCompat.OVER_SCROLL_NEVER : ViewCompat.OVER_SCROLL_ALWAYS);
}
}
private void logMeasureWarning(int child) {
if (BuildConfig.DEBUG) {
Log.w("MyLinearLayoutManager", "Can't measure child #" + child + ", previously used dimensions will be reused." +
"To remove this message either use #setChildSize() method or don't run RecyclerView animations");
}
}
private void initChildDimensions(int width, int height, boolean vertical) {
if (childDimensions[CHILD_WIDTH] != 0 || childDimensions[CHILD_HEIGHT] != 0) {
/** Already initialized, skipping. */
return;
}
if (vertical) {
childDimensions[CHILD_WIDTH] = width;
childDimensions[CHILD_HEIGHT] = childSize;
} else {
childDimensions[CHILD_WIDTH] = childSize;
childDimensions[CHILD_HEIGHT] = height;
}
}
#Override
public void setOrientation(int orientation) {
/** Might be called before the constructor of this class is called. */
//noinspection ConstantConditions
if (childDimensions != null) {
if (getOrientation() != orientation) {
childDimensions[CHILD_WIDTH] = 0;
childDimensions[CHILD_HEIGHT] = 0;
}
}
super.setOrientation(orientation);
}
public void clearChildSize() {
hasChildSize = false;
setChildSize(DEFAULT_CHILD_SIZE);
}
public void setChildSize(int childSize) {
hasChildSize = true;
if (this.childSize != childSize) {
this.childSize = childSize;
requestLayout();
}
}
private void measureChild(RecyclerView.Recycler recycler, int position, int widthSize, int heightSize, int[] dimensions) {
final View child;
try {
child = recycler.getViewForPosition(position);
} catch (IndexOutOfBoundsException e) {
if (BuildConfig.DEBUG) {
Log.w("MyLinearLayoutManager", "MyLinearLayoutManager doesn't work well with animations. Consider switching them off", e);
}
return;
}
final RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) child.getLayoutParams();
final int hPadding = getPaddingLeft() + getPaddingRight();
final int vPadding = getPaddingTop() + getPaddingBottom();
final int hMargin = p.leftMargin + p.rightMargin;
final int vMargin = p.topMargin + p.bottomMargin;
/** We must make insets dirty in order calculateItemDecorationsForChild to work. */
makeInsetsDirty(p);
/** This method should be called before any getXxxDecorationXxx() methods. */
calculateItemDecorationsForChild(child, tmpRect);
final int hDecoration = getRightDecorationWidth(child) + getLeftDecorationWidth(child);
final int vDecoration = getTopDecorationHeight(child) + getBottomDecorationHeight(child);
final int childWidthSpec = getChildMeasureSpec(widthSize, hPadding + hMargin + hDecoration, p.width, canScrollHorizontally());
final int childHeightSpec = getChildMeasureSpec(heightSize, vPadding + vMargin + vDecoration, p.height, canScrollVertically());
child.measure(childWidthSpec, childHeightSpec);
dimensions[CHILD_WIDTH] = getDecoratedMeasuredWidth(child) + p.leftMargin + p.rightMargin;
dimensions[CHILD_HEIGHT] = getDecoratedMeasuredHeight(child) + p.bottomMargin + p.topMargin;
/** As view is recycled let's not keep old measured values. */
makeInsetsDirty(p);
recycler.recycleView(child);
}
private static void makeInsetsDirty(RecyclerView.LayoutParams p) {
if (!canMakeInsetsDirty) return;
try {
if (insetsDirtyField == null) {
insetsDirtyField = RecyclerView.LayoutParams.class.getDeclaredField("mInsetsDirty");
insetsDirtyField.setAccessible(true);
}
insetsDirtyField.set(p, true);
} catch (NoSuchFieldException e) {
onMakeInsertDirtyFailed();
} catch (IllegalAccessException e) {
onMakeInsertDirtyFailed();
}
}
private static void onMakeInsertDirtyFailed() {
canMakeInsetsDirty = false;
if (BuildConfig.DEBUG) {
Log.w("MyLinearLayoutManager", "Can't make LayoutParams insets dirty, decorations measurements might be incorrect");
}
}
}

If you put RecyclerView inside NestedScrollView and enable recyclerView.setNestedScrollingEnabled(false);, scrolling will working well.
However, there is a problem
RecyclerView don't recycle
For example, your RecyclerView (inside NestedScrollView or ScrollView) have 100 item.
When Activity launch, 100 item will create (onCreateViewHolder and onBindViewHolder of 100 item will called at same time).
Example, for each item, you will load a large image from API => activity created -> 100 image will load.
It make starting Activity slowness and lagging.
Possible solution:
- Thinking about using RecyclerView with multiple type.
However, if in your case, there are just a few item in RecyclerView and recycle or don't recycle don't affect performance a lot, you can use RecyclerView inside ScrollView for simple

UPDATE:
this answer is out dated now as there are widgets like NestedScrollView and RecyclerView that support nested scrolling.
you should never put a scrollable view inside another scrollable view !
i suggest you make your main layout recycler view and put your views as items of recycler view.
take a look at this example it show how to use multiple views inside recycler view adapter.
link to example

Add this line to your RecyclerView xml view:
android:nestedScrollingEnabled="false"
And your RecyclerView will be smoothly scrolled with flexible height.
Hope it helps.

It seems that NestedScrollView does solve the problem.
I've tested using this layout:
<android.support.v4.widget.NestedScrollView
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/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/dummy_text" />
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp">
<android.support.v7.widget.RecyclerView
android:id="#+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</android.support.v7.widget.CardView>
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
And it works without issues.

I was having the same problem. That's what i tried and it works. I am sharing my xml and java code. Hope this will help someone.
Here is the xml
<?xml version="1.0" encoding="utf-8"?>
< NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="#+id/iv_thumbnail"
android:layout_width="match_parent"
android:layout_height="200dp" />
<TextView
android:id="#+id/tv_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Description" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Buy" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Reviews" />
<android.support.v7.widget.RecyclerView
android:id="#+id/rc_reviews"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</android.support.v7.widget.RecyclerView>
</LinearLayout>
</NestedScrollView >
Here is the related java code. It works like a charm.
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
recyclerView.setLayoutManager(linearLayoutManager);
recyclerView.setNestedScrollingEnabled(false);

I used CustomLayoutManager to disable RecyclerView Scrolling.
Also don't use Recycler View as WrapContent, use it as 0dp, Weight=1
public class CustomLayoutManager extends LinearLayoutManager {
private boolean isScrollEnabled;
// orientation should be LinearLayoutManager.VERTICAL or HORIZONTAL
public CustomLayoutManager(Context context, int orientation, boolean isScrollEnabled) {
super(context, orientation, false);
this.isScrollEnabled = isScrollEnabled;
}
#Override
public boolean canScrollVertically() {
//Similarly you can customize "canScrollHorizontally()" for managing horizontal scroll
return isScrollEnabled && super.canScrollVertically();
}
}
Use CustomLayoutManager in RecyclerView:
CustomLayoutManager mLayoutManager = new CustomLayoutManager(getBaseActivity(), CustomLayoutManager.VERTICAL, false);
recyclerView.setLayoutManager(mLayoutManager);
((DefaultItemAnimator) recyclerView.getItemAnimator()).setSupportsChangeAnimations(false);
recyclerView.setAdapter(statsAdapter);
UI XML:
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#drawable/background_main"
android:fillViewport="false">
<LinearLayout
android:id="#+id/contParentLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<edu.aku.family_hifazat.libraries.mpchart.charts.PieChart
android:id="#+id/chart1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="#dimen/x20dp"
android:minHeight="#dimen/x300dp">
</edu.aku.family_hifazat.libraries.mpchart.charts.PieChart>
</FrameLayout>
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
</android.support.v7.widget.RecyclerView>
</LinearLayout>
</ScrollView>

Actually the main purpose of the RecyclerView is to compensate for ListView and ScrollView. Instead of doing what you're actually doing: Having a RecyclerView in a ScrollView, I would suggest having only a RecyclerView that can handle many types of children.

This does the trick:
recyclerView.setNestedScrollingEnabled(false);

First you should use NestedScrollView instead of ScrollView and put the RecyclerView inside NestedScrollView.
Use Custom layout class to measure the height and width of screen:
public class CustomLinearLayoutManager extends LinearLayoutManager {
public CustomLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
private int[] mMeasuredDimension = new int[2];
#Override
public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state,
int widthSpec, int heightSpec) {
final int widthMode = View.MeasureSpec.getMode(widthSpec);
final int heightMode = View.MeasureSpec.getMode(heightSpec);
final int widthSize = View.MeasureSpec.getSize(widthSpec);
final int heightSize = View.MeasureSpec.getSize(heightSpec);
int width = 0;
int height = 0;
for (int i = 0; i < getItemCount(); i++) {
if (getOrientation() == HORIZONTAL) {
measureScrapChild(recycler, i,
View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
heightSpec,
mMeasuredDimension);
width = width + mMeasuredDimension[0];
if (i == 0) {
height = mMeasuredDimension[1];
}
} else {
measureScrapChild(recycler, i,
widthSpec,
View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
mMeasuredDimension);
height = height + mMeasuredDimension[1];
if (i == 0) {
width = mMeasuredDimension[0];
}
}
}
switch (widthMode) {
case View.MeasureSpec.EXACTLY:
width = widthSize;
case View.MeasureSpec.AT_MOST:
case View.MeasureSpec.UNSPECIFIED:
}
switch (heightMode) {
case View.MeasureSpec.EXACTLY:
height = heightSize;
case View.MeasureSpec.AT_MOST:
case View.MeasureSpec.UNSPECIFIED:
}
setMeasuredDimension(width, height);
}
private void measureScrapChild(RecyclerView.Recycler recycler, int position, int widthSpec,
int heightSpec, int[] measuredDimension) {
View view = recycler.getViewForPosition(position);
recycler.bindViewToPosition(view, position);
if (view != null) {
RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) view.getLayoutParams();
int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec,
getPaddingLeft() + getPaddingRight(), p.width);
int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec,
getPaddingTop() + getPaddingBottom(), p.height);
view.measure(childWidthSpec, childHeightSpec);
measuredDimension[0] = view.getMeasuredWidth() + p.leftMargin + p.rightMargin;
measuredDimension[1] = view.getMeasuredHeight() + p.bottomMargin + p.topMargin;
recycler.recycleView(view);
}
}
}
And implement below code in the activity/fragment of RecyclerView:
final CustomLinearLayoutManager layoutManager = new CustomLinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(mAdapter);
recyclerView.setNestedScrollingEnabled(false); // Disables scrolling for RecyclerView, CustomLinearLayoutManager used instead of MyLinearLayoutManager
recyclerView.setHasFixedSize(false);
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int visibleItemCount = layoutManager.getChildCount();
int totalItemCount = layoutManager.getItemCount();
int lastVisibleItemPos = layoutManager.findLastVisibleItemPosition();
Log.i("getChildCount", String.valueOf(visibleItemCount));
Log.i("getItemCount", String.valueOf(totalItemCount));
Log.i("lastVisibleItemPos", String.valueOf(lastVisibleItemPos));
if ((visibleItemCount + lastVisibleItemPos) >= totalItemCount) {
Log.i("LOG", "Last Item Reached!");
}
}
});

If RecyclerView showing only one row inside ScrollView. You just need to set height of your row to android:layout_height="wrap_content".

Sorry being late to the party, but it seems like there is another solution which works perfectly for the case you mentioned.
If you use a recycler view inside a recycler view, it seems to work perfectly fine. I have personally tried and used it, and it seems to give no slowness and no jerkyness at all. Now I am not sure if this is a good practice or not, but nesting multiple recycler views , even nested scroll view slows down. But this seems to work nicely. Please give it a try. I am sure nesting is going to be perfectly fine with this.

Another approach to address the issue is to use ConstraintLayout inside ScrollView:
<ScrollView>
<ConstraintLayout> (this is the only child of ScrollView)
<...Some Views...>
<RecyclerView> (layout_height=wrap_content)
<...Some Other Views...>
But I would still stick to the androidx.core.widget.NestedScrollView approach, proposed by Yang Peiyong.

You can try with setting recycler view Hight as wrap_content.
in my case its working fine. I am trying with 2 different recycler view in scroll view

The best solution is to keep multiple Views in a Single View / View Group and then keep that one view in the SrcollView. ie.
Format -
<ScrollView>
<Another View>
<RecyclerView>
<TextView>
<And Other Views>
</Another View>
</ScrollView>
Eg.
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:text="any text"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:text="any text"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</ScrollView>
Another Eg. of ScrollView with multiple Views
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:orientation="vertical"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/imageView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#FFFFFF"
/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="10dp"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#string/CategoryItem"
android:textSize="20sp"
android:textColor="#000000"
/>
<TextView
android:textColor="#000000"
android:text="₹1000"
android:textSize="18sp"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:textColor="#000000"
android:text="so\nugh\nos\nghs\nrgh\n
sghs\noug\nhro\nghreo\nhgor\ngheroh\ngr\neoh\n
og\nhrf\ndhog\n
so\nugh\nos\nghs\nrgh\nsghs\noug\nhro\n
ghreo\nhgor\ngheroh\ngr\neoh\nog\nhrf\ndhog"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout>
</ScrollView>

For those people who trying to do it just for design purposes - leave it. Redesign your app and leave only RecyclerView. It will be better solution than doing ANY hardcode.

You can also override LinearLayoutManager to make RecyclerView roll smoothly:
#Override
public boolean canScrollVertically(){
return false;
}

Solution which worked for me
Use NestedScrollView with height as wrap_content, and for your RecyclerView setup this:
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
app:layoutManager="android.support.v7.widget.LinearLayoutManager" />
And set view holder layout params:
android:layout_width="match_parent"
android:layout_height="wrap_content"

Related

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)
}

Margin/padding in last Child in RecyclerView

I'm trying to add Padding/Margin Bottom in the last row and Padding/Margin Top in the first row. I can not do it in the item xml as it would affect all of my Children.
I have headers and children in my RecyclerView Adapter so I can not use the
android:padding="4dp"
android:clipToPadding="false"
I need to use it individually on the last first row of each header
This issue is even easier to solve. You can apply necessary padding to the RecylerView itself and set clipToPadding to false, otherwise, the padding will chop off your scrolling area. Here is an example
<android.support.v7.widget.RecyclerView
android:padding="4dp"
android:clipToPadding="false"
android:layout_width="match_parent"
android:layout_height="match_parent" />
See the padding will add 4dp on all sides including top and bottom. Then the clipToPadding parameter makes sure your child items are not chopped off. Now, add 4dp padding to all sides for your child items, and you are good to go. In total you get 8dp padding on sides and between items.
Instead of adding padding to both the top and bottom items, You can just add the padding to the top and bottom of your RecyclerView and set the clipToPadding attribute to false.
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingTop="8dp"
android:paddingBottom="8dp" />
<android.support.v7.widget.RecyclerView
android:id="#+id/rv_tpf"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:paddingBottom="100dp" />
Add android:clipToPadding="false" and android:paddingBottom="100dp" in your recyclerview.
use ItemDecoration:
private class SpacesItemDecoration extends RecyclerView.ItemDecoration {
private int space;
public SpacesItemDecoration(int space) {
this.space = space;
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int position = parent.getChildAdapterPosition(view);
boolean isLast = position == state.getItemCount()-1;
if(isLast){
outRect.bottom = space;
outRect.top = 0; //don't forget about recycling...
}
if(position == 0){
outRect.top = space;
// don't recycle bottom if first item is also last
// should keep bottom padding set above
if(!isLast)
outRect.bottom = 0;
}
}
}
and
//8dp as px
int space = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8,
getResources().getDisplayMetrics()); // calculated
//int space = getResources().getDimensionPixelSize(
// R.dimen.list_item_padding_vertical); // from resources
recyclerView.addItemDecoration(new SpacesItemDecoration(space));
Add android:clipToPadding="false" and android:paddingBottom="65dp" in your recyclerview. If you are using fab menu button and actions on recycler view cell.
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/dinner_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:paddingBottom="65dp"/>
I use this in kotlin to give bottom margin to last item only
override fun onBindViewHolder(holder: RecyclerView.ViewHolder(view), position: Int) {
if (position == itemsList.lastIndex){
val params = holder.itemView.layoutParams as FrameLayout.LayoutParams
params.bottomMargin = 100
holder.itemView.layoutParams = params
}else{
val params = holder.itemView.layoutParams as RecyclerView.LayoutParams
params.bottomMargin = 0
holder.itemView.layoutParams = params
}
//other codes ...
}
For some reason the old clipToPadding=false solution isn't working for me. So I added an ItemDecoration
https://gist.github.com/kassim/582888fa5960791264fc92bc41fb6bcf
public class BottomPaddingDecoration extends RecyclerView.ItemDecoration {
private final int bottomPadding;
public BottomPaddingDecoration(int bottomPadding) {
this.bottomPadding = bottomPadding;
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int position = ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewLayoutPosition();
if (position == parent.getAdapter().getItemCount() - 1) {
outRect.set(0, 0, 0, bottomPadding);
}
}
}
Java equivalent to #Radesh answer:
#Override
public void onBindViewHolder(#NonNull RecyclerView.ViewHolder holder, int position) {
if (position == itemsList.size() - 1) {
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) holder.itemView.getLayoutParams();
params.bottomMargin = 100;
holder.itemView.setLayoutParams(params);
} else {
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) holder.itemView.getLayoutParams();
params.bottomMargin = 0;
holder.itemView.setLayoutParams(params);
}
}
I have modified amazing answer #snachmsm answer for better and give you idea how to use
properly
public class SpacesItemDecoration extends DividerItemDecoration {
private int space;
public SpacesItemDecoration(Context clContext,int oriantation,int space) {
super(clContext,oriantation);
this.space = space;
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect,view,parent,state);
int position = parent.getChildAdapterPosition(view);
boolean isLast = position == state.getItemCount()-1;
if(isLast){
outRect.bottom = space;
outRect.top = 0; //don't forget about recycling...
}
/* if(position == 0){
outRect.top = space;
// don't recycle bottom if first item is also last
// should keep bottom padding set above
if(!isLast)
outRect.bottom = 0;
}*/
}
}
Long story short :
int freeSpaceAtBottom = 100; // the bottom free space in pixels
myRecyclerView.setClipToPadding(false);
myRecyclerView.setPadding(0,0,0,freeSpaceAtBottom);
setClipToPadding Sets whether this list view will clip its children to its padding and resize (but not clip) any EdgeEffect to the padded region, if padding is present. (1)

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.

Updating layout height on changing element's visibility

In my layout I have two ListViews positioned one after another in a vertical LinearLayout like this:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:orientation="vertical"
android:layout_height="fill_parent">
<ListView android:id="#+id/events_list"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
<ListView android:id="#+id/list"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
I want to implement for each element in the first ListView onItemClick event, which will expand each row to show additional information.
To achieve this I decided to add all the elements in the initial adapter getView function, but some of them with the "View.GONE" visibility, and then on click I change their visibility to visible.
The problem is that the initial height of the list element does not expand, only scrolling is added.
I am aware of the ExpandableListView, but I never used it and I don't know if it would be the right solutions for this quite simple case.
Excerpt of the layout of the element I'm adding to the first ListView (may be useful):
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:orientation="horizontal"
android:layout_height="wrap_content">
...
</LinealLayout>
Based on what you're describing I think you need to use an expandable listview. Updating the invividual items in a list is a pain because of how the list view works. ListView will only initially create as many views as it takes to fill the screen and then recycle those views as the user scrolls the list. Sure you can change the values - e.g. update text fields and pictures - but the view it self is not reinflated every time, only the contents of the child elements changed. This is probably why the height doesn't chane. The Adapter is showing the new child views but doing so within the height of the originaly inflated list item.
One I can think of you getting this to work is by using View.INVISIBLE instead of View.GONE. This way you would inflate a view of the height required to display all the elements and show/hide as someone click. The problem is that this does not achieve the expand/contract state you are looking for. Plus its going to have a ton of ugly white space where things are hidden.
That being said I don't know that setting all these view elements to gone is a good idea. ExpandableListView is designed such that you can have a regular view and expand a different view on click. This is much more efficient than parsing ever possible XML element for each view and then trying to show/hide different fields onClick. Look into using this method. It's not much harder than regular list view and will much better suit what you're trying to do.
I ended up using a non-animated technique as in Collapsed List example from android sample apps.
Basically they change a boolean variable in the adapter and re-draw the whole list by sending notifyDatasetChanged().
public class NestedListView extends ListView implements OnTouchListener, OnScrollListener {
private int listViewTouchAction;
private static final int MAXIMUM_LIST_ITEMS_VIEWABLE = 99;
public NestedListView(Context context, AttributeSet attrs) {
super(context, attrs);
listViewTouchAction = -1;
setOnScrollListener(this);
setOnTouchListener(this);
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
if (getAdapter() != null && getAdapter().getCount() > MAXIMUM_LIST_ITEMS_VIEWABLE) {
if (listViewTouchAction == MotionEvent.ACTION_MOVE) {
scrollBy(0, -1);
}
}
}
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int newHeight = 0;
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (heightMode != MeasureSpec.EXACTLY) {
ListAdapter listAdapter = getAdapter();
if (listAdapter != null && !listAdapter.isEmpty()) {
int listPosition = 0;
for (listPosition = 0; listPosition < listAdapter.getCount()
&& listPosition < MAXIMUM_LIST_ITEMS_VIEWABLE; listPosition++) {
View listItem = listAdapter.getView(listPosition, null, this);
//now it will not throw a NPE if listItem is a ViewGroup instance
if (listItem instanceof ViewGroup) {
listItem.setLayoutParams(new LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
}
listItem.measure(widthMeasureSpec, heightMeasureSpec);
newHeight += listItem.getMeasuredHeight();
}
newHeight += getDividerHeight() * listPosition;
}
if ((heightMode == MeasureSpec.AT_MOST) && (newHeight > heightSize)) {
if (newHeight > heightSize) {
newHeight = heightSize;
}
}
} else {
newHeight = getMeasuredHeight();
}
setMeasuredDimension(getMeasuredWidth(), newHeight);
}
#Override
public boolean onTouch(View v, MotionEvent event) {
if (getAdapter() != null && getAdapter().getCount() > MAXIMUM_LIST_ITEMS_VIEWABLE) {
if (listViewTouchAction == MotionEvent.ACTION_MOVE) {
scrollBy(0, 1);
}
}
return false;
}

Replacement for the linearLayout weights mechanism

Background:
Google suggests to avoid using nested weighted linearLayouts because of performance.
using nested weighted linearLayout is awful to read, write and maintain.
there is still no good alternative for putting views that are % of the available size. Only solutions are weights and using OpenGL. There isn't even something like the "viewBox" shown on WPF/Silverlight to auto-scale things.
This is why I've decided to create my own layout which you tell for each of its children exactly what should be their weights (and surrounding weights) compared to its size.
It seems I've succeeded , but for some reason I think there are some bugs which I can't track down.
One of the bugs is that textView, even though I give a lot of space for it, it puts the text on the top instead of in the center. imageViews on the other hand work very well. Another bug is that if I use a layout (for example a frameLayout) inside my customized layout, views within it won't be shown (but the layout itself will).
Please help me figure out why it occurs.
How to use: instead of the next usage of linear layout (I use a long XML on purpose, to show how my solution can shorten things):
<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">
<View android:layout_width="wrap_content" android:layout_height="0px"
android:layout_weight="1" />
<LinearLayout android:layout_width="match_parent"
android:layout_height="0px" android:layout_weight="1"
android:orientation="horizontal">
<View android:layout_width="0px" android:layout_height="wrap_content"
android:layout_weight="1" />
<TextView android:layout_width="0px" android:layout_weight="1"
android:layout_height="match_parent" android:text="#string/hello_world"
android:background="#ffff0000" android:gravity="center"
android:textSize="20dp" android:textColor="#ff000000" />
<View android:layout_width="0px" android:layout_height="wrap_content"
android:layout_weight="1" />
</LinearLayout>
<View android:layout_width="wrap_content" android:layout_height="0px"
android:layout_weight="1" />
</LinearLayout>
What I do is simply (the x is where to put the view itself in the weights list):
<com.example.weightedlayouttest.WeightedLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res/com.example.weightedlayouttest"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" tools:context=".MainActivity">
<TextView android:layout_width="0px" android:layout_height="0px"
app:horizontalWeights="1,1x,1" app:verticalWeights="1,1x,1"
android:text="#string/hello_world" android:background="#ffff0000"
android:gravity="center" android:textSize="20dp" android:textColor="#ff000000" />
</com.example.weightedlayouttest.WeightedLayout>
My code of the special layout is:
public class WeightedLayout extends ViewGroup
{
#Override
protected WeightedLayout.LayoutParams generateDefaultLayoutParams()
{
return new WeightedLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);
}
#Override
public WeightedLayout.LayoutParams generateLayoutParams(final AttributeSet attrs)
{
return new WeightedLayout.LayoutParams(getContext(),attrs);
}
#Override
protected ViewGroup.LayoutParams generateLayoutParams(final android.view.ViewGroup.LayoutParams p)
{
return new WeightedLayout.LayoutParams(p.width,p.height);
}
#Override
protected boolean checkLayoutParams(final android.view.ViewGroup.LayoutParams p)
{
final boolean isCorrectInstance=p instanceof WeightedLayout.LayoutParams;
return isCorrectInstance;
}
public WeightedLayout(final Context context)
{
super(context);
}
public WeightedLayout(final Context context,final AttributeSet attrs)
{
super(context,attrs);
}
public WeightedLayout(final Context context,final AttributeSet attrs,final int defStyle)
{
super(context,attrs,defStyle);
}
#Override
protected void onLayout(final boolean changed,final int l,final int t,final int r,final int b)
{
for(int i=0;i<this.getChildCount();++i)
{
final View v=getChildAt(i);
final WeightedLayout.LayoutParams layoutParams=(WeightedLayout.LayoutParams)v.getLayoutParams();
//
final int availableWidth=r-l;
final int totalHorizontalWeights=layoutParams.getLeftHorizontalWeight()+layoutParams.getViewHorizontalWeight()+layoutParams.getRightHorizontalWeight();
final int left=l+layoutParams.getLeftHorizontalWeight()*availableWidth/totalHorizontalWeights;
final int right=r-layoutParams.getRightHorizontalWeight()*availableWidth/totalHorizontalWeights;
//
final int availableHeight=b-t;
final int totalVerticalWeights=layoutParams.getTopVerticalWeight()+layoutParams.getViewVerticalWeight()+layoutParams.getBottomVerticalWeight();
final int top=t+layoutParams.getTopVerticalWeight()*availableHeight/totalVerticalWeights;
final int bottom=b-layoutParams.getBottomVerticalWeight()*availableHeight/totalVerticalWeights;
//
v.layout(left+getPaddingLeft(),top+getPaddingTop(),right+getPaddingRight(),bottom+getPaddingBottom());
}
}
// ///////////////
// LayoutParams //
// ///////////////
public static class LayoutParams extends ViewGroup.LayoutParams
{
int _leftHorizontalWeight =0,_rightHorizontalWeight=0,_viewHorizontalWeight=0;
int _topVerticalWeight =0,_bottomVerticalWeight=0,_viewVerticalWeight=0;
public LayoutParams(final Context context,final AttributeSet attrs)
{
super(context,attrs);
final TypedArray arr=context.obtainStyledAttributes(attrs,R.styleable.WeightedLayout_LayoutParams);
{
final String horizontalWeights=arr.getString(R.styleable.WeightedLayout_LayoutParams_horizontalWeights);
//
// handle horizontal weight:
//
final String[] words=horizontalWeights.split(",");
boolean foundViewHorizontalWeight=false;
int weight;
for(final String word : words)
{
final int viewWeightIndex=word.lastIndexOf('x');
if(viewWeightIndex>=0)
{
if(foundViewHorizontalWeight)
throw new IllegalArgumentException("found more than one weights for the current view");
weight=Integer.parseInt(word.substring(0,viewWeightIndex));
setViewHorizontalWeight(weight);
foundViewHorizontalWeight=true;
}
else
{
weight=Integer.parseInt(word);
if(weight<0)
throw new IllegalArgumentException("found negative weight:"+weight);
if(foundViewHorizontalWeight)
_rightHorizontalWeight+=weight;
else _leftHorizontalWeight+=weight;
}
}
if(!foundViewHorizontalWeight)
throw new IllegalArgumentException("couldn't find any weight for the current view. mark it with 'x' next to the weight value");
}
//
// handle vertical weight:
//
{
final String verticalWeights=arr.getString(R.styleable.WeightedLayout_LayoutParams_verticalWeights);
final String[] words=verticalWeights.split(",");
boolean foundViewVerticalWeight=false;
int weight;
for(final String word : words)
{
final int viewWeightIndex=word.lastIndexOf('x');
if(viewWeightIndex>=0)
{
if(foundViewVerticalWeight)
throw new IllegalArgumentException("found more than one weights for the current view");
weight=Integer.parseInt(word.substring(0,viewWeightIndex));
setViewVerticalWeight(weight);
foundViewVerticalWeight=true;
}
else
{
weight=Integer.parseInt(word);
if(weight<0)
throw new IllegalArgumentException("found negative weight:"+weight);
if(foundViewVerticalWeight)
_bottomVerticalWeight+=weight;
else _topVerticalWeight+=weight;
}
}
if(!foundViewVerticalWeight)
throw new IllegalArgumentException("couldn't find any weight for the current view. mark it with 'x' next to the weight value");
}
//
arr.recycle();
}
public LayoutParams(final int width,final int height)
{
super(width,height);
}
public LayoutParams(final ViewGroup.LayoutParams source)
{
super(source);
}
public int getLeftHorizontalWeight()
{
return _leftHorizontalWeight;
}
public void setLeftHorizontalWeight(final int leftHorizontalWeight)
{
_leftHorizontalWeight=leftHorizontalWeight;
}
public int getRightHorizontalWeight()
{
return _rightHorizontalWeight;
}
public void setRightHorizontalWeight(final int rightHorizontalWeight)
{
if(rightHorizontalWeight<0)
throw new IllegalArgumentException("negative weight :"+rightHorizontalWeight);
_rightHorizontalWeight=rightHorizontalWeight;
}
public int getViewHorizontalWeight()
{
return _viewHorizontalWeight;
}
public void setViewHorizontalWeight(final int viewHorizontalWeight)
{
if(viewHorizontalWeight<0)
throw new IllegalArgumentException("negative weight:"+viewHorizontalWeight);
_viewHorizontalWeight=viewHorizontalWeight;
}
public int getTopVerticalWeight()
{
return _topVerticalWeight;
}
public void setTopVerticalWeight(final int topVerticalWeight)
{
if(topVerticalWeight<0)
throw new IllegalArgumentException("negative weight :"+topVerticalWeight);
_topVerticalWeight=topVerticalWeight;
}
public int getBottomVerticalWeight()
{
return _bottomVerticalWeight;
}
public void setBottomVerticalWeight(final int bottomVerticalWeight)
{
if(bottomVerticalWeight<0)
throw new IllegalArgumentException("negative weight :"+bottomVerticalWeight);
_bottomVerticalWeight=bottomVerticalWeight;
}
public int getViewVerticalWeight()
{
return _viewVerticalWeight;
}
public void setViewVerticalWeight(final int viewVerticalWeight)
{
if(viewVerticalWeight<0)
throw new IllegalArgumentException("negative weight :"+viewVerticalWeight);
_viewVerticalWeight=viewVerticalWeight;
}
}
}
I accepted your challenge and attempted to create the layout you describe in response to my comment. You are right. It is surprisingly difficult to accomplish. Besides that, I do like shooting house flies. So I jumped on board and came up with this solution.
Extend the existing layout classes rather than creating your own from scratch. I went with RelativeLayout to start with but the same approach can be used by all of them. This gives you the ability to use the default behavior for that layout on child views that you don't want to manipulate.
I added four attributes to the layout called top, left, width and height. My intention was to mimic HTML by allowing values such as "10%", "100px", "100dp" etc.. At this time the only value accepted is an integer representing the % of parent. "20" = 20% of the layout.
For better performance I allow the super.onLayout() to execute through all of it's iterations and only manipulate the views with the custom attributes on it's last pass. Since these views will be positioned and scaled independently of the siblings we can move them after everything else has settled.
Here is atts.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="HtmlStyleLayout">
<attr name="top" format="integer"/>
<attr name="left" format="integer"/>
<attr name="height" format="integer"/>
<attr name="width" format="integer"/>
</declare-styleable>
</resources>
Here is my layout class.
package com.example.helpso;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.RelativeLayout;
public class HtmlStyleLayout extends RelativeLayout{
private int pass =0;
#Override
protected HtmlStyleLayout.LayoutParams generateDefaultLayoutParams()
{
return new HtmlStyleLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT,RelativeLayout.LayoutParams.WRAP_CONTENT);
}
#Override
public HtmlStyleLayout.LayoutParams generateLayoutParams(final AttributeSet attrs)
{
return new HtmlStyleLayout.LayoutParams(getContext(),attrs);
}
#Override
protected RelativeLayout.LayoutParams generateLayoutParams(final android.view.ViewGroup.LayoutParams p)
{
return new HtmlStyleLayout.LayoutParams(p.width,p.height);
}
#Override
protected boolean checkLayoutParams(final android.view.ViewGroup.LayoutParams p)
{
final boolean isCorrectInstance=p instanceof HtmlStyleLayout.LayoutParams;
return isCorrectInstance;
}
public HtmlStyleLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setScaleType(View v){
try{
((ImageView) v).setScaleType (ImageView.ScaleType.FIT_XY);
}catch (Exception e){
// The view is not an ImageView
}
}
#Override
protected void onLayout(final boolean changed,final int l,final int t,final int r,final int b)
{
super.onLayout(changed, l, t, r, b); //Let the parent layout do it's thing
pass++; // After the last pass of
final int childCount = this.getChildCount(); // the parent layout
if(true){ // we do our thing
for(int i=0;i<childCount;++i)
{
final View v=getChildAt(i);
final HtmlStyleLayout.LayoutParams params = (HtmlStyleLayout.LayoutParams)v.getLayoutParams();
int newTop = v.getTop(); // set the default value
int newLeft = v.getLeft(); // of these to the value
int newBottom = v.getBottom(); // set by super.onLayout()
int newRight= v.getRight();
boolean viewChanged = false;
if(params.getTop() >= 0){
newTop = ( (int) ((b-t) * (params.getTop() * .01)) );
viewChanged = true;
}
if(params.getLeft() >= 0){
newLeft = ( (int) ((r-l) * (params.getLeft() * .01)) );
viewChanged = true;
}
if(params.getHeight() > 0){
newBottom = ( (int) ((int) newTop + ((b-t) * (params.getHeight() * .01))) );
setScaleType(v); // set the scale type to fitxy
viewChanged = true;
}else{
newBottom = (newTop + (v.getBottom() - v.getTop()));
Log.i("heightElse","v.getBottom()=" +
Integer.toString(v.getBottom())
+ " v.getTop=" +
Integer.toString(v.getTop()));
}
if(params.getWidth() > 0){
newRight = ( (int) ((int) newLeft + ((r-l) * (params.getWidth() * .01))) );
setScaleType(v);
viewChanged = true;
}else{
newRight = (newLeft + (v.getRight() - v.getLeft()));
}
// only call layout() if we changed something
if(viewChanged)
Log.i("SizeLocation",
Integer.toString(i) + ": "
+ Integer.toString(newLeft) + ", "
+ Integer.toString(newTop) + ", "
+ Integer.toString(newRight) + ", "
+ Integer.toString(newBottom));
v.layout(newLeft, newTop, newRight, newBottom);
}
pass = 0; // reset the parent pass counter
}
}
public class LayoutParams extends RelativeLayout.LayoutParams
{
private int top, left, width, height;
public LayoutParams(final Context context, final AttributeSet atts) {
super(context, atts);
TypedArray a = context.obtainStyledAttributes(atts, R.styleable.HtmlStyleLayout);
top = a.getInt(R.styleable.HtmlStyleLayout_top , -1);
left = a.getInt(R.styleable.HtmlStyleLayout_left, -1);
width = a.getInt(R.styleable.HtmlStyleLayout_width, -1);
height = a.getInt(R.styleable.HtmlStyleLayout_height, -1);
a.recycle();
}
public LayoutParams(int w, int h) {
super(w,h);
Log.d("lp","2");
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
Log.d("lp","3");
}
public LayoutParams(ViewGroup.MarginLayoutParams source) {
super(source);
Log.d("lp","4");
}
public int getTop(){
return top;
}
public int getLeft(){
return left;
}
public int getWidth(){
return width;
}
public int getHeight(){
return height;
}
}
}
Here is an example activity xml
<com.example.helpso.HtmlStyleLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:html="http://schemas.android.com/apk/res/com.example.helpso"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<ImageView
android:id="#+id/imageView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:scaleType="fitXY"
android:src="#drawable/bg" />
<ImageView
android:id="#+id/imageView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="#drawable/overlay"
html:height="10"
html:left="13"
html:top="18"
html:width="23" />
</com.example.helpso.HtmlStyleLayout>
Here are the images I used for testing.
If you do not set a value for a particular attribute it's default will be used. So if you set width but not height the image will scale in width and wrap_content for height.
Zipped project folder.
apk
I found the source of the bug. The problem is that I was using the layout's child count as in indicator of how many calls to onLayout it will make. This doesn't seem to hold true in older versions of android. I noticed in 2.1 onLayout is only called once. So I changed
if(pass == childCount){
to
if(true){
and it started working as expected.
I still thinks it's beneficial to adjust the layout only after the super is done. Just need to find a better way to know when that is.
EDIT
I didn't realize that your intention was to patch together images with pixel by pixel precision. I achieved the precision you are looking for by using double float precision variables instead of integers. However, you will not be able accomplish this while allowing your images to scale. When an images is scaled up pixels are added at some interval between the existing pixels. The color of the new pixels are some weighted average of the surrounding pixels. When you scale the images independently of each other they don't share any information. The result is that you will always have some artifact at the seam. Add to that the result of rounding since you can't have a partial pixel and you will always have a +/-1 pixel tolerance.
To verify this you can attempt the same task in your premium photo editing software. I use PhotoShop. Using the same images as in my apk, I placed them in seperate files. I scaled them both by 168% vertically and 127% horizontally. I then placed them in a file together and attempted to align them. The result was exactly the same as is seen in my apk.
To demonstrate the accuracy of the layout, I added a second activity to my apk. On this activity I did not scale the background image. Everything else is exactly the same. The result is seamless.
I also added a button to show/hide the overlay image and one to switch between the activities.
I updated both the apk and the zipped project folder on my google drive. You can get them by the links above.
After trying your code, I just find the reason of the problems you mentioned, and it is because in your customed layout, you only layout the child properly, however you forgot to measure your child properly, which will directly affect the drawing hierarchy, so simply add the below code, and it works for me.
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec)-this.getPaddingRight()-this.getPaddingRight();
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec)-this.getPaddingTop()-this.getPaddingBottom();
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if(heightMode == MeasureSpec.UNSPECIFIED || widthMode == MeasureSpec.UNSPECIFIED)
throw new IllegalArgumentException("the layout must have a exact size");
for (int i = 0; i < this.getChildCount(); ++i) {
View child = this.getChildAt(i);
LayoutParams lp = (LayoutParams)child.getLayoutParams();
int width = lp._viewHorizontalWeight * widthSize/(lp._leftHorizontalWeight+lp._rightHorizontalWeight+lp._viewHorizontalWeight);
int height = lp._viewVerticalWeight * heightSize/(lp._topVerticalWeight+lp._bottomVerticalWeight+lp._viewVerticalWeight);
child.measure(width | MeasureSpec.EXACTLY, height | MeasureSpec.EXACTLY);
}
this.setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
}
Now there is a nicer solution than the custom layout I've made:
PercentRelativeLayout
Tutorial can be found here and a repo can be found here.
Example code:
<android.support.percent.PercentRelativeLayout
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"/>
<ImageView
app:layout_widthPercent="50%"
app:layout_heightPercent="50%"
app:layout_marginTopPercent="25%"
app:layout_marginLeftPercent="25%"/>
</android.support.percent.PercentFrameLayout/>
or:
<android.support.percent.PercentFrameLayout
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"/>
<ImageView
app:layout_widthPercent="50%"
app:layout_heightPercent="50%"
app:layout_marginTopPercent="25%"
app:layout_marginLeftPercent="25%"/>
</android.support.percent.PercentFrameLayout/>
I wonder though if it can handle the issues I've shown here.
I propose to use following optimizations:
<FrameLayout 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:gravity="center">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content" android:text="#string/hello_world"
android:background="#ffff0000" android:gravity="center"
android:textSize="20dp" android:textColor="#ff000000" />
</FrameLayout>
or use http://developer.android.com/reference/android/widget/LinearLayout.html#attr_android:weightSum
or use TableLayout with layout_weight for rows and columns
or use GridLayout.

Categories

Resources