屏幕和绘图
屏幕
- 屏幕大小:指屏幕对角线长度,单位为寸
- 分辨率:指屏幕宽高的像素点个数,如720x1280
- PPI:每英寸像素(Pixels Per Inch),指对角线的像素点数除以屏幕大小,又称屏幕密度DPI(Dots Per Inch)
系统屏幕密度
不同手机的大小和像素密度都不同,为统一,定义了几个标准的DPI值
独立像素密度dp
相同的像素,在不同密度的屏幕上显示,长度会不同,因为高密度的屏幕包含更多像素点
- 规定密度为mdpi(即密度值为160)时1px=1dp
- 故上图各分辨率的换算比例为3:4:6:8:12
单位转换
如下工具类提供了px、dp、sp的互相转换
public class DisplayUtil {
// dp = 像素/密度
public static int px2dip(Context context, float pxValue) {
float scale = context.getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5f);
}
public static int dip2px(Context context, float dipValue) {
float scale = context.getResources().getDisplayMetrics().density;
return (int) (dipValue * scale + 0.5f);
}
// sp = 像素/缩放密度
public static int px2sp(Context context, float pxValue) {
float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (pxValue / fontScale + 0.5f);
}
public static int sp2px(Context context, float spValue) {
float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue * fontScale + 0.5f);
}
}
或可以使用TypedValue.applyDimension()方法
public int dp2px(int dp) {
return (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
dp,
getResources().getDisplayMetrics());
}
public int sp2px(int sp) {
return (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP,
sp,
getResources().getDisplayMetrics());
}
XML绘图(需放在Drawable)
bitmap
对应BitmapDrawable,如下可将图片并转为Bitmap
<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@mipmap/ic_launcher" />
- src:资源id
- antialias:抗锯齿,使图片变得平滑,但会降低清晰度(可忽略不计),应该开启
- dither:抖动,可让高质量图片在低质量屏幕保持较好的显示效果,应该开启
- filter:过滤,当图片尺寸被拉伸/压缩时,可保持较好的显示效果,应该开启
- gravity:当图片小于容器尺寸时,对图片定位
- minMap:纹理映射,不常用,默认false
- tileMode:平铺模式,默认disable关闭,开启后gravity失效,repeat重复平铺,mirror镜面投影平铺,clamp边缘像素扩展平铺
nine-patch
对应NinePatchDrawable,表示.9格式照片,可以自动根据所需宽高缩放且不失真,属性同上
shape
对应GradientDrawable,可实现不同类型的形状,如下实现渐变阴影
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="45"
android:endColor="#805FBBFF"
android:startColor="#FF5DA2FF" />
<padding
android:bottom="7dp"
android:left="7dp"
android:right="7dp"
android:top="7dp" />
<corners android:radius="8dp" />
</shape>
效果如图
- shape:rectangle(矩形)、oval(椭圆)、line(横线)、ring(圆环),后两者需要<stroke>指定线宽和颜色
对于ring,有下列特殊属性
<corner>:指定四个角的角度,只用于rectangle
<gradient>:渐变效果
- angle:渐变角度,值为45的倍数,默认0表示从左到右,90表示从上到下
- centerX/Y:渐变中心点坐标
- start/center/endColor:渐变起始/中间/结束颜色
- type:渐变类型,liner(默认)、radial、sweep
- gradientRadius:渐变半径,当type=radial时生效
- userLevel:默认false,作为StateListDrawable时为true
<solid>:纯色填充
<stoke>:描边
- width/color:宽度/颜色
- dashWidth/dashGap:虚线 线段宽度/线段间隔,任一为0则虚线无效
<size>:设置固定宽高,可通过getIntrinsicWidth/Height()获取,并非最终宽高,作为View背景时还会被拉伸
layer-list
对应LayerDrawable,可实现类似PS中图层的概念
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@mipmap/ic_launcher" />
<item
android:bottom="50dp"
android:drawable="@mipmap/ic_launcher"
android:left="50dp"
android:right="50dp"
android:top="50dp" />
</layer-list>
上面两个item分别为图片进行叠加,效果如图
selector
对应StateListDrawable,实现不同事件设置反馈(默认状态应放在最后),如下实现修改为圆角矩形,点击后切换颜色
<?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>
- constantSize:固定大小是否随状态改变,true表示不变且大小为内部所有Drawable的最大值,默认为false
- variablePadding:padding是否随状态改变,true表示改变,默认为false,不建议开启
level-list
对应LevelListDrawable,设置不同等级范围(默认0,最大10000)显示不同的Drawable
<?xml version="1.0" encoding="utf-8"?>
<level-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="@drawable/ic_launcher_background"
android:maxLevel="10"
android:minLevel="6" />
<item
android:drawable="@drawable/ic_launcher"
android:maxLevel="5"
android:minLevel="0" />
</level-list>
当其作为background时,可通过setLevel()修改等级,当其作为ImageView的src时,可通过setImageLevel()修改等级
transition
对应TransitionDrawable,实现淡入淡出
<?xml version="1.0" encoding="utf-8"?>
<transition xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_launcher_background" />
<item android:drawable="@drawable/ic_launcher" />
</transition>
如上设置为android:background
<?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">
<ImageView
android:id="@+id/iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/test" />
</LinearLayout>
通过startTransition()实现淡入淡出效果
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ImageView iv = findViewById(R.id.iv);
TransitionDrawable background = (TransitionDrawable) iv.getBackground();
background.startTransition(3000);
}
}
inset
对应InsetDrawable,将其他Drawable内嵌到自己当中(也可用layer替代),当希望背景比自己的实际区域小的时候就可以使用
<?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_launcher_background"
android:insetLeft="15dp"
android:insetTop="15dp"
android:insetRight="15dp"
android:insetBottom="15dp"/>
scale
对应ScaleDrawable,根据自身的level和缩放比例进行缩放
- sacleGravity:等同于android:gravity
- scaleWidth/Height:宽/高缩放比例
必须设置level,默认为0不可见,不会调用draw()
@Override
public void draw(Canvas canvas) {
final Drawable d = getDrawable();
if (d != null && d.getLevel() != 0) {
d.draw(canvas);
}
}
如下iw一般为0,w -= (int) (w * (MAX_LEVEL - level) * mState.mScaleWidth / MAX_LEVEL)可知
- level=10000时相当于未缩放
- level的值越大,Drawable越大
- scaleWidth/Height越大,Drawable越小
@Override
protected void onBoundsChange(Rect bounds) {
final Drawable d = getDrawable();
final Rect r = mTmpRect;
final boolean min = mState.mUseIntrinsicSizeAsMin;
final int level = getLevel();
int w = bounds.width();
if (mState.mScaleWidth > 0) {
final int iw = min ? d.getIntrinsicWidth() : 0;
w -= (int) ((w - iw) * (MAX_LEVEL - level) * mState.mScaleWidth / MAX_LEVEL);
}
int h = bounds.height();
if (mState.mScaleHeight > 0) {
final int ih = min ? d.getIntrinsicHeight() : 0;
h -= (int) ((h - ih) * (MAX_LEVEL - level) * mState.mScaleHeight / MAX_LEVEL);
}
final int layoutDirection = getLayoutDirection();
Gravity.apply(mState.mGravity, w, h, bounds, r, layoutDirection);
if (w > 0 && h > 0) {
d.setBounds(r.left, r.top, r.right, r.bottom);
}
}
如下缩小图片为原来的30%
<?xml version="1.0" encoding="utf-8"?>
<scale xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_launcher"
android:scaleWidth="70%"
android:scaleHeight="70%"
android:scaleGravity="center" />
<?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">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/ic_launcher" />
<ImageView
android:id="@+id/iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/test" />
</LinearLayout>
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ImageView iv = findViewById(R.id.iv);
ScaleDrawable background = (ScaleDrawable) iv.getBackground();
background.setLevel(1);
}
}
clip
对应ClipDrawable,根据自身level裁剪一个Drawable,0表示完全裁剪,10000则完全不裁剪
- clipOrientation:裁剪方向,水平和竖直
- gravity有如下选项
如下设置从上往下裁剪50%
<?xml version="1.0" encoding="utf-8"?>
<clip xmlns:android="http://schemas.android.com/apk/res/android"
android:clipOrientation="vertical"
android:drawable="@drawable/ic_launcher"
android:gravity="bottom" />
<?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">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/ic_launcher" />
<ImageView
android:id="@+id/iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/test" />
</LinearLayout>
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ImageView iv = findViewById(R.id.iv);
ClipDrawable background = (ClipDrawable) iv.getBackground();
background.setLevel(5000);
}
}
自定义Drawable
Drawable一般作为ImageView中的图像显示,或者作为View的背景,但自定义Drawable无法在xml中使用,如下实现一个圆形Drawable,随着宽高变化而变化
public class CustomDrawable extends Drawable {
private Paint mPaint;
public CustomDrawable(int color) {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(color);
}
@Override
public void draw(@NonNull Canvas canvas) {
Rect bounds = getBounds();
float exactCenterX = bounds.exactCenterX();
float exactCenterY = bounds.exactCenterY();
canvas.drawCircle(exactCenterX, exactCenterY, Math.min(exactCenterX, exactCenterY), mPaint);
}
@Override
public void setAlpha(int alpha) {
mPaint.setAlpha(alpha);
invalidateSelf();
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {
mPaint.setColorFilter(colorFilter);
invalidateSelf();
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
}
<?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">
<View
android:id="@+id/iv"
android:layout_width="100dp"
android:layout_height="100dp" />
</LinearLayout>
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
View iv = findViewById(R.id.iv);
iv.setBackground(new CustomDrawable(Color.RED));
}
}
绘图技巧
Canvas
下面为常用的方法
- save():将已绘制图像保存,在此之后的操作绘制在新图层
- restore():合并图层,将save()之后绘制的图像和save()之前的合并
- translate():坐标系平移
- ratate():坐标系旋转
如下绘制一个时钟,通过平移、旋转坐标系简化实现
public class Clock extends View {
private int mWidth;
private int mHeight;
public Clock(Context context) {
super(context);
}
public Clock(Context context, AttributeSet attrs) {
super(context, attrs);
}
public Clock(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mWidth = getMeasuredWidth();
mHeight = getMeasuredHeight();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 绘制外圆,以(mWidth / 2, mHeight / 2)为圆形,以mWidth / 2为半径
Paint paintCircle = new Paint();
paintCircle.setStyle(Paint.Style.STROKE);
paintCircle.setAntiAlias(true);
paintCircle.setStrokeWidth(5);
canvas.drawCircle(mWidth / 2, mHeight / 2, mWidth / 2, paintCircle);
// 绘制刻度
Paint paintDegree = new Paint();
for (int i = 0; i < 24; i++) {
if (i == 0 || i == 6 || i == 12 || i == 18) {
paintDegree.setStrokeWidth(5);
paintDegree.setTextSize(30);
canvas.drawLine(mWidth / 2, mHeight / 2 - mWidth / 2,
mWidth / 2, mHeight / 2 - mWidth / 2 + 60,
paintDegree);
String degree = String.valueOf(i);
canvas.drawText(degree,
mWidth / 2 - paintDegree.measureText(degree) / 2,
mHeight / 2 - mWidth / 2 + 90,
paintDegree);
} else {
paintDegree.setStrokeWidth(3);
paintDegree.setTextSize(15);
canvas.drawLine(mWidth / 2, mHeight / 2 - mWidth / 2,
mWidth / 2, mHeight / 2 - mWidth / 2 + 30,
paintDegree);
String degree = String.valueOf(i);
canvas.drawText(degree,
mWidth / 2 - paintDegree.measureText(degree) / 2,
mHeight / 2 - mWidth / 2 + 60,
paintDegree);
}
// 将画布以圆心旋转15°,简化坐标运算
canvas.rotate(15, mWidth / 2, mHeight / 2);
}
// 绘制指针
Paint paintHour = new Paint();
paintHour.setStrokeWidth(20);
Paint paintMinute = new Paint();
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();
}
}
Layer
图层基于栈结构,入栈后操作都发生在该图层上,出栈后则把图像绘制到上层Canvas
- 入栈:saveLayer()、saveLayerAlpha()
- 出栈:restore()、restoreToCount()
public class MyView extends View {
private static final String TAG = MyView.class.getSimpleName();
private Paint mPaint;
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mPaint = new Paint();
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(Color.WHITE);
mPaint.setColor(Color.BLUE);
canvas.drawCircle(150, 150, 100, mPaint);
canvas.saveLayerAlpha(0, 0, 400, 400, 127);
mPaint.setColor(Color.RED);
canvas.drawCircle(200, 200, 100, mPaint);
canvas.restore();
}
}
如下图,透明度为127的红色圆叠加在蓝色圆上方
PorterDuffXfermode
PorterDuffXfermod设置的是两个图层交集区域的显示方式,dst是先画的图形,而src是后画的图形,共有16种模式,如图
如下实现圆角图形,mOut为先画的图像,mBitmap为后画的图像,用SrcIn取交集
public class MyView extends View {
private static final String TAG = MyView.class.getSimpleName();
private Bitmap mBitmap;
private Bitmap mOut;
private Paint mPaint;
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
mOut = Bitmap.createBitmap(mBitmap.getWidth(), mBitmap.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(mOut);
mPaint = new Paint();
mPaint.setAntiAlias(true);
canvas.drawRoundRect(0, 0, mBitmap.getWidth(), mBitmap.getHeight(), 80, 80, mPaint);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(mBitmap, 0, 0, mPaint);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(mOut, 0, 0, null);
}
}
效果如图
如下代码设置透明的画笔,实时获取坐标调用drawPath()涂抹上面的图层,实现刮刮乐
public class XfermodeView extends View {
private Bitmap mBgBitmap, mFgBitmap;
private Paint mPaint;
private Canvas mCanvas;
private Path mPath;
public XfermodeView(Context context) {
this(context, null);
}
public XfermodeView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public XfermodeView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaint = new Paint();
mPaint.setAlpha(0);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeJoin(Paint.Join.ROUND);
mPaint.setStrokeWidth(50);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPath = new Path();
mBgBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.b);
mFgBitmap = Bitmap.createBitmap(mBgBitmap.getWidth(), mBgBitmap.getHeight(), Bitmap.Config.ARGB_8888);
mCanvas = new Canvas(mFgBitmap);
mCanvas.drawColor(Color.GRAY);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mPath.reset();
mPath.moveTo(event.getX(), event.getY());
break;
case MotionEvent.ACTION_MOVE:
mPath.lineTo(event.getX(), event.getY());
break;
}
mCanvas.drawPath(mPath, mPaint);
invalidate();
return true;
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(mBgBitmap, 0, 0, null);
canvas.drawBitmap(mFgBitmap, 0, 0, null);
}
}
Shader
用于实现渐变、渲染效果,包括
- BitmapShader——位图Shader
- LinnerGradient——线性Shader
- RadialGradient——光束Shader
- SweepGradient——梯度Shader
- ComposeShader——混合Shader
上述渐变都有三种模式选择:
- CLAMP:拉伸,拉伸的是图片最后一个像素,不断重复
- REPEAT:重复,横向、纵向不断重复
- MIRROR:镜像,横向、纵向不断翻转重复
如下,将图片填充到BitmapShader,创建一支具有图像填充功能的Paint
public class MyView extends View {
private static final String TAG = MyView.class.getSimpleName();
private Bitmap mBitmap;
private Paint mPaint;
private BitmapShader mBitmapShader;
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
mPaint = new Paint();
mPaint.setShader(mBitmapShader);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(mBitmap.getWidth() / 2.0f, mBitmap.getHeight() / 2.0f, 50, mPaint);
}
}
利用Paint在图片中心绘制圆形,效果如下
如下改为REPEAT,并扩大半径,可看到图片不断重复绘制
public class MyView extends View {
private static final String TAG = MyView.class.getSimpleName();
private Bitmap mBitmap;
private Paint mPaint;
private BitmapShader mBitmapShader;
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
mPaint = new Paint();
mPaint.setShader(mBitmapShader);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(mBitmap.getWidth() / 2.0f, mBitmap.getHeight() / 2.0f, 500, mPaint);
}
}
如下利用PorterDuffXfermode和LinnerGradient实现倒影效果的图片
public class ReflectView extends View {
private Bitmap mSrcBitmap, mRefBitmap;
private Paint mPaint;
private PorterDuffXfermode mXfermode;
public ReflectView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mSrcBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.b);
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, mSrcBitmap.getHeight() * 2,
0xDD000000, 0x10000000, Shader.TileMode.REPEAT));
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, mRefBitmap.getHeight(), mRefBitmap.getWidth(), mSrcBitmap.getHeight() * 2, mPaint);
mPaint.setXfermode(null);
}
}
先将图片翻转,再添加黑色透明度递减的渐变,DST_IN是为了避免上面的图像被渐变效果遮挡
PathEffect
PathEffect指用各种笔触效果来绘制一个路径
如上,分别为
- 无效果
- CornerPathEffect:线段拐角圆滑
- DiscretePathEffect:线段出现杂点
- DashPathEffect:绘制虚线,用一个数组来设置各个点之间的间隔,还可设置偏移量实现动态路径效果
- PathPathEffect:类似DashPathEffect,但还可设置点的图形,如上面的圆点虚线
- ConposePathEffect:组合效果
public class MyView extends View {
private static final String TAG = MyView.class.getSimpleName();
private Path mPath;
private PathEffect[] mPathEffect;
private Paint mPaint;
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mPath = new Path();
mPath.moveTo(0, 0);
for (int i = 0; i <= 30; i++) {
mPath.lineTo(i * 35, (float) (Math.random() * 100));
}
mPathEffect = new PathEffect[6];
mPathEffect[0] = null;
mPathEffect[1] = new CornerPathEffect(30);
mPathEffect[2] = new DiscretePathEffect(3.0F, 5.0F);
mPathEffect[3] = new DashPathEffect(new float[]{20, 10, 5, 10}, 0);
Path path = new Path();
path.addRect(0, 0, 8, 8, Path.Direction.CCW);
mPathEffect[4] = new PathDashPathEffect(path, 12, 0, PathDashPathEffect.Style.ROTATE);
mPathEffect[5] = new ComposePathEffect(mPathEffect[3], mPathEffect[1]);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (PathEffect pathEffect : mPathEffect) {
mPaint.setPathEffect(pathEffect);
canvas.drawPath(mPath, mPaint);
canvas.translate(0, 200);
}
}
}
SurfaceView
Android通过VSYNC信号刷新View进行屏幕的重绘,时间间隔为16ms,若在需要频繁刷新的界面上执行太多的操作逻辑,则容易阻塞主线程,造成画面卡顿
当需要频繁刷新或刷新时数据处理量比较大时,可使用SurfaceView
- View用于主动刷新,SurfaceView用于被动刷新
- View在主线程刷新,SurfaceView在子线程刷新
- View绘图未使用双缓冲机制,SurfaceView则使用双缓冲机制
SurfaceView的模板代码如下,主要原理是初始化时自行创建子线程并调用draw()方法绘制
public class SurfaceViewTemplate extends SurfaceView implements SurfaceHolder.Callback, Runnable {
//内部Holder,用于获取Canvas和提交绘制
private SurfaceHolder mHolder;
//用于控制线程的标志位
private boolean mIsDrawing;
//用于绘图的Canvas
private Canvas mCanvas;
public SurfaceViewTemplate(Context context) {
this(context, null);
}
public SurfaceViewTemplate(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SurfaceViewTemplate(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mHolder = getHolder();
mHolder.addCallback(this); //将自身作为回调
setFocusableInTouchMode(true);
setFocusable(true);
this.setKeepScreenOn(true);
}
//创建时开启线程,调用draw()
@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();
//获取Canvas开始绘制,获取的并非新的Canvas,而是上次的
} catch (Exception e) {
e.printStackTrace();
} finally {
if (mHolder != null) {
//绘制完后需提交
mHolder.unlockCanvasAndPost(mCanvas);
}
}
}
}
如下代码利用SurfaceView实现正弦函数的动态绘制
public class SurfaceViewTemplate extends SurfaceView implements SurfaceHolder.Callback, Runnable {
private SurfaceHolder mHolder;
private boolean mIsDrawing;
private Canvas mCanvas;
private int x;
private int y;
private Path mPath;
private Paint mPaint;
public SurfaceViewTemplate(Context context) {
this(context, null);
}
public SurfaceViewTemplate(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SurfaceViewTemplate(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPath = new Path();
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mHolder = getHolder();
mHolder.addCallback(this);
setFocusableInTouchMode(true);
setFocusable(true);
this.setKeepScreenOn(true);
}
@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();
mCanvas.drawColor(Color.WHITE); //设置背景
mCanvas.drawPath(mPath, mPaint);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (mHolder != null) {
mHolder.unlockCanvasAndPost(mCanvas);
}
}
}
}
效果如图
如下代码利用SurfaceView实现绘图板,根据手指移动绘制路径,在draw()中调用sleep()避免过度频繁绘制
public class SurfaceViewTemplate extends SurfaceView implements SurfaceHolder.Callback, Runnable {
private SurfaceHolder mHolder;
private boolean mIsDrawing;
private Canvas mCanvas;
private Path mPath;
private Paint mPaint;
public SurfaceViewTemplate(Context context) {
this(context, null);
}
public SurfaceViewTemplate(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SurfaceViewTemplate(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPath = new Path();
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mHolder = getHolder();
mHolder.addCallback(this);
setFocusableInTouchMode(true);
setFocusable(true);
this.setKeepScreenOn(true);
}
@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 boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mPath.moveTo(x, y);
break;
case MotionEvent.ACTION_MOVE:
mPath.lineTo(x, y);
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
@Override
public void run() {
long start = System.currentTimeMillis();
while (mIsDrawing) {
draw();
}
long end = System.currentTimeMillis();
if (end - start < 100) {
SystemClock.sleep(100 - (end - start));
}
}
private void draw() {
try {
mCanvas = mHolder.lockCanvas();
mCanvas.drawColor(Color.WHITE);
mCanvas.drawPath(mPath, mPaint);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (mHolder != null) {
mHolder.unlockCanvasAndPost(mCanvas);
}
}
}
}
效果如图