Android自定義View全解

目錄

目錄.png

1. 自定義View基礎(chǔ)

1.1 分類

自定義View的實現(xiàn)方式有以下幾種

類型 定義
自定義組合控件 多個控件組合成為一個新的控件,方便多處復(fù)用
繼承系統(tǒng)View控件 繼承自TextView等系統(tǒng)控件拳芙,在系統(tǒng)控件的基礎(chǔ)功能上進行擴展
繼承View 不復(fù)用系統(tǒng)控件邏輯,繼承View進行功能定義
繼承系統(tǒng)ViewGroup 繼承自LinearLayout等系統(tǒng)控件,在系統(tǒng)控件的基礎(chǔ)功能上進行擴展
繼承ViewViewGroup 不復(fù)用系統(tǒng)控件邏輯,繼承ViewGroup進行功能定義

1.2 View繪制流程

View的繪制基本由measure()蟋软、layout()、draw()這個三個函數(shù)完成

函數(shù) 作用 相關(guān)方法
measure() 測量View的寬高 measure(),setMeasuredDimension(),onMeasure()
layout() 計算當(dāng)前View以及子View的位置 layout(),onLayout(),setFrame()
draw() 視圖的繪制工作 draw(),onDraw()

1.3 坐標(biāo)系

在Android坐標(biāo)系中嗽桩,以屏幕左上角作為原點岳守,這個原點向右是X軸的正軸,向下是Y軸正軸涤躲。如下所示:


Android坐標(biāo)系.png

除了Android坐標(biāo)系,還存在View坐標(biāo)系贡未,View坐標(biāo)系內(nèi)部關(guān)系如圖所示种樱。


視圖坐標(biāo)系.png

View獲取自身高度

由上圖可算出View的高度:

  • width = getRight() - getLeft();
  • height = getBottom() - getTop();

View的源碼當(dāng)中提供了getWidth()和getHeight()方法用來獲取View的寬度和高度,其內(nèi)部方法和上文所示是相同的俊卤,我們可以直接調(diào)用來獲取View得寬高嫩挤。

View自身的坐標(biāo)

通過如下方法可以獲取View到其父控件的距離。

  • getTop()消恍;獲取View到其父布局頂邊的距離岂昭。
  • getLeft();獲取View到其父布局左邊的距離狠怨。
  • getBottom()约啊;獲取View到其父布局頂邊的距離。
  • getRight()佣赖;獲取View到其父布局左邊的距離恰矩。

1.4 構(gòu)造函數(shù)

無論是我們繼承系統(tǒng)View還是直接繼承View,都需要對構(gòu)造函數(shù)進行重寫憎蛤,構(gòu)造函數(shù)有多個外傅,至少要重寫其中一個才行。如我們新建TestView俩檬,

public class TestView extends View {
    /**
     * 在java代碼里new的時候會用到
     * @param context
     */
    public TestView(Context context) {
        super(context);
    }

    /**
     * 在xml布局文件中使用時自動調(diào)用
     * @param context
     */
    public TestView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * 不會自動調(diào)用萎胰,如果有默認style時,在第二個構(gòu)造函數(shù)中調(diào)用
     * @param context
     * @param attrs
     * @param defStyleAttr
     */
    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    /**
     * 只有在API版本>21時才會用到
     * 不會自動調(diào)用棚辽,如果有默認style時技竟,在第二個構(gòu)造函數(shù)中調(diào)用
     * @param context
     * @param attrs
     * @param defStyleAttr
     * @param defStyleRes
     */
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }
}

1.5 自定義屬性

Android系統(tǒng)的控件以android開頭的都是系統(tǒng)自帶的屬性。為了方便配置自定義View的屬性屈藐,我們也可以自定義屬性值灵奖。
Android自定義屬性可分為以下幾步:

  1. 自定義一個View
  2. 編寫values/attrs.xml嚼沿,在其中編寫styleable和item等標(biāo)簽元素
  3. 在布局文件中View使用自定義的屬性(注意namespace)
  4. 在View的構(gòu)造方法中通過TypedArray獲取

實例說明

  • 自定義屬性的聲明文件
    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <declare-styleable name="test">
            <attr name="text" format="string" />
            <attr name="testAttr" format="integer" />
        </declare-styleable>
    </resources>
  • 自定義View類
public class MyTextView extends View {
    private static final String TAG = MyTextView.class.getSimpleName();

    //在View的構(gòu)造方法中通過TypedArray獲取
    public MyTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.test);
        String text = ta.getString(R.styleable.test_testAttr);
        int textAttr = ta.getInteger(R.styleable.test_text, -1);
        Log.e(TAG, "text = " + text + " , textAttr = " + textAttr);
        ta.recycle();
    }
}
  • 布局文件中使用
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res/com.example.test"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <com.example.test.MyTextView
        android:layout_width="100dp"
        android:layout_height="200dp"
        app:testAttr="520"
        app:text="helloworld" />

</RelativeLayout>

屬性值的類型format

(1). reference:參考某一資源ID

  • 屬性定義:
<declare-styleable name = "名稱">
     <attr name = "background" format = "reference" />
</declare-styleable>
  • 屬性使用:
<ImageView android:background = "@drawable/圖片ID"/>

(2). color:顏色值

  • 屬性定義:
<attr name = "textColor" format = "color" />
  • 屬性使用:
<TextView android:textColor = "#00FF00" />

(3). boolean:布爾值

  • 屬性定義:
<attr name = "focusable" format = "boolean" />
  • 屬性使用:
<Button android:focusable = "true"/>

(4). dimension:尺寸值

  • 屬性定義:
<attr name = "layout_width" format = "dimension" />
  • 屬性使用:
<Button android:layout_width = "42dip"/>

(5). float:浮點值

  • 屬性定義:
<attr name = "fromAlpha" format = "float" />
  • 屬性使用:
<alpha android:fromAlpha = "1.0"/>

(6). integer:整型值

  • 屬性定義:
<attr name = "framesCount" format="integer" />
  • 屬性使用:
<animated-rotate android:framesCount = "12"/>

(7). string:字符串

  • 屬性定義:
<attr name = "text" format = "string" />
  • 屬性使用:
<TextView android:text = "我是文本"/>

(8). fraction:百分數(shù)

  • 屬性定義:
<attr name = "pivotX" format = "fraction" />
  • 屬性使用:
<rotate android:pivotX = "200%"/>

(9). enum:枚舉值

  • 屬性定義:
<declare-styleable name="名稱">
    <attr name="orientation">
        <enum name="horizontal" value="0" />
        <enum name="vertical" value="1" />
    </attr>
</declare-styleable>
  • 屬性使用:
<LinearLayout  
    android:orientation = "vertical">
</LinearLayout>

注意:枚舉類型的屬性在使用的過程中只能同時使用其中一個,不能 android:orientation = “horizontal|vertical"

(10). flag:位或運算

  • 屬性定義:
<declare-styleable name="名稱">
    <attr name="gravity">
            <flag name="top" value="0x01" />
            <flag name="bottom" value="0x02" />
            <flag name="left" value="0x04" />
            <flag name="right" value="0x08" />
            <flag name="center_vertical" value="0x16" />
            ...
    </attr>
</declare-styleable>
  • 屬性使用:
<TextView android:gravity="bottom|left"/>

注意:位運算類型的屬性在使用的過程中可以使用多個值

(11). 混合類型:屬性定義時可以指定多種類型值

  • 屬性定義:
<declare-styleable name = "名稱">
     <attr name = "background" format = "reference|color" />
</declare-styleable>
  • 屬性使用:
<ImageView
android:background = "@drawable/圖片ID" />
或者:
<ImageView
android:background = "#00FF00" />

2. View繪制流程

這一章節(jié)偏向于解釋View繪制的源碼實現(xiàn)瓷患,可以更好地幫助我們掌握整個繪制過程骡尽。

View的繪制基本由measure()、layout()擅编、draw()這個三個函數(shù)完成

函數(shù) 作用 相關(guān)方法
measure() 測量View的寬高 measure(),setMeasuredDimension(),onMeasure()
layout() 計算當(dāng)前View以及子View的位置 layout(),onLayout(),setFrame()
draw() 視圖的繪制工作 draw(),onDraw()

2.1 Measure()

MeasureSpec

MeasureSpec是View的內(nèi)部類攀细,它封裝了一個View的尺寸,在onMeasure()當(dāng)中會根據(jù)這個MeasureSpec的值來確定View的寬高爱态。

MeasureSpec的值保存在一個int值當(dāng)中谭贪。一個int值有32位,前兩位表示模式mode后30位表示大小size锦担。即MeasureSpec = mode + size俭识。

MeasureSpec當(dāng)中一共存在三種modeUNSPECIFIEDEXACTLY
AT_MOST洞渔。

對于View來說套媚,MeasureSpec的mode和Size有如下意義

模式 意義 對應(yīng)
EXACTLY 精準(zhǔn)模式,View需要一個精確值磁椒,這個值即為MeasureSpec當(dāng)中的Size match_parent
AT_MOST 最大模式堤瘤,View的尺寸有一個最大值,View不可以超過MeasureSpec當(dāng)中的Size值 wrap_content
UNSPECIFIED 無限制浆熔,View對尺寸沒有任何限制本辐,View設(shè)置為多大就應(yīng)當(dāng)為多大 一般系統(tǒng)內(nèi)部使用

使用方式

    // 獲取測量模式(Mode)
    int specMode = MeasureSpec.getMode(measureSpec)

    // 獲取測量大小(Size)
    int specSize = MeasureSpec.getSize(measureSpec)

    // 通過Mode 和 Size 生成新的SpecMode
    int measureSpec=MeasureSpec.makeMeasureSpec(size, mode);

在View當(dāng)中医增,MeasureSpace的測量代碼如下:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        //當(dāng)父View要求一個精確值時慎皱,為子View賦值
        case MeasureSpec.EXACTLY:
            //如果子view有自己的尺寸,則使用自己的尺寸
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
                //當(dāng)子View是match_parent,將父View的大小賦值給子View
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
                //如果子View是wrap_content叶骨,設(shè)置子View的最大尺寸為父View
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // 父布局給子View了一個最大界限
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                //如果子view有自己的尺寸宝冕,則使用自己的尺寸
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // 父View的尺寸為子View的最大尺寸
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //父View的尺寸為子View的最大尺寸
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // 父布局對子View沒有做任何限制
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
            //如果子view有自己的尺寸,則使用自己的尺寸
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //因父布局沒有對子View做出限制邓萨,當(dāng)子View為MATCH_PARENT時則大小為0
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //因父布局沒有對子View做出限制地梨,當(dāng)子View為WRAP_CONTENT時則大小為0
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
    
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

這里需要注意,這段代碼只是在為子View設(shè)置MeasureSpec參數(shù)而不是實際的設(shè)置子View的大小缔恳。子View的最終大小需要在View中具體設(shè)置宝剖。

從源碼可以看出來,子View的測量模式是由自身LayoutParam和父View的MeasureSpec來決定的歉甚。

在測量子View大小時:

父View mode 子View
UNSPECIFIED 父布局沒有做出限制万细,子View有自己的尺寸,則使用,如果沒有則為0
EXACTLY 父布局采用精準(zhǔn)模式赖钞,有確切的大小腰素,如果有大小則直接使用,如果子View沒有大小雪营,子View不得超出父view的大小范圍
AT_MOST 父布局采用最大模式弓千,存在確切的大小,如果有大小則直接使用献起,如果子View沒有大小洋访,子View不得超出父view的大小范圍

onMeasure()

整個測量過程的入口位于Viewmeasure方法當(dāng)中,該方法做了一些參數(shù)的初始化之后調(diào)用了onMeasure方法谴餐,這里我們主要分析onMeasure姻政。

onMeasure方法的源碼如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

很簡單這里只有一行代碼,涉及到了三個方法我們挨個分析岂嗓。

  • setMeasuredDimension(int measuredWidth, int measuredHeight) :該方法用來設(shè)置View的寬高汁展,在我們自定義View時也會經(jīng)常用到。
  • getDefaultSize(int size, int measureSpec):該方法用來獲取View默認的寬高厌殉,結(jié)合源碼來看食绿。
/**
*   有兩個參數(shù)size和measureSpec
*   1、size表示View的默認大小年枕,它的值是通過`getSuggestedMinimumWidth()方法來獲取的炫欺,之后我們再分析乎完。
*   2熏兄、measureSpec則是我們之前分析的MeasureSpec,里面存儲了View的測量值以及測量模式
*/
public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        //從這里我們看出树姨,對于AT_MOST和EXACTLY在View當(dāng)中的處理是完全相同的摩桶。所以在我們自定義View時要對這兩種模式做出處理。
        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }
  • getSuggestedMinimumWidth():getHeight和該方法原理是一樣的帽揪,這里只分析這一個硝清。
//當(dāng)View沒有設(shè)置背景時,默認大小就是mMinWidth转晰,這個值對應(yīng)Android:minWidth屬性芦拿,如果沒有設(shè)置時默認為0.
//如果有設(shè)置背景,則默認大小為mMinWidth和mBackground.getMinimumWidth()當(dāng)中的較大值查邢。
protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

ViewGroup的測量過程與View有一點點區(qū)別蔗崎,其本身是繼承自View,它沒有對Viewmeasure方法以及onMeasure方法進行重寫扰藕。

為什么沒有重寫onMeasure呢缓苛?ViewGroup除了要測量自身寬高外還需要測量各個子View的大小,而不同的布局測量方式也都不同(可參考LinearLayout以及FrameLayout)邓深,所以沒有辦法統(tǒng)一設(shè)置未桥。因此它提供了測量子View的方法measureChildren()以及measureChild()幫助我們對子View進行測量笔刹。

measureChildren()以及measureChild()的源碼這里不再分析,大致流程就是遍歷所有的子View冬耿,然后調(diào)用Viewmeasure()方法舌菜,讓子View測量自身大小。具體測量流程上面也以及介紹過了


measure過程會因為布局的不同或者需求的不同而呈現(xiàn)不同的形式淆党,使用時還是要根據(jù)業(yè)務(wù)場景來具體分析酷师,如果想再深入研究可以看一下LinearLayoutonMeasure方法。

2.2 Layout()

要計算位置首先要對Android坐標(biāo)系有所了解染乌,前面的內(nèi)容我們也有介紹過山孔。

layout()過程,對于View來說用來計算View的位置參數(shù),對于ViewGroup來說荷憋,除了要測量自身位置台颠,還需要測量子View的位置。

layout()方法是整個Layout()流程的入口勒庄,看一下這部分源碼

/**
*  這里的四個參數(shù)l串前、t、r实蔽、b分別代表View的左荡碾、上、右局装、下四個邊界相對于其父View的距離坛吁。
*
*/
public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        //這里通過setFrame或setOpticalFrame方法確定View在父容器當(dāng)中的位置。
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        //調(diào)用onLayout方法铐尚。onLayout方法是一個空實現(xiàn)拨脉,不同的布局會有不同的實現(xiàn)。
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);

        }

    }

從源碼我們知道宣增,在layout()方法中已經(jīng)通過setOpticalFrame(l, t, r, b)setFrame(l, t, r, b)方法對View自身的位置進行了設(shè)置玫膀,所以onLayout(changed, l, t, r, b)方法主要是ViewGroup對子View的位置進行計算。

有興趣的可以看一下LinearLayoutonLayout源碼爹脾,可以幫助加深理解帖旨。

2.3 Draw()

draw流程也就是的View繪制到屏幕上的過程,整個流程的入口在Viewdraw()方法之中灵妨,而源碼注釋也寫的很明白解阅,整個過程可以分為6個步驟。

  1. 如果需要闷串,繪制背景瓮钥。
  2. 有過有必要,保存當(dāng)前canvas。
  3. 繪制View的內(nèi)容碉熄。
  4. 繪制子View桨武。
  5. 如果有必要,繪制邊緣锈津、陰影等效果呀酸。
  6. 繪制裝飾,如滾動條等等琼梆。

通過各個步驟的源碼再做分析:

    public void draw(Canvas canvas) {

       
        int saveCount;
        // 1. 如果需要性誉,繪制背景
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // 2. 有過有必要,保存當(dāng)前canvas茎杂。
        final int viewFlags = mViewFlags;
      
        if (!verticalEdges && !horizontalEdges) {
            // 3. 繪制View的內(nèi)容错览。
            if (!dirtyOpaque) onDraw(canvas);

            // 4. 繪制子View。
            dispatchDraw(canvas);

            drawAutofilledHighlight(canvas);

            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // 6. 繪制裝飾煌往,如滾動條等等倾哺。
            onDrawForeground(canvas);

            // we're done...
            return;
        }
    }
    
    /**
    *  1.繪制View背景
    */
    private void drawBackground(Canvas canvas) {
        //獲取背景
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }

        setBackgroundBounds();

        //獲取便宜值scrollX和scrollY,如果scrollX和scrollY都不等于0刽脖,則會在平移后的canvas上面繪制背景羞海。
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {
            background.draw(canvas);
        } else {
            canvas.translate(scrollX, scrollY);
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }
    
    /**
    * 3.繪制View的內(nèi)容,該方法是一個空的實現(xiàn),在各個業(yè)務(wù)當(dāng)中自行處理曲管。
    */
    protected void onDraw(Canvas canvas) {
    }
    
    /**
    * 4. 繪制子View却邓。該方法在View當(dāng)中是一個空的實現(xiàn),在各個業(yè)務(wù)當(dāng)中自行處理院水。
    *  在ViewGroup當(dāng)中對dispatchDraw方法做了實現(xiàn)腊徙,主要是遍歷子View,并調(diào)用子類的draw方法衙耕,一般我們不需要自己重寫該方法昧穿。
    */
    protected void dispatchDraw(Canvas canvas) {

    }
        

3. 自定義組合控件

自定義組合控件就是將多個控件組合成為一個新的控件勺远,主要解決多次重復(fù)使用同一類型的布局橙喘。如我們頂部的HeaderView以及dailog等,我們都可以把他們組合成一個新的控件胶逢。

我們通過一個自定義HeaderView實例來了解自定義組合控件的用法厅瞎。

1. 編寫布局文件

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:id="@+id/header_root_layout"
    android:layout_height="45dp"
    android:background="#827192">

    <ImageView
        android:id="@+id/header_left_img"
        android:layout_width="45dp"
        android:layout_height="45dp"
        android:layout_alignParentLeft="true"
        android:paddingLeft="12dp"
        android:paddingRight="12dp"
        android:src="@drawable/back"
        android:scaleType="fitCenter"/>

    <TextView
        android:id="@+id/header_center_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:lines="1"
        android:maxLines="11"
        android:ellipsize="end"
        android:text="title"
        android:textStyle="bold"
        android:textColor="#ffffff"/>
    
    <ImageView
        android:id="@+id/header_right_img"
        android:layout_width="45dp"
        android:layout_height="45dp"
        android:layout_alignParentRight="true"
        android:src="@drawable/add"
        android:scaleType="fitCenter"
        android:paddingRight="12dp"
        android:paddingLeft="12dp"/>

</RelativeLayout>

布局很簡單,中間是title的文字初坠,左邊是返回按鈕和簸,右邊是一個添加按鈕。

2. 實現(xiàn)構(gòu)造方法

//因為我們的布局采用RelativeLayout碟刺,所以這里繼承RelativeLayout锁保。
//關(guān)于各個構(gòu)造方法的介紹可以參考前面的內(nèi)容
public class YFHeaderView extends RelativeLayout {

    public YFHeaderView(Context context) {
        super(context);
    }

    public YFHeaderView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

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

}

3. 初始化UI

    //初始化UI,可根據(jù)業(yè)務(wù)需求設(shè)置默認值。
    private void initView(Context context) {
        LayoutInflater.from(context).inflate(R.layout.view_header, this, true);
        img_left = (ImageView) findViewById(R.id.header_left_img);
        img_right = (ImageView) findViewById(R.id.header_right_img);
        text_center = (TextView) findViewById(R.id.header_center_text);
        layout_root = (RelativeLayout) findViewById(R.id.header_root_layout);
        layout_root.setBackgroundColor(Color.BLACK);
        text_center.setTextColor(Color.WHITE);

    }
    

4. 提供對外的方法

可以根據(jù)業(yè)務(wù)需求對外暴露一些方法爽柒。

    //設(shè)置標(biāo)題文字的方法
    private void setTitle(String title) {
        if (!TextUtils.isEmpty(title)) {
            text_center.setText(title);
        }
    }
    //對左邊按鈕設(shè)置事件的方法
    private void setLeftListener(OnClickListener onClickListener) {
        img_left.setOnClickListener(onClickListener);
    }

    //對右邊按鈕設(shè)置事件的方法
    private void setRightListener(OnClickListener onClickListener) {
        img_right.setOnClickListener(onClickListener);
    }

5. 在布局當(dāng)中引用該控件

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.yf.view.YFHeaderView
        android:layout_width="match_parent"
        android:layout_height="45dp">

    </com.example.yf.view.YFHeaderView>

</LinearLayout>

到這里基本的功能已經(jīng)有了吴菠。除了這些基礎(chǔ)功能外,我們還可以做一些功能擴展浩村,比如可以在布局時設(shè)置我的View顯示的元素做葵,因為可能有些需求并不需要右邊的按鈕。這時候就需要用到自定義屬性來解決了心墅。

前面已經(jīng)簡單介紹過自定義屬性的相關(guān)知識酿矢,我們之間看代碼

1.首先在values目錄下創(chuàng)建attrs.xml

內(nèi)容如下:

<resources>

    <declare-styleable name="HeaderBar">
        <attr name="title_text_clolor" format="color"></attr>
        <attr name="title_text" format="string"></attr>
        <attr name="show_views">
            <flag name="left_text" value="0x01" />
            <flag name="left_img" value="0x02" />
            <flag name="right_text" value="0x04" />
            <flag name="right_img" value="0x08" />
            <flag name="center_text" value="0x10" />
            <flag name="center_img" value="0x20" />
        </attr>
    </declare-styleable>
</resources>

這里我們定義了三個屬性,文字內(nèi)容怎燥、顏色以及要顯示的元素瘫筐。

2.在java代碼中進行設(shè)置

    private void initAttrs(Context context, AttributeSet attrs) {
        TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.HeaderBar);
        //獲取title_text屬性
        String title = mTypedArray.getString(R.styleable.HeaderBar_title_text);
        if (!TextUtils.isEmpty(title)) {
            text_center.setText(title);
        }
        //獲取show_views屬性,如果沒有設(shè)置時默認為0x26
        showView = mTypedArray.getInt(R.styleable.HeaderBar_show_views, 0x26);
        text_center.setTextColor(mTypedArray.getColor(R.styleable.HeaderBar_title_text_clolor, Color.WHITE));
        mTypedArray.recycle();
        showView(showView);

    }
    
    private void showView(int showView) {
        //將showView轉(zhuǎn)換為二進制數(shù)铐姚,根據(jù)不同位置上的值設(shè)置對應(yīng)View的顯示或者隱藏严肪。
        Long data = Long.valueOf(Integer.toBinaryString(showView));
        element = String.format("%06d", data);
        for (int i = 0; i < element.length(); i++) {
            if(i == 0) ;
            if(i == 1) text_center.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);
            if(i == 2) img_right.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);
            if(i == 3) ;
            if(i == 4) img_left.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);
            if(i == 5) ;
        }

    }

3.在布局文件中進行設(shè)置

    <com.example.yf.view.YFHeaderView
        android:layout_width="match_parent"
        android:layout_height="45dp"
        app:title_text="標(biāo)題"
        app:show_views="center_text|left_img|right_img">

    </com.example.yf.view.YFHeaderView>

OK,到這里整個View基本定義完成谦屑。整個YFHeaderView的代碼如下

public class YFHeaderView extends RelativeLayout {

    private ImageView img_left;
    private TextView text_center;
    private ImageView img_right;
    private RelativeLayout layout_root;
    private Context context;
    String element;

    private int showView;

    public YFHeaderView(Context context) {
        super(context);
        this.context = context;
        initView(context);
    }

    public YFHeaderView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        initView(context);
        initAttrs(context, attrs);
    }

    public YFHeaderView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
        initView(context);
        initAttrs(context, attrs);
    }

    private void initAttrs(Context context, AttributeSet attrs) {
        TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.HeaderBar);
        String title = mTypedArray.getString(R.styleable.HeaderBar_title_text);
        if (!TextUtils.isEmpty(title)) {
            text_center.setText(title);
        }
        showView = mTypedArray.getInt(R.styleable.HeaderBar_show_views, 0x26);
        text_center.setTextColor(mTypedArray.getColor(R.styleable.HeaderBar_title_text_clolor, Color.WHITE));
        mTypedArray.recycle();
        showView(showView);

    }

    private void showView(int showView) {
        Long data = Long.valueOf(Integer.toBinaryString(showView));
        element = String.format("%06d", data);
        for (int i = 0; i < element.length(); i++) {
            if(i == 0) ;
            if(i == 1) text_center.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);
            if(i == 2) img_right.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);
            if(i == 3) ;
            if(i == 4) img_left.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);
            if(i == 5) ;
        }

    }

    private void initView(final Context context) {
        LayoutInflater.from(context).inflate(R.layout.view_header, this, true);
        img_left = (ImageView) findViewById(R.id.header_left_img);
        img_right = (ImageView) findViewById(R.id.header_right_img);
        text_center = (TextView) findViewById(R.id.header_center_text);
        layout_root = (RelativeLayout) findViewById(R.id.header_root_layout);
        layout_root.setBackgroundColor(Color.BLACK);
        text_center.setTextColor(Color.WHITE);

        img_left.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(context, element + "", Toast.LENGTH_SHORT).show();
            }
        });
    }

    private void setTitle(String title) {
        if (!TextUtils.isEmpty(title)) {
            text_center.setText(title);
        }
    }


    private void setLeftListener(OnClickListener onClickListener) {
        img_left.setOnClickListener(onClickListener);
    }

    private void setRightListener(OnClickListener onClickListener) {
        img_right.setOnClickListener(onClickListener);
    }

}

4. 繼承系統(tǒng)控件

繼承系統(tǒng)的控件可以分為繼承View子類(如TextVIew等)和繼承ViewGroup子類(如LinearLayout等)驳糯,根據(jù)業(yè)務(wù)需求的不同,實現(xiàn)的方式也會有比較大的差異氢橙。這里介紹一個比較簡單的酝枢,繼承自View的實現(xiàn)方式。

業(yè)務(wù)需求:為文字設(shè)置背景悍手,并在布局中間添加一條橫線帘睦。

因為這種實現(xiàn)方式會復(fù)用系統(tǒng)的邏輯表锻,大多數(shù)情況下我們希望復(fù)用系統(tǒng)的onMeaseuronLayout流程瓣蛀,所以我們只需要重寫onDraw方法 。實現(xiàn)非常簡單鉴象,話不多說滞欠,直接上代碼古胆。

public class LineTextView extends TextView {

    //定義畫筆,用來繪制中心曲線
    private Paint mPaint;
    
    /**
     * 創(chuàng)建構(gòu)造方法
     * @param context
     */
    public LineTextView(Context context) {
        super(context);
        init();
    }

    public LineTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public LineTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mPaint = new Paint();
        mPaint.setColor(Color.BLACK);
    }

    //重寫draw方法筛璧,繪制我們需要的中間線以及背景
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();
        mPaint.setColor(Color.BLUE);
        //繪制方形背景
        RectF rectF = new RectF(0,0,width,height);
        canvas.drawRect(rectF,mPaint);
        mPaint.setColor(Color.BLACK);
        //繪制中心曲線逸绎,起點坐標(biāo)(0,height/2),終點坐標(biāo)(width,height/2)
        canvas.drawLine(0,height/2,width,height/2,mPaint);
    }
}

對于View的繪制還需要對Paint()夭谤、canvas以及Path的使用有所了解棺牧,不清楚的可以稍微了解一下。

這里的實現(xiàn)比較簡單朗儒,因為具體實現(xiàn)會與業(yè)務(wù)環(huán)境密切相關(guān)颊乘,這里只是做一個參考参淹。

5. 直接繼承View

直接繼承View會比上一種實現(xiàn)方復(fù)雜一些,這種方法的使用情景下乏悄,完全不需要復(fù)用系統(tǒng)控件的邏輯承二,除了要重寫onDraw外還需要對onMeasure方法進行重寫。

我們用自定義View來繪制一個正方形纲爸。

  • 首先定義構(gòu)造方法亥鸠,以及做一些初始化操作
ublic class RectView extends View{
    //定義畫筆
    private Paint mPaint = new Paint();

    /**
     * 實現(xiàn)構(gòu)造方法
     * @param context
     */
    public RectView(Context context) {
        super(context);
        init();
    }

    public RectView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public RectView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mPaint.setColor(Color.BLUE);

    }

}
  • 重寫draw方法,繪制正方形识啦,注意對padding屬性進行設(shè)置
/**
     * 重寫draw方法
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //獲取各個編劇的padding值
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();
        //獲取繪制的View的寬度
        int width = getWidth()-paddingLeft-paddingRight;
        //獲取繪制的View的高度
        int height = getHeight()-paddingTop-paddingBottom;
        //繪制View负蚊,左上角坐標(biāo)(0+paddingLeft,0+paddingTop),右下角坐標(biāo)(width+paddingLeft,height+paddingTop)
        canvas.drawRect(0+paddingLeft,0+paddingTop,width+paddingLeft,height+paddingTop,mPaint);
    }

之前我們講到過View的measure過程颓哮,再看一下源碼對這一步的處理

    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

在View的源碼當(dāng)中并沒有對AT_MOSTEXACTLY兩個模式做出區(qū)分家妆,也就是說View在wrap_contentmatch_parent兩個模式下是完全相同的,都會是match_parent冕茅,顯然這與我們平時用的View不同伤极,所以我們要重寫onMeasure方法。

  • 重寫onMeasure方法
    /**
     * 重寫onMeasure方法
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        //處理wrap_contentde情況
        if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(300, 300);
        } else if (widthMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(300, heightSize);
        } else if (heightMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSize, 300);
        }
    }

整個自定義View的代碼如下:

public class RectView extends View {
    //定義畫筆
    private Paint mPaint = new Paint();

    /**
     * 實現(xiàn)構(gòu)造方法
     *
     * @param context
     */
    public RectView(Context context) {
        super(context);
        init();
    }

    public RectView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public RectView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mPaint.setColor(Color.BLUE);

    }

    /**
     * 重寫onMeasure方法
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(300, 300);
        } else if (widthMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(300, heightSize);
        } else if (heightMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSize, 300);
        }
    }

    /**
     * 重寫draw方法
     *
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //獲取各個編劇的padding值
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();
        //獲取繪制的View的寬度
        int width = getWidth() - paddingLeft - paddingRight;
        //獲取繪制的View的高度
        int height = getHeight() - paddingTop - paddingBottom;
        //繪制View姨伤,左上角坐標(biāo)(0+paddingLeft,0+paddingTop)哨坪,右下角坐標(biāo)(width+paddingLeft,height+paddingTop)
        canvas.drawRect(0 + paddingLeft, 0 + paddingTop, width + paddingLeft, height + paddingTop, mPaint);
    }
}

整個過程大致如下,直接繼承View時需要有幾點注意:

1乍楚、在onDraw當(dāng)中對padding屬性進行處理当编。
2、在onMeasure過程中對wrap_content屬性進行處理徒溪。
3忿偷、至少要有一個構(gòu)造方法。

6. 繼承ViewGroup

自定義ViewGroup的過程相對復(fù)雜一些臊泌,因為除了要對自身的大小和位置進行測量之外鲤桥,還需要對子View的測量參數(shù)負責(zé)。

需求實例

實現(xiàn)一個類似于Viewpager的可左右滑動的布局渠概。

代碼比較多茶凳,我們結(jié)合注釋分析。

public class HorizontaiView extends ViewGroup {

    private int lastX;
    private int lastY;

    private int currentIndex = 0;
    private int childWidth = 0;
    private Scroller scroller;
    private VelocityTracker tracker;

    
    /**
     * 1.創(chuàng)建View類高氮,實現(xiàn)構(gòu)造函數(shù)
     * 實現(xiàn)構(gòu)造方法
     * @param context
     */
    public HorizontaiView(Context context) {
        super(context);
        init(context);
    }

    public HorizontaiView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

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

    private void init(Context context) {
        scroller = new Scroller(context);
        tracker = VelocityTracker.obtain();
    }

    /**
     * 2慧妄、根據(jù)自定義View的繪制流程顷牌,重寫`onMeasure`方法剪芍,注意對wrap_content的處理
     * 重寫onMeasure方法
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //獲取寬高的測量模式以及測量值
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        //測量所有子View
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        //如果沒有子View,則View大小為0窟蓝,0
        if (getChildCount() == 0) {
            setMeasuredDimension(0, 0);
        } else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
            View childOne = getChildAt(0);
            int childWidth = childOne.getMeasuredWidth();
            int childHeight = childOne.getMeasuredHeight();
            //View的寬度=單個子View寬度*子View個數(shù)罪裹,View的高度=子View高度
            setMeasuredDimension(getChildCount() * childWidth, childHeight);
        } else if (widthMode == MeasureSpec.AT_MOST) {
            View childOne = getChildAt(0);
            int childWidth = childOne.getMeasuredWidth();
            //View的寬度=單個子View寬度*子View個數(shù)饱普,View的高度=xml當(dāng)中設(shè)置的高度
            setMeasuredDimension(getChildCount() * childWidth, heightSize);
        } else if (heightMode == MeasureSpec.AT_MOST) {
            View childOne = getChildAt(0);
            int childHeight = childOne.getMeasuredHeight();
            //View的寬度=xml當(dāng)中設(shè)置的寬度,View的高度=子View高度
            setMeasuredDimension(widthSize, childHeight);
        }
    }

    /**
     * 3状共、接下來重寫`onLayout`方法套耕,對各個子View設(shè)置位置。
     * 設(shè)置子View位置
     * @param changed
     * @param l
     * @param t
     * @param r
     * @param b
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        int left = 0;
        View child;
        for (int i = 0; i < childCount; i++) {
            child = getChildAt(i);
            if (child.getVisibility() != View.GONE) {
                childWidth = child.getMeasuredWidth();
                child.layout(left, 0, left + childWidth, child.getMeasuredHeight());
                left += childWidth;
            }
        }
    }
}

到這里我們的View布局就已經(jīng)基本結(jié)束了峡继。但是要實現(xiàn)Viewpager的效果冯袍,還需要添加對事件的處理。事件的處理流程之前我們有分析過碾牌,在制作自定義View的時候也是會經(jīng)常用到的康愤,不了解的可以參考之前的文章Android Touch事件分發(fā)超詳細解析

    /**
     * 4舶吗、因為我們定義的是ViewGroup征冷,從onInterceptTouchEvent開始。
     * 重寫onInterceptTouchEvent,對橫向滑動事件進行攔截
     * @param event
     * @return
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercrpt = false;
        //記錄當(dāng)前點擊的坐標(biāo)
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - lastX;
                int delatY = y - lastY;
                //當(dāng)X軸移動的絕對值大于Y軸移動的絕對值時誓琼,表示用戶進行了橫向滑動检激,對事件進行攔截
                if (Math.abs(deltaX) > Math.abs(delatY)) {
                    intercrpt = true;
                }
                break;
        }
        lastX = x;
        lastY = y;
        //intercrpt = true表示對事件進行攔截
        return intercrpt;
    }
    
    /**
     * 5、當(dāng)ViewGroup攔截下用戶的橫向滑動事件以后腹侣,后續(xù)的Touch事件將交付給`onTouchEvent`進行處理叔收。
     * 重寫onTouchEvent方法
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        tracker.addMovement(event);
        //獲取事件坐標(biāo)(x,y)
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - lastX;
                int delatY = y - lastY;
                //scrollBy方法將對我們當(dāng)前View的位置進行偏移
                scrollBy(-deltaX, 0);
                break;
            //當(dāng)產(chǎn)生ACTION_UP事件時,也就是我們抬起手指
            case MotionEvent.ACTION_UP:
                //getScrollX()為在X軸方向發(fā)生的便宜傲隶,childWidth * currentIndex表示當(dāng)前View在滑動開始之前的X坐標(biāo)
                //distance存儲的就是此次滑動的距離
                int distance = getScrollX() - childWidth * currentIndex;
                //當(dāng)本次滑動距離>View寬度的1/2時今穿,切換View
                if (Math.abs(distance) > childWidth / 2) {
                    if (distance > 0) {
                        currentIndex++;
                    } else {
                        currentIndex--;
                    }
                } else {
                    //獲取X軸加速度,units為單位伦籍,默認為像素蓝晒,這里為每秒1000個像素點
                    tracker.computeCurrentVelocity(1000);
                    float xV = tracker.getXVelocity();
                    //當(dāng)X軸加速度>50時,也就是產(chǎn)生了快速滑動帖鸦,也會切換View
                    if (Math.abs(xV) > 50) {
                        if (xV < 0) {
                            currentIndex++;
                        } else {
                            currentIndex--;
                        }
                    }
                }
                //對currentIndex做出限制其范圍為【0芝薇,getChildCount() - 1】
                currentIndex = currentIndex < 0 ? 0 : currentIndex > getChildCount() - 1 ? getChildCount() - 1 : currentIndex;
                //滑動到下一個View
                smoothScrollTo(currentIndex * childWidth, 0);
                tracker.clear();
                break;
        }
        lastX = x;
        lastY = y;
        return true;
    }


    private void smoothScrollTo(int destX, int destY) {
        //startScroll方法將產(chǎn)生一系列偏移量,從(getScrollX(), getScrollY())作儿,destX - getScrollX()和destY - getScrollY()為移動的距離
        scroller.startScroll(getScrollX(), getScrollY(), destX - getScrollX(), destY - getScrollY(), 1000);
        //invalidate方法會重繪View洛二,也就是調(diào)用View的onDraw方法,而onDraw又會調(diào)用computeScroll()方法
        invalidate();
    }

    //重寫computeScroll方法
    @Override
    public void computeScroll() {
        super.computeScroll();
        //當(dāng)scroller.computeScrollOffset()=true時表示滑動沒有結(jié)束
        if (scroller.computeScrollOffset()) {
            //調(diào)用scrollTo方法進行滑動攻锰,滑動到scroller當(dāng)中計算到的滑動位置
            scrollTo(scroller.getCurrX(), scroller.getCurrY());
            //沒有滑動結(jié)束晾嘶,繼續(xù)刷新View
            postInvalidate();
        }
    }

這部分代碼比較多,為了方便閱讀娶吞,在代碼當(dāng)中進行了注釋垒迂。
之后就是在XML代碼當(dāng)中引入自定義View

<com.example.yf.view.HorizontaiView
        android:id="@+id/test_layout"
        android:layout_width="match_parent"
        android:layout_height="400dp">
        <ListView
            android:id="@+id/list1"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

        </ListView>

        <ListView
            android:id="@+id/list2"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

        </ListView>

        <ListView
            android:id="@+id/list3"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

        </ListView>

    </com.example.yf.view.HorizontaiView>

好了妒蛇,可以運行看一下效果了机断。

總結(jié)

本篇文章對常用的自定義View的方式進行了總結(jié)楷拳,并簡單分析了View的繪制流程。對各種實現(xiàn)方式寫了簡單的實現(xiàn)吏奸。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末欢揖,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子奋蔚,更是在濱河造成了極大的恐慌她混,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件泊碑,死亡現(xiàn)場離奇詭異产上,居然都是意外死亡,警方通過查閱死者的電腦和手機蛾狗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門晋涣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人沉桌,你說我怎么就攤上這事谢鹊。” “怎么了留凭?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵佃扼,是天一觀的道長。 經(jīng)常有香客問我蔼夜,道長兼耀,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任求冷,我火速辦了婚禮瘤运,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘匠题。我一直安慰自己拯坟,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布韭山。 她就那樣靜靜地躺著郁季,像睡著了一般。 火紅的嫁衣襯著肌膚如雪钱磅。 梳的紋絲不亂的頭發(fā)上梦裂,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機與錄音盖淡,去河邊找鬼年柠。 笑死,一個胖子當(dāng)著我的面吹牛禁舷,可吹牛的內(nèi)容都是我干的彪杉。 我是一名探鬼主播毅往,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼牵咙,長吁一口氣:“原來是場噩夢啊……” “哼派近!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起洁桌,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤渴丸,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后另凌,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谱轨,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年吠谢,在試婚紗的時候發(fā)現(xiàn)自己被綠了土童。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡工坊,死狀恐怖献汗,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情王污,我是刑警寧澤罢吃,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站昭齐,受9級特大地震影響尿招,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜阱驾,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一就谜、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧里覆,春花似錦吁伺、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至割去,卻和暖如春窟却,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背呻逆。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工夸赫, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人咖城。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓茬腿,卻偏偏與公主長得像呼奢,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子切平,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容