一:屏幕的尺寸信息
Android手机屏幕分辨率,大小五花八门,千奇百怪,要在不同屏幕上保持绘图的准确性,必须对这些屏幕有充分的认识。
1.屏幕参数
- 屏幕大小 :对角线的长度,用“寸”来度量,例如4.7寸,5.1寸手机。
- 分辨率 :分辨率是指手机屏幕像素点的个数,例如720*1280就是指屏幕的分辨率,指宽有720个像素点,而高有1280个像素点。
- PPI :每英寸像素(Pixels per Inch),又被称为Dpi(dots per Inch)。它是由对角线的像素点数除以屏幕的大小得到的,通常达到400PPI就是非常高的屏幕密度了。
2.系统屏幕密度
Android系统使用mdpi即密度为160的屏幕作为标准。各个分辨率的直接换算比例ldpi:mdpi:hdpi:xhdpi:xxhdpi = 3:4:6:8:12。
4.单位转换
public class DisplayUtil {
/**
* 将px值转换为dip或dp值,保证尺寸大小不变
*
* @param context
* @param pxValue
* @return
*/
public static int px2dip(Context context, float pxValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5f);
}
/**
* 将dip或dp转换为px值,保证尺寸大小不变
*
* @param context
* @param dipValue
* @return
*/
public static int dip2px(Context context, float dipValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dipValue * scale + 0.5f);
}
/**
* px转换为sp
*
* @param context
* @param pxValue
* @return
*/
public static int px2sp(Context context, float pxValue) {
final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (pxValue / fontScale + 0.5f);
}
public static int sp2px(Context context, float spValue) {
final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue * fontScale + 0.5f);
}
}
TypedValue也可以帮助我们转换
/**
* dp2px
*
* @param context
* @param dp
* @return
*/
protected int dp2px(Context context, int dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
dp, context.getResources().getDisplayMetrics());
}
/**
* sp2px
*
* @param context
* @param sp
* @return
*/
protected int sp2px(Context context, int sp) {
return (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
sp,context.getResources().getDisplayMetrics());
}
二:2D绘图基础
setAntiAlias(); //设置画笔的锯齿效果
setColor(); //设置画笔的颜色
setARGB;//设置画笔的ARGB值;
setAlpha();//设置画笔的Alpha值
setTextSize();//设置字体的尺寸
setStyle();/设置画笔的风格(实习,或者空心)
setStrokeWidth()//设置空心边框的宽度
常用基本2d自绘图形
public class BasicView {
public static class point extends View {
public point(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint();
paint.setColor(0xFFFFFF);
canvas.drawPoint((float) getWidth() / 2, (float) getHeight() / 2, paint);
}
}
public static class singleLine extends View {
public singleLine(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint();
canvas.drawLine(0, (float) getHeight() / 2, (float) (getWidth() * 0.75),
(float) getHeight() / 2, paint);
}
}
public static class lines extends View {
public lines(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float[] pts = {
0, 0, getWidth(), getHeight() / 3,
getWidth(), getHeight() / 3, 0, getHeight() / 3,
0, getHeight() / 3, getWidth(), getHeight() / 2
};
canvas.drawLines(pts, new Paint());
}
}
public static class Rect extends View {
public Rect(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(0, (float)
getHeight() / 4, (float) (getWidth() * 0.9),
(float) (getHeight() * 0.9), new Paint());
}
}
public static class RoundRect extends View {
public RoundRect(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRoundRect(0, (float)
getHeight() / 4, (float) (getWidth() * 0.9),
(float) (getHeight() * 0.9), (float) 2, (float) 2, new Paint());
}
}
public static class Arc extends View {
public Arc(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawArc((float) 5, (float) 5,
(float) (getWidth() / 2), (float) (getHeight() / 2), 90,
160, false, new Paint());
canvas.drawArc((float) (getWidth() / 2), (float) (getHeight() / 2),
(float) (getWidth() - 5), (float) (getHeight() - 5), 90,
160, true, new Paint());
}
public static class Oval extends View {
public Oval(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawOval(5, 5, getWidth() - 5, getHeight() - 5, new Paint());
}
}
/**
* 指定位置绘制文本
*/
public static class PosText extends View {
public PosText(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint mPaint = new Paint();
mPaint.setColor(0xFFFFFF);
canvas.drawPosText("Hell",
new float[]{
(float) (getWidth() * 0.2), (float) (getHeight() * 0.2),
(float) (getWidth() * 0.4), (float) (getHeight() * 0.4),
(float) (getWidth() * 0.6), (float) (getHeight() * 0.6),
(float) (getWidth() * 0.8), (float) (getHeight() * 0.8),
}, mPaint);
}
}
public static class Path extends View {
public Path(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint mPaint = new Paint();
mPaint.setColor(0xFFFFFF);
android.graphics.Path path = new android.graphics.Path();
path.moveTo(5, 5);
path.lineTo((float) (getWidth() * 0.8), (float) (getHeight() * 0.8));
canvas.drawPath(path, mPaint);
}
}
}
}
三.Android XML绘图
1.BitMap
可以在drawable目录创建XML文件
<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@drawable/ic_launcher_background" />
2.Shape
Shape可以在XML中绘制各种形状
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!--默认是rectangle-->
<!--当shape= rectangle的时候使用-->
<corners
android:bottomLeftRadius="1dp"
android:bottomRightRadius="1dp"
android:radius="1dp"
android:topLeftRadius="1dp"
android:topRightRadius="1dp" />
<!--半径,会被后面的单个半径属性覆盖,默认是1dp-->
<!--渐变-->
<gradient
android:angle="1dp"
android:centerColor="@color/colorAccent"
android:centerX="1dp"
android:centerY="1dp"
android:gradientRadius="1dp"
android:startColor="@color/colorAccent"
android:type="linear"
android:useLevel="true" />
<!--内间距-->
<padding
android:bottom="1dp"
android:left="1dp"
android:right="1dp"
android:top="1dp" />
<!--大小,主要用于imageview用于scaletype-->
<size
android:width="1dp"
android:height="1dp" />
<!--填充颜色-->
<solid android:color="@color/colorAccent" />
<!--指定边框-->
<stroke
android:width="1dp"
android:color="@color/colorAccent" />
<!--虚线宽度-->
android:dashWidth= "1dp"
<!--虚线间隔宽度-->
android:dashGap= "1dp"
</shape>
实现一个阴影效果
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="225"
android:endColor="#000000"
android:startColor="#FF0000" />
<padding
android:bottom="7dp"
android:left="7dp"
android:right="7dp"
android:top="7dp" />
<corners android:radius="8dp" />
</shape>
3.Layer
Android中可以用layer实现图片层效果
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/beauty2"/>
<item android:drawable="@drawable/beauty"
android:left="200dp"
android:right="200dp"
android:top="200dp"
android:bottom="200dp"/>
</layer-list>
4.Selector
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 默认时候的背景-->
<item android:drawable="@mipmap/ic_launcher" />
<!-- 没有焦点时候的背景-->
<item android:drawable="@mipmap/ic_launcher" android:state_window_focused="false" />
<!-- 非触摸模式下获得焦点并点击时的背景图片-->
<item android:drawable="@mipmap/ic_launcher" android:state_pressed="true" android:state_window_focused="true" />
<!-- 触摸模式下获得焦点并点击时的背景图片-->
<item android:drawable="@mipmap/ic_launcher" android:state_focused="false" android:state_pressed="true" />
<!--选中时的图片背景-->
<item android:drawable="@mipmap/ic_launcher" android:state_selected="true" />
<!--获得焦点时的图片背景-->
<item android:drawable="@mipmap/ic_launcher" android:state_focused="true" />
</selector>
应用示例
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<!--填充颜色-->
<solid android:color="#33444444" />
<!--设置按钮的四个角为弧形-->
<corners android:radius="5dp" />
<!--间距-->
<padding android:bottom="10dp" android:left="10dp" android:right="10dp" android:top="10dp" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<!--填充颜色-->
<solid android:color="#FFFFFF" />
<!--设置按钮的四个角为弧形-->
<corners android:radius="5dp" />
<!--间距-->
<padding android:bottom="10dp" android:left="10dp" android:right="10dp" android:top="10dp" />
</shape>
</item>
</selector>
四.绘图技巧
1.Canvas
常用方法
Canvas.save()
Canvas.restore()
Canvas.translate()
Canvas.rotate()首先,我们来看一下前面两个方法
在讲解这两个方法之前,首先来了解一下Android绘图的坐标体系,这个其实这个前面已经讲了,这里不赘述,而Canvas.save()这个方法,从字面上的意思可以理解为保存画布,他的作用就是讲之前的图像保存起来,让后续的操作能像在新的画布一样操作,这跟PS的图层基本差不多.而Canvas.restore()这个方法,则可以理解为合并图层,,就是讲之前保存下来的东西合并。而后面两个方法尼?从字母上理解画布平移或者旋转,但是把他理解为坐标旋转更加形象,前面说了,我们绘制的时候默认坐标点事左上角的起始点,那么我们调用translate(x,y)之后,则将原点(0,0)移动到(x,y)之后的所有绘图都是在这一点上执行的,这里可能说的不够详细,最典型的例子是画一个表盘了,那我们这里就演示一下画一个表盘
public class Dashboard extends View {
private Paint paintCircle;
private float mWidth;
private float mHeight;
private Paint painDegree;
private Paint paintHour;
private Paint paintMinute;
public Dashboard(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
// 获取屏幕的宽高
WindowManager wm = (WindowManager) getContext().getSystemService(
Context.WINDOW_SERVICE);
mWidth = wm.getDefaultDisplay().getWidth();
mHeight = wm.getDefaultDisplay().getHeight();
paintCircle = new Paint();
paintCircle.setStyle(Paint.Style.STROKE);
paintCircle.setAntiAlias(true);
paintCircle.setStrokeWidth(5);
painDegree = new Paint();
paintHour = new Paint();
paintMinute = new Paint();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(mWidth / 2, mHeight / 2, mWidth / 2, paintCircle);
painDegree.setStrokeWidth(3);
for (int i = 0; i < 24; i++) {
//区分整点与非整点
if (i == 0 || i == 6 || i == 12 || i == 18) {
painDegree.setStrokeWidth(5);
painDegree.setTextSize(30);
canvas.drawLine(mWidth / 2, mHeight / 2 - mWidth / 2,
mWidth / 2, mHeight / 2 - mWidth / 2 + 60, painDegree);
String degree = String.valueOf(i);
canvas.drawText(degree, mWidth / 2 - painDegree.measureText(degree) / 2,
mHeight / 2 - mWidth / 2 + 90, painDegree);
} else {
painDegree.setStrokeWidth(3);
painDegree.setTextSize(15);
canvas.drawLine(mWidth / 2, mHeight / 2 - mWidth / 2,
mWidth / 2, mHeight / 2 - mWidth / 2 + 30, painDegree);
String degree = String.valueOf(i);
canvas.drawText(degree, mWidth / 2 - painDegree.measureText(degree) / 2,
mHeight / 2 - mWidth / 2 + 60, painDegree);
}
canvas.rotate(15, mWidth / 2, mHeight / 2);
}
paintHour.setStrokeWidth(20);
paintMinute.setStrokeWidth(10);
canvas.save();
canvas.translate(mWidth / 2, mHeight / 2);
canvas.drawLine(0, 0, 100, 100, paintHour);
canvas.drawLine(0, 0, 100, 200, paintMinute);
canvas.restore();
}
}
4.2Layer图层
在Android中使用saveLayer()方法来创建一个图层,图层同样是基于栈的结构进行管理的。
Android通过调用saveLayer()方法,saveLayerAlpha()方法将一个图层入栈,使用restore()方法,restoreToCount()方法将一个图层出栈。入栈的时候,后面所有的操作都发生在这个图层上,而出栈的时候,则会把图像绘制到上层Canvas上。
使用Layer示例
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.WHITE);
paint.setColor(Color.BLUE);
canvas.drawCircle(150,150,100, paint);
canvas.saveLayerAlpha(0,0,400,400,127,LAYER_TYPE_NONE);
paint.setColor(Color.RED);
canvas.drawCircle(200,200,100, paint);
canvas.restore();
}
分别设置LayerAlpha为0,127,255的效果图如下
五:Android图像处理之色彩特效处理
Android对于图片的处理,最常使用位图—Bitmap,它包含了一张图片的所有数据。整个图片由点阵和颜色值组成,点阵就是一个包含像素矩阵,每一个元素对应着一个图片的像素。而颜色值----ARGB,分别对应透明度,红,绿,蓝这四个通道分量,它们共同决定了每个像素点显示的颜色。
1.色彩矩阵分析
色彩处理通常从以下三个角度来描述一个图像
色调----物体传播的颜色。
饱和度----颜色的纯度,从0(灰)到100%(饱和)来进行描述。
亮度-----颜色的相对明暗程度。Android系统使用一个4*5的数字矩阵来描述颜色矩阵
图中矩阵A就是一个4X5的颜色矩阵,在Android中,他们会以一段一维数组的形式来存储[a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t],则C就是一个颜色矩形分量,在处理图像中,使用矩阵乘法运算AC处理颜色分量矩阵如下图
矩阵运算的乘法公式相信大家都在线性代数课程中学过,计算过程如下
从这个公式我们可以发现
归纳如下
- 第一行的abcde用来决定新的颜色值R——红色
- 第二行的fghij用来决定新的颜色值G——绿色
- 第三行的kimno用来决定新的颜色值B——蓝色
- 第四行的pqrst用来决定新的颜色值A——透明
这样划分好各自的范围之后,这些值就比较慢明确了,不过只这样说可能会比较抽象,我们通过几个小例子不断的去分析首先,我们来看一下矩阵变换的计算公式,以R分量为例,计算过程是
R1 = a * R + b* G + c*B+d *A + e
如果把这个矩阵公式带入R1 = AC,那么根据矩阵的乘法运算法则,可以得到R1 = R;因此,这个矩阵通常是用来作为初始的颜色矩阵来使用,他不会对原有颜色进行任何改变那么当我们要变换颜色值的时候通常有两种方法,一个是直接改变颜色的offset,即修改颜色的分量。另一种方法直接改变RGBA值的系数来改变颜色分量的值。
1)改变颜色偏移量
在上面这个矩阵中,我们修改了R,G,所对应的颜色偏移量,那么最后的处理结果,就是图像的红色绿色分别增加了100,而我们知道,红色混合绿色的到了黄色,所以最终的颜色处理结果就是让整个图片的色调偏黄色
2)改变颜色系数
在上面这个矩阵中改变了G分量所对应的系数据g,这样在矩形运算后G分量会变成以前的两倍,最终效果就是图像的色调更加偏绿。
5.1.3改变色光属性
在Android中,系统封装了一个类-ColorMatrix,也就是前面所说的颜色矩阵。同过这个类可以很方便地改变矩阵来处理颜色效果。创建一个ColorMatrix代码如下
ColorMatrix colorMatrix = new ColorMatrix();
有了它就可以进行各种处理了
- 色调
系统使用0,1,2来代表Red,Green,Blue三种颜色的处理,第二个参数就是需要处理的值。
通过下面代码的方法可以为RGB三种颜色分量分别重新设置了不同的色调值。
ColorMatrix hueMatrix= new ColorMatrix();
hueMatrix.setRotate(0,hue0);
hueMatrix.setRotate(1,hue1);
hueMatrix.setRotate(2,hue2);
- 饱和度
饱和度为0就是灰度图像。
ColorMatrix saturationMatrix= new ColorMatrix();
saturationMatrix.setSaturation(saturation);
- 亮度
当三原色以相同比例混合的时候,就会显示白色。系统同样以这个原理改变图像的亮度,当亮度为0时,图像全黑。
ColorMatrix lumMatrix= new ColorMatrix();
lumMatrix.setScale(rScale,gScale,bScale,aScale);
- 系统封装了矩阵的乘法运算,使用postConcat()方法将矩阵的作用效果混合。从而叠加处理效果。
一个小引用:滑动SeekBar改变图像
Activity
public class AdjustPicActivity extends AppCompatActivity {
private SeekBar seekBar1;
private SeekBar seekbar2;
private SeekBar seekbar3;
private AdjustPicActivity.mySeekbarListener mySeekbarListener;
private final static double MID_VALUE = 50;
private float mHue;
private float mStauration;
private float mLum;
private ImageView imageView;
private Bitmap bitmap;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_adjust_pic);
imageView = findViewById(R.id.img);
seekBar1 = findViewById(R.id.tone_seekbar);
seekbar2 = findViewById(R.id.saturation_seekbar);
seekbar3 = findViewById(R.id.luminance_seekbar);
mySeekbarListener = new mySeekbarListener();
seekBar1.setOnSeekBarChangeListener(mySeekbarListener);
seekbar2.setOnSeekBarChangeListener(mySeekbarListener);
seekbar3.setOnSeekBarChangeListener(mySeekbarListener);
bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.seekbar_bg_pic);
}
class mySeekbarListener implements SeekBar.OnSeekBarChangeListener {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
switch (seekBar.getId()) {
case R.id.tone_seekbar:
mHue = (float) ((progress - MID_VALUE) * 1.0F / MID_VALUE * 180);
break;
case R.id.saturation_seekbar:
mStauration = (float) (progress * 1.0F / MID_VALUE);
break;
case R.id.luminance_seekbar:
mLum = (float) (progress * 1.0F / MID_VALUE);
break;
}
imageView.setImageBitmap(ImageHelper.handleImageEffect(bitmap,mHue,mStauration,mLum));
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
}
}
对应Activity的xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="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=".ch5.DragActivity">
<ImageView
android:id="@+id/img"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:src="@drawable/seekbar_bg_pic" />
<SeekBar
android:id="@+id/tone_seekbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginEnd="40dp"
android:max="100"
android:maxHeight="15dp"
android:minHeight="15dp"
android:paddingTop="60dp"
android:progress="50"
android:thumb="@drawable/thumb" />
<SeekBar
android:id="@+id/saturation_seekbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginEnd="40dp"
android:max="100"
android:maxHeight="15dp"
android:minHeight="15dp"
android:paddingTop="30dp"
android:progress="50"
android:progressDrawable="@drawable/seekbar_bg"
android:thumb="@drawable/thumb" />
<SeekBar
android:id="@+id/luminance_seekbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginEnd="40dp"
android:max="100"
android:maxHeight="15dp"
android:minHeight="15dp"
android:paddingTop="30dp"
android:progress="50"
android:thumb="@drawable/thumb" />
</LinearLayout>
5.5Android颜色矩阵—ColorMatrix
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="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=".CH6.section6.ColorMatrixActivity">
<ImageView
android:id="@+id/colorMatrix_img"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2" />
<GridLayout
android:id="@+id/group"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="3"
android:columnCount="5"
android:rowCount="4" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="btnChange"
android:text="change" />
<Button
android:onClick="btnReset"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="change" />
</LinearLayout>
</LinearLayout>
public class ColorMatrixActivity extends AppCompatActivity {
private Bitmap bitmap;
private ImageView mImageView;
private GridLayout mGroup;
private int mEtWidth;
private int mEtHeight;
private EditText[] mEts;
private float[] mColorMatrix;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_color_matrix);
mEts = new EditText[20];
mColorMatrix = new float[20];
bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.seekbar_bg_pic);
mImageView = findViewById(R.id.colorMatrix_img);
mGroup = findViewById(R.id.group);
mImageView.setImageBitmap(bitmap);
mGroup.post(new Runnable() {
@Override
public void run() {
mEtWidth = mGroup.getWidth() / 5;
mEtHeight = mGroup.getHeight() / 4;
addEts();
initMatrix();
}
});
}
private void initMatrix() {
for (int i = 0; i < 20; i++) {
if (i % 6 == 0) {
mEts[i].setText(String.valueOf(1));
} else {
mEts[i].setText(String.valueOf(0));
}
}
}
private void addEts() {
for (int i = 0; i < 20; i++) {
EditText editText = new EditText(this);
mEts[i] = editText;
mGroup.addView(editText, mEtWidth, mEtHeight);
}
}
public void btnChange(View v) {
getMatrix();
setImageMatrix();
}
public void btnReset(View v) {
initMatrix();
getMatrix();
setImageMatrix();
}
public void getMatrix() {
for (int i = 0; i < 20; i++) {
mColorMatrix[i] = Float.valueOf(mEts[i].getText().toString());
}
}
public void setImageMatrix() {
Bitmap mBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
ColorMatrix colorMatrix = new ColorMatrix(mColorMatrix);
Canvas canvas = new Canvas(mBitmap);
Paint paint = new Paint();
paint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));
canvas.drawBitmap(bitmap, 0, 0, paint);
mImageView.setImageBitmap(mBitmap);
}
}
5.3常用图像颜色处理效果
特定算法的颜色矩阵的具体矩阵如何的略了
5.4像素点分析
作为更加精确的图像处理方式,可以通过改变每个像素点的具体ARGB值,来达到处理一张图像效果的目的。但传递进来的原始图片是不能修改的,一般根据原始图片生成一张新的图片来修改。
在Android中,系统提供了Bitmap.getPixels()方法来帮为我们提取整个Bitmap中的像素点,并保存到一个数组中,该方法如下所示。
public void getPixels(@ColorInt int[] pixels, int offset, int stride,
int x, int y, int width, int height){
...................
}
各个参数的含义
- pixels 接收位图颜色值的数组
- offset 写入到pixels[]中的第一个像素索引值
- stride—pixels[]中的行间距
- x 从位图中读取的第一个像素的x坐标值
- y 从位图中读取的第一个像素的y坐标值
- width 从每一行中读取的像素宽度
- height 读取的行数
通常情况下,可以使用如下代码
bitmap.getpixels(oldPx,0,bm.getWidth,0,0,width,height)
接下来可以获取到每个像素具体的ARGB值了
color = oldPx[i];
r = Color.red(color);
g = Color.green(color);
b = Color.blue(color);
a = Color.alpha(color);
获取到具体的颜色值之后,就可以通过相应的算法来修改它的ARGB值,从而来重构一张新的图像。当然,这些算法都是前辈们研究,总结出来的图像处理算法,由于我们不是专业处理人员,直接用。例如
r1 = (int)(0.393*r + 0.769*g + 0.189*b);
g1 = (int)(0.349*r + 0.686*g + 0.168*b);
b1 = (int)(0.272*r + 0.534*g +0.131*b);
合成新的像素点
newPx[i] = Color.argb(a,r1,g1,b1)
最后将处理之后的像素点重新设置给Bitmap,从而达到图像处理的目的。
bmp.setPixels(newPx,0,width,0,0,width,height);
5.5常用图像处理效果
处理方法是专业人士研究成果,通过特定的算法对每个像素点进行处理,就可以得到不同的处理效果。
5.5.1 底片效果
若存在ABC三个像素点,要求B点对应的底片效果算法,代码如下。
B.r = 255 - B.r;
B.g = 255 - B.g;
B.b = 255- B.b;
关键处理代码
//就是对每一个点都像上面B点那样处理
public static Bitmap handleImageNegative(Bitmap bm) {
int width = bm.getWidth();
int height = bm.getHeight();
int color;
int r, g, b, a;
Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
int[] oldPx = new int[width * height];
int[] newPx = new int[width * height];
bm.getPixels(oldPx, 0, width, 0, 0, width, height);
for (int i = 0; i < width * height; i++) {
color = oldPx[i];
r = Color.red(color);
g = Color.green(color);
b = Color.blue(color);
a = Color.alpha(color);
r = 255 - r;
g = 255 - g;
b = 255 - b;
if (r > 255) {
r = 255;
} else if (r < 0) {
r = 0;
}
if (g > 255) {
g = 255;
} else if (g < 0) {
g = 0;
}
if (b > 255) {
b = 255;
} else if (g < 0) {
b = 0;
}
newPx[i] = Color.argb(a, r, g, b);
}
bmp.setPixels(newPx, 0, width, 0, 0, width, height);
return bmp;
}
5.5.2 老照片效果
求某像素点老照片效果算法
r1 = (int)(0.393*r+0.769*g+0.189*b);
g1 = (int)(0.349*r+0.686*g+0.168*b);
b1 = (int)(0.272*r+0.534*g+0.131*b);
5.5.3浮雕效果
若存在ABC三个像素点,要求B点对应的浮雕效果算法,代码如下
B.r = C.r-B.r+127;
B.g = C.g-B.g+127;
B.b = C.b-B.b+127;
六:Android图像处理之图形特效处理
前面是图像的色彩处理技巧,这一节是图形图像处理技巧。
6.1Android变形矩阵----Matrix
对于色彩处理有ColorMatrix,对于图像图形处理同样是矩阵,每个像素点都表达了其坐标的X,Y信息。Android图形变换矩阵是一个33的矩阵,如图
当使用变换矩阵去处理每一个像素点的时候与颜色矩阵的矩阵乘法一样,计算公式如下
X1 = a * X + b * Y + c
Y1 = d * X + e * Y + f
1 = g * X + h * Y + i
通常情况下,会让g = h =0,i = 1,这样就使 1 = gX+h*Y +i恒成立。因此只需要重点关注上面几个参数就行了。
图形变换矩阵的初始矩阵,对角线元素a e i为1,其他元素为0的矩阵
图像的变形处理的四类基本变换
- Transslate-----平移变换
- Rotate------旋转变换
- Scale ------缩放变换
- Skew ----错切变换
6.1.1平移变换
由上图有
X = X0+ΔX
Y = Y0+ΔY
6.1.2旋转变换
旋转变换矩阵
如果以任意点O为中心来旋转变换,通常需要以下三步骤
- 将坐标原点平移到O点
- 使用前面讲的以坐标原点为中心的旋转方法进行旋转变换
- 将坐标原点还原
6.1.3缩放变换
一个像素点使不存在缩放的概念的,但是由于图像是由很多个像素点组成的,如果将每个点的坐标都进行相同比例的缩放,最终就会让整个图像形成缩放的效果,缩放公式如下
`x=K1 * x0
y = K2 * y0
如果写成矩阵形式,就如下
`6.1.4错切变换
错切变换让所有点的X坐标(或者Y坐标)保持不变,而对应的y坐标(或者x坐标)按比例发生平移,且平移的大小和该点到X轴或y轴的垂直距离成正比
变换公式如下
x = x0 + k1 + y0
y = k2 x x0 + y0
变形矩阵代码和前面颜色矩阵类似一致。在图形变换中通过一个一维数组来模拟矩阵,并通过setValues()方法将一个一维数组转换为图形变换矩阵,代码如下。
private float[] mimageMatrix = new float[9];
Matrix matrix = new Matrix();
matrix.setValues(mImageMatrix);
当获得了变换矩阵后就可以将一个变换矩阵对应的图像画出来了
canvas.drawBitmap(mBitmap,matrix,null);
与色彩矩阵一样,图形变换矩阵Android系统也封装了系统API,Android中使用Matrix来封装矩阵,使用API方法不用去设置矩阵的每一个元素值。
- matriX.setRotate()旋转变换
- matriX.setTranslate()平移变换
- matriX.setScale缩放变换
- matriX.setSkew错切变换
- pre()和post()提供矩阵的前乘和后乘运算
Matrix的set方法会重置矩阵中的所有值,而post与pre则不会,这两个方法常用来实现矩阵的混合作用。不过矩阵不满足交换律,所以矩阵的前乘运算与后乘运算是两种不同的运算。
举个例子
- 先平移到(300,100)
- 再旋转45度
- 最后平移到(200,200)
如果使用后乘运算,表示当前矩阵乘上参数代表的矩阵,代码如下
matriX.setRotate(45);
matrxX.postTranslate(200,200)
如果使用后乘运算,,表示当前矩阵乘上参数代表的矩阵,代码如下
matriX.setTanslate(200,200);
matriX.preRotate(45);
6.2像素块分析
drawBitmapMesh()与操纵像素点来改变颜色的原理类似,只不过是把图像分成了一个个的小块,然后通过改变每一个图像块来修改整个图像。,该方法代码如下
public void drawBitmapMesh(@NonNull Bitmap bitmap, int meshWidth, int meshHeight,
@NonNull float[] verts, int vertOffset, @Nullable int[] colors, int colorOffset,
@Nullable Paint paint){...}
关键参数如下
- bitmap :将要扭曲的图像
- meshWidth:需要的横向网格数目
- meshHeight:需要的纵向网格数目
- verts:网格交叉点坐标数组
- vertOffset:verts数组中开始跳过的(x,y)坐标对的数目。
最重要的参数是一个数组-----verts
要使用drawBitmapMesh()方法先要将图片分割为若干个图像块。所以,在图像上横纵各画N-1条线,将图像分成NN块,而横纵各N条线就交织成了NN个点,而每个点的坐标则以x1,y,x2,y2,…,xn,yn的形式保存在verts数组中。这个数组每两个数保存一个交织点,第一个是横坐标,第二个是纵坐标。而整个drawBitmapMesh()方法改变图像的方式,就是靠这些坐标值的改变来重新定位每一个图像块,从而达到图像效果处理的功能。
例子drawBitmapMesh()方法实现旗帜飞扬效果
旗帜飞扬核心算法思想:
图片每个交织点的横坐标较之前不发生变化,而纵坐标较之前呈现一个三角函数的周期性变化
public class WaveFlag extends View {
private Bitmap bitmap;
private final static int HEIGHT = 80;
private final static int WIDTH = 60;
float[] orig, verts;
private final float A = 3.0f;
private double k ;
public WaveFlag(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.seekbar_bg_pic);
orig = new float[((WIDTH + 2) * 2 * HEIGHT + 1)];
verts = new float[(WIDTH + 2) * 2 * HEIGHT + 1];
float bitmapWidth = bitmap.getWidth();
float bitmapHeight = bitmap.getHeight();
int index = 0;
for (int y = 0; y <= HEIGHT; y++) {
float fy = bitmapHeight * y / HEIGHT;
for (int x = 0; x <= WIDTH; x++) {
float fx = bitmapWidth * x / WIDTH;
orig[index * 2 + 0] = verts[index * 2 + 0] = fx;
//这里人为将坐标+100是为了让图像下移,避免扭曲后被屏幕遮挡
orig[index * 2 + 1] = verts[index * 2 + 1] = fy + 100;
index += 1;
}
}
}
public void flagWave() {
for (int j = 0; j < HEIGHT; j++) {
for (int i = 0; i <= WIDTH; i++) {
verts[(j * (WIDTH + 1) + i) * 2 + 0] += 0;
float offsetY = (float) Math.sin((float) i / WIDTH * 2 * Math.PI + Math.PI * k);
verts[(j*(WIDTH+1)+i)*2+1] = orig[(j*WIDTH+i)*2+1]+offsetY* A;
}
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
flagWave();
k+=0.1F;
canvas.drawBitmapMesh(bitmap,WIDTH,HEIGHT,verts,0,null,0,null);
invalidate();
}
}
6.7Android图像处理之画笔特效处理
见我的另一篇文章Android实现刮刮卡。
6.7.2 Shader
Shader又被称为着色器,渲染器,它用来实现一系列渲染的效果。Android中的Shader
- BitmapShader—位图Shader
- LinearGradient-----线性Shader
- RadialGradient----光束Shader
- SeepGradient----梯度Shader
- ComposeShader----混合Shader
除了第一个Shader以外,其他Shader功能很名副其实(正常)。而BitmapShader它的作用是通过Paint对画布进行指定Bitmap的填充,填充时有以下几种模式可以选择。
- CLAMP拉伸----拉伸的是图片最后的那一个像素,不断重复
- REPEAT重复-----横向,纵向不断重复
- MIRROR镜像------横向不断翻转重复,纵向不断翻转重复
一个BitmapShader的小应用
public class ShaderCustomView extends View {
private Bitmap mBitmap;
private BitmapShader mBitmapShader;
private Paint mPaint;
public ShaderCustomView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.seekbar_bg_pic);
mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
mPaint = new Paint();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setShader(mBitmapShader);
canvas.drawCircle(500, 250, 200, mPaint);
}
}
若把CLAMP模式改为REPEATE用小图标替换,就是下面这种了
LinerGradient的小应用
public class ShaderCustomView extends View {
private Bitmap mBitmap;
private BitmapShader mBitmapShader;
private Paint mPaint;
public ShaderCustomView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.icon_search);
// mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
mPaint = new Paint();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// mPaint.setShader(mBitmapShader);
mPaint.setShader(new LinearGradient(0, 0, 400, 400, Color.BLUE, Color.YELLOW, Shader.TileMode.REPEAT));
canvas.drawRect(0, 0, 400,400, mPaint);
}
}
这里将绘图大小与渲染器大小设置区域大小相等,所以看不出REPEATE效果,如果把canvas绘制的图形设置为0 0 800 800REPEATE效果如下
水中倒影LinearGradient与PoterDuffXfermode结合使用
public class ReflectView extends View {
private Bitmap mSrcBitmap;
private Bitmap mRefBitmap;
private Paint mPaint;
private PorterDuffXfermode mXfermode;
public ReflectView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initRes(context);
}
private void initRes(Context context) {
mSrcBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.seekbar_bg_pic);
Matrix matrix = new Matrix();
matrix.setScale(1F, -1F);
mRefBitmap = Bitmap.createBitmap(mSrcBitmap, 0, 0,
mSrcBitmap.getWidth(), mSrcBitmap.getHeight(), matrix, true);
mPaint = new Paint();
mPaint.setShader(new LinearGradient(0, mSrcBitmap.getHeight(), 0, (float) (mSrcBitmap.getHeight() * 1.25),
0XDD000000, 0X10000000, Shader.TileMode.CLAMP));
mXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.BLACK);
canvas.drawBitmap(mSrcBitmap, 0, 0, null);
canvas.drawBitmap(mRefBitmap, 0, mSrcBitmap.getHeight(), null);
mPaint.setXfermode(mXfermode);
canvas.drawRect(0, mSrcBitmap.getHeight(), mRefBitmap.getWidth(), mSrcBitmap.getHeight() * 2, mPaint);
mPaint.setXfermode(null);
}
}
7.3PathEffect
Android提供的几种绘制PathEffect方式:
没效果
CornerPathEffect:拐弯角变得圆滑
DiscretePathEffect:线段上会产生许多杂点
DashPathEffect:绘制虚线,用一个数据来设置各个点之间的间隔
PathDashPathEffect:绘制虚线,可以使用方形点虚线和圆形点虚线
ComposePathEffect:可任意组合两种路径(PathEffect)的特性
我们通过一个实例来认识这些效果:
public class PathEffectView extends View {
private final Path mPath;
private final Paint mPaint;
private PathEffect[] mEffects;
public PathEffectView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mEffects = new PathEffect[6];
mPath = new Path();
mPath.moveTo(0, 0);
for (int i = 0; i <= 30; i++) {
mPath.lineTo(i * 35, (float) (Math.random() * 100));
}
mPaint = new Paint();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mEffects[0] = null;
mEffects[1] = new CornerPathEffect(30);
mEffects[2] = new DiscretePathEffect(3.0F, 5.0F);
mEffects[3] = new DashPathEffect(new float[]{20, 10, 5, 10}, 0);
Path path = new Path();
path.addRect(0, 0, 8, 8, Path.Direction.CCW);
mEffects[4] = new PathDashPathEffect(path, 12, 0, PathDashPathEffect.Style.ROTATE);
mEffects[5] = new ComposePathEffect(mEffects[3], mEffects[1]);
for (int i = 0; i < mEffects.length; i++) {
mPaint.setPathEffect(mEffects[i]);
canvas.drawPath(mPath, mPaint);
canvas.translate(0, 200);
}
}
}
八:View之孪生兄弟----SurfaceView
8.1SurfaceView与View的区别
Android系统提供了View进行绘图处理,View虽然可以满足大部分绘图需求,但是某些时候却也力不从心。View通过刷新来重绘视图,Android系统通过发出VSYNC信号来进行屏幕的重绘,刷新的时间间隔为16ms。如果在16ms内View完成了你所需要执行的所有操作,那么用户在视觉上,就不会产生卡顿的感觉;如果执行的逻辑太多,特别是需要频繁刷新的页面上,例如游戏页面,那么就会不断阻塞主线程,从而导致画面卡顿。
"Skipped 47 frames!The application may be doing too much work on its main thread"
这些警告的产生,很多情况下就是因为绘制过程处理太多逻辑造成的。
所以有了SurfaceView来解决这个问题。
SurfaceView可以说是View的孪生兄弟,但也有下面的几个不同点
- View主要适用于主动刷新的情况,而SurfaceView主要适用于被动更新,例如频繁地刷新。
View在主线程中对画面进行刷新,而SurfaceView通常会通过子线程来进行页面的刷新
View在绘图时没有使用双缓冲机制,而SurfaceView在底层实现机制中就已实现了双缓冲机制。总结:如果你的自定义View需要频繁地刷新,或者刷新时数据处理量比较大,就使用SurfaceView取代View。
8
8.2SurfaView的使用
SurfaceView使用比View复杂,但是在使用时有模板代码可以套
- 创建SurfaceView
创建自定义的SurfaceView继承自SurfaceView,并实现两个接口-----SurfaceHolder.Callback和Runnable,代码如下。
public class SurfaceViewTemplate extends SurfaceView implements SurfaceView.Callback,Runnable
通过实现这两个接口,就需要在自定义的SurfaView中实现接口的方法,对于SurfaceViewHolder.Callback方法,需要实现如下方法
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
Runnable接口实现run方法
@Override
public void run() {
}
- 初始化SurfaceView
在自定义SurfaceView的构造方法中,需要对SurfaceView进行初始化。在自定义的SurfaceView中通常需要定义下面的三个成员变量,代码如下
//SurfaceHolder
private SurfaceHolder mHolder;
//用于绘图Canvas
private Canvas mCanvas;
//子线程标志位
private boolean mIsDrawing;
初始化方法就是对SurfaceView进行初始化,通常使用以下代码来初始化一个SurfaceHolder对象,并注册SurfaceHolder的回调方法。
mHolder = getHolder();
mHolder.addCallback(this);
另外两个成员变量Canvas已经很熟悉了,而mIsDrawing就是来控制SurfaceView绘图的子线程
- 使用SurfaceView
SurfaceHolder对象的lockCanvas()方法可以获得当前的Canvas绘图对象。接下来具体的绘图逻辑和View中的canvas一样。
注意:获取到的Canvas对象还是继续上次的Canvas对象,而不是一个新的对象。因此之前的所有绘图操作都会被保留,如果要擦除,在绘图前调用drawColor()方法进行清屏操作。绘制的时候充分利用SurfaceView的三个回调方法,在surfaceCreated()方法中开启子线程进行绘制,而子线程使用一个while(mIsDrawing)的循环来不停地进行绘制,而在具体的逻辑中,通过lockCanvas方法获得的Canvas对象进行绘制,并通过unlockCanvasAndPost(mCanvas)方法对画布内容进行提交。
整个SurfaceView的模板代码如下
public class SurfaceViewTemplate extends SurfaceView implements SurfaceHolder.Callback, Runnable {
//SurfaceHolder
private SurfaceHolder mHolder;
//用于绘图Canvas
private Canvas mCanvas;
//子线程标志位
private boolean mIsDrawing;
public SurfaceViewTemplate(Context context) {
super(context);
initView();
}
private void initView() {
mHolder = getHolder();
mHolder.addCallback(this);
}
public SurfaceViewTemplate(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
mIsDrawing = true;
new Thread(this).start();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
mIsDrawing = false;
}
@Override
public void run() {
while (mIsDrawing) {
draw();
}
}
private void draw() {
try {
mCanvas = mHolder.lockCanvas();
//draw something
} catch (Exception e) {
} finally {
if (mCanvas != null) {
mHolder.unlockCanvasAndPost(mCanvas);
}
}
}
}
上面模板代码可以满足大部分SurfaceView的绘图需求,mHolder.unlockCanvasAndPost(mCanvas)方法放到finally代码快中保证每次都能将内容提交。
8.3SurfaceView实例
使用SurfaceView进行频繁刷新。
8.3.1正弦曲线
绘制思想:
要绘制一个正弦曲线,只要不断修改横纵坐标的值,并让它们满足正弦函数即可。因此使用一个path对象来保存正弦函数上的坐标点,在子线程的while循环中,不断改变横纵坐标值,代码如下
public class sinSurfaceView extends SurfaceView implements SurfaceHolder.Callback, Runnable {
//SurfaceHolder
private SurfaceHolder mHolder;
//用于绘图Canvas
private Canvas mCanvas;
//子线程标志位
private boolean mIsDrawing;
private Path mPath;
private Paint mPaint;
private int y;
private int x;
public sinSurfaceView(Context context) {
super(context);
initView();
}
private void initView() {
mHolder = getHolder();
mHolder.addCallback(this);
mPath = new Path();
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(5);
}
public sinSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
mIsDrawing = true;
new Thread(this).start();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
mIsDrawing = false;
}
@Override
public void run() {
while (mIsDrawing) {
draw();
x += 1;
y = (int) (100 * Math.sin(x * 2 * Math.PI / 180) + 400);
mPath.lineTo(x, y);
}
}
private void draw() {
try {
mCanvas = mHolder.lockCanvas();
// draw something
//SurfaceView背景
mCanvas.drawColor(Color.WHITE);
mCanvas.drawPath(mPath, mPaint);
} catch (Exception e) {
} finally {
if (mCanvas != null) {
mHolder.unlockCanvasAndPost(mCanvas);
}
}
}
}
8.3.2绘画板
核心思想:
使用SurfaceView来实现一个简单的绘画板,绘图的方法与在View中进行绘图使用的方法一样,也是通过Path对象来记录手指滑动的路径来进行绘图。在surfaceView的onTouchEvent()中来记录path路径,代码如下
代码
public class DrawingBoard extends SurfaceView implements SurfaceHolder.Callback, Runnable {
private Canvas mCanvas;
private SurfaceHolder mSurfaceHolder;
private boolean mIsDrawing;
private Path mPath;
private Paint mPaint;
public DrawingBoard(Context context) {
super(context);
init();
}
public DrawingBoard(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mSurfaceHolder = getHolder();
mSurfaceHolder.addCallback(this);
mPath = new Path();
mPaint = new Paint();
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
mIsDrawing = true;
new Thread(this).start();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
mIsDrawing = false;
}
@Override
public void run() {
long start = System.currentTimeMillis();
while (mIsDrawing) {
draw();
}
long end = System.currentTimeMillis();
//50 - 100
if (end - start < 100) {
try {
Thread.sleep(100 - (end - start));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case ACTION_DOWN:
mPath.moveTo(x, y);
break;
case ACTION_MOVE:
mPath.lineTo(x, y);
break;
case ACTION_UP:
break;
}
return true;
}
private void draw() {
try {
mCanvas = mSurfaceHolder.lockCanvas();
mCanvas.drawColor(Color.WHITE);
mCanvas.drawPath(mPath, mPaint);
} catch (Exception e) {
} finally {
if (mCanvas != null) {
mSurfaceHolder.unlockCanvasAndPost(mCanvas);
}
}
}
}