編輯:關於Android編程
雖然android本身給我們提供了形形色色的控件,基本能夠滿足日常開發的需求,但是面對日益同質化的app界面,和不同的業務需求.我們可能就需要自定義一些View來獲得比較好的效果.自定義View是android開發者走向高級開發工程師必須要走的一關.
一般我們構造函數可以寫成這樣:
public MyView(Context context) { this(context, null); } public MyView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MyView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); }二.自定義命名空間: 當我們自定義View的時候許多的屬性我們當然不希望被寫死.例如我自定義了一個圓.這個圓的顏色我更希望不在自定義控件裡面寫死,而是在使用的時候在布局文件中進 行指定這個顏色的時候.我們就需要用到自定義命名空間來對自定義View的屬性進行設置了. 步驟1:首先在Values文件夾裡面新建attrs文件. 步驟2.編寫attrs文件.attrs有兩種寫法.
(2.)如果是有公共的屬性部分,可以將屬性包含在公共屬性部分裡面.也就是說公共屬性可以被多個自定義控件屬性樣式使用.
所有的format類型
理論知識講的有點多,可能有點空洞,下面通過一個小的例子,來測試一下我們的命名空間是否可以正常使用. 例子:我們自定義一個View,這個View的形狀是一個圓形,並且我們不希望將圓的顏色寫死,可以在布局文件中進行設置. 其他的我們都先不管,只是測試一下自定義命名空間. 步驟1.創建一個View繼承自View.並且重寫它的構造函數
public class MyView extends View { public MyView(Context context) { this(context, null); } public MyView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MyView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } }步驟2:在Values文件夾下面創建一個attrs的文件,寫上自定義的屬性.
步驟3:在布局文件中進行引用自定義命名空間.這裡給設置的顏色是橘黃色.
步驟4:在自定義View中設置我們自定義的屬性.
public class MyView extends View { private int mColor; private Paint mP; private float mRadius; public MyView(Context context) { this(context, null); } public MyView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MyView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); //拿到自定義屬性 TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyView); mColor = typedArray.getColor(R.styleable.MyView_roundColor, 0XFF00FF00); mRadius = typedArray.getDimension(R.styleable.MyView_radius, 50); //回收資源 typedArray.recycle(); //創建畫筆 mP = new Paint(); //設置畫筆顏色 mP.setColor(mColor); //設置抗鋸齒 mP.setAntiAlias(true); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //畫圓 canvas.drawCircle(mRadius, mRadius, mRadius, mP); } }Ok代碼書寫完畢,我們來看看實現的效果是怎麼樣的.
當然,這裡只是實現了自定義控件的一小部分功能,接著我們來看看一個問題: 我們將我們自定義控件的background設置為紅色來看看效果.這裡控件都是設置包裹內容的.
效果如下圖所示:
看到這個效果可能會有點驚奇了,明明我設置的是包裹內容的,為什麼控件確實填充了父窗體?帶著這樣的疑問我們接下來學習,自定義控件的另外一個非常重要的函數:onmeasure(); onmeuse()方法:測量自己的大小,為正式布局提供建議。(注意,只是建議,至於用不用,要看onLayout); 主要的作用就是處理自定義VIewgroup的時候是wrap_content的時候該ViewGrop的大小. 定義:如果layout_widht和layout_height是match_parent或具體的xxxdp,那就非常簡單了,直接調用setMeasuredDimension()方 法,設置ViewGroup的寬高即可.But如果是wrap_content,就比較麻煩了,如果不重寫onMeasure()方法,系統則會不知道該默認多 大尺寸,就會默認填充整個父布局,所以,重寫onMeasure()方法的目的,就是為了能夠給 View 一個wrap_content屬性下的默認大 小。 調用此方法會傳進來的兩個參數:int widthMeasureSpec,int heightMeasureSpec.他們是父類傳遞過來給當前view的一個建議值, 即把當前view的尺寸設置為寬widthMeasureSpec,高heightMeasureSpec雖然表面上看起來他們是int類型的數字,其實他們是由 mode+size兩部分組成的。widthMeasureSpec和heightMeasureSpec轉化成二進制數字表示,他們都是30位的。前兩位代表mode(測量模 式),後面28位才是他們的實際數值(size)。 MeasureSpec.getMode()獲取模式 MeasureSpec.getSize()獲取尺寸 mode的值有三種為: EXACTLY:表示設置了精確的值,一般當childView設置其寬、高為精確值(也就是我們在布局文件中設定的值如50dp)、match_parent時,ViewGroup會將其設置為EXACTLY;
AT_MOST:表示子布局被限制在一個最大值內,一般當childView設置其寬、高為wrap_content時,ViewGroup會將其設置為AT_MOST;
UNSPECIFIED:表示子布局想要多大就多大,一般出現在AadapterView的item的heightMode中、ScrollView的childView的heightMode中;此種模式比較少見。
我們需要判斷當布局文件中設置控件為包裹內容的時候,控件的大小的值就可以了.因此重寫onmeasure()方法如下:@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //獲取測量的模式 int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); //獲取測量的值 int withSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); //設置控件的大小 setMeasuredDimension(widthMode == MeasureSpec.AT_MOST ? (int) mRadius * 2 : withSize, heightMode == MeasureSpec.AT_MOST ? (int) mRadius * 2 : heightSize); }設置成功以後,直接將工程運行起來就可以看到效果了:
上面的例子都是演示的畫圓,如果想畫其他的形狀應該怎麼辦呢?我們需要通過重寫onDraw() 方法對控件重寫進行繪制就可以了. Ondraw(). draw就是畫的意思從字面意思我們也可以知道.通過重寫該方法我們可以對繪制出相關的控件.那麼繪制的時候在是在什麼上面進行繪制呢?我們 先來重寫ondraw()看看:
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); }
我們可以看到通過重寫ondraw()會傳過來一個Canvas類,這個類實際上就是一塊兒畫布,我們可以創建畫筆在上面進行繪圖. Canvas的使用.
這個類相當於一個畫布,你可以在裡面畫很多東西;
我們可以把這個Canvas理解成系統提供給我們的一塊內存區域(但實際上它只是一套畫圖的API,真正的內存是下面的 Bitmap),而且它還提供了一整套對這個內存區域進行操作的方法,所有的這些操作都是畫圖API。也就是說在這種方式
下我們已經能一筆一劃或者使用畫筆來畫我們所需要的東西了,要畫什麼要顯示什麼都由我們自己控制。這種方式根據環 境還分為兩種:一種就是使用普通View的canvas畫圖,還有一種就是使用專門的SurfaceView的canvas來畫圖。兩種的 主要是區別就是可以在SurfaceView中定義一個專門的線程來完成畫圖工作,應用程序不需要等待View的刷圖,提高性 能。前面一種適合處理量比較小,幀率比較小的動畫,比如說簡單的View樣式或者是象棋游戲之類的;而後一種主要用 在游戲,高品質動畫方面的畫圖。
Canvas可以繪制的對象有:弧線(arcs)、填充顏色(argb和color)、 Bitmap、圓(circle和oval)、點(point)、線(line)、矩形 (Rect)、圖片(Picture)、圓角矩形 (RoundRect)、文本(text)、頂點(Vertices)、路徑(path)。通過組合這些對象我們可以畫出一 些簡單有趣的界面出來,但是光有這些功能還是不夠的,如果我要畫一個儀表盤(數字圍繞顯示在一個圓圈中)呢? 幸好Android 還提供了一些對Canvas位置轉換的方法:rorate、scale、translate、skew(扭曲)等,而且它允許你通過獲得它的轉換矩陣對象 (getMatrix方法) 直接操作它。這些操作就像是雖然你的筆還是原來的地方畫,但是畫紙旋轉或者移動了,所以你畫的東西的方 位就產生變化。為了方便一些轉換操作,Canvas 還提供了保存和回滾屬性的方法(save和restore),比如你可以先保存目前畫紙 的位置(save),然後旋轉90度,向下移動100像素後畫一些圖形,畫完後調用restore方法返回到剛才保存的位置. 畫一些比較常見的幾何圖形: 畫圓:canvas.drawCircle()canvas.drawCircle(100, 100, 90, paint);畫弧形:canvas.drawArc();
//繪制弧線區域 //先要繪制矩形 RectF rect = new RectF(0, 0, 100, 100); canvas.drawArc(rect, //弧線所使用的矩形區域大小 270, //開始角度 90, //掃過的角度 true, //是否使用中心 paint); //畫筆顏色填充:canvas.drawColor();
canvas.drawColor(Color.BLUE);畫一條線:canvas.drawLine()
canvas.drawLine(10,//x起點位置 10, //y起點位置 100, //x終點位置 100, //y終點位置 paint); //畫筆畫橢圓:
//定義一個矩形區域 RectF oval = new RectF(0,0,200,300); //矩形區域內切橢圓 canvas.drawOval(oval, paint);畫帶有弧度的文字:canvas.drawPosText();
//按照既定點 繪制文本內容 canvas.drawPosText("Android", new float[]{ 10,10, //第一個字母在坐標10,10 20,20, //第二個字母在坐標20,20 30,30, //.... 40,40, 50,50, 60,60, 70,70, }, paint);畫矩形:canvas.drawRect();
RectF rect = new RectF(50, 50, 200, 200); canvas.drawRect(rect, paint);畫帶有弧度的矩形:canvas.drawRoundRect();
RectF rect = new RectF(50, 50, 200, 200); canvas.drawRoundRect(rect, 30, //x軸的半徑 30, //y軸的半徑 paint);畫封閉的圖形:
Path path = new Path(); //定義一條路徑 path.moveTo(10, 10); //移動到 坐標10,10 path.lineTo(50, 60); path.lineTo(200,80); path.lineTo(10, 10); canvas.drawPath(path, paint);畫文字跟隨一條線:
Path path = new Path(); //定義一條路徑 path.moveTo(10, 10); //移動到 坐標10,10 path.lineTo(50, 60); path.lineTo(200,80); path.lineTo(10, 10); canvas.drawTextOnPath("Android", path, 10, 10, paint);畫圖片:
drawBitmap(Bitmap bitmap, float left, float top, Paint paint)canvas的一些常規的方法:
canvas.rotate(360 / count,//旋轉的角度 0f, x軸的坐標 0f); //旋轉畫紙 canvas.translate(200, 200); //將位置移動畫紙的坐標點到x為200,y為200 canvas.save(); //保存畫布的狀態 canvas.restore(); //取出保存的狀態
canvas.save();和canvas.restore();是兩個相互匹配出現的,作用是用來保存畫布的狀態和取出保存的狀態的。這裡稍微解釋一下,
當我們對畫布進行旋轉,縮放,平移等操作的時候其實我們是想對特定的元素進行操作,比如圖片,一個矩形等,但是當你用canvas的方法來進行這些操作的時候,其實是對整個畫布進行了操作,那麼之後在畫布上的元素都會受到影響,所以我們在操作之前調用canvas.save()來保存畫布當前的狀態,當操作之後取出之前保存過的狀態,這樣就不會對其他的元素進行影響.
畫筆Paint
從上面列舉的幾個Canvas.drawXxx()的方法看到,其中都有一個類型為paint的參數,可以把它理解為一個"畫筆",通過這個畫筆,在Canvas這張畫布上作畫。它位於"android.graphics.Paint"包下,主要用於設置繪圖風格,包括畫筆顏色、畫筆粗細、填充風格等。
Paint中提供了大量設置繪圖風格的方法,這裡僅列出一些常用的:
invalidate()和postInvalidate()的區別.
通過上面的講解,我在自定義一個靜態的View已經是一件非常容易的事情了,但是我們使用的自定義的View有很多是需要根據一個變量去不斷繪制的,這個時候就引出了新的函數invalidate()和postinvalidate(),使用此函數可以是的ondraw()不斷的去執行從而達到不斷繪制的效果.
Android中實現view的更新有兩組方法,一組是invalidate,另一組是postInvalidate,其中前者是在UI線程自身中使用,而後者在非UI線程中使用。
接下來通過一個稍微綜合一點的例子來對自定義View做一個總結:
先來看看實現的效果:
我們先來分析一下,這個效果實際上就是外面是不斷的去繪制一個扇形,然後中間蓋了一個小的圓:
好了,接下來我們來講一講實現的步驟:
步驟一:首先定義attrs文件:
步驟二:編寫自定義View:
public class ProgressView extends View { private float mArcRadius; private float mSmallRoundRadius; private int mArcColor; private int mSmallRoundColor; private Paint mRoundpaint; private Paint mArcpaint; private float sweepAngle; private int mTextColor; private Paint mTextPaint; private int mTextSize; public ProgressView(Context context) { this(context, null); } public ProgressView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ProgressView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ProgressView); //扇形半徑 mArcRadius = array.getDimension(R.styleable.ProgressView_arcRadius, 50); //小圓半徑 mSmallRoundRadius = array.getDimension(R.styleable.ProgressView_smallRoundRadius, 50); //扇形顏色 mArcColor = array.getColor(R.styleable.ProgressView_arcColor, 0XFF00FF00); //小圓顏色 mSmallRoundColor = array.getColor(R.styleable.ProgressView_smallRoundColor, 0XFF00FF00); //百分比字體顏色 mTextColor = array.getColor(R.styleable.ProgressView_textColor, 0XFF00FF00); //百分比字體大小 mTextSize = array.getDimensionPixelSize(R.styleable.ProgressView_textSize, 15); //釋放資源 array.recycle(); //畫圓的畫筆 mRoundpaint = new Paint(); mRoundpaint.setColor(mSmallRoundColor); mRoundpaint.setAntiAlias(true); //畫扇形的畫筆 mArcpaint = new Paint(); mArcpaint.setColor(mArcColor); mArcpaint.setAntiAlias(true); //繪制中間文字部分的畫筆文本 mTextPaint = new Paint(); mTextPaint.setColor(mTextColor); mTextPaint.setAntiAlias(true); mTextPaint.setTextSize(mTextSize); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //設置View的大小 int withMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int withSize = MeasureSpec.getSize(widthMeasureSpec); int heightsize = MeasureSpec.getSize(heightMeasureSpec); setMeasuredDimension(withMode == MeasureSpec.AT_MOST ? (int) mArcRadius * 2 : withSize, heightMode == MeasureSpec.AT_MOST ? (int) mArcRadius * 2 : heightsize); } @Override protected void onDraw(Canvas canvas) { //畫圓弧 RectF rect = new RectF(0, 0, (int) mArcRadius * 2, (int) mArcRadius * 2); canvas.drawArc(rect, 270, (float) (sweepAngle * 3.6), true, mArcpaint); //畫小圓 canvas.drawCircle(mArcRadius, mArcRadius, mSmallRoundRadius, mRoundpaint); String text = (int) (sweepAngle) + "%"; float textLength = mTextPaint.measureText(text); //把文本畫在圓心居中 canvas.drawText(text, mArcRadius - textLength / 2, mArcRadius, mTextPaint); super.onDraw(canvas); } //提供一個給外界的方法可以不斷的去設置扇形的弧度 public void percent(float sweepAngle) { if (sweepAngle <= 100) { this.sweepAngle = sweepAngle; //刷新界面 postInvalidate(); } } }步驟三:在布局文件中進行使用:
步驟四:在需要用到的地方模擬數據去使用自定義的View.
public class SecondActivity extends AppCompatActivity { private int mTotalProgress; private int mCurrentProgress; private ProgressView mPv; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_second); mPv = (ProgressView) findViewById(R.id.pv); initVariable(); findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mCurrentProgress=0; new Thread(new ProgressRunable()).start(); } }); } private void initVariable() { mTotalProgress = 100; mCurrentProgress = 0; } class ProgressRunable implements Runnable { @Override public void run() { while (mCurrentProgress < mTotalProgress) { mCurrentProgress += 1; mPv.percent((float) mCurrentProgress); try { Thread.sleep(50); } catch (Exception e) { e.printStackTrace(); } } } } }
至此大功告成.
前陣子要的工作是給桌面(Launcher啟動器,其實也是一個activity)添加一個觸摸特效(一個View),而這個特效是每次觸碰都會有,不管你在桌面上做什麼操作都會顯
引言布局是直接影響用戶體驗的關鍵部分。如果實現的不好,那麼布局很有可能會導致內存的緊張。Android的SDK包含的一些工具可以用來檢查布局性能上的問題。結合本章的課程學
從mediaTek項目中移植出來的,完整的代碼可以在我的GitHub上看到,下面看一下效果: 主Activity: package com
話不多說先上圖:類似於這樣的效果,很簡單。這是一個listview,item是自定義的view,有兩個特點: 傳入長度,動態改變柱狀圖的長度;根據長度改變色值,