Sticky ScrollView Item at the Bottom - Android

10,124

Solution 1

I have the same requirement to implement this kind of view and i have figured out solution as per my code and logic. So you can apply code which i am sharing with you!

Below is the my project image how it is working using my code.

enter image description here

Layout XML

  <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/white"
        android:orientation="vertical">
     <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="56dp"
        android:background="@color/red">


// Fix header For Product Name

</RelativeLayout>
         <ScrollView
                android:id="@+id/scroll_main"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="0.5">

        <LinearLayout
                        android:id="@+id/lin_upper"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:orientation="vertical">

        /// For the Conternt that you want to put upper Side OF bottom View that is Fix>> In my case pager, PagerIndicator ,ProductName, Price ,Size And size list>>

        // Create this to get Upper Content height.. And Put your Content..


        </LinearLayout>
         <LinearLayout
                        android:id="@+id/llin_inner_button"
                        android:layout_width="match_parent"
                        android:layout_height="48dp"
                        android:orientation="horizontal"
                        android:visibility="visible">

        // Copy Bottom View that you want to Stick

        </LinearLayout>
         <TextView
                        android:id="@+id/txt_temp"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_marginLeft="8dp"
                        android:layout_marginTop="8dp"
                        android:text="Lorazepam belongs to a group of drugs"/>

        </ScrollView>

        <LinearLayout
                android:id="@+id/llin_outer_button"
                android:layout_width="match_parent"
                android:layout_height="48dp"
                android:orientation="horizontal">

        // Your actual BottomView Here...

        </LinearLayout>

    </LinearLayout>

Java File

Declare your variables

 private int lay_height = 0;
 int height = 0;

Now you need to get your height as per device screen height including status bar height and soft buttons height and you need to add header height(if necessary) in to total height.

 public int getStatusBarHeight() {
    int result = 0;
    int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
    if (resourceId > 0) {
        result = getResources().getDimensionPixelSize(resourceId);
    }
    return result;
}

@SuppressLint("NewApi")
private int getSoftButtonsBarHeight() {
    // getRealMetrics is only available with API 17 and +
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
        DisplayMetrics metrics = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(metrics);
        int usableHeight = metrics.heightPixels;
        getWindowManager().getDefaultDisplay().getRealMetrics(metrics);
        int realHeight = metrics.heightPixels;
        if (realHeight > usableHeight)
            return realHeight - usableHeight;
        else
            return 0;
    }
    return 0;
}

public int pxToDp(int px) {
    DisplayMetrics displayMetrics = this.getResources().getDisplayMetrics();
    int dp = Math.round(px / (displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT));
    return dp;
}

public static float dipToPixels(Context context, float dipValue) {
    DisplayMetrics metrics = context.getResources().getDisplayMetrics();
    return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dipValue, metrics);
}

Calculate Height

    Display display = getWindowManager().getDefaultDisplay();
    Point size = new Point();
    display.getSize(size);
    int width = size.x;


    if (getSoftButtonsBarHeight() == 0) {
        height = size.y - getStatusBarHeight() - getSoftButtonsBarHeight() - (int) dipToPixels(ProductDetailActivity.this, 104);

    } else {
        height = size.y - getStatusBarHeight() - getSoftButtonsBarHeight() - (int) dipToPixels(ProductDetailActivity.this, 56);
    }

    Log.v("height_sc", height + "" + "   " + getStatusBarHeight() + "  " + getSoftButtonsBarHeight() + "   " + size.y);

    ViewTreeObserver observer = lin_upper.getViewTreeObserver();
    observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {

        @Override
        public void onGlobalLayout() {
            // TODO Auto-generated method stub
            lay_height = lin_upper.getHeight();
            int headerLayoutWidth = lin_upper.getWidth();
            lin_upper.getViewTreeObserver().removeGlobalOnLayoutListener(
                    this);

            Log.v("height", lay_height + "");
        }
    });

Now you need to implement functionality for scroll view in onScrollChanged() method.

 scroll_main.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {

        @Override
        public void onScrollChanged() {
            new Handler().post(new Runnable() {
                @Override
                public void run() {
                    int scrollX = scroll_main.getScrollX(); //for horizontalScrollView
                    int scrollY = scroll_main.getScrollY(); //for verticalScrollView


                    int sc = scrollY + height;

                    Log.v("bottom", lay_height + "  Y=" + sc + "  " + scrollY + "   " + height);

                    if (sc >= lay_height) {

                        llin_outer_button.setVisibility(View.GONE);

                    } else {
                        llin_outer_button.setVisibility(View.VISIBLE);
                    }
                }
            });
        }
    });

Solution 2

I have implemented a library to do that. You can give it a try. Here is the link to that lib https://github.com/amarjain07/StickyScrollView

Share:
10,124
Varun Barve
Author by

Varun Barve

Updated on June 28, 2022

Comments

  • Varun Barve
    Varun Barve almost 2 years

    I wanted to implement a sticky scrollview item which sticks at the bottom of the screen. Below are a couple of screenshots to explain my question.

    1. The below screen shows the fixed view/layout at the bottom of the screen saying 'Save to and Add to Bag'

    Layout/View Stuck to the bottom of the page

    1. When the user scrolls down the page, the layout/view scrolls with the page. As shown in the below screen.

    The layout/view scrolling with the page

    Things i have tried:

    1.StickyScrollViewItems by emilsjolander: https://github.com/emilsjolander/StickyScrollViewItems/blob/master/library/src/com/emilsjolander/components/StickyScrollViewItems/StickyScrollView.java

    I tried to reverse the header to the bottom, but no luck!

    Your help will be deeply appreciated.

    Thank you.

    EDIT:

    Following is the scrollview I tried to make. The sticky view still sticks at the top, where as it should stick to the bottom.

    public class StickyScrollView extends ScrollView {
    
    /**
     * Tag for views that should stick and have constant drawing. e.g. TextViews, ImageViews etc
     */
    public static final String STICKY_TAG = "sticky";
    
    /**
     * Flag for views that should stick and have non-constant drawing. e.g. Buttons, ProgressBars etc
     */
    public static final String FLAG_NONCONSTANT = "-nonconstant";
    
    /**
     * Flag for views that have aren't fully opaque
     */
    public static final String FLAG_HASTRANSPARANCY = "-hastransparancy";
    
    /**
     * Default height of the shadow peeking out below the stuck view.
     */
    private static final int DEFAULT_SHADOW_HEIGHT = 10; // dp;
    
    private ArrayList<View> stickyViews;
    private View currentlyStickingView;
    private float stickyViewTopOffset, stickViewBottomOffset;
    private int stickyViewLeftOffset;
    private boolean redirectTouchesToStickyView;
    private boolean clippingToPadding;
    private boolean clipToPaddingHasBeenSet;
    
    private int mShadowHeight;
    private Drawable mShadowDrawable;
    
    private final Runnable invalidateRunnable = new Runnable() {
    
        @Override
        public void run() {
            if (currentlyStickingView != null) {
                int l = getLeftForViewRelativeOnlyChild(currentlyStickingView);
                int t = getBottomForViewRelativeOnlyChild(currentlyStickingView);
                int r = getRightForViewRelativeOnlyChild(currentlyStickingView);
                //int b = (int) (getScrollY() + (currentlyStickingView.getHeight() + stickyViewTopOffset));
                int b = getBottomForViewRelativeOnlyChild(currentlyStickingView);
                invalidate(l, t, r, b);
            }
            postDelayed(this, 16);
        }
    };
    
    public StickyScrollView(Context context) {
        this(context, null);
    }
    
    public StickyScrollView(Context context, AttributeSet attrs) {
        this(context, attrs, android.R.attr.scrollViewStyle);
    }
    
    public StickyScrollView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        setup();
    
        TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.StickyScrollView, defStyle, 0);
    
        final float density = context.getResources().getDisplayMetrics().density;
        int defaultShadowHeightInPix = (int) (DEFAULT_SHADOW_HEIGHT * density + 0.5f);
    
        mShadowHeight = a.getDimensionPixelSize(
                R.styleable.StickyScrollView_stuckShadowHeight,
                defaultShadowHeightInPix);
    
        int shadowDrawableRes = a.getResourceId(
                R.styleable.StickyScrollView_stuckShadowDrawable, -1);
    
        if (shadowDrawableRes != -1) {
            mShadowDrawable = context.getResources().getDrawable(
                    shadowDrawableRes);
        }
    
        a.recycle();
    
    }
    
    /**
     * Sets the height of the shadow drawable in pixels.
     *
     * @param height
     */
    public void setShadowHeight(int height) {
        mShadowHeight = height;
    }
    
    
    public void setup() {
        stickyViews = new ArrayList<View>();
    }
    
    private int getLeftForViewRelativeOnlyChild(View v) {
        int left = v.getLeft();
        while (v.getParent() != getChildAt(0)) {
            v = (View) v.getParent();
            left += v.getLeft();
        }
        return left;
    }
    
    private int getTopForViewRelativeOnlyChild(View v) {
        int top = v.getTop();
        while (v.getParent() != getChildAt(0)) {
            v = (View) v.getParent();
            top += v.getTop();
        }
        return top;
    }
    
    private int getRightForViewRelativeOnlyChild(View v) {
        int right = v.getRight();
        while (v.getParent() != getChildAt(0)) {
            v = (View) v.getParent();
            right += v.getRight();
        }
        return right;
    }
    
    private int getBottomForViewRelativeOnlyChild(View v) {
        int bottom = v.getBottom();
        while (v.getParent() != getChildAt(0)) {
            v = (View) v.getParent();
            bottom += v.getBottom();
        }
        return bottom;
    }
    
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        if (!clipToPaddingHasBeenSet) {
            clippingToPadding = true;
        }
        notifyHierarchyChanged();
    }
    
    @Override
    public void setClipToPadding(boolean clipToPadding) {
        super.setClipToPadding(clipToPadding);
        clippingToPadding = clipToPadding;
        clipToPaddingHasBeenSet = true;
    }
    
    @Override
    public void addView(View child) {
        super.addView(child);
        findStickyViews(child);
    }
    
    @Override
    public void addView(View child, int index) {
        super.addView(child, index);
        findStickyViews(child);
    }
    
    @Override
    public void addView(View child, int index, android.view.ViewGroup.LayoutParams params) {
        super.addView(child, index, params);
        findStickyViews(child);
    }
    
    @Override
    public void addView(View child, int width, int height) {
        super.addView(child, width, height);
        findStickyViews(child);
    }
    
    @Override
    public void addView(View child, android.view.ViewGroup.LayoutParams params) {
        super.addView(child, params);
        findStickyViews(child);
    }
    
    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        if (currentlyStickingView != null) {
            canvas.save();
            //canvas.translate(getPaddingLeft() + stickyViewLeftOffset, getScrollY() + stickyViewTopOffset + (clippingToPadding ? getPaddingTop() : 0));
            canvas.translate(getPaddingLeft() + stickyViewLeftOffset, getScrollY() - stickViewBottomOffset + (clippingToPadding ? getPaddingBottom() : 0));
    
            //canvas.clipRect(0, (clippingToPadding ? -stickyViewTopOffset : 0),
            //getWidth() - stickyViewLeftOffset,
            //currentlyStickingView.getHeight() + mShadowHeight + 1);
    
            canvas.clipRect(0, currentlyStickingView.getHeight() - mShadowHeight, getWidth() - stickyViewLeftOffset, (clippingToPadding ? 0 : stickViewBottomOffset));
    
            if (mShadowDrawable != null) {
                int left = 0;
                int right = currentlyStickingView.getWidth();
                int top = currentlyStickingView.getHeight();
                int bottom = currentlyStickingView.getHeight() + mShadowHeight;
                mShadowDrawable.setBounds(left, top, right, bottom);
                mShadowDrawable.draw(canvas);
            }
    
            //canvas.clipRect(0, (clippingToPadding ? -stickyViewTopOffset : 0), getWidth(), currentlyStickingView.getHeight());
            canvas.clipRect(0, currentlyStickingView.getHeight(), getWidth(), (clippingToPadding ? 0 : stickViewBottomOffset));
            if (getStringTagForView(currentlyStickingView).contains(FLAG_HASTRANSPARANCY)) {
                showView(currentlyStickingView);
                currentlyStickingView.draw(canvas);
                hideView(currentlyStickingView);
            } else {
                currentlyStickingView.draw(canvas);
            }
            canvas.restore();
        }
    }
    
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            redirectTouchesToStickyView = true;
        }
    
        if (redirectTouchesToStickyView) {
            redirectTouchesToStickyView = currentlyStickingView != null;
            if (redirectTouchesToStickyView) {
                redirectTouchesToStickyView =
                        //ev.getY() <= (currentlyStickingView.getHeight() + stickyViewTopOffset)
                        ev.getY() <= (currentlyStickingView.getHeight() - stickViewBottomOffset) &&
                                ev.getX() >= getLeftForViewRelativeOnlyChild(currentlyStickingView) &&
                                ev.getX() <= getRightForViewRelativeOnlyChild(currentlyStickingView);
            }
        } else if (currentlyStickingView == null) {
            redirectTouchesToStickyView = false;
        }
        if (redirectTouchesToStickyView) {
            //ev.offsetLocation(0, -1 * ((getScrollY() + stickyViewTopOffset) - getTopForViewRelativeOnlyChild(currentlyStickingView)));
            ev.offsetLocation(0, 1 * ((getScrollY() + stickViewBottomOffset) - getBottomForViewRelativeOnlyChild(currentlyStickingView)));
        }
        return super.dispatchTouchEvent(ev);
    }
    
    private boolean hasNotDoneActionDown = true;
    
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (redirectTouchesToStickyView) {
            //ev.offsetLocation(0, ((getScrollY() + stickyViewTopOffset) - getTopForViewRelativeOnlyChild(currentlyStickingView)));
            ev.offsetLocation(0, ((getScrollY() - stickViewBottomOffset) - getTopForViewRelativeOnlyChild(currentlyStickingView)));
        }
    
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            hasNotDoneActionDown = false;
        }
    
        if (hasNotDoneActionDown) {
            MotionEvent down = MotionEvent.obtain(ev);
            down.setAction(MotionEvent.ACTION_DOWN);
            super.onTouchEvent(down);
            hasNotDoneActionDown = false;
        }
    
        if (ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL) {
            hasNotDoneActionDown = true;
        }
    
        return super.onTouchEvent(ev);
    }
    
    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        doTheStickyThing();
    }
    
    private void doTheStickyThing() {
        View viewThatShouldStick = null;
        View approachingView = null;
        for (View v : stickyViews) {
            int viewTop = getTopForViewRelativeOnlyChild(v) - getScrollY() + (clippingToPadding ? 0 : getPaddingTop());
            int viewBottom = getBottomForViewRelativeOnlyChild(v) - getScrollY() + (clippingToPadding ? 0 : getPaddingBottom());
            Log.e("VIEW BOTTOM: ", "VIEW BOTTOM: " + viewBottom);
    
            //Log.e("VIEW TOP: ", "VIEW TOP: " + viewTop);
    
            //BOTTOM
            if (viewBottom >= 0) {
                if (viewThatShouldStick == null || viewBottom > (getBottomForViewRelativeOnlyChild(viewThatShouldStick) - getScrollY() + (clippingToPadding ? 0 : getPaddingBottom()))) {
                    viewThatShouldStick = v;
                    Log.e("VIEW BOTTOM: ", "VIEW THAT SHOULD STICK: " + viewThatShouldStick);
                }
            } else {
                if (approachingView == null || viewBottom < (getBottomForViewRelativeOnlyChild(approachingView) - getScrollY() + (clippingToPadding ? 0 : getPaddingBottom()))) {
                    approachingView = v;
                    Log.e("VIEW BOTTOM: ", "APPROACHING VIEW: " + approachingView);
                }
            }
    
            //            //TOP
            //            if (viewTop <= 0) {
            //                if (viewThatShouldStick == null || viewTop > (getTopForViewRelativeOnlyChild(viewThatShouldStick) - getScrollY() + (clippingToPadding ? 0 : getPaddingTop()))) {
            //                    viewThatShouldStick = v;
            //                }
            //            } else {
            //                if (approachingView == null || viewTop < (getTopForViewRelativeOnlyChild(approachingView) - getScrollY() + (clippingToPadding ? 0 : getPaddingTop()))) {
            //                    approachingView = v;
            //                }
            //            }
        }
    
        //BOTTOM
        if (viewThatShouldStick != null) {
            stickViewBottomOffset = approachingView == null ? 0 : Math.min(0, getBottomForViewRelativeOnlyChild(approachingView) - getScrollY() + (clippingToPadding ? 0 : getPaddingBottom() - viewThatShouldStick.getHeight()));
            if (viewThatShouldStick != currentlyStickingView) {
                if (currentlyStickingView != null) {
                    stopStickingCurrentlyStickingView();
                    Log.e("BOTTOM UNSTUCK: ", "BOTTOM UNSTUCK: ");
                }
                stickyViewLeftOffset = getLeftForViewRelativeOnlyChild(viewThatShouldStick);
                startStickingView(viewThatShouldStick);
                Log.e("BOTTOM STUCK: ", "BOTTOM STUCK: " + viewThatShouldStick);
            }
        } else if (currentlyStickingView != null) {
            Log.e("BOTTOM UNSTUCK: ", "BOTTOM UNSTUCK: ");
            stopStickingCurrentlyStickingView();
        }
    
        //TOP
        //        if (viewThatShouldStick != null) {
        //            stickyViewTopOffset = approachingView == null ? 0 : Math.min(0, getTopForViewRelativeOnlyChild(approachingView) - getScrollY() + (clippingToPadding ? 0 : getPaddingTop()) - viewThatShouldStick.getHeight());
        ////            Log.e("VIEW TOP: ", "STICKY VIEW TOP OFFSET: " + stickyViewTopOffset);
        //            if (viewThatShouldStick != currentlyStickingView) {
        //                if (currentlyStickingView != null) {
        //                    stopStickingCurrentlyStickingView();
        //                }
        //                // only compute the left offset when we start sticking.
        //                stickyViewLeftOffset = getLeftForViewRelativeOnlyChild(viewThatShouldStick);
        //                startStickingView(viewThatShouldStick);
        //            }
        //        } else if (currentlyStickingView != null) {
        //            stopStickingCurrentlyStickingView();
        //        }
    }
    
    private void startStickingView(View viewThatShouldStick) {
        currentlyStickingView = viewThatShouldStick;
        if (getStringTagForView(currentlyStickingView).contains(FLAG_HASTRANSPARANCY)) {
            hideView(currentlyStickingView);
        }
        if (((String) currentlyStickingView.getTag()).contains(FLAG_NONCONSTANT)) {
            post(invalidateRunnable);
        }
    }
    
    private void stopStickingCurrentlyStickingView() {
        if (getStringTagForView(currentlyStickingView).contains(FLAG_HASTRANSPARANCY)) {
            showView(currentlyStickingView);
        }
        currentlyStickingView = null;
        removeCallbacks(invalidateRunnable);
    }
    
    /**
     * Notify that the sticky attribute has been added or removed from one or more views in the View hierarchy
     */
    public void notifyStickyAttributeChanged() {
        notifyHierarchyChanged();
    }
    
    private void notifyHierarchyChanged() {
        if (currentlyStickingView != null) {
            stopStickingCurrentlyStickingView();
        }
        stickyViews.clear();
        findStickyViews(getChildAt(0));
        doTheStickyThing();
        invalidate();
    }
    
    private void findStickyViews(View v) {
        if (v instanceof ViewGroup) {
            ViewGroup vg = (ViewGroup) v;
            for (int i = 0; i < vg.getChildCount(); i++) {
                String tag = getStringTagForView(vg.getChildAt(i));
                if (tag != null && tag.contains(STICKY_TAG)) {
                    stickyViews.add(vg.getChildAt(i));
                } else if (vg.getChildAt(i) instanceof ViewGroup) {
                    findStickyViews(vg.getChildAt(i));
                }
            }
        } else {
            String tag = (String) v.getTag();
            if (tag != null && tag.contains(STICKY_TAG)) {
                stickyViews.add(v);
            }
        }
    }
    
    private String getStringTagForView(View v) {
        Object tagObject = v.getTag();
        return String.valueOf(tagObject);
    }
    
    private void hideView(View v) {
        if (Build.VERSION.SDK_INT >= 11) {
            v.setAlpha(0);
        } else {
            AlphaAnimation anim = new AlphaAnimation(1, 0);
            anim.setDuration(0);
            anim.setFillAfter(true);
            v.startAnimation(anim);
        }
    }
    
    private void showView(View v) {
        if (Build.VERSION.SDK_INT >= 11) {
            v.setAlpha(1);
        } else {
            AlphaAnimation anim = new AlphaAnimation(0, 1);
            anim.setDuration(0);
            anim.setFillAfter(true);
            v.startAnimation(anim);
        }
    }
    }