曲线
开发中并不常用,但是学会曲线绘制能让你的软件更具创造性
和无穷的魅力
。其他API是绘制基础,我认为曲线是绘画的灵魂。有了它直接起飞。这节课我们学习曲线以及曲线的应用等。
曲线常见的API
1.一阶曲线
2.二阶曲线
3.三阶曲线
我们在初中高中学习中学习了各种直线
,圆
,椭圆
,正玄
…曲线
等对应的坐标系方程吧,接下来我们回顾一下我们的直线和曲线等方程。
package com.example.android_draw.view
import android.content.Context
import android.graphics.Canvas
import android.util.AttributeSet
import android.view.View
/**
*
* ┌─────────────────────────────────────────────────────────────┐
* │┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐│
* ││Esc│!1 │@2 │#3 │$4 │%5 │^6 │&7 │*8 │(9 │)0 │_- │+= │|\ │`~ ││
* │├───┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴───┤│
* ││ Tab │ Q │ W │ E │ R │ T │ Y │ U │ I │ O │ P │{[ │}] │ BS ││
* │├─────┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴─────┤│
* ││ Ctrl │ A │ S │ D │ F │ G │ H │ J │ K │ L │: ;│" '│ Enter ││
* │├──────┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴────┬───┤│
* ││ Shift │ Z │ X │ C │ V │ B │ N │ M │< ,│> .│? /│Shift │Fn ││
* │└─────┬──┴┬──┴──┬┴───┴───┴───┴───┴───┴──┬┴───┴┬──┴┬─────┴───┘│
* │ │Fn │ Alt │ Space │ Alt │Win│ HHKB │
* │ └───┴─────┴───────────────────────┴─────┴───┘ │
* └─────────────────────────────────────────────────────────────┘
* 版权:渤海新能 版权所有
*
* @author feiWang
* 版本:1.5
* 创建日期:2/8/21
* 描述:Android_Draw
* E-mail : [email protected]
* CSDN:https://blog.csdn.net/m0_37667770/article
* GitHub:https://github.com/luhenchang
*/
class LHC_Cubic_View @JvmOverloads constructor(
context: Context?,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
init {
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
}
}
为了方便观察和绘制进行了网格和坐标轴的绘制。我相信学过上一篇文章的对于画布的变换操作已经熟练掌握了,网格坐标轴的代码我就不再讲解,看图。
记得我们初中学过Y(x)=ax+b
的直线方程吧。我们来看看这个方程映射到坐标系中的图像。
首先定义一个函数 y=2x-80
获取一段点集合,为了看效果我们x偶数时候绘制,然后绘制点即可。代码如下:
private var number=0..420
//直线方程y=2x-80
private fun drawzxLine(canvas: Canvas) {
pointList= ArrayList()
//绘制方程式y=10x+20
val gPaint = getPaint()
number.forEach {
t ->
val point=PointF()
if (t%2==0) {
//x轴偶数点进行绘制
point.x = t.toFloat()
point.y = 2f * t - 80
pointList.add(point)
canvas.drawPoint(point.x, point.y, gPaint)
}
}
}
1.同样圆的方程,椭圆的方程等都可以这样进行映射到坐标系。
2. 所表示的曲线是以O(a,b)为圆心,以r为半径的圆。
3.例如:(x-10)2+(y-10)2=1602我们进行映射到坐标系内。
(x-10)2+(y-10)2=1602
1.移项 (y-10)2=1602-(x-10)2
2.开方 转换为函数Math方程式如下:
开方之后有正负值需要注意
- y=sqrt(160.0.pow(2.0).toFloat() - ((point.x - 10).toDouble()).pow(2.0)).toFloat() + 10
- y=-sqrt(160.0.pow(2.0).toFloat() - ((pointDown.x - 10).toDouble()).pow(2.0)).toFloat() + 10
//绘制圆圈
number.forEach {
t ->
val point = PointF()
val pointDown = PointF()
//(x-10)2+(y-10)2=1602
point.x = t.toFloat()
pointDown.x = t.toFloat()
//y计算应该不用我说吧。
point.y =
sqrt(160.0.pow(2.0).toFloat() - ((point.x - 10).toDouble()).pow(2.0)).toFloat() + 10
pointDown.y = -sqrt(
160.0.pow(2.0).toFloat() - ((pointDown.x - 10).toDouble()).pow(2.0)
).toFloat() + 10
canvas.drawPoint(point.x, point.y, gPaint)
canvas.drawPoint(pointDown.x, pointDown.y, gPaint)
}
通过上面我们发现凡是函数都可以和坐标系绘制进行一一映射,当然了贝塞尔曲线
也是有方程式的。有如下:
线性贝塞尔曲线
二次方贝塞尔曲线
三次方贝塞尔曲线
P0、P1、P2、P3四个点在平面或在三维空间中定义了三次方贝塞尔曲线。曲线起始于P0走向P1,并从P2的方向来到P3。一般不会经过P1或P2;公式如下:
当然在Android端的Native层已经封装好了方法,二次方贝塞尔曲线
和三次方贝塞尔曲线
,已知函数当然可以进行封装。
在Android端提供了二阶和三阶
`二次方贝塞尔曲线`:
public void quadTo(float x1, float y1, float x2, float y2)
public void rQuadTo(float dx1, float dy1, float dx2, float dy2)
`三次方贝塞尔曲线`:
public void cubicTo(float x1, float y1, float x2, float y2,float x3, float y3)
public void rCubicTo(float x1, float y1, float x2, float y2,float x3, float y3)
接下来我们绘制一个二阶曲线,控制点可以随着手势的移动和下按进行对应的屏幕移动,对于手势坐标系和屏幕坐标系的映射转换
上节折线里面说很明白了,这里不多做解释。
//记录移动的canvas画布坐标,不是手势坐标,由手势坐标转换为canvas坐标进行刷新
private var moveX: Float = 160f
private var moveY: Float = 160f
private fun drawQuz(canvas: Canvas) {
controllRect = Rect(
(moveX - 30f).toInt(),
(moveY + 30f).toInt(),
(moveX + 30).toInt(),
(moveY - 30f).toInt()
)
val quePath = Path()
canvas.drawCircle(0f, 0f, 10f, getPaintCir(Paint.Style.FILL))
canvas.drawCircle(320f, 0f, 10f, getPaintCir(Paint.Style.FILL))
//第一个点和控制点的连线到最后一个点链线。为了方便观察
val lineLeft = Path()
lineLeft.moveTo(0f, 0f)
lineLeft.lineTo(moveX, moveY)
lineLeft.lineTo(320f, 0f)
canvas.drawPath(lineLeft, getPaint(Paint.Style.STROKE))
//第一个p0处画一个圆。第二个p1处画一个控制点圆,最后画一个。
canvas.drawCircle(moveX, moveY, 10f, getPaintCir(Paint.Style.FILL))
quePath.quadTo(moveX, moveY, 320f, 0f)
canvas.drawPath(quePath, getPaint(Paint.Style.STROKE))
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
ACTION_DOWN,
ACTION_MOVE -> {
//在控制点附近范围内部,进行移动
Log.e("x=", "onTouchEvent: (x,y)"+(event.x - width / 2).toInt()+":"+(-(event.y - height / 2)).toInt())
//将手势坐标转换为屏幕坐标
moveX = event.x - width / 2
moveY = -(event.y - height / 2)
invalidate()
}
}
return true
}
上图可以拖动控制点,在起点和结尾之间的曲线随着控制点发生了变形。控制点靠近那一侧弧度的凸起就偏向那一侧
,初步的认识这一个规律即可,而练习中不断的去调节控制点达到我们的需求。但是在上图中我们回发现弧度不够圆圈,在三阶函数里面可以很好的调节弧度。接下来我们来看看三阶函数
三阶曲线
任意
我们想要拖动的控制点
进行观察我们的三阶曲线。在上章节折线中对于手势配合Rect的contains方法可以进行局部的点击
,当然了拖动也是没问题的。距形包裹住控制点
,手势滑动时时刷新控制点
和对应的距形
即可。 private fun drawCubic(canvas: Canvas) {
val cubicPath=Path()
cubicPath.moveTo(0f,0f)
cubicLeftRect= Rect(
(moveCubiX - 30f).toInt(),
(moveCubiY - 30f).toInt(),
(moveCubiX + 30).toInt(),
(moveCubiY + 30f).toInt()
)
cubicRightRect=Rect(
(moveCubiXX - 30f).toInt(),
(moveCubiYY - 30f).toInt(),
(moveCubiXX + 30).toInt(),
(moveCubiYY + 30f).toInt()
)
val lineLeft = Path()
lineLeft.moveTo(0f, 0f)
lineLeft.lineTo(moveCubiX, moveCubiY)
lineLeft.lineTo(moveCubiXX, moveCubiYY)
lineLeft.lineTo(320f, 0f)
canvas.drawPath(lineLeft, getPaint(Paint.Style.STROKE,Color.GRAY))
//canvas.drawRect(cubicLeftRect, getPaint(Paint.Style.FILL,Color.RED))
//canvas.drawRect(cubicRightRect, getPaint(Paint.Style.FILL,Color.RED))
canvas.drawCircle(moveCubiX, moveCubiY, 10f, getPaintCir(Paint.Style.FILL))
canvas.drawCircle(moveCubiXX, moveCubiYY, 10f, getPaintCir(Paint.Style.FILL))
cubicPath.cubicTo(moveCubiX,moveCubiY,moveCubiXX,moveCubiYY,320f,0f)
canvas.drawPath(cubicPath, getPaint(Paint.Style.STROKE,Color.RED))
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
ACTION_DOWN,
ACTION_MOVE -> {
//在控制点附近范围内部,进行移动
Log.e(
"x=",
"onTouchEvent: (x,y)" + (event.x - width / 2).toInt() + ":" + (-(event.y - height / 2)).toInt()
)
//二阶曲线
if (controllRect.contains((event.x - width / 2).toInt(),(-(event.y - height / 2)).toInt())) {
Log.e("点击来","对" )
moveX = event.x - width / 2
moveY = -(event.y - height / 2)
invalidate()
//三阶曲线控制点1
}else if(cubicLeftRect.contains((event.x - width / 2).toInt(),(-(event.y - height / 2)).toInt())){
moveCubiX= event.x - width / 2
moveCubiY= -(event.y - height / 2)
invalidate()
//三阶曲线控制点2
}else if(cubicRightRect.contains((event.x - width / 2).toInt(),(-(event.y - height / 2)).toInt())){
moveCubiXX= event.x - width / 2
moveCubiYY= -(event.y - height / 2)
invalidate()
}
}
}
return true
}
到这里我想我们应该大概的明白二阶和三阶曲线对于弧度的大致方向控制
了吧。你以为这样就结束了么。接下来下来开始正式的进入曲线应用。
在qq群里么有人问过这样的效果如何去做。我想到这里对于大家来说应该很简单了吧。我们接下来就利用上面学习的曲线内容进行编写。
首先准备
1.一张图片
2.绘制矩形框
3.绘制直线和曲线
4.拖动曲线
5.改变色相饱和度亮度等
图片在Android中并没有提供直接操作Bitmap来改变色相饱和度的API,所以我们需要将bitmap绘制到画布上。接下来我们开始写代码。首先新建类LHC_Image_View
,attrs里面添加属性。
属性
<declare-styleable name="LHC_Image_View">
<attr name="defaultImag" format="reference" />
declare-styleable>
获取属性图片,进行绘制到canvas上面,在这里我们需要知道一点关于颜色的API。ColorMatrix
图像是由无数的像素点组成,每一个像素点用RGBA四个值来描述,具体是由一个4*5的矩阵来控制
每个像素的显示,点的RGBA又共同组成了位图的外观表现形式,所以改变控制像素点的RGBA值,就可以改变位图展示的效果
`ColorMatrix`
用4x5矩阵来表示,用于转换位图的颜色和alpha分量。
[ a, b, c, d, e,
f, g, h, i, j,
k, l, m, n, o,
p, q, r, s, t ]
计算如下:
R = a*R + b*G + c*B + d*A + e;
G = f*R + g*G + h*B + i*A + j;
B = k*R + l*G + m*B + n*A + o;
A = p*R + q*G + r*B + s*A + t;
通常见的默认标准矩阵
[ 1 0 0 0 0 R=225 R1=225 + 0 + 0 + 0+偏移量=225
0 1 0 0 0 x G=225 = G1=225 + 0 + 0 + 0+偏移量=225 = RGBA=[225,225,225,225]
0 0 1 0 0 B=225 B1=225 + 0 + 0 + 0+偏移量=225
0 0 0 1 0 ] A=225 A1=225 + 0 + 0 + 0+偏移量=225
我想降低绿色和蓝色,那么我们可以将矩阵中的2行2列1变为0.5,3行3列的1变为0.5等操作。
ColorMatrix构造方法:我们使用ColorMatrix()
public ColorMatrix() {
reset();
}
/**
* Create a new colormatrix initialized with the specified array of values.
*/
public ColorMatrix(float[] src) {
System.arraycopy(src, 0, mArray, 0, 20);
}
/**
* Create a new colormatrix initialized with the specified colormatrix.
*/
public ColorMatrix(ColorMatrix src) {
System.arraycopy(src.mArray, 0, mArray, 0, 20);
}
在ColorMatrix中常见的使用方法:setRotate(int axis, float degrees)中用到了degrees * Math.PI / 180d
这里为了控制矩阵数组开发者便于编写使用来正余玄函数,将-1到1放大到了0-360度,当然这是我的猜想,毕竟-1到1之间的小数不好把握。为了便于大家使用不妨附上一副正玄图像:
由图我们可以看到0度和90度分别是0和1,那接下来我们进行绘制一个图片设置为int=0代表红色…degrees=90度,源码中修改了矩阵的为
[ 1 0 0 0 0 R
0 1 0 0 0 x G
0 0 1 0 0 B =R G B A
0 0 0 1 0 ] A
[ 1 0 0 0 0 R
0 0 1 0 0 x G =R B -G A
0 -1 0 0 0 B
0 0 0 1 0 ] A
结果红没变,绿变为蓝,蓝色变为负,我们知道红和蓝成黄色。那么我们接下来验证一下我们的结果。
public void setRotate(int axis, float degrees) {
reset();
double radians = degrees * Math.PI / 180d;
float cosine = (float) Math.cos(radians);
float sine = (float) Math.sin(radians);
switch (axis) {
// Rotation around the red color
case 0:
mArray[6] = mArray[12] = cosine;
mArray[7] = sine;
mArray[11] = -sine;
break;
// Rotation around the green color
case 1:
mArray[0] = mArray[12] = cosine;
mArray[2] = -sine;
mArray[10] = sine;
break;
// Rotation around the blue color
case 2:
mArray[0] = mArray[6] = cosine;
mArray[1] = sine;
mArray[5] = -sine;
break;
default:
throw new RuntimeException();
}
}
验证结果:
class LHC_Image_View @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val MIDDLE_VALUE=127
private var mdrawable: Drawable?
lateinit var bitmap:Bitmap
init {
val array: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.LHC_Image_View)
mdrawable = array.getDrawable(R.styleable.LHC_Image_View_defaultImag)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (mdrawable!=null){
bitmap =mdrawable!!.toBitmap(width,height, Bitmap.Config.ARGB_8888)
}else{
return
}
//实例化一支画笔
val mPaint = Paint()
mPaint.strokeWidth=10f
//实例化处理色相的颜色矩阵