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