我自己实现的效果是这样的,每次听到某大牛谈

作者:118kjcom开奖现场
背景红色的运动过程

在这里,我会直接给出代码来讲解。为了便于计算,我们先将控件的宽高设为相等,让它变成一个正方形:

 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec,widthMeasureSpec); }

然后就是绘制背景红色了,其实这个红色有上下两个,但是两个其实是一样的,所以我就只介绍上半部分。

 pathUp.reset(); pathUp.moveTo; pathUp.cubicTo(x2,y26,0,y345,radius,y345); pathUp.cubicTo(radius*2,y345,x6,y26,radius*2,0); pathUp.lineTo; canvas.drawPath(pathUp,paint);

这里的过程就是从x1点开始连接一直连接到x7,最后在连到x1。其中x2表示x2点的x坐标,y26表示x2点和x6点的y坐标,其余类同。radius表示圆的半径,也就是正方形边长的一半。代码里,x3点和x5点的横坐标被我设置为和x1,x7一样,这样可以让圆扁一点。我们需要变化的点,其实就是x2,x6,y26,y345这四个点而已。y345,y26需要上下移动,来达到变小变大的效果。至于x2,x6则是需要向外扩大,不然最后y345就算捅穿地表也不能把两边填满。现在我们知道了需要变化的点的轨迹,那我们怎么控制它们呢?我在View里写了一个方法:

public void controllAnimation(int progress,float max)

这个方法接收两个参数,第一个是当前值,第二个是最大的值。通过 当前值/最大值 我们就可以获取一个百分比的进度值。通过这个进度值我们就可以计算出当前点的位置:

 public void controllAnimation(int progress,float max){ double fraction = progress/max; //这里根据进度改变,慢慢的变化 y345 =  (radius*2*); if(y345<radius){ x2 = 0; x6 = radius*2-x2; }else { x2 =  (-radius*2*((fraction-0.5)/fraction)); x6 = radius*2-x2; } y26 =  (radius*fraction); invalidate(); }

现在这个我用了一个seekBar,等到将其放进真的比如RecyclerView的侧滑删除时,只要将滑动距离作为progress参数传进去,滑动最大值作为max传进去就可以了。

图片 1叉叉.png

叉叉就简单多了,图上的每个点都是中点。叉叉也是分上半部和下半部,我们这里就只讲讲上半部分,下半部分一样的。

 //这里画那个叉叉 //这是上半部分 pathCha.reset(); pathCha.moveTo((halfRadius*3)/2,radius); pathCha.lineTo(halfRadius,chaY1); pathCha.lineTo((halfRadius*3)/2,chaY2); pathCha.lineTo(radius,chaY1); pathCha.lineTo(-((halfRadius*3)/2),chaY2); pathCha.lineTo(halfRadius*3,chaY1); pathCha.lineTo(-((halfRadius*3)/2),radius); pathCha.lineTo((halfRadius*3)/2,radius);

这里radius表示半径,halfRadius表示半径的一半。chaY1等于/4chaY2等于 radius/2chaY1表示x1,x3,x5点的y坐标,chaY2表示x2,x4点的y坐标。我们需要改变的就是chaY1和chaY2的值,来达到一种叉叉慢慢出现的感觉。代码如下:

 if(y345<radius){ //一些代码 }else { ... //慢慢的画出叉 chaY1 =  (radius-(chaLength*2*((fraction-0.5)/fraction))); chaY2 =  (radius-(halfRadius*2*((fraction-0.5)/fraction))); }

我们的叉叉是在上下两个红色接触在一起的时候才绘制的,也就是进度为一半的时候。

这样就把这个View说完了,View的代码也不是很长,我就直接贴出来吧:

public class CeHuaView extends View { Paint paint,paintCha; Path pathUp,pathDown,pathCha,pathCha2; int width; float radius; float halfRadius; //这里取名字有点随便,因为不知道怎么取,可以看我博客里的图,应该就能知道意思了 //这里y表示上半部分,yy表示下半部分,x同理 float y345,yy345; float y26,yy26; float x2,x6; float chaLength; float chaY1,chaY2; public CeHuaView(Context context) { this(context,null); } public CeHuaView(Context context, @Nullable AttributeSet attrs) { this(context, attrs,0); } public CeHuaView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { pathUp = new Path(); pathDown = new Path(); pathCha = new Path(); pathCha2 = new Path(); paint = new Paint(); paint.setColor(Color.parseColor("#ff5777")); paint.setAntiAlias; paint.setStyle(Paint.Style.FILL); paintCha = new Paint(); paintCha.setColor(Color.parseColor("#ffffff")); paintCha.setAntiAlias; paintCha.setStyle(Paint.Style.FILL); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec,widthMeasureSpec); width = MeasureSpec.getSize(widthMeasureSpec); radius = width/2; halfRadius = width/4; y345 = radius; yy345 = radius; y26 = halfRadius; yy26 = halfRadius*3; x2 = 0; x6 = radius*2; chaLength = radius/4; chaY1 = chaLength*3; chaY2 = chaLength*2; } public void controllAnimation(int progress,float max){ double fraction = progress/max; //这里根据进度改变,慢慢的变化 y345 =  (radius*2*); yy345 = radius*2-y345; if(y345<radius){ x2 = 0; x6 = radius*2-x2; }else { x2 =  (-radius*2*((fraction-0.5)/fraction)); x6 = radius*2-x2; //慢慢的画出叉 chaY1 =  (radius-(chaLength*2*((fraction-0.5)/fraction))); chaY2 =  (radius-(halfRadius*2*((fraction-0.5)/fraction))); } y26 =  (radius*fraction); yy26 =  (-(radius*fraction)); invalidate(); } @Override protected void onDraw(Canvas canvas) { super.onDraw; //这里画粘稠的效果 //这是上半部分 pathUp.reset(); pathUp.moveTo; pathUp.cubicTo(x2,y26,0,y345,radius,y345); pathUp.cubicTo(radius*2,y345,x6,y26,radius*2,0); pathUp.lineTo; canvas.drawPath(pathUp,paint); //这里画粘稠的效果 //这是下半部分 pathDown.reset(); pathDown.moveTo(0,radius*2); pathDown.cubicTo(x2,yy26,0,yy345,radius,yy345); pathDown.cubicTo(radius*2,yy345,x6,yy26,radius*2,radius*2); pathDown.lineTo(0,radius*2); canvas.drawPath(pathDown,paint); if(y345>radius){ //这里画那个叉叉 //这是上半部分 pathCha.reset(); pathCha.moveTo((halfRadius*3)/2,radius); pathCha.lineTo(halfRadius,chaY1); pathCha.lineTo((halfRadius*3)/2,chaY2); pathCha.lineTo(radius,chaY1); pathCha.lineTo(-((halfRadius*3)/2),chaY2); pathCha.lineTo(halfRadius*3,chaY1); pathCha.lineTo(-((halfRadius*3)/2),radius); pathCha.lineTo((halfRadius*3)/2,radius); //这是下半部分 pathCha.lineTo(-((halfRadius*3)/2),radius); pathCha.lineTo(halfRadius*3,radius*2-chaY1); pathCha.lineTo(-((halfRadius*3)/2),radius*2-chaY2); pathCha.lineTo(radius,radius*2-chaY1); pathCha.lineTo((halfRadius*3)/2,radius*2-chaY2); pathCha.lineTo(halfRadius,radius*2-chaY1); canvas.drawPath(pathCha,paintCha); } }}

图片 2recycler.gif

因为这篇文章主要也不是讲RecyclerView的侧滑实现的,所以这方面的知识大家可以去这里看看。在RecyclerView应用的代码也参考自这里。

好了,本篇文章结束了。还有很多不完善的地方,也和UI中国的效果有出入,需要调整。才疏学浅,如有错误,欢迎大家批评指正。

本篇文章的代码:CeHuaView

每次听到某大牛谈论自定义View,顿时敬佩之心,如滔滔江水连绵不绝,心想我什么时候能有如此境界,好了,心动不如行动,于是我开始了自定义View之路,虽然过程有坎坷,但是结果我还是挺满意的。我知道大牛还遥不可及,但是我已使出洪荒之力。此篇博客记录本人初入自定义View之路。

背景的红色

当看到红色的效果的时候,我就想到了贝塞尔曲线。贝塞尔曲线是个神奇的东西,几乎所有神奇的曲线效果都可以用贝塞尔曲线来做。为了让这篇文章保持简洁,所以我就不介绍贝塞尔曲线了,如果有不了解的人,可以去这里看看。

到这里,我就假设你们都已经知道怎么画贝塞尔曲线啦。我想你们也猜到了,红色效果其实就是用的三阶贝塞尔曲线。

图片 3红色效果.png

这个图画的就是红色效果的示意图(为了介绍所以将图画成了半圆,实际效果里,圆是扁扁的)。为了便于计算我将它设置为了一个正方形,图中的x2,x3,x5,x6都是中点。我们用Path.cubicTo()方法将x1到x7这七个点连起来之后就可以得一个半圆形了。我们整个效果,其实就是控制这几个点而已。

自定义控件

有了上边的操作,接下来就开始到了真正自定义控件的时候了,创建一个CustomBall类继承View类,先看构造方法,我们写成构造方法最终调用三个参数的构造方法,获取自定义属性的值及初始化工作就在三个参数构造方法中进行。下面我先先来绘制一个圆,文字画在圆心试试手,效果如图

图片 4

这里写图片描述

当然绘制这个图形,首先获取我们自定义属性值,可通过下面获取属性值
注意通过TypedArray 获取属性值后要执行typedArray.recycle();回收内存,防止内存泄漏。

/**
* 获取自定义属性
*/
TypedArray typedArray=context.obtainStyledAttributes(attrs,R.styleable.customBallView);
        centerText=typedArray.getString(R.styleable.customBallView_centerText);
        Log.e("TAG","centerText"+centerText);
        centerTextSize=typedArray.getDimension(R.styleable.customBallView_centerTextSize,24f);
        centerTextColor=typedArray.getColor(R.styleable.customBallView_centerTextColor,0xFFFFFF);
        ballColor=typedArray.getColor(R.styleable.customBallView_ballColor,0xFF4081);
        radius=typedArray.getDimension(R.styleable.customBallView_ballRadius,260f);
        typedArray.recycle();

初始化画笔

    /**
     * 初始化画笔
     */
    private void initPaint() {
        roundPaint = new Paint();
        roundPaint.setColor(ballColor);
        roundPaint.setAntiAlias(true);//抗锯齿
        fontPaint = new Paint();
        fontPaint.setTextSize(centerTextSize);
        fontPaint.setColor(centerTextColor);
        fontPaint.setAntiAlias(true);
        fontPaint.setFakeBoldText(true);//粗体

    }

接下来我们先画一个圆,先通过下面方法获取空间本身的宽和高,然后调用canvas.drawCircle(width/2, height/2, radius, roundPaint);画圆,在原点设置为控件中心位置,即点(width/2, height/2),半径为radius,画笔roundPaint,接下来绘制文字,将位子绘制在圆的中心。

        width = getWidth() ;
        height = getHeight();

如果我们通过canvas.drawText(centerText, width/2, height/2, fontPaint);绘制文字的话,发现文字并不是在中心位置,那么我们可以做一下调整,canvas.drawText(centerText, width/2, height/2, fontPaint);先通过float textWidth = fontPaint.measureText(centerText);获取文字的宽度,canvas.drawText(centerText, width/2-textWidth /2, height/2, fontPaint);此时文字依然不在中心,那么此时我们研究一下文字到底是怎么绘制的,为什么坐标试试中心了,绘制出来的效果依然有偏差呢。

要关注文字绘制的话,FontMetrics这个类是必须要知道的因为它的作用是测量文字,它里面呢就定义了top,ascent,descent,bottom,leading五个成员变量其他什么也没有。先看源码

public static class FontMetrics {
        /**
         * The maximum distance above the baseline for the tallest glyph in
         * the font at a given text size.
         */
        public float   top;
        /**
         * The recommended distance above the baseline for singled spaced text.
         */
        public float   ascent;
        /**
         * The recommended distance below the baseline for singled spaced text.
         */
        public float   descent;
        /**
         * The maximum distance below the baseline for the lowest glyph in
         * the font at a given text size.
         */
        public float   bottom;
        /**
         * The recommended additional space to add between lines of text.
         */
        public float   leading;
    }

这个类是Paint的静态内部类,通过注释我们就知道了每个变量的含义,为了更生动的理解这几个变量含义,我们通过下面的一张图来分别解释每个变量的含义

图片 5

这里写图片描述

  • Baseline(基线) 在Android中,文字的绘制都是从Baseline处开始的
  • ascent(上坡度)Baseline往上至文字“最高处”的距离我们称之为ascent,
  • descent(下坡度)Baseline往下至文字“最低处”的距离我们称之为descent(下坡度)
  • leading(行间距)表示上一行文字的descent到该行文字的ascent之间的距离
  • top 对于ascent上面还有一部分内边距,内边距加上ascent即为top值
  • bottom descent和内边距的加上descent距离

值得注意的一点,Baseline上方的值为负,下方的值为正如下图文字30%的ascent,descent,top,bottom。

图片 6

这里写图片描述

通过上面的分析,我们就得出了将文本绘制中心的代码如下

//测量文字的宽度
float textWidth = fontPaint.measureText(centerText);
        float x = width / 2 - textWidth / 2;
        Paint.FontMetrics fontMetrics = fontPaint.getFontMetrics();
        float dy = -(fontMetrics.descent + fontMetrics.ascent) / 2;
        float y = height / 2  + dy;
        canvas.drawText(centerText, x, y, fontPaint);

至此这个简单自定义的View基本实现,此时我改了布局配置文件为宽高

        android:layout_width="wrap_content"
        android:layout_height="wrap_content"

或者

        android:layout_width="match_parent"
        android:layout_height="match_parent"

Oh my God,为什么效果是一样的啊,此时再回到自定义的类,我们发现我们没有实现onMeasure里面测量的代码,接下来让我们实现onMeasure操作,如下

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //测量模式
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        //测量规格大小
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int width;
        int height;
        if (widthMode == MeasureSpec.EXACTLY) {
            width=widthSize;
        } else if (widthMode == MeasureSpec.AT_MOST) {
            width=(int)Math.min(widthSize,radius*2);
        } else {

            width=windowWidth;
        }
        if (heightMode == MeasureSpec.EXACTLY) {
            height=heightSize;
        } else if (heightMode == MeasureSpec.AT_MOST) {
            height=(int)Math.min(heightSize,radius*2);
        } else {
            height=windowHeight;
        }
        setMeasuredDimension(width,height);
    }

测量主要依靠MeasureSpec,MeasureSpec(测量规格)是一个32位的int数据.其中高2位代表SpecMode即某种测量模式,低32位为SpecSize代表在该模式下的规格大小,测量模式有三种

  • EXACTLY 确切的,在布局文件中设置的宽高是固定的,此时测量大小就是我们设置的宽高
  • AT_MOST 至多,不能超出
  • UNSPECIFIED 未指定

MeasureSpec的详细解释

通过上面的分析,绘制此图形的完整代码为 点击查看

我自己实现的效果是这样的:

图片 7

之前在UI中国上看到一个侧滑效果,觉得还不错,于是就想实现一下。UI效果是这样的:

这里写图片描述

*本篇文章已授权微信公众号 guolin_blog 独家发布 *

自定义属性

自定义属性,就是在资源文件夹下values目录中创建一个attrs.xml文件,
文件结构如下所示,atrr标签就是我们要自定义的一些属性,name就是自定义属性的名字,那么format是做什么的呢?

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="">
        <attr name="centerText" format=""></attr>
        <attr name=" ">
            <enum name=""  value=" "></enum>
            <enum name="" value=" "></enum>
        </attr>
    </declare-styleable>
</resources>

format是属性对应的值的类型,有十个值

  • enm 枚举类型,例 android:orientation="vertical" 此值有horizontal,和 vertical
  • dimension 尺寸值
  • color 颜色值,例 android:textColor = "#00FF00"
  • boolean 布尔值,true or false
  • flag 位或运算
  • float 浮点型
  • fraction 百分数,
  • reference 参考某一资源ID,例 android:background = "@drawable/ic_launcher"
  • string 字符串类型
  • integer 整型值

知道了这些值得含义,就可以自定义我们自己的属性了,对于这个进度条,我们可以自定义圆的半径,颜色,和圆中心文本的大小,颜色,文本,最后attrs.xml文件为

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CustomBallView">
        <attr name="centerText" format="string"></attr>
        <attr name="centerTextSize" format="dimension"></attr>
        <attr name="centerTextColor" format="color"></attr>
        <attr name="ballColor" format="color"></attr>
        <attr name="ballRadius" format="dimension"></attr>
    </declare-styleable>
</resources>

好了,那我们就来看看这个效果是怎么实现的。

布局文件配置相关内容

在布局文件要配置我们自定义的属性,首先要自定义命名空间,

图片 8

这里写图片描述

如上图,如果在as中命名空间写成http://schemas.android.com/apk/res/包名 此时as会报错,这是gradle造成的,在eclipse中如果自定义的属性 是不能用res-auto的 必须得替换成你自定义view所属的包名,如果你在恰好使用的自定义属性被做成了lib 那就只能使用res-auto了,而在android-studio里,无论你是自己写自定义view 还是引用的lib里的自定义的view 都只能使用res-auto这个写法。以前那个包名的写法 在android-studio里是被废弃无法使用的
所以配置后的布局文件如下

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:customBallView="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.example.xh.customball.MainActivity"
    tools:showIn="@layout/activity_main">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="gone"
        android:text="Hello World!" />

    <com.example.xh.customball.CustomBall
        android:background="@color/colorPrimary"
        android:layout_centerInParent="true"
        android:layout_margin="10dp"
        customBallView:centerText="30%"
        customBallView:centerTextSize="28dp"
        customBallView:centerTextColor="#000000"
        customBallView:ballColor="@color/colorAccent"
        customBallView:ballRadius="30dp"
        android:layout_width="260dp"
        android:layout_height="260dp">
    </com.example.xh.customball.CustomBall>
</LinearLayout>

效果并没有完全一致,UI效果里当两部分重合后有一种像水滴重合一样的效果,我并不知道用代码如何实现(如果大家知道请告诉我。。。),所以我这里只是简单的做了一个慢慢出现的效果。

控件升级

上面我们已经实现了圆形和文本的绘制,那么接下来,我们先开始实现中心新进度的更新绘制。先看效果图

图片 9

这里写图片描述

通过效果图,我们看到实现此效果就是不断的更新进度值,然后重绘,,那么我们只需开启一个线程实现更新进度值,为了更好的控制我们再加点击事件,当单机时开始增大进度,双击时暂停进度,并弹出Snackbar,其中有一个重置按钮,点击重置时将进度设置为0,重绘界面。

  • 响应点击事件
    因为要实现双击事件,我们可以直接用GestureDetector(手势检测),通过这个类我们可以识别很多的手势,主要是通过他的onTouchEvent(event)方法完成了不同手势的识别GestureDetector里有一个内部类 SimpleOnGestureListener。SimpleOnGestureListener类是GestureDetector提供给我们的一个更方便的响应不同手势的类,这个类实现了上述两个接口(OnGestureListener, OnDoubleTapListener,但是所有的方法体都是空的),该类是static class,也就是说它实际上是一个外部类。程序员可以在外部继承这个类,重写里面的手势处理方法
    public static class SimpleOnGestureListener implements OnGestureListener, OnDoubleTapListener,
            OnContextClickListener {
//单击抬起
        public boolean onSingleTapUp(MotionEvent e) {
            return false;
        }
//长按
        public void onLongPress(MotionEvent e) {
        }
//滚动
        public boolean onScroll(MotionEvent e1, MotionEvent e2,
                float distanceX, float distanceY) {
            return false;
        }
//快速滑动
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
                float velocityY) {
            return false;
        }
//
        public void onShowPress(MotionEvent e) {
        }

        public boolean onDown(MotionEvent e) {
            return false;
        }

        public boolean onDoubleTap(MotionEvent e) {
            return false;
        }

        public boolean onDoubleTapEvent(MotionEvent e) {
            return false;
        }

        public boolean onSingleTapConfirmed(MotionEvent e) {
            return false;
        }

        public boolean onContextClick(MotionEvent e) {
            return false;
        }
    }

下面是我们自定继承SimpleOnGestureListener,由于我们只要响应单击和双击事件,那么我们只需要重写onDoubleTap双击(),onSingleTapConfirmed(单击)方法即可,

    public class MyGestureDetector extends GestureDetector.SimpleOnGestureListener {

        @Override
        public boolean onDoubleTap(MotionEvent e) {
            getHandler().removeCallbacks(singleTapThread);
            singleTapThread=null;
            Snackbar.make(CustomBall.this, "暂停进度,是否重置进度?", Snackbar.LENGTH_LONG).setAction("重置", new OnClickListener() {
                @Override
                public void onClick(View v) {
                    currentProgress=0;
                    invalidate();
                }
            }).show();
            return super.onDoubleTap(e);
        }

        @Override
        public boolean onSingleTapConfirmed(MotionEvent e) {
            Snackbar.make(CustomBall.this, "单机了", Snackbar.LENGTH_LONG).setAction("Action", null).show();
            startProgressAnimation();
            return super.onSingleTapConfirmed(e);
        }
    }

当点击时Snackbar做个提醒单击了View,然后调用startProgressAnimation()方法初始化一个线程,通过postDelayed将线程加入的消息队列,延迟100ms执行,通过singleTapThread == null判断条件,避免过多的创建对象

    private void startProgressAnimation() {
        if (singleTapThread == null) {
            singleTapThread = new SingleTapThread();
            getHandler().postDelayed(singleTapThread, 100);
        }
    }

我们将SingleTapThread 实现Runnable接口,在run方法里书写我们的处理逻辑,其实很简单,先判断当前进度值是不是大于最大进度(100),如果小于最大的值,我们就将currentProgress(当前进度值)加1的操作,然后调用invalidate()方法重绘界面,之后还需要再次将线程加入消息队列,依然延迟100ms执行。对于当如果当前进度已经加载到100%,此时我们将此线程从消息队列移除。

    private class SingleTapThread implements Runnable {
        @Override
        public void run() {
            if (currentProgress < maxProgress) {
                currentProgress++;
                invalidate();
                getHandler().postDelayed(singleTapThread, 100);

            } else {
                getHandler().removeCallbacks(singleTapThread);
            }
        }
    }

接下来还需要注册事件,我们可以在onDraw()方法中通过GestureDetector的构造方法可以将自定义的MyGestureDetector对象传递进去,然后通setOnTouchListener设置监听器,这样GestureDetector能处理不同的手势了

      if (detector==null){
            detector = new GestureDetector(new MyGestureDetector());
            setOnTouchListener(new OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    return detector.onTouchEvent(event);
                }
            });

        }

还有最重要的一点是,View默认是不可点击的,所以我们需要 setClickable(true)设置View可点击的,OK,到这里我们就完成的中心进度值得更新,接下来就开始绘制里面的波浪形状,效果图如下

图片 10

这里写图片描述

最后的最后

感谢我可爱的女朋友。

既然是初出茅庐,自然是按部就班的进行,先来一张效果图

图片 11cehua.gif

项目源码【传送门】

这是一个自定义View。效果分为两个部分,一个是背景的红色,一个是白色的叉叉。

实现水波浪效果

水波纹效果是通过二阶贝塞尔曲线实现的,先简单看下什么是贝塞尔曲线

在数学的数值分析领域中,贝塞尔曲线(英语:Bézier curve)是电脑图形学中相当重要的参数曲线。更高维度的广泛化贝塞尔曲线就称作贝塞尔曲面,其中贝塞尔三角是一种特殊的实例。
贝塞尔曲线于1962年,由法国工程师皮埃尔·贝塞尔(Pierre Bézier)所广泛发表,他运用贝塞尔曲线来为汽车的主体进行设计。贝塞尔曲线最初由Paul de Casteljau于1959年运用de Casteljau算法开发,以稳定数值的方法求出贝塞尔曲线 - - - - -维基百科

  • 线性贝塞尔曲线
    给定点P0、P1,线性贝塞尔曲线只是一条两点之间的直线。这条线由下式给出:

图片 12

这里写图片描述

绘制效果为

图片 13

这里写图片描述

  • 二次方贝塞尔曲线
    二次方贝塞尔曲线的路径由给定点P0、P1、P2的函数B(t)追踪:

图片 14

这里写图片描述

图片 15

这里写图片描述

  • 三次方贝塞尔曲线
    P0、P1、P2、P3四个点在平面或在三维空间中定义了三次方贝塞尔曲线。曲线起始于P0走向P1,并从P2的方向来到P3。一般不会经过P1或P2;这两个点只是在那里提供方向资讯。P0和P1之间的间距,决定了曲线在转而趋进P2之前,走向P1方向的“长度有多长”。
    曲线的参数形式为:
![](https://upload-images.jianshu.io/upload_images/2764996-8f3e7ae93c2908ac)

这里写图片描述

当然贝塞尔曲线是一个很复杂的东西,他可以延伸N阶贝塞尔曲线,如果想要真正搞明白,想自定义比较复杂或者比较酷炫的动画,那高等数学知识必须要搞明白,很多时候,我们只需要了解二次贝塞尔曲线就可以了,或者说,即使贝塞尔曲线不是那么熟悉,也不用怕,android API 封装了常用的贝塞尔曲线,我们只需要传入坐标就可以实现很多动画。

首先我们需要初始化贝塞尔曲线区域的画笔设置。其中重要的一点就是setXfermode()方法,此方法可以设置与其他绘制图形的交集,合集,补集等运算,在这个项目中,我们使用了交集(绘制贝塞尔曲线区域和圆区域的交集)

       progressPaint = new Paint();
        progressPaint.setAntiAlias(true);
        progressPaint.setColor(progressColor);
        //取两层绘制交集。显示上层
        progressPaint.setXfermode(new                 PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

初始化画笔后,就开始绘制我们的图形,先初始化一个
宽和高都为radius * 2的正方形画布作为缓冲区画布,我们可以先在缓冲区画布绘制,绘制完成后一次再绘制到画布上。

        bitmap = Bitmap.createBitmap((int) radius * 2, (int) radius * 2, Bitmap.Config.ARGB_8888);

        bitmapCanvas = new Canvas(bitmap);

然后绘制圆心(width / 2, height / 2)半径为radius的圆

bitmapCanvas.drawCircle(width / 2, height / 2, radius, roundPaint);

水波从圆的最下方开始(进度为0),到最上方(进度最大值maxProgress)结束,那么我们需要根据当前进度值动态计算水波的高度

 float y = (1 - (float) currentProgress / maxProgress) * radius * 2

图片 16

这里写图片描述

如图,我们就可以先将path.lineTo将每个点连起来,可以先从(width,y)绘制,那么需要调用path.moveTo(width, y);方法将操作点移动到该坐标上,接下下就开始依次连接其余三个点(width,height),(0,height),(0,y)。由于我们之前画笔设置的是取交集(progressPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN))),所以此时会绘制与圆相交的部分,也就是圆内的部分。

图片 17

这里写图片描述

下面就是绘制贝塞尔曲线

            path.rQuadTo(space, -d, space * 2, 0);
            path.rQuadTo(space, d, space * 2, 0);

第一个是绘制向下弯曲,第二个是绘制向上弯曲。为了从左到右都绘制曲线,我们根据圆的直径计算一下,需要几次才能平铺,然后循环执行上面两句,直到平铺圆形区域,为了展示当进度增大时将波纹幅度降低的效果(直到进度为100%,幅度降为0)我们根据当前进度值动态计算了幅度值,计算方法如下

float d = (1 - (float) currentProgress / maxProgress) *space;

由于我们需要以实心的方式绘制区域,那么我们调用
path.close();将所画区域封闭,也就是实心的效果。

        path.close();
        bitmapCanvas.drawPath(path, progressPaint);

Ok,到这里,自定义的水波形状的进度条就完成了,再次上效果图
(注:此水波左右移动是后来加的效果,具体实现点击代码查看)

图片 18

这里写图片描述

由于本人目前水平有限,文字若有不足的地方,欢迎指正,谢谢。

本文由118kjcom最快开奖现场发布,转载请注明来源

关键词: