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.
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);
}
}
}
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.
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);
}
}
});
}
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With