I have just started using ItemDecoration. I am trying to achieve a divider in the middle of a two-column RecyclerView managed by StaggeredGridLayoutManager.
Here's my ItemDecoration code :
public class LobbyItemDecoration extends RecyclerView.ItemDecoration {
private int margin;
private Drawable drawable;
public LobbyItemDecoration(int margin, Drawable drawable){
this.margin = margin;
this.drawable = drawable;
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
int position = parent.getChildAdapterPosition(view);
StaggeredGridLayoutManager.LayoutParams lp = (StaggeredGridLayoutManager.LayoutParams)view .getLayoutParams();
int spanIndex = lp.getSpanIndex();
if(position >= 0){
if (position < ((LobiAdapter)parent.getAdapter()).getmDataset().size()){
if(spanIndex == 1){
outRect.left = margin;
((LobiAdapter)parent.getAdapter()).getmDataset().get(position).left = false;
} else {
outRect.right = margin;
((LobiAdapter)parent.getAdapter()).getmDataset().get(position).left = true;
}
outRect.bottom = margin;
}
}
}
#Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (drawable == null) { super.onDrawOver(c, parent, state);return; }
if(parent.getItemAnimator() != null && parent.getItemAnimator().isRunning()) {
return;
}
final int top = parent.getPaddingTop();
final int bottom = parent.getHeight() - parent.getPaddingBottom();
final int childCount = parent.getChildCount();
for (int i=1; i < childCount; i++) {
final View child = parent.getChildAt(i);
StaggeredGridLayoutManager.LayoutParams lp = (StaggeredGridLayoutManager.LayoutParams)child .getLayoutParams();
int spanIndex = lp.getSpanIndex();
int size = drawable.getIntrinsicWidth();
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
final int ty = (int)(child.getTranslationY() + 0.5f);
final int tx = (int)(child.getTranslationX() + 0.5f);
final int leftIndex1 = child.getLeft() - (margin * 4 / 3) - tx;
final int rightIndex1 = child.getLeft() - (margin * 2 / 3) - tx;
final int leftIndex0 = child.getRight() + (margin * 2 / 3) + tx;
final int rightIndex0 = child.getRight() + (margin * 4 / 3) + tx;
if(spanIndex == 1){
// drawable.setBounds(100, top, 5, bottom);
drawable.setBounds(leftIndex1, top, rightIndex1, bottom);
drawable.draw(c);
} else {
drawable.setBounds(leftIndex0, top, rightIndex0, bottom);
drawable.draw(c);
// drawable.setBounds(5, top, 100, bottom);
}
}
}
}
Problem is as title said, whenever I load new item or scroll, the decoration is sometimes gone or misplaced. By gone I mean, literally gone, it's not there anymore until I scroll down or up to recycle the View.
And by misplaced I mean, when I scroll down, and the loading ViewHolder is in the left column, the divider "sticks" to the left column. If the loading ViewHolder is in the right column, it is normal.
Just in case : I use a ViewHolder to be a progress indicator by adding one null item and check it in getItemViewType() then remove it later after finishing the load from server.
This is how I add :
for (PostModel postModel :
dataSet) {
this.mDataset.add(postModel);
notifyItemInserted(mDataset.size() - 1);
}
StaggeredGridLayoutManager is buggy. For me this helped:
diffResult.dispatchUpdatesTo(this);
recyclerView.postDelayed(() -> {
layoutManager.invalidateSpanAssignments(); // to fix first item in second column issue
recyclerView.invalidateItemDecorations(); // to fix wrong spacing
}, 50);
I want to create custom container, that can lay children one by one from bottom with offset. Currently I created such container but I have problems with animation, when view is added to container it should slide from bottom, when view is remove it should slide to bottom. With add animation all fine, but with remove I got some issues, views dont want to go in needed position?
This is onLayout() method:
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
int offset;
if (i > (itemCount - VISIBLE_ITEMS)) {
offset = (itemCount - i - 1) * this.offset;
}
// we adding invisible items
else {
offset = (VISIBLE_ITEMS - 1) * this.offset;
}
int bottom = getBottom() - offset;
int left = getLeft();
int right = getRight();
int top = bottom - child.getMeasuredHeight();
child.layout(left, top, right, bottom);
}
}
This is method for adding new view:
public void animateAdd(final View view){
addView(view);
final ViewTreeObserver observer = getViewTreeObserver();
observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
#Override
public boolean onPreDraw() {
observer.removeOnPreDrawListener(this);
AnimatorSet animator = new AnimatorSet();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
int startY = i == getChildCount() - 1 ? child.getHeight() : offset;
if (isNeedToAnimate(i)) {
Log.d(TAG, "onPreDraw: startY = " + startY);
animator.playTogether(ObjectAnimator.ofFloat(child, TRANSLATION_Y, startY, 0));
animator.playTogether(createColorAnimator(child, i));
}
}
animator.setDuration(300);
animator.start();
return true;
}
});
}
This is method for remove action:
public void animateRemove() {
if (getChildCount() == 0) {
return;
}
final View removeView = getChildAt(getChildCount() - 1);
removeViewInLayout(removeView);
final ViewTreeObserver observer = getViewTreeObserver();
observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
#Override
public boolean onPreDraw() {
observer.removeOnPreDrawListener(this);
final AnimatorSet animator = new AnimatorSet();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
int endY = i == getChildCount() - 1 ? child.getHeight() : offset;
if (isNeedToAnimateRemove(i)) {
Animator anim = ObjectAnimator.ofFloat(child, TRANSLATION_Y, endY);
animator.playTogether(anim);
animator.playTogether(createColorAnimator(child, i));
}
}
animator.setDuration(300);
animator.start();
return true;
}
});
}
I have created a GridView control, which inhertis from a ScrollView, the idea of this control, is that it will contain multiple Views arranged in a grid format with a given number of columns and rows.
When the view is first built, the GridView doesn't know the size of its container, so I wait until the onSizeChanged method is called, then I apply the relevant sizing.
When the below is run, it doesn't re-size the grid to show it correctly, each control is only as big as it needs to be to show the text.
When the `onSizeChanged' method is called, it has the correct size, and applies the correct size to each child view, but it doesn't affect the way the controls are drawn (i.e. they're still all bunched up on the top left of the screen).
Despite this, I have actually got it working, but it draws twice. I do this by creating a Runnable which calls ResizeList. Then calling new Handler().post(r) straight after I call BuildIt.
Although this is a solution, I just don't understand why it doesn't work in the below form.
Incidentally, if the GridView is the main View added to the Activity, it displays fine, it's only when it's subsequently added. Which is why I have the Button, which you have to press to show the grid.
Can anyone suggest why the below code doesn't work properly?
public class MainActivity extends Activity {
GridView sv;
FrameLayout flay;
#Override protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
flay=new FrameLayout(this);
this.setContentView(flay);
Button b=new Button(this);
b.setText("press me to show grid view");
b.setOnClickListener(ocl);
flay.addView(b);
}
OnClickListener ocl=new OnClickListener()
{
#Override public void onClick(View v)
{
BuildIt();
}};
private void BuildIt()
{
flay.removeAllViews(); // remove the button control
sv=new GridView(this);
for (int c1=0 ; c1<30 ; c1++)
{
TextView tv=new TextView(this);
tv.setText("Item "+c1);
tv.setGravity(android.view.Gravity.CENTER);
sv.addListItem(tv);
}
flay.addView(sv);
sv.ConstructList();
}
}
The GridView class
public class GridView extends ScrollView
{
final int rows=4;
final int cols=4;
private ArrayList<View> allViews=new ArrayList<View>();
private LinearLayout ll;
public GridView(Context context)
{
super(context);
ll=new LinearLayout(context);
ll.setOrientation(LinearLayout.VERTICAL);
this.addView(ll);
}
public void addListItem(View v)
{
allViews.add(v);
}
public void ConstructList()
{
int c1=0;
ll.removeAllViews(); // Just in case we're re-building
LinearLayout row=null;
for (View v : allViews)
{
if (c1%cols==0)
{
row=new LinearLayout(this.getContext());
ll.addView(row);
}
row.addView(v);
c1++;
}
}
private void ResizeList()
{
int useHeight=getHeight()/rows;
int useWidth=getWidth()/cols;
LinearLayout.LayoutParams lpCol=new LinearLayout.LayoutParams(useWidth, useHeight);
Log.i("log","About to set width/height="+useWidth+"/"+useHeight);
int numKids= ll.getChildCount();
for (int c1=0 ; c1<numKids ; c1++)
{
LinearLayout ll2=(LinearLayout)ll.getChildAt(c1);
for (int c2=0 ; c2<ll2.getChildCount() ; c2++) // use getChildCount rather than cols, just in case it's the last one
{
View v=ll2.getChildAt(c2);
v.setLayoutParams(lpCol);
}
}
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh)
{
super.onSizeChanged(w, h, oldw, oldh);
ResizeList();
}
}
I have a function which is used to resize the child's width and height in gridView.
May be this could help you :
public static void setGridChild_Height(GridView gridView, int columns) {
ListAdapter listAdapter = gridView.getAdapter();
if (listAdapter == null) {
// pre-condition
return;
}
int totalHeight = 0;
int items = listAdapter.getCount();
int rows = 0;
for (int j = 0; j < items; j++) {
View view = gridView.getChildAt(j);
if (view != null && view.getHeight() > totalHeight) {
totalHeight = view.getHeight();
}
}
System.out.println("totalHeight -> " + totalHeight);
if (totalHeight > 0) {
for (int j = 0; j < items; j++) {
View view = gridView.getChildAt(j);
if (view != null && view.getHeight() < totalHeight) {
view.setMinimumHeight(totalHeight);
}
}
}
// View listItem = listAdapter.getView(0, null, gridView);
// listItem.measure(0, 0);
// totalHeight = listItem.getMeasuredHeight();
//
// float x = 1;
// if (items > columns) {
// x = items / columns;
// rows = (int) (x + 1);
// totalHeight *= rows;
// }
//
// ViewGroup.LayoutParams params = gridView.getLayoutParams();
// params.height = totalHeight;
// gridView.setLayoutParams(params);
}
Try to change logic as per your requirement.
Code is not tested perfectly.
It's because onSizeChanged when newly added to the view hierarchy uses it's old sizes of "0" (according to the docs: http://developer.android.com/reference/android/view/View.html#onSizeChanged(int, int, int, int))
I think what you want is to a addOnLayoutChangedListener : http://developer.android.com/reference/android/view/View.html#addOnLayoutChangeListener(android.view.View.OnLayoutChangeListener)
Using the ViewTreeObserver might be another option that will work for you: How can you tell when a layout has been drawn?
Hi I'm using the TouchListView control from here: https://github.com/commonsguy/cwac-touchlist
and I've added some buttons to add to the list in the footer:
mFooter = getLayoutInflater().inflate(R.layout.edit_homepage_footer_layout, null);
mListView = (TouchListView) findViewById(R.id.sectionList);
mListView.addFooterView(mFooter);
It all seems to be working fine until I drag an item in the list, at which point the footer collapses (to the height of one list item I think) obscuring the buttons I have added.
Can anyone suggest a fix/workaround for this?
I actually worked this out shortly after asking it (always the way...)
The issue is in the doExpansion() and unExpandViews() methods which were modifying every item in the list including the footer. To fix it I created a method to check whether we are dealing with a draggable item or the footer:
private boolean isDraggableItem(View view) {
View dragger = view.findViewById(grabberId);
return dragger != null;
}
And then modified the methods mentioned as follows:
private void unExpandViews(boolean deletion) {
for (int i = 0; ; i++) {
View v = getChildAt(i);
if (v == null) {
if (deletion) {
// HACK force update of mItemCount
int position = getFirstVisiblePosition();
int y = getChildAt(0).getTop();
setAdapter(getAdapter());
setSelectionFromTop(position, y);
// end hack
}
layoutChildren(); // force children to be recreated where needed
v = getChildAt(i);
if (v == null) {
break;
}
}
if (isDraggableItem(v)) { //check this view isn't the footer
ViewGroup.LayoutParams params = v.getLayoutParams();
params.height = mItemHeightNormal;
v.setLayoutParams(params);
v.setVisibility(View.VISIBLE);
}
}
}
private void doExpansion() {
Log.d(logTag, "Doing expansion");
int childnum = mDragPos - getFirstVisiblePosition();
if (mDragPos > mFirstDragPos) {
childnum++;
}
View first = getChildAt(mFirstDragPos - getFirstVisiblePosition());
for (int i = 0; ; i++) {
View vv = getChildAt(i);
if (vv == null) {
break;
}
int height = mItemHeightNormal;
int visibility = View.VISIBLE;
if (vv.equals(first)) {
// processing the item that is being dragged
if (mDragPos == mFirstDragPos) {
// hovering over the original location
visibility = View.INVISIBLE;
} else {
// not hovering over it
height = 1;
}
} else if (i == childnum) {
if (mDragPos < getCount() - 1) {
height = mItemHeightExpanded;
}
}
if (isDraggableItem(vv)) { //check this view isn't the footer
ViewGroup.LayoutParams params = vv.getLayoutParams();
params.height = height;
vv.setLayoutParams(params);
vv.setVisibility(visibility);
}
}
}
Would be worth updating the github project to include this I think.
I have a ScrollView which holds a series of Views. I would like to be able to determine if a view is currently visible (if any part of it is currently displayed by the ScrollView). I would expect the below code to do this, surprisingly it does not:
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
}
This works:
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
}
Use View#getHitRect instead of View#getDrawingRect on the view you're testing. You can use View#getDrawingRect on the ScrollView instead of calculating explicitly.
Code from View#getDrawingRect:
public void getDrawingRect(Rect outRect) {
outRect.left = mScrollX;
outRect.top = mScrollY;
outRect.right = mScrollX + (mRight - mLeft);
outRect.bottom = mScrollY + (mBottom - mTop);
}
Code from View#getHitRect:
public void getHitRect(Rect outRect) {
outRect.set(mLeft, mTop, mRight, mBottom);
}
If you want to detect that the view is FULLY 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;
}
}
This extension help detect view fully visible.
It also work if your View is a child of child of ... of ScrollView (eg: 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() and view.getX() return the x,y value to FIRST PARENT.
2) Here is example about how getDrawingRect will return
Link
My Solution is use NestedScrollView Scroll element:
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");
}
}
}
});
}
To expand a bit on Bill Mote's answer using getLocalVisibleRect, you may want to check if the view is only partially 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
}
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;
}
}
I faced the same problem today. While Googling and reading Android reference I found this post and a method I ended up using instead;
public final boolean getLocalVisibleRect (Rect r)
Nice of them not to only providing Rect but also boolean indicating if View visible at all. On negative side this method is undocumented :(
I you want to detect if your View is fully visible, try with this method:
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.
}
}
Strictly speaking you can get the visibility of a view with:
if (myView.getVisibility() == View.VISIBLE) {
//VISIBLE
} else {
//INVISIBLE
}
The posible constant values of the visibility in a View are:
VISIBLE
This view is visible. Use with setVisibility(int) and android:visibility.
INVISIBLE
This view is invisible, but it still takes up space for layout purposes. Use with setVisibility(int) and android:visibility.
GONE
This view is invisible, and it doesn't take any space for layout purposes. Use with setVisibility(int) and android:visibility.
Kotlin way;
An extension for listing scroll view's scroll and get an action if child view visible on screen.
#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
}
}
Use this extension function for any scrollable view
nestedScrollView.setChildViewOnScreenListener(childView) {
action()
}
You can use the FocusAwareScrollView which notifies when view becomes 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
}
}
});
}
Here is class :
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);
}
}
I ended up implementing a combination of two of the Java answers ( #bill-mote https://stackoverflow.com/a/12428154/3686125 and #denys-vasylenko https://stackoverflow.com/a/25528434/3686125 ) in my project as a set of Kotlin extensions, which support either standard vertial ScrollView or HorizontalScrollView controls.
I just tossed these in a Kotlin file named Extensions.kt, no class, just methods.
I used these to determine which item to snap to when a user stops scrolling in various scrollviews in my project:
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)
Example usage, iterating through scrollview's LinearLayout children and logging outputs:
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)}")
}
}
My way:
scrollView.viewTreeObserver?.addOnScrollChangedListener {
scrollView.getDrawingRect(Rect())
myViewInsideScrollView.getLocalVisibleRect(Rect())
}
I know its very late. But i have a good solution. Below is the code snippet for getting view visibility percentage in scroll view.
First of all set touch listener on scroll view for getting callback for scroll stop.
#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;
}
In above code snippet, we are getting call backs for scroll view touch events and post a runnable after 150 millis(Not mandatory) after getting the callback for scroll stopped. In that runnable we will get location of scroll view on the screen and scroll view height. Then get the direct child viewgroup instance of scroll view and get the child counts. In my case direct child of scroll view is LinearLayout named scrollViewRootChild. Then iterate all the child views of scrollViewRootChild. In above code snippet you can see I am getting the location of the child on the screen in a integer array named viewLocation, get height of view in variable name viewHeight. Then i called a private method getViewVisibilityOnScrollStopped. You can get the understanding of the internal working of this method by reading 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.
}
}
If you feel any improvement in this code please contribute.
Using #Qberticus answer which was to the point but great btw, I compined a bunch of codes to check if whenever a scrollview is called and got scrolled it trigger the #Qberticus answer and you can do whatever you want, in my case I have a social network containing videos so when the view is drawed on the screen I play the video same idea like facebook and Instagram.
Here's the code:
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
{
}
}
}
}
});