自定义View我的Andorid 收藏Android开发

贝塞尔曲线下拉控件动画效果实现

2017-04-23  本文已影响519人  笑漫人生
导语:

根据手势做自己想要的动画效果呈现到界面,是一件超级酷炫的事情!阅读本文需要你了解这几个知识点:

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如下:

Paste_Image.png

二、动态布局

为了使下拉刷新控件适用任何布局,需要自定义一个布局,最好是继承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,才能响应滑动监听。后续加入事件监听,下拉完成后的后续操作。

上一篇下一篇

猜你喜欢

热点阅读