I have a RecyclerView with Expandable Child Views, when the child ViewGroup is clicked it inflates an amount of views animating the ViewGroup height from 0 to the measured viewgroup height, like the following gif:
The problem is: I'm calling smoothScrollToPosition on recyclerView, it smooth scroll to the view position, but it considers the current view height, which is still not expanded, in the above gif i'm touching on the under view of the recyclerview, which dont scroll to position because the view is already visible, but when i touch again (calling the smoothscrolltoposition again) it scroll the view to the correct position, because the view is already expanded.
Is there any approach to scroll the view to the top of screen or just scroll to make content visible?
For references:
This is the method called to inflate the views:
collapsible_content.removeAllViews();
for(int i = 0; i < 5; i++) {
View link_view = getLayoutInflater().inflate(R.layout.list_item_timeline_step_link, collapsible_content, false);
TextView text = (TextView) link_view.findViewById(R.id.step_link_text);
text.setText("Test");
collapsible_content.addView(link_view);
}
And this is my method to expand:
public void toggle() {
collapsible_content.measure(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
Animation a;
if (mExpanded) {
a = new ExpandAnimation(collapsible_content.getLayoutParams().height, 0);
} else {
a = new ExpandAnimation(collapsible_content.getLayoutParams().height, getMeasuredHeight());
}
a.setDuration(mAnimationDuration);
collapsible_content.startAnimation(a);
mExpanded = !mExpanded;
}
And the animation:
private class ExpandAnimation extends Animation {
private final int mStartHeight;
private final int mDeltaHeight;
public ExpandAnimation(int startHeight, int endHeight) {
mStartHeight = startHeight;
mDeltaHeight = endHeight - startHeight;
}
#Override
protected void applyTransformation(float interpolatedTime,
Transformation t) {
final int newHeight = (int) (mStartHeight + mDeltaHeight *
interpolatedTime);
collapsible_content getLayoutParams().height = newHeight;
if (newHeight <= 0) {
collapsible_content setVisibility(View.GONE);
} else {
collapsible_content setVisibility(View.VISIBLE);
}
collapsible_content requestLayout();
}
#Override
public boolean willChangeBounds() {
return true;
}
}
My solution was to constant check for view bottom within applyTransformation method, and compare it with RecyclerView height, if the bottom get higher than the RV height, i scroll by the diff values:
final int bottom = collapsible_content.getBottom();
final int listViewHeight = mRecyclerView.getHeight();
if (bottom > listViewHeight) {
final int top = collapsible_content.getTop();
if (top > 0) {
mRecyclerView.smoothScrollBy(0, Math.min(bottom - listViewHeight + mRecyclerView.getPaddingBottom(), top));
}
}
The trick was to use Math.min to get the view top, so it don't scroll up making the top not visible.
Solution based on ListViewAnimations
Add an animationlistener and start the scrolling of the recyclerview after the expanding animation is finished.
Related
I have an imageView and a textView inside a cardView.
The alpha of the cardView is set to .5f.
The cardView is used in a vertical recyclerView.
What I'm trying to do here is as the user scrolls through the reyclerView the alpha of the completely visible cardView should always change to 1f and for the non-completely visible cardViews alpha stays .5f.
There is only one completely visible cardView at a time.
Here is what I tried but it doesn't work.
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int center = recyclerView.getHeight() / 2;
View centerView = recyclerView.findChildViewUnder( recyclerView.getTop(), center);
int centerPos = recyclerView.getChildAdapterPosition(centerView);
if (prevCenterPos != centerPos) {
// dehighlight the previously highlighted view
View prevView =
recyclerView.getLayoutManager().findViewByPosition(prevCenterPos);
if (prevView != null) {
prevView.setAlpha(.5f);
}
// highlight view in the middle
if (centerView != null) {
prevView.setAlpha(1f);
}
prevCenterPos = centerPos;
}
}
As you are finding center point with this
int center = recyclerView.getHeight() / 2;
which is not proper way you should use:
mLayoutManager.findFirstCompletelyVisibleItemPosition()
try this:
val firstCompelteVisible = mLayoutManager.findFirstCompletelyVisibleItemPosition()
val centerView =
recyclerView.layoutManager!!.findViewByPosition(firstCompelteVisible)
if (prevCenterPos != centerPos) {
// dehighlight the previously highlighted view
val prevView =
recyclerView.layoutManager!!.findViewByPosition(prevCenterPos)
if (prevView != null) {
prevView.alpha = .5f
}
// highlight view in the middle
if (centerView != null) {
prevView!!.alpha = 1f
}
prevCenterPos = centerPos
}
I hope this will work.
How can i calculate the height of the first visible item in my recyclerview ?
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
switch (newState) {
case RecyclerView.SCROLL_STATE_IDLE:
Log.d("scroll_stop","The RecyclerView is not scrolling : ");
View firstItemView = mGLM.findViewByPosition(mGLM.findFirstVisibleItemPosition());
View mVizibleView= mPlm.findViewByPosition(mGLM.findFirstVisibleItemPosition());
}
});
But this give me the full height of the view
Taking the Size of any RecyclerView Item is pointless, If you getHeight() or getWidth() of any item it's just gonna give a Null Error and your app is gonna crash.
What you can do, Is to Use your ViewHolder from recyclerView Adapter and First set a Specific Height to any Item you want and Then use that Height somewhere Else.
Look at the Code Below, Maybe helps:
class myViewHolder extends RecyclerView.ViewHolder {
RemaltiveLayout itemLayout;
public myViewHolder(View itemView) {
super(itemView);
itemLayout = (RemaltiveLayout) itemView.findViewById(R.id.myItemLayout);
itemLayout.getLayoutParams().height = res.getDimensionPixelSize(R.dimen.height_value);
}
You should be able to use item at a specific location position as well
This is not gonna work with Width though, cause items are always being stretched to fill columns of layoutManager
Steps to get first item height:
Get the layout manager from the recyclerview reference in onScrolled()
Get the first the visible item position using layoutManager.findFirstVisibleItemPosition()
If fist visible item position is 0 then get the 0th child height layoutManager.getChildAt(0)
Sample code:
RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(#NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
if (layoutManager != null) {
int child0Height = 0;
int currentFirstVisibleItem = layoutManager.findFirstVisibleItemPosition();
if (currentFirstVisibleItem == 0) {
final View childAt0 = layoutManager.getChildAt(0);
if (childAt0 != null) {
child0Height = childAt0.getHeight();
}
}
}
}
};
I am using two different ExpandableListViews in my fragment inside a scroll view, one right below the other.
The problem is that only one ExpandableListView heading is displayed when the activity is called. Please refer the image below:
Also, when I click the expandable list view, the list view expands and the other ExpandableListView also displays. Refer the image below:
I want the both the Expandable list views to display when the activity gets called for the first time.
This is my xml:
<ExpandableListView
android:id="#+id/exLInTheMoodFor"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:scrollbars="none"
android:groupIndicator="#null"
android:layout_below="#+id/lblInTheMoodFor"
android:layout_marginTop="10dp"/>
And this is the java code for defining and initializing the Expandable list view and setting height:
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View convertView = inflater.inflate(R.layout.cafes_more_fragment, container, false);
final ExpandListChild1 items = new ExpandListChild1();
exLInTheMoodFor = (ExpandableListView) convertView.findViewById(R.id.exLInTheMoodFor);
ExpListItems = SetStandardGroups();
ExpAdapter = new ExpandListAdapterProduct(activity, ExpListItems);
exLInTheMoodFor.setAdapter(ExpAdapter);
exLInTheMoodFor.setOnGroupClickListener(new ExpandableListView.OnGroupClickListener() {
#Override
public boolean onGroupClick(ExpandableListView expandableListView, View view, int i, long l) {
setListViewHeight(exLInTheMoodFor, i);
return false;
}
});
return convertView;
}
//for set height show method in expnadable list view
private void setListViewHeight(ExpandableListView listView, int group) {
android.widget.ExpandableListAdapter listAdapter = (android.widget.ExpandableListAdapter) listView.getExpandableListAdapter();
int totalHeight = 0;
int desiredWidth = View.MeasureSpec.makeMeasureSpec(listView.getWidth(),
View.MeasureSpec.EXACTLY);
for (int i = 0; i < listAdapter.getGroupCount(); i++) {
View groupItem = listAdapter.getGroupView(i, false, null, listView);
groupItem.measure(desiredWidth, View.MeasureSpec.UNSPECIFIED);
totalHeight += groupItem.getMeasuredHeight();
if (((listView.isGroupExpanded(i)) && (i != group))
|| ((!listView.isGroupExpanded(i)) && (i == group))) {
for (int j = 0; j < listAdapter.getChildrenCount(i); j++) {
View listItem = listAdapter.getChildView(i, j, false, null,
listView);
listItem.measure(desiredWidth, View.MeasureSpec.UNSPECIFIED);
totalHeight += listItem.getMeasuredHeight();
}
}
}
ViewGroup.LayoutParams params = listView.getLayoutParams();
int height = totalHeight
+ (listView.getDividerHeight() * (listAdapter.getGroupCount() - 1));
if (height < 10)
height = 200;
params.height = height;
listView.setLayoutParams(params);
listView.requestLayout();
}
Expanding all groups in the list
for(int i=0; i < myAdapter.getGroupCount(); i++)
mexpandableListView.expandGroup(i);
if Expanding one item in the list
mexpandableListView.expandGroup(0);
One item Expanding and balance item compressed
mexpandableListView.setOnGroupExpandListener(new ExpandableListView.OnGroupExpandListener() {
#Override
public void onGroupExpand(int i) {
ExpandableAdapter myAdapter= (ExpandableHomeworkAdapter) mexpandableListView.getExpandableListAdapter();
if (myAdapter== null){
return;
}
for (int k=0;k<myAdapter.getGroupCount();k++){
if (k!=i){
mexpandableListView.collapseGroup(k);
}
}
}
});
The problem was using listview/expandablelistview inside scrollview. Generally this combination causes the visibility issue of listview existing inside the scrollview. Full explanation can be found from this answer. This link and this link should also be helpful for solving your problem. I hope this helps.
I haven't been able to find any post about it...
We have the good old RecyclerView.ItemDecoration code (taken from Suleiman's Mansonry Github project):
public class SpacesItemDecoration extends RecyclerView.ItemDecoration {
private final int mSpace;
public SpacesItemDecoration(int space) {
this.mSpace = space;
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
outRect.left = mSpace;
outRect.right = mSpace;
outRect.bottom = mSpace;
// Add top margin only for the first item to avoid double space between items
if (parent.getChildAdapterPosition(view) == 0)
outRect.top = mSpace;
}
}
I want to have a condition that sets mSpace (offset/margin) depending on the current LayoutManager in the RecyclerView.
For example:
if(/* LayoutManager is LinearLayoutManager*/){
//Set larger margin
}else{
//Set lower margin
}
So... as I was re-reading the question to check if anything was missing, and I realized that you actually get a RecyclerView reference (parent) as an argument of getItemOffsets().
So you can just call parent.getLayoutManager() from inside the function.
Example:
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
if (parent.getLayoutManager() instanceof LinearLayoutManager){
margin = 2;
}else if (parent.getLayoutManager() instanceof StaggeredGridLayoutManager){
margin = 1;
}else{
margin = 0;
}
//Do magic
}
What i've tried to far:
Calling measure()
tv.measure(0, 0);
int height = tv.getMeasuredHeight();
Calling measure() with specified sizes/modes
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(99999, View.MeasureSpec.AT_MOST);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
tv.measure(widthMeasureSpec, heightMeasureSpec);
int height = tv.getMeasuredHeight();
Calling getTextBounds()
Rect bounds = new Rect();
tv.getPaint().getTextBounds(tv.getText().toString(), 0, tv.getText().length(), bounds);
int height = bounds.height();
Calling measure() and then calling getTextBounds()
Calling getLineCount() * getLineHeight()
None seem to work. They all return incorrect values (container view gets incorrect height - it's either too small or too large)
Ideas on how to calculate this simple thing??
You need to specify the available width so the height can be properly calculated.
Note: In cases where you just need to get the height of a view that is already drawn, use ViewTreeObserver. See this question for a thing to consider in that case.
This is why I do, in a piece of code where I want to scale a view from hidden to its full necessary height:
int availableWidth = getParentWidth(viewToScale);
int widthSpec = View.MeasureSpec.makeMeasureSpec(availableWidth, View.MeasureSpec.AT_MOST);
int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
viewToScale.measure(widthSpec, heightSpec);
int measuredHeight = viewToScale.getMeasuredHeight();
// Offtopic: Now I animate the height from 1 to measuredHeight (0 doesn't work)
You may pass the availableWidth yourself, but I calculate it from the parent:
private int getParentWidth(View viewToScale)
{
final ViewParent parent = viewToScale.getParent();
if (parent instanceof View) {
final int parentWidth = ((View) parent).getWidth();
if (parentWidth > 0) {
return parentWidth;
}
}
throw new IllegalStateException("View to scale must have parent with measured width");
}
Where are you calling those methods?
The best way to do what you are trying to do is to use ViewTreeObserver and add onPreDrawListener.
Take a look at this i think it will help
What you can do here is get ViewTreeObserver associated with this TextView and add OnGlobalLayoutListener to it:
final ViewTreeObserver vto = textView.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
// textView dimensions are calculated at this stage, but textView
// isn't rendered yet. Do what you need to do and remove OnGlobalLayoutListener
// after
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
textView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
} else {
textView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
}
}
I had a similar problem recently, trying to measure the height of a ViewGroup containing several multiline TextView.
What worked for me was to measure() the TextView, then add (lineCount-1)*lineHeight to its measuredHeight.
Here is the code to measure only one TextView :
private int getMeasuredHeight(TextView textView) {
textView.measure(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
int height = textView.getMeasuredHeight();
height += (textView.getLineCount()-1) * textView.getLineHeight();
return height;
}
And in the case of a ViewGroup with many TextViews, here is my code :
private int getMeasuredHeight(ViewGroup viewGroup) {
viewGroup.measure(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
int height = viewGroup.getMeasuredHeight();
for(TextView textView : findAllTextView(viewGroup)) {
height += (textView.getLineCount()-1) * textView.getLineHeight();
}
return height;
}
private List<TextView> findAllTextView(ViewGroup v) {
List<TextView> result = new ArrayList<>();
for (int i = 0; i < v.getChildCount(); i++) {
Object child = v.getChildAt(i);
if (child instanceof TextView)
result.add((TextView) child);
else if (child instanceof ViewGroup)
for(TextView tv: findAllTextView((ViewGroup) child))
result.add(tv);
}
return result;
}