自定义控件其实很简单5/12
尊重原创转载请注明:From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige 侵权必究!
炮兵镇楼
最近龙体欠安,很多任务都堆着,虽说如此,依然没有停下学习的步伐,虽然偶尔还会有点头痛,但是孤依旧在学习……自赞一个~
在1/3中我们结束了全部的Paint方法学习还略带地说了下Matri的简单用法,这两节呢,我们将甩掉第二个陌生又熟悉的情妇:Canvas。Canvas从我们该系列教程的第一节起就嘚啵嘚啵个没完没了,几乎每个View都扯到了它,就像我之前说的那样,自定义控件的关键一步就是如何去绘制控件,绘制说白了就是画,既然要画那么笔和纸是必须的,Canvas就是Android给我们的纸,弥足轻重,它决定了我们能画什么:
上面所罗列出来的各种drawXXX方法就是Canvas中定义好的能画什么的方法(drawPaint除外),除了各种基本型比如矩形圆形椭圆直曲线外Canvas也能直接让我们绘制各种图片以及颜色等等,但是Canvas真正屌的我觉得不是它能画些什么,而是对画布的各种活用,上一节最后的一个例子大家已经粗略见识了变换Canvas配合save和restore方法给我们绘制图形带来的极大便利,事实上Canvas的活用远不止此,在讲Canvas之前,我想先给大家说说Canvas中非常屌毛而且很有个性的一个方法:
- drawBitmapMesh(Bitmap bitmap, int meshWidth, int meshHeight, float[] verts, int vertOffset, int[] colors, int colorOffset, Paint paint)
drawBitmapMesh是个很屌毛的方法,为什么这样说呢?因为它可以对Bitmap做几乎任何改变,是的,你没听错,是任何,几乎无所不能,这个屌毛方法我曾一度怀疑谷歌那些逗比为何将它屈尊在Canvas下,因为它对Bitmap的处理实在在强大了。上一节我们在讲到Matrix的时候说过Matrix可以对我们的图像做多种变换,实际上drawBitmapMesh也可以,只不过需要一点计算,比如我们可以使用drawBitmapMesh来模拟错切skew的效果:
实现过程也非常非常简单:
- public class BitmapMeshView extends View {
- private static final int WIDTH = 19;// 横向分割成的网格数量
- private static final int HEIGHT = 19;// 纵向分割成的网格数量
- private static final int COUNT = (WIDTH + 1) * (HEIGHT + 1);// 横纵向网格交织产生的点数量
- private Bitmap mBitmap;// 位图资源
- private float[] verts;// 交点的坐标数组
- public BitmapMeshView(Context context, AttributeSet attrs) {
- super(context, attrs);
- // 获取位图资源
- mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.gril);
- // 实例化数组
- verts = new float[COUNT * 2];
- /*
- * 生成各个交点坐标
- */
- int index = 0;
- float multiple = mBitmap.getWidth();
- for (int y = 0; y <= HEIGHT; y++) {
- float fy = mBitmap.getHeight() * y / HEIGHT;
- for (int x = 0; x <= WIDTH; x++) {
- float fx = mBitmap.getWidth() * x / WIDTH + ((HEIGHT - y) * 1.0F / HEIGHT * multiple);
- setXY(fx, fy, index);
- index += 1;
- }
- }
- }
- /**
- * 将计算后的交点坐标存入数组
- *
- * @param fx
- * x坐标
- * @param fy
- * y坐标
- * @param index
- * 标识值
- */
- private void setXY(float fx, float fy, int index) {
- verts[index * 2 + 0] = fx;
- verts[index * 2 + 1] = fy;
- }
- @Override
- protected void onDraw(Canvas canvas) {
- // 绘制网格位图
- canvas.drawBitmapMesh(mBitmap, WIDTH, HEIGHT, verts, 0, null, 0, null);
- }
- }
其他的我就不说了,关键代码就一段:
- /*
- * 生成各个交点坐标
- */
- int index = 0;
- float multiple = mBitmap.getWidth();
- for (int y = 0; y <= HEIGHT; y++) {
- float fy = mBitmap.getHeight() * y / HEIGHT;
- for (int x = 0; x <= WIDTH; x++) {
- float fx = mBitmap.getWidth() * x / WIDTH + ((HEIGHT - y) * 1.0F / HEIGHT * multiple);
- setXY(fx, fy, index);
- index += 1;
- }
- }
这段代码生成了200个点的坐标数据全部存入verts数组,verts数组中,偶数位表示x轴坐标,奇数位表示y轴坐标,最终verts数组中的元素构成为:[x,y,x,y,x,y,x,y,x,y,x,y,x,y………………]共200 * 2=400个元素,为什么是400个?如果你不是蠢13的话一定能计算过来。那么现在我们一定很好奇,drawBitmapMesh到底是个什么个意思呢?,其实drawBitmapMesh的原理灰常简单,它按照meshWidth和meshHeight这两个参数的值将我们的图片划分成一定数量的网格,比如上面我们传入的meshWidth和meshHeight均为19,意思就是把整个图片横纵向分成19份:
横纵向19个网格那么意味着横纵向分别有20条分割线对吧,这20条分割线交织又构成了20 * 20个交织点
每个点又有x、y两个坐标……而drawBitmapMesh的verts参数就是存储这些坐标值的,不过是图像变化后的坐标值,什么意思?说起来有点抽象,借用国外大神的两幅图来理解:
如上图,黄色的点是使用mesh分割图像后分割线的交点之一,而drawBitmapMesh的原理就是通过移动这些点来改变图像:
如上图,移动黄色的点后,图像被扭曲改变,你能想象在一幅刚画好的油画上有手指尖一抹的感觉么?油画未干,手指抹过的地方必将被抹得一塌糊涂,drawBitmapMesh的原理就与之类似,只不过我们不常只改变一点,而是改变大量的点来达到效果,而参数verts则存储了改变后的坐标,drawBitmapMesh依据这些坐标来改变图像,如果上面的代码中我们不将每行的x轴坐标进行平移而是单纯地计算了一下均分后的各点坐标:
- /*
- * 生成各个交点坐标
- */
- int index = 0;
- // float multiple = mBitmap.getWidth();
- for (int y = 0; y <= HEIGHT; y++) {
- float fy = mBitmap.getHeight() * y / HEIGHT;
- for (int x = 0; x <= WIDTH; x++) {
- float fx = mBitmap.getWidth() * x / WIDTH;
- // float fx = mBitmap.getWidth() * x / WIDTH + ((HEIGHT - y) * 1.0F / HEIGHT * multiple);
- setXY(fx, fy, index);
- index += 1;
- }
- }
你会发现图像没有任何改变,为什么呢?因为上面我们说过,verts表示了图像变化后各点的坐标,而点坐标的变化是参照最原始均分后的坐标点,也就是图:
中的各个交织点,在此基础上形成变化,比如我们最开始的错切效果,原理很简单,我们这里把图像分成了横竖20条分割线(实际上错切变换只需要四个顶点即可,这里我只作点稍复杂的演示),我们只需将第一行的点x轴向上移动一定距离,而第二行的点移动的距离则比第一行点稍短,依次类推即可,每行点移动的距离我们通过
- (HEIGHT - y) * 1.0F / HEIGHT * multiple
来计算,最终形成错切的效果
drawBitmapMesh不能存储计算后点的值,每次调用drawBitmapMesh方法改变图像都是以基准点坐标为参考的,也就是说,不管你执行drawBitmapMesh方法几次,只要参数没改变,效果不累加。
drawBitmapMesh可以做出很多很多的效果,比如类似放大镜的:
- /*
- * 生成各个交点坐标
- */
- int index = 0;
- float multipleY = mBitmap.getHeight() / HEIGHT;
- float multipleX = mBitmap.getWidth() / WIDTH;
- for (int y = 0; y <= HEIGHT; y++) {
- float fy = multipleY * y;
- for (int x = 0; x <= WIDTH; x++) {
- float fx = multipleX * x;
- setXY(fx, fy, index);
- if (5 == y) {
- if (8 == x) {
- setXY(fx - multipleX, fy - multipleY, index);
- }
- if (9 == x) {
- setXY(fx + multipleX, fy - multipleY, index);
- }
- }
- if (6 == y) {
- if (8 == x) {
- setXY(fx - multipleX, fy + multipleY, index);
- }
- if (9 == x) {
- setXY(fx + multipleX, fy + multipleY, index);
- }
- }
- index += 1;
- }
- }
这时我们将图片眼睛附近的四个点外移到临近的四个点上,图像该区域就会被像放大一样:
太恶心了……我们借助另外一个例子来更好地理解drawBitmapMesh,这个例子与API DEMO类似,我只是参考了国外大神的效果给他加上了一些标志点和位移线段来更好地展示drawBitmapMesh做了什么:
- public class BitmapMeshView2 extends View {
- private static final int WIDTH = 9, HEIGHT = 9;// 分割数
- private static final int COUNT = (WIDTH + 1) * (HEIGHT + 1);// 交点数
- private Bitmap mBitmap;// 位图对象
- private float[] matrixOriganal = new float[COUNT * 2];// 基准点坐标数组
- private float[] matrixMoved = new float[COUNT * 2];// 变换后点坐标数组
- private float clickX, clickY;// 触摸屏幕时手指的xy坐标
- private Paint origPaint, movePaint, linePaint;// 基准点、变换点和线段的绘制Paint
- public BitmapMeshView2(Context context, AttributeSet set) {
- super(context, set);
- setFocusable(true);
- // 实例画笔并设置颜色
- origPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- origPaint.setColor(0x660000FF);
- movePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- movePaint.setColor(0x99FF0000);
- linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- linePaint.setColor(0xFFFFFB00);
- // 获取位图资源
- mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.bt);
- // 初始化坐标数组
- int index = 0;
- for (int y = 0; y <= HEIGHT; y++) {
- float fy = mBitmap.getHeight() * y / HEIGHT;
- for (int x = 0; x <= WIDTH; x++) {
- float fx = mBitmap.getWidth() * x / WIDTH;
- setXY(matrixMoved, index, fx, fy);
- setXY(matrixOriganal, index, fx, fy);
- index += 1;
- }
- }
- }
- /**
- * 设置坐标数组
- *
- * @param array
- * 坐标数组
- * @param index
- * 标识值
- * @param x
- * x坐标
- * @param y
- * y坐标
- */
- private void setXY(float[] array, int index, float x, float y) {
- array[index * 2 + 0] = x;
- array[index * 2 + 1] = y;
- }
- @Override
- protected void onDraw(Canvas canvas) {
- // 绘制网格位图
- canvas.drawBitmapMesh(mBitmap, WIDTH, HEIGHT, matrixMoved, 0, null, 0, null);
- // 绘制参考元素
- drawGuide(canvas);
- }
- /**
- * 绘制参考元素
- *
- * @param canvas
- * 画布
- */
- private void drawGuide(Canvas canvas) {
- for (int i = 0; i < COUNT * 2; i += 2) {
- float x = matrixOriganal[i + 0];
- float y = matrixOriganal[i + 1];
- canvas.drawCircle(x, y, 4, origPaint);
- float x1 = matrixOriganal[i + 0];
- float y1 = matrixOriganal[i + 1];
- float x2 = matrixMoved[i + 0];
- float y2 = matrixMoved[i + 1];
- canvas.drawLine(x1, y1, x2, y2, origPaint);
- }
- for (int i = 0; i < COUNT * 2; i += 2) {
- float x = matrixMoved[i + 0];
- float y = matrixMoved[i + 1];
- canvas.drawCircle(x, y, 4, movePaint);
- }
- canvas.drawCircle(clickX, clickY, 6, linePaint);
- }
- /**
- * 计算变换数组坐标
- */
- private void smudge() {
- for (int i = 0; i < COUNT * 2; i += 2) {
- float xOriginal = matrixOriganal[i + 0];
- float yOriginal = matrixOriganal[i + 1];
- float dist_click_to_origin_x = clickX - xOriginal;
- float dist_click_to_origin_y = clickY - yOriginal;
- float kv_kat = dist_click_to_origin_x * dist_click_to_origin_x + dist_click_to_origin_y * dist_click_to_origin_y;
- float pull = (float) (1000000 / kv_kat / Math.sqrt(kv_kat));
- if (pull >= 1) {
- matrixMoved[i + 0] = clickX;
- matrixMoved[i + 1] = clickY;
- } else {
- matrixMoved[i + 0] = xOriginal + dist_click_to_origin_x * pull;
- matrixMoved[i + 1] = yOriginal + dist_click_to_origin_y * pull;
- }
- }
- }
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- clickX = event.getX();
- clickY = event.getY();
- smudge();
- invalidate();
- return true;
- }
- }
运行后的效果如下:
大波妹子图上我们绘制了很多蓝色和红色的点,默认状态下,蓝色和红色的点是重合在一起的,两者间通过一线段连接,当我们手指在图片上移动时,会出现一个黄色的点,黄色的点代表我们当前的触摸点,而红色的点代表变换后的坐标点,蓝色的点代表基准坐标点:
可以看到越靠近触摸点的红点越向触摸点坍塌,红点表示当前变换后的点坐标,蓝点表示基准点的坐标,所有的变化都是参照蓝点进行的,这个例子可以很容易地理解drawBitmapMesh:
大波妹子揉啊揉~~~~揉啊揉~~~~
drawBitmapMesh参数中有个vertOffset,该参数是verts数组的偏移值,意为从第一个元素开始才对位图就行变化,这些大家自己去尝试下吧,还有colors和colorOffset,类似。
drawBitmapMesh说实话真心很屌,但是计算复杂确是个鸡肋,这么屌的一个方法被埋没其实是由原因可循的,高不成低不就,如上所示,有些变换我们可以使用Matrix等其他方法简单实现,但是drawBitmapMesh就要通过一些列计算,太复杂。那真要做复杂的图形效果呢,考虑到效率我们又会首选OpenGL……这真是一个悲伤的故事……无论怎样,请记住这位烈士一样的方法…………总有用处的
好了,真的要开始搞Canvas,开始搞了哦~~谁先上?
要学懂Canvas就要知道Canvas的本质是什么,那有盆友就会说了,麻痹你不是扯过无数次Canvas是画布么,难道又不是了?是,Canvas是画布,但是我们真的是在Canvas上画东西么?在前几节的一些例子中我们曾这样使用过Canvas:
- Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
- Canvas canvas = new Canvas(bitmap);
- canvas.drawColor(Color.RED);
也就是说将Bitmap注入到Canvas中,尔后Canvas所有的操作都会在这个Bitmap上进行,如果,此时我们的界面中有一个ImageView,那么我们可以直接将绘制后的Bitmap显示出来:
- public class MainActivity extends Activity {
- private ImageView ivMain;
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- ivMain = (ImageView) findViewById(R.id.main_iv);
- Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
- Canvas canvas = new Canvas(bitmap);
- canvas.drawColor(Color.RED);
- ivMain.setImageBitmap(bitmap);
- }
- }
运行效果如图所示:
我们只是简单地填充了一块红色色块,色块的大小由bitmap决定,更确切地说,这个Canvas的大小是由bitmap决定的,类似的方法我们在前几节的例子中也不少用到,这里就不多说了。除了我们自己去new一个Canvas外,我们更常获得Canvas对象的地方是在View的:
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- }
在这里通过onDraw方法的参数传递我们可以获取一个Canvas对象,好奇的同学一定很想知道这个Canvas对象是如何来的,跟我们自己new的有何区别。事实上两者区别不大,最终都是new过来的,只是onDraw方法传过来的Canvas对象拥有一些绘制的上下文关联。这一过程涉及到太多的源码,这里我只简单地提一下。在framework中,Activty被创建时(更准确地说是在addView的时候)会同时创建一个叫做ViewRootImpl的对象,ViewRootImpl是个很碉堡的类,它负责很多GUI的东西,包括我们常见的窗口显示、用户的输入输出等等,同时,它也负责Window跟WMS通信(Window你可以想象是一个容器,里面包含着我们的一个Activity,而AMS呢全称为Activity Manager Service,顾名思义很好理解它的作用),当ViewRootImpl跟WMS建立通信注册了Window后就会发出第一次渲染View Hierachy的请求,涉及到的方法均在ViewRootImpl下:setView、requestLayout、scheduleTraversals等,大家有兴趣可以自己去搜罗看看,在performTraversals方法中ViewRootImpl就会去创建Surface,而此后的渲染则可以通过Surface的lockCanvas方法获取Surface的Canvas来进行,然后遍历View Hierachy把需要绘制的View通过Canvas(View.onDraw(Canvas canvas))绘制到Surface上,绘制完成后解锁(Surface.unlockCanvasAndPost)让SurfaceFlinger将Surface绘制到屏幕上。我们onDraw(Canvas canvas)方法中传入的Canvas对象大致就是这么来的,说起简单,其实中间还有大量的过程被我省略了………………还是不扯为好,扯了讲通宵都讲不完。
上面我们概述了下onDraw参数列表中的Canvas对象是怎么来的,那么Canvas的实质是什么呢?我们通过追踪Canvas的两个构造方法可以发现两者的实现过程:
无参构造方法:
- /**
- * Construct an empty raster canvas. Use setBitmap() to specify a bitmap to
- * draw into. The initial target density is {@link Bitmap#DENSITY_NONE};
- * this will typically be replaced when a target bitmap is set for the
- * canvas.
- */
- public Canvas() {
- if (!isHardwareAccelerated()) {
- // 0 means no native bitmap
- mNativeCanvas = initRaster(0);
- mFinalizer = new CanvasFinalizer(mNativeCanvas);
- } else {
- mFinalizer = null;
- }
- }
含Bitmap对象作为参数的构造方法:
- /**
- * Construct a canvas with the specified bitmap to draw into. The bitmap
- * must be mutable.
- *
- * <p>The initial target density of the canvas is the same as the given
- * bitmap's density.
- *
- * @param bitmap Specifies a mutable bitmap for the canvas to draw into.
- */
- public Canvas(Bitmap bitmap) {
- if (!bitmap.isMutable()) {
- throw new IllegalStateException("Immutable bitmap passed to Canvas constructor");
- }
- throwIfCannotDraw(bitmap);
- mNativeCanvas = initRaster(bitmap.ni());
- mFinalizer = new CanvasFinalizer(mNativeCanvas);
- mBitmap = bitmap;
- mDensity = bitmap.mDensity;
- }
大家看到这两个构造方法我都把它的注释给COPY出来了,目的就是想告诉大家,虽然说无参的构造方法并没有传入Bitmap对象,但是Android依然建议(苛刻地说是要求)我们使用Canvas的setBitmap()方法去为Canvas指定一个Bitmap对象!为什么Canvas非要一样Bitmap对象呢?原因很简单,Canvas需要一个Bitmap对象来保存像素。Canvas有大量的代码被封装并通过jni调用,事实上Android涉及图形图像处理的大量方法都是通过jni调用的,比如上面两个构造方法都调用了一个initRaster方法,这个方法的实现灰常简单:
- static SkCanvas* initRaster(JNIEnv* env, jobject, SkBitmap* bitmap) {
- if (bitmap) {
- return new SkCanvas(*bitmap);
- } else {
- // Create an empty bitmap device to prevent callers from crashing
- // if they attempt to draw into this canvas.
- SkBitmap emptyBitmap;
- return new SkCanvas(emptyBitmap);
- }
- }
可以看到bitmap又被封装成了一个SkCanvas对象。上面我们曾说过,onDraw中传来的Cnavas对象来自于ViewRootImpl的Surface,当调用Surface.lockCanvas时会从图像缓存队列中取出一个可用缓存,把当前Posted Buffer的内容COPY到新缓存中然后加锁该缓存区域并设置为Locked Buffer。此时会根据新缓存的内存地址构建一个SkBitmap并将该SkBitmap设置到SkCanvas中并返回与之对应Canvas。而当调用Surface.unlockCanvasAndPost时则会清空SkCanvas并将SkBitmap设置为空,此时Locked Buffer将会被解锁并重新扔回图像缓存队列中,同时将Poated Buffer设置为Locked Buffer,旧的Posted Buffer就可以被下次取出来使用,设置Locked Buffer为空,当SF下次进行screen composite的时候就会把当前Poated Buffer绘制到屏幕上,这算是Canvas到屏幕绘制的一个小过程,当然事实比我说的复杂得多,这又是我的一个删减版本而已,懂得就听,不懂的权当废话不用管,我们不会涉及到这么深,像什么HardwareCanvas、GL之类的太过深入没必要去学,这里只阐述一个小原理而已。
对我们普通开发者来说,要记住的的是,一个Canvas需要一个Bitmap来保存像素信息,你说不要行不行?当然可以,画得东西没法保存而已,既然没法保存那我画来还有何意义呢?isn't it?
Canvas所提供的各种方法根据功能来看大致可以分为几类,第一是以drawXXX为主的绘制方法,第二是以clipXXX为主的裁剪方法,第三是以scale、skew、translate和rotate组成的Canvas变换方法,最后一类则是以saveXXX和restoreXXX构成的画布锁定和还原,还有一些渣渣方法就不归类了。
绘制图形、变换锁定还原画布我们都在前面的一些code中使用过,那么什么叫裁剪画布呢?我们来看一段code:
- public class CanvasView extends View {
- public CanvasView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
- @Override
- protected void onDraw(Canvas canvas) {
- canvas.drawColor(Color.BLUE);
- canvas.clipRect(0, 0, 500, 500);
- canvas.drawColor(Color.RED);
- }
- }
这段代码灰常简单,我们在onDraw中将整个画布绘制成蓝色,然后我们在当前画布上从[0,0]为左端点开始裁剪出一块500x500大小的矩形,再次将画布绘制成红色,你会发现只有被裁剪的区域才能被绘制成红色:
是不是有点懂裁剪的意思了?不懂?没事,我们再画一个圆加深理解:
- public class CanvasView extends View {
- private Paint mPaint;
- public CanvasView(Context context, AttributeSet attrs) {
- super(context, attrs);
- mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- mPaint.setStyle(Paint.Style.FILL);
- mPaint.setColor(Color.GREEN);
- }
- @Override
- protected void onDraw(Canvas canvas) {
- canvas.drawColor(Color.BLUE);
- canvas.clipRect(0, 0, 500, 500);
- canvas.drawColor(Color.RED);
- canvas.drawCircle(500, 600, 100, mPaint);
- }
- }
如代码所示,我们在以[500,600]为圆心绘制一个半径为100px的绿色圆,按道理来说,这个圆应该刚好与红色区域下方相切对吧,但是事实上呢我们见不到任何效果,为什么?因为如上所说,当前画布被“裁剪”了,只有500x500也就是上图中红色区域的大小了,如果我们所绘制的东西在该区域外部,即便绘制了你也看不到,这时我们稍增大圆的半径:
- canvas.drawCircle(500, 600, 150, mPaint);
终于看到我们的圆“露”出来了~~现在你能稍微明白裁剪的作用了么?上面的代码中我们使用到了Canvas的
- clipRect(int left, int top, int right, int bottom)
方法,与之类似的还有
- clipRect(float left, float top, float right, float bottom)
方法,一个int一个float,不扯了。除此之外还有两个与之对应的方法
- clipRect(Rect rect)
- clipRect(RectF rect)
,Rect和RectF是类似的,只不过RectF中涉及计算的时候数值类型均为float型,两者均表示一块规则矩形,何以见得呢?我们以Rect为例来Test一下:
- public class CanvasView extends View {
- private Rect mRect;
- public CanvasView(Context context, AttributeSet attrs) {
- super(context, attrs);
- mRect = new Rect(0, 0, 500, 500);
- }
- @Override
- protected void onDraw(Canvas canvas) {
- canvas.drawColor(Color.BLUE);
- canvas.clipRect(mRect);
- canvas.drawColor(Color.RED);
- }
- }
如代码所示这样我们得到的结果跟上面的结果并无二致,蓝色的底,500x500大小的红色矩形,但是Rect的意义远不止于此,鉴于Rect类并不复杂,我就讲两个其比较重要的方法,我们稍微更改下我们的代码:
- public class CanvasView extends View {
- private Rect mRect;
- public CanvasView(Context context, AttributeSet attrs) {
- super(context, attrs);
- mRect = new Rect(0, 0, 500, 500);
- mRect.intersect(250, 250, 750, 750);
- }
- @Override
- protected void onDraw(Canvas canvas) {
- canvas.drawColor(Color.BLUE);
- canvas.clipRect(mRect);
- canvas.drawColor(Color.RED);
- }
- }
大家看到我在实例化了一个Rect后调用了intersect方法,这个方法的作用是什么?来看看效果先:
PS:黄色线框为后期加上的辅助线非程序生成
可以看到原先的红色区域变小了,这是怎么回事呢?其实intersect的作用跟我们之前学到的图形混合模式有点类似,它会取两个区域的相交区域作为最终区域,上面我们的第一个区域是在实例化Rect时确定的(0, 0, 500, 500),第二个区域是调用intersect方法时指定的(250, 250, 750, 750),这两个区域对应上图的两个黄色线框,两者相交的地方则为最终的红色区域,而intersect方法的计算方式是相当有趣的,它不是单纯地计算相交而是去计算相交区域最近的左上端点和最近的右下端点,不知道大家是否明白这个意思,我们来看Rect中的另一个union方法你就会懂,union方法与intersect相反,取的是相交区域最远的左上端点作为新区域的左上端点,而取最远的右下端点作为新区域的右下端点,比如:
- mRect.union(250, 250, 750, 750);
运行后我们会看到如下结果:
是不是觉得不是我们想象中的那样单纯地两个区域相加?没事,好好体会,后面还有类似的。类似的方法Rect和RectF都有很多,效果都是显而易见的就不多说了,有兴趣大家可以自己去try。
说到这里会有很多童鞋会问,裁剪只是个矩形区域,如果我想要更多不规则的裁剪区域怎么办呢?别担心,Android必然也考虑到这样的情况,其提供了一个
- clipPath(Path path)
方法给我们以Path的方式创建更多不规则的裁剪区域,在1/4讲PathEffect的时候我们曾对Path有所接触,但是依旧不了解
Path是android中用来封装几何学路径的一个类,因为Path在图形绘制上占的比重还是相当大的,这里我们先来学习一下这个Path,来看看其一些具体的用法:
- public class PathView extends View {
- private Path mPath;// 路径对象
- private Paint mPaint;// 画笔对象
- public PathView(Context context, AttributeSet attrs) {
- super(context, attrs);
- /*
- * 实例化画笔并设置属性
- */
- mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
- mPaint.setStyle(Paint.Style.STROKE);
- mPaint.setColor(Color.CYAN);
- // 实例化路径
- mPath = new Path();
- // 连接路径到点[100,100]
- mPath.lineTo(100, 100);
- }
- @Override
- protected void onDraw(Canvas canvas) {
- // 绘制路径
- canvas.drawPath(mPath, mPaint);
- }
- }
这里我们用到了Path的一个方法
- lineTo(float x, float y)
该方法很简单咯,顾名思义将路径连接至某个坐标点,事实也是如此:
注意,当我们没有移动Path的点时,其默认的起点为画布的[0,0]点,当然我们可以通过
- moveTo(float x, float y)
方法来改变这个起始点的位置:
- // 实例化路径
- mPath = new Path();
- //移动点至[300,300]
- mPath.moveTo(300, 300);
- // 连接路径到点[100,100]
- mPath.lineTo(100, 100);
效果如下:
当然我们可以考虑多次调用lineTo方法来绘制更复杂的图形:
- // 实例化路径
- mPath = new Path();
- // 移动点至[300,300]
- mPath.moveTo(100, 100);
- // 连接路径到点
- mPath.lineTo(300, 100);
- mPath.lineTo(400, 200);
- mPath.lineTo(200, 200);
一个没有封闭的类似平行四边形的线条:
如果此时我们想闭合该曲线让它变成一个形状该怎么做呢?聪明的你一定想到
- mPath.lineTo(100, 100)
然而Path给我提供了更便捷的方法
- close()
去闭合曲线:
- // 实例化路径
- mPath = new Path();
- // 移动点至[300,300]
- mPath.moveTo(100, 100);
- // 连接路径到点
- mPath.lineTo(300, 100);
- mPath.lineTo(400, 200);
- mPath.lineTo(200, 200);
- // 闭合曲线
- mPath.close();
那么有些朋友会问Path就只能光绘制这些单调的线段么?肯定不是!Path在绘制的方法中提供了许多XXXTo的方法来帮助我们绘制各类直线、曲线,例如,方法
- quadTo(float x1, float y1, float x2, float y2)
可以让我们绘制二阶贝赛尔曲线,什么叫贝赛尔曲线?其实很简单,使用三个或多个点来确定的一条曲线,贝塞尔曲线在图形图像学中有相当重要的地位,Path中也提供了一些方法来给我们模拟低阶贝赛尔曲线。
贝塞尔曲线的定义也比较简单,你只需要一个起点、一个终点和至少零个控制点则可定义一个贝赛尔曲线,当控制点为零时,只有起点和终点,此时的曲线说白了就是一条线段,我们称之为
PS:以下图片和公式均来自维基百科和互联网
一阶贝赛尔曲线
其公式可概括为:
其中B(t)为时间为t时点的坐标,P0为起点、Pn为终点
贝塞尔曲线于1962年由法国数学家Pierre Bézier第一次研究使用并给出了详细的计算公式,So该曲线也是由其名字命名。Path中给出的quadTo方法属于
二阶贝赛尔曲线
二阶贝赛尔曲线的一个明显特征是其拥有一个控制点,大家可以这样想想贝赛尔曲线,在一根两端固定橡皮筋上有一块磁铁,现在我们拿另一块磁铁去吸引橡皮筋上的磁铁,因为引力,橡皮筋会随着我们手上磁铁的移动而改变形状,又因为橡皮筋的张力让束缚在橡皮筋上的磁铁不会轻易吸附到我们手上的磁铁,这时橡皮筋的状态就可以看成是一条贝塞尔曲线,而我们手中的磁铁就是一个控制点,通过这个控制点我们“拉扯”橡皮筋的曲度。
二阶贝赛尔曲线的公式为:
同样的,Path中也提供了三阶贝塞尔曲线的方法cubicTo,按照上面我们的推论,三阶应该是有两个控制点才对对吧
三阶贝赛尔曲线
公式:
高阶贝赛尔曲线在Path中没有对应的方法,对我们来说三阶也足够了,不过大家可以了解下,难得我在墙外找到如此动感的贝赛尔曲线高清无码动图
高阶贝塞尔曲线
四阶:
五阶:
贝塞尔曲线通用公式:
回到我们Path的quadTo方法,我们可以使用它来绘制一条曲线:
- // 实例化路径
- mPath = new Path();
- // 移动点至[100,100]
- mPath.moveTo(100, 100);
- // 连接路径到点
- mPath.quadTo(200, 200, 300, 100);
看图说话:
其中quadTo的前两个参数为控制点的坐标,后两个参数为终点坐标,至于起点嘛……这么二的问题就别问了……是不是很简单?如果你这么认为那就太小看贝塞尔曲线了。在我们对Path有一定的了解后会使用Path和裁剪做个有趣的东西,接着看Path的三阶贝赛尔曲线:
- cubicTo(float x1, float y1, float x2, float y2, float x3, float y3)
与quadTo类似,前四个参数表示两个控制点,最后两个参数表示终点:
- // 实例化路径
- mPath = new Path();
- // 移动点至[100,100]
- mPath.moveTo(100, 100);
- // 连接路径到点
- mPath.cubicTo(200, 200, 300, 0, 400, 100);
很好理解:
贝塞尔曲线是图形图像学中相当重要的一个概念,活用它可以得到很多很有意思的效果,比如,我在界面中简单模拟一下杯子中水消匿的效果:
当然你也可以反过来让模拟往杯子里倒水的效果~实现过程非常简单,说白了就是不断移动二阶曲线的控制点同时不断更改顶部各点的Y坐标,然后不断重绘:
- public class WaveView extends View {
- private Path mPath;// 路径对象
- private Paint mPaint;// 画笔对象
- private int vWidth, vHeight;// 控件宽高
- private float ctrX, ctrY;// 控制点的xy坐标
- private float waveY;// 整个Wave顶部两端点的Y坐标,该坐标与控制点的Y坐标增减幅一致
- private boolean isInc;// 判断控制点是该右移还是左移
- public WaveView(Context context, AttributeSet attrs) {
- super(context, attrs);
- // 实例化画笔并设置参数
- mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
- mPaint.setColor(0xFFA2D6AE);
- // 实例化路径对象
- mPath = new Path();
- }
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- // 获取控件宽高
- vWidth = w;
- vHeight = h;
- // 计算控制点Y坐标
- waveY = 1 / 8F * vHeight;
- // 计算端点Y坐标
- ctrY = -1 / 16F * vHeight;
- }
- @Override
- protected void onDraw(Canvas canvas) {
- /*
- * 设置Path起点
- * 注意我将Path的起点设置在了控件的外部看不到的区域
- * 如果我们将起点设置在控件左端x=0的位置会使得贝塞尔曲线变得生硬
- * 至于为什么刚才我已经说了
- * 所以我们稍微让起点往“外”走点
- */
- mPath.moveTo(-1 / 4F * vWidth, waveY);
- /*
- * 以二阶曲线的方式通过控制点连接位于控件右边的终点
- * 终点的位置也是在控件外部
- * 我们只需不断让ctrX的大小变化即可实现“浪”的效果
- */
- mPath.quadTo(ctrX, ctrY, vWidth + 1 / 4F * vWidth, waveY);
- // 围绕控件闭合曲线
- mPath.lineTo(vWidth + 1 / 4F * vWidth, vHeight);
- mPath.lineTo(-1 / 4F * vWidth, vHeight);
- mPath.close();
- canvas.drawPath(mPath, mPaint);
- /*
- * 当控制点的x坐标大于或等于终点x坐标时更改标识值
- */
- if (ctrX >= vWidth + 1 / 4F * vWidth) {
- isInc = false;
- }
- /*
- * 当控制点的x坐标小于或等于起点x坐标时更改标识值
- */
- else if (ctrX <= -1 / 4F * vWidth) {
- isInc = true;
- }
- // 根据标识值判断当前的控制点x坐标是该加还是减
- ctrX = isInc ? ctrX + 20 : ctrX - 20;
- /*
- * 让“水”不断减少
- */
- if (ctrY <= vHeight) {
- ctrY += 2;
- waveY += 2;
- }
- mPath.reset();
- // 重绘
- invalidate();
- }
- }
除了上面的几个XXXTo外,Path还提供了一个
- arcTo (RectF oval, float startAngle, float sweepAngle)
方法用来生成弧线,其实说白了就是从圆或椭圆上截取一部分而已 = =
- // 实例化路径
- mPath = new Path();
- // 移动点至[100,100]
- mPath.moveTo(100, 100);
- // 连接路径到点
- RectF oval = new RectF(100, 100, 200, 200);
- mPath.arcTo(oval, 0, 90);
效果如下图:
这里要注意哦,使用Path生成的路径必定都是连贯的,虽然我们使用arcTo绘制的是一段弧但其最终都会与我们的起始点[100,100]连接起来,如果你不想连怎么办?简单,强制让arcTo绘制的起点作为Path的起点不就是了?Path也提供了另一个重载方法:
- arcTo (RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo)
该方法只是多了一个布尔值,值为true时将会把弧的起点作为Path的起点:
- mPath.arcTo(oval, 0, 90, true);
like below:
Path中除了上面介绍的几个XXXTo方法外还有一套rXXXTo方法:
- rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3)
- rLineTo(float dx, float dy)
- rMoveTo(float dx, float dy)
- rQuadTo(float dx1, float dy1, float dx2, float dy2)
这一系列rXXXTo方法其实跟上面的那些XXXTo差不多的,唯一的不同是rXXXTo方法的参考坐标是相对的而XXXTo方法的参考坐标始终是参照画布原点坐标,什么意思呢?举个简单的例子:
- public class PathView extends View {
- private Path mPath;// 路径对象
- private Paint mPaint;// 画笔对象
- public PathView(Context context, AttributeSet attrs) {
- super(context, attrs);
- /*
- * 实例化画笔并设置属性
- */
- mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
- mPaint.setStyle(Paint.Style.STROKE);
- mPaint.setColor(Color.CYAN);
- mPaint.setStrokeWidth(5);
- // 实例化路径
- mPath = new Path();
- // 移动点至[100,100]
- mPath.moveTo(100, 100);
- // 连接路径到点
- mPath.lineTo(200, 200);
- }
- @Override
- protected void onDraw(Canvas canvas) {
- // 绘制路径
- canvas.drawPath(mPath, mPaint);
- }
- }
上述代码我们从点[100,100]开始连接点[200,200]构成了一条线段:
这个点[200,200]是相对于画布圆点坐标[0,0]而言的,这点大家应该好理解,如果我们换成
- mPath.rLineTo(200, 200);
那么它的意思就是将会以[100,100]作为原点坐标,连接以其为原点坐标的坐标点[200,200],如果换算成一画布原点的话,实际上现在的[200,200]就是[300,300]了:
懂了么?而这个前缀r也就是relative(相对)的简写,so easy是么!头脑简单!
XXXTo方法可以连接Path中的曲线而Path提供的另一系列addXXX方法则可以让我们直接往Path中添加一些曲线,比如
- addArc(RectF oval, float startAngle, float sweepAngle)
方法允许我们将一段弧形添加至Path,注意这里我用到了“添加”这个词汇,也就是说,通过addXXX方法添加到Path中的曲线是不会和上一次的曲线进行连接的:
- public class PathView extends View {
- private Path mPath;// 路径对象
- private Paint mPaint;// 路径画笔对象
- public PathView(Context context, AttributeSet attrs) {
- super(context, attrs);
- /*
- * 实例化画笔并设置属性
- */
- mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
- mPaint.setStyle(Paint.Style.STROKE);
- mPaint.setColor(Color.CYAN);
- mPaint.setStrokeWidth(5);
- // 实例化路径
- mPath = new Path();
- // 移动点至[100,100]
- mPath.moveTo(100, 100);
- // 连接路径到点
- mPath.lineTo(200, 200);
- // 添加一条弧线到Path中
- RectF oval = new RectF(100, 100, 300, 400);
- mPath.addArc(oval, 0, 90);
- }
- @Override
- protected void onDraw(Canvas canvas) {
- // 绘制路径
- canvas.drawPath(mPath, mPaint);
- }
- }
如图和代码所示,虽然我们先绘制了由[100,100]到[200,200]的线段,但是在我们往Path中添加了一条弧线后该弧线并没与线段连接。除了addArc,Path还提供了一系列的add方法
- addCircle(float x, float y, float radius, Path.Direction dir)
- addOval(float left, float top, float right, float bottom, Path.Direction dir)
- addRect(float left, float top, float right, float bottom, Path.Direction dir)
- addRoundRect(float left, float top, float right, float bottom, float rx, float ry, Path.Direction dir)
这些方法和addArc有很明显的区别,就是多了一个Path.Direction参数,其他呢都大同小异,除此之外不知道大家还发现没有,addArc是往Path中添加一段弧,说白了就是一条开放的曲线,而上述几种方法都是一个具体的图形,或者说是一条闭合的曲线,Path.Direction的意思就是标识这些闭合曲线的闭合方向。那什么叫闭合方向呢?光说大家一定会蒙,有学习激情的童鞋看到后肯定会马上敲代码试验一下两者的区别,可是不管你如何改,单独地在一条闭合曲线上你是看不出所谓闭合方向的区别的,这时我们可以借助Canvas的另一个方法来简单地说明一下
- drawTextOnPath(String text, Path path, float hOffset, float vOffset, Paint paint)
这个方法呢很简单沿着Path绘制一段文字,参数也是一看就该懂得了不多说。Path.Direction只有两个常量值CCW和CW分别表示逆时针方向闭合和顺时针方向闭合,我们来看一段代码
- public class PathView extends View {
- private Path mPath;// 路径对象
- private Paint mPaint;// 路径画笔对象
- private TextPaint mTextPaint;// 文本画笔对象
- public PathView(Context context, AttributeSet attrs) {
- super(context, attrs);
- /*
- * 实例化画笔并设置属性
- */
- mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
- mPaint.setStyle(Paint.Style.STROKE);
- mPaint.setColor(Color.CYAN);
- mPaint.setStrokeWidth(5);
- mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.LINEAR_TEXT_FLAG);
- mTextPaint.setColor(Color.DKGRAY);
- mTextPaint.setTextSize(20);
- // 实例化路径
- mPath = new Path();
- // 添加一条弧线到Path中
- RectF oval = new RectF(100, 100, 300, 400);
- mPath.addOval(oval, Path.Direction.CW);
- }
- @Override
- protected void onDraw(Canvas canvas) {
- // 绘制路径
- canvas.drawPath(mPath, mPaint);
- // 绘制路径上的文字
- canvas.drawTextOnPath("ad撒发射点发放士大夫斯蒂芬斯蒂芬森啊打扫打扫打扫达发达省份撒旦发射的", mPath, 0, 0, mTextPaint);
- }
- }
我们往Path中添加了一条闭合方向为CW椭圆形的闭合曲线并将其绘制在Canvas上,同时呢我们沿着该曲线绘制了一段文本,效果如下:
如果我们把闭合方向改为CCW那么会发生什么呢?
- mPath.addOval(oval, Path.Direction.CCW);
沿着Path的文字全都在闭合曲线的“内部”了,Path.Direction闭合方向大概就是这么个意思。对于我们平时开发来说,掌握Path的以上一些方法已经是足够了,当然Path的方法还有很多,但是因为平时开发涉及的少,我也就不累赘了,毕竟用得少或者根本不会用到的东西说了也是浪费口水,对吧。
Path用的也相当广泛,在之前的章节中我们也讲过一个PathEffect类,两者结合可以得到很多很酷的效果。在众多的用途中,使用Path做折线图算是最最最常见的了,仅仅使用以上我们讲到的一些Path的方法可以完成很多的折线图效果。
在上一节最后的一个例子中我们绘制了一个自定义的圈圈View,当时我跟大家说过在你想去自定义一个控件的时候一定要把自己看作一个designer而不是coder,你要用设计的眼光去看待一个控件,那么我们在做一个折线图的控件之前就应该要分析一个折线图应该是怎样的,下面我google一些简单折线图的例子:
这种比较简单
这种呢有文字标注稍难
这种就复杂了点
不管是哪种折线图,我们都可以发现其必有一个横坐标和一个纵坐标且其上都有刻度,一般情况下来说横纵坐标上的刻度数量是一样的。对于平面折线图来说,分析到上面一点就差不多了,而我们要做的折线图控件我在PS里简单地做了一个design:
设计地很简单,当中有一些辅助参数什么的,实际上整个控件就几个元素:
如上图所示,两个带刻度的轴和一个网格还有两个轴文字标识和一条曲线,very simple!图好像很简单~~但是真要code起来就不是件容易的事了,首先我们要考虑到不同的数据、其次是屏幕的适配,说到适配,上一节我们曾讲过,因为屏幕的多元化,我们必定不能写死一个参数,so~我们在上一节画圈圈的时候是使用控件的边长来作为所有数值的基准参考,这次也一样。
因为折线图的形状是跟外部数据相关的,所以在设计的时候我们必定要考虑到对外公布一个设置数据的方法:
- /**
- * 设置数据
- *
- * @param pointFs
- * 点集合
- */
- public synchronized void setData(List<PointF> pointFs, String signX, String signY) {
- /*
- * 数据为空直接GG
- */
- if (null == pointFs || pointFs.size() == 0)
- throw new IllegalArgumentException("No data to display !");
- /*
- * 控制数据长度不超过10个
- * 对于折线图来说数据太多就没必要用折线图表示了而是使用散点图
- */
- if (pointFs.size() > 10)
- throw new IllegalArgumentException("The data is too long to display !");
- // 设置数据并重绘视图
- this.pointFs = pointFs;
- this.signX = signX;
- this.signY = signY;
- invalidate();
- }
折线图是表示的数据一般不会太多,如果太多,在有限的先是空间内必定显示鸡肋……当然股票那种巨幅大盘之类的另说。所以在上面的数据设置中我强制将数据长度控制在10个以内。
PS:该方法在设计上不太符合设计原则,这里就当大家都不会设计模式设计原则了 = = Fuck……
上面我们说过会以控件的边长作为基准参考计算各种数值,因为我们还没学习如何测量控件,这里还是和上一节一样强制将控件的宽高设置一致(强制竖屏):
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- // 在我们没学习测量控件之前强制宽高一致
- super.onMeasure(widthMeasureSpec, widthMeasureSpec);
- }
而控件尺寸我们在onSizeChanged方法中获取,这个方法是官方比较推崇的获取控件尺寸的方法,如果你不需要更精确的测量的话,同时我们也就将就在该方法内计算各类数值了:
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- // 获取控件尺寸
- viewSize = w;
- // 计算纵轴标识文本坐标
- textY_X = viewSize * TIME_X;
- textY_Y = viewSize * TIME_Y;
- // 计算横轴标识文本坐标
- textX_X = viewSize * MONEY_X;
- textX_Y = viewSize * MONEY_Y;
- // 计算xy轴标识文本大小
- textSignSzie = viewSize * TEXT_SIGN;
- // 计算网格左上右下两点坐标
- left = viewSize * LEFT;
- top = viewSize * TOP;
- right = viewSize * RIGHT;
- bottom = viewSize * BOTTOM;
- // 计算粗线宽度
- thickLineWidth = viewSize * THICK_LINE_WIDTH;
- // 计算细线宽度
- thinLineWidth = viewSize * THIN_LINE_WIDTH;
- }
其中的常量值均为比例值,根据控件中元素占比计算实际的像素大小,绘制逻辑稍微有点复杂,但是并不难,这里我就直接上全部代码了:
- public class PolylineView extends View {
- private static final float LEFT = 1 / 16F, TOP = 1 / 16F, RIGHT = 15 / 16F, BOTTOM = 7 / 8F;// 网格区域相对位置
- private static final float TIME_X = 3 / 32F, TIME_Y = 1 / 16F, MONEY_X = 31 / 32F, MONEY_Y = 15 / 16F;// 文字坐标相对位置
- private static final float TEXT_SIGN = 1 / 32F;// 文字相对大小
- private static final float THICK_LINE_WIDTH = 1 / 128F, THIN_LINE_WIDTH = 1 / 512F;// 粗线和细线相对大小
- private TextPaint mTextPaint;// 文字画笔
- private Paint linePaint, pointPaint;// 线条画笔和点画笔
- private Path mPath;// 路径对象
- private Bitmap mBitmap;// 绘制曲线的Btimap对象
- private Canvas mCanvas;// 装载mBitmap的Canvas对象
- private List<PointF> pointFs;// 数据列表
- private float[] rulerX, rulerY;// xy轴向刻度
- private String signX, signY;// 设置X和Y坐标分别表示什么的文字
- private float textY_X, textY_Y, textX_X, textX_Y;// 文字坐标
- private float textSignSzie;// xy坐标标识文本字体大小
- private float thickLineWidth, thinLineWidth;// 粗线和细线宽度
- private float left, top, right, bottom;// 网格区域左上右下两点坐标
- private int viewSize;// 控件尺寸
- private float maxX, maxY;// 横纵轴向最大刻度
- private float spaceX, spaceY;// 刻度间隔
- public PolylineView(Context context, AttributeSet attrs) {
- super(context, attrs);
- // 实例化文本画笔并设置参数
- mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.LINEAR_TEXT_FLAG);
- mTextPaint.setColor(Color.WHITE);
- // 实例化线条画笔并设置参数
- linePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
- linePaint.setStyle(Paint.Style.STROKE);
- linePaint.setColor(Color.WHITE);
- // 实例化点画笔并设置参数
- pointPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
- pointPaint.setStyle(Paint.Style.FILL);
- pointPaint.setColor(Color.WHITE);
- // 实例化Path对象
- mPath = new Path();
- // 实例化Canvas对象
- mCanvas = new Canvas();
- // 初始化数据
- initData();
- }
- /**
- * 初始化数据支撑
- * View初始化时可以考虑给予一个模拟数据
- * 当然我们可以通过setData方法设置自己的数据
- */
- private void initData() {
- Random random = new Random();
- pointFs = new ArrayList<PointF>();
- for (int i = 0; i < 20; i++) {
- PointF pointF = new PointF();
- pointF.x = (float) (random.nextInt(100) * i);
- pointF.y = (float) (random.nextInt(100) * i);
- pointFs.add(pointF);
- }
- }
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- // 在我们没学习测量控件之前强制宽高一致
- super.onMeasure(widthMeasureSpec, widthMeasureSpec);
- }
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- // 获取控件尺寸
- viewSize = w;
- // 计算纵轴标识文本坐标
- textY_X = viewSize * TIME_X;
- textY_Y = viewSize * TIME_Y;
- // 计算横轴标识文本坐标
- textX_X = viewSize * MONEY_X;
- textX_Y = viewSize * MONEY_Y;
- // 计算xy轴标识文本大小
- textSignSzie = viewSize * TEXT_SIGN;
- // 计算网格左上右下两点坐标
- left = viewSize * LEFT;
- top = viewSize * TOP;
- right = viewSize * RIGHT;
- bottom = viewSize * BOTTOM;
- // 计算粗线宽度
- thickLineWidth = viewSize * THICK_LINE_WIDTH;
- // 计算细线宽度
- thinLineWidth = viewSize * THIN_LINE_WIDTH;
- }
- @Override
- protected void onDraw(Canvas canvas) {
- // 填充背景
- canvas.drawColor(0xFF9596C4);
- // 绘制标识元素
- drawSign(canvas);
- // 绘制网格
- drawGrid(canvas);
- // 绘制曲线
- drawPolyline(canvas);
- }
- /**
- * 绘制曲线
- * 这里我使用一个新的Bitmap对象结合新的Canvas对象来绘制曲线
- * 当然你可以直接在原来的canvas(onDraw传来的那个)中直接绘制如果你还没被坐标搞晕的话……
- *
- * @param canvas
- * 画布
- *
- */
- private void drawPolyline(Canvas canvas) {
- // 生成一个Bitmap对象大小和我们的网格大小一致
- mBitmap = Bitmap.createBitmap((int) (viewSize * (RIGHT - LEFT) - spaceX), (int) (viewSize * (BOTTOM - TOP) - spaceY), Bitmap.Config.ARGB_8888);
- // 将Bitmap注入Canvas
- mCanvas.setBitmap(mBitmap);
- // 为画布填充一个半透明的红色
- mCanvas.drawARGB(75, 255, 0, 0);
- // 重置曲线
- mPath.reset();
- /*
- * 生成Path和绘制Point
- */
- for (int i = 0; i < pointFs.size(); i++) {
- // 计算x坐标
- float x = mCanvas.getWidth() / maxX * pointFs.get(i).x;
- // 计算y坐标
- float y = mCanvas.getHeight() / maxY * pointFs.get(i).y;
- y = mCanvas.getHeight() - y;
- // 绘制小点点
- mCanvas.drawCircle(x, y, thickLineWidth, pointPaint);
- /*
- * 如果是第一个点则将其设置为Path的起点
- */
- if (i == 0) {
- mPath.moveTo(x, y);
- }
- // 连接各点
- mPath.lineTo(x, y);
- }
- // 设置PathEffect
- // linePaint.setPathEffect(new CornerPathEffect(200));
- // 重置线条宽度
- linePaint.setStrokeWidth(thickLineWidth);
- // 将Path绘制到我们自定的Canvas上
- mCanvas.drawPath(mPath, linePaint);
- // 将mBitmap绘制到原来的canvas
- canvas.drawBitmap(mBitmap, left, top + spaceY, null);
- }
- /**
- * 绘制网格
- *
- * @param canvas
- * 画布
- */
- private void drawGrid(Canvas canvas) {
- // 锁定画布
- canvas.save();
- // 设置线条画笔宽度
- linePaint.setStrokeWidth(thickLineWidth);
- // 计算xy轴Path
- mPath.moveTo(left, top);
- mPath.lineTo(left, bottom);
- mPath.lineTo(right, bottom);
- // 绘制xy轴
- canvas.drawPath(mPath, linePaint);
- // 绘制线条
- drawLines(canvas);
- // 释放画布
- canvas.restore();
- }
- /**
- * 绘制网格
- *
- * @param canvas
- * 画布
- */
- private void drawLines(Canvas canvas) {
- // 计算刻度文字尺寸
- float textRulerSize = textSignSzie / 2F;
- // 重置文字画笔文字尺寸
- mTextPaint.setTextSize(textRulerSize);
- // 重置线条画笔描边宽度
- linePaint.setStrokeWidth(thinLineWidth);
- // 获取数据长度
- int count = pointFs.size();
- // 计算除数的值为数据长度减一
- int divisor = count - 1;
- // 计算横轴数据最大值
- maxX = 0;
- for (int i = 0; i < count; i++) {
- if (maxX < pointFs.get(i).x) {
- maxX = pointFs.get(i).x;
- }
- }
- // 计算横轴最近的能被count整除的值
- int remainderX = ((int) maxX) % divisor;
- maxX = remainderX == 0 ? ((int) maxX) : divisor - remainderX + ((int) maxX);
- // 计算纵轴数据最大值
- maxY = 0;
- for (int i = 0; i < count; i++) {
- if (maxY < pointFs.get(i).y) {
- maxY = pointFs.get(i).y;
- }
- }
- // 计算纵轴最近的能被count整除的值
- int remainderY = ((int) maxY) % divisor;
- maxY = remainderY == 0 ? ((int) maxY) : divisor - remainderY + ((int) maxY);
- // 生成横轴刻度值
- rulerX = new float[count];
- for (int i = 0; i < count; i++) {
- rulerX[i] = maxX / divisor * i;
- }
- // 生成纵轴刻度值
- rulerY = new float[count];
- for (int i = 0; i < count; i++) {
- rulerY[i] = maxY / divisor * i;
- }
- // 计算横纵坐标刻度间隔
- spaceY = viewSize * (BOTTOM - TOP) / count;
- spaceX = viewSize * (RIGHT - LEFT) / count;
- // 锁定画布并设置画布透明度为75%
- int sc = canvas.saveLayerAlpha(0, 0, canvas.getWidth(), canvas.getHeight(), 75, Canvas.ALL_SAVE_FLAG);
- // 绘制横纵线段
- for (float y = viewSize * BOTTOM - spaceY; y > viewSize * TOP; y -= spaceY) {
- for (float x = viewSize * LEFT; x < viewSize * RIGHT; x += spaceX) {
- /*
- * 绘制纵向线段
- */
- if (y == viewSize * TOP + spaceY) {
- canvas.drawLine(x, y, x, y + spaceY * (count - 1), linePaint);
- }
- /*
- * 绘制横向线段
- */
- if (x == viewSize * RIGHT - spaceX) {
- canvas.drawLine(x, y, x - spaceX * (count - 1), y, linePaint);
- }
- }
- }
- // 还原画布
- canvas.restoreToCount(sc);
- // 绘制横纵轴向刻度值
- int index_x = 0, index_y = 1;
- for (float y = viewSize * BOTTOM - spaceY; y > viewSize * TOP; y -= spaceY) {
- for (float x = viewSize * LEFT; x < viewSize * RIGHT; x += spaceX) {
- /*
- * 绘制横轴刻度数值
- */
- if (y == viewSize * BOTTOM - spaceY) {
- canvas.drawText(String.valueOf(rulerX[index_x]), x, y + textSignSzie + spaceY, mTextPaint);
- }
- /*
- * 绘制纵轴刻度数值
- */
- if (x == viewSize * LEFT) {
- canvas.drawText(String.valueOf(rulerY[index_y]), x - thickLineWidth, y + textRulerSize, mTextPaint);
- }
- index_x++;
- }
- index_y++;
- }
- }
- /**
- * 绘制标识元素
- *
- * @param canvas
- * 画布
- */
- private void drawSign(Canvas canvas) {
- // 锁定画布
- canvas.save();
- // 设置文本画笔文字尺寸
- mTextPaint.setTextSize(textSignSzie);
- // 绘制纵轴标识文字
- mTextPaint.setTextAlign(Paint.Align.LEFT);
- canvas.drawText(null == signY ? "y" : signY, textY_X, textY_Y, mTextPaint);
- // 绘制横轴标识文字
- mTextPaint.setTextAlign(Paint.Align.RIGHT);
- canvas.drawText(null == signX ? "x" : signX, textX_X, textX_Y, mTextPaint);
- // 释放画布
- canvas.restore();
- }
- /**
- * 设置数据
- *
- * @param pointFs
- * 点集合
- */
- public synchronized void setData(List<PointF> pointFs, String signX, String signY) {
- /*
- * 数据为空直接GG
- */
- if (null == pointFs || pointFs.size() == 0)
- throw new IllegalArgumentException("No data to display !");
- /*
- * 控制数据长度不超过10个
- * 对于折线图来说数据太多就没必要用折线图表示了而是使用散点图
- */
- if (pointFs.size() > 10)
- throw new IllegalArgumentException("The data is too long to display !");
- // 设置数据并重绘视图
- this.pointFs = pointFs;
- this.signX = signX;
- this.signY = signY;
- invalidate();
- }
- }
代码纯天然,我连封装都没有做什么 = = So、你可以从代码中直接看到哥的思路~~如果没有设置数据,我这里给了一个初始化的随机数据,话说……随机生成的数据画出来的曲线挺带感的:
如果你想得到我们设计图的那种曲线,就需要自己去做特定的数据,实际应用中曲线的数据也肯定是特性的,比如天气-时间曲线图之类,这里的数据我们就直接在MainActivity中做:
- public class MainActivity extends Activity {
- private PolylineView mPolylineView;
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- mPolylineView = (PolylineView) findViewById(R.id.main_pv);
- List<PointF> pointFs = new ArrayList<PointF>();
- pointFs.add(new PointF(0.3F, 0.5F));
- pointFs.add(new PointF(1F, 2.7F));
- pointFs.add(new PointF(2F, 3.5F));
- pointFs.add(new PointF(3F, 3.2F));
- pointFs.add(new PointF(4F, 1.8F));
- pointFs.add(new PointF(5F, 1.5F));
- pointFs.add(new PointF(6F, 2.2F));
- pointFs.add(new PointF(7F, 5.5F));
- pointFs.add(new PointF(8F, 7F));
- pointFs.add(new PointF(8.6F, 5.7F));
- mPolylineView.setData(pointFs, "Money", "Time");
- }
- }
xml里面的代码就不给了,运行效果如下:
大家发现得出的曲线很生硬,在1/4中我们曾讲过PathEffect,可以应用到这里,如果大家还不知道PathEffect……可以去看我前面的文章。
自定义控件很重要的一个地方就是屏幕的适配,我们以控件的边长作为基准参考可以避免很多的大小不一问题,上面的图我都是在mx3上截取的,mx3分辨率高达1800*1080,我们可以换个手机测试下,以下是模拟器240*300分辨率上的样子:
可以看到虽然刻度有点看不清了,但是整个控件的比例大小保持得很好。但是,如我所说,控件都是不完美的,如果能有完美的控件那就不需要我们自定义了,这个折线图控件也一样,首先它只能满足特定的数据,而且风格就是这样,如果我们把数据增多,比如20条数据:
可以看到轴上的刻度已经很紧凑了……这时我们可以考虑控制刻度的位数或使用科学记数法等等,但是……最有效的办法还是控制数据长度……………………哟西!
简单地介绍了Path之后回到我们的Canvas中,关于裁剪的方法
- clipPath(Path path)
是不是变得透彻起来呢?
我们可以利用该方法从Canvas中“挖”取一块不规则的画布:
- public class CanvasView extends View {
- private Path mPath;
- public CanvasView(Context context, AttributeSet attrs) {
- super(context, attrs);
- mPath = new Path();
- mPath.moveTo(50, 50);
- mPath.lineTo(75, 23);
- mPath.lineTo(150, 100);
- mPath.lineTo(80, 110);
- mPath.close();
- }
- @Override
- protected void onDraw(Canvas canvas) {
- canvas.drawColor(Color.BLUE);
- canvas.clipPath(mPath);
- canvas.drawColor(Color.RED);
- }
- }
回顾Canvas中有关裁剪的方法,你会发现有一大堆带有Region.Op参数的重载方法:
- clipPath(Path path, Region.Op op)
- clipRect(Rect rect, Region.Op op)
- clipRect(RectF rect, Region.Op op)
- clipRect(float left, float top, float right, float bottom, Region.Op op)
- clipRegion(Region region, Region.Op op)
要明白这些方法的Region.Op参数那么首先要了解Region为何物。Region的意思是“区域”,在Android里呢它同样表示的是一块封闭的区域,Region中的方法都非常的简单,我们重点来瞧瞧Region.Op,Op是Region的一个枚举类,里面呢有六个枚举常量:
那么Region.Op究竟有什么用呢?其实它就是个组合模式,在1/6中我们曾学过一个叫图形混合模式的,而在本节开头我们也曾讲过Rect也有类似的组合方法,Region.Op灰常简单,如果你看过1/6的图形混合模式的话。这里我就给出一段测试代码,大家可以尝试去改变不同的组合模式看看效果
- public class CanvasView extends View {
- private Region mRegionA, mRegionB;// 区域A和区域B对象
- private Paint mPaint;// 绘制边框的Paint
- public CanvasView(Context context, AttributeSet attrs) {
- super(context, attrs);
- // 实例化画笔并设置属性
- mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
- mPaint.setStyle(Paint.Style.STROKE);
- mPaint.setColor(Color.WHITE);
- mPaint.setStrokeWidth(2);
- // 实例化区域A和区域B
- mRegionA = new Region(100, 100, 300, 300);
- mRegionB = new Region(200, 200, 400, 400);
- }
- @Override
- protected void onDraw(Canvas canvas) {
- // 填充颜色
- canvas.drawColor(Color.BLUE);
- canvas.save();
- // 裁剪区域A
- canvas.clipRegion(mRegionA);
- // 再通过组合方式裁剪区域B
- canvas.clipRegion(mRegionB, Region.Op.DIFFERENCE);
- // 填充颜色
- canvas.drawColor(Color.RED);
- canvas.restore();
- // 绘制框框帮助我们观察
- canvas.drawRect(100, 100, 300, 300, mPaint);
- canvas.drawRect(200, 200, 400, 400, mPaint);
- }
- }
以下是各种组合模式的效果
DIFFERENCE
最终区域为第一个区域与第二个区域不同的区域。
INTERSECT
最终区域为第一个区域与第二个区域相交的区域。
REPLACE
最终区域为第二个区域。
REVERSE_DIFFERENCE
最终区域为第二个区域与第一个区域不同的区域。
UNION
最终区域为第一个区域加第二个区域。
XOR
最终区域为第一个区域加第二个区域并减去两者相交的区域。
Region.Op就是这样,它和我们之前讲到的图形混合模式几乎一模一样换汤不换药……我在做示例的时候仅仅是使用了一个Region,实际上Rect、Cricle、Ovel等封闭的曲线都可以使用Region.Op,介于篇幅,而且也不难以理解就不多说了。
有些童鞋会问那么Region和Rect有什么区别呢?首先最重要的一点,Region表示的是一个区域,而Rect表示的是一个矩形,这是最根本的区别之一,其次,Region有个很特别的地方是它不受Canvas的变换影响,Canvas的local不会直接影响到Region自身,什么意思呢?我们来看一个simple你就会明白:
- public class CanvasView extends View {
- private Region mRegion;// 区域对象
- private Rect mRect;// 矩形对象
- private Paint mPaint;// 绘制边框的Paint
- public CanvasView(Context context, AttributeSet attrs) {
- super(context, attrs);
- // 实例化画笔并设置属性
- mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
- mPaint.setStyle(Paint.Style.STROKE);
- mPaint.setColor(Color.DKGRAY);
- mPaint.setStrokeWidth(2);
- // 实例化矩形对象
- mRect = new Rect(0, 0, 200, 200);
- // 实例化区域对象
- mRegion = new Region(200, 200, 400, 400);
- }
- @Override
- protected void onDraw(Canvas canvas) {
- canvas.save();
- // 裁剪矩形
- canvas.clipRect(mRect);
- canvas.drawColor(Color.RED);
- canvas.restore();
- canvas.save();
- // 裁剪区域
- canvas.clipRegion(mRegion);
- canvas.drawColor(Color.RED);
- canvas.restore();
- // 为画布绘制一个边框便于观察
- canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), mPaint);
- }
- }
大家看到,我在[0, 0, 200, 200]和[200, 200, 400, 400]的位置分别绘制了Rect和Region,它们两个所占大小是一样的:
画布因为和屏幕一样大,so~~我们看不出描边的效果,这时,我们将Canvas缩放至75%大小,看看会发生什么:
- @Override
- protected void onDraw(Canvas canvas) {
- // 缩放画布
- canvas.scale(0.75F, 0.75F);
- canvas.save();
- // 裁剪矩形
- canvas.clipRect(mRect);
- canvas.drawColor(Color.RED);
- canvas.restore();
- canvas.save();
- // 裁剪区域
- canvas.clipRegion(mRegion);
- canvas.drawColor(Color.RED);
- canvas.restore();
- // 为画布绘制一个边框便于观察
- canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), mPaint);
- }
这时我们会看到,Rect随着Canvas的缩放一起缩放了,但是Region依旧泰山不动地淡定:
呼呼呼……关于Canvas的一部分内容就先介绍到此,Canvas的内容比我想象的还要多啊啊啊啊啊啊啊!!!!!主要是Canvas涉及不少的擦边球类一写根本停不下来,妈蛋!!!!
下一节争取Over掉Canvas的内容,7/12进入测量的学习,好吧,就这样吧,我也是醉了……%¥#%¥#%
源码下载:传送门