我想親手做一個(gè)刷新加載控件

其實(shí)很久以前就想自己實(shí)現(xiàn)一個(gè)下拉刷新上拉加載控件,之前一直用的是開源的控件,從一開始的PullToRefreshView到后來的SwipeRefreshLayout。雖然功能上都能實(shí)現(xiàn),但用別人的總感覺不得勁精绎。看到別的app上用的炫酷的刷新加載控件就是心癢癢锌妻。

1.美團(tuán)的是這樣的

美團(tuán).jpg
美團(tuán).jpg

2.京東的是這樣的

S61028-162146.jpg

3.新浪微博的是這樣的

微博.jpg
微博.jpg

看著都挺炫酷的代乃,那咱們是不是也可以搞一個(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)行起來的效果大概是這樣的:

刷新加載-1.gif
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末闲先,一起剝皮案震驚了整個(gè)濱河市状土,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌伺糠,老刑警劉巖蒙谓,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異训桶,居然都是意外死亡累驮,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門舵揭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來谤专,“玉大人,你說我怎么就攤上這事午绳≈檬蹋” “怎么了?”我有些...
    開封第一講書人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵箱叁,是天一觀的道長(zhǎng)墅垮。 經(jīng)常有香客問我,道長(zhǎng)耕漱,這世上最難降的妖魔是什么算色? 我笑而不...
    開封第一講書人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮螟够,結(jié)果婚禮上灾梦,老公的妹妹穿的比我還像新娘峡钓。我一直安慰自己,他們只是感情好若河,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開白布能岩。 她就那樣靜靜地躺著,像睡著了一般萧福。 火紅的嫁衣襯著肌膚如雪拉鹃。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,749評(píng)論 1 289
  • 那天鲫忍,我揣著相機(jī)與錄音膏燕,去河邊找鬼。 笑死悟民,一個(gè)胖子當(dāng)著我的面吹牛坝辫,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播射亏,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼近忙,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了智润?” 一聲冷哼從身側(cè)響起及舍,我...
    開封第一講書人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎做鹰,沒想到半個(gè)月后击纬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鼎姐,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡钾麸,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了炕桨。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片饭尝。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖献宫,靈堂內(nèi)的尸體忽然破棺而出钥平,到底是詐尸還是另有隱情,我是刑警寧澤姊途,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布涉瘾,位于F島的核電站,受9級(jí)特大地震影響捷兰,放射性物質(zhì)發(fā)生泄漏立叛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一贡茅、第九天 我趴在偏房一處隱蔽的房頂上張望秘蛇。 院中可真熱鬧其做,春花似錦、人聲如沸赁还。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)艘策。三九已至蹈胡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間朋蔫,已是汗流浹背审残。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留斑举,地道東北人搅轿。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像富玷,于是被迫代替她去往敵國(guó)和親璧坟。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容