Android自定義下拉刷新動畫--仿百度外賣下拉刷新
好久沒寫博客了,小編之前一段時間一直在找工作,從天津來到了我們的大帝都,感覺還不錯。好了廢話不多說了,開始我們今天的主題吧。現如今的APP各式各樣,同樣也帶來了各種需求,一個下拉刷新都能玩出花樣了,前兩天訂飯的時候不經意間看到了“百度外賣”的下拉刷新,今天的主題就是它–自定義下拉刷新動畫。
看一下實現效果吧:
動畫
我們先來看看Android中的動畫吧:
Android中的動畫分為三種:
- Tween動畫,這一類的動畫提供了旋轉、平移、縮放等效果。
- Alpha – 淡入淡出
- Scale – 縮放效果
- Roate – 旋轉效果
- Translate – 平移效果
- Frame動畫(幀動畫),這一類動畫可以創建一個Drawable序列,按照指定時間間歇一個一個顯示出來。
- Property動畫(屬性動畫),Android3.0之后引入出來的屬性動畫,它更改的是對象的實際屬性。
分析
我們可以看到百度外賣的下拉刷新的頭是一個騎車的快遞員在路上疾行,分析一下我們得到下面的動畫:
- 背景圖片的平移動畫
- 太陽的自旋轉動畫
- 兩個小輪子的自旋轉動畫
這就很簡單了,接下來我們去百度外面的圖片資源文件里找到這幾張圖片:(下載百度外賣的apk直接解壓即可)
定義下拉刷新頭文件:headview.xml
這里注意一下:我們定義了兩張背景圖片的ImageView是為了可以實現背景的平移動畫效果。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content">
<ImageView android:id="@+id/iv_back1" android:src="@drawable/pull_back" android:layout_width="match_parent" android:layout_height="100dp" />
<ImageView android:id="@+id/iv_back2" android:src="@drawable/pull_back" android:layout_width="match_parent" android:layout_height="100dp" />
<RelativeLayout android:id="@+id/main" android:layout_centerHorizontal="true" android:layout_width="wrap_content" android:layout_height="wrap_content">
<ImageView android:layout_marginTop="45dp" android:id="@+id/iv_rider" android:background="@drawable/pull_rider" android:layout_width="50dp" android:layout_height="50dp" />
<ImageView android:id="@+id/wheel1" android:layout_marginLeft="10dp" android:layout_marginTop="90dp" android:background="@drawable/pull_wheel" android:layout_width="15dp" android:layout_height="15dp" />
<ImageView android:id="@+id/wheel2" android:layout_marginLeft="40dp" android:layout_marginTop="90dp" android:background="@drawable/pull_wheel" android:layout_width="15dp" android:layout_height="15dp" />
</RelativeLayout>
<ImageView android:id="@+id/ivsun" android:layout_marginTop="20dp" android:layout_toRightOf="@+id/main" android:background="@drawable/pull_sun" android:layout_width="30dp" android:layout_height="30dp" />
</RelativeLayout>
接下來我們定義動畫效果:
背景圖片的平移效果:
實現兩個animation xml文件,一個起始位置在100%,結束位置在0%,設置repeat屬性為循環往復。
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" android:interpolator="@android:anim/accelerate_interpolator">
<translate android:fromXDelta="100%p" android:toXDelta="0%p" android:repeatMode="restart" android:interpolator="@android:anim/linear_interpolator" android:repeatCount="infinite" android:duration="5000" />
</set>
另一個起始位置在0%,結束位置在-100%
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" android:interpolator="@android:anim/accelerate_interpolator">
<translate android:fromXDelta="0%p" android:toXDelta="-100%p" android:repeatMode="restart" android:interpolator="@android:anim/linear_interpolator" android:repeatCount="infinite" android:duration="5000" />
</set>
太陽圍繞中心旋轉動畫:
從0-360度開始循環旋轉,旋轉所用時間為1s,旋轉中心距離view的左定點上邊緣為50%的距離,也就是正中心。
下面是具體屬性:
android:fromDegrees 起始的角度度數
android:toDegrees 結束的角度度數,負數表示逆時針,正數表示順時針。如10圈則比android:fromDegrees大3600即可
android:pivotX 旋轉中心的X坐標
浮點數或是百分比。浮點數表示相對于Object的左邊緣,如5; 百分比表示相對于Object的左邊緣,如5%; 另一種百分比表示相對于父容器的左邊緣,如5%p; 一般設置為50%表示在Object中心
android:pivotY 旋轉中心的Y坐標
浮點數或是百分比。浮點數表示相對于Object的上邊緣,如5; 百分比表示相對于Object的上邊緣,如5%; 另一種百分比表示相對于父容器的上邊緣,如5%p; 一般設置為50%表示在Object中心
android:duration 表示從android:fromDegrees轉動到android:toDegrees所花費的時間,單位為毫秒。可以用來計算速度。
android:interpolator表示變化率,但不是運行速度。一個插補屬性,可以將動畫效果設置為加速,減速,反復,反彈等。默認為開始和結束慢中間快,
android:startOffset 在調用start函數之后等待開始運行的時間,單位為毫秒,若為10,表示10ms后開始運行
android:repeatCount 重復的次數,默認為0,必須是int,可以為-1表示不停止
android:repeatMode 重復的模式,默認為restart,即重頭開始重新運行,可以為reverse即從結束開始向前重新運行。在android:repeatCount大于0或為infinite時生效
android:detachWallpaper 表示是否在壁紙上運行
android:zAdjustment 表示被animated的內容在運行時在z軸上的位置,默認為normal。
normal保持內容當前的z軸順序
top運行時在最頂層顯示
bottom運行時在最底層顯示
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<rotate android:fromDegrees="0" android:toDegrees="360" android:duration="1000" android:repeatCount="-1" android:pivotX="50%" android:pivotY="50%" />
</set>
同理輪子的動畫也一樣,不占代碼了。
動畫定義完了我們開始定義下拉刷新列表,下拉刷新網上有很多,不詳細的說了,簡單的改造一下,根據刷新狀態開啟關閉動畫即可。
注釋寫的很詳細,看一下代碼吧:
package com.hankkin.baidugoingrefreshlayout;
import android.widget.AbsListView;
import android.widget.ListView;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.ImageView;
import android.widget.RelativeLayout;
/** * Created by Hankkin on 16/4/10. */
public class BaiDuRefreshListView extends ListView implements AbsListView.OnScrollListener{
private static final int DONE = 0; //刷新完畢狀態
private static final int PULL_TO_REFRESH = 1; //下拉刷新狀態
private static final int RELEASE_TO_REFRESH = 2; //釋放狀態
private static final int REFRESHING = 3; //正在刷新狀態
private static final int RATIO = 3;
private RelativeLayout headView; //下拉刷新頭
private int headViewHeight; //頭高度
private float startY; //開始Y坐標
private float offsetY; //Y軸偏移量
private OnBaiduRefreshListener mOnRefreshListener; //刷新接口
private int state; //狀態值
private int mFirstVisibleItem; //第一項可見item索引
private boolean isRecord; //是否記錄
private boolean isEnd; //是否結束
private boolean isRefreable; //是否刷新
private ImageView ivWheel1,ivWheel2; //輪組圖片組件
private ImageView ivRider; //騎手圖片組件
private ImageView ivSun,ivBack1,ivBack2; //太陽、背景圖片1、背景圖片2
private Animation wheelAnimation,sunAnimation; //輪子、太陽動畫
private Animation backAnimation1,backAnimation2; //兩張背景圖動畫
public BaiDuRefreshListView(Context context) {
super(context);
init(context);
}
public BaiDuRefreshListView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public BaiDuRefreshListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
public interface OnBaiduRefreshListener{
void onRefresh();
}
/** * 回調接口,想實現下拉刷新的listview實現此接口 * @param onRefreshListener */
public void setOnBaiduRefreshListener(OnBaiduRefreshListener onRefreshListener){
mOnRefreshListener = onRefreshListener;
isRefreable = true;
}
/** * 刷新完畢,從主線程發送過來,并且改變headerView的狀態和文字動畫信息 */
public void setOnRefreshComplete(){
//一定要將isEnd設置為true,以便于下次的下拉刷新
isEnd = true;
state = DONE;
changeHeaderByState(state);
}
private void init(Context context) {
//關閉view的OverScroll
setOverScrollMode(OVER_SCROLL_NEVER);
setOnScrollListener(this);
//加載頭布局
headView = (RelativeLayout) LayoutInflater.from(context).inflate(R.layout.headview,this,false);
//測量頭布局
measureView(headView);
//給ListView添加頭布局
addHeaderView(headView);
//設置頭文件隱藏在ListView的第一項
headViewHeight = headView.getMeasuredHeight();
headView.setPadding(0, -headViewHeight, 0, 0);
//獲取頭布局圖片組件
ivRider = (ImageView) headView.findViewById(R.id.iv_rider);
ivSun = (ImageView) headView.findViewById(R.id.ivsun);
ivWheel1 = (ImageView) headView.findViewById(R.id.wheel1);
ivWheel2 = (ImageView) headView.findViewById(R.id.wheel2);
ivBack1 = (ImageView) headView.findViewById(R.id.iv_back1);
ivBack2 = (ImageView) headView.findViewById(R.id.iv_back2);
//獲取動畫
wheelAnimation = AnimationUtils.loadAnimation(context, R.anim.tip);
sunAnimation = AnimationUtils.loadAnimation(context, R.anim.tip1);
backAnimation1 = AnimationUtils.loadAnimation(context, R.anim.a);
backAnimation2 = AnimationUtils.loadAnimation(context, R.anim.b);
state = DONE;
isEnd = true;
isRefreable = false;
}
@Override
public void onScrollStateChanged(AbsListView absListView, int i) {
}
@Override
public void onScroll(AbsListView absListView, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
mFirstVisibleItem = firstVisibleItem;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (isEnd) {//如果現在時結束的狀態,即刷新完畢了,可以再次刷新了,在onRefreshComplete中設置
if (isRefreable) {//如果現在是可刷新狀態 在setOnMeiTuanListener中設置為true
switch (ev.getAction()){
//用戶按下
case MotionEvent.ACTION_DOWN:
//如果當前是在listview頂部并且沒有記錄y坐標
if (mFirstVisibleItem == 0 && !isRecord) {
//將isRecord置為true,說明現在已記錄y坐標
isRecord = true;
//將當前y坐標賦值給startY起始y坐標
startY = ev.getY();
}
break;
//用戶滑動
case MotionEvent.ACTION_MOVE:
//再次得到y坐標,用來和startY相減來計算offsetY位移值
float tempY = ev.getY();
//再起判斷一下是否為listview頂部并且沒有記錄y坐標
if (mFirstVisibleItem == 0 && !isRecord) {
isRecord = true;
startY = tempY;
}
//如果當前狀態不是正在刷新的狀態,并且已經記錄了y坐標
if (state!=REFRESHING && isRecord ) {
//計算y的偏移量
offsetY = tempY - startY;
//計算當前滑動的高度
float currentHeight = (-headViewHeight+offsetY/3);
//用當前滑動的高度和頭部headerView的總高度進行比 計算出當前滑動的百分比 0到1
float currentProgress = 1+currentHeight/headViewHeight;
//如果當前百分比大于1了,將其設置為1,目的是讓第一個狀態的橢圓不再繼續變大
if (currentProgress>=1) {
currentProgress = 1;
}
//如果當前的狀態是放開刷新,并且已經記錄y坐標
if (state == RELEASE_TO_REFRESH && isRecord) {
setSelection(0);
//如果當前滑動的距離小于headerView的總高度
if (-headViewHeight+offsetY/RATIO<0) {
//將狀態置為下拉刷新狀態
state = PULL_TO_REFRESH;
//根據狀態改變headerView,主要是更新動畫和文字等信息
changeHeaderByState(state);
//如果當前y的位移值小于0,即為headerView隱藏了
}else if (offsetY<=0) {
//將狀態變為done
state = DONE;
stopAnim();
//根據狀態改變headerView,主要是更新動畫和文字等信息
changeHeaderByState(state);
}
}
//如果當前狀態為下拉刷新并且已經記錄y坐標
if (state == PULL_TO_REFRESH && isRecord) {
setSelection(0);
//如果下拉距離大于等于headerView的總高度
if (-headViewHeight+offsetY/RATIO>=0) {
//將狀態變為放開刷新
state = RELEASE_TO_REFRESH;
//根據狀態改變headerView,主要是更新動畫和文字等信息
changeHeaderByState(state);
//如果當前y的位移值小于0,即為headerView隱藏了
}else if (offsetY<=0) {
//將狀態變為done
state = DONE;
//根據狀態改變headerView,主要是更新動畫和文字等信息
changeHeaderByState(state);
}
}
//如果當前狀態為done并且已經記錄y坐標
if (state == DONE && isRecord) {
//如果位移值大于0
if (offsetY>=0) {
//將狀態改為下拉刷新狀態
state = PULL_TO_REFRESH;
changeHeaderByState(state);
}
}
//如果為下拉刷新狀態
if (state == PULL_TO_REFRESH) {
//則改變headerView的padding來實現下拉的效果
headView.setPadding(0,(int)(-headViewHeight+offsetY/RATIO) ,0,0);
}
//如果為放開刷新狀態
if (state == RELEASE_TO_REFRESH) {
//改變headerView的padding值
headView.setPadding(0,(int)(-headViewHeight+offsetY/RATIO) ,0, 0);
}
}
break;
//當用戶手指抬起時
case MotionEvent.ACTION_UP:
//如果當前狀態為下拉刷新狀態
if (state == PULL_TO_REFRESH) {
//平滑的隱藏headerView
this.smoothScrollBy((int)(-headViewHeight+offsetY/RATIO)+headViewHeight, 500);
//根據狀態改變headerView
changeHeaderByState(state);
}
//如果當前狀態為放開刷新
if (state == RELEASE_TO_REFRESH) {
//平滑的滑到正好顯示headerView
this.smoothScrollBy((int)(-headViewHeight+offsetY/RATIO), 500);
//將當前狀態設置為正在刷新
state = REFRESHING;
//回調接口的onRefresh方法
mOnRefreshListener.onRefresh();
//根據狀態改變headerView
changeHeaderByState(state);
}
//這一套手勢執行完,一定別忘了將記錄y坐標的isRecord改為false,以便于下一次手勢的執行
isRecord = false;
break;
}
}
}
return super.onTouchEvent(ev);
}
/** * 根據狀態改變headerView的動畫和文字顯示 * @param state */
private void changeHeaderByState(int state){
switch (state) {
case DONE://如果的隱藏的狀態
//設置headerView的padding為隱藏
headView.setPadding(0, -headViewHeight, 0, 0);
startAnim();
break;
case RELEASE_TO_REFRESH://當前狀態為放開刷新
break;
case PULL_TO_REFRESH://當前狀態為下拉刷新
startAnim();
break;
case REFRESHING://當前狀態為正在刷新
break;
default:
break;
}
}
/** * 測量View * @param child */
private void measureView(View child) {
ViewGroup.LayoutParams p = child.getLayoutParams();
if (p == null) {
p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
int childWidthSpec = ViewGroup.getChildMeasureSpec(0, 0 + 0, p.width);
int lpHeight = p.height;
int childHeightSpec;
if (lpHeight > 0) {
childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight,
MeasureSpec.EXACTLY);
} else {
childHeightSpec = MeasureSpec.makeMeasureSpec(0,
MeasureSpec.UNSPECIFIED);
}
child.measure(childWidthSpec, childHeightSpec);
}
/** * 開啟動畫 */
public void startAnim(){
ivBack1.startAnimation(backAnimation1);
ivBack2.startAnimation(backAnimation2);
ivSun.startAnimation(sunAnimation);
ivWheel1.startAnimation(wheelAnimation);
ivWheel2.startAnimation(wheelAnimation);
}
/** * 關閉動畫 */
public void stopAnim(){
ivBack1.clearAnimation();
ivBack2.clearAnimation();
ivSun.clearAnimation();
ivWheel1.clearAnimation();
ivWheel2.clearAnimation();
}
}
好了,自定義下拉刷新動畫我們就實現了,其實很簡單,所有的下拉刷新動畫都類似這樣實現的。源碼我已經上傳到Github上了:
https://github.com/Hankkin/BaiduGoingRefreshLayout
求star啊。有不合理的地方還希望大家多多指正,共同進步哈。