我有一个包含一系列 Views
的 ScrollView
。我希望能够确定一个视图当前是否可见(如果它的任何部分当前由 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
}
这有效:
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
}
在您正在测试的视图上使用 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);
}
如果您想检测视图是否完全可见:
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
是 ScrollView
的 ... 的子级(例如:ScrollView
-> {4 } -> 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 值返回给 FIRST PARENT。
https://i.stack.imgur.com/tGa5g.png
我的解决方案是使用 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");
}
}
}
});
}
要使用 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
}
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 是否可见。不利的一面是,这种方法是无证的:(
如果您想检测您的视图是否完全可见,请尝试使用以下方法:
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
}
视图中可见性的可能常量值为:
VISIBLE 此视图可见。与 setVisibility(int) 和 android:visibility 一起使用。
INVISIBLE 此视图是不可见的,但仍会占用空间用于布局。与 setVisibility(int) 和 android:visibility 一起使用。
GONE 此视图是不可见的,并且不占用任何空间用于布局。与 setVisibility(int) 和 android:visibility 一起使用。
科特林方式;
如果子视图在屏幕上可见,则用于列出滚动视图的滚动并获取操作的扩展。
@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()
}
您可以使用 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);
}
}
我最终在我的项目中实现了两个 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)
示例用法,遍历滚动视图的 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)}")
}
}
var
并抑制 ide 提示?
我知道已经很晚了。但我有一个很好的解决方案。下面是在滚动视图中获取视图可见性百分比的代码片段。
首先在滚动视图上设置触摸侦听器以获取滚动停止的回调。
@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 毫秒(非强制性)后发布了一个可运行对象。在该可运行文件中,我们将获得滚动视图在屏幕上的位置和滚动视图高度。然后获取滚动视图的直接子视图组实例并获取子计数。在我的情况下,滚动视图的直接子级是名为 scrollViewRootChild 的 LinearLayout。然后迭代scrollViewRootChild的所有子view。在上面的代码片段中,您可以看到我在一个名为 viewLocation 的整数数组中获取屏幕上孩子的位置,在变量名称 viewHeight 中获取视图的高度。然后我调用了一个私有方法getViewVisibilityOnScrollStopped。您可以通过阅读文档来了解此方法的内部工作原理。
/**
* 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.
}
}
如果您觉得此代码有任何改进,请贡献。
我的方式:
scrollView.viewTreeObserver?.addOnScrollChangedListener {
scrollView.getDrawingRect(Rect())
myViewInsideScrollView.getLocalVisibleRect(Rect())
}
使用@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
{
}
}
}
}
});
GridView
/ListView
/GridViewWithHeader
与SwipeRefreshLayout
一起工作。getHitRect
在父坐标中返回一个矩形,但getLocalVisibleRect
在滚动视图的本地坐标中返回一个矩形,不是吗?