热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

导语:根据手势做自己想要的动画效果呈现到界面,是一件超级酷炫的事情!阅读本文需要你了解这几个知识点:1、贝塞尔曲线绘制方法2、差值器之DecelerateInterpolator3
导语:

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

1、贝塞尔曲线绘制方法
2、差值器之DecelerateInterpolator
3、Touch事件拦截机制
4、手势滑动监听
5、View的动态布局
6、自定义View

一、绘制贝塞尔曲线

自定义WaveView,重写onDraw方法。


@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);
}

可以看出绘制贝塞尔曲线用的path.quadTo方法:

quadTo(float x1, float x2, float y1, float y2)
x1,y1为控制点的坐标,x2,y2为终点坐标值。

headHeight为绘制区域头部矩形区域,waveHeight为贝塞尔曲线区域。

WaveView的代码如下:


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);
}

}

如果定义headHeigt=100,wavehttps://img.php1.cn/3cd4a/1eebe/cd5/8343fdbffb0056b5.webp" src="https://img.php1.cn/3cd4a/1eebe/cd5/8343fdbffb0056b5.webp" alt="《贝塞尔曲线下拉控件动画效果实现》" /> Paste_Image.png

二、动态布局

为了使下拉刷新控件适用任何布局,需要自定义一个布局,最好是继承FrameLayout布局,因为FrameLayout布局是叠加的。
在onAttachedToWindow方法中再新建一个FrameLayout,将下拉刷新头部的贝塞尔控件和文案显示控件放置里面,置顶。


@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);
}

三、Touch事件拦截

下拉刷新,事件拦截有如下两种情况:

1、正在下拉中
2、子控件不能往上滑动

判断是否正在下拉可以用一个布尔值搞定
判断子控件是否能往上滑动需要我们去写一个方法


/**
* 判断是否可以上拉
* @return
*/
private boolean canChildScrollUp() {
if(childView instanceof AbsListView) {
AbsListView absLv = (AbsListView) childView;
return absLv.getChildCount() > 0
&& (absLv.getFirstVisiblePosition() > 0 || absLv.getChildAt(0).getTop() } else {
return ViewCompat.canScrollVertically(childView, -1);
}
}

这个方法可以用来判断View是否可以往上滑动,这里讲View分成两类,一类是列表ListView控件,一类是普通的View类。ListView控件判断是否有孩子,并且第一孩子需要在界面呈现,并且第一孩子的顶部坐标要小于ListView控件的paddingTop值。普通View类可以根据sdk自带的canScrollVertically去判断,有兴趣可以去看看源码。

该方法为了兼容更多Android系统,建议修改成下面的代码:


/**
* 用来判断是否可以上拉
*
* @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() } else {
return ViewCompat.canScrollVertically(mChildView, -1) || mChildView.getScrollY() > 0;
}
} else {
return ViewCompat.canScrollVertically(mChildView, -1);
}
}

然后重写onInterceptTouchEvent方法,完善Touch事件拦截


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);
}

手势滑动监听

监听手势滑动以及手势取消两个过程,即ACTION_MOVE和ACTION_CANCLE | ACTION_UP。
滑动过程主要根据滑动距离做动画效果,以及判断下拉刷新状态。滑动结束主要处理子控件的位置回归何处。当然,当onInterceptTouchEvent方法返回true,表示当前FrameLayout拦截Touch事件,触摸事件就会交给onTouch处理,所以重写onTouch方法如下:


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);
}

差值器DecelerateInterpolator

该差值器实现的效果:在动画开始的地方快然后慢。这里就不再赘述其他差值器了,感兴趣可以去看看差值器的源码,需要懂些数学公式。
该下拉刷新控件两个地方用到DecelerateInterpolator差值器,下拉刷新的过程以及刷新完成后的控件位置回归过程。


/**
* 控件滑动结束后回归动画
* @param values
*/
private void setChildViewTransY(float&#8230; 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 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() } 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 return valve;
}

}

布局代码



android:layout_@+id/waveFlayout"
android:layout_
android:layout_>
android:layout_
android:layout_>
android:layout_
android:layout_>
android:id="@+id/txtShow"
android:layout_
android:layout_
android:gravity="center"
android:text="hello world!" />





布局的格式调不来,注意一点就好,WaveView里面嵌套ScrollView或ListView,才能响应滑动监听。后续加入事件监听,下拉完成后的后续操作。


推荐阅读
  • 涉及的知识点-ViewGroup的测量与布局-View的测量与布局-滑动冲突的处理-VelocityTracker滑动速率跟踪-Scroller实现弹性滑动-屏幕宽高的获取等实现步 ... [详细]
  • 本文详细介绍了Android中的坐标系以及与View相关的方法。首先介绍了Android坐标系和视图坐标系的概念,并通过图示进行了解释。接着提到了View的大小可以超过手机屏幕,并且只有在手机屏幕内才能看到。最后,作者表示将在后续文章中继续探讨与View相关的内容。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • WPF开发心率检测大数据曲线图的高性能实现方法
    本文介绍了在WPF开发中实现心率检测大数据曲线图的高性能方法。作者尝试过使用Canvas和第三方开源库,但性能和功能都不理想。最终作者选择使用DrawingVisual对象,并结合局部显示的方式实现了自己想要的效果。文章详细介绍了实现思路和具体代码,对于不熟悉DrawingVisual的读者可以去微软官网了解更多细节。 ... [详细]
  • 本文介绍了在MFC下利用C++和MFC的特性动态创建窗口的方法,包括继承现有的MFC类并加以改造、插入工具栏和状态栏对象的声明等。同时还提到了窗口销毁的处理方法。本文详细介绍了实现方法并给出了相关注意事项。 ... [详细]
  • java线条处理技术_Java使用GUI绘制线条的示例
    在Java的GUI编程中,如何使用GUI绘制线条?以下示例演示了如何使用Graphics2D类的Line2D对象的draw()方法作为参数来绘制一条线。 ... [详细]
  • 如何自行分析定位SAP BSP错误
    The“BSPtag”Imentionedintheblogtitlemeansforexamplethetagchtmlb:configCelleratorbelowwhichi ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 自动轮播,反转播放的ViewPagerAdapter的使用方法和效果展示
    本文介绍了如何使用自动轮播、反转播放的ViewPagerAdapter,并展示了其效果。该ViewPagerAdapter支持无限循环、触摸暂停、切换缩放等功能。同时提供了使用GIF.gif的示例和github地址。通过LoopFragmentPagerAdapter类的getActualCount、getActualItem和getActualPagerTitle方法可以实现自定义的循环效果和标题展示。 ... [详细]
  • 如何在HTML中获取鼠标的当前位置
    本文介绍了在HTML中获取鼠标当前位置的三种方法,分别是相对于屏幕的位置、相对于窗口的位置以及考虑了页面滚动因素的位置。通过这些方法可以准确获取鼠标的坐标信息。 ... [详细]
  • 本文总结了在编写JS代码时,不同浏览器间的兼容性差异,并提供了相应的解决方法。其中包括阻止默认事件的代码示例和猎取兄弟节点的函数。这些方法可以帮助开发者在不同浏览器上实现一致的功能。 ... [详细]
  • 本文介绍了利用ARMA模型对平稳非白噪声序列进行建模的步骤及代码实现。首先对观察值序列进行样本自相关系数和样本偏自相关系数的计算,然后根据这些系数的性质选择适当的ARMA模型进行拟合,并估计模型中的位置参数。接着进行模型的有效性检验,如果不通过则重新选择模型再拟合,如果通过则进行模型优化。最后利用拟合模型预测序列的未来走势。文章还介绍了绘制时序图、平稳性检验、白噪声检验、确定ARMA阶数和预测未来走势的代码实现。 ... [详细]
  • 颜色迁移(reinhard VS welsh)
    不要谈什么天分,运气,你需要的是一个截稿日,以及一个不交稿就能打爆你狗头的人,然后你就会被自己的才华吓到。------ ... [详细]
  • 概述H.323是由ITU制定的通信控制协议,用于在分组交换网中提供多媒体业务。呼叫控制是其中的重要组成部分,它可用来建立点到点的媒体会话和多点间媒体会议 ... [详细]
  • Matlab 中的一些小技巧(2)
    1.Ctrl+D打开子程序  在MATLAB的Editor中,将输入光标放到一个子程序名称中间,然后按Ctrl+D可以打开该子函数的m文件。当然这个子程序要在路径列表中(或在当前工作路径中)。实际上 ... [详细]
author-avatar
李老鱼儿_654
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有