package com.wuxiaolong.androidsamples.xscrollview;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.animation.DecelerateInterpolator;
import android.widget.AbsListView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.ScrollView;
import android.widget.Scroller;
import android.widget.TextView;
import com.wuxiaolong.androidsamples.R;
/**
* Created by WuXiaolong on 2015/9/22.
*/
public class XScrollView extends ScrollView implements AbsListView.OnScrollListener {
// private static final String TAG = "XScrollView";
private final static int SCROLL_BACK_HEADER = 0;
private final static int SCROLL_BACK_FOOTER = 1;
private final static int SCROLL_DURATION = 400;
// when pull up >= 50px
private final static int PULL_LOAD_MORE_DELTA = 50;
// support iOS like pull
private final static float OFFSET_RADIO = 1.8f;
private float mLastY = -1;
// used for scroll back
private Scroller mScroller;
// user's scroll listener
private AbsListView.OnScrollListener mScrollListener;
// for mScroller, scroll back from header or footer.
private int mScrollBack;
// the interface to trigger refresh and load more.
private IXScrollViewListener mListener;
private LinearLayout mLayout;
private LinearLayout mContentLayout;
private XHeaderView mHeader;
// header view content, use it to calculate the Header's height. And hide it when disable pull refresh.
private RelativeLayout mHeaderContent;
private TextView mHeaderTime;
private int mHeaderHeight;
private XFooterView mFooterView;
private boolean mEnablePullRefresh = true;
private boolean mPullRefreshing = false;
private boolean mEnablePullLoad = true;
private boolean mEnableAutoLoad = false;
private boolean mPullLoading = false;
public XScrollView(Context context) {
super(context);
initWithContext(context);
}
public XScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
initWithContext(context);
}
public XScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initWithContext(context);
}
private void initWithContext(Context context) {
mLayout = (LinearLayout) View.inflate(context, R.layout.xscrollview_layout, null);
mContentLayout = (LinearLayout) mLayout.findViewById(R.id.content_layout);
mScroller = new Scroller(context, new DecelerateInterpolator());
// XScrollView need the scroll event, and it will dispatch the event to user's listener (as a proxy).
this.setOnScrollListener(this);
// init header view
mHeader = new XHeaderView(context);
mHeaderContent = (RelativeLayout) mHeader.findViewById(R.id.header_content);
mHeaderTime = (TextView) mHeader.findViewById(R.id.header_hint_time);
LinearLayout headerLayout = (LinearLayout) mLayout.findViewById(R.id.header_layout);
headerLayout.addView(mHeader);
// init footer view
mFooterView = new XFooterView(context);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT);
params.gravity = Gravity.CENTER;
LinearLayout footLayout = (LinearLayout) mLayout.findViewById(R.id.footer_layout);
footLayout.addView(mFooterView, params);
// init header height
ViewTreeObserver observer = mHeader.getViewTreeObserver();
if (null != observer) {
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
@SuppressWarnings("deprecation")
@Override
public void onGlobalLayout() {
mHeaderHeight = mHeaderContent.getHeight();
ViewTreeObserver observer = getViewTreeObserver();
if (null != observer) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
observer.removeGlobalOnLayoutListener(this);
} else {
observer.removeOnGlobalLayoutListener(this);
}
}
}
});
}
this.addView(mLayout);
}
/**
* Set the content ViewGroup for XScrollView.
*
* @param content
*/
public void setContentView(ViewGroup content) {
if (mLayout == null) {
return;
}
if (mContentLayout == null) {
mContentLayout = (LinearLayout) mLayout.findViewById(R.id.content_layout);
}
if (mContentLayout.getChildCount() > 0) {
mContentLayout.removeAllViews();
}
mContentLayout.addView(content);
}
/**
* Set the content View for XScrollView.
*
* @param content
*/
public void setView(View content) {
if (mLayout == null) {
return;
}
if (mContentLayout == null) {
mContentLayout = (LinearLayout) mLayout.findViewById(R.id.content_layout);
}
mContentLayout.addView(content);
}
/**
* Enable or disable pull down refresh feature.
*
* @param enable
*/
public void setPullRefreshEnable(boolean enable) {
mEnablePullRefresh = enable;
// disable, hide the content
mHeaderContent.setVisibility(enable ? View.VISIBLE : View.INVISIBLE);
}
/**
* Enable or disable pull up load more feature.
*
* @param enable
*/
public void setPullLoadEnable(boolean enable) {
mEnablePullLoad = enable;
if (!mEnablePullLoad) {
mFooterView.setBottomMargin(0);
mFooterView.hide();
mFooterView.setPadding(0, 0, 0, mFooterView.getHeight() * (-1));
mFooterView.setOnClickListener(null);
} else {
mPullLoading = false;
mFooterView.setPadding(0, 0, 0, 0);
mFooterView.show();
mFooterView.setState(XFooterView.STATE_NORMAL);
// both "pull up" and "click" will invoke load more.
mFooterView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
startLoadMore();
}
});
}
}
/**
* Enable or disable auto load more feature when scroll to bottom.
*
* @param enable
*/
public void setAutoLoadEnable(boolean enable) {
mEnableAutoLoad = enable;
}
/**
* Stop refresh, reset header view.
*/
public void stopRefresh() {
if (mPullRefreshing) {
mPullRefreshing = false;
resetHeaderHeight();
}
}
/**
* Stop load more, reset footer view.
*/
public void stopLoadMore() {
if (mPullLoading) {
mPullLoading = false;
mFooterView.setState(XFooterView.STATE_NORMAL);
}
}
/**
* Set last refresh time
*
* @param time
*/
public void setRefreshTime(String time) {
mHeaderTime.setText(time);
}
/**
* Set listener.
*
* @param listener
*/
public void setIXScrollViewListener(IXScrollViewListener listener) {
mListener = listener;
}
/**
* Auto call back refresh.
*/
public void autoRefresh() {
mHeader.setVisibleHeight(mHeaderHeight);
if (mEnablePullRefresh && !mPullRefreshing) {
// update the arrow image not refreshing
if (mHeader.getVisibleHeight() > mHeaderHeight) {
mHeader.setState(XHeaderView.STATE_READY);
} else {
mHeader.setState(XHeaderView.STATE_NORMAL);
}
}
mPullRefreshing = true;
mHeader.setState(XHeaderView.STATE_REFRESHING);
refresh();
}
private void invokeOnScrolling() {
if (mScrollListener instanceof OnXScrollListener) {
OnXScrollListener l = (OnXScrollListener) mScrollListener;
l.onXScrolling(this);
}
}
private void updateHeaderHeight(float delta) {
mHeader.setVisibleHeight((int) delta + mHeader.getVisibleHeight());
if (mEnablePullRefresh && !mPullRefreshing) {
// update the arrow image unrefreshing
if (mHeader.getVisibleHeight() > mHeaderHeight) {
mHeader.setState(XHeaderView.STATE_READY);
} else {
mHeader.setState(XHeaderView.STATE_NORMAL);
}
}
// scroll to top each time
post(new Runnable() {
@Override
public void run() {
XScrollView.this.fullScroll(ScrollView.FOCUS_UP);
}
});
}
private void resetHeaderHeight() {
int height = mHeader.getVisibleHeight();
if (height == 0)
return;
// refreshing and header isn't shown fully. do nothing.
if (mPullRefreshing && height <= mHeaderHeight) return;
// default: scroll back to dismiss header.
int finalHeight = 0;
// is refreshing, just scroll back to show all the header.
if (mPullRefreshing && height > mHeaderHeight) {
finalHeight = mHeaderHeight;
}
mScrollBack = SCROLL_BACK_HEADER;
mScroller.startScroll(0, height, 0, finalHeight - height, SCROLL_DURATION);
// trigger computeScroll
invalidate();
}
private void updateFooterHeight(float delta) {
int height = mFooterView.getBottomMargin() + (int) delta;
if (mEnablePullLoad && !mPullLoading) {
if (height > PULL_LOAD_MORE_DELTA) {
// height enough to invoke load more.
mFooterView.setState(XFooterView.STATE_READY);
} else {
mFooterView.setState(XFooterView.STATE_NORMAL);
}
}
mFooterView.setBottomMargin(height);
// scroll to bottom
post(new Runnable() {
@Override
public void run() {
XScrollView.this.fullScroll(ScrollView.FOCUS_DOWN);
}
});
}
private void resetFooterHeight() {
int bottomMargin = mFooterView.getBottomMargin();
if (bottomMargin > 0) {
mScrollBack = SCROLL_BACK_FOOTER;
mScroller.startScroll(0, bottomMargin, 0, -bottomMargin, SCROLL_DURATION);
invalidate();
}
}
private void startLoadMore() {
if (!mPullLoading) {
mPullLoading = true;
mFooterView.setState(XFooterView.STATE_LOADING);
loadMore();
}
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (mLastY == -1) {
mLastY = ev.getRawY();
}
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastY = ev.getRawY();
break;
case MotionEvent.ACTION_MOVE:
final float deltaY = ev.getRawY() - mLastY;
mLastY = ev.getRawY();
if (isTop() && (mHeader.getVisibleHeight() > 0 || deltaY > 0)) {
// the first item is showing, header has shown or pull down.
updateHeaderHeight(deltaY / OFFSET_RADIO);
invokeOnScrolling();
} else if (isBottom() && (mFooterView.getBottomMargin() > 0 || deltaY < 0)) {
// last item, already pulled up or want to pull up.
updateFooterHeight(-deltaY / OFFSET_RADIO);
}
break;
default:
// reset
mLastY = -1;
resetHeaderOrBottom();
break;
}
return super.onTouchEvent(ev);
}
private void resetHeaderOrBottom() {
if (isTop()) {
// invoke refresh
if (mEnablePullRefresh && mHeader.getVisibleHeight() > mHeaderHeight) {
mPullRefreshing = true;
mHeader.setState(XHeaderView.STATE_REFRESHING);
refresh();
}
resetHeaderHeight();
} else if (isBottom()) {
// invoke load more.
if (mEnablePullLoad && mFooterView.getBottomMargin() > PULL_LOAD_MORE_DELTA) {
startLoadMore();
}
resetFooterHeight();
}
}
private boolean isTop() {
return getScrollY() <= 0 || mHeader.getVisibleHeight() > mHeaderHeight || mContentLayout.getTop() > 0;
}
private boolean isBottom() {
return Math.abs(getScrollY() + getHeight() - computeVerticalScrollRange()) <= 5 ||
(getScrollY() > 0 && null != mFooterView && mFooterView.getBottomMargin() > 0);
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
if (mScrollBack == SCROLL_BACK_HEADER) {
mHeader.setVisibleHeight(mScroller.getCurrY());
} else {
mFooterView.setBottomMargin(mScroller.getCurrY());
}
postInvalidate();
invokeOnScrolling();
}
super.computeScroll();
}
public void setOnScrollListener(AbsListView.OnScrollListener l) {
mScrollListener = l;
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (mScrollListener != null) {
mScrollListener.onScrollStateChanged(view, scrollState);
}
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
// Grab the last child placed in the ScrollView, we need it to determinate the bottom position.
View view = getChildAt(getChildCount() - 1);
if (null != view) {
// Calculate the scroll diff
int diff = (view.getBottom() - (view.getHeight() + view.getScrollY()));
// if diff is zero, then the bottom has been reached
if (diff == 0 && mEnableAutoLoad) {
// notify that we have reached the bottom
startLoadMore();
}
}
super.onScrollChanged(l, t, oldl, oldt);
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
int totalItemCount) {
// send to user's listener
if (mScrollListener != null) {
mScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
}
}
private void refresh() {
if (mEnablePullRefresh && null != mListener) {
mListener.onRefresh();
}
}
private void loadMore() {
if (mEnablePullLoad && null != mListener) {
mListener.onLoadMore();
}
}
/**
* You can listen ListView.OnScrollListener or this one. it will invoke
* onXScrolling when header/footer scroll back.
*/
public interface OnXScrollListener extends AbsListView.OnScrollListener {
public void onXScrolling(View view);
}
/**
* Implements this interface to get refresh/load more event.
*/
public interface IXScrollViewListener {
public void onRefresh();
public void onLoadMore();
}
}