1.前言
逛街的時候井佑,看到一篇Android Canvas 方法總結(jié),這篇文章將Canvas一些基本操作介紹的很詳細眠寿。從零開始的朋友可以先去刷點經(jīng)驗躬翁,剩下的同學(xué)拿起手術(shù)刀,我們一起來將Canvas血腥解剖吧盯拱。
2.Canvas簡介
官方文檔介紹如下
The Canvas class holds the "draw" calls. To draw something, you need 4 basic components:
a Bitmap to hold the pixels,
a Canvas to host the draw calls (writing into the bitmap),
a drawing primitive (e.g. Rect, Path, text, Bitmap),
a paint (to describe the colors and styles for the drawing).
用人話說大概是這樣
一個Canvas類對象有四大基本要素
1盒发、用來保存像素的Bitmap
2、用來在Bitmap上進行繪制操作的Canvas
3狡逢、要繪制的東西
4迹辐、繪制用的畫筆Paint
Bitmap和Canvas的關(guān)系類似于畫板與畫布,不理解沒有關(guān)系甚侣,后面還會詳細介紹的明吩。
3.Canvas基本繪制
一開始,先來點簡單的基礎(chǔ)題殷费。因為太懶了不想從頭寫起印荔,我就假設(shè)大家都看過了Android Canvas 方法總結(jié)低葫,來寫一些這里面沒有介紹的方法。
3.1 圓角矩形
畫筆初始化
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(10);
mPaint.setColor(Color.RED);
在onDraw()
中進行繪制仍律,參數(shù)二嘿悬、三越大,圓角半徑越大
private void drawRoundRect(Canvas canvas) {
RectF r = new RectF(100, 100, 400, 500);
//x-radius ,y-radius圓角的半徑
canvas.drawRoundRect(r, 80, 80, mPaint);
}
效果圖如下
3.2 圓角矩形路徑
那么有的時候水泉,我不需要那么整齊的圓角善涨,該怎么辦呢
private void drawRoundRectPath(Canvas canvas) {
RectF r = new RectF(100, 100, 400, 500);
Path path = new Path();
float radii[] = {80, 80, 80, 80, 80, 80, 20, 20};
path.addRoundRect(r, radii, Path.Direction.CCW);
canvas.drawPath(path, mPaint);
}
這里的radii就定義了四個角的弧度,接著使用Path就可以將其繪制出來草则。事實上钢拧,Path是無所不能的,我們將在以后單獨介紹他炕横。
3.3 Region區(qū)域
Region是區(qū)域的意思源内,它表示的Canvas圖層上的一塊封閉的區(qū)域。
來看看它的構(gòu)造方法
/** Create an empty region
*/
public Region() {
this(nativeConstructor());
}
/** Return a copy of the specified region
*/
public Region(Region region) {
this(nativeConstructor());
nativeSetRegion(mNativeRegion, region.mNativeRegion);
}
/** Return a region set to the specified rectangle
*/
public Region(Rect r) {
mNativeRegion = nativeConstructor();
nativeSetRect(mNativeRegion, r.left, r.top, r.right, r.bottom);
}
/** Return a region set to the specified rectangle
*/
public Region(int left, int top, int right, int bottom) {
mNativeRegion = nativeConstructor();
nativeSetRect(mNativeRegion, left, top, right, bottom);
}
看上去挺萬金油的份殿,什么都能傳進去膜钓,那么Region能干嘛呢?我們可以用它來進行一些交并補集的操作,比如下面代碼就能展示兩個region的交集
region1.op(region2, Region.Op.INTERSECT);//交集部分 region1是調(diào)用者A卿嘲,region2是求交集的B
去源碼里看看這個Op颂斜,發(fā)現(xiàn)是個枚舉類
public enum Op {
DIFFERENCE(0),
INTERSECT(1),
UNION(2),
XOR(3),
REVERSE_DIFFERENCE(4),
REPLACE(5);
...
}
可見交并補的類型還挺多,我們用一張圖來介紹吧
那么這里就出現(xiàn)了一個問題拾枣,這些Op過后的region都是不規(guī)則的了沃疮,系統(tǒng)要如何將他們繪制出來呢?
嘿嘿嘿放前,請回憶起當(dāng)年被微積分支配的恐懼吧忿磅!
private void drawRegion(Canvas canvas){
RectF r = new RectF(100, 100, 400, 500);
Path path = new Path();
float radii[] = {80, 80, 80, 80, 80, 80, 20, 20};
path.addRoundRect(r, radii, Path.Direction.CCW);
//創(chuàng)建一塊矩形的區(qū)域
Region region = new Region(100, 100, 600, 800);
Region region1 = new Region();
region1.setPath(path, region);//path的橢圓區(qū)域和矩形區(qū)域進行交集
//結(jié)合區(qū)域迭代器使用(得到圖形里面的所有的矩形區(qū)域)
RegionIterator iterator = new RegionIterator(region1);
Rect rect = new Rect();
mPaint.setStrokeWidth(1);
while (iterator.next(rect)) {
canvas.drawRect(rect, mPaint);
}
}
看看代碼,這里通過迭代器用一個個小矩形填滿整個region控件凭语,為了方便展示葱她,我們把paint的類型設(shè)成stroke,效果圖如下
解釋下似扔,這里首先繪制了最大的那個矩形吨些,然后在極限的距離上縮小矩形,再通過這些小矩形將剩下的部分都填充起來炒辉。
4.Canvas基本變換
4.1 Canavas坐標系
Canvas里面牽扯兩種坐標系:Canvas自己的坐標系豪墅、繪圖坐標系
Canvas的坐標系,它就在View的左上角黔寇,從坐標原點往右是X軸正半軸偶器,往下是Y軸的正半軸,有且只有一個,唯一不變
繪圖坐標系屏轰,它不是唯一不變的颊郎,它與Canvas的Matrix有關(guān)系,當(dāng)Matrix發(fā)生改變的時候霎苗,繪圖坐標系對應(yīng)的進行改變姆吭,同時這個過程是不可逆的(通過相反的矩陣還原),而Matrix又是通過我們設(shè)置translate唁盏、rotate内狸、scale、skew來進行改變的
上面這段話還是挺好理解的厘擂,我們用代碼來驗證下
private void drawMatrix(Canvas canvas){
// 繪制坐標系
RectF r = new RectF(0, 0, 400, 500);
mPaint.setColor(Color.GREEN);
canvas.drawRect(r, mPaint);
// 第一次繪制坐標軸
canvas.drawLine(0,0,canvas.getWidth(),0,mPaint);// X 軸
mPaint.setColor(Color.BLUE);
canvas.drawLine(0,0,0,canvas.getHeight(),mPaint);// Y 軸
//平移--即改變坐標原點
canvas.translate(50, 50);
// 第二次繪制坐標軸
mPaint.setColor(Color.GREEN);
canvas.drawLine(0,0,canvas.getWidth(),0,mPaint);// X 軸
mPaint.setColor(Color.BLUE);
canvas.drawLine(0,0,0,canvas.getHeight(),mPaint);// Y 軸
canvas.rotate(45);
// 第三次繪制坐標軸
mPaint.setColor(Color.GREEN);
canvas.drawLine(0,0,canvas.getWidth(),0,mPaint);// X 軸
mPaint.setColor(Color.BLUE);
canvas.drawLine(0,0,0,canvas.getHeight(),mPaint);// Y 軸
}
運行結(jié)果如下
剩下的translate昆淡、rotate、scale驴党、skew等方法瘪撇,在之前推薦的那篇文章里就有获茬,這里就不再重復(fù)勞動了
5.Canvas狀態(tài)保存
5.1 狀態(tài)棧
狀態(tài)棧通過save港庄、 restore方法來保存和還原變換操作Matrix以及Clip剪裁,也可以通過restoretoCount直接還原到對應(yīng)棧的保存狀態(tài)恕曲。需要注意的是鹏氧,一開始canvas就是在棧1的位置,執(zhí)行一次save就進棧一次(此時為2)佩谣,執(zhí)行一次restore就出棧一次把还。
private void saveRestore(Canvas canvas){
RectF r = new RectF(0, 0, 400, 500);
mPaint.setColor(Color.GREEN);
canvas.drawRect(r, mPaint);
canvas.save();
//平移
canvas.translate(50, 50);
mPaint.setColor(Color.BLUE);
canvas.drawRect(r, mPaint);
canvas.restore();
mPaint.setColor(Color.YELLOW);
r = new RectF(0, 0, 200, 200);
canvas.drawRect(r, mPaint);
}
效果圖如下
有的同學(xué)可能會把狀態(tài)棧理解為圖層,其實這是不對滴茸俭。我們來看下save方法的源碼
/**
* Saves the current matrix and clip onto a private stack.
* <p>
* Subsequent calls to translate,scale,rotate,skew,concat or clipRect,
* clipPath will all operate as usual, but when the balancing call to
* restore() is made, those calls will be forgotten, and the settings that
* existed before the save() will be reinstated.
*
* @return The value to pass to restoreToCount() to balance this save()
*/
public int save() {
return native_save(mNativeCanvasWrapper, MATRIX_SAVE_FLAG | CLIP_SAVE_FLAG);
}
注釋上說的很明白吊履,save只是保存了matrix和clip的狀態(tài),并沒有保存一個真正的圖層调鬓。真正的圖層是在Layer棧中保存的艇炎。
5.2 Layer棧
Layer棧通過saveLayer新建一個透明的圖層,并且會將saveLayer之前的一些Canvas操作延續(xù)過來腾窝,后續(xù)的繪圖操作都在新建的layer上面進行缀踪,當(dāng)我們調(diào)用restore或者 restoreToCount 時更新到對應(yīng)的圖層和畫布上。
下面這段代碼要仔細看
private void saveLayer(Canvas canvas) {
RectF rectF = new RectF(0, 0, 400, 500);
Paint paint = new Paint();
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(10);
paint.setColor(Color.GREEN);
canvas.drawRect(rectF, paint);
canvas.translate(50, 50);
canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(), null, Canvas.ALL_SAVE_FLAG);
canvas.drawColor(Color.BLUE);// 通過drawColor可以發(fā)現(xiàn)saveLayer是新建了一個圖層虹脯,
paint.setColor(Color.YELLOW);
canvas.drawRect(rectF, paint);
canvas.restore();
RectF rectF1 = new RectF(0, 0, 300, 400);
paint.setColor(Color.RED);
canvas.drawRect(rectF1, paint);
}
先上效果圖
我們慢慢解釋驴娃。一開始畫了個綠色的框,之后移動了(50,50)的距離循集,再通過saveLayer新建了一個圖層唇敞。注意這里用到了Canvas.ALL_SAVE_FLAG,別的FLAG還有只保存Matrix的,只保存Clip的等等疆柔,大家可以自己去看蕉世。接著在新的圖層上畫了藍色背景和黃色的矩形,從結(jié)果可以看出之前的一些Canvas操作會被延續(xù)到新的圖層婆硬。調(diào)用restore后狠轻,兩個圖層合二為一,由于圖層2是藍色背景彬犯,因此就把圖層1的綠色邊框覆蓋了向楼。最后再繪制另一個紅色的框,此時只有一個圖層了谐区,所以就繪制在當(dāng)前圖層的最上方湖蜕。
文章開頭說Bitmap和Canvas的關(guān)系類似于畫板與畫布,就是因為每次Canvas執(zhí)行saveLayer時都會新建一個透明的圖層宋列,與之前的圖層疊加后更新到Bitmap上昭抒,從而將繪制的內(nèi)容展示出來。
這些大概就是save與saveLayer的區(qū)別所在了炼杖,如果覺得自己明白了灭返,就去看看給女朋友化妝系列的代碼,看看是否可以理解其中的save與saveLayer操作坤邪,以及為什么要這樣做熙含。
6.Drawable與Canvas
6.1 Drawable簡介
下面我們將用一個例子來加深學(xué)習(xí)效果,不過開始前還需要將Drawable這位兄弟介紹給大家艇纺。首先上一段官方注釋:
A Drawable is a general abstraction for "something that can be drawn."
顧名思義怎静,Drawable就是可以被畫出來的一個東西,這是一個抽象類黔衡,繼承自它的實現(xiàn)類如下(windows中查詢類繼承關(guān)系的快捷鍵是Ctrl+H)
是不是發(fā)現(xiàn)有些熟悉的字眼蚓聘?比如layer、shape盟劫、color等等夜牡。對啦,就是可以在drawable文件夾中進行定義的xml資源文件捞高,系統(tǒng)會將這些xml轉(zhuǎn)換成都解析成相應(yīng)的drawable對象氯材。
由于drawable是可繪制的對象,canvas是繪制的畫紙硝岗,因此這兩位是密不可分的好基友氢哮。除去上圖中系統(tǒng)實現(xiàn)的drawable外,我們還可以根據(jù)需要自定義drawable型檀。事實上自定義drawable才是日常會用到的東西冗尤,下面一起來看看這種基本操作。
6.1 基本操作
這個自定義控件的功能不太好描述,先展示下效果圖
最外層是一個HorizontalScrollView裂七,里面包裹著許多ImageView皆看,可以拖動,中間選中的區(qū)域呈現(xiàn)灰色背零,其余的顯示彩色腰吟。這些灰色、彩色的圖是兩套資源徙瓶。
要實現(xiàn)上述功能毛雇,我們需要兩個控件,一個是外層控制觸摸事件的ViewGroup侦镇,可以通過繼承HorizontalScrollView來完成灵疮。另一個是用來注入ImageView變換顏色的Drawable,這就需要自定義來實現(xiàn)了壳繁。
6.1.1 RevealDrawable
創(chuàng)建RevealDrawable繼承自Drawable震捣,需要重寫public void draw(@NonNull Canvas canvas)
方法。
對于每一部分的圖片而言闹炉,會有以下幾種繪制情況:
1.灰色
2.彩色
3.左灰右彩
4.左彩右灰
灰色和彩色好辦蒿赢,直接將兩種圖片當(dāng)做參數(shù)傳入RevealDrawable中,在需要時通過draw(canvas)
繪制出來即可剩胁。那么混合色該怎么做诉植?又要如何去判斷左右或者說灰彩各占的比例呢祥国?
對于問題一昵观,我們可以用之前所說的canvas裁剪來完成,需要注意save()
和restore()
的調(diào)用舌稀;至于問題二啊犬,Drawable源碼中有這么一行參數(shù):private int mLevel = 0;
,很顯然壁查,Google早已考慮到Drawable的這種使用場景觉至,而mLevel
就是用來確定比例的撵幽,其值為0~10000办成,可以由我們在外層動態(tài)去設(shè)置惫搏。
總結(jié)一下突雪,整體思路就是HorizontalScrollView根據(jù)Scroll的距離為RevealDrawable動態(tài)設(shè)置level擦秽,而RevealDrawable則根據(jù)被設(shè)置的level展示出不同的圖像效果腌紧。剩下的就是數(shù)學(xué)問題了期丰。
這里就展示其核心的繪制方法欢瞪,注釋都有挂捻,完整的代碼等閑了整理下一起放到大型同性交友平臺上碉纺。
@Override
public void draw(Canvas canvas) {
// 繪制
int level = getLevel();//from 0 (minimum) to 10000
//三個區(qū)間
//右邊區(qū)間和左邊區(qū)間--設(shè)置成灰色
if(level == 10000|| level == 0){
mUnselectedDrawable.draw(canvas);
}
else if(level==5000){//全部選中--設(shè)置成彩色
mSelectedDrawable.draw(canvas);
}else{
//混合效果的Drawable
/**
* 將畫板切割成兩塊-左邊和右邊
*/
final Rect r = mTmpRect;
//得到當(dāng)前自身Drawable的矩形區(qū)域
Rect bounds = getBounds();
{
//1.先繪制灰色部分
//level 0~5000~10000
//比例
float ratio = (level/5000f) - 1f;
int w = bounds.width();
if(mOrientation==HORIZONTAL){
w = (int) (w* Math.abs(ratio));
}
int h = bounds.height();
if(mOrientation==VERTICAL){
h = (int) (h* Math.abs(ratio));
}
int gravity = ratio < 0 ? Gravity.LEFT : Gravity.RIGHT;
//從一個已有的bounds矩形邊界范圍中摳出一個矩形r
Gravity.apply(
gravity,//從左邊還是右邊開始摳
w,//目標矩形的寬
h, //目標矩形的高
bounds, //被摳出來的rect
r);//目標rect
canvas.save();//保存畫布
canvas.clipRect(r);//切割
mUnselectedDrawable.draw(canvas);//畫
canvas.restore();//恢復(fù)之前保存的畫布
}
{
//2.再繪制彩色部分
//level 0~5000~10000
//比例
float ratio = (level/5000f) - 1f;
int w = bounds.width();
if(mOrientation==HORIZONTAL){
w -= (int) (w* Math.abs(ratio));
}
int h = bounds.height();
if(mOrientation==VERTICAL){
h -= (int) (h* Math.abs(ratio));
}
int gravity = ratio < 0 ? Gravity.RIGHT : Gravity.LEFT;
//從一個已有的bounds矩形邊界范圍中摳出一個矩形r
Gravity.apply(
gravity,//從左邊還是右邊開始摳
w,//目標矩形的寬
h, //目標矩形的高
bounds, //被摳出來的rect
r);//目標rect
canvas.save();//保存畫布
canvas.clipRect(r);//切割
mSelectedDrawable.draw(canvas);//畫
canvas.restore();//恢復(fù)之前保存的畫布
}
}
}
6.1.2 GalleryHorizontalScrollView
外層的GalleryHorizontalScrollView繼承自HorizontalScrollView,需要處理滑動事件與level設(shè)置,別忘了ScrollView的子View必須是ViewGroup
private void init() {
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
);
container = new LinearLayout(getContext());
container.setLayoutParams(params);
setOnScrollChangeListener(this);
}
onLayout()
的作用是在控件初始化時設(shè)置Padding值骨田,以便于一開始耿导,將第一個ImageView展示在正中間的位置(為了好看,沒什么特別的用處)
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
View v = container.getChildAt(0);
icon_width = v.getWidth();//單個圖片的寬度
centerX = getWidth() / 2;//整個sv的寬度
centerX = centerX - icon_width/2;
container.setPadding(centerX, 0, centerX, 0);
}
起初觸摸事件是在touch
方法中完成的态贤,后來發(fā)現(xiàn)這個方法精度太高舱呻,滑動抖動明顯,因此換在scroll
方法中執(zhí)行悠汽。
private void reveal() {
// 漸變效果
//得到hzv滑出去的距離
int scrollX = getScrollX();
Log.d(TAG, "reveal: "+scrollX);
//找到兩張漸變的圖片的下標--左狮荔,右
int index_left = scrollX/icon_width;
int index_right = index_left + 1;
//設(shè)置圖片的level
for (int i = 0; i < container.getChildCount(); i++) {
if(i==index_left||i==index_right){
//變化
//比例:
float ratio = 5000f/icon_width;
ImageView iv_left = (ImageView) container.getChildAt(index_left);
//scrollX%icon_width:代表滑出去的距離
//滑出去了icon_width/2 icon_width/2%icon_width
iv_left.setImageLevel(
(int)(5000-scrollX%icon_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%icon_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);
}
7.Canvas緩存
在上面的例子中我們介紹了Canvas與自定義Drawable的配合使用,接下來我們上一個Canvas與Bitmap混合實現(xiàn)緩存的效果介粘。其實這句話是廢話殖氏,因為創(chuàng)建Canvas時就必須傳入Bitmap為參,畢竟Bitmap才是真正保存像素的地方姻采。
緩存的思想就是先將像素保存到CacheCanvas的CacheBitmap中雅采,再將這個CacheBitmap保存到View的Canvas上。
我們在初始化方法中創(chuàng)建緩存對象慨亲。
private void init() {
//創(chuàng)建一個與該VIew相同大小的緩沖區(qū)
cacheBitmap = Bitmap.createBitmap(VIEW_WIDTH, VIEW_HEIGHT, Bitmap.Config.ARGB_8888);
//創(chuàng)建緩沖區(qū)Cache的Canvas對象
cacheCanvas = new Canvas();
path = new Path();
//設(shè)置cacheCanvas將會繪制到內(nèi)存的bitmap上
cacheCanvas.setBitmap(cacheBitmap);
paint = new Paint();
paint.setColor(Color.RED);
paint.setFlags(Paint.DITHER_FLAG);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(5);
paint.setAntiAlias(true);
paint.setDither(true);//防抖動婚瓜,比較清晰
}
接著onTouch()
中根據(jù)手勢進行繪制,注意是繪制到緩存canvas上
@Override
public boolean onTouchEvent(MotionEvent event) {
//獲取拖動時間的發(fā)生位置
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
path.moveTo(x, y);
preX = x;
preY = y;
break;
case MotionEvent.ACTION_MOVE:
path.quadTo(preX, preY, x, y);//繪制圓滑曲線
preX = x;
preY = y;
break;
case MotionEvent.ACTION_UP:
//這是是調(diào)用了cacheBitmap的Canvas在繪制
cacheCanvas.drawPath(path, paint);
path.reset();
break;
}
invalidate();//在UI線程刷新VIew
return true;
}
invalidate()
會回調(diào)draw()
方法刑棵,此時再將緩存bitmap繪制到View的canvas中巴刻。
@Override
protected void onDraw(Canvas canvas) {
Paint p = new Paint();
//將cacheBitmap繪制到該View
canvas.drawBitmap(cacheBitmap, 0, 0, p);
}
這樣一來,手指滑動軌跡就會略有延遲后再繪制到用戶界面上蛉签,類似于寫字板的效果胡陪。
8.總結(jié)
關(guān)于canvas的介紹就到此為止,了解canvas的基本繪制碍舍,知道它的兩個坐標系柠座,學(xué)會和drawable、bitmap混合使用基本就差不多了片橡。希望能給大家?guī)砗眠\妈经。
前段時間秋招找工作實習(xí)什么的斷更好久,從今天開始捧书,立個FLAG在此吹泡,一天一篇!