Currently I'm developing a screen which will generate views dynamically based on a response and it involves a lot of cases where I should change the focus/position of scroll view to the selected view. So far I have managed to solve most of issues by using this method when view is not visible on screen:
fun View.requestViewOnScreen() {
val rect = Rect(0, 0, width, height)
requestRectangleOnScreen(rect, false)
}
The problem is that I have a hierarchy of nested views, and when the view is visible, it stays where it is when I call the above code, even if i try it with the code below (also with delay):
scrollView.smoothScrollTo(0, view.top)
Hierarchy is like:
NestedScrollView
ConstraintLayout
CustomView
LinearLayout
The view which I want to position it on the top/center of screen
ScrollView widget:
<androidx.core.widget.NestedScrollView
android:id="#+id/scrollView"
android:layout_width="0dp"
android:layout_height="0dp"
android:fillViewport="true"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="#+id/stickyCtaView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible">
Any idea how can I adapt my code to make the NestedScrollView to scroll and position the requested view in center? Also I want to note that I have a reference to the view object itself. Thanks in advance!
view.top returns the top position of a view relative to its direct parent, in your case it's the top position in relation to a greater parent is required for that you can use a method like the following to aggregate the top position of a view in relation to the scrollView and scroll to it
private fun ScrollView.scrollTo(target: View) {
var topPosition = 0
var view = target
while (view !== this) {
topPosition += view.top
view = view.parent as View
}
smoothScrollTo(0, topPosition)
}
so to scroll to any view you just call scrollView.scrollTo(view)
Related
I wish to add a Custom ItemDecoration in my RecyclerView that is a layout defined in a XML file.
So far I was able to inflate the XML and position using canvas.translate (yet, without understanding everything).
Currently I have this code to draw:
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
for (i in 0 until parent.childCount) {
val child = parent.getChildAt(i)
val left = child.marginLeft
val top = child.top
context?.let {
//Inflate the Layout and set the Values (a text in this case)
val view = LayoutInflater.from(it).inflate(R.layout.my_decoration_layout, parent, false)
val textView = view.findViewById<TextView>(R.id.textView)
textView.text = "This is an ItemDecoration"
//Calculate the Size. Im using "hardcoded" values, but this does not seems to change how the View is rendered
view.measure(1000,1000)
view.layout(0, 0, 1000, 1000)
//Draw. I had to translate the canvas to apply the offset for each "ViewHolder"
canvas.save()
canvas.translate(left.toFloat(), top.toFloat())
view.draw(canvas)
canvas.restore()
}
}
}
The XML is (note the background colors to see the Rendered Area):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#android:color/holo_blue_light">
<androidx.appcompat.widget.AppCompatTextView
android:id="#+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#android:color/holo_red_light"/>
</LinearLayout>
With this, I could inflate and position my XML between each ViewHolder, but I still have two big issues:
My TextView is drawed over the ViewHolder, so I get here an overlap.
Would be great if this "Draw" operation pushes the ViewHolder.
Even my custom XML layout has it's width as "match_parent", it only wraps the text view.
If possible, Id like to know which exactly what "measure" and "layout" means and how it affect my View "area".
And how to prevent the overlap.
I "solved" the Overlap issue drawing a Text on the Canvas, using
Paint().apply {
color = Color.BLACK
style = Paint.Style.FILL
textSize = 40f
canvas.drawText(year.toString(), left.toFloat(), top.toFloat(), this)
}
But since my layout is a bit more complex, would be nice to understand how to do it with XML layouts.
Thanks
Currently, I need to use paddingTop and paddingBottom of RecyclerView, as I want to avoid complex space calculation, in my first RecyclerView item and last item.
However, I notice that, requiresFadingEdge effect will be affected as well.
This is my XML
<androidx.recyclerview.widget.RecyclerView
android:requiresFadingEdge="vertical"
android:paddingTop="0dp"
android:paddingBottom="0dp"
android:overScrollMode="always"
android:background="?attr/recyclerViewBackground"
android:id="#+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false" />
When paddingTop and paddingBottom is 40dp
As you can see, the fading effect shift down by 40dp, which is not what I want.
When paddingTop and paddingBottom is 0dp
Fading effect looks fine. But, I need to have non-zero paddingTop and paddingBottom, for my RecyclerView.
Is there a way to make RecyclerView's requiresFadingEdge unaffected by paddingTop and paddingBottom?
I found the best and kind of official solution for this. Override this 3 methods of RecyclerView. Which play most important role for fading edge.
First create your recyclerView.
public class MyRecyclerView extends RecyclerView {
// ... required constructor
#Override
protected boolean isPaddingOffsetRequired() {
return true;
}
#Override
protected int getTopPaddingOffset() {
return -getPaddingTop();
}
#Override
protected int getBottomPaddingOffset() {
return getPaddingBottom();
}
}
That's it. Use this recyclerview and you will see fadding edge unaffected.
Following content is just for explanation. If you want to know behind the scene.
To understand how this edge effect is working I dig into the class where android:requiresFadingEdge is used, And I found that it's not handled by RecyclerView instead It's handled by View class which is parent for all view.
In onDraw method of View class I found the code for drawing fade edge by using help of this method isPaddingOffsetRequired. Which used only for handling the fade effect.
According to documentation this method should be overridden by child class If you want to change the behaviour of fading edge. Bydefault It return false. So By returning true we are asking view to apply some offset for edge at the time of view drawing.
Look following snippet of onDraw method of View class to understand the calculation.
final boolean offsetRequired = isPaddingOffsetRequired();
if (offsetRequired) {
paddingLeft += getLeftPaddingOffset();
}
int left = mScrollX + paddingLeft;
int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
int top = mScrollY + getFadeTop(offsetRequired);
int bottom = top + getFadeHeight(offsetRequired);
if (offsetRequired) {
right += getRightPaddingOffset();
bottom += getBottomPaddingOffset();
}
As we can see top variable is initialize using getFadeTop(offsetRequired).
protected int getFadeTop(boolean offsetRequired) {
int top = mPaddingTop;
if (offsetRequired) top += getTopPaddingOffset();
return top;
}
In this method, top is calculated by adding value of topOffSet when offset is needed. So to reverse the effect we need to pass negative value of padding which you are passing. so we need to return -getPaddingTop().
Now for bottom we are not passing negative value because bottom is working on top + height. So passing negative value make fade more shorter from the bottom so we need to add bottom padding to make it proper visible.
You can override this 4 method to play with it. getLeftPaddingOffset(), getRightPaddingOffset(), getTopPaddingOffset(), getBottomPaddingOffset()
I suggest a few solutions
, I hope to be helpful
1- RecyclerView.ItemDecoration (keep requiresFadingEdge)
class ItemDecoration(private val spacing: Int) : RecyclerView.ItemDecoration(){
override fun getItemOffsets(outRect: Rect, view: View, parent:
RecyclerView, state: RecyclerView.State?) {
val position = parent.getChildAdapterPosition(view)
when(position)
0 -> { outRect.top = spacing /*40dp*/ }
parent.adapter.itemCount-1 -> { outRect.bottom = spacing /*40dp*/ }
}
2- Multiple ViewHolders (depending on the ViewHolder either keep or remove requiresFadingEdge)
https://stackoverflow.com/a/26245463/5255963
3- Gradient (remove requiresFadingEdge)
Make two gradients with drawables like this:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:type="linear"
android:angle="90"
android:startColor="#000"
android:endColor="#FFF" />
</shape>
And set background to two views at the top and bottom of the Recyclerview.
There are many ways to achieve that but I preferred below solution that works for me.
You can use a third-party library called Android-FadingEdgeLayout checkout here
Here is a dependency.
implementation 'com.github.bosphere.android-fadingedgelayout:fadingedgelayout:1.0.0'
In yours.xml
<com.bosphere.fadingedgelayout.FadingEdgeLayout
android:id="#+id/fading_edge_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:fel_edge="top|left|bottom|right"
app:fel_size_top="40dp"
app:fel_size_bottom="40dp"
app:fel_size_left="0dp"
app:fel_size_right="0dp">
<androidx.recyclerview.widget.RecyclerView
android:paddingTop="0dp"
android:paddingBottom="0dp"
android:overScrollMode="always"
android:id="#+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false" />
</com.bosphere.fadingedgelayout.FadingEdgeLayout>
Change your ReacyclerView property according to your need. below is example image with all sided sades. I hope that will help you.
Credits Android-FadingEdgeLayout
I'm struggeling with the RecyclerView. I use a recycler view to display the details of my model class.
//My model class
MyModel {
String name;
Double latitude;
Double longitude;
Boolean isOnline;
...
}
Since some of the values might not be present, I use the RecyclerView with custom view types (one representing each value of my model).
//Inside my custom adapter
public void setModel(T model) {
//Reset values
itemCount = 0;
deviceOfflineViewPosition = -1;
mapViewPosition = -1;
//If device is offline, add device offline item
if (device.isOnline() == null || !device.isOnline()) {
deviceOfflineViewPosition = itemCount;
itemCount++;
}
//Add additional items if necessary
...
//Always add the map as the last item
mapViewPosition = itemCount;
itemCount++;
notifyDataSetChanged();
}
#Override
public int getItemViewType(int position) {
if (position == deviceOfflineViewPosition) {
return ITEM_VIEW_TYPE_OFFLINE;
} else if (position == mapViewPosition) {
return ITEM_VIEW_TYPE_MAP;
} else if (...) {
//Check for other view types
}
}
With the RecyclerView I can easily determine at runtime which values are available and add corresponding items to the RecyclerView datasource. I simplyfied the code but my model has a lot more values and I have a lot more view types.
The last item in the RecyclerView is always a map and it is always present. Even if there is no value at all in my model, there will at least be one item, the map.
PROBLEM: How can I make the last item in RecyclerView fill the remaining space on screen and also have a min heigh. The size shall be what ever value is lager: the remaining space or the min height. For example:
Model has a few values, which in sum take up 100dp of a 600dp screen -> map heigh should be 500dp
Model has a lot of values, which in sum take up 500dp of a 600dp screen -> map heigh should be a min value of 200dp
Model has no values -> map fills whole screen
You can find the remaining space in RecyclerView after laying out the last item and add that remaining space to the minHeight of the last item.
val isLastItem = getItemCount() - 1 == position
if (isLastItem) {
val lastItemView = holder.itemView
lastItemView.doOnLayout {
val recyclerViewHeight = recyclerView.height
val lastItemBottom = lastItemView.bottom
val heightDifference = recyclerViewHeight - lastItemBottom
if (heightDifference > 0) {
lastItemView.minimumHeight = lastItemView.height + heightDifference
}
}
}
In onBindHolder check if the item is last item using getItemCount() - 1 == position. If it is the last item, find the height difference by subtracting recyclerView height with lastItem bottom (getBottom() gives you the bottom most pixel of the view relative to it's parent. In this case, our parent is RecyclerView).
If the difference is greater than 0, then add that to the current height of the last view and set it as minHeight. We are setting this as minHeight instead of setting directly as height to support dynamic content change for the last view.
Note: This code is Kotlin, and doOnLayout function is from Android KTx. Also your RecyclerView height should be match_parent for this to work (I guess that's obvious).
You can extend LinearLayoutManager to layout the last item yourself.
This is a FooterLinearLayoutManager that will move the last item of the list to the bottom of the screen (if it isn't already there). By overriding layoutDecoratedWithMargins the LinearLayouyManager calls us with where the item should go, but we can match this against the parent height.
Note: This will not "resize" the view, so fancy backgrounds or similar probably won't work, it will just move the last item to the bottom of the screen.
/**
* Moves the last list item to the bottom of the screen.
*/
class FooterLinearLayoutManager(context: Context) : LinearLayoutManager(context) {
override fun layoutDecoratedWithMargins(child: View, left: Int, top: Int, right: Int, bottom: Int) {
val lp = child.layoutParams as RecyclerView.LayoutParams
if (lp.viewAdapterPosition < itemCount - 1)
return super.layoutDecoratedWithMargins(child, left, top, right, bottom)
val parentBottom = height - paddingBottom
return if (bottom < parentBottom) {
val offset = parentBottom - bottom
super.layoutDecoratedWithMargins(child, left, top + offset, right, bottom + offset)
} else {
super.layoutDecoratedWithMargins(child, left, top, right, bottom)
}
}
}
I used muthuraj solution to solve a similar problem, I wanted the last item to be shown at the last normally if previous items fill up the height of the page or more height but in case the previous item doesn't fill up the height, I wanted last item to be shown on the bottom of the page.
When previous item + specialItem take all the place.
--------------
item
item
item
specialItem
--------------
When previous item + specialItem take more than the height
--------------
item
item
item
item
item
item
specialItem
--------------
When previous item + specialItem take less than the height
--------------
specialItem
--------------
or
--------------
item
specialItem
--------------
to archive this I use this code
val lastItemView = holder.itemView
//TODO use a better option instead of waiting for 200ms
Handler().postDelayed({
val lastItemTop = lastItemView.top
val remainingSpace = recyclerViewHeight() - lastItemTop
val heightToSet = Math.max(remainingSpace, minHeight)
if (lastItemView.height != heightToSet) {
val layoutParams = lastItemView.layoutParams
layoutParams.height = heightToSet
lastItemView.layoutParams = layoutParams
}
}, 200)
the reason I use Handler().postDelayed is that doOnLayout never gets called for me and I couldn't figure out why so instead run the code with 200ms delay until I found something better to work with.
This worked for me, let the recycler view with layout_height="0dp", and put the top constraint to the bottom of the corresponding above item, and the bottom constraint to the parent, then it will be sized with the remaining space:
<?xml version="1.0" encoding="utf-8"?>
<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">
<ToggleButton
android:id="#+id/toggleUpcomingButton"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:gravity="top"
android:text="#string/upcoming"
app:layout_constraintEnd_toStartOf="#+id/togglePupularButton"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ToggleButton
android:id="#+id/togglePupularButton"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="top"
android:text="#string/popular"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="#+id/toggleUpcomingButton"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/text_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="#string/popular_movies"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/toggleUpcomingButton" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/text_title" />
</androidx.constraintlayout.widget.ConstraintLayout>
In scrollview, if I add any view in middle, normally all the views below the added view scrolls downside. But I want to scroll the top views of added view to upside without disturbing the bottom views. Is it possible in scrollview , please help me ?
In the figure , If view 4 was added , then view 1 has to be scrolled upwards , without changing the positions of view 2 and view 3.
You can probably get the height of the view you are adding with and then scroll the scrollview manually that many pixels
scrollView.scrollBy(0, viewAdded.getHeight())
I've been wanting to try this question for quite some time, I finally got the chance today. The method is pretty simple (in fact, #dweebo already mentioned it earlier) - we move the ScrollView up as we add the view. For getting precise (and valid) dimensions when adding, we use a ViewTreeObserver. Here's the code you can get hints from:
// Getting reference to ScrollView
final ScrollView scrollView = (ScrollView) findViewById(R.id.scrollView);
// Assuming a LinearLayout container within ScrollView
final LinearLayout parent = (LinearLayout) findViewById(R.id.parent);
// The child we are adding
final View view = new View(ScaleActivity.this);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 100);
view.setLayoutParams(params);
// Finally, adding the child
parent.addView(view, 2); // at index 2
// This is what we need for the dimensions when adding
ViewTreeObserver viewTreeObserver = parent.getViewTreeObserver();
viewTreeObserver.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
#Override
public boolean onPreDraw() {
parent.getViewTreeObserver().removeOnPreDrawListener(this);
scrollView.scrollBy(0, view.getHeight());
// For smooth scrolling, run below line instead
// scrollView.smoothScrollBy(0, view.getHeight())
return false;
}
});
I have a number of elements in a ListView that scroll off the screen.
I would like there to be blank space at the end of the View. That is, the user should be able to scroll past the last element such that the last element is in the middle of the viewport.
I could use an OverScroller, but I think that would only enable the View to have a bouncy effect like one often sees on the iPhone.
Is there something I might have overlooked?
The scrolled-to-the-botton screen should look something like this:
The accepted answer is too complicated, and addFooterView is not for this kind of thing. The proper and simpler way is to set the paddingTop and paddingBottom, and you need to set clipToPadding to "false". In your list view or grid view, add the following:
android:paddingTop="100dp"
android:paddingBottom="100dp"
android:clipToPadding="false"
You'll get blank space at the top and the bottom that moves with your finger scroll.
Inflate any layout of your choice (this could be an XML of and ImageView with no drawable and with set height and width of your choice)
Measure the screen height and create new LayoutParams and set the height of it to 1/2 of the screen height
Set the new layout params on your inflated view
Use the ListView's addFooterView() method to add that view to the bottom of your list (there is also an addHeaderView())
Code to measure screen height
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
int screenHeight = display.getHeight();
Code to set half screen height:
View layout = inflater.inflate(R.layout.mylistviewfooter, container, false);
ViewGroup.LayoutParams lp = layout.getLayoutParams();
lp.height = screenHeight/2;
layout.setLayoutParams(lp);
myListView.addFooterView(layout);
An Aside:
When you add a footer or header view to any listview, it has to be done before adding the adapter. Also, if you need to get your adapter class after doing this you will need to know calling the listview's adapter by getAdapter() will return an instance of HeaderViewListAdapter in which you will need to call its getWrappedAdapter method
Something like this :
MyAdapterClassInstance myAdapter = (MyAdapterClassInstance) ((HeaderViewListAdapter) myListView.getAdapter()).getWrappedAdapter();
this 100% works.
in adapter set your code like this
//in getCount
#Override
public int getCount() {
return ArrayList.size()+1;
}
//in getview make your code like this
public View getView(final int i, View view, ViewGroup viewGroup) {
view = inflter.inflate(R.layout.yourlayout, null);
if(i<getCount()-1) {
//your code
}
else{
ViewGroup itemContainer =(ViewGroup) view.findViewById(R.id.container);
itemContainer.setVisibility(View.INVISIBLE);
}
Return view;
}
if you have multiple listviews in your app, create an xml of a footer, something like this:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:minHeight="200dp"
android:layout_width="match_parent"
android:layout_height="200dp"></LinearLayout>
and then in the code, use this:
listView.addFooterView(LayoutInflater.from(getActivity()).inflate(R.layout.empty200, null));
This do the job in a simple way
android:paddingBottom="100dp"
android:clipToPadding="false"
Try the followings:
View footer = new View(getActivity());
footer.setLayoutParams( new AbsListView.LayoutParams( LayoutParams.FILL_PARENT, 100 ));
// 100 is the height, you can change it.
mListView.addFooterView(footer, null, false);