前言
Android中繪圖離不開的就是Canvas了,Canvas是一個龐大的知識體系,有Java層的,也有jni層深入到Framework。Canvas有許多的知識內容,構建了一個武器庫一般,所謂十八般武藝是也,Paint是Canvas的一個重要的合作伙伴,但今天要講的不是Canvas也不是Paint,而是與Paint相關的知識點Shader.
什么是Shader?
Shader在英語辭典中被解釋為著色器。查閱維基百科,有以下結論:
In the field of computer graphics, a shader is a computer program that is used to do shading: the production of appropriate levels of color within an image, or, in the modern era, also to produce special effects or do video post-processing. A definition in layperson’s terms might be given as “a program that tells a computer how to draw something in a specific and unique way.
在計算機圖形領域,一個Shader是指一段用來著色的計算機程序,通常用來生成一張圖片中適當等級的顏色值,或者是生成特殊的視覺效果,或者是對視頻畫面進行處理。對于非專業人士的角度來看,它可以被描述為–“一種告訴計算機怎么樣通過某種特殊手段繪制一些圖像的程序”。
看起來還是比較抽象難懂,但是我覺得正確理解它的定義是應該的,這能讓我們真正寫出非常高效的代碼。
Android中也有Shader的概念,對照上面的定義,它應該也是將圖形畫面產生某種特殊效果的一類東西。具體是不是這樣的呢?我可以先告訴你答案–是的。
為了提高大家對Shader的興趣,先讓大家看看通過Shader得到的一些效果圖片。
是不是挺有趣啊?如果你對這些感興趣,請跟隨我的節奏,看下面內容。
Android中Shader相關知識點
看API終于不要FQ了,其實我也一直沒有FQ,想看API的時候,直接去www.androidxref.com查看源碼去了。那么現在可以直接上官網中文頁面,查看了。Android中Shader的API地址為Shader
Android中對Shader是這樣解釋的
Shader是一種基類對象,它在圖形繪制過程中返回一段段顏色值,通過調用Paint.setShader()方法,可以將它的子類安裝進畫筆,這樣Paint對象在繪制過程中所獲取的顏色就是來自Shader對象。
上面提到了Shader的子類,Shader有5個子類 BitmapShader, ComposeShader, LinearGradient, RadialGradient, SweepGradient。 本文的目的也是分別講它的各個子類。
圖片渲染器 BitmapShader
BitmapShader將一張圖片當作紋理(在OpenGL中,紋理就是貼圖的意思,可以理解為一個沒有顏色的正文形被貼上了一張圖片,這樣視覺效果就是一張正方形的圖片)來繪制。而這張圖片可以通過設置BitmapShader的tiling mode來達到鏡面和重復的效果。
BitmapShader (Bitmap bitmap, Shader.TileMode tileX, Shader.TileMode tileY)
上面是BitmapShader的構造方法。
bitmap是指紋理圖片
tileX是指在X方向軸的tiling mode
tileY是指在Y方向軸的tiling mode
很多人可能有疑問,這個TileMode是什么?
神秘莫測的TileMode
什么是TileMode呢?
事實上它只是一個枚舉而已。它只有三個值。
Shader.TileMode CLAMP
Shader.TileMode MIRROR
Shader.TileMode REPEAT
CLAMP
它的意思當要繪制的區間大于圖片紋理本身的區間時,多出來的空間位置將被紋理圖片的邊緣顏色填充。文字很難解釋,我用圖片來代替吧。
原圖如下:
原圖的分辨率是562*336
我們編寫一個自定義View–CustomView。然后在它的onDraw()方法中畫一個矩形,并且設置畫筆的Shader為BitmapShader,Shader的tiling模式為CLAMP.
代碼如下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int w = getWidth();
int h = getHeight();
Bitmap bmp = BitmapFactory.decodeResource(getResources(),R.drawable.yourname);
mShader = new BitmapShader(bmp, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
mPaint.setShader(mShader);
canvas.drawRect(0,0,w,h,mPaint);
}
大家現在只需要關注mShader = new BitmapShader(bmp, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);這行代碼就可以了,剩下的呆會講。
在MainActivity中的布局文件中,我們加入這個自定義View。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.frank.gradientdemo.MainActivity">
<com.frank.gradientdemo.CustomView
android:layout_width="match_parent"
android:layout_height="400dp" />
</RelativeLayout>
我們可以看到CustomView的寬占手機整個屏幕,高是400dp.
我們在代碼中以CustomView的寬高畫一個矩形,并以上面的圖片作為貼圖紋理,效果如下:
好像和原圖有點不一樣? 紅框外面的是什么?我們把手機弄成橫屏再看
這次不一樣了!紅框右邊也和下邊一個樣子了,讓我們把注意力回到CLAMP的定義。
它的意思當要繪制的區間大于圖片紋理本身的區間時,多出來的空間位置將被紋理圖片的邊緣顏色填充。
結合例子看,這下應該能明白它的含義了吧。上面的例子中,如果貼圖的紋理本身小于要繪制的區域,那么超出部分將會以邊緣的顏色填充。所以就造成了上面的現象。大家可以細細體會一下。我們看下一個知識點。
MIRROR
這個模式能夠讓紋理以鏡像的方式在X和Y方向復制。
這個模式很容易理解大家看圖。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int w = getWidth();
int h = getHeight();
Bitmap bmp = BitmapFactory.decodeResource(getResources(),R.drawable.yourname);
mShader = new BitmapShader(bmp, Shader.TileMode.MIRROR, Shader.TileMode.MIRROR);
mPaint.setShader(mShader);
canvas.drawRect(0,0,w,h,mPaint);
}
這個就是鏡像的效果
REPEAT
它的作用是將圖片紋理沿XY軸進行復制。什么意思?看圖就懂,在這里,我要換一張圖片,作為演示效果。
然后代碼如下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int w = getWidth();
int h = getHeight();
Bitmap bmp = BitmapFactory.decodeResource(getResources(),R.drawable.repeat);
mShader = new BitmapShader(bmp, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
mPaint.setShader(mShader);
canvas.drawRect(0,0,w,h,mPaint);
}
效果:
大家有沒有覺得Repeat模式特別有用呢?一張圖就鋪滿整個空間。
混合雙打
上面講過的內容都是針對XY方向為同一種模式。能不能混合使用呢?
X—->CLAMP Y—->MIRROR
mShader = new BitmapShader(bmp, Shader.TileMode.CLAMP, Shader.TileMode.MIRROR);
X—->MIRROR Y—->CLAMP
mShader = new BitmapShader(bmp, Shader.TileMode.MIRROR, Shader.TileMode.CLAMP);
X—->CLAMP Y—->REPEAT
mShader = new BitmapShader(bmp, Shader.TileMode.CLAMP, Shader.TileMode.REPEAT);
可以看到右邊的部分拉伸了,然后上下復制同樣的圖像。
X—->REPEAT Y—->CLAMP
mShader = new BitmapShader(bmp, Shader.TileMode.REPEAT, Shader.TileMode.CLAMP);
可以看到右邊進行了復制,下面進行了拉伸。
X—->REPEAT Y—->MIRROR
mShader = new BitmapShader(bmp, Shader.TileMode.REPEAT, Shader.TileMode.MIRROR);
右邊的復制,下面的是鏡像。
X—->MIRROR Y—->REPEAT
mShader = new BitmapShader(bmp, Shader.TileMode.MIRROR, Shader.TileMode.REPEAT);
右邊的是鏡像,下面的是上面圖像的復制。
好了,TILEMODE講完了,我們進入主題(感覺怪怪的,這篇文章不是講TILEMODE的嗎?)
BitmapShader (Bitmap bitmap, Shader.TileMode tileX, Shader.TileMode tileY)
我們再來回顧下它的構造方法,bitmap是紋理圖片,兩個TileMode的參數對象我們也已經知道了含義與用法。現在我們來了解一下它的用法。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int w = getWidth();
int h = getHeight();
int radius = w <= h ? w/2 : h/2;
//1 解析bitmap對象
Bitmap bmp = BitmapFactory.decodeResource(getResources(),R.drawable.repeat);
//2 以bitmap對象生成BitmapShader,并且設置它的X和Y軸方向上的TILEMODE
mShader = new BitmapShader(bmp, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
//3 將BitmapShader對象安裝到畫筆對象上
mPaint.setShader(mShader);
//4 以該畫筆繪制圖形
canvas.drawCircle(w/2,h/2,radius,mPaint);
}
上面的代碼是繪制一個圓形,然后用圖片重復鋪圖。效果如下:
是不是很有感覺? 像自定義圓形圖片控件效果一樣。這小狗憂傷的讓我想想起了張嘉佳的《從你的全世界路過》的梅茜和劉大黑。
我們再發散思維下圓形圖像控件代碼編寫?
相信大家都知道,用可以設置先用canvas繪制一張圖片,然后設置畫筆的Xfermode Paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); 然后再繪制一個圓。
現在我們嘗試用BitmapShader的方式去編寫這么一個功能。
思路:
- 首先我們要確保這個自定義View是正方形的。
- 我們以目標圖片創建一個BitmapShader,然后設置進畫筆。
- 我們用設置好的畫筆利用Canvas繪制一個圓形。
- 關鍵一點,我們需要對原始的bitmap進行尺寸的調整,使得它的寬高至少要等于圓形的半徑。
代碼如下:
public class CustomView extends View {
private Paint mPaint;
private Shader mShader;
public CustomView(Context context) {
this(context,null);
}
public CustomView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint();
mPaint.setAntiAlias(true);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//這里為了方便演示,將尺寸固定為400*400
setMeasuredDimension(400,400);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int w = getWidth();
int h = getHeight();
int radius = w <= h ? w/2 : h/2;
//原圖
Bitmap bmp = BitmapFactory.decodeResource(getResources(),R.drawable.repeat);
//以目標寬高創建一個縮放過的圖片
Bitmap result = Bitmap.createScaledBitmap(bmp,w,h,false);
//用位圖創建BitmapShader
mShader = new BitmapShader(result, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
mPaint.setShader(mShader);
//畫圓
canvas.drawCircle(w/2,h/2,radius,mPaint);
}
}
效果圖
更牛X的功能。
我們已經知道怎么樣通過BitmapShader去渲染一個矩形或者是圓形了,但它的神奇之處就在于此嗎???
當然不是!!! Shader被稱為著色器,它用來渲染物體。在OPENGL 3d世界中,紋理可以看作是光禿禿的模型的皮膚,它可以為正文體,圓球,甚至復雜的人像模型著色。而在Canvas的范疇內,Shader肯定只是為了2d平面著色,除了矩形,圓形,它肯定還適用于三角形和其它多邊形以及任何閉合的不規則圖形,如何的圖形稱為不規則圖形呢?
我想說文字算不算???
看圖說話:
小狗狗的圖像粘貼到文字上了。代碼卻十分的簡單。
Bitmap bmp = BitmapFactory.decodeResource(getResources(),R.drawable.repeat);
mShader = new BitmapShader(bmp, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
mPaint.setTextSize(200.0f);
mPaint.setColor(Color.RED);
mPaint.setTypeface(Typeface.DEFAULT_BOLD);
mPaint.setShader(mShader);
canvas.drawText("小狗狗",0,h/2,mPaint);