导语:
根据手势做自己想要的动画效果呈现到界面,是一件超级酷炫的事情!阅读本文需要你了解这几个知识点:
1、贝塞尔曲线绘制方法
2、差值器之DecelerateInterpolator
3、Touch事件拦截机制
4、手势滑动监听
5、View的动态布局
6、自定义View
一、绘制贝塞尔曲线
自定义WaveView,重写onDraw方法。
<pre><code>
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//重置画笔
path.reset();
path.lineTo(0, headHeight);
//绘制贝塞尔曲线
path.quadTo(getMeasuredWidth() / 2, headHeight + waveHeight, getMeasuredWidth(), headHeight);
path.lineTo(getMeasuredWidth(), 0);
canvas.drawPath(path, paint);
}
</pre></code>
可以看出绘制贝塞尔曲线用的path.quadTo方法:
quadTo(float x1, float x2, float y1, float y2)
x1,y1为控制点的坐标,x2,y2为终点坐标值。
headHeight为绘制区域头部矩形区域,waveHeight为贝塞尔曲线区域。
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);
//重置画笔
path.reset();
path.lineTo(0, headHeight);
//绘制贝塞尔曲线
path.quadTo(getMeasuredWidth() / 2, headHeight + waveHeight, getMeasuredWidth(), headHeight);
path.lineTo(getMeasuredWidth(), 0);
canvas.drawPath(path, paint);
}
}
</pre></code>
如果定义headHeigt=100,waveHeight=200,绘制出来的View如下:
二、动态布局
为了使下拉刷新控件适用任何布局,需要自定义一个布局,最好是继承FrameLayout布局,因为FrameLayout布局是叠加的。
在onAttachedToWindow方法中再新建一个FrameLayout,将下拉刷新头部的贝塞尔控件和文案显示控件放置里面,置顶。
<pre><code>
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
//添加一个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、子控件不能往上滑动
判断是否正在下拉可以用一个布尔值搞定
判断子控件是否能往上滑动需要我们去写一个方法
<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>
这个方法可以用来判断View是否可以往上滑动,这里讲View分成两类,一类是列表ListView控件,一类是普通的View类。ListView控件判断是否有孩子,并且第一孩子需要在界面呈现,并且第一孩子的顶部坐标要小于ListView控件的paddingTop值。普通View类可以根据sdk自带的canScrollVertically去判断,有兴趣可以去看看源码。
该方法为了兼容更多Android系统,建议修改成下面的代码:
<pre><code>
/**
* 用来判断是否可以上拉
*
* @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>
然后重写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; //计算当前滑动距离
if(y > 0 && !canChildScrollUp()) {
return true;
}
break;
}
return super.onInterceptTouchEvent(ev);
}
</pre></code>
手势滑动监听
监听手势滑动以及手势取消两个过程,即ACTION_MOVE和ACTION_CANCLE | ACTION_UP。
滑动过程主要根据滑动距离做动画效果,以及判断下拉刷新状态。滑动结束主要处理子控件的位置回归何处。当然,当onInterceptTouchEvent方法返回true,表示当前FrameLayout拦截Touch事件,触摸事件就会交给onTouch处理,所以重写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);
//计算滑动距离
float offsetY = decelerInterpolator.getInterpolation(y / WAVE_HEIGHT / 2) * y / 2;
//子控件移动同样距离
childView.setTranslationY(offsetY);
//控件高度
mFlayout.getLayoutParams().height = (int) offsetY;
mFlayout.requestLayout();
//贝塞尔曲线
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:
//如果滑动距离大于贝塞尔头部矩形区域高度,子控件回到矩形区域高度位置,否则子控件置顶
if(childView.getTranslationY() >= WAVE_HEAD_HEIGHT) {
setChildViewTransY(WAVE_HEAD_HEIGHT);
} else {
setChildViewTransY(0);
}
return true;
}
return super.onTouchEvent(event);
}
</pre></code>
差值器DecelerateInterpolator
该差值器实现的效果:在动画开始的地方快然后慢。这里就不再赘述其他差值器了,感兴趣可以去看看差值器的源码,需要懂些数学公式。
该下拉刷新控件两个地方用到DecelerateInterpolator差值器,下拉刷新的过程以及刷新完成后的控件位置回归过程。
<pre><code>
/**
* 控件滑动结束后回归动画
* @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>
运行效果
源码
<pre><code>
public class WaveFrameLayout extends FrameLayout {
FrameLayout mFlayout;
private boolean mIsRefreshing;//刷新的状态
private float mTouchY;//当前触摸位置
private float mCurrentY;//当前位置
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();
//添加一个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; //计算当前滑动距离
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);
//计算滑动距离
float offsetY = decelerInterpolator.getInterpolation(y / WAVE_HEIGHT / 2) * y / 2;
//子控件移动同样距离
childView.setTranslationY(offsetY);
//控件高度
mFlayout.getLayoutParams().height = (int) offsetY;
mFlayout.requestLayout();
//贝塞尔曲线
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:
//如果滑动距离大于贝塞尔头部矩形区域高度,子控件回到矩形区域高度位置,否则子控件置顶
if(childView.getTranslationY() >= WAVE_HEAD_HEIGHT) {
setChildViewTransY(WAVE_HEAD_HEIGHT);
} else {
setChildViewTransY(0);
}
return true;
}
return super.onTouchEvent(event);
}
/**
* 控件滑动结束后回归动画
* @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>
布局的格式调不来,注意一点就好,WaveView里面嵌套ScrollView或ListView,才能响应滑动监听。后续加入事件监听,下拉完成后的后续操作。