自定义View之文字游乐场(一)

本文通过四个游乐场项目:摩天轮、滑梯、冲浪和射击,介绍了如何利用Canvas的Path功能绘制动态图形,并详细讲解了绘制过程中的关键技术点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近看了Canvas画图中的Path,不由感叹其功能之强大,同时脑海中也产生了很多飞舞的线条,近日将其整理出来,最终归结到一个游乐场中的四个项目:
1,摩天轮
2,滑梯
3,冲浪
4,射击

闲话少说,先上个图:
这里写图片描述

由于总体内容较多,下面只捡关键点进行说明:

一、摩天轮(FerrisWheel)

将摩天轮拆解,分为4部分:支架、辐条、外轮,以及靠在外轮上的人(这里我是简单的用字来表示了,或者你也可以理解为是游客说的话)。
这里要注意,支架是不动的,而另外3部分是运动的,所以在绘图时需要注意各部分的关联。
由于辐条、外轮、以及外轮上的字需要同步转动,这里采用了旋转画布来实现这个转动。
使用画布旋转时要注意,要先保存画布的状态,然后设置旋转角度:

canvas.save();
canvas.rotate(progressValue*360/100, circleCenter.x, circleCenter.y);

在完成旋转绘图后,要进行画布状态的恢复:

canvas.restore();

在轮上写字,我采用了 canvas.drawTextOnPath()方法,其原型如下:

void drawTextOnPath (String text, Path path, float hOffset, float vOffset, Paint paint)

通过调整第3个参数 hOffset 来调整文字沿着path的位置,使初始出现时文字在最底部(我们总是从摩天轮的底部登上去,然后才能体验高处的刺激)。
然后调整第四个参数 vOffset 来控制文字与线的垂直位置,使字既挨着线又能完整显示。
最后,还有一个问题,我写出来的文字,是朝圆外的,而我希望字是朝圆内的,怎么办?
原来在画圆的时候,还有个参数是表示绘制的方向的,我们可以指定是按顺时针方向绘制还是逆时针绘制。调整为逆时针方向绘制(使用参数 Path.Direction.CCW),文字方向就如我所愿了,如下:

path.addCircle(circleCenter.x, circleCenter.y, radius, Path.Direction.CCW);

具体实现如下:

package com.customview.view;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Shader;
import android.text.StaticLayout;
import android.graphics.Paint.Cap;
import android.graphics.Paint.FontMetrics;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

import com.customview.LogUtil;
import com.customview.R;

public class FerrisWheelView extends View
{
    private Paint mPaint;//画笔
    private int progressValue=0;//进度值

    public FerrisWheelView(Context context, AttributeSet attrs)
    {       
        this(context, attrs, 0);
    }

    public FerrisWheelView(Context context)
    {
        this(context, null);
    }

    public FerrisWheelView(Context context, AttributeSet attrs, int defStyle)
    {
        super(context, attrs, defStyle);

        mPaint = new Paint();
        progressValue=0;
    }

    public void setProgressValue(int progressValue){

        this.progressValue = progressValue;
        postInvalidate();       
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int mWidth = getMeasuredWidth();
        int mHeight = getMeasuredHeight();

        //按比例计算View各部分的值
        float unit = Math.min(((float)mWidth)/300, ((float)mHeight)/300);
        float lineWidth = 5*unit;//线粗

        //初始化画笔
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth((float) lineWidth );
        mPaint.setStyle(Style.STROKE);
        mPaint.setStrokeCap(Cap.ROUND);
        mPaint.setShader(null);
        mPaint.setColor(Color.RED);

        //准备上变化的字
        String text ;
        if(progressValue%100 < 100/4){
            text="哦";
        } else if(progressValue%100 < 100/2){
            text="耶";
        } else if(progressValue%100 < 100*3/4){
            text="哇";
        } else {
            text="噢";
        }  

        Path path = new Path();
        path.reset();
        int radius = mWidth/2*4/5;//设置半径        
        PointF circleCenter = new PointF(mWidth/2, mHeight/2 );//中心点

        mPaint.setColor(Color.BLACK);
        mPaint.setStrokeWidth(2*unit );
        //支架
        for(int i=1;i<3;i++){
            canvas.drawLine(circleCenter.x, circleCenter.y, 
                    circleCenter.x-radius*i/10, circleCenter.y+radius*11/10, 
                    mPaint);
            canvas.drawLine(circleCenter.x, circleCenter.y, 
                    circleCenter.x+radius*i/10, circleCenter.y+radius*11/10, 
                    mPaint);
        }               
        canvas.drawLine(circleCenter.x-radius*2/4, circleCenter.y+radius*11/10, 
                circleCenter.x+radius*2/4, circleCenter.y+radius*11/10, 
                mPaint);



        //进行画布旋转,影响后面的绘制
        canvas.save();
        canvas.rotate(progressValue*360/100, circleCenter.x, circleCenter.y);

        //轮辐
        mPaint.setColor(Color.LTGRAY);
        mPaint.setStrokeWidth(1*unit );     
        for(int i=0;i<12;i++){
            float offsetX = radius*(float)Math.sin(3.14159*i/12);//3.14 360
            float offsetY = radius*(float)Math.cos(3.14159*i/12);
            canvas.drawLine(circleCenter.x-offsetX, circleCenter.y-offsetY, 
                            circleCenter.x+offsetX, circleCenter.y+offsetY, 
                            mPaint);
        }       

        //外轮
        mPaint.setColor(Color.RED);
        mPaint.setStrokeWidth((float) lineWidth );      
        path.addCircle(circleCenter.x, circleCenter.y, radius, Path.Direction.CCW);//顺时针  CW   CCW
        canvas.drawPath(path, mPaint);

        //文字
        mPaint.setTextSize(20*unit);
        mPaint.setStyle(Paint.Style.FILL);
        canvas.drawTextOnPath(text, path, radius*3*(float)1.57-20, -15, mPaint);

        //旋转效果取消
        canvas.restore();

    }

}

二、滑梯(Sliding)

将滑梯分解开,是一个Path,由四条线组成:滑梯的曲面,另外3条直线进行连接。
这个曲线,这里采用了3阶贝塞尔曲线,通过调整两个控制点,就画出了一条平滑的曲线。
怎么让文字沿着这条曲线进行运动?
动态调整drawTextOnPath()的第3个参数 hOffset 来调整文字沿着path的位置。
另外还要考虑到滑动的速度是逐渐增加的,这就是调整 hOffset 变化的速度了。
还有个核心问题,我希望文字运行到曲线的终点就停下来。可是,哪里是曲线的终点呢?换个问题,曲线的长度是多少?
这真是个麻烦的问题呢,我的数学学得不好,要是谷歌提供个方法能获取曲线长度就好了。上网搜了一下,原来谷歌早就考虑到了我们的需求,提供了一个便利的途径:使用PathMeasure。
获取曲线的长度很简单,两句话搞定:

PathMeasure pathMeasure = new PathMeasure(path, false);
float length = pathMeasure.getLength();

PathMeasure(path, false)中第一个参数好理解,就是路径,第二个参数,是指这个路径是否闭合。
具体实现如下:

package com.customview.view;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Cap;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.view.View;
import com.customview.LogUtil;


public class SlidingView extends View
{ 
    private Paint mPaint;//画笔
    private int progressValue=0;//进度值
    private float offsetTextH=0;

    public SlidingView(Context context, AttributeSet attrs)
    {       
        this(context, attrs, 0);
    }

    public SlidingView(Context context)
    {
        this(context, null);
    }

    public SlidingView(Context context, AttributeSet attrs, int defStyle)
    {
        super(context, attrs, defStyle);

        mPaint = new Paint();
        progressValue=0;
    }

    public void setProgressValue(int progressValue){

        //进度重置时,字的位置也重置
        if(progressValue==0){
            offsetTextH=0;
        }

        this.progressValue = progressValue;
        postInvalidate();

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int mWidth = getMeasuredWidth();
        int mHeight = getMeasuredHeight();

        //按比例计算View各部分的值
        float unit = Math.min(((float)mWidth)/300, ((float)mHeight)/300);
        float lineWidth = 5*unit;//线粗

        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth((float) lineWidth );
        mPaint.setStyle(Style.STROKE);
        mPaint.setStrokeCap(Cap.ROUND);
        mPaint.setShader(null);
        mPaint.setColor(Color.RED);

        String text = "滑";
        Path path = new Path();

        //画出滑梯的曲线
        PointF start, control1, control2, end;
        start = new PointF(10, 200);
        end = new PointF(mWidth-10, mHeight-100);
        control1 = new PointF((start.x+end.x)/2-50, start.y);
        control2 = new PointF((start.x+end.x)/2+50, end.y);
        path.reset();
        path.moveTo(start.x, start.y);
        path.cubicTo(control1.x, control1.y, control2.x, control2.y, end.x, end.y);

        //获取曲线的长度
        PathMeasure pathMeasure = new PathMeasure(path, false);
        float length = pathMeasure.getLength();


        //画出3条连接线,将区域封闭,填充颜色
        PointF end1 = new PointF(end.x, mHeight-10);
        PointF end2 = new PointF(start.x, end1.y);
        path.lineTo(end1.x, end1.y);
        path.lineTo(end2.x, end2.y);        
        path.lineTo(start.x, start.y);

        mPaint.setColor(0xff478EEB);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setStrokeWidth((float) lineWidth );
        canvas.drawPath(path, mPaint);


        //沿曲线写字,其位置动态变化
        float acceleration=0;
        LogUtil.logWithMethod(new Exception(),"progressValue="+progressValue);
        if(progressValue>20){
            acceleration = ((float)progressValue-20)/10;
            LogUtil.logWithMethod(new Exception(),"acceleration="+acceleration);
        }
        offsetTextH += 3*(1+acceleration);
        if(offsetTextH>(length-40)){
            offsetTextH=length-40;
        }
        LogUtil.logWithMethod(new Exception(),"offsetTextH="+offsetTextH);

        mPaint.setStrokeWidth(1*unit);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(Color.BLACK);       
        mPaint.setTextSize(18*unit);
        canvas.drawTextOnPath(text, path, offsetTextH, -2, mPaint);

    }

}

三、冲浪(Surfing)

这个画面与滑梯的画面看起来比较类似,最初,我的想法也是用一个3阶贝塞尔曲线,通过动态调整起点、终点、控制点,来模拟波浪的起伏效果,但是效果总是不太好,特别是对于波峰的移动,没有找到办法来实现。
后来参考网上的方法,使用了四个二阶贝塞尔曲线来实现,两个在屏幕中显示,两个在屏幕之外,通过整体的移动,从而实现波浪的起伏效果。

另外还有一个问题,就是文字的位置如何控制?
我们可以通过PathMeasure的getLength()方法,获取4个贝塞尔曲线的总长度,但是随着曲线的整体移动,怎么保证文字在画面的中央呢?
肯定是调整drawTextOnPath()的第3个参数 hOffset 来调整文字沿着path的位置,具体如何设置呢?
我开始是这样设置的:
hOffset=length*3/4 - start.x;
说明一下,length是测量的曲线总长度,其左边的一半,在初始状态,是在屏幕之外的左侧,其右边的一半,是恰好完整显示在屏幕中的。所以其3/4位置,就是屏幕的中央。
start.x,是显示在屏幕上的左侧那个贝塞尔曲线的起点的x坐标。
看运行效果,文字基本居中,随波浪起伏,可是有个小问题,就是完成一次整屏幕的移动后,进行画面回归初始状态,准备开始下一轮移动的时刻,发现文字有一个明显的跳动移位。
不知道聪明的你有没有猜到问题出在哪里。
我是后来经过打印数据分析,发现是我没有区分清楚length与start.x的意义,从而导致的问题。
length是曲线长度,start.x是坐标。若计算4条贝塞尔曲线的x坐标的跨度,是2倍的屏幕宽度,而曲线长度,肯定是要大于2倍屏幕宽度的(两点之间,直线最短)。
所以,直接使用x坐标是不行的,我就将坐标与长度按照线性的比例修改了,如下:

hOffset=length*3/4 - length*start.x/(2*mWidth);

最终发现效果是非常好的,文字没有了那个切换时的跳动。
其实仔细分析的话,曲线与坐标,肯定不是线性的,只是切换时刻,恰好是移动了一个屏幕的距离,而这正好对应length的一半,从而实现平滑切换。
具体实现如下:

package com.customview.view;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Shader;
import android.text.StaticLayout;
import android.graphics.Paint.Cap;
import android.graphics.Paint.FontMetrics;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

import com.customview.LogUtil;
import com.customview.R;

public class SurfingView extends View
{

    private Paint mPaint;//画笔
    private int progressValue=0;//进度值
    private float offsetHorizontal=0,offsetVertical=0;

    public SurfingView(Context context, AttributeSet attrs)
    {       
        this(context, attrs, 0);
    }

    public SurfingView(Context context)
    {
        this(context, null);
    }

    public SurfingView(Context context, AttributeSet attrs, int defStyle)
    {
        super(context, attrs, defStyle);

        mPaint = new Paint();
        progressValue=0;
    }

    public void setProgressValue(int progressValue){

        this.progressValue = progressValue;
        postInvalidate();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int mWidth = getMeasuredWidth();
        int mHeight = getMeasuredHeight();

        //按比例计算View各部分的值
        float unit = Math.min(((float)mWidth)/300, ((float)mHeight)/300);
        float lineWidth = 5*unit;//线粗


        mPaint.setAntiAlias(true);
        mPaint.setStyle(Style.STROKE);
        mPaint.setStrokeWidth((float) lineWidth );
        mPaint.setColor(Color.RED);
        String text = "冲浪";
        Path path = new Path();


        //水平方向移动的偏移量,渐变
        if(offsetHorizontal>=mWidth){
            offsetHorizontal = 0;
        } else {
            offsetHorizontal+=30;
        }

        //垂直方向的幅度值,固定
        offsetVertical = mHeight*1/12;//1/10

        //画出左侧的波浪线
        PointF startLeft, controlLeft1, controlLeft2, endLeft;
        startLeft = new PointF(-mWidth+offsetHorizontal, (mHeight*2/5));
        endLeft = new PointF(0+offsetHorizontal, startLeft.y);//start.y
        controlLeft1 = new PointF(startLeft.x+(endLeft.x-startLeft.x)*1/4, startLeft.y-offsetVertical);
        controlLeft2 = new PointF(startLeft.x+(endLeft.x-startLeft.x)*3/4, endLeft.y+offsetVertical);
        path.reset();
        path.moveTo(startLeft.x, startLeft.y);
        path.quadTo(controlLeft1.x, controlLeft1.y, (startLeft.x+endLeft.x)*1/2, (startLeft.y+endLeft.y)*1/2);  
        path.quadTo(controlLeft2.x, controlLeft2.y, endLeft.x, endLeft.y);


        //画出波浪线
        PointF start, control1, control2, end;
        start = new PointF(0+offsetHorizontal, (mHeight*2/5));
        end = new PointF(mWidth+offsetHorizontal, start.y);//start.y
        control1 = new PointF(start.x+(end.x-start.x)*1/4, start.y-offsetVertical);
        control2 = new PointF(start.x+(end.x-start.x)*3/4, end.y+offsetVertical);
        path.quadTo(control1.x, control1.y, (start.x+end.x)*1/2, (start.y+end.y)*1/2);  
        path.quadTo(control2.x, control2.y, end.x, end.y);

        //获取波浪线的长度
        PathMeasure pathMeasure = new PathMeasure(path, false);
        float length = pathMeasure.getLength();


        //再画出3条连接线
        PointF end1 = new PointF(end.x, mHeight);
        PointF end2 = new PointF(startLeft.x, end1.y);
        path.lineTo(end1.x, end1.y);
        path.lineTo(end2.x, end2.y);        
        path.lineTo(startLeft.x, startLeft.y);

        //波浪线与3条连接线,形成封闭区域,填充颜色
        mPaint.setColor(Color.BLUE);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setStrokeWidth((float) lineWidth );
        canvas.drawPath(path, mPaint);


        //写字,其位置逐渐移动
        float offsetTextH=length-200-progressValue;
        if(offsetTextH<200){
            offsetTextH=200;
        }
        offsetTextH=length*3/4 - length*start.x/(2*mWidth);
//      LogUtil.logWithMethod(new Exception(),"offsetTextH="+offsetTextH+" start.x="+start.x);
        mPaint.setStrokeWidth(1*unit);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(Color.BLACK);       
        mPaint.setTextSize(18*unit);    
        canvas.drawTextOnPath(text, path, offsetTextH, -2, mPaint);

    }
}

相关源码地址:
http://download.youkuaiyun.com/detail/lintax/9646000

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值