自定义ViewGroup

自定义ViewGroup是另外一种重要的自定义View形式,当我们需要自定义子视图的排列方式时,通常下幼通过这种形式实现。例如,最常用的下拉刷新组件,实现下拉刷新、上拉加载跟他更多的原理就是自定义了一个ViewGroup,将HeaderView、ContentView、FooterView从上到下依次布局,如图2-16所示(红色区域为屏幕的显示区域运行时可看到色彩)。然后再初始时通过Scroller滚动使用该组件在y轴上滚动HeaderView的高度,这样当以来该ViewGroup显示在用户眼前时候HeaderView就被隐藏掉了,如图2-17所示。而Content View的高度和宽度都是match_parent的,因此,此时屏幕上只显示CotnentView,HeaderView和FooterView都被隐藏在屏幕之外。当contentView被滚动到顶部,此时如果用户继续下拉,那么该下拉刷新组件将拦截触摸事件,然后根据用户的触摸事件获取到手指滑动的y轴距离,并通过Scroller将该下拉刷新组件在y轴上滚动手指滑动的距离,实现HeaderView的显示与隐藏,从而达到下拉的效果,如图所示。当用户胡到那个到最底部时候会触发加载更多的操作,此时会通过Scroller滚动该下拉刷新组件,将FooterView显示出来,实现加载更多的效果。

Scroller的使用

为了更好的理解下拉刷新的实现,我们要先了解Scroller的作用以及如何使用。这里我们将做一个简单的示例来说明。
Scroller是一个帮助View滚动的辅助类,在使用它之前,用户需要通过startScroll来设置滚动的参数,即起始点坐标和(x,y)轴上要滚动的距离。Scroller它封装了滚动时间、要滚动的目标x轴和y轴,以及在每个时间内View应该滚动到的(x,y)轴的坐标点,这样用户就可以在有效的滚动周期内通过Scroller的getCurX()和getCurY()来获取当前时刻View应该滚动的位置,然后通过调用View的scrollTo或者ScrollBy方法进行滚动。那么如何判断滚动是否结束呢?我们只需要覆写View类的computScroll方法,该方法会在View绘制时被调用,在里面调用Scroller的computeScrollOffset来判断滚动是否完成,如果返回true表明滚动未完成,否则滚动完成。上述说的scrollTo或者scrollBy的调用就是在computeScrollOffset为true的情况下调用,并且最后还要调用目标View的postInvalidate()或者invalidate()以实现View的重绘。View的重绘又会导致computeScroll方法被调用,从而继续整个滚动过程,直至computeScrollOffset返回false,即滚动结束。整个过程有点儿绕,我们看一个实例。


public class ScrollLayout extends LinearLayout {private String TAG = ScrollLayout.class.getSimpleName();private Scroller mScroller;public ScrollLayout(@NonNull Context context) {super(context);initScroller(context);}public ScrollLayout(@NonNull Context context, @Nullable AttributeSet attrs) {super(context, attrs);initScroller(context);}public ScrollLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);initScroller(context);}void initScroller(Context context){mScroller = new Scroller(context);}@Overridepublic void computeScroll() {//        super.computeScroll();if(mScroller.computeScrollOffset()) {//滚动到此,View应该滚动到的x,y坐标上this.scrollTo(mScroller.getCurrX(),  mScroller.getCurrY());//请求重新绘制该View,从而又会倒是computeScroll调用,然后继续滚动// 直到computeScrollOffset返回falsethis.postInvalidate();}}//调用这个方法进行滚动,这里我们只滚动竖直方向public void scrollTo(int y){// 参数1和参数2分别为滚动的起始点水平、竖直方向的滚动偏移量// 参数3和参数4为水平和竖直方向上滚动的距离mScroller.startScroll(getScrollX(), getScrollY(), 0, y);this.invalidate();}}

ScrollLayout scrollView = new ScrollLayout(getContext());
scrollView.scrollTo(100);

通过上面这段代码会让scrollView在y轴上向下滚动100哥像素点。我们结合代码来分析以下。首先调用scrollTo(inty)方法,然后再该方法中通过mScroller.startScroll()方法来设置滚动的参数,再调用invalidate()方法使得该View重绘。重绘时调用computeScroll方法,再该方法中通过mScroller.computeScrollOffset()判断滚动是否完成,如果返回true,代表没有滚动完成,此时把该View滚动到此刻View应该滚动到的x、y位置,这个位置通过mScroller的getCurrX和getCurrY获得。然后继续调用重绘方法,继续执行滚动过程,直至滚动完成。‘
了解了Scroller原理后,我们继续看通用的下拉刷新组件的实现吧。

下拉刷新的实现

代码量不多,但是也挺有用的,我们这里只拿出重要的点来分析,完整的源码请发文gitee
https://gitee.com/WhatINeed/SmartChart/tree/master/app/src/main/java/com/bin/david/smartchart/scroller
以下知识重要的代码段

public abstract class RefreshLayoutBase<T extends View> extends ViewGroup implements AbsListView.OnScrollListener {public interface OnRefreshListener {void onRefresh();}public interface OnLoadListener {void onLoadMore();}//  滚动控制器protected Scroller mScroller;// 下拉刷新时显示的header viewprotected View mHeaderView;// 上拉加载更多时显示的footer Viewprotected View mFooterView;//  本次触摸滑动y坐标上的偏移量protected int mYOffset;// 内容视图,即用户触摸导致下拉刷新、上拉加载的主视图,如ListView、GridView等protected T mContentView;//  最初的滚动位置,第一次布局时滚动header高度的距离protected int mInitScrollY = 0;// 最后一次触摸事件的Y轴坐标protected int mLastY = 0;// 空闲状态public static final int STATUS_IDLE = 0;// 下拉或者上拉状态,还没有到达可刷新的状态public static final int STATUS_PULL_TO_REFRESH = 1;// 下拉或者上拉状态public static final int STATUS_RELEASE_TO_REFRESH = 2;// 刷新中public static final int STATUS_REFRESHING = 3;// Loading中public static final int STATUS_LOADING = 4;// 当前状态public int mCurrentStatus = STATUS_IDLE;// header 中的箭头图标private ImageView mArrowImageView;//  箭头是否向上private boolean isArrowUp;//  header中的文本标签private TextView mTipsTextView;//  header中的时间标签private TextView mTimeTextView;// header中的进度条private ProgressBar mProgressBar;//屏幕的高度private int mScreenHeight;//  header的高度private int mHeaderHeight;// 下拉刷新回调private OnRefreshListener mOnRefreshListener;// 加载更多的回调private OnLoadListener mLoadListener;public void setOnRefreshListener(OnRefreshListener mOnRefreshListener) {this.mOnRefreshListener = mOnRefreshListener;}public void setOnLoadListener(OnLoadListener mLoadListener) {this.mLoadListener = mLoadListener;}public RefreshLayoutBase(Context context) {this(context, null);}public RefreshLayoutBase(Context context, AttributeSet attrs) {super(context, attrs, 0);}public RefreshLayoutBase(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);//初始化Scroller对象mScroller = new Scroller(context);// 获取屏幕高度mScreenHeight = context.getResources().getDisplayMetrics().heightPixels;//header的高度为屏幕高度的1/4mHeaderHeight = mScreenHeight / 4;//初始化整个布局initLayout(context);}// 初始化整个布局,从上到下分别为header、内容视图、footerprivate void initLayout(Context context) {//设置header viewsetupHeaderView(context);//设置内容视图setupContentView(context);//设置布局参数setDefaultContentLayoutParams();//添加内容视图,如ListView、GridView等addView(mContentView);//footer viewsetupFooterView(context);}/*** 设置Content View的默认布局参数*/protected void setDefaultContentLayoutParams() {ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);mContentView.setLayoutParams(params);}// 初始化header viewprotected void setupHeaderView(Context context) {mHeaderView = LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_header, this, false);mHeaderView.setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, mHeaderHeight));mHeaderView.setBackgroundColor(Color.RED);// header的高度为1/4的屏幕高度,但是,它只有100px是有效的显示区域// 取余paddingTop,这样是为了达到下拉的效果mHeaderView.setPadding(0, mHeaderHeight - 100, 0, 0);addView(mHeaderView);//初始化header view中的子视图mArrowImageView = mHeaderView.findViewById(R.id.pull_to_refresh_image);mTipsTextView = mHeaderView.findViewById(R.id.pull_to_refresh_text);mTimeTextView = mHeaderView.findViewById(R.id.pull_to_refresh_updated_at);mProgressBar = mHeaderView.findViewById(R.id.pull_to_refresh_progress);}// 初始化ContentView, 子类复写protected abstract void setupContentView(Context context);//初始化footerviewprotected void setupFooterView(Context context) {mFooterView = LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_fotter, this, false);addView(mFooterView);}//是否已经到了最顶部,子类需要复写该方法,使得mContentView滑动到最顶端时返回true//如果达到最顶端用户继续下拉则拦截事件protected abstract boolean isTop();//是否已经到了最底部,子类需要复写该方法,使得mContentiew滑动到最底端时返回false//从而出发自动加载更多的操作protected abstract boolean isBottom();/*** 丈量视图的宽、高。宽度为用户设置的宽度,高度则为header、content view、footer这三个子控件高度之和** @param widthMeasureSpec* @param heightMeasureSpec*/@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);// MeasureSpec中的宽度值int width = MeasureSpec.getSize(widthMeasureSpec);//子视图的个数int childCount = getChildCount();// 最终的高度int finalHeight = 0;for (int i = 0; i < childCount; i++) {View child = getChildAt(i);//测量每个子视图的尺寸measureChild(child, widthMeasureSpec, heightMeasureSpec);//所有子视图的高度和就是该下拉率新组件的总高度finalHeight += child.getMeasuredHeight();}//设置该下拉刷新组件的尺寸setMeasuredDimension(width, finalHeight);}/*** 布局函数,将header、content view、footer这3个View从上到下布局。* 布局完成后通过Scroller滚动到header的底部,即滚动距离为header的高度+本视图的paddingTop,从而达到隐藏header的效果** @param changed* @param l* @param t* @param r* @param b*/@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {int childCound = getChildCount();int left = getPaddingLeft();int top = getPaddingTop();for (int i = 0; i < childCound; i++) {View child = getChildAt(i);child.layout(left, top, child.getMeasuredWidth(), child.getMeasuredHeight() + top);top += child.getMeasuredHeight();}//计算初始化滑动的y轴距离mInitScrollY = mHeaderView.getMeasuredHeight() + getPaddingTop();//滑动header view高度的位置,从而达到隐藏header view的效果scrollTo(0, mInitScrollY);}/*** 在适当的时候拦截触摸事件,这里指的适当的时候是当mContentView滑动到顶部,并且是下拉时拦截触摸事件,否则不拦截,交给child、view来处理*/@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {//  获取触摸事件的类型final int action = ev.getActionMasked();// 取消事件和抬起事件则直接返回falseif (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {return false;}switch (action) {case MotionEvent.ACTION_DOWN:mLastY = (int) ev.getRawY();break;case MotionEvent.ACTION_MOVE:mYOffset = (int) ev.getRawY() - mLastY;//如果拉到了顶部,并且是下拉,则拦截触摸事件//从而转到onTouchEvent来处理下拉刷新事件if (isTop() && mYOffset > 0) {return true;}break;}//默认不拦截触摸事件,使得该控件的子视图得到处理机会return false;}/*** 在这里处理触摸事件以达到下拉刷新或者上拉自动加载的问题** @param event* @return*/@Overridepublic boolean onTouchEvent(MotionEvent event) {switch (event.getAction()) {//滑动事件case MotionEvent.ACTION_MOVE://获取手指触摸的当前y坐标int currentY = (int) event.getRawY();//当前坐标减去按下时的y坐标得到y轴上的偏移量mYOffset = currentY - mLastY;if (mCurrentStatus != STATUS_LOADING) {//在y轴方向上滚动该控件changeScrollY(mYOffset);}//旋转Header中的箭头图标rotateHeaderArrow();//修改Header中的文本信息changeTips();//mLastY设置为这次的y轴坐标mLastY = currentY;break;case MotionEvent.ACTION_UP://下拉刷新的具体操作doRefresh();break;default:break;}// 返回true, 消费该事件return true;}private void rotateHeaderArrow() {}private void changeTips() {}/*** 修改y轴上的滚动值,从而实现Header被下拉的效果** @param distance 这次触摸事件的y轴与上一次的y轴的差值*/private void changeScrollY(int distance) {//最大值为scrollY(header隐藏),最小值为0(Header完全显示)int curY = getScrollY();//下拉if (distance > 0 && curY - distance > getPaddingTop()) {scrollBy(0, -distance);} else if (distance < 0 && curY - distance <= mInitScrollY) {//上拉过程scrollBy(0, -distance);}curY = getScrollY();int slop = mInitScrollY / 2;if (curY > 0 && curY < slop) {mCurrentStatus = STATUS_RELEASE_TO_REFRESH;} else if (curY > 0 && curY > slop) {mCurrentStatus = STATUS_PULL_TO_REFRESH;}}//执行下拉刷新private void doRefresh() {changeHeaderViewStatus();//执行刷新操作if (mCurrentStatus == STATUS_REFRESHING && mOnRefreshListener != null) {mOnRefreshListener.onRefresh();}}/*** 手指抬起时,根据用户下拉的高度来判断是否是有效的下拉刷新操作* 如果下拉的距离超过Header view的1/2* 那么则认为是有效的下拉刷新操作,否则恢复原来的视图状态*/private void changeHeaderViewStatus() {int curScrollY = getScrollY();// 超过1/2则认为是有效的下拉刷新,否则还原if (curScrollY < mInitScrollY / 2) {// 滚动到能够正常显示Header的位置mScroller.startScroll(getScrollX(), curScrollY, 0, mHeaderView.getPaddingTop() - curScrollY);mCurrentStatus = STATUS_REFRESHING;mTipsTextView.setText("refreshing");mArrowImageView.clearAnimation();mArrowImageView.setVisibility(View.GONE);mProgressBar.setVisibility(View.VISIBLE);} else {mScroller.startScroll(getScrollX(), curScrollY, 0, mInitScrollY - curScrollY);mCurrentStatus = STATUS_IDLE;}invalidate();}/*** 加载结束,恢复状态*/public void loadCompelte() {// 隐藏footerstartScroll(mInitScrollY - getScrollY());mCurrentStatus = STATUS_IDLE;}/*** 刷新结束,恢复状态*/public void refreshComplete() {mCurrentStatus = STATUS_IDLE;//隐藏Header viewmScroller.startScroll(getScrollX(), getScrollY(), 0, mInitScrollY - getScrollY());invalidate();updateHeaderTimeStamp();//  200毫秒后处理arrow和progressbar,免得太突兀this.postDelayed(new Runnable() {@Overridepublic void run() {mArrowImageView.setVisibility(View.VISIBLE);mProgressBar.setVisibility(View.GONE);}}, 100);}private void updateHeaderTimeStamp() {}@Overridepublic void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {//用户发设置了加载更多监听器,且到了最底部,并且是上拉操作,那么执行加载更多if (mLoadListener != null&& isBottom()&& mScroller.getCurrY() <= mInitScrollY&& mYOffset <= 0&& mCurrentStatus == STATUS_IDLE) {// 显示Footer ViewshowFooterView();//调用加载更多doLoadMore();}}/*** 设置滚动的参数*/private void startScroll(int yOffset) {mScroller.startScroll(getScrollX(), getScrollY(), 0, yOffset);invalidate();}//显示footer viewprivate void showFooterView() {startScroll(mFooterView.getMeasuredHeight());mCurrentStatus = STATUS_LOADING;}// 执行下拉(自动)加载更多的操作private void doLoadMore() {if (mLoadListener != null) {mLoadListener.onLoadMore();}}
}

在构造函数中调用initLayout函数初始化整个布局,从上到下分别为Header view、内容视图、Footer view,我们先看这3部分的相关函数:

    // 初始化整个布局,从上到下分别为header、内容视图、footerprivate void initLayout(Context context) {//设置header viewsetupHeaderView(context);//设置内容视图setupContentView(context);//设置布局参数setDefaultContentLayoutParams();//添加内容视图,如ListView、GridView等addView(mContentView);//footer viewsetupFooterView(context);}// 初始化header viewprotected void setupHeaderView(Context context) {mHeaderView = LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_header, this, false);mHeaderView.setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, mHeaderHeight));mHeaderView.setBackgroundColor(Color.RED);// header的高度为1/4的屏幕高度,但是,它只有100px是有效的显示区域// 取余paddingTop,这样是为了达到下拉的效果mHeaderView.setPadding(0, mHeaderHeight - 100, 0, 0);addView(mHeaderView);//初始化header view中的子视图mArrowImageView = mHeaderView.findViewById(R.id.pull_to_refresh_image);mTipsTextView = mHeaderView.findViewById(R.id.pull_to_refresh_text);mTimeTextView = mHeaderView.findViewById(R.id.pull_to_refresh_updated_at);mProgressBar = mHeaderView.findViewById(R.id.pull_to_refresh_progress);}// 初始化ContentView, 子类复写protected abstract void setupContentView(Context context);//初始化footerviewprotected void setupFooterView(Context context) {mFooterView = LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_fotter, this, false);addView(mFooterView);}

其中header view 和footer view都是从默认的布局中加载,因此,它们是固定的。但是,最中间的内容视图是可变的,例如,我们显示内容的控件可能是ListView、GridView、TextView等,因此,这部分是未知的,所以setContentView留给子类去具体化。还有两外两个抽象函数,分别为判断是否下拉到顶部已经上拉到底部的函数,因为不同内容视图判断是否滚动到顶部、底部的实现代码也是不一样,因此,也需要抽象化。函数定义如下:

    //是否已经到了最顶部,子类需要复写该方法,使得mContentView滑动到最顶端时返回true//如果达到最顶端用户继续下拉则拦截事件protected abstract boolean isTop();//是否已经到了最底部,子类需要复写该方法,使得mContentiew滑动到最底端时返回false//从而出发自动加载更多的操作protected abstract boolean isBottom();

初始化这3部分视图之后,接下来的第一个关键步骤就是视图测量与布局,也就是我们自定义ViewGroup中必备的两个步骤。上文我们已经说过,header view、内容视图、footer是纵向布局的,因此,需要将它们从上到下布局。在布局之前还要测量各个子视图的尺寸以下拉刷新组件自身的尺寸。代码如下:

/*** 丈量视图的宽、高。宽度为用户设置的宽度,高度则为header、content view、footer这三个子控件高度之和** @param widthMeasureSpec* @param heightMeasureSpec*/@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);// MeasureSpec中的宽度值int width = MeasureSpec.getSize(widthMeasureSpec);//子视图的个数int childCount = getChildCount();// 最终的高度int finalHeight = 0;for (int i = 0; i < childCount; i++) {View child = getChildAt(i);//测量每个子视图的尺寸measureChild(child, widthMeasureSpec, heightMeasureSpec);//所有子视图的高度和就是该下拉率新组件的总高度finalHeight += child.getMeasuredHeight();}//设置该下拉刷新组件的尺寸setMeasuredDimension(width, finalHeight);}/*** 布局函数,将header、content view、footer这3个View从上到下布局。* 布局完成后通过Scroller滚动到header的底部,即滚动距离为header的高度+本视图的paddingTop,从而达到隐藏header的效果** @param changed* @param l* @param t* @param r* @param b*/@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {int childCound = getChildCount();int left = getPaddingLeft();int top = getPaddingTop();for (int i = 0; i < childCound; i++) {View child = getChildAt(i);child.layout(left, top, child.getMeasuredWidth(), child.getMeasuredHeight() + top);top += child.getMeasuredHeight();}//计算初始化滑动的y轴距离mInitScrollY = mHeaderView.getMeasuredHeight() + getPaddingTop();//滑动header view高度的位置,从而达到隐藏header view的效果scrollTo(0, mInitScrollY);}

在onMeasure中我们测量了该组件自身的大小以及所有子视图的大小,并且将该控件设置为所有的子视图高度之和,在这里也就是header、content view、footer的高度之和,这样在布局时我们才有足够的控件竖向放置子视图。
在onLayout时,会将Header view、内容视图、Footer view从上到下布局,即Header view实际上显示该viewGroup向上滚动HeaderView 的高度,使得Header View变得不可见,如上文的图所示。当用户向下拉时候,该组件判断内容视图滑到了顶部,此时又通过Scroller将该组件向下滚动,使得Header View慢慢显示出来。实现这些功能就需要我们处理该控件的触摸事件,通过内容视图滚动到了顶部或者底部来判断是否需要拦截触摸事件。相关代码如下:

/*** 在适当的时候拦截触摸事件,这里指的适当的时候是当mContentView滑动到顶部,并且是下拉时拦截触摸事件,否则不拦截,交给child、view来处理*/@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {//  获取触摸事件的类型final int action = ev.getActionMasked();// 取消事件和抬起事件则直接返回falseif (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {return false;}switch (action) {case MotionEvent.ACTION_DOWN:mLastY = (int) ev.getRawY();break;case MotionEvent.ACTION_MOVE:mYOffset = (int) ev.getRawY() - mLastY;//如果拉到了顶部,并且是下拉,则拦截触摸事件//从而转到onTouchEvent来处理下拉刷新事件if (isTop() && mYOffset > 0) {return true;}break;}//默认不拦截触摸事件,使得该控件的子视图得到处理机会return false;}

onnterceptTouchEvent是ViewGroup中对触摸事件进行拦截的函数,当返回true时后续的触摸事件就会被该ViewGroup拦截,此时子视图将不会获得触摸事件。相应地,返回false则表示不进行拦截。例如在上述onInterceptTouchEvent函数中,我们在ACTION_DOWN事件(手指第一次按下)时记录了y轴的坐标,当用户的手指在屏幕上滑动时就会产生ACTION_MOVE事件,此时我们y轴坐标,并且与最初ACTION_DOWN事件的y轴相减。如果mYOffset大于0,那么表示用户的手指是从上到下滑动,如果此时内容视图已经是到了顶部,例如,ListView的第一个可见元素就是第一项,那么则返回true,也就是将后续的触摸事件拦截。此时,后续的ACTION_MOVE、ACTION_UP等事件就会又该组件进行处理,处理函数为onTouchEvent函数,代码如下:


/*** 在这里处理触摸事件以达到下拉刷新或者上拉自动加载的问题** @param event* @return*/@Overridepublic boolean onTouchEvent(MotionEvent event) {switch (event.getAction()) {//滑动事件case MotionEvent.ACTION_MOVE://获取手指触摸的当前y坐标int currentY = (int) event.getRawY();//当前坐标减去按下时的y坐标得到y轴上的偏移量mYOffset = currentY - mLastY;if (mCurrentStatus != STATUS_LOADING) {//在y轴方向上滚动该控件changeScrollY(mYOffset);}//旋转Header中的箭头图标rotateHeaderArrow();//修改Header中的文本信息changeTips();//mLastY设置为这次的y轴坐标mLastY = currentY;break;case MotionEvent.ACTION_UP://下拉刷新的具体操作doRefresh();break;default:break;}// 返回true, 消费该事件return true;}

在onTouchEvent函数中,我们会判断触摸事件的类型,如果还是ACTION_MOVE事件,那么计算当前触摸事件的y坐标与ACTION_DOWN时的y坐标的差值,然后调用changeScrollY函数在y轴上滚动该控件。如果用户一直向下滑动手指,那么mYOffset值将不断增大,那么此时该控件将不断地往上滚,Header View的可见高度也就越来越大。我们看看changeScrollY函数的实现

/*** 修改y轴上的滚动值,从而实现Header被下拉的效果** @param distance 这次触摸事件的y轴与上一次的y轴的差值*/
private void changeScrollY(int distance) {//最大值为scrollY(header隐藏),最小值为0(Header完全显示)int curY = getScrollY();//下拉if (distance > 0 && curY - distance > getPaddingTop()) {scrollBy(0, -distance);} else if (distance < 0 && curY - distance <= mInitScrollY) {//上拉过程scrollBy(0, -distance);}curY = getScrollY();int slop = mInitScrollY / 2;if (curY > 0 && curY < slop) {mCurrentStatus = STATUS_RELEASE_TO_REFRESH;} else if (curY > 0 && curY > slop) {mCurrentStatus = STATUS_PULL_TO_REFRESH;}
}

从上述程序中可以看到,changeSrollY函数实际上就是根据这一次与上一次y轴的差值来滚动当前控件,由于两次触摸事件的差值最小,因此,滚动起来相对比较流畅。当distance小于0时,则是向上滚动,此时Header View的可见范围越来越小,最后完全隐藏。当distrance大于0时则是向下滚动,此时Header View的可见乏味越来越大,这样一来也就实现了下拉时显示Header View效果。当然在下拉过来过程中,我们也会修改HeaderView布局中的一些控件状态,例如箭头的ImageView、文本信息等。
HeaderView 显示之后,当我们的额手指离开屏幕时,如果在y轴上的滚动高度大于HeaderView有效区域高度的二分之一,那么就会触发刷新操作,否则就会日通过Scroller将HeaderView再次隐藏起来。相关代码为ACTION_UP触摸事件中调用的doRefresh函数:

    //执行下拉刷新private void doRefresh() {changeHeaderViewStatus();//执行刷新操作if (mCurrentStatus == STATUS_REFRESHING && mOnRefreshListener != null) {mOnRefreshListener.onRefresh();}}/*** 手指抬起时,根据用户下拉的高度来判断是否是有效的下拉刷新操作* 如果下拉的距离超过Header view的1/2* 那么则认为是有效的下拉刷新操作,否则恢复原来的视图状态*/private void changeHeaderViewStatus() {int curScrollY = getScrollY();// 超过1/2则认为是有效的下拉刷新,否则还原if (curScrollY < mInitScrollY / 2) {// 滚动到能够正常显示Header的位置mScroller.startScroll(getScrollX(), curScrollY, 0, mHeaderView.getPaddingTop() - curScrollY);mCurrentStatus = STATUS_REFRESHING;mTipsTextView.setText("refreshing");mArrowImageView.clearAnimation();mArrowImageView.setVisibility(View.GONE);mProgressBar.setVisibility(View.VISIBLE);} else {mScroller.startScroll(getScrollX(), curScrollY, 0, mInitScrollY - curScrollY);mCurrentStatus = STATUS_IDLE;}invalidate();}

在changeHeaderViewStatus函数中,当判断为满足下拉刷新的条件时,就会设置当前组件的状态为STATUS_REFRESHING状态,并且设置正好显示HeaderView区域,最后调用OnRefreshListener实现用户设置的下拉刷新操作。刷新操作执行完成之后,用户需要调用refreshComplete函数告知当前控件刷新完毕,此时当前控件会将HeaderView隐藏。相关代码如下:

     * 刷新结束,恢复状态*/public void refreshComplete() {mCurrentStatus = STATUS_IDLE;//隐藏Header viewmScroller.startScroll(getScrollX(), getScrollY(), 0, mInitScrollY - getScrollY());invalidate();updateHeaderTimeStamp();//  200毫秒后处理arrow和progressbar,免得太突兀this.postDelayed(new Runnable() {@Overridepublic void run() {mArrowImageView.setVisibility(View.VISIBLE);mProgressBar.setVisibility(View.GONE);}}, 100);}

在refreshComplete中将重置控件的状态,并且将HeaderView滚动到屏幕之外。此时,整个下拉刷新操作就完成了。滚动到底部时加载更多比下拉刷新就要简单一些,只需要判断是否滚动到底部,如果已经到底部那么直接触发加载更多,因此,当前控件需要舰艇内容视图的滚动事件:

    @Overridepublic void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {//用户发设置了加载更多监听器,且到了最底部,并且是上拉操作,那么执行加载更多if (mLoadListener != null&& isBottom()&& mScroller.getCurrY() <= mInitScrollY&& mYOffset <= 0&& mCurrentStatus == STATUS_IDLE) {// 显示Footer ViewshowFooterView();//调用加载更多doLoadMore();}}/*** 设置滚动的参数*/private void startScroll(int yOffset) {mScroller.startScroll(getScrollX(), getScrollY(), 0, yOffset);invalidate();}//显示footer viewprivate void showFooterView() {startScroll(mFooterView.getMeasuredHeight());mCurrentStatus = STATUS_LOADING;}// 执行下拉(自动)加载更多的操作private void doLoadMore() {if (mLoadListener != null) {mLoadListener.onLoadMore();}}
public abstract class RefreshLayoutBase<T extends View> extends ViewGroup implements AbsListView.OnScrollListener

在onScroll中监听内容视图的滚动事件,当内容视图滚动到底部时显示FooterView,并且调用OnLoadListener回掉执行加载更多的操作。当操作执行完毕后用户需要调用loadComplete函数告知当前控件加载完毕,下拉刷新徐建此时隐藏FooterView并且设置为STATUS_IDLE状态。
这就是整个REgreshLayoutBase类的核心逻辑,下面我们看看具体实现类,例如内容视图时ListeView的实现

RefreshListView复写了RefreshLayotuBase的3个函数,分别设置内容视图、判断是否滚动到顶部、判断是否时滚动到底部。需要注意的时,在setcontentView函数中,我们将mContentView(在这里也就是ListView)的onScrollListener设置为this,这是因为需要监听ListView的滚动状态,当滚动到最后一项时触发加载个恩多操作。因为RefreshLayoutBase实现了onScrollListener接口,而判断是否调用加载更多的代码被封装在了RefreshLayoutBase类中,因此,在这里直接调用onContentView对象的SetOn Scroll Listener(this)即可。使用示例代码如下:

Scroller的使用相关推荐

  1. 代码解说Android Scroller、VelocityTracker

    在编写自己定义滑动控件时经常会用到Android触摸机制和Scroller及VelocityTracker.Android Touch系统简单介绍(二):实例具体解释onInterceptTouchE ...

  2. flex Scroller

    <?xml version="1.0" encoding="utf-8"?> <s:Application xmlns:fx="ht ...

  3. Android Scroller与computeScroll方法的调用关系

    2019独角兽企业重金招聘Python工程师标准>>> Android ViewGroup中的Scroller与computeScroll的有什么关系? 答:没有直接的关系 知道了答 ...

  4. 通过Scroller.js制作上拉加载和下拉刷新

    为什么80%的码农都做不了架构师?>>>    之前做移动端webAPP开发,一直是用的IScroll来做滚动列表,但是IScroll没有直接提供上下拉刷新的功能,虽然我们基于ISc ...

  5. Android Scroller简单用法

    Android里Scroller类是为了实现View平滑滚动的一个Helper类.通常在自定义的View时使用,在View中定义一个私有成员mScroller = new Scroller(conte ...

  6. Egret之eui.Scroller

    (纯码农制作 ,不喜勿喷)先看看结果 : 一 : Cell(render) Skin 的制作: 看结果是横向的滚动 , 向左 . 如果第一次显示第二个cell , 那么需向左滑动Cell的宽度 + C ...

  7. Android自己定义组件系列【2】——Scroller类

    在上一篇中介绍了View类的scrollTo和scrollBy两个方法,对这两个方法不太了解的朋友能够先看<自己定义View及ViewGroup> scrollTo和scrollBy尽管实 ...

  8. Android Scroller 使用详解

    1.Scroller到底是什么? 答:Scroller只是个计算器,提供插值计算,让滚动过程具有动画属性,但它并不是View,只是单纯地为滑动提供计算. 2.Scroller只是提供计算,那谁来让Vi ...

  9. 使用Scroller实现弹性滑动

    scrollTo.scrollBy View内部为了实现滑动提供了这两个方法,但是使用这两个方法滑动的效果是瞬间的不够平滑,如何实现View的弹性滑动呢?这正是本博文讨论的主题.另外这两个函数滑动的是 ...

  10. Scroller解析

    解决什么问题?出现的使命是? View.scrollTo及scrollBy方法是促使View内容的滑动,但其结果是瞬间达到目标位置,很生硬. Scroller就是提供一个策略来让这种生硬的移动变的平滑 ...

最新文章

  1. bug带来的兄弟感情
  2. wifiphisher 钓鱼工具的使用
  3. Javascript中的valueOf与toString
  4. 25 类:接口 抽象父类 多态 鸭子类型 格式化方法与析构方法 反射 异常处理 自定义异常 断言...
  5. c++ 输入输出流  ios::out 和ios::trunc有什么区别
  6. storm java开发环境搭建,看这里!Storm【单机版】环境搭建
  7. Angular应用的部署方式
  8. 在Windows XP3下搭建cocos2d-x-android开发环境
  9. web.xml中配置DispatcherServlet前端控制器和CharacterEncodingFilter字符过滤器后web-app标签显红报错
  10. 树莓派相机操作 —— luvcview 的安装、raspistill:摄像头命令
  11. CS231n-Numpy入门
  12. 淘宝帝国是如何创建的连载02
  13. Ubuntu 下安装 Vysor投屏软件
  14. linux下对IP地址的转发和端口的伪装----利用iptables部署
  15. 汽车加油问题 java_汽车加油问题
  16. css 设置层级关系,css层级关系怎么设置
  17. 拨号盘拨号数字间距太小 调大 修改通讯录里面收藏和所有联系人字体颜色
  18. 蓝牙电话通话机制原理
  19. 微信小程序 长按图片不出现菜单_HTML5微信长按图片不会弹出菜单的解决方法
  20. 无线 配置ldap 认证服务器,在无线局域网控制器wlcs上使用ldap的web认证配置示例-cisco.pdf...

热门文章

  1. 操作系统之哲学原理 第2版
  2. 华硕FL8000U拆换机械硬盘
  3. 贝叶斯统计第二版第五章答案_贝叶斯统计第二部分
  4. 算法工程师的一万小时定律
  5. Excel Vba拆分并填充单元格
  6. 乐学python视频资源_铁乐学python_day04-作业
  7. hexo+github搭建博客(超级详细版,精细入微)
  8. 以太网 STP临时环路的产生、STP BPDU的转发过程、根桥故障案例分析。
  9. h5活动是什么意思_H5活动页面可以有哪些作用
  10. 失焦事件及取消失焦事情