Canvas可以用来绘制直线、点、几何图形、曲线、Bitmap、圆弧等等,做出很多很棒的效果,例如QQ的消息气泡就是使用Canvas画的
Canvas中常用的方法
- 初始化参数
Paint paint = new Paint();paint.setColor(Color.RED);paint.setStyle(Paint.Style.FILL);paint.setStrokeWidth(8);
- 绘制直线
canvas.drawLine(0, 0, 100, 100, paint);
- 绘制一组直线
float[] lines = { 0, 0, 100, 100, 200, 200, 300, 300};canvas.drawLines(lines,paint);
- 绘制点
canvas.drawPoint(100, 100, paint);
- 绘制矩形
Rect rect = new Rect(0, 0, 200, 100);canvas.drawRect(rect, paint);//canvas.drawRect(0, 0, 200, 100, paint);
- 绘制圆角矩形
RectF rectF = new RectF(100, 100, 300, 200);canvas.drawRoundRect(rectF, 20, 20, paint);
- 绘制圆形
canvas.drawCircle(300, 300, 200, paint);canvas.drawOval(100, 100, 300, 200, paint);
- 绘制弧度
RectF rectF = new RectF(100, 100, 300, 200);canvas.drawArc(rectF, 0, 90, true, paint);
使用Path参与绘制
- 绘制直线
//使用PathPath path = new Path();//落笔位置path.moveTo(100, 100);//移动path.lineTo(200, 100);path.lineTo(200, 200);//闭合线path.close();//按路径绘制canvas.drawPath(path, paint);
- 绘制其他线条,使用
path.addXxx()
float[] radii = { 10, 10, 20, 30, 40, 40, 60, 50};RectF rectF = new RectF(100, 100, 600, 500);path.addRoundRect(rectF, radii, Path.Direction.CCW);canvas.drawPath(path, paint);
使用Region区域绘制
//创建一块矩形区域Region region = new Region(100, 100, 500, 400);RegionIterator iterator = new RegionIterator(region);Rect rect = new Rect();while (iterator.next(rect)) { canvas.drawRect(rect, paint);}
以上只是画出一个矩形,另外并没有什么现象,这是因为只有一个Region
两个Region实现取交集,使用并且不断分割交集部分Path path = new Path();RectF rectF = new RectF(100, 100, 600, 800);path.addOval(rectF, Path.Direction.CCW);Region region1 = new Region();Region region2 = new Region(100, 100, 500, 400);region1.setPath(path, region2);RegionIterator iterator = new RegionIterator(region1);Rect rect = new Rect();while (iterator.next(rect)) { canvas.drawRect(rect, paint);}
以上执行结果如下
Region的其他操作 并集:region.union(r);
交集:region.op(r, Op.INTERSECT);
Canvas的细节问题
当canvas执行drawXXX的时候就会新建一个新的画布图层
虽然新建一个画布图层,但是还是会沿用之前设置的平移变换,不可逆的(save和restore来解决) 之所以这样设计,是考虑到了绘制复杂图形的时候,可能会变换画布位置,那么就会造成之前绘制的图像发生错位,导致前功尽弃Canvas变换
平移(Translate)
Rect rect = new Rect(100, 100, 800, 1000);paint.setColor(Color.RED);canvas.drawRect(rect, paint);canvas.translate(100, 100);paint.setColor(Color.GREEN);canvas.drawRect(rect, paint);
缩放(Scale)
Rect rect = new Rect(100, 100, 600, 800);paint.setColor(Color.RED);canvas.drawRect(rect, paint);canvas.scale(1.2F, 1.5F);paint.setColor(Color.GREEN);canvas.drawRect(rect, paint);
旋转(Rotate)
Rect rect = new Rect(400, 100, 700, 800);paint.setColor(Color.RED);canvas.drawRect(rect, paint);canvas.rotate(25);//默认围绕原点//canvas.rotate(25, 400, 100);//指定旋转点paint.setColor(Color.GREEN);canvas.drawRect(rect, paint);
斜拉画布(Skew)
Rect rect = new Rect(100, 100, 400, 800);paint.setColor(Color.RED);canvas.drawRect(rect, paint);canvas.skew(0.5F, 0);paint.setColor(Color.GREEN);canvas.drawRect(rect, paint);
裁剪画布(clip)
RectF rectF = new RectF(100, 100, 500, 800);paint.setColor(Color.RED);canvas.drawRect(rectF, paint);paint.setColor(Color.GREEN);canvas.clipRect(new Rect(200, 200, 400, 400));canvas.drawColor(Color.BLUE);
变换操作的影响
当对画板进行操作以后,会对后续的画布操作造成影响,那么要实现不对后续操作就需要在操作前保存画布,需要时候恢复画布
canvas.save(); //保存当前画布···//画布操作canvas.restore();//恢复保存的画布
canvas实际上是保存到画布栈里面去了,每一次保存,就使得一个当前画布入栈,每一次恢复,就有一个canvas出栈
因此,一般来说保存和恢复是成对出现的自定义Drawable动画
Drawable就是一个可画的对象,其可能是一张位图(BitmapDrawable),也可能是一个图形(ShapeDrawable),还有可能是一个图层(LayerDrawable),我们根据画图的需求,创建相应的可画对象,就可以将这个可画对象当作一块“画布(Canvas)”,在其上面操作可画对象,并最终将这种可画对象显示在画布上,有点类似于"内存画布"
自定义Drawablepublic class RevealDrawableView extends Drawable { private int orientation; private Drawable selected; private Drawable unselected; private int widthLeft; private int heightLeft; private int widthRight; private int heightRight; public static final int HORIZONTAL = 1; public static final int VERTICAL = 2; public RevealDrawableView(Drawable unselected, Drawable selected, int orientation) { this.unselected = unselected; this.selected = selected; this.orientation = orientation; } @Override public void draw(@NonNull Canvas canvas) { //得到当前level,转化成百分比 int level = getLevel(); if (level == 10000 || level == 0) { //滑出画入状态 unselected.draw(canvas); } else if (level == 5000) { //在正中间状态 selected.draw(canvas); } else { Rect bounds = getBounds();//得到当前Drawable自身的矩形区域 float ratio = (level / 5000F) - 1F;//得到比例-1~+1 int width = bounds.width(); int height = bounds.height(); widthLeft = widthRight = width; heightLeft = heightRight = height; //得到左右宽高 if (orientation == HORIZONTAL) { widthLeft = (int) (width * Math.abs(ratio)); widthRight = width - widthLeft; } if (orientation == VERTICAL) { heightLeft = (int) (height * Math.abs(ratio)); heightRight = height - heightLeft; } int gravity = ratio < 0 ? Gravity.LEFT : Gravity.RIGHT; //得到当前左边区域 Rect rectLeft = new Rect(); //抠图位置,宽度,高度,目标,输出位置 Gravity.apply(gravity, widthLeft, heightLeft, bounds, rectLeft); canvas.save();//保存画布 canvas.clipRect(rectLeft);//剪切画布 unselected.draw(canvas);//画未选中图片 canvas.restore();//恢复画布 //得到右边矩形区域 gravity = ratio < 0 ? Gravity.RIGHT : Gravity.LEFT; Rect rectRight = new Rect(); //抠图位置,宽度,高度,目标,输出位置 Gravity.apply(gravity, widthRight, heightRight, bounds, rectRight); canvas.save(); canvas.clipRect(rectRight); selected.draw(canvas);//画选中图片 canvas.restore(); } } @Override protected void onBoundsChange(Rect bounds) { //设置矩形 unselected.setBounds(bounds); selected.setBounds(bounds); } @Override public int getIntrinsicWidth() { //得到Drawable的实际宽度 return Math.max(unselected.getIntrinsicWidth(), selected.getIntrinsicWidth()); } @Override public int getIntrinsicHeight() { //得到Drawable的实际高度 return Math.max(unselected.getIntrinsicHeight(), selected.getIntrinsicHeight()); } @Override protected boolean onLevelChange(int level) { //每次level改变时刷新 invalidateSelf(); return true; } @Override public void setAlpha(int alpha) { } @Override public void setColorFilter(@Nullable ColorFilter colorFilter) { } @Override public int getOpacity() { return PixelFormat.UNKNOWN; }}
自定义控件
public class GallaryHScrollView extends HorizontalScrollView implements OnTouchListener { private LinearLayout container; private int centerX; private int width; public GallaryHScrollView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public GallaryHScrollView(Context context) { super(context); init(); } private void init() { //ScrollView水平滑动,存在大量ImageView LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); container = new LinearLayout(getContext()); container.setLayoutParams(params); setOnTouchListener(this); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); //得到某一张图片的宽度 View view = container.getChildAt(0); width = view.getWidth(); //得到中间x坐标 centerX = getWidth() / 2; //中心坐标改为中心图片的左边界 centerX = centerX - width / 2; //给LinearLayout和hzv之间设置边框距离 container.setPadding(centerX, 0, centerX, 0); } @Override public boolean onTouch(View v, MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_MOVE) { //渐变图片 reveal(); } return false; } private void reveal() { // 渐变效果 //得到滑出去的距离 int scrollX = getScrollX(); //找到两张渐变的图片的下标--左,右 int index_left = scrollX / width; int index_right = index_left + 1; //设置图片的level for (int i = 0; i < container.getChildCount(); i++) { if (i == index_left || i == index_right) { //变化 float ratio = 5000f / width;//比例 ImageView iv_left = (ImageView) container.getChildAt(index_left); //scrollX%icon_width:代表滑出去的距离 iv_left.setImageLevel((int) (5000 - scrollX % width * ratio)); //右边 if (index_right < container.getChildCount()) { ImageView iv_right = (ImageView) container.getChildAt(index_right); //scrollX%icon_width:代表滑出去的距离 //滑出去了icon_width/2 icon_width/2%icon_width iv_right.setImageLevel((int) (10000 - scrollX % width * ratio)); } } else { //灰色 ImageView iv = (ImageView) container.getChildAt(i); iv.setImageLevel(0); } } } //添加图片的方法 public void addImageViews(Drawable[] revealDrawables) { for (int i = 0; i < revealDrawables.length; i++) { ImageView img = new ImageView(getContext()); img.setImageDrawable(revealDrawables[i]); container.addView(img); if (i == 0) { img.setImageLevel(5000); } } addView(container); }}
测试工具类
public class SourceUtils { private static int[] mImgIds = new int[]{ R.drawable.avft, R.drawable.box_stack, R.drawable.bubble_frame, R.drawable.bubbles, R.drawable.bullseye, R.drawable.circle_filled, R.drawable.circle_outline, R.drawable.avft, R.drawable.box_stack, R.drawable.bubble_frame, R.drawable.bubbles, R.drawable.bullseye, R.drawable.circle_filled, R.drawable.circle_outline }; private static int[] mImgIds_active = new int[]{ R.drawable.avft_active, R.drawable.box_stack_active, R.drawable.bubble_frame_active, R.drawable.bubbles_active, R.drawable.bullseye_active, R.drawable.circle_filled_active, R.drawable.circle_outline_active, R.drawable.avft_active, R.drawable.box_stack_active, R.drawable.bubble_frame_active, R.drawable.bubbles_active, R.drawable.bullseye_active, R.drawable.circle_filled_active, R.drawable.circle_outline_active }; public static int[] getImgIds() { return mImgIds; } public static int[] getImgIdsActive() { return mImgIds_active; }}
布局
测试
public class RevealDrawableActivity extends AppCompatActivity { private Drawable[] revealDrawables; private GallaryHScrollView gallaryHScrollView; protected int level = 10000; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_reveal_drawable); revealDrawables = new Drawable[SourceUtils.getImgIds().length]; for (int i = 0; i < SourceUtils.getImgIds().length; i++) { RevealDrawableView rd = new RevealDrawableView( getResources().getDrawable(SourceUtils.getImgIds()[i]), getResources().getDrawable(SourceUtils.getImgIdsActive()[i]), RevealDrawableView.HORIZONTAL); revealDrawables[i] = rd; } gallaryHScrollView = (GallaryHScrollView) findViewById(R.id.ghs); gallaryHScrollView.addImageViews(revealDrawables); }}
效果
自定义控件Search动画
自定义View控件
public class SearchView extends View { private Paint paint; private BaseController controller; public SearchView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } private void init() { paint = new Paint(); paint.setStrokeWidth(8); } public void setController(BaseController controller) { this.controller = controller; controller.setSearchView(this); invalidate(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); controller.draw(canvas, paint); } public void startAnimation() { if (controller != null) { controller.startAnim(); } } public void resetAnimation() { if (controller != null) { controller.resetAnim(); } }}
通过控制器来控制绘画
public abstract class BaseController { public static final int STATE_ANIM_RESET = 0; public static final int STATE_ANIM_START = 1; public int state = STATE_ANIM_RESET; public float progress = -1; private SearchView searchView; public abstract void draw(Canvas canvas, Paint paint); public void startAnim() { } public void resetAnim() { } public int getWidth() { return searchView.getWidth(); } public int getHeight() { return searchView.getHeight(); } public ValueAnimator startValueAnimation() { final ValueAnimator animator = ValueAnimator.ofFloat(0, 1); animator.setDuration(5000); animator.setInterpolator(new AnticipateInterpolator()); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { progress = (float) animator.getAnimatedValue(); searchView.invalidate(); } }); animator.start(); progress = 0; return animator; } public void setSearchView(SearchView searchView) { this.searchView = searchView; }}
实现控制器方法
public class MyController extends BaseController { private int color = Color.GREEN; private int cx, cy, cr; private RectF rectF; private int j = 15; public MyController() { rectF = new RectF(); } @Override public void draw(Canvas canvas, Paint paint) { canvas.drawColor(color); switch (state) { case STATE_ANIM_START: drawStartAnimView(canvas, paint); break; case STATE_ANIM_RESET: drawResetAnimView(canvas, paint); break; } } private void drawStartAnimView(Canvas canvas, Paint paint) { canvas.save(); if (progress <= 0.5f) { //绘制圆和把手 canvas.drawArc(rectF, 45, 360 * (progress * 2 - 1), false, paint); canvas.drawLine(rectF.right - j, rectF.bottom - j, rectF.right + cr - j, rectF.bottom + cr - j, paint); } else { //绘制把手 canvas.drawLine( rectF.right - j + cr * (progress * 2 - 1), rectF.bottom - j + cr * (progress * 2 - 1), rectF.right - j + cr, rectF.bottom + cr - j, paint); } //绘制下面的横线 canvas.drawLine( (rectF.right - j + cr) * (1 - progress * 0.8f), rectF.bottom + cr - j, rectF.right - j + cr, rectF.bottom + cr - j, paint); canvas.restore(); rectF.left = cx - cr + progress * 250; rectF.right = cx + cr + progress * 250; rectF.top = cy - cr; rectF.bottom = cy + cr; } private void drawResetAnimView(Canvas canvas, Paint paint) { cr = getWidth() / 20; cx = getWidth() / 2; cy = getHeight() / 2; rectF.left = cx - cr; rectF.right = cx + cr; rectF.top = cy - cr; rectF.bottom = cy + cr; canvas.save(); paint.reset(); paint.setAntiAlias(true); paint.setColor(Color.WHITE); paint.setStrokeWidth(5); paint.setStyle(Paint.Style.STROKE); canvas.rotate(45, cx, cy); canvas.drawLine(cx + cr, cy, cx + cr * 2, cy, paint); canvas.drawArc(rectF, 0, 360, false, paint); canvas.restore(); } @Override public void startAnim() { super.startAnim(); state = STATE_ANIM_START; startValueAnimation(); } @Override public void resetAnim() { super.resetAnim(); state = STATE_ANIM_RESET; startValueAnimation(); }}
布局
测试
public class SearchActivity extends AppCompatActivity { private SearchView serachView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_search); serachView = findViewById(R.id.search_view); serachView.setController(new MyController()); } public void start(View view) { serachView.startAnimation(); } public void reset(View view) { serachView.resetAnimation(); }}
测试结果