android View的相關知識點

自定義View—–利用Canvas畫圖

對於android開發者來說,炫酷的界面可以給APP加分。但是由於第三方的UI有時候不符合我們的需求,這時候就需要自己寫View,因此對於View繪製的基礎我們還是需要掌握。

自定義View構造方法

  • View(Context context) 在代碼中簡單創建View被調用 View view = new View(this)
  • View(Context context, AttributeSet attrs) 當xml中佈局了自定義View後,在inflate佈局時被調用
  • View(Context context, AttributeSet attrs, int defStyleAttr) 和第二種構造方法類似,但多了一個defStyleAttr參數,當xml文件中有@style屬性時被調用。
  • View(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)和第三種構造方法類似,但又多了一個參數,當xml文件中有theme屬性時被調用
    屬性值定義的優先級:xml>style>Theme中的默認Sytle>默認Style(通過obtainStyledAttributes的第四個參數指定)>在Theme中直接指定屬性值

一般來說,自定義View都是繼承自View,而我們通常是在Canvas畫布上來繪製圖形。獲取Canvas主要有兩種方式

  • 通過重寫View.onDraw方法,View中的Canvas對象會被當做參數傳遞過來,我們操作這個Canvas,效果會直接反應在View中。
  • 如下所示,直接創建一個新的Canvas
Bitmap b = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);    
Canvas canvas = new Canvas(b);

這裏主要講解第一種情況下的自定義View
在畫圖時,有時候需要知道手機屏幕的大小,下面這種方法是獲取屏幕大小的常規方法(API21)

DisplayMetrics metrics = new DisplayMetrics();       getWindowManager().getDefaultDisplay().getMetrics(metrics);
int width = metrics.widthPixels;
int height = metrics.heightPixels;

之前網上還有一些其他的獲取屏幕的方法,例如通過canvas來獲取等,這裏就不一一列舉了。但值得注意的是

WindowManager manager = getWindowManager();
Display display = manager.getDefaultDisplay();
int width = display.getWidth();
int height = display.getHeight();

該代碼中getWidth()方法和getHeight()方法已經在新的API中被劃掉了,也就是不建議這樣來獲取屏幕尺寸。

下面是我寫的一段關於自定義View的測試代碼,主要就是在onDraw方法中繪製圖形,共參考

package com.example.root.myapplication;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.View;


public class MainActivity extends ActionBarActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
//        setContentView(R.layout.activity_main);
        setContentView(new CustomView_1(this));
    }


    /**inter class**/
    class CustomView_1 extends View{
        Paint paint ;

        public CustomView_1(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }


        public CustomView_1(Context context){
            super(context);
            paint = new Paint();
            //setting the painting pen
            paint.setColor(Color.BLUE);
            paint.setStrokeJoin(Paint.Join.ROUND);
            paint.setStrokeCap(Paint.Cap.ROUND);
            paint.setStrokeWidth(1);
        }
        @Override
        protected void onDraw(Canvas canvas){

            DisplayMetrics metrics = new DisplayMetrics();
            getWindowManager().getDefaultDisplay().getMetrics(metrics);
            int width = metrics.widthPixels;
            int length = metrics.heightPixels;
            //paint the background to yellow
            canvas.drawColor(Color.YELLOW);
            //paint a circle
            canvas.drawCircle(width/4, width/4, width/4, paint);
            //avoid area
            RectF rect = new RectF(width/2,0,width,width/2);
            //paint a arc
            canvas.drawArc(rect,0,90,true,paint);
            rect = new RectF(400,400,500,500);
            //paint a rect
            canvas.drawRoundRect(rect,15,15,paint);
        }

    }
}

運行試試,Done
關於canvas中的移動translate rotate,其實也很好理解,之前用canvas畫了一個時鐘,在畫刻度的時候用到了rotate。當我們用到了這兩個方法時,其實相當於改變了作圖時的參考座標(位置和方向)
eg:

canvas.translate(100, 100);  %表示畫布的(0,0)點向下,向右移動了一百個像素
canvas.rotate(30,0,0);%表示將畫布以(0,0)點爲中心向右旋轉30度,之後X,Y軸的方向也跟着轉變了

View以及ViewGroup部分源碼剖析

View是Android 所有圖形界面相關組建、界面、佈局等的基類,從官方SDK中可以看到View類直接繼承自Object類,而且實現了Drawable.Callback KeyEvent.Callback AccessibilityEventSource等接口。平時用到的ImageView, KeyboardView, MediaRouteButton, ProgressBar, SurfaceView, TextView, ViewGroup等都是直接繼承於它,而像AbsListView, AbsSpinner, AbsoluteLayout, AdapterView,等也間接繼承於它。
SDK中列出了常常見View需要用到的一些方法
Creation(創建)

  • Constructors()
  • onFinishInflate()

Layout(佈局)

  • onMeasure(int, int)
  • onLayout(boolean, int, int, int, int)
  • onSizeChanged(int, int, int, int)

Drawing(繪圖)

  • onDraw(Canvas canvas)

Event processing(事件處理)

  • onKeyDown(int, KeyEvent)
  • onKeyUp(int, KeyEvent)
  • onTrackballEvent(MotionEvent)
  • onTouchEvent(MotionEvent)

Focus(聚焦)

  • onFocusChanged(boolean, int, android.graphics.Rect)
  • onWindowFocusChanged(boolean)

Attaching(綁定)

  • onAttachedToWindow()
  • onDetachedFromWindow()
  • onWindowVisibilityChanged(int)

本文重點將下面幾個方法
onFinishInflate() 。當View和他的所有子View從XML中解析完成後調用,因此一般用在ViewGroup子類中,用於獲取子View的引用,例如

   @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        int count = getChildCount();
        if (count > 0) {
            for (int i = 0; i < count; ++i) {
            //getChildAt就是獲取子View
                addHeaderView(getChildAt(i));
            }
            removeAllViews();
        }
    }

  /**
     * Returns the view at the specified position in the group.
     * 
     * @param index the position at which to get the view from
     * @return the view at the specified position or null if the position
     *         does not exist within the group
     */
    public View getChildAt(int index) {
        if (index < 0 || index >= mChildrenCount) {
            return null;
        }
        return mChildren[index];
    }

onMeasure(int, int)。 測量這個View的高和寬,其實也是設置View的寬和高度。如果子View中沒有重寫這個方法,那麼將會在View對象中調用默認的onMeasure方法

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //onMeasure方法實際上實在調用setMeasuredDimension()方法
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
    /**final 類型,不能被覆蓋**/
    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }
    /**private 類型,不能被子類覆蓋*/
    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }
    /**
     * This is called to find out how big a view should be. The parent
     * supplies constraint information in the width and height parameters.
     * /**final 類型,不能被覆蓋**/用於改變子容器中子View大小
     */
       public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int oWidth  = insets.left + insets.right;
            int oHeight = insets.top  + insets.bottom;
            widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
            heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
        }

由上面的程序可以看出,onMeasure()方法最終轉化成了對mMeasuredWidth、mMeasuredHeight的設置。
而子類的View測量多是通過覆蓋onMeasure()方法實現對View測量。
對於像TextView這類View,重寫的onMeasure()方法其實質上就是調用了View的setMeasuredDimension方法

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int widthMode =MeasureSpec.getMode(widthMeasureSpec);
            int heightMode =MeasureSpec.getMode(heightMeasureSpec);
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
            int heightSize = MeasureSpec.getSize(heightMeasureSpec);
            int width;
            int height;
            ...
            setMeasuredDimension(width, height);
    }

而對於像ViewGroup一類(實際上是他們的實現類)的容器重寫的onMeasure()方法還需調用View類中的measure()方法對容器中的子View進行操作

 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // Sets up mListPadding
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int childWidth = 0;
        int childHeight = 0;
        int childState = 0;
        ...
        if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED)) {
            final View child = obtainView(0, mIsScrap);
            //measureScrapChild方法調用了View的measure方法
            measureScrapChild(child, 0, widthMeasureSpec);
            ...
            }
        ...
        setMeasuredDimension(widthSize , heightSize);
}

onLayout(boolean, int, int,int, int)。onLayout是用來指定各個子View的位置,這個方法的使用主要在ViewGroup中。這個方法也和上面的方法類似,也只是子類覆蓋的一個方法,真正調用的方法是Layout()。

onDraw(android.graphics.Canvas)。onDraw是用來對View進行繪圖的,這個方法也和上面的方法類似,也只是子類覆蓋的一個方法,真正調用的方法是draw()方法。
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas’ layers to prepare for fading
* 3. Draw view’s content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/

關於View值得注意的細節

  • Invalidate()和postInvalidate()
    Invalidate()只能在主線程(UI線程)中被調用,postInvalidate()可以在子線程(非UI線程)中被調用。下面是源碼介紹
    /**
     * Invalidate the whole view. If the view is visible,
     * {@link #onDraw(android.graphics.Canvas)} will be called at some point in
     * the future.
     * <p>
     * This must be called from a UI thread. To call from a non-UI thread, call
     * {@link #postInvalidate()}.
     */
    public void invalidate() {
        invalidate(true);
    }

    /**
     * <p>Cause an invalidate to happen on a subsequent cycle through the event loop.
     * Use this to invalidate the View from a non-UI thread.</p>
     *
     * <p>This method can be invoked from outside of the UI thread
     * only when this View is attached to a window.</p>
     *
     * @see #invalidate()
     * @see #postInvalidateDelayed(long)
     */
    public void postInvalidate() {
        postInvalidateDelayed(0);
    }

請注意,在view的內容或者大小改變時,常會調用invalidate() (postInvalidate())和 requestLayout(). 這兩個調用是確保穩定運行的關鍵。當view的某些內容發生變化的時候,需要調用invalidate來通知系統對這個view進行redraw,當某些元素變化會引起組件大小變化時,需要調用requestLayout方法。調用時若忘了這兩個方法,將會導致hard-to-find bugs。
本文參考博客http://blog.csdn.net/cwcwj3069/article/details/49867747

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章