爲什麼要自定義View?
- 自定義View可以大大簡化佈局層次,提高效率
- 原生控件無法滿足需求的時候,自定義View就會顯得非常重要
- 程序員掌握了非常大的自由,只要遵循一定的步驟,幾乎可以完成所有你能想到的控件,當然這個過程還是有很多細節和需要注意的地方的。
自定義View的步驟
這裏我們做一個和前文
自定義ViewGroup–CascadeLayout類似的控件CascadeView,還是先看一下效果圖
1. 繼承View,構造函數
CascadeView extends View
2.然後在構造函數中進行一些初始化的操作
在使用構造函數的時候有一點需要注意的地方,如果是使用Java代碼創建CascadeView,一般我們會使用CascadeView(Context context),如果相應在XML文件中使用CascadeView,我們需要使用CascadeView(Context context, AttributeSet attrs)這個構造函數,否則不會起作用的。爲了防止代碼的冗餘,可以把一些相同的代碼抽取出來,這裏的示例我就不這麼做了。
private Bitmap[] mPokers = new Bitmap[3];
private int[] mPokersId = new int[] {R.drawable.poker_39,R.drawable.poker_40,R.drawable.poker_48};
private int mHeight;
private int mWidth;
private int mPaddingTop;
private int mPaddingLeft;
public CascadeView(Context context) {
super(context);
}
public CascadeView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CascadeView);
mHeight = a.getDimensionPixelSize(R.styleable.CascadeView_poker_height, 0);
mWidth = a.getDimensionPixelSize(R.styleable.CascadeView_poker_width, 0);
mPaddingTop = a.getDimensionPixelSize(R.styleable.CascadeView_poker_paddingTop, 0);
mPaddingLeft = a.getDimensionPixelSize(R.styleable.CascadeView_poker_paddingLeft, 0);
for(int i=0;i<mPokers.length;i++){
mPokers[i] = drawableToBitmap(mPokersId[i],mWidth,mHeight);
}
}
這裏有一個將drawable資源id轉換爲bitmap的函數
private Bitmap drawableToBitmap(int drawableId,int width,int height)
{
return Bitmap.createScaledBitmap(
BitmapFactory.decodeResource(getResources(), drawableId), width, height, false);
}
如果是drawable轉換爲Bitmap,可以使用下面的函數
private Bitmap drawableToBitmap(Drawable drawable)
{
if(drawable instanceof BitmapDrawable){
BitmapDrawable bd = (BitmapDrawable) drawable;
return bd.getBitmap();
}
int h = drawable.getIntrinsicHeight();
int w = drawable.getIntrinsicWidth();
Bitmap bitmap = Bitmap.createBitmap(w,h,Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0,0,w,h);
drawable.draw(canvas);
return bitmap;
}
- 接下來最重要的異步就是重寫onDraw函數了,ondraw函數就是把東西繪製到你的屏幕上,這個函數在界面刷新的時候會頻繁的刷新(例如我們在代碼中調用invalidate,都會調用onDraw函數),因此,有一個非常重要的原則就是,不能再onDraw方法中進行復雜耗時的操作,如果非要做的話,可以放到異步線程裏面去,不要再UI線程中。
@Override
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
canvas.save();
for(Bitmap b:mPokers){
canvas.translate(mPaddingLeft, mPaddingTop);
canvas.drawBitmap(b, 0, 0,null);
}
canvas.restore();
}
- 在XML文件中使用
<com.daven.demo.CascadeView
android:id="@+id/cascadeview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
daven:poker_height="130dp"
daven:poker_paddingTop="20dp"
daven:poker_paddingLeft="30dp"
daven:poker_width="100dp" />
好了,到了這裏,程序其實就已經差不多了。
使用ClipRect優化過度繪製
但是有一點,其實我們完全可以對程序進行一個優化。我們把手機的GPU過度繪製打開,可以看到下面的圖:
其中綠色和紅色的部分,就是幾張撲克的重疊的部分,我們完全可以使用ClipRect把這幾個部分的東西剪裁掉,只保留最上面的一層。整個過程如下
@Override
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
canvas.save();
for(int i = 0; i < mPokers.length; i++){
canvas.translate(mPaddingLeft, mPaddingTop);
canvas.save();
if( i < mPokers.length -1){
canvas.clipRect(0,0,mWidth,mHeight);
canvas.clipRect( mPaddingLeft, mPaddingTop, mWidth, mHeight,Region.Op.DIFFERENCE);
}
canvas.drawBitmap(mPokers[i], 0, 0, null);
canvas.restore();
}
canvas.restore();
}
這裏我們需要說明一下Region.Op的用法和區別
- DIFFERENCE:之前剪切過除去當前要剪切的區域;
- INTERSECT:當前要剪切的區域在之前剪切過內部的部分;
- UNION:當前要剪切的區域加上之前剪切過內部的部分;
- XOR:異或,當前要剪切的區域與之前剪切過的進行異或;
- REVERSE_DIFFERENCE:與DIFFERENCE相反,以當前要剪切的區域爲參照物,當前要剪切的區域除去之前剪切過的區域;
- REPLACE:用當前要剪切的區域代替之前剪切過的區域。
- 沒帶Op參數效果與INTERSECT的效果一樣,兩個區域的交集。
我們可以看到那張K,被我們剪裁成這個樣子了,重疊的部分完全沒有了
處理View上面的點擊事件onTouchEvent
到了這裏,我們的View放在那裏好像還只是一個擺設,我們想如果能讓其相應我們的點擊事件那該多好,當時view本身就是可以有監聽函數的,但是如果我們需要在點擊該view的某個區域進行相應該怎麼辦呢?
這個時候我們就需要自己寫監聽函數了!
1.首先我們寫一個監聽器的接口
private OnViewClickListener mOnClickListener;
public void setOnClickListener(OnViewClickListener e) {
mOnClickListener = e;
}
public interface OnViewClickListener {
public void OnClick(int position);
}
- 重寫onTouchEvent(), 判斷點擊的座標是否在K那張牌上面,如果是就響應這個回調函數
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction()){
case MotionEvent.ACTION_UP:
if( (x > mPaddingLeft && x < 2 * mPaddingLeft
&& y > mPaddingTop && y < mHeight)||
(x > mPaddingLeft && x < mWidth
&& y > mPaddingTop && y < 2*mPaddingTop) ){
mOnClickListener.OnClick(0);
}
return true;
}
return true;
}
- 最後一點就是在MainActivity中使用這個監聽器了,和我們使用其他的監聽器一樣!
CascadeView c = (CascadeView) findViewById(R.id.cascadeview);
c.setOnClickListener(new CascadeView.OnViewClickListener() {
@Override
public void OnClick(int position) {
Toast.makeText(getBaseContext(), " You clicked K", Toast.LENGTH_SHORT).show();
}
});
點擊到了之後就會回調到這個函數,然後彈出Toast提示!