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参与绘制

  • 绘制直线
//使用Path
Path 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)”,在其上面操作可画对象,并最终将这种可画对象显示在画布上,有点类似于"内存画布"

自定义Drawable

public 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;
}
}

布局

<?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"> <com.cj5785.testcanvas.GallaryHScrollView
android:id="@+id/ghs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:background="@android:color/darker_gray"
android:scrollbars="none" /> </LinearLayout>

测试

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();
}
}

布局

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"> <com.cj5785.testcanvas.search.SearchView
android:id="@+id/search_view"
android:layout_width="match_parent"
android:layout_height="match_parent" /> <Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:text="start"
android:onClick="start"/> <Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:text="reset"
android:onClick="reset"/> </RelativeLayout>

测试

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();
}
}

测试结果

高级UI-画板Canvas的更多相关文章

  1. Android 高级UI设计笔记07:RecyclerView 的详解

    1. 使用RecyclerView       在 Android 应用程序中列表是一个非常重要的控件,适用场合非常多,如新闻列表.应用列表.消息列表等等,但是从Android 一出生到现在并没有非常 ...

  2. firefox 扩展开发笔记(三):高级ui交互编程

    firefox 扩展开发笔记(三):高级ui交互编程 前言 前两篇链接 1:firefox 扩展开发笔记(一):jpm 使用实践以及调试 2:firefox 扩展开发笔记(二):进阶开发之移动设备模拟 ...

  3. 高级UI晋升之View渲染机制(二)

    更多Android高级架构进阶视频学习请点击:https://space.bilibili.com/474380680 优化性能一般从渲染,运算与内存,电量三个方面进行,今天开始说聊一聊Android ...

  4. iOS开发——高级UI&带你玩转UITableView

    带你玩装UITableView 在实际iOS开发中UITableView是使用最多,也是最重要的一个控件,如果你不会用它,那别说什么大神了,菜鸟都不如. 其实关于UItableView事非常简单的,实 ...

  5. 创新高性能移动 UI 框架-Canvas UI 框架

    WebView 里无法获得的能力虽然是「体验增强」与「端基本能力」,但现都基本上有成熟解决方法.但后期的 UI 和 Layout 的性能反而是目前 Web 技术欠缺的.所以,无论是 Titanium ...

  6. Unity5.3——UI之Canvas

    原文:http://docs.unity3d.com/Manual/UISystem.html Canvas 所有的UI都应该放在Canvas里面(子层).Canvas是一个带有Canvas组件的Ga ...

  7. canvas画画板,canvas画五角星,canvas制作钟表、Konva写钟表

    制作一个画画板,有清屏有橡皮擦有画笔可以换颜色 style样式 <head> <meta charset="UTF-8"> <title>画画板 ...

  8. 高级UI晋升之自定义View实战(九)

    更多Android高级架构进阶视频学习请点击:https://space.bilibili.com/474380680 1.前言: 本文采用自定义view的方法来实现一键清除的动画这个功能. 2.效果 ...

  9. 高级UI晋升之自定义View实战(六)

    更多Android高级架构进阶视频学习请点击:https://space.bilibili.com/474380680本篇文章将从Android 自定义属性动画&Camera动画来介绍自定义V ...

随机推荐

  1. Docker 基本操作(附 redis、nginx部署)

    下载安装 Docker 也有一个月了.中间看过几次也没有深入的了解研究.就只是拉取了两个镜像简单的看了看. 昨天因一个项目中需要用到 Redis ,因为是 Windows 系统,看了下安装包比较老了有 ...

  2. Linux配置静态IP以及解决配置静态IP后无法上网的问题

    式一.图形界面配置

  3. Haskell语言学习笔记(94)Enum Bounded

    Enum class Enum a where succ, pred :: a -> a toEnum :: Int -> a fromEnum :: a -> Int enumFr ...

  4. tornado处理跨域问题

    报错信息一: Access to XMLHttpRequest at 'http://localhost:4445/api/v/getmsg' from origin 'http://localhos ...

  5. JavaScript基础02-条件语句及介绍函数

    条件语句 if  和  if - else  语句 if(你还没睡着么?){ 是 那我们去撸串--- } 当条件判断为真true时,执行花括号内的语句,如果条件为假false,跳过花括号内的语句 if ...

  6. JAVA常用处理数据

    price(350)*(10/100) price.multiply(maxPayIntegrateRate.divide(new BigDecimal("100.0")) max ...

  7. php解决大文件断点续传

    核心原理: 该项目核心就是文件分块上传.前后端要高度配合,需要双方约定好一些数据,才能完成大文件分块,我们在项目中要重点解决的以下问题. * 如何分片: * 如何合成一个文件: * 中断了从哪个分片开 ...

  8. The database returned no natively generated identity value错误解决方案

    原因:hibernate项目中在学生表的配置文件中: <id name="studentno" column="studentno"> <ge ...

  9. PHP正则表达式提取html超链接中的href地址

    $preg='/<a .*?href="(.*?)".*?>/is'; preg_match_all($preg,$str,$array2); ;$i<count ...

  10. GoCN每日新闻(2019-10-02)

    GoCN每日新闻(2019-10-02) GoCN每日新闻(2019-10-02) 1. Golang中基于Gin和Casbin的web使用方式 https://dev.to/maxwellhertz ...