J'ai un ScrollView
qui contient une série de Views
. J'aimerais pouvoir déterminer si une vue est actuellement visible (si une partie de celle-ci est actuellement affichée par la variable ScrollView
). Je m'attendrais à ce que le code ci-dessous le fasse, ce qui est surprenant:
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
}
Utilisez View#getHitRect
au lieu de View#getDrawingRect
dans la vue que vous testez. Vous pouvez utiliser View#getDrawingRect
sur ScrollView
au lieu de calculer explicitement.
Code de View#getDrawingRect
:
public void getDrawingRect(Rect outRect) {
outRect.left = mScrollX;
outRect.top = mScrollY;
outRect.right = mScrollX + (mRight - mLeft);
outRect.bottom = mScrollY + (mBottom - mTop);
}
Code de View#getHitRect
:
public void getHitRect(Rect outRect) {
outRect.set(mLeft, mTop, mRight, mBottom);
}
Cela marche:
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
}
Si vous voulez détecter que la vue estENTIÈREMENTvisible:
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;
}
}
Pour développer un peu la réponse de Bill Mote à l'aide de getLocalVisibleRect, vous pouvez vérifier si la vue n'est que partiellement visible:
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
}
View
est entièrement visible
, essayez avec cette méthode: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
}
Les valeurs constantes possibles de la visibilité dans une vue sont les suivantes:
VISIBLE Cette vue est visible. Utiliser avec setVisibility (int) et Android: visibilité.
INVISIBLE Cette vue est invisible, mais elle prend toujours de la place pour la mise en page. Utiliser avec setVisibility (int) et Android: visibilité.
GONE Cette vue est invisible et ne nécessite aucun espace pour la mise en page. Utiliser avec setVisibility (int) et Android: visibilité.
J'ai rencontré le même problème aujourd'hui. Tout en cherchant sur Google et en lisant la référence Android, j'ai trouvé ce post et une méthode que j'ai finalement utilisée;
public final boolean getLocalVisibleRect (Rect r)
Sympa de ne pas fournir uniquement Rect mais aussi booléen indiquant si View est visible. Du côté négatif cette méthode est non documentée :(
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;
}
}
Ma solution est d'utiliser NestedScrollView
Elément de défilement:
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");
}
}
}
});
}
Vous pouvez utiliser la FocusAwareScrollView
qui informe quand la vue devient visible:
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
}
}
});
}
Voici la classe:
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);
}
}
Cette extension aide à détecter la vue Fully visible.
Cela fonctionne également si votre View
est un enfant d'un enfant de ... de ScrollView
(ex: 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
}
Note
1) view.getY()
et view.getX()
renvoient la valeur x, y à FIRST PARENT .
2) Voici un exemple sur le retour de getDrawingRect
Lien
J'ai fini par implémenter une combinaison de deux des Java réponses (@ bill-mote https://stackoverflow.com/a/12428154/3686125 et @ denys-vasylenko - https://stackoverflow.com/a/25528434/3686125 ) dans mon projet en tant qu'ensemble d'extensions Kotlin, qui prennent en charge les contrôles standard ScrollView verticaux ou HorizontalScrollView.
Je viens de les jeter dans un fichier Kotlin nommé Extensions.kt, pas de classe, juste des méthodes.
J'ai utilisé ces éléments pour déterminer l'élément sur lequel s'aligner lorsqu'un utilisateur cesse de faire défiler les différentes vues de défilement de mon projet:
fun View.isPartiallyOrFullyVisible(horizontalScrollView: HorizontalScrollView) : Boolean {
@Suppress("CanBeVal") var scrollBounds = Rect()
horizontalScrollView.getHitRect(scrollBounds)
return getLocalVisibleRect(scrollBounds)
}
fun View.isPartiallyOrFullyVisible(scrollView: ScrollView) : Boolean {
@Suppress("CanBeVal") var scrollBounds = Rect()
scrollView.getHitRect(scrollBounds)
return getLocalVisibleRect(scrollBounds)
}
fun View.isFullyVisible(horizontalScrollView: HorizontalScrollView) : Boolean {
@Suppress("CanBeVal") var 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 {
@Suppress("CanBeVal") var 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)
Exemple d'utilisation, en parcourant les enfants LinearLayout de scrollview et en enregistrant les sorties:
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)}")
}
}
Je sais que c'est très tard. Mais j'ai une bonne solution. Vous trouverez ci-dessous un extrait de code permettant d’obtenir le pourcentage de visibilité d’une vue dans une vue défilement.
Tout d’abord, configurez l’écoute tactile en mode défilement pour obtenir le rappel pour arrêter le défilement.
@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;
}
Dans l'extrait de code ci-dessus, nous recevons des rappels pour les événements tactiles de la vue de défilement et publions un message exécutable après 150 millisecondes (non obligatoire) après l'arrêt du rappel pour le défilement. Dans ce runnable, nous allons obtenir l'emplacement de la vue de défilement à l'écran et la hauteur de la vue de défilement. Ensuite, obtenez l'instance directe du groupe de vue enfant de la vue de défilement et obtenez le nombre d'enfants. Dans mon cas, l'enfant direct de la vue de défilement est LinearLayout nommé scrollViewRootChild . Ensuite, parcourez toutes les vues enfants de scrollViewRootChild . Dans l'extrait de code ci-dessus, vous voyez que je reçois l'emplacement de l'enfant à l'écran dans un tableau entier nommé viewLocation , la hauteur d'affichage dans le nom de variable viewHeight . Ensuite, j'ai appelé une méthode privée getViewVisibilityOnScrollStopped . Vous pouvez comprendre le fonctionnement interne de cette méthode en lisant la documentation.
/**
* 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.
}
}
Si vous sentez une amélioration de ce code, contribuez s'il vous plaît.