導(dǎo)語(yǔ):
根據(jù)手勢(shì)做自己想要的動(dòng)畫(huà)效果呈現(xiàn)到界面,是一件超級(jí)酷炫的事情新博!閱讀本文需要你了解這幾個(gè)知識(shí)點(diǎn):
1语稠、貝塞爾曲線(xiàn)繪制方法
2、差值器之DecelerateInterpolator
3睦裳、Touch事件攔截機(jī)制
4造锅、手勢(shì)滑動(dòng)監(jiān)聽(tīng)
5廉邑、View的動(dòng)態(tài)布局
6糙箍、自定義View
一牵祟、繪制貝塞爾曲線(xiàn)
自定義WaveView诺苹,重寫(xiě)onDraw方法筝尾。
<pre><code>
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//重置畫(huà)筆
path.reset();
path.lineTo(0, headHeight);
//繪制貝塞爾曲線(xiàn)
path.quadTo(getMeasuredWidth() / 2, headHeight + waveHeight, getMeasuredWidth(), headHeight);
path.lineTo(getMeasuredWidth(), 0);
canvas.drawPath(path, paint);
}
</pre></code>
可以看出繪制貝塞爾曲線(xiàn)用的path.quadTo方法:
quadTo(float x1, float x2, float y1, float y2)
x1,y1為控制點(diǎn)的坐標(biāo)站辉,x2,y2為終點(diǎn)坐標(biāo)值饰剥。
headHeight為繪制區(qū)域頭部矩形區(qū)域汰蓉,waveHeight為貝塞爾曲線(xiàn)區(qū)域顾孽。
WaveView的代碼如下:
<pre><code>
public class WaveView extends View {
private int waveHeight;
private int headHeight;
private Path path;
private Paint paint;
private int color;
public WaveView(Context context) {
this(context, null, 0);
}
public WaveView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public WaveView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
path = new Path();
paint = new Paint();
paint.setColor(Color.argb(150, 43, 43, 43));
paint.setAntiAlias(true);
}
public void setColor(int color) {
this.color = color;
paint.setColor(color);
invalidate();
}
public int getHeadHeight() {
return headHeight;
}
public void setHeadHeight(int headHeight) {
this.headHeight = headHeight;
}
public int getWaveHeight() {
return waveHeight;
}
public void setWaveHeight(int waveHeight) {
this.waveHeight = waveHeight;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//重置畫(huà)筆
path.reset();
path.lineTo(0, headHeight);
//繪制貝塞爾曲線(xiàn)
path.quadTo(getMeasuredWidth() / 2, headHeight + waveHeight, getMeasuredWidth(), headHeight);
path.lineTo(getMeasuredWidth(), 0);
canvas.drawPath(path, paint);
}
}
</pre></code>
如果定義headHeigt=100,waveHeight=200拦英,繪制出來(lái)的View如下:
二、動(dòng)態(tài)布局
為了使下拉刷新控件適用任何布局霎冯,需要自定義一個(gè)布局,最好是繼承FrameLayout布局慷荔,因?yàn)镕rameLayout布局是疊加的拧廊。
在onAttachedToWindow方法中再新建一個(gè)FrameLayout吧碾,將下拉刷新頭部的貝塞爾控件和文案顯示控件放置里面倦春,置頂睁本。
<pre><code>
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
//添加一個(gè)FrameLayout布局
mFlayout = new FrameLayout(getContext());
LayoutParams lp = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0);
lp.gravity = Gravity.TOP;
mFlayout.setLayoutParams(lp);
this.addView(mFlayout);
//頭部貝塞爾控件和文案顯示控件
View refreshView = LayoutInflater.from(getContext()).inflate(R.layout.fresh_layout, null);
txtRefresh = (TextView) refreshView.findViewById(R.id.txtRefresh);
waveView = (WaveView) refreshView.findViewById(R.id.wave);
waveView.setWaveHeight(WAVE_HEIGHT);
waveView.setHeadHeight(WAVE_HEAD_HEIGHT);
waveView.invalidate();
mFlayout.addView(refreshView);
//獲取子控件
childView = getChildAt(0);
}
</pre></code>
三、Touch事件攔截
下拉刷新凡泣,事件攔截有如下兩種情況:
1骂维、正在下拉中
2贺纲、子控件不能往上滑動(dòng)
判斷是否正在下拉可以用一個(gè)布爾值搞定
判斷子控件是否能往上滑動(dòng)需要我們?nèi)?xiě)一個(gè)方法
<pre><code>
/**
* 判斷是否可以上拉
* @return
*/
private boolean canChildScrollUp() {
if(childView instanceof AbsListView) {
AbsListView absLv = (AbsListView) childView;
return absLv.getChildCount() > 0
&& (absLv.getFirstVisiblePosition() > 0 || absLv.getChildAt(0).getTop() < absLv.getPaddingTop());
} else {
return ViewCompat.canScrollVertically(childView, -1);
}
}
</pre></code>
這個(gè)方法可以用來(lái)判斷View是否可以往上滑動(dòng)侮措,這里講View分成兩類(lèi)福铅,一類(lèi)是列表ListView控件滑黔,一類(lèi)是普通的View類(lèi)略荡。ListView控件判斷是否有孩子汛兜,并且第一孩子需要在界面呈現(xiàn)粥谬,并且第一孩子的頂部坐標(biāo)要小于ListView控件的paddingTop值漏策。普通View類(lèi)可以根據(jù)sdk自帶的canScrollVertically去判斷,有興趣可以去看看源碼储矩。
該方法為了兼容更多Android系統(tǒng)即硼,建議修改成下面的代碼:
<pre><code>
/**
* 用來(lái)判斷是否可以上拉
*
* @return boolean
*/
public boolean canChildScrollUp() {
if (mChildView == null) {
return false;
}
if (Build.VERSION.SDK_INT < 14) {
if (mChildView instanceof AbsListView) {
final AbsListView absListView = (AbsListView) mChildView;
return absListView.getChildCount() > 0
&& (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
.getTop() < absListView.getPaddingTop());
} else {
return ViewCompat.canScrollVertically(mChildView, -1) || mChildView.getScrollY() > 0;
}
} else {
return ViewCompat.canScrollVertically(mChildView, -1);
}
}
</pre></code>
然后重寫(xiě)onInterceptTouchEvent方法只酥,完善Touch事件攔截
<pre><code>
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(mIsRefreshing) {
return true; //如果下拉刷新,則攔截
}
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mTouchY = ev.getY();
mCurrentY = mTouchY;
case MotionEvent.ACTION_MOVE:
float currentY = ev.getY();
float y = currentY - mTouchY; //計(jì)算當(dāng)前滑動(dòng)距離
if(y > 0 && !canChildScrollUp()) {
return true;
}
break;
}
return super.onInterceptTouchEvent(ev);
}
</pre></code>
手勢(shì)滑動(dòng)監(jiān)聽(tīng)
監(jiān)聽(tīng)手勢(shì)滑動(dòng)以及手勢(shì)取消兩個(gè)過(guò)程,即ACTION_MOVE和ACTION_CANCLE | ACTION_UP瓮增。
滑動(dòng)過(guò)程主要根據(jù)滑動(dòng)距離做動(dòng)畫(huà)效果,以及判斷下拉刷新?tīng)顟B(tài)拳恋∶耍滑動(dòng)結(jié)束主要處理子控件的位置回歸何處。當(dāng)然轰驳,當(dāng)onInterceptTouchEvent方法返回true弟灼,表示當(dāng)前FrameLayout攔截Touch事件田绑,觸摸事件就會(huì)交給onTouch處理俺陋,所以重寫(xiě)onTouch方法如下:
<pre><code>
public boolean onTouchEvent(MotionEvent event) {
if(mIsRefreshing) {
return super.onTouchEvent(event);
}
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
float currentY = event.getY();
float y = currentY - mTouchY;
y = Math.min(WAVE_HEIGHT * 2, y);
y = Math.max(0, y);
//計(jì)算滑動(dòng)距離
float offsetY = decelerInterpolator.getInterpolation(y / WAVE_HEIGHT / 2) * y / 2;
//子控件移動(dòng)同樣距離
childView.setTranslationY(offsetY);
//控件高度
mFlayout.getLayoutParams().height = (int) offsetY;
mFlayout.requestLayout();
//貝塞爾曲線(xiàn)
float fraction = offsetY / WAVE_HEAD_HEIGHT;
waveView.setHeadHeight((int) (WAVE_HEAD_HEIGHT * limitValue(1, fraction)));
waveView.setWaveHeight((int) (WAVE_HEIGHT * Math.max(0, fraction - 1)));
waveView.invalidate();
if(WAVE_HEAD_HEIGHT > WAVE_HEAD_HEIGHT * limitValue(1, fraction)) {
txtRefresh.setText("下拉刷新");
} else {
txtRefresh.setText("釋放刷新");
}
return true;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
//如果滑動(dòng)距離大于貝塞爾頭部矩形區(qū)域高度,子控件回到矩形區(qū)域高度位置苔可,否則子控件置頂
if(childView.getTranslationY() >= WAVE_HEAD_HEIGHT) {
setChildViewTransY(WAVE_HEAD_HEIGHT);
} else {
setChildViewTransY(0);
}
return true;
}
return super.onTouchEvent(event);
}
</pre></code>
差值器DecelerateInterpolator
該差值器實(shí)現(xiàn)的效果:在動(dòng)畫(huà)開(kāi)始的地方快然后慢缴挖。這里就不再贅述其他差值器了,感興趣可以去看看差值器的源碼焚辅,需要懂些數(shù)學(xué)公式映屋。
該下拉刷新控件兩個(gè)地方用到DecelerateInterpolator差值器,下拉刷新的過(guò)程以及刷新完成后的控件位置回歸過(guò)程同蜻。
<pre><code>
/**
* 控件滑動(dòng)結(jié)束后回歸動(dòng)畫(huà)
* @param values
*/
private void setChildViewTransY(float... values) {
ObjectAnimator ani = ObjectAnimator.ofFloat(childView, View.TRANSLATION_Y, values);
ani.setInterpolator(new DecelerateInterpolator());
ani.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int height = (int) childView.getTranslationY();
mFlayout.getLayoutParams().height = height;
mFlayout.requestLayout();
}
});
ani.start();
}
</pre></code>
運(yùn)行效果
源碼
<pre><code>
public class WaveFrameLayout extends FrameLayout {
FrameLayout mFlayout;
private boolean mIsRefreshing;//刷新的狀態(tài)
private float mTouchY;//當(dāng)前觸摸位置
private float mCurrentY;//當(dāng)前位置
private View childView;
private WaveView waveView;
TextView txtRefresh;
private final int WAVE_HEIGHT = 200;
private final int WAVE_HEAD_HEIGHT = 100;
private DecelerateInterpolator decelerInterpolator;
public WaveFrameLayout(Context context) {
super(context);
init(context);
}
public WaveFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public WaveFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
if (isInEditMode()) {
return;
}
decelerInterpolator = new DecelerateInterpolator(10);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
//添加一個(gè)FrameLayout布局
mFlayout = new FrameLayout(getContext());
LayoutParams lp = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0);
lp.gravity = Gravity.TOP;
mFlayout.setLayoutParams(lp);
this.addView(mFlayout);
//頭部貝塞爾控件和文案顯示控件
View refreshView = LayoutInflater.from(getContext()).inflate(R.layout.fresh_layout, null);
txtRefresh = (TextView) refreshView.findViewById(R.id.txtRefresh);
waveView = (WaveView) refreshView.findViewById(R.id.wave);
waveView.setWaveHeight(WAVE_HEIGHT);
waveView.setHeadHeight(WAVE_HEAD_HEIGHT);
waveView.invalidate();
mFlayout.addView(refreshView);
//獲取子控件
childView = getChildAt(0);
}
/**
* 判斷是否可以上拉
* @return
*/
private boolean canChildScrollUp() {
if(childView instanceof AbsListView) {
AbsListView absLv = (AbsListView) childView;
return absLv.getChildCount() > 0
&& (absLv.getFirstVisiblePosition() > 0 || absLv.getChildAt(0).getTop() < absLv.getPaddingTop());
} else {
return ViewCompat.canScrollVertically(childView, -1);
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(mIsRefreshing) {
return true; //如果下拉刷新棚点,則攔截
}
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mTouchY = ev.getY();
mCurrentY = mTouchY;
case MotionEvent.ACTION_MOVE:
float currentY = ev.getY();
float y = currentY - mTouchY; //計(jì)算當(dāng)前滑動(dòng)距離
if(y > 0 && !canChildScrollUp()) {
return true;
}
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if(mIsRefreshing) {
return super.onTouchEvent(event);
}
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
float currentY = event.getY();
float y = currentY - mTouchY;
y = Math.min(WAVE_HEIGHT * 2, y);
y = Math.max(0, y);
//計(jì)算滑動(dòng)距離
float offsetY = decelerInterpolator.getInterpolation(y / WAVE_HEIGHT / 2) * y / 2;
//子控件移動(dòng)同樣距離
childView.setTranslationY(offsetY);
//控件高度
mFlayout.getLayoutParams().height = (int) offsetY;
mFlayout.requestLayout();
//貝塞爾曲線(xiàn)
float fraction = offsetY / WAVE_HEAD_HEIGHT;
waveView.setHeadHeight((int) (WAVE_HEAD_HEIGHT * limitValue(1, fraction)));
waveView.setWaveHeight((int) (WAVE_HEIGHT * Math.max(0, fraction - 1)));
waveView.invalidate();
if(WAVE_HEAD_HEIGHT > WAVE_HEAD_HEIGHT * limitValue(1, fraction)) {
txtRefresh.setText("下拉刷新");
} else {
txtRefresh.setText("釋放刷新");
}
return true;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
//如果滑動(dòng)距離大于貝塞爾頭部矩形區(qū)域高度瘫析,子控件回到矩形區(qū)域高度位置,否則子控件置頂
if(childView.getTranslationY() >= WAVE_HEAD_HEIGHT) {
setChildViewTransY(WAVE_HEAD_HEIGHT);
} else {
setChildViewTransY(0);
}
return true;
}
return super.onTouchEvent(event);
}
/**
* 控件滑動(dòng)結(jié)束后回歸動(dòng)畫(huà)
* @param values
*/
private void setChildViewTransY(float... values) {
ObjectAnimator ani = ObjectAnimator.ofFloat(childView, View.TRANSLATION_Y, values);
ani.setInterpolator(new DecelerateInterpolator());
ani.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int height = (int) childView.getTranslationY();
mFlayout.getLayoutParams().height = height;
mFlayout.requestLayout();
}
});
ani.start();
}
/**
* 限定值
*/
public float limitValue(float a, float b) {
float valve = 0;
final float min = Math.min(a, b);
final float max = Math.max(a, b);
valve = valve > min ? valve : min;
valve = valve < max ? valve : max;
return valve;
}
}
</pre></code>
布局代碼
<pre><code>
<?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="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@color/colorPrimary"
android:gravity="center"
android:text="下拉控件"
android:textColor="@color/colorAccent" />
<WaveFrameLayout
android:id="@+id/waveFlayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/txtShow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="hello world!" />
</LinearLayout>
</ScrollView>
</WaveFrameLayout>
</LinearLayout>
</pre></code>
布局的格式調(diào)不來(lái),注意一點(diǎn)就好,WaveView里面嵌套ScrollView或ListView,才能響應(yīng)滑動(dòng)監(jiān)聽(tīng)愈犹。后續(xù)加入事件監(jiān)聽(tīng)勋锤,下拉完成后的后續(xù)操作。