Android: 如何检查 ScrollView 中的 View 是否可见?

我有一个 ScrollView,其中持有一个系列的 Views。我希望能够确定一个视图是否当前可见(如果它的任何部分当前由 ScrollView显示)。我希望下面的代码可以做到这一点,但令人惊讶的是它没有:

Rect bounds = new Rect();
view.getDrawingRect(bounds);


Rect scrollBounds = new Rect(scroll.getScrollX(), scroll.getScrollY(),
scroll.getScrollX() + scroll.getWidth(), scroll.getScrollY() + scroll.getHeight());


if(Rect.intersects(scrollBounds, bounds))
{
//is  visible
}
111009 次浏览

在测试的视图中使用 View#getHitRect而不是 View#getDrawingRect。可以在 ScrollView上使用 View#getDrawingRect而不是显式计算。

来自 View#getDrawingRect的代码:

 public void getDrawingRect(Rect outRect) {
outRect.left = mScrollX;
outRect.top = mScrollY;
outRect.right = mScrollX + (mRight - mLeft);
outRect.bottom = mScrollY + (mBottom - mTop);
}

来自 View#getHitRect的代码:

public void getHitRect(Rect outRect) {
outRect.set(mLeft, mTop, mRight, mBottom);
}
public static int getVisiblePercent(View v) {
if (v.isShown()) {
Rect r = new Rect();
v.getGlobalVisibleRect(r);
double sVisible = r.width() * r.height();
double sTotal = v.getWidth() * v.getHeight();
return (int) (100 * sVisible / sTotal);
} else {
return -1;
}
}

我今天也遇到了同样的问题。在搜索和阅读 Android 参考资料时,我发现了这篇文章,并最终使用了一种方法;

public final boolean getLocalVisibleRect (Rect r)

它们不仅提供了 Rect,而且还提供了布尔值来指示 View 是否可见。从负面来看,这种方法是没有文件记载的:

这种方法是有效的:

Rect scrollBounds = new Rect();
scrollView.getHitRect(scrollBounds);
if (imageView.getLocalVisibleRect(scrollBounds)) {
// Any portion of the imageView, even a single pixel, is within the visible window
} else {
// NONE of the imageView is within the visible window
}

为了使用 getLocalVisibleRect 扩展 Bill Mote 的回答,您可能想检查视图是否只是部分可见:

Rect scrollBounds = new Rect();
scrollView.getHitRect(scrollBounds);
if (!imageView.getLocalVisibleRect(scrollBounds)
|| scrollBounds.height() < imageView.getHeight()) {
// imageView is not within or only partially within the visible window
} else {
// imageView is completely visible
}

如果要检测视图是否为 完全正确可见:

private boolean isViewVisible(View view) {
Rect scrollBounds = new Rect();
mScrollView.getDrawingRect(scrollBounds);


float top = view.getY();
float bottom = top + view.getHeight();


if (scrollBounds.top < top && scrollBounds.bottom > bottom) {
return true;
} else {
return false;
}
}

如果你想检测你的 View是否完全 visible,可以试试这个方法:

private boolean isViewVisible(View view) {
Rect scrollBounds = new Rect();
mScrollView.getDrawingRect(scrollBounds);
float top = view.getY();
float bottom = top + view.getHeight();
if (scrollBounds.top < top && scrollBounds.bottom > bottom) {
return true; //View is visible.
} else {
return false; //View is NOT visible.
}
}

严格地说,你可以通过以下方式获得视野的可见性:

if (myView.getVisibility() == View.VISIBLE) {
//VISIBLE
} else {
//INVISIBLE
}

视图中可见性的可能常数值是:

看得见 这个视图是可见的。可以与 setVisiability (int)和 android: visible 一起使用。

隐形 这个视图是不可见的,但是它仍然占用了布局的空间。

消失了 这个视图是不可见的,并且它不会占用任何布局空间。

我的解决方案是使用 NestedScrollView滚动元素:

    final Rect scrollBounds = new Rect();
scroller.getHitRect(scrollBounds);


scroller.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {
@Override
public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {


if (myBtn1 != null) {


if (myBtn1.getLocalVisibleRect(scrollBounds)) {
if (!myBtn1.getLocalVisibleRect(scrollBounds)
|| scrollBounds.height() < myBtn1.getHeight()) {
Log.i(TAG, "BTN APPEAR PARCIALY");
} else {
Log.i(TAG, "BTN APPEAR FULLY!!!");
}
} else {
Log.i(TAG, "No");
}
}


}
});
}

使用@Qberticus 的回答,这是一个很重要的问题,但顺便说一句,我编写了一堆代码来检查每当滚动视图被调用并被滚动时,是否会触发@Qberticus 的回答,你可以做任何你想做的事情,在我的例子中,我有一个包含视频的社交网络,所以当视图被绘制到屏幕上时,我播放视频的想法与 Facebook 和 Instagram 一样。 密码是这样的:

mainscrollview.getViewTreeObserver().addOnScrollChangedListener(new OnScrollChangedListener() {


@Override
public void onScrollChanged() {
//mainscrollview is my scrollview that have inside it a linearlayout containing many child views.
Rect bounds = new Rect();
for(int xx=1;xx<=postslayoutindex;xx++)
{


//postslayoutindex is the index of how many posts are read.
//postslayoutchild is the main layout for the posts.
if(postslayoutchild[xx]!=null){


postslayoutchild[xx].getHitRect(bounds);


Rect scrollBounds = new Rect();
mainscrollview.getDrawingRect(scrollBounds);


if(Rect.intersects(scrollBounds, bounds))
{
vidPreview[xx].startPlaywithoutstoppping();
//I made my own custom video player using textureview and initialized it globally in the class as an array so I can access it from anywhere.
}
else
{


}




}
}
}
});

你可以使用 FocusAwareScrollView通知视图何时可见:

FocusAwareScrollView focusAwareScrollView = (FocusAwareScrollView) findViewById(R.id.focusAwareScrollView);
if (focusAwareScrollView != null) {


ArrayList<View> viewList = new ArrayList<>();
viewList.add(yourView1);
viewList.add(yourView2);


focusAwareScrollView.registerViewSeenCallBack(viewList, new FocusAwareScrollView.OnViewSeenListener() {


@Override
public void onViewSeen(View v, int percentageScrolled) {


if (v == yourView1) {


// user have seen view1


} else if (v == yourView2) {


// user have seen view2
}
}
});


}

下面是课程安排:

import android.content.Context;
import android.graphics.Rect;
import android.support.v4.widget.NestedScrollView;
import android.util.AttributeSet;
import android.view.View;


import java.util.ArrayList;
import java.util.List;


public class FocusAwareScrollView extends NestedScrollView {


private List<OnScrollViewListener> onScrollViewListeners = new ArrayList<>();


public FocusAwareScrollView(Context context) {
super(context);
}


public FocusAwareScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}


public FocusAwareScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}


public interface OnScrollViewListener {
void onScrollChanged(FocusAwareScrollView v, int l, int t, int oldl, int oldt);
}


public interface OnViewSeenListener {
void onViewSeen(View v, int percentageScrolled);
}


public void addOnScrollListener(OnScrollViewListener l) {
onScrollViewListeners.add(l);
}


public void removeOnScrollListener(OnScrollViewListener l) {
onScrollViewListeners.remove(l);
}


protected void onScrollChanged(int l, int t, int oldl, int oldt) {
for (int i = onScrollViewListeners.size() - 1; i >= 0; i--) {
onScrollViewListeners.get(i).onScrollChanged(this, l, t, oldl, oldt);
}
super.onScrollChanged(l, t, oldl, oldt);
}


@Override
public void requestChildFocus(View child, View focused) {
super.requestChildFocus(child, focused);
}


private boolean handleViewSeenEvent(View view, int scrollBoundsBottom, int scrollYOffset,
float minSeenPercentage, OnViewSeenListener onViewSeenListener) {
int loc[] = new int[2];
view.getLocationOnScreen(loc);
int viewBottomPos = loc[1] - scrollYOffset + (int) (minSeenPercentage / 100 * view.getMeasuredHeight());
if (viewBottomPos <= scrollBoundsBottom) {
int scrollViewHeight = this.getChildAt(0).getHeight();
int viewPosition = this.getScrollY() + view.getScrollY() + view.getHeight();
int percentageSeen = (int) ((double) viewPosition / scrollViewHeight * 100);
onViewSeenListener.onViewSeen(view, percentageSeen);
return true;
}
return false;
}


public void registerViewSeenCallBack(final ArrayList<View> views, final OnViewSeenListener onViewSeenListener) {


final boolean[] viewSeen = new boolean[views.size()];


FocusAwareScrollView.this.postDelayed(new Runnable() {
@Override
public void run() {


final Rect scrollBounds = new Rect();
FocusAwareScrollView.this.getHitRect(scrollBounds);
final int loc[] = new int[2];
FocusAwareScrollView.this.getLocationOnScreen(loc);


FocusAwareScrollView.this.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {


boolean allViewsSeen = true;


@Override
public void onScrollChange(NestedScrollView v, int x, int y, int oldx, int oldy) {


for (int index = 0; index < views.size(); index++) {


//Change this to adjust criteria
float viewSeenPercent = 1;


if (!viewSeen[index])
viewSeen[index] = handleViewSeenEvent(views.get(index), scrollBounds.bottom, loc[1], viewSeenPercent, onViewSeenListener);


if (!viewSeen[index])
allViewsSeen = false;
}


//Remove this if you want continuous callbacks
if (allViewsSeen)
FocusAwareScrollView.this.setOnScrollChangeListener((NestedScrollView.OnScrollChangeListener) null);
}
});
}
}, 500);
}
}

我知道现在很晚了。但我有个好办法。下面是在滚动视图中获取视图可见性百分比的代码片段。

首先在滚动视图上设置触摸侦听器以获得滚动停止的回调。

@Override
public boolean onTouch(View v, MotionEvent event) {
switch ( event.getAction( ) ) {
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
if(mScrollView == null){
mScrollView = (ScrollView) findViewById(R.id.mScrollView);
}
int childCount = scrollViewRootChild.getChildCount();


//Scroll view location on screen
int[] scrollViewLocation = {0,0};
mScrollView.getLocationOnScreen(scrollViewLocation);


//Scroll view height
int scrollViewHeight = mScrollView.getHeight();
for (int i = 0; i < childCount; i++){
View child = scrollViewRootChild.getChildAt(i);
if(child != null && child.getVisibility() == View.VISIBLE){
int[] viewLocation = new int[2];
child.getLocationOnScreen(viewLocation);
int viewHeight = child.getHeight();
getViewVisibilityOnScrollStopped(scrollViewLocation, scrollViewHeight,
viewLocation, viewHeight, (String) child.getTag(), (childCount - (i+1)));
}
}
}
}, 150);
break;
}
return false;
}

在上面的代码片段中,我们收到了滚动视图触摸事件的回调,并且在150毫秒(非强制)后发布了一个可运行的滚动视图触摸事件的回调。在这个可运行文件中,我们可以得到滚动视图在屏幕上的位置和滚动视图的高度。然后得到滚动视图的直接子视图组实例并得到子计数。在我的例子中,滚动视图的直接子级是 LinearLayout,名为 视图 RootChild。然后迭代 视图 RootChild的所有子视图。在上面的代码片段中,你可以看到我正在获取屏幕上名为 ViewLocation的整数数组中子元素的位置,以变量名 高度获取视图高度。然后我调用了一个私有方法 停止。通过阅读文档,您可以了解这种方法的内部工作原理。

/**
* getViewVisibilityOnScrollStopped
* @param scrollViewLocation location of scroll view on screen
* @param scrollViewHeight height of scroll view
* @param viewLocation location of view on screen, you can use the method of view claas's getLocationOnScreen method.
* @param viewHeight height of view
* @param tag tag on view
* @param childPending number of views pending for iteration.
*/
void getViewVisibilityOnScrollStopped(int[] scrollViewLocation, int scrollViewHeight, int[] viewLocation, int viewHeight, String tag, int childPending) {
float visiblePercent = 0f;
int viewBottom = viewHeight + viewLocation[1]; //Get the bottom of view.
if(viewLocation[1] >= scrollViewLocation[1]) {  //if view's top is inside the scroll view.
visiblePercent = 100;
int scrollBottom = scrollViewHeight + scrollViewLocation[1];    //Get the bottom of scroll view
if (viewBottom >= scrollBottom) {   //If view's bottom is outside from scroll view
int visiblePart = scrollBottom - viewLocation[1];  //Find the visible part of view by subtracting view's top from scrollview's bottom
visiblePercent = (float) visiblePart / viewHeight * 100;
}
}else{      //if view's top is outside the scroll view.
if(viewBottom > scrollViewLocation[1]){ //if view's bottom is outside the scroll view
int visiblePart = viewBottom - scrollViewLocation[1]; //Find the visible part of view by subtracting scroll view's top from view's bottom
visiblePercent = (float) visiblePart / viewHeight * 100;
}
}
if(visiblePercent > 0f){
visibleWidgets.add(tag);        //List of visible view.
}
if(childPending == 0){
//Do after iterating all children.
}
}

如果您觉得这个代码有任何改进,请贡献。

此扩展有助于检测可见的视图 完全
如果你的 ViewScrollView的孩子(例如: ScrollView-> LinearLayout-> ContraintLayout-> ...-> YourView) ,它也可以工作。

fun ScrollView.isViewVisible(view: View): Boolean {
val scrollBounds = Rect()
this.getDrawingRect(scrollBounds)
var top = 0f
var temp = view
while (temp !is ScrollView){
top += (temp).y
temp = temp.parent as View
}
val bottom = top + view.height
return scrollBounds.top < top && scrollBounds.bottom > bottom
}

注意

1) view.getY()view.getX()将 x,y 值返回给 第一任家长

2)下面是 getDrawingRect如何返回的例子 enter image description here 林克

我最终在我的项目中实现了两个 Java 应答(@bill-mote https://stackoverflow.com/a/12428154/3686125和@denys-vasylenko https://stackoverflow.com/a/25528434/3686125)的组合,作为一组 Kotlin 扩展,它们支持标准的垂直 scrollView 或 HorizontalscrollView 控件。

我只是把它们放在一个名为 Extensions.kt 的 Kotlin 文件中,没有类,只有方法。

我使用它们来确定当用户停止在我的项目中的各种滚动视图中滚动时,应该对哪个条目进行管理:

fun View.isPartiallyOrFullyVisible(horizontalScrollView: HorizontalScrollView) : Boolean {
val scrollBounds = Rect()
horizontalScrollView.getHitRect(scrollBounds)
return getLocalVisibleRect(scrollBounds)
}


fun View.isPartiallyOrFullyVisible(scrollView: ScrollView) : Boolean {
val scrollBounds = Rect()
scrollView.getHitRect(scrollBounds)
return getLocalVisibleRect(scrollBounds)
}


fun View.isFullyVisible(horizontalScrollView: HorizontalScrollView) : Boolean {
val scrollBounds = Rect()
horizontalScrollView.getDrawingRect(scrollBounds)
val left = x
val right = left + width
return scrollBounds.left < left && scrollBounds.right > right
}


fun View.isFullyVisible(scrollView: ScrollView) : Boolean {
val scrollBounds = Rect()
scrollView.getDrawingRect(scrollBounds)
val top = y
val bottom = top + height
return scrollBounds.top < top && scrollBounds.bottom > bottom
}


fun View.isPartiallyVisible(horizontalScrollView: HorizontalScrollView) : Boolean = isPartiallyOrFullyVisible(horizontalScrollView) && !isFullyVisible(horizontalScrollView)
fun View.isPartiallyVisible(scrollView: ScrollView) : Boolean = isPartiallyOrFullyVisible(scrollView) && !isFullyVisible(scrollView)

示例使用,遍历 scrollview 的 LinearLayout 子节点和日志输出:

val linearLayoutChild: LinearLayout = getChildAt(0) as LinearLayout
val scrollView = findViewById(R.id.scroll_view) //Replace with your scrollview control or synthetic accessor
for (i in 0 until linearLayoutChild.childCount) {
with (linearLayoutChild.getChildAt(i)) {
Log.d("ScrollView", "child$i left=$left width=$width isPartiallyOrFullyVisible=${isPartiallyOrFullyVisible(scrollView)} isFullyVisible=${isFullyVisible(scrollView)} isPartiallyVisible=${isPartiallyVisible(scrollView)}")
}
}

科特林路;

一个扩展列出滚动视图的滚动,并获得一个操作,如果子视图在屏幕上可见。

@SuppressLint("ClickableViewAccessibility")
fun View.setChildViewOnScreenListener(view: View, action: () -> Unit) {
val visibleScreen = Rect()


this.setOnTouchListener { _, motionEvent ->
if (motionEvent.action == MotionEvent.ACTION_MOVE) {
this.getDrawingRect(visibleScreen)


if (view.getLocalVisibleRect(visibleScreen)) {
action()
}
}


false
}
}

对任何可滚动视图使用此扩展函数

nestedScrollView.setChildViewOnScreenListener(childView) {
action()
}

我的方式:

scrollView.viewTreeObserver?.addOnScrollChangedListener {
scrollView.getDrawingRect(Rect())
myViewInsideScrollView.getLocalVisibleRect(Rect())
}