其實(shí)很久以前就想自己實(shí)現(xiàn)一個(gè)下拉刷新上拉加載控件,之前一直用的是開源的控件,從一開始的PullToRefreshView到后來的SwipeRefreshLayout。雖然功能上都能實(shí)現(xiàn),但用別人的總感覺不得勁精绎。看到別的app上用的炫酷的刷新加載控件就是心癢癢锌妻。
1.美團(tuán)的是這樣的
2.京東的是這樣的
3.新浪微博的是這樣的
看著都挺炫酷的代乃,那咱們是不是也可以搞一個(gè)。既然要搞當(dāng)然是先選一個(gè)簡(jiǎn)單的來搞仿粹,畢竟誰(shuí)都喜歡撿軟柿子捏搁吓。那哪個(gè)最簡(jiǎn)單吶,不管你們認(rèn)為是哪個(gè)反正我覺得微博的看起來簡(jiǎn)單一點(diǎn)吭历。昨天下午趁著有點(diǎn)時(shí)間簡(jiǎn)單的實(shí)現(xiàn)了一下微博的刷新加載效果堕仔。先別急,咱一步一步慢慢來毒涧。首先觀察一下整體結(jié)構(gòu)贮预,界面分成三部分贝室。
- 頭部刷新布局
- 可滾動(dòng)控件(ListView契讲,RecycleView)
- 尾部加載布局
可以看成剛開始的時(shí)候頭部刷新布局和尾部都藏在可滾動(dòng)布局的下面,當(dāng)滾動(dòng)到上下邊界的時(shí)候才一點(diǎn)點(diǎn)顯示出來滑频,很容易想到通過繼承FrameLayout實(shí)現(xiàn)捡偏。為了提高擴(kuò)展性我們?cè)谡嬲念^部布局和尾部布局外面再套一層布局,看下面的具體代碼就能明白峡迷。在onAttachedToWindow方法中創(chuàng)建外層頭部尾部布局最為合適银伟,這時(shí)候自定義的控件已經(jīng)附著到根布局上。
<pre>
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
//添加頭部
if (mHeadLayout == null) {
FrameLayout headViewLayout = new FrameLayout(getContext());
LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0);
layoutParams.gravity = Gravity.TOP;
headViewLayout.setLayoutParams(layoutParams);
headViewLayout.setBackgroundColor(Color.parseColor("#F2F2F2"));
mHeadLayout = headViewLayout;
this.addView(mHeadLayout);
}
//添加底部
if (mBottomLayout == null) {
FrameLayout bottomViewLayout = new FrameLayout(getContext());
LayoutParams layoutParams2 = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0);
layoutParams2.gravity = Gravity.BOTTOM;
bottomViewLayout.setLayoutParams(layoutParams2);
bottomViewLayout.setBackgroundColor(Color.parseColor("#F2F2F2"));
mBottomLayout = bottomViewLayout;
this.addView(mBottomLayout);
}
mChildView = getChildAt(0);
if (mChildView == null) return;
}
</pre>
加上mHeadLayout == null和mBottomLayout == null的判斷是因?yàn)閳?zhí)行onResume會(huì)重新觸發(fā)onAttachedToWindow()重復(fù)創(chuàng)建HeadLayout和BottomLayout绘搞,接下就要分析刷新和加載的狀態(tài)
刷新狀態(tài):
- 下拉刷新
- 釋放刷新 (箭頭旋轉(zhuǎn))
- 正在刷新(隱藏箭頭彤避,顯示刷新動(dòng)畫)
加載狀態(tài):
- 加載中
- 加載結(jié)束
對(duì)應(yīng)的頭部刷新控件如下
<pre>
public class SinaRefreshView extends FrameLayout implements IHeaderView{
private ImageView refreshArrow;
private ImageView loadingView;
private TextView refreshTextView;
public SinaRefreshView(Context context) {
this(context, null);
}
public SinaRefreshView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SinaRefreshView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init(){
View rootView = View.inflate(getContext(), R.layout.layout_sinaheader, null);
refreshArrow = (ImageView) rootView.findViewById(R.id.iv_arrow);
refreshTextView = (TextView) rootView.findViewById(R.id.tv);
loadingView = (ImageView) rootView.findViewById(R.id.iv_loading);
addView(rootView);
}
private String pullDownStr = "下拉刷新";
private String releaseRefreshStr = "釋放刷新";
private String refreshingStr = "正在刷新";
@Override
public void onPullingDown(float fraction, float headHeight) {
if (fraction < 1f) refreshTextView.setText(pullDownStr);
if (fraction > 1f) refreshTextView.setText(releaseRefreshStr);
refreshArrow.setRotation(fraction * 180);
}
@Override
public void onPullReleasing(float fraction, float headHeight) {
if (fraction < 1f) {
refreshTextView.setText(pullDownStr);
refreshArrow.setRotation(fraction * 180);
if (refreshArrow.getVisibility() == GONE) {
refreshArrow.setVisibility(VISIBLE);
loadingView.setVisibility(GONE);
}
}
}
@Override
public void startAnim() {
refreshTextView.setText(refreshingStr);
refreshArrow.setVisibility(GONE);
loadingView.setVisibility(VISIBLE);
((AnimationDrawable)loadingView.getDrawable()).start();
}
}
</pre>
對(duì)應(yīng)的布局
<pre>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="horizontal"
>
<ImageView
android:id="@+id/iv_arrow"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_arrow"/>
<ImageView
android:id="@+id/iv_loading"
android:layout_width="34dp"
android:layout_height="34dp"
android:src="@drawable/anim_loading_view"
android:visibility="gone"/>
<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:text="下拉刷新"
android:textSize="16sp"/>
</LinearLayout>
</pre>
對(duì)應(yīng)的尾部加載控件
<pre>
public class LoadingView extends ImageView implements IBottomView{
public LoadingView(Context context) {
this(context, null);
}
public LoadingView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
int size = SettingUtil.dip2px(context,48);
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(size,size);
params.gravity = Gravity.CENTER;
setLayoutParams(params);
setImageResource(R.drawable.anim_loading_view);
}
@Override
public void startAnim() {
((AnimationDrawable)getDrawable()).start();
}
@Override
public void onFinish() {
((AnimationDrawable)getDrawable()).stop();
}
}
</pre>
接下來設(shè)置頭部布局和尾部布局,布局這一塊算是徹底搞定了
<pre>
public void setHeaderView(final IHeaderView headerView) {
if (headerView != null) {
mHeadLayout.removeAllViewsInLayout();
mHeadLayout.addView(headerView.getView());
mHeadView = headerView;
}
}
public void setBottomView(final IBottomView bottomView) {
if (bottomView != null) {
mBottomLayout.removeAllViewsInLayout();
mBottomLayout.addView(bottomView.getView());
mBottomView = bottomView;
}
}
</pre>
然后就是重點(diǎn)了夯辖,分析觸摸事件,這里用到兩個(gè)方法onInterceptTouchEvent和onTouchEvent琉预,什么時(shí)候打斷觸摸事件的傳遞,自己消耗
- 滾動(dòng)到上邊界蒿褂,滾動(dòng)的view無法再滾動(dòng)
- 滾動(dòng)到下邊界圆米,滾動(dòng)的view無法再滾動(dòng)
<pre>
/**
* 攔截觸摸事件
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
mTouchY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
float dy = event.getY() - mTouchY;
if (dy > 0 && !ScrollingUtil.canChildScrollUp(mChildView)){
state = PULL_DOWN_REFRESH;
return true;
}else if (dy < 0 && !ScrollingUtil.canChildScrollDown(mChildView)){
state = PULL_UP_LOAD;
return true;
}
break;
case MotionEvent.ACTION_UP:
break;
}
return super.onInterceptTouchEvent(event);
}
</pre>
這里可以通過ViewCompat.canScrollVertically(mChildView, 1);來判斷滾動(dòng)的view是否可以再滾動(dòng)。接下來就是消耗觸摸事件啄栓。
處于下拉刷新狀態(tài)的情況下:
- 隨著手指的下拉娄帖,同步刷新頭部布局的高度,以及通過setTranslationY()使得滾動(dòng)控件在Y方向上偏移昙楚,同時(shí)設(shè)置頭部控件的狀態(tài)近速。
- 手指釋放時(shí)判斷頭部布局的高度是否達(dá)到了刷新要求,若沒有達(dá)到刷新要求直接回滾。若達(dá)到了刷新要求先回到刷新高度数焊,然后執(zhí)行刷新動(dòng)畫以及刷新回調(diào)永淌。
處于上拉加載的情況的:
- 隨著手指的下拉,當(dāng)下拉距離小于設(shè)置的尾部高度時(shí)佩耳,同步刷新尾部布局的高度遂蛀,以及通過setTranslationY()使得滾動(dòng)控件在Y方向上偏移,同時(shí)設(shè)置尾部控件的狀態(tài)干厚。
- 手指釋放時(shí)判斷尾部布局的高度是否達(dá)到了加載要求李滴,若沒有達(dá)到加載要求直接回滾。若達(dá)到了加載要求蛮瞄,然后執(zhí)行加載動(dòng)畫以及加載回調(diào)所坯。
<pre>
/**-
觸摸事件
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
if (isRefreshing || isLoadingmore) return super.onTouchEvent(event);switch (event.getAction()){
case MotionEvent.ACTION_MOVE:
float dy = event.getY() - mTouchY;
float offsetY = dy/2;
if (state == PULL_DOWN_REFRESH) {
dy = Math.max(0, offsetY);
mChildView.setTranslationY(dy);
mHeadLayout.getLayoutParams().height = (int) dy;
mHeadLayout.requestLayout();
if(dy/refreshHeadHeight<1.02)
mHeadView.onPullingDown(dy/refreshHeadHeight,refreshHeadHeight);
}else if (state == PULL_UP_LOAD) {
if(offsetY>0){
break;
}
dy = Math.min(bottomHeight, Math.abs(offsetY));
dy = Math.max(0, dy);
mChildView.setTranslationY(-dy);
mBottomLayout.getLayoutParams().height = (int)dy;
mBottomLayout.requestLayout();
}
break;
case MotionEvent.ACTION_UP:
if (state == PULL_DOWN_REFRESH) {
if(mChildView.getTranslationY() >= refreshHeadHeight){
animChildView(refreshHeadHeight);
isRefreshing = true;
mHeadView.startAnim();
if(mOnRefreshListener!=null)
mOnRefreshListener.onRefresh(LXSRefreshLayout.this);
}else{
animChildView(0);
}
} else if (state == PULL_UP_LOAD) {
if(Math.abs(mChildView.getTranslationY()) >= bottomHeight){
isLoadingmore = true;
mBottomView.startAnim();
animChildView(-bottomHeight);
if(mOnRefreshListener!=null)
mOnRefreshListener.onLoadMore(LXSRefreshLayout.this);
}else{
animChildView(0);
}
}
break;
}
return super.onTouchEvent(event);
}
</pre>
-
這里有兩個(gè)注意點(diǎn):
- 當(dāng)處于刷新和加載狀態(tài)下時(shí)消耗掉觸摸事件
- move過程需要判斷一下是否滑動(dòng)到了邊緣,不然會(huì)有問題
當(dāng)沒有達(dá)到刷新或則加載要求的時(shí)候需要回滾挂捅,直接通過mChildView.setTranslationY(0)太過于生硬芹助,這邊通過屬性動(dòng)畫過度
<pre>
private void animChildView(float endValue, long duration) {
ObjectAnimator oa = ObjectAnimator.ofFloat(mChildView, "translationY", mChildView.getTranslationY(), endValue);
oa.setDuration(duration);
oa.setInterpolator(new DecelerateInterpolator());//設(shè)置速率為遞減
oa.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int height = (int) mChildView.getTranslationY();
if (state == PULL_DOWN_REFRESH) {
mHeadLayout.getLayoutParams().height = height;
mHeadLayout.requestLayout();
mHeadView.onPullReleasing(height/refreshHeadHeight,refreshHeadHeight,refreshHeadHeight);
}else if (state == PULL_UP_LOAD) {
mBottomLayout.getLayoutParams().height = -height;
mBottomLayout.requestLayout();
}
}
});
oa.start();
}
</pre>
最后是設(shè)置回調(diào)接口以及刷險(xiǎn)加載結(jié)束的處理方法
<pre>
/**
* 刷新結(jié)束
*/
public void finishRefreshing() {
isRefreshing = false;
if (mChildView != null) {
animChildView(0f);
}
}
/**
* 加載更多結(jié)束
*/
public void finishLoadmore() {
isLoadingmore = false;
if (mChildView != null) {
animChildView(0f);
mBottomView.onFinish();
}
}
public void setOnRefreshListener(OnRefreshListener onRefreshListener) {
mOnRefreshListener = onRefreshListener;
}
public interface OnRefreshListener{
void onRefresh(LXSRefreshLayout refreshLayout);
void onLoadMore(LXSRefreshLayout refreshLayout);
}
</pre>
使用方法跟SwipeRefreshLayout一模一樣,運(yùn)行起來的效果大概是這樣的: