Android 自定义view——签名板

本文介绍如何在Android中创建一个自定义签名板View。内容涵盖自定义View的原因、关键步骤,如重写onMeasure()和onDraw()方法,以及Path的各种绘图方法如moveTo、lineTo、quadTo、cubicTo和arcTo的使用。通过实例展示了如何绘制贝塞尔曲线和弧线,提供了一个完整的签名板实现。

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


首先来看一下咱酷炫的效果图吧!由于是在模拟器上运行,鼠标拖动操作的,所以写的字较丑,见谅见谅哈!

这个签名板涉及到以下几个知识点:
一、自定义view的制作
二、将布局存成图像并保存



我们来简单说说自定义view:


首先我们要明白,为什么要自定义View?

主要是Android系统内置的View无法实现我们的需求,我们需要针对我们的业务需求定制我们想要的View。自定义View我们大部分时候只需重写两个函数:onMeasure()、onDraw()。onMeasure负责对当前View的尺寸进行测量,onDraw负责把当前这个View绘制出来。当然了,你还得写至少写2个构造函数:

//这个方法的调用环境,例如在activity中,在onCreate()方法里,setContentView(new MyView(this));
//这样直接new自定义的View的时候调用  
 public MyView(Context context) {
        super(context);
    }
//这个方法的调用环境,是在布局文件 XXX.xml 中我们用到这个View时调用,也可以看下面的案例
    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs); 
    }

onMeasure()这个方法也是自定义View需要重写的,

View类的默认处理不满足我们的要求的话,我们就得重写onMeasure函数。我们可以给自己的View定义一个在width和height在wrap_content下一个默认值,如果width为300dp,height为250dp。就应该像以下这么写:

private int getSize(int sizedp,int measureSpec){
        int finalsize = 0;
        int mode=MeasureSpec.getMode(measureSpec);//提取测量模式
        int size = MeasureSpec.getSize(measureSpec);//提取尺寸
        switch (mode){
            case MeasureSpec.AT_MOST://表示子布局限制在一个最大值内,一般为WARP_CONTENT
                finalsize=sizedp*density;
                break;
            case MeasureSpec.EXACTLY://一般是设置了明确的值或者是MATCH_PARENT
                finalsize=size;
                break;
            case MeasureSpec.UNSPECIFIED://没有指定大小,就设为默认大小,表示子布局想要多大就多大,很少使用
                finalsize=sizedp*density;//此处的density我后面会讲到
                break;
        }
        return finalsize;
    }
@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(getSize(300,widthMeasureSpec),getSize(250,heightMeasureSpec));
    }


有了默认的大小,我们开始画图。
画图的话要重写View中的onDraw方法:


@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mycanvas=canvas;//这个是画布
        Paint paint=new Paint();//画笔
        paint.setColor(Color.BLACK);//设置画笔的颜色
        paint.setStrokeWidth(density);//设置画笔画出线的粗细,
        paint.setAntiAlias(true);//防锯齿,使画出的线条更加的圆滑
        paint.setDither(true);//防抖动
        paint.setStyle(Paint.Style.STROKE);//设置画笔为空心
        paint.setStrokeCap(Paint.Cap.ROUND);//设置线冒样式,取值有Cap.ROUND(圆形线冒)、Cap.SQUARE(方形线冒)、Paint.Cap.BUTT(无线冒)
        paint.setStrokeJoin(Paint.Join.MITER);//设置线段连接处样式,取值有:Join.MITER(结合处为锐角)、Join.Round(结合处为圆弧)、Join.BEVEL(结合处为直线)
        canvas.drawPath(path,paint);//根据路径画线
    }



Path有几个方法:moveTo,lineTo,quadTo,cubicTo,arcTo


1.moveTo 不会进行绘制,只用于移动移动画笔。


2.lineTo  用于进行直线绘制。


3、quadTo  用于绘制圆滑曲线,即贝塞尔曲线

mPath.quadTo(x1, y1, x2, y2) (x1,y1) 为控制点,(x2,y2)为结束点。 控制点从字面上就表达清楚了,就是一条直线,根据控制点的位置进行弯曲,形成曲线


4、cubicTo 同样是用来实现贝塞尔曲线的。
mPath.cubicTo(x1, y1, x2, y2, x3, y3) (x1,y1) 为控制点,(x2,y2)为控制点,(x3,y3) 为结束点。
那么,cubicTo 和 quadTo 有什么不一样呢?
就是多了一个控制点而已。
然后,我们想绘制和上一个一样的曲线,应该怎么写呢?
mPath.moveTo(100, 500);
mPath.quadTo(300, 100, 600, 500);
canvas.drawPath(mPath, mPaint);
如果我们不加 moveTo 呢?
则以(0,0)为起点,(100,500)和(300,100)为控制点绘制贝塞尔曲线。


5、arcTo 用于绘制弧线(实际是截取圆或椭圆的一部分)。
mPath.arcTo(ovalRectF, startAngle, sweepAngle) , ovalRectF为椭圆的矩形,startAngle 为开始角度,sweepAngle 为结束角度。
mRectF = new RectF(10, 10, 600, 600);
mPath.arcTo(mRectF, 0, 90);
canvas.drawPath(mPath, mPaint);
由于new RectF(10, 10, 600, 600)为正方形,又截取 0 ~ 90 度 ,则所得曲线为四分之一圆的弧线。



然后我们来说下前面提到的density:

具体可以参考下这个帖子http://www.cnblogs.com/yaozhongxiao/archive/2014/07/14/3842908.html


dpi :dots per inch , 直接来说就是一英寸多少个像素点。常见取值 120,160,240。我一般称作像素密度,简称密度
density : 直接翻译的话貌似叫 密度。常见取值 1.5 , 1.0 。和标准dpi的比例(160px/inc)
  在android里面,获取一个窗口的metrics,里面有这么几个值
 metrics.density; 其实是 DPI / 160 后得到的值。
 metrics.densityDpi;就是我们常说的dpi。
我们写布局的时候,肯定还是要知道1个dp到底有多少px的。
  换算公式如下: dp = (DPI/(160像素/英寸))px = density px
  注意,这里都是带单位的。px是单位,dp是单位,density没单位。
  为了方便,假设dpi是240 像素/英寸 , 那么density就是1.5
  那么就是 dp=1.5px ,注意这是带了单位的,也就是 设备无关像素 = density 像素
  那么转换为数值计算的话,应该是下面这个式子
  PX = density * DP




最后,我们来看看怎么样将布局存成图像
首先要调用view.setDrawingCacheEnabled方法打开图像缓存,然后使用view.getDrawingCache 方法获取View的Bitmap对象。保存成jpg图像使用Bitmap.compress方法即可。


好,接下来,上代码:

自定义View:

package com.example.my_signature_app;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;



public class MySignatureView extends View {
    Canvas mycanvas;
    int density;
    float startx;
    float starty;
    Path path;
    public MySignatureView(Context context) {
        super(context);
    }

    public MySignatureView(Context context, AttributeSet attrs) {
        super(context, attrs);
        density= (int)context.getResources().getDisplayMetrics().density;
        path=new Path();

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(getSize(300,widthMeasureSpec),getSize(250,heightMeasureSpec));
    }
    private int getSize(int sizedp,int measureSpec){

        int finalsize = 0;
        int mode=MeasureSpec.getMode(measureSpec);
        int size = MeasureSpec.getSize(measureSpec);
        switch (mode){
            case MeasureSpec.AT_MOST:
                finalsize=sizedp*density;
                break;
            case MeasureSpec.EXACTLY:
                finalsize=size;
                break;
            case MeasureSpec.UNSPECIFIED:
                finalsize=sizedp*density;
                break;
        }
        return finalsize;

    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                drawStart(event.getX(),event.getY());

                break;
            case MotionEvent.ACTION_MOVE:
                drawMove(event.getX(),event.getY());
                break;
            case MotionEvent.ACTION_UP:
                break;

        }
        invalidate();
        return true;
    }

    private void drawStart(float x, float y) {
        path.moveTo(x, y);
        startx=x;starty=y;
    }
    private void drawMove(float x,float y){
        path.quadTo(startx,starty,(startx+x)/2,(starty+y)/2);//曲线终点设置为startx/starty和x/y的一半,这样画出的线更圆滑
        startx=x;starty=y;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mycanvas=canvas;
        Paint paint=new Paint();
        paint.setColor(Color.BLACK);
        paint.setStrokeWidth(density);
        paint.setAntiAlias(true);
        paint.setDither(true);
        paint.setStyle(Paint.Style.STROKE);//设置画笔为空心
        paint.setStrokeCap(Paint.Cap.ROUND);//设置线冒样式,取值有Cap.ROUND(圆形线冒)、Cap.SQUARE(方形线冒)、Paint.Cap.BUTT(无线冒)
        paint.setStrokeJoin(Paint.Join.MITER);//设置线段连接处样式,取值有:Join.MITER(结合处为锐角)、Join.Round(结合处为圆弧)、Join.BEVEL(结合处为直线)
        canvas.drawPath(path,paint);
    }
}



Activity:

public class MainActivity extends AppCompatActivity {
    private Button btn;
    private ImageView imageView;
    private String path="/sdcard/test.jpg";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        btn=(Button)findViewById(R.id.signature_btn);
        imageView=(ImageView)findViewById(R.id.signature_iv);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                showDialog();
            }
        });
    }
    private void showDialog(){
        final Dialog dialog=new Dialog(MainActivity.this);
        dialog.setTitle("请签名");
        final Window window=dialog.getWindow();
        window.setContentView(R.layout.dialog);
        Button cancel=(Button)window.findViewById(R.id.cancel_btn);
        Button ok=(Button)window.findViewById(R.id.ok_btn);
       final MySignatureView signatureview=(MySignatureView)window.findViewById(R.id.signature);
        cancel.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                dialog.dismiss();
            }
        });
        ok.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                signatureview.setDrawingCacheEnabled(true);//打开图像缓存
                Bitmap bitmap=signatureview.getDrawingCache();
                try{
                    //获取可视组件的截图
                    //将截图保存在SD卡根目录的test.png图像文件中
                    File file=new File(path);
                    if(file.exists()){
                        file.delete();
                    }
                    FileOutputStream fos=new FileOutputStream(path);
                    //将bitmap对象中的图像数据压缩成png格式的图像数据,并将这些数据保存在test.png文件中
                    bitmap.compress(Bitmap.CompressFormat.PNG,100,fos);
                    //关闭文件流输出
                    fos.close();
                }catch (Exception e){
                    Log.e("error",e.getMessage());
                }
                Bitmap bm = BitmapFactory.decodeFile(path);
                DisplayMetrics dm = new DisplayMetrics();
                getWindowManager().getDefaultDisplay().getMetrics(dm);
                int screenWidth=dm.widthPixels;
                if(bm.getWidth()<=screenWidth){
                    imageView.setImageBitmap(bm);
                }else{
                    Bitmap bmp=Bitmap.createScaledBitmap(bm, screenWidth, bm.getHeight()*screenWidth/bm.getWidth(), true);
                    imageView.setImageBitmap(bmp);
                }
                dialog.dismiss();
            }
        });
        dialog.show();
    }
}


Activity的布局:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.my_signature_app.MainActivity">

    <ImageView
        android:id="@+id/signature_iv"
        android:layout_width="350dp"
        android:layout_height="300dp"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true" />

    <Button
        android:id="@+id/signature_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="30dp"
        android:background="@android:color/holo_blue_dark"
        android:text="点我签名哟"
        android:textColor="@android:color/white" />

</RelativeLayout>



签名板弹窗的布局:

<?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">
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <com.example.my_signature_app.MySignatureView
            android:id="@+id/signature"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:background="#ffffff" />
    </RelativeLayout>

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:layout_above="@+id/signature"
        android:background="#c8c8c8" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/signature"
        android:layout_marginBottom="30dp"
        android:layout_marginTop="20dp"
        android:gravity="center"
        android:orientation="horizontal">

        <Button
            android:id="@+id/cancel_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginRight="40dp"
            android:text="取消" />

        <Button
            android:id="@+id/ok_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="确定" />
    </LinearLayout>
</LinearLayout>


看源码请点击这里:http://download.youkuaiyun.com/detail/aa_chao/9695364
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值