How can I check if a ScrollView is higher than the screen? When the content of a ScrollView fits on the screen, the ScrollView isn't scrollable, when it's contents exceed the screen height it's scrollable.
How can I check the condition of a ScrollView in that regard?
This is the code from ScrollView, which is private, but can be adapted to be used outside of the class itself
/**
* #return Returns true this ScrollView can be scrolled
*/
private boolean canScroll() {
View child = getChildAt(0);
if (child != null) {
int childHeight = child.getHeight();
return getHeight() < childHeight + mPaddingTop + mPaddingBottom;
}
return false;
}
Too late, but I'm using the following code and it looks more safe for me:
if (view.canScrollVertically(1) || view.canScrollVertically(-1)) {
// you code here
}
A ScrollView always has 1 child. All you need to do is get the height of the child
int scrollViewHeight = scrollView.getChildAt(0).getHeight();
and Calculate Height of Your Screen
if both are equal(or scrollView Height is more) then it fits on your screen.
In my case, I was checking to see if my scrollView(which contained text) was scrollable vertically when the activity was created. On phones, it would scroll but on tablets it couldn't. canScrollVertically was returning me incorrect value because it couldn't be determined yet. I fixed this issue by calling it in the OnGlobalLayoutListener.
(Kotlin)
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
// Must use onGlobalLayout or else canScrollVertically will not return the correct value because the layout hasn't been made yet
scrollView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
// If the scrollView can scroll, disable the accept menu item button
if ( scrollView.canScrollVertically(1) || scrollView.canScrollVertically(-1) )
acceptMenuItem?.isEnabled = false
// Remove itself after onGlobalLayout is first called or else it would be called about a million times per second
scrollView.viewTreeObserver.removeOnGlobalLayoutListener(this)
}
})
}
My use case was displaying terms of use. I didn't want the accept button to be enabled until the user scrolled to the bottom. I know this is late but i hope this resolves some confusion about canScrollVertically
Related
I'm having troubles with some animation in a recycler view. I do the relevant measurements in onViewAttachedToWindow:
override fun onViewAttachedToWindow(holder: PairingViewHolder) {
super.onViewAttachedToWindow(holder)
// get originalHeight & expandedHeight if not gotten before
if (holder.expandedHeight < 0) {
// Execute pending bindings, otherwise the measurement will be wrong.
holder.itemViewDataBinding.executePendingBindings()
holder.cardContainer.layoutParams.width = expandedWidth
holder.expandedHeight = 0 // so that this block is only called once
holder.cardContainer.doOnLayout { view ->
holder.originalHeight = view.height
holder.expandView.isVisible = true
// show expandView and record expandedHeight in next layout pass
// (doOnPreDraw) and hide it immediately.
view.doOnPreDraw {
holder.expandedHeight = view.height
holder.expandView.isVisible = false
holder.cardContainer.layoutParams.width = originalWidth
}
}
}
}
The problem is that doOnPreDraw gets called just for some views. It is something related to the visibility of the views I guess, since the smaller the items (expanded) are, the highest the count of the ones on which onPreDraw gets called.
My guess is that since I'm expanding them in onLayout, the recyclerView consider visible only the ones that when expanded are actually visible on screen. In onPreDraw I collapse them, resulting in some views being able to animate correctly and some not.
How would you solve this?
Thanks in advance.
I have a RecyclerView with items of varying heights with a scrollbar.
Because of the different heights of the items, the scrollbar changes it's vertical size, dependent on which items are currently displayed (see screenshots).
I have created an example project that displays the problem here.
Has anyone had the same problem and fixed it?
How can I override the calculation of the scrollbar height and position to come up with an own implementation?
EDIT: The scrollbar's position and height can be controlled by overriding RecyclerViews computeVerticalScrollOffset, computeVerticalScrollRange and computeVerticalScrollExtent.
I have no idea though on how to implement these to make the scrollbar work properly with dynamic item heights.
The problem, I reckon, is that RecyclerView estimates the total height of all items based on the items currently visible and sets position and height of the scrollbar accordingly. One way to solve this might be to give a better estimation of the total height of all items.
The best way to handle this situation may be to somehow calculate the scroll bar range based on the size of each item. That may not be practical or desirable. In lieu of that, here is a simple implementation of a custom RecyclerView that you can play with to try to get what you want. It will show you how you can use the various scroll methods to control the scroll bar. It will stick the size of the thumb to an initial size based upon the number of items displayed. The key thing to remember is that the scroll range is arbitrary but all other measurements (extent, offset) must use the same units.
See the documentation for computeVerticalScrollRange().
Here is a video of the result.
Update: The code has been updated to correct a few issues: The movement of the thumb is less jerky and the thumb will now come to rest at the bottom as the RecyclerView scrolls to the bottom. There are also a few caveats that are given after the code.
MyRecyclerView.java (updated)
public class MyRecyclerView extends RecyclerView {
// The size of the scroll bar thumb in our units.
private int mThumbHeight = UNDEFINED;
// Where the RecyclerView cuts off the views when the RecyclerView is scrolled to top.
// For example, if 1/4 of the view at position 9 is displayed at the bottom of the RecyclerView,
// mTopCutOff will equal 9.25. This value is used to compute the scroll offset.
private float mTopCutoff = UNDEFINED;
public MyRecyclerView(Context context) {
super(context);
}
public MyRecyclerView(Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
}
public MyRecyclerView(Context context, #Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/**
* Retrieves the size of the scroll bar thumb in our arbitrary units.
*
* #return Scroll bar thumb height
*/
#Override
public int computeVerticalScrollExtent() {
return (mThumbHeight == UNDEFINED) ? 0 : mThumbHeight;
}
/**
* Compute the offset of the scroll bar thumb in our scroll bar range.
*
* #return Offset in scroll bar range.
*/
#Override
public int computeVerticalScrollOffset() {
return (mTopCutoff == UNDEFINED) ? 0 : (int) ((getCutoff() - mTopCutoff) * ITEM_HEIGHT);
}
/**
* Computes the scroll bar range. It will simply be the number of items in the adapter
* multiplied by the given item height. The scroll extent size is also computed since it
* will not vary. Note: The RecyclerView must be positioned at the top or this method
* will throw an IllegalStateException.
*
* #return The scroll bar range
*/
#Override
public int computeVerticalScrollRange() {
if (mThumbHeight == UNDEFINED) {
LinearLayoutManager lm = (LinearLayoutManager) getLayoutManager();
int firstCompletePositionw = lm.findFirstCompletelyVisibleItemPosition();
if (firstCompletePositionw != RecyclerView.NO_POSITION) {
if (firstCompletePositionw != 0) {
throw (new IllegalStateException(ERROR_NOT_AT_TOP_OF_RANGE));
} else {
mTopCutoff = getCutoff();
mThumbHeight = (int) (mTopCutoff * ITEM_HEIGHT);
}
}
}
return getAdapter().getItemCount() * ITEM_HEIGHT;
}
/**
* Determine where the RecyclerVIew display cuts off the list of views. The range is
* zero through (getAdapter().getItemCount() - 1) inclusive.
*
* #return The position in the RecyclerView where the displayed views are cut off. If the
* bottom view is partially displayed, this will be a fractional number.
*/
private float getCutoff() {
LinearLayoutManager lm = (LinearLayoutManager) getLayoutManager();
int lastVisibleItemPosition = lm.findLastVisibleItemPosition();
if (lastVisibleItemPosition == RecyclerView.NO_POSITION) {
return 0f;
}
View view = lm.findViewByPosition(lastVisibleItemPosition);
float fractionOfView;
if (view.getBottom() < getHeight()) { // last visible position is fully visible
fractionOfView = 0f;
} else { // last view is cut off and partially displayed
fractionOfView = (float) (getHeight() - view.getTop()) / (float) view.getHeight();
}
return lastVisibleItemPosition + fractionOfView;
}
private static final int ITEM_HEIGHT = 1000; // Arbitrary, make largish for smoother scrolling
private static final int UNDEFINED = -1;
private static final String ERROR_NOT_AT_TOP_OF_RANGE
= "RecyclerView must be positioned at the top of its range.";
}
Caveats
The following issues may need to be addressed depending on the implementation.
The sample code works only for vertical scrolling. The sample code also assumes that the contents of the RecyclerView are static. Any updates to the data backing the RecyclerView may cause scrolling issues. If any changes are made that effect the height of any view displayed on the first full screen of the RecyclerView, the scrolling will be off. Changes below that will probably work OK. This is due to how the code calculates the scrolling offset.
To determine the base value for the scrolling offset, (variable mTopCutOff), the RecyclerView must be scrolled to the top the first time computeVerticalScrollRange() is invoked so views can be measured; otherwise, the code will stop with an "IllegalStateException". This is especially troublesome on an orientation change if the RecyclerView is scrolled at all. A simple way around this would be to inhibit restoration of the scrolling position so it defaults to the top on an orientation change.
(The following is probably not the best solution...)
var lm: LinearLayoutManager = object : LinearLayoutManager(this) {
override fun onRestoreInstanceState(state: Parcelable?) {
// Don't restore
}
}
I hope this helps. (btw, your MCVE made this a lot easier.)
Use item positions as metric of scroll progress. This will cause your scroll indicator to become a bit jumpy, but at least it will remain fixed-sized.
There are multiple implementations of custom scroll indicators for RecyclerView. Most double as fast scrollers.
Here is my own implementation, based on RecyclerViewFastScroller library. Basically, one have to create a custom View subclass, that will be animated, similarly to ScrollView and DrawerLayout:
Store current offset
During animation offset position of thumb View via View#offset* calls
During layout set position based on current offset.
You probably don't want to start learning all that magic now, just use some existing fast scrolling library (RecyclerViewFastScroller or one of it's clones).
Inspired by Cheticamp's solution I managed to spin my own extension of RecyclerView which doesn't have the computeVerticalScrollRange limitations.
In fact, this alternative solution doesn't require extending computeVerticalScrollRange at all.
By reasoning with things in terms of spans I managed to think of a solution that doesn't depend on calculating the height of any items in the RecyclerView.
Each item in the list has a span of 1, and I am fixing the scrollbar thumb size to a certain number of spans (meaning the scrollbar doesn't change its height as the user scrolls).
Now consider the following things:
rangeSpanCount to be the number of spans a.k.a the number of items in the adapter
firstSpan to be the position of the first visible span (first completely visible if any, otherwise the first partially visible)
lastSpan to be the position of the last visible span (last completely visible if any, otherwise the last partially visible)
visibleSpanCount, equal to lastSpan - firstSpan, to be the number of spans currently visible in the screen
remainingSpanCount, equal to rangeSpanCount - 1 - visibleSpanCount, to be the number of spans remaining in the RecyclerView
Then for the sake of the explanation assume we have a list of 9 spans, and only 3 of them can be visible at any given time (although the logic holds even if the number of visible spans at a given moment is dynamic):
0 1 2 3 4 5 6 7 8 9
0 1 2{------------}
| the size of this range is:
{--}2 3 4 |===========> rangeSpanCount - 1 - visibleSpanCount
{-----}3 4 5
{-------}4 5 6
{-----------}6 7 8
{-------------}7 8 9
| you can see that this range is simply computed as:
|===========> firstSpan - 0
Then notice how we can use the range that grows as the scrolling from top to bottom happens and the range of spans that is left out of sight at any given moment to calculate the progress of the scrolling throughout the RecyclerView.
First we figure out how much has the growing range grown:
partialProgress = (firstSpan - 0) / remainingSpanCount
(From 0% all the way to 100% when firstSpan == remainingSpanCount)
Then we calculate which span among the visible ones better represent the progress of the scrolling throughout the RecyclerView. Basically, we want to make sure the first span (of position 0) is chosen when RecyclerView is at the very top and the last span (of position rangeSpanCount - 1) to be chosen when we reach the very bottom. This is important otherwise your scrolling will be off when reaching these edges.
progressSpan = firstSpan + (visibleSpanCount * partialProgress)
And finally, you can use the position of this chosen span and the total number of spans to figure out the actual progress percentage across the RecyclerView, and use the real computed scroll range to determine the best offset for the scrollbar:
scrollProgress = progressSpan / rangeSpanCount
scrollOffset = scrollProgress * super.computeVerticalScrollRange()
And that's it! This solution can be adapted to support the horizontal axis, so it carries none of the caveats from Cheticamp's alternative.
It has one caveat, though: the movement of the scrollbar thumb is discrete, not continuous along the axis, meaning the jumping from one position to the next is noticeable. It is consistent, though, never "shaking" itself / going back and forth while the user performs a scroll to any direction.
This caveat can probably be solved by working with a much higher number of spans in respect to the number of items in the adapter (e.g. having multiple spans per item) but I didn't give it too much thought right now.
I hope my explanation is reasonably clear... and I thank you all for helping me with your answers, it really helped point me to the right direction!
Below you can check out the complete solution and source code:
package cz.nn.calllog.view.utils.recyclerview
import android.content.Context
import android.util.AttributeSet
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
class SmartScrollbarRecyclerView(
context: Context,
attributeSet: AttributeSet?,
defaultStyleAttribute: Int
) : RecyclerView(context, attributeSet, defaultStyleAttribute) {
constructor(
context: Context,
attributeSet: AttributeSet
) : this(context, attributeSet, 0)
constructor(
context: Context
) : this(context, null, 0)
override fun computeVerticalScrollExtent(): Int {
return checkCalculationPrerequisites(
onFailure = {
super.computeVerticalScrollExtent()
},
onSuccess = { _, rangeSpan, scrollRange ->
val extentSpanCount = 1.5F
val scrollExtent = (extentSpanCount / rangeSpan)
(scrollExtent * scrollRange).toInt()
}
)
}
override fun computeVerticalScrollOffset(): Int {
return checkCalculationPrerequisites(
onFailure = {
super.computeVerticalScrollOffset()
},
onSuccess = { layoutManager, rangeSpanCount, scrollRange ->
val firstSpanPosition = calculateFirstVisibleItemPosition(layoutManager)
val lastSpanPosition = calculateLastVisibleItemPosition(layoutManager)
val visibleSpanCount = lastSpanPosition - firstSpanPosition
val remainingSpanCount = rangeSpanCount - 1 - visibleSpanCount
val partialProgress = (firstSpanPosition / remainingSpanCount)
val progressSpanPosition = firstSpanPosition + (visibleSpanCount * partialProgress)
val scrollProgress = progressSpanPosition / rangeSpanCount
(scrollProgress * scrollRange).toInt()
}
)
}
private fun calculateFirstVisibleItemPosition(layoutManager: LinearLayoutManager): Int {
val firstCompletelyVisibleItemPosition = layoutManager.findFirstCompletelyVisibleItemPosition()
return if (firstCompletelyVisibleItemPosition == -1) {
layoutManager.findFirstVisibleItemPosition()
} else {
firstCompletelyVisibleItemPosition
}
}
private fun calculateLastVisibleItemPosition(layoutManager: LinearLayoutManager): Int {
val lastCompletelyVisibleItemPosition = layoutManager.findLastCompletelyVisibleItemPosition()
return if (lastCompletelyVisibleItemPosition == -1) {
layoutManager.findLastVisibleItemPosition()
} else {
lastCompletelyVisibleItemPosition
}
}
private fun checkCalculationPrerequisites(
onFailure: () -> Int,
onSuccess: (LinearLayoutManager, Float, Int) -> Int
): Int {
val layoutManager = layoutManager
if (layoutManager !is LinearLayoutManager) {
return onFailure.invoke()
}
val scrollRange = computeVerticalScrollRange()
if (scrollRange < height) {
return 0
}
val rangeSpanCount = calculateRangeSpanCount()
if (rangeSpanCount == 0F) {
return 0
}
return onSuccess.invoke(layoutManager, rangeSpanCount, scrollRange)
}
private fun calculateRangeSpanCount(): Float {
val recyclerAdapter = adapter ?: return 0F
return recyclerAdapter.itemCount.toFloat()
}
}
If I'm not mistaken the attribute android:scollBarSize="Xdp" should work for you. Add it to your RecyclerView xml.
That way you decide the size, and it will remain fixed.
I am developing my first Android App and after a good start, I have spent days of deep debugging on a problem, which by now seems to be an error in the implementation of View.requestRectangleOnScreen in API-23 and probably many levels before that. Just now, I have discovered that the implementation of this routine is changed significantly in API-25.
The problem is that a request for focus on an EditText placed inside a HorizontalScrollView may cause the HorizontalScrollView to scroll away from the field requesting the focus.
In my case it is an EditText with centered text, which is then placed in the center of 1048576 pixels and scrolled roughly half a million pixels to the right making the text centered and visible (this part is perfectly ok!) But then this offset of half a million pixels is propagated up the parent chain and makes the HorizontalScrollView move to its far right and far away from the input field.
I have tracked it down to the View.requestRectangleOnScreen routine, which in the API-23 sources is as follows:
public boolean requestRectangleOnScreen(Rect rectangle, boolean immediate) {
if (mParent == null) {
return false;
}
View child = this;
RectF position = (mAttachInfo != null) ? mAttachInfo.mTmpTransformRect : new RectF();
position.set(rectangle);
ViewParent parent = mParent;
boolean scrolled = false;
while (parent != null) {
rectangle.set((int) position.left, (int) position.top,
(int) position.right, (int) position.bottom);
scrolled |= parent.requestChildRectangleOnScreen(child,
rectangle, immediate);
if (!child.hasIdentityMatrix()) {
child.getMatrix().mapRect(position);
}
position.offset(child.mLeft, child.mTop);
if (!(parent instanceof View)) {
break;
}
View parentView = (View) parent;
position.offset(-parentView.getScrollX(), -parentView.getScrollY());
child = parentView;
parent = child.getParent();
}
return scrolled;
}
The idea is to make the rectangle visible by scrolling it onto the screen in every containing View, starting at the leaf level and passing the request up the chain of parents. The initial rectangle is given in child coordinates, which of course have to be adjusted as we work our way up the chain of parents. This is done with the statement
position.offset(-parentView.getScrollX(), -parentView.getScrollY());
close to the end of the code above.
What I have found, is that this is wrong because we are transforming the position given in child coordinates using the scroll X/Y values pertaining to the parent coordinates. Using the scroll X/Y of the child instead solved my problem but it was not possible to make a perfect override of this routine because it relies on private member variables. Specifically, I found no way of mimicing the mAttachInfo.
Now, digging a bit further, I found that the code for this routine in API-25 has changed significantly and (IMHO) correctly to the following:
public boolean requestRectangleOnScreen(Rect rectangle, boolean immediate) {
if (mParent == null) {
return false;
}
View child = this;
RectF position = (mAttachInfo != null) ? mAttachInfo.mTmpTransformRect : new RectF();
position.set(rectangle);
ViewParent parent = mParent;
boolean scrolled = false;
while (parent != null) {
rectangle.set((int) position.left, (int) position.top,
(int) position.right, (int) position.bottom);
scrolled |= parent.requestChildRectangleOnScreen(child, rectangle, immediate);
if (!(parent instanceof View)) {
break;
}
// move it from child's content coordinate space to parent's content coordinate space
position.offset(child.mLeft - child.getScrollX(), child.mTop -child.getScrollY());
child = (View) parent;
parent = child.getParent();
}
return scrolled;
}
The most important change being the line
position.offset(child.mLeft - child.getScrollX(), child.mTop -child.getScrollY());
where the scroll X/Y adjustment is now made with child values.
Now, I have two questions.
First, do you agree with my observations above?
Second, how do I implement an App that can be used on both API-23 and API-25 under the given circumstances?
My current thoughts are to sub class the EditText and override the requestRectangleOnScreen method such that when the API is 25 and above, it just calls the super class method and when the API is below 25, I basically do a full override using code along the lines of the code from API-25 but then missing out on the mAttachInfo part.
I Use Recyclerview Replace with list view
I want to keep Recyclerview always scroll bottom.
ListView can use this method setTranscriptMode(AbsListView.TRANSCRIPT_MODE_ALWAYS_SCROLL)
RecyclerView I use method smoothScrollToPosition(myAdapter.getItemCount() - 1)
but when Soft keyboard Pop ,its replace RecyclerView content.
If you want to keep the scroll position anchored to the bottom of the RecyclerView, it's useful in chat apps. just call setStackFromEnd(true) to on the LinearLayoutManager to make the keyboard keep the list items anchored on the bottom (the keyboard) and not the top.
This is because RV thinks its reference point is TOP and when keyboard comes up, RV's size is updated by the parent and RV keeps its reference point stable. (thus keeps the top position at the same location)
You can set LayoutManager#ReverseLayout to true in which case RV will layout items from the end of the adapter.
e.g. adapter position 0 is at the bottom, 1 is above it etc...
This will of course require you to reverse the order of your adapter.
I'm not sure but setting stack from end may also give you the same result w/o reordering your adapter.
recyclerView.scrollToPosition(getAdapter().getItemCount()-1);
I have faced the same problem and I solved it using the approach mentioned here. It is used to detect whether soft keyboard is open or not and if it is open, just call the smoothScrollToPosition() method.
A much simpler solution is to give your activity's root view a known ID, say '#+id/activityRoot', hook a GlobalLayoutListener into the ViewTreeObserver, and from there calculate the size diff between your activity's view root and the window size:
final View activityRootView = findViewById(R.id.activityRoot);
activityRootView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
int heightDiff = activityRootView.getRootView().getHeight() - activityRootView.getHeight();
if (heightDiff > 100) {
recyclerView.smoothScrollToPosition(myAdapter.getItemCount() - 1);
}
}
});
Easy!
I have also faced same problem. But following code help me. I hope this is useful.
In this staus is arraylist.
recyclerView.scrollToPosition(staus.size()-1);
next one is:-
In This you can use adapter class
recyclerView.scrollToPosition(showAdapter.getItemCount()-1);
I ran into this problem myself and I ended up creating my own LayoutManager to solve it. It's a pretty straightforward solution that can be broken down into three steps:
Set stackFromEnd to true.
Determine whether forceTranscriptScroll should be set to true whenever onItemsChanged is called. Per the documentation, onItemsChanged gets called whenever the contents of the adapter changes. If transcriptMode is set to Disabled, forceTranscriptScroll will always be false, if it's set to AlwaysScroll, it will always be true, and if it's set to Normal, it will only be true if the last item in the adapter is completely visible.
In onLayoutCompleted, scroll to the last item in the list if forceTranscriptScroll is set to true and the last item in the list isn't already completely visible.
Below is the code that accomplishes these three steps:
import android.content.Context
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
class TranscriptEnabledLinearLayoutManager(context: Context, transcriptMode: TranscriptMode = TranscriptMode.Normal) :
LinearLayoutManager(context) {
enum class TranscriptMode {
Disabled, Normal, AlwaysScroll
}
private var transcriptMode: TranscriptMode = TranscriptMode.Disabled
set(value) {
field = value
// Step 1
stackFromEnd = field != TranscriptMode.Disabled
}
private var forceTranscriptScroll = false
init {
this.transcriptMode = transcriptMode
}
// Step 2
override fun onItemsChanged(recyclerView: RecyclerView) {
super.onItemsChanged(recyclerView)
forceTranscriptScroll = when (transcriptMode) {
TranscriptMode.Disabled -> false
TranscriptMode.Normal -> {
findLastCompletelyVisibleItemPosition() == itemCount - 1
}
TranscriptMode.AlwaysScroll -> true
}
}
// Step 3
override fun onLayoutCompleted(state: RecyclerView.State?) {
super.onLayoutCompleted(state)
val recyclerViewState = state ?: return
if (!recyclerViewState.isPreLayout && forceTranscriptScroll) {
// gets the position of the last item in the list. returns if list is empty
val lastAdapterItemPosition = recyclerViewState.itemCount.takeIf { it > 0 }
?.minus(1) ?: return
val lastCompletelyVisibleItem = findLastCompletelyVisibleItemPosition()
if (lastCompletelyVisibleItem != lastAdapterItemPosition ||
recyclerViewState.targetScrollPosition != lastAdapterItemPosition) {
scrollToPositionWithOffset(lastAdapterItemPosition, 0)
}
forceTranscriptScroll = false
}
}
}
I was wondering, how to check whether the current ScrollView is scrollable? It seems that, there isn't public method called canScroll or isScrollable in ScrollView.
Scrollable : You can move the ViewGroup inside the ScrollView up and down, and the scroll bar will be visible when you move it up and down. So, if there is only little rows in the ScrollView, and they can fit inside single screen, ScrollView is not scrollable then.
You can do some little math to calculate the views raw height and the height of the content. If the difference of this heights is < 0 the view is scrollable.
To calculate the raw height you can use View.getMeasuredHeight().
Because ScrollView is a ViewGroup and has max one child, get the height of that child with ViewGroup.getChildAt(0).getHeight();
Use a ViewTreeObserver to get the heights, because it will be called at the moment the layout / view is changing the visibility, otherwise the heights could be 0.
ScrollView scrollView = (ScrollView)findViewById(R.id...);
ViewTreeObserver observer = scrollView.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
int viewHeight = scrollView.getMeasuredHeight();
int contentHeight = scrollView.getChildAt(0).getHeight();
if(viewHeight - contentHeight < 0) {
// scrollable
}
}
});
scrollView.viewTreeObserver
.addOnGlobalLayoutListener {
val isScrollable = scrollView.canScrollVertically(1)
}
I think I might be missing something, but shouldn't it be as simple as checking if
scrollView.getHeight() >= parentView.getMeasuredHeight()
you might actually need: scrollView.getChildAt(0).getHeight() and/or parentView.getHeight() instead, but the idea is the same.