前言
說(shuō)起下拉刷新和上拉加載大家應(yīng)該都不陌生。從ListView時(shí)代的PulltoRefreshView到RecycleView時(shí)代的SwipeRefreshLayout,再到Github上封裝好的適應(yīng)各種View,并且同時(shí)支持下拉刷新和上拉加載的庫(kù);對(duì)于實(shí)現(xiàn)這樣的一個(gè)功能已經(jīng)是輕車熟路了。但是正是由于這種情況,導(dǎo)致了以下幾個(gè)問(wèn)題。
- 實(shí)現(xiàn)的效果單一,沒(méi)有自身的特性
這一點(diǎn)使用過(guò)Google提供的SwipeRefreshLayout的同學(xué)應(yīng)該深有體會(huì),這個(gè)控件的確好用,把它套在RecycleView的外面,實(shí)現(xiàn)相應(yīng)的接口就可以很方便的實(shí)現(xiàn)下拉刷新的效果。但是它能夠?qū)崿F(xiàn)的交互效果也很單一,甚至說(shuō)有點(diǎn)無(wú)聊,基本上沒(méi)有可定制的內(nèi)容,我們能做的最多就是改一改旋轉(zhuǎn)的ProgressBar的顏色及其背景顏色。
- 集成三方控件,過(guò)于冗余
相信現(xiàn)在大部分人遇到列表都是用RecycleView來(lái)實(shí)現(xiàn),但是由于SwipeRefreshLayout沒(méi)有自帶上拉加載的功能,所以我們只能曲線救國(guó);要么復(fù)寫SwipeRefreshLayout自己加上上拉加載的功能;要么放棄使用SwipeRefreshLayout而是從github上找其他的實(shí)現(xiàn)方式,后者應(yīng)該是很多人的選擇;但是使用第三方控件,也會(huì)帶來(lái)額外的一些問(wèn)題,比如三方控件過(guò)于的重和龐大,功能過(guò)于復(fù)雜,有時(shí)候我們?yōu)榱四骋粋€(gè)簡(jiǎn)單的功能,可能要引入一些其他無(wú)用的代碼,這是不好的。
因此,我們需要一款輕量級(jí)的庫(kù),他能夠讓我們更自由的定制下拉加載和上拉刷新時(shí)的效果,同時(shí)又不會(huì)過(guò)于的繁重。
UltimateRefreshView
一個(gè)讓你關(guān)注于刷新動(dòng)畫(huà)效果的下拉刷新,上拉加載的控件。
老規(guī)矩,先來(lái)看看效果。
這里放了幾張能看清的動(dòng)畫(huà),這些動(dòng)畫(huà)效果完全不同;但是只用一個(gè)UltimateRefreshView就夠了。更多動(dòng)畫(huà)效果請(qǐng)到Github查看
功能
- 支持ListView,RecycleView,ScrollView,WebView
- 一行代碼指定是否支持上拉加載,下拉刷新
- 自由定制刷新時(shí)頭部和尾部的動(dòng)畫(huà)效果
使用方式
首先,是引入庫(kù)
compile 'com.reoobter:ultrapullview:1.0.0'
其次,實(shí)現(xiàn)各自的動(dòng)畫(huà)效果
這里我們就以美團(tuán)APP頂部下拉刷新的動(dòng)畫(huà)為例來(lái)看看如何實(shí)現(xiàn)動(dòng)畫(huà)效果
meituan_header_refresh_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:background="@color/white"
android:orientation="vertical">
<ImageView
android:id="@+id/loading"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_margin="10dp"
android:scaleX="0"
android:scaleY="0"
android:src="@drawable/pull_image"/>
</LinearLayout>
這個(gè)布局文件很簡(jiǎn)單,整個(gè)只有一個(gè)ImageView。我們的實(shí)現(xiàn)思路,就是在不同的結(jié)點(diǎn)修改ImageView的內(nèi)容,從而呈現(xiàn)出整個(gè)下拉刷新時(shí)所有的動(dòng)畫(huà)效果。那么這些結(jié)點(diǎn)是哪些呢?
public class MeiTuanHeaderAdapter extends BaseHeaderAdapter {
private ImageView loading;
private int viewHeight;
private float pull_distance=0;
public MeiTuanHeaderAdapter(Context context) {
super(context);
}
@Override
public View getHeaderView() {
View mView = mInflater.inflate(R.layout.meituan_header_refresh_layout, null, false);
loading = (ImageView) mView.findViewById(R.id.loading);
MeasureTools.measureView(mView);
viewHeight = mView.getMeasuredHeight();
return mView;
}
@Override
public void pullViewToRefresh(int deltaY) {
//這里乘以0.3 是因?yàn)閁ltimateRefreshView 源碼中對(duì)于滑動(dòng)有0.3的阻尼系數(shù),為了保持一致
pull_distance=pull_distance+deltaY*0.3f;
float scale = pull_distance / viewHeight;
loading.setScaleX(scale);
loading.setScaleY(scale);
}
@Override
public void releaseViewToRefresh(int deltaY) {
loading.setImageResource(R.drawable.mei_tuan_loading_pre);
AnimationDrawable mAnimationDrawable= (AnimationDrawable) loading.getDrawable();
mAnimationDrawable.start();
}
@Override
public void headerRefreshing() {
loading.setImageResource(R.drawable.mei_tuan_loading);
AnimationDrawable mAnimationDrawable= (AnimationDrawable) loading.getDrawable();
mAnimationDrawable.start();
}
@Override
public void headerRefreshComplete() {
loading.setImageResource(R.drawable.pull_image);
loading.setScaleX(0);
loading.setScaleY(0);
pull_distance=0;
}
}
通過(guò)代碼我們可以總結(jié)出有4個(gè)重要的結(jié)點(diǎn)
- 下拉進(jìn)行時(shí),這個(gè)時(shí)候隨著手指滑動(dòng),整個(gè)頂部的view逐漸顯示出來(lái)
- 頂部view完全被下拉出來(lái),這個(gè)時(shí)候頂部view已經(jīng)完全顯示出來(lái)了,手指釋放(抬起)后將進(jìn)入下一個(gè)結(jié)點(diǎn)。
- 正在刷新進(jìn)行時(shí),刷新進(jìn)行時(shí),這個(gè)結(jié)點(diǎn)就是刷新動(dòng)畫(huà)執(zhí)行的時(shí)候。
- 刷新完成,在這個(gè)結(jié)點(diǎn)觸發(fā)了刷新完成的動(dòng)作
為了實(shí)現(xiàn)美團(tuán)頂部刷新動(dòng)畫(huà)的效果,在第一個(gè)結(jié)點(diǎn)我們便開(kāi)始執(zhí)行動(dòng)畫(huà),根據(jù)刷新的位移比,使用scale動(dòng)畫(huà)逐漸放大初始圖片(綠色橢圓);在第二個(gè)結(jié)點(diǎn),這個(gè)結(jié)點(diǎn)一般都很短暫,這個(gè)時(shí)候頂部已經(jīng)完全展示,執(zhí)行了小人偶翻轉(zhuǎn)出現(xiàn)的動(dòng)畫(huà);在第三個(gè)結(jié)點(diǎn),這個(gè)結(jié)點(diǎn)一般是比較耗時(shí)的,在這里用幀動(dòng)畫(huà)播放了一個(gè)小人偶左右搖擺的動(dòng)畫(huà);最后,在第四個(gè)結(jié)點(diǎn),將所有內(nèi)容初始化到下拉之前的狀態(tài),方便下次下拉刷星動(dòng)畫(huà)的執(zhí)行。這樣就完成了一次下拉刷新的動(dòng)畫(huà)效果。
下面就可以非常方便的把這個(gè)動(dòng)畫(huà)效果運(yùn)用到View上。
最后,將動(dòng)畫(huà)效果適配到UltimateRefreshView之上
這里就以ListView為例。
首先是布局實(shí)現(xiàn):
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
tools:context=".subfragment.ListViewFragment"
>
<com.sak.ultilviewlib.UltimateRefreshView
android:id="@+id/refreshView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:orientation="vertical">
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="none"/>
</com.sak.ultilviewlib.UltimateRefreshView>
</FrameLayout>
布局文件很簡(jiǎn)單,將所要實(shí)現(xiàn)的下拉刷新的控件放在UltimateRefreshView控件內(nèi)即可。
public class ListViewFragment extends Fragment {
private UltimateRefreshView mUltimateRefreshView;
private int page = 0;
private int PER_PAGE_NUM = 15;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View view = inflater.inflate(R.layout.fragment_list_view, container, false);
initView(view);
return view;
}
private void initView(View view) {
View headview = LayoutInflater.from(getContext()).inflate(R.layout.list_headview_layout,
null, false);
ListView listView = (ListView) view.findViewById(R.id.listView);
final List<String> datas = new ArrayList<>();
for (int i = 0; i < PER_PAGE_NUM; i++) {
datas.add("this is item " + i);
}
final ArrayAdapter<String> adapter = new ArrayAdapter<String>(getContext(), android.R.layout.simple_list_item_1, datas);
listView.setAdapter(adapter);
listView.addHeaderView(headview);
mUltimateRefreshView = (UltimateRefreshView) view.findViewById(R.id.refreshView);
mUltimateRefreshView.setBaseHeaderAdapter(new MeiTuanHeaderAdapter(getContext()));
mUltimateRefreshView.setBaseFooterAdapter();
mUltimateRefreshView.setOnHeaderRefreshListener(new OnHeaderRefreshListener() {
@Override
public void onHeaderRefresh(UltimateRefreshView view) {
page = 0;
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
datas.clear();
for (int i = page * PER_PAGE_NUM; i < PER_PAGE_NUM; i++) {
datas.add("this is item " + i);
}
adapter.notifyDataSetChanged();
mUltimateRefreshView.onHeaderRefreshComplete();
}
}, 2000);
}
});
mUltimateRefreshView.setOnFooterRefreshListener(new OnFooterRefreshListener() {
@Override
public void onFooterRefresh(UltimateRefreshView view) {
page++;
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
for (int i = page * PER_PAGE_NUM; i < (page + 1) * PER_PAGE_NUM; i++) {
datas.add("this is item " + i);
}
adapter.notifyDataSetChanged();
mUltimateRefreshView.onFooterRefreshComplete();
}
}, 200);
}
});
}
}
這里最關(guān)鍵的代碼就是:
mUltimateRefreshView.setBaseHeaderAdapter(new MeiTuanHeaderAdapter(getContext()));
mUltimateRefreshView.setBaseFooterAdapter();
這樣,就為L(zhǎng)istView設(shè)定了下拉刷新和上拉加載時(shí)的動(dòng)畫(huà)效果。
為了方便使用,同時(shí)提供了無(wú)參的setAdapter方法,當(dāng)不提供參數(shù)時(shí),將使用默認(rèn)的動(dòng)畫(huà)效果。
這里,下拉刷新使用了我們剛才定義的MeiTuanHeaderAdapter這個(gè)效果,上拉加載的動(dòng)畫(huà)效果則使用了默認(rèn)的效果;當(dāng)然,如果你不需要上拉加載效果或下拉刷新效果的話,不設(shè)定對(duì)應(yīng)的Adapter即可。即不執(zhí)行(setBaseFooterAdapter和setBaseHeaderAdapter方法)
最后,為mUltimateRefreshView設(shè)置屬性動(dòng)畫(huà)執(zhí)行時(shí)的監(jiān)聽(tīng)器,這個(gè)回調(diào)方法會(huì)在刷新動(dòng)畫(huà)執(zhí)行時(shí)(也就是上面所說(shuō)的第三個(gè)結(jié)點(diǎn)時(shí)開(kāi)始執(zhí)行時(shí))被調(diào)用,在這個(gè)方法里我們一般會(huì)進(jìn)行網(wǎng)絡(luò)請(qǐng)求或一些耗時(shí)操作,這里我們用Handler模擬了一個(gè)簡(jiǎn)單的耗時(shí)任務(wù),當(dāng)耗時(shí)操作完成時(shí),調(diào)用對(duì)應(yīng)的刷新完成方法,這樣一次下拉刷新或者是上拉加載就執(zhí)行完了(也就是上面所說(shuō)的第四個(gè)結(jié)點(diǎn))。
這里重點(diǎn)討論整個(gè)刷新過(guò)程中動(dòng)畫(huà)效果的實(shí)現(xiàn),下拉或上拉時(shí)數(shù)據(jù)如何刷新不做重點(diǎn)分析。
當(dāng)我們需要下拉刷新動(dòng)畫(huà)時(shí),繼承BaseHeaderAdapter類并在各個(gè)方法(即下拉事件的各個(gè)結(jié)點(diǎn))按動(dòng)畫(huà)所需的效果,做不同的實(shí)現(xiàn)即可。
同理,當(dāng)需要上拉加載動(dòng)畫(huà)時(shí),繼承BaseFooterAdapter即可。BaseFooterAdapter類中結(jié)點(diǎn)的定義和BaseHeaderAdapter類中完全一致,只不過(guò)滑動(dòng)方向從下拉變成了上拉而已。這里以簡(jiǎn)單模仿一下京東上拉加載時(shí)的動(dòng)畫(huà)效果。
package com.sak.app.adapter;
import android.content.Context;
import android.view.View;
import android.widget.ImageView;
import com.bumptech.glide.Glide;
import com.sak.app.R;
import com.sak.ultilviewlib.adapter.BaseFooterAdapter;
/**
* Created by engineer on 2017/4/30.
*/
public class JDAppFooterAdapter extends BaseFooterAdapter {
private ImageView loading;
private Context mContext;
public JDAppFooterAdapter(Context context) {
super(context);
mContext=context;
}
@Override
public View getFooterView() {
View footerView = mInflater.inflate(R.layout.jd_footer_refresh_layout, null, false);
loading = (ImageView) footerView.findViewById(R.id.loading);
return footerView;
}
@Override
public void pullViewToRefresh(int deltaY) {
Glide.with(mContext).load(R.drawable.jd_loading_logo).into(loading);
}
@Override
public void releaseViewToRefresh(int deltaY) {
}
@Override
public void footerRefreshing() {
}
@Override
public void footerRefreshComplete() {
loading.setImageDrawable(null);
}
}
這里我們繼承了BaseFooterAdapter,可以看到他和BaseHeaderAdapter 十分相似。這里為了方便,沒(méi)有做很復(fù)雜的實(shí)現(xiàn),在上拉開(kāi)始執(zhí)行的時(shí)候,取巧的用Glide加載了一張gif 的動(dòng)畫(huà),這樣上拉時(shí)就會(huì)呈現(xiàn)一個(gè)簡(jiǎn)單的動(dòng)畫(huà)。現(xiàn)在用戶在上拉時(shí)一般都會(huì)很快完成分頁(yè)數(shù)據(jù)的加載,上拉動(dòng)畫(huà)的實(shí)現(xiàn)其實(shí)不用太復(fù)雜。
更多內(nèi)容可以參考源碼中demo的實(shí)現(xiàn)。
總結(jié)
看以看到,我們?cè)O(shè)置動(dòng)畫(huà)刷新的方法是在mUltimateRefreshView上執(zhí)行的,也就說(shuō)無(wú)論在mUltimateRefreshView內(nèi)部嵌套的是ListView,還是RecycleView或者ScrollView都一樣。頭部和尾部的動(dòng)畫(huà)效果完全不受嵌套子View的影響。因此,我們可以將更多的精力用于實(shí)現(xiàn)頭部和尾部刷新時(shí)絢麗的動(dòng)畫(huà)效果,而不再糾結(jié)于各種滑動(dòng)事件的處理。
頭部和尾部動(dòng)畫(huà)的實(shí)現(xiàn),完全和mUltimateRefreshView本身是分離的,極大的減少了耦合。需要什么樣的動(dòng)畫(huà),通過(guò)setBaseFooterAdapter和setBaseHeaderAdapter方法進(jìn)行設(shè)置即可,十分靈活,不同的頭部和尾部動(dòng)畫(huà)可以自由搭配。
所以,趕緊來(lái)玩吧!
最后,再次給出Github 源碼。歡迎感興趣的同學(xué) star & fork 。