高级UI-画板Canvas
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的更多相关文章
- Android 高级UI设计笔记07:RecyclerView 的详解
1. 使用RecyclerView 在 Android 应用程序中列表是一个非常重要的控件,适用场合非常多,如新闻列表.应用列表.消息列表等等,但是从Android 一出生到现在并没有非常 ...
- firefox 扩展开发笔记(三):高级ui交互编程
firefox 扩展开发笔记(三):高级ui交互编程 前言 前两篇链接 1:firefox 扩展开发笔记(一):jpm 使用实践以及调试 2:firefox 扩展开发笔记(二):进阶开发之移动设备模拟 ...
- 高级UI晋升之View渲染机制(二)
更多Android高级架构进阶视频学习请点击:https://space.bilibili.com/474380680 优化性能一般从渲染,运算与内存,电量三个方面进行,今天开始说聊一聊Android ...
- iOS开发——高级UI&带你玩转UITableView
带你玩装UITableView 在实际iOS开发中UITableView是使用最多,也是最重要的一个控件,如果你不会用它,那别说什么大神了,菜鸟都不如. 其实关于UItableView事非常简单的,实 ...
- 创新高性能移动 UI 框架-Canvas UI 框架
WebView 里无法获得的能力虽然是「体验增强」与「端基本能力」,但现都基本上有成熟解决方法.但后期的 UI 和 Layout 的性能反而是目前 Web 技术欠缺的.所以,无论是 Titanium ...
- Unity5.3——UI之Canvas
原文:http://docs.unity3d.com/Manual/UISystem.html Canvas 所有的UI都应该放在Canvas里面(子层).Canvas是一个带有Canvas组件的Ga ...
- canvas画画板,canvas画五角星,canvas制作钟表、Konva写钟表
制作一个画画板,有清屏有橡皮擦有画笔可以换颜色 style样式 <head> <meta charset="UTF-8"> <title>画画板 ...
- 高级UI晋升之自定义View实战(九)
更多Android高级架构进阶视频学习请点击:https://space.bilibili.com/474380680 1.前言: 本文采用自定义view的方法来实现一键清除的动画这个功能. 2.效果 ...
- 高级UI晋升之自定义View实战(六)
更多Android高级架构进阶视频学习请点击:https://space.bilibili.com/474380680本篇文章将从Android 自定义属性动画&Camera动画来介绍自定义V ...
随机推荐
- Alpha冲刺(9/10)——追光的人
1.队友信息 队员学号 队员博客 221600219 小墨 https://www.cnblogs.com/hengyumo/ 221600240 真·大能猫 https://www.cnblogs. ...
- 我理解的Linux内存管理
众所周知,内存管理是Linux内核中最基础,也是相当重要的部分.理解相关原理,不管是对内存的理解,还是对大家写用户态代码都很有帮助.很多书上.很多文章都写了相关内容,但个人总觉得内容太复杂,不是太容易 ...
- Partition HDU - 4602 (不知道为什么被放在了FFT的题单里)
题目链接:Vjudge 传送门 相当于把nnn个点分隔为若干块,求所有方案中大小为kkk的块数量 我们把大小为kkk的块,即使在同一种分隔方案中的块 单独考虑,它可能出现的位置是在nnn个点的首.尾. ...
- lxml 和 pyquery 示例 爬 卡牌
import requests from pyquery import PyQuery as pq import json import jsonpath from lxml import etree ...
- 学到了林海峰,武沛齐讲的Day50 django
http请求中产生两个核心对象: http请求:HttpRequest对象 http响应:HttpResponse对象 所在位置:django.http 5/8结束
- fibnacci数列递归实现
斐波那契数列 Fibonacci sequence又称黄金分割数列.因数学家列昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为"兔子数列" ...
- vlang 试用
vlang 是最近出来的一门编程语言,集成了rust,golang, 等语言的特性,轻量.简洁.编译 快速,详细的比价参数可以参考官方文档 安装 目前尽管官方提供了linux以及mac 的二进制文件, ...
- 手动停止jquery ajax请求
主要调用jquery提供的ajax abort方法,详细代码如下: <html> <head> <meta charset="UTF-8"> & ...
- Android入门教程(五)
关注我,每天都有优质技术文章推送,工作,学习累了的时候放松一下自己. 欢迎大家关注我的微信公众号:「醉翁猫咪」 字面量: 1.整数字面量为整型(int) 2.小数字面量为双精度浮点型(double) ...
- Flume 测试 Kafka 案例
Flume Kafka 测试案例,Flume 的配置. a1.sources = s1 a1.channels = c1 a1.sinks = k1 a1.sources.s1.type = netc ...