android自定義view【控件篇】

kotlin語法總結

自定義屬性與自定義style

假如我們想要自定義textview 的屬性
先在res/value 下創(chuàng)建文件 attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="MyTextView">

        <attr name="header" format="reference"/>
        <attr name="headerHeight" format="dimension"/>
        <attr name="headerVisibleHeight" format="dimension"/>

        <attr name="age" >
            <flag name="child" value="10"/>
            <flag name="young" value="18"/>
            <flag name="old" value="60"/>
        </attr>

    </declare-styleable>

</resources>

然后在xml根布局中添加命名空間 這里的MyTextView可以定義為任何名字xmlns:MyTextView="http://schemas.android.com/apk/res-auto"
接著就可以在具體的自定義view標簽中使用 MyTextView: 屬性名 = 具體的值

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:MyTextView="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical">

<com.bhb.cutomview.view.View12
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    MyTextView:header = "@mipmap/mgirl"
    MyTextView:headerVisibleHeight = "100dp"
    MyTextView:headerHeight = "300dp"
    MyTextView:age = "young"
    />

</LinearLayout>

接下來在代碼中 使用TypedArray獲取屬性的值

class View12: TextView {


    constructor(context: Context?, attrs: AttributeSet?):super(context, attrs){

        //  傳入attr.xml 文件中定義的Styleable名稱
        var typedArray = context?.obtainStyledAttributes(attrs , R.styleable.MyTextView)

        // 根據(jù)屬性的不同format  調(diào)用TypedArray不同的函數(shù)獲取對應的值
        var headerHeight = typedArray?.getDimension(R.styleable.MyTextView_headerHeight, -1f);
        var age = typedArray?.getInt(R.styleable.MyTextView_age , -1)

        //獲取完畢之后需要釋放資源
        typedArray?.recycle()

        setText("headerHeight: $headerHeight  age: $age")

    }

}

效果如圖所示


自定義屬性.jpg

自定義屬性的format類型

轉(zhuǎn)自 https://blog.csdn.net/dj0379/article/details/49662161
1. reference:參考某一資源ID顶滩。

(1)屬性定義:

        <declare-styleable name = "名稱">

               <attr name = "background" format = "reference" />

        </declare-styleable>

(2)屬性使用:

         <ImageView

                 android:layout_width = "42dip"
                 android:layout_height = "42dip"
                 android:background = "@drawable/圖片ID"

                 />

2. color:顏色值定嗓。

(1)屬性定義:

        <declare-styleable name = "名稱">

               <attr name = "textColor" format = "color" />

        </declare-styleable>

(2)屬性使用:

        <TextView

                 android:layout_width = "42dip"
                 android:layout_height = "42dip"
                 android:textColor = "#00FF00"

                 />

3. boolean:布爾值蒿讥。

(1)屬性定義:

        <declare-styleable name = "名稱">

               <attr name = "focusable" format = "boolean" />

        </declare-styleable>

(2)屬性使用:

        <Button

                android:layout_width = "42dip"
                android:layout_height = "42dip"

                android:focusable = "true"

                />

4. dimension:尺寸值囚聚。

(1)屬性定義:

        <declare-styleable name = "名稱">

               <attr name = "layout_width" format = "dimension" />

        </declare-styleable>

(2)屬性使用:

        <Button

                android:layout_width = "42dip"
                android:layout_height = "42dip"

                />

5. float:浮點值潮罪。

(1)屬性定義:

        <declare-styleable name = "AlphaAnimation">

               <attr name = "fromAlpha" format = "float" />
               <attr name = "toAlpha" format = "float" />

        </declare-styleable>

(2)屬性使用:

        <alpha
               android:fromAlpha = "1.0"
               android:toAlpha = "0.7"

               />

6. integer:整型值螃壤。

(1)屬性定義:

        <declare-styleable name = "AnimatedRotateDrawable">

               <attr name = "visible" />
               <attr name = "frameDuration" format="integer" />
               <attr name = "framesCount" format="integer" />
               <attr name = "pivotX" />
               <attr name = "pivotY" />
               <attr name = "drawable" />

        </declare-styleable>

(2)屬性使用:

        <animated-rotate

               xmlns:android = "[http://schemas.android.com/apk/res/android](http://schemas.android.com/apk/res/android)"  
               android:drawable = "@drawable/圖片ID"  
               android:pivotX = "50%"  
               android:pivotY = "50%"  
               android:framesCount = "12"  
               android:frameDuration = "100"

               />

7. string:字符串描扯。

(1)屬性定義:

        <declare-styleable name = "MapView">
               <attr name = "apiKey" format = "string" />
        </declare-styleable>

(2)屬性使用:

        <com.google.android.maps.MapView
                android:layout_width = "fill_parent"
                android:layout_height = "fill_parent"
                android:apiKey = "0jOkQ80oD1JL9C6HAja99uGXCRiS2CGjKO_bc_g"

                />

8. fraction:百分數(shù)盯滚。

(1)屬性定義:

        <declare-styleable name="RotateDrawable">
               <attr name = "visible" />
               <attr name = "fromDegrees" format = "float" />
               <attr name = "toDegrees" format = "float" />
               <attr name = "pivotX" format = "fraction" />
               <attr name = "pivotY" format = "fraction" />
               <attr name = "drawable" />
        </declare-styleable>

(2)屬性使用:

        <rotate

               xmlns:android = "[http://schemas.android.com/apk/res/android](http://schemas.android.com/apk/res/android)" 

android:interpolator = "@anim/動畫ID"

               android:fromDegrees = "0" 

android:toDegrees = "360"

               android:pivotX = "200%"

               android:pivotY = "300%" 

android:duration = "5000"

               android:repeatMode = "restart"

               android:repeatCount = "infinite"

               />

9. enum:枚舉值踢械。

(1)屬性定義:

        <declare-styleable name="名稱">
               <attr name="orientation">
                      <enum name="horizontal" value="0" />
                      <enum name="vertical" value="1" />
               </attr>            

        </declare-styleable>

(2)屬性使用:

        <LinearLayout

                xmlns:android = "[http://schemas.android.com/apk/res/android](http://schemas.android.com/apk/res/android)"
                android:orientation = "vertical"
                android:layout_width = "fill_parent"
                android:layout_height = "fill_parent"
                >
        </LinearLayout>

10. flag:位或運算。

 (1)屬性定義:

         <declare-styleable name="名稱">
                <attr name="windowSoftInputMode">
                        <flag name = "stateUnspecified" value = "0" />
                        <flag name = "stateUnchanged" value = "1" />
                        <flag name = "stateHidden" value = "2" />
                        <flag name = "stateAlwaysHidden" value = "3" />
                        <flag name = "stateVisible" value = "4" />
                        <flag name = "stateAlwaysVisible" value = "5" />
                        <flag name = "adjustUnspecified" value = "0x00" />
                        <flag name = "adjustResize" value = "0x10" />
                        <flag name = "adjustPan" value = "0x20" />
                        <flag name = "adjustNothing" value = "0x30" />
                 </attr>         

         </declare-styleable>

 (2)屬性使用:

        <activity

               android:name = ".StyleAndThemeActivity"
               android:label = "@string/app_name"
               android:windowSoftInputMode = "stateUnspecified | stateUnchanged | stateHidden">
               <intent-filter>
                      <action android:name = "android.intent.action.MAIN" />
                      <category android:name = "android.intent.category.LAUNCHER" />
               </intent-filter>
         </activity>

 注意:

 屬性定義時可以指定多種類型值魄藕。

(1)屬性定義:

        <declare-styleable name = "名稱">

               <attr name = "background" format = "reference|color" />

        </declare-styleable>

(2)屬性使用:

         <ImageView

                 android:layout_width = "42dip"
                 android:layout_height = "42dip"
                 android:background = "@drawable/圖片ID|#00FF00"

                 />

測量與布局

ViewGroup繪制流程

onMeasure 測量當前控件的大小
onLayuot 使用layout函數(shù)對所有子控件進行布局
onDraw 根據(jù)布局的位置繪圖

onMeasure函數(shù)與MeasureSpec

onMeasure 函數(shù)測量完成之后 要通過void setMeasuredDimension(int measuredWidth, int measuredHeight) 函數(shù)設置給系統(tǒng) 給onLayuot函數(shù)提供數(shù)值

MeasureSpec的組成
measuredWidth 和 measuredHeight 都是int 類型 在內(nèi)存中都是32位
前兩位代表mode 后30位代表數(shù)值
有三種模式
未指定 UNSPECIFIED = 0 << MODE_SHIFT; 子元素可以得到任意想要的大小
確切的值 EXACTLY = 1 << MODE_SHIFT; 子元素有確定的大小
最多 AT_MOST = 2 << MODE_SHIFT; 子元素可以得到最大的大小

其中MODE_SHIFT = 30
也就是三種模式的二進制值分別是 0 1 2 向左移30位 即
00 0000000000 0000000000 0000000000
01 0000000000 0000000000 0000000000
10 0000000000 0000000000 0000000000

模式提取
MeasureSpec 提供了函數(shù)獲取measuredWidth或者measuredHeight 的模式

 public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
二進制值為 
11 0000000000 0000000000 0000000000

數(shù)值提取
MeasureSpec 也提供了函數(shù)

 public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
~MODE_MASK  二進制值為
00 1111111111 1111111111 1111111111

int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
模式與數(shù)值主要用到了 與 非 運算

xml 中定義的大小 對應的模式

xml定義 對應的模式 描述
wrap_content AT_MOST 最多得到父控件的大小
match_parent EXACTLY 得到的是父控件已經(jīng)確定的大小
具體的值 EXACTLY 指定的大小

UNSPECIFIED 基本用不到

onLayout 函數(shù)

void onLayout(boolean changed, int left, int top, int right, int bottom)
在ViewGroup中這是一個抽象函數(shù) 說明凡事派生自ViewGoup 的類都必須自己去實現(xiàn)這個函數(shù)
像 LinearLayout RelativeLayout 等布局都重寫了這個函數(shù) 然后在按照自己的規(guī)則對子視圖進行布局

示例 自定義LinearLayout

自定義 縱向顯示的LinearLayout

class View12LinearLayout : ViewGroup {

    constructor(context: Context?, attrs: AttributeSet?):super(context, attrs){

    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val widthMode = MeasureSpec.getMode(widthMeasureSpec)
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)
        val widthSize = MeasureSpec.getSize(widthMeasureSpec)
        val heightSize = MeasureSpec.getSize(heightMeasureSpec)

        var height = 0
        var width = 0
        for (index in 0 until childCount){
            // 測量子控件
            var child = getChildAt(index)
            measureChild(child , widthMeasureSpec , heightMeasureSpec)
            var childHeight = child.measuredHeight
            var childWidth = child.measuredWidth
            // 高度累加  寬度取最大值
            height += childHeight
            width = Math.max(childWidth , width)
        }

        // 告訴系統(tǒng)計算完的值
        setMeasuredDimension( if(widthMode == MeasureSpec.EXACTLY) widthSize else width ,
            if(heightMode == MeasureSpec.EXACTLY) heightSize else height)
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        var top = 0
        var count = childCount
        for (index in 0 until count){

            var child = getChildAt(index)
            var childheight = child.measuredHeight
            var childwidth = child.measuredWidth

            // 遍歷每一個ziview   然后設置其布局位置
            // 因為是縱向排列 x起點是0  y起點累加子view的高度
            child.layout(0 ,top ,childwidth , top+childheight)
            top += childheight
        }
    }


}

xml布局


<com.bhb.cutomview.view.View12LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:text = "第一個view"
        android:background="#ff0000"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <TextView
        android:text = "第二個view"
        android:background="#ff00ff"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <TextView
        android:text = "第三個view"
        android:background="#ee3345"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</com.bhb.cutomview.view.View12LinearLayout>
獲取子view控件的 margin

現(xiàn)在我們把第二個view 設置margin值 運行起來之后發(fā)現(xiàn)并沒有效果
這是因為測量和布局都是我們自己實現(xiàn)的沒有根據(jù)margin布局

 <TextView
        android:text = "第二個view"
        android:layout_marginTop="10dp"
        android:layout_marginBottom="20dp"
        android:background="#ff00ff"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

接下來我們需要重寫三個函數(shù)

 override fun generateLayoutParams(p: LayoutParams?): LayoutParams {
        return MarginLayoutParams(p)
    }

    override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
        return MarginLayoutParams(context , attrs)
    }

    override fun generateDefaultLayoutParams(): LayoutParams {
        return MarginLayoutParams(LayoutParams.MATCH_PARENT , LayoutParams.MATCH_PARENT)
    }

然后修改 onMeasure 和 onLayout 的部分代碼

onMeasure 修改 
    for (index in 0 until childCount){
            // 測量子控件
            var child = getChildAt(index)

//            獲取margin
            var lp = child.layoutParams as MarginLayoutParams

            measureChild(child , widthMeasureSpec , heightMeasureSpec)

            // 計算高度加上 margintop  和 marginbottom
            var childHeight = child.measuredHeight + lp.topMargin + lp.bottomMargin
            var childWidth = child.measuredWidth + lp.leftMargin + lp.rightMargin
            // 高度累加  寬度取最大值
            height += childHeight
            width = Math.max(childWidth , width)
        }


onLayout 修改
 for (index in 0 until count){

            var child = getChildAt(index)

            //            獲取margin
            var lp = child.layoutParams as MarginLayoutParams

            var childheight = child.measuredHeight
            var childwidth = child.measuredWidth + lp.leftMargin + lp.rightMargin

            // 遍歷每一個ziview   然后設置其布局位置
            // 因為是縱向排列 x起點是0  y起點累加子view的高度
            // 布局位置 以及子view高度  根據(jù)margin計算
            child.layout(0 ,top + lp.topMargin  ,childwidth , top + lp.topMargin +childheight )
            top += childheight  + lp.topMargin + lp.bottomMargin
        }

margin布局.jpg

為什么要把 child.layoutParams 強轉(zhuǎn)成 MarginLayoutParams
首先container初始化子控件時内列, 會調(diào)用generateLayoutParams 函數(shù)來為子控件生成對應的布局屬性 但默認只生成 layout_width 和 layout_height , 即正常情況下調(diào)用generateLayoutParams函數(shù)生成的layoutparams 實例是不能獲取到 margin的

如果我們還需要與margin相關的參數(shù)就只能重寫generateLayoutParams函數(shù)

Viewgroup源碼

public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }

  public LayoutParams(Context c, AttributeSet attrs) {
            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
            setBaseAttributes(a,
                    R.styleable.ViewGroup_Layout_layout_width,
                    R.styleable.ViewGroup_Layout_layout_height);
            a.recycle();
        }

 protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
            width = a.getLayoutDimension(widthAttr, "layout_width");
            height = a.getLayoutDimension(heightAttr, "layout_height");
        }

可以看出generateLayoutParams生成的layoutparams最終是由setBaseAttributes決定的
而setBaseAttributes函數(shù)值獲取了 layout_width 和 layout_height

MarginLayoutParams 源碼

public MarginLayoutParams(Context c, AttributeSet attrs) {
            super();

            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
            setBaseAttributes(a,
                    R.styleable.ViewGroup_MarginLayout_layout_width,
                    R.styleable.ViewGroup_MarginLayout_layout_height);

            int margin = a.getDimensionPixelSize(
                    com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1);
            if (margin >= 0) {
                leftMargin = margin;
                topMargin = margin;
                rightMargin= margin;
                bottomMargin = margin;
            } else {
                int horizontalMargin = a.getDimensionPixelSize(
                        R.styleable.ViewGroup_MarginLayout_layout_marginHorizontal, -1);
                int verticalMargin = a.getDimensionPixelSize(
                        R.styleable.ViewGroup_MarginLayout_layout_marginVertical, -1);

                if (horizontalMargin >= 0) {
                    leftMargin = horizontalMargin;
                    rightMargin = horizontalMargin;
                } else {
                    leftMargin = a.getDimensionPixelSize(
                            R.styleable.ViewGroup_MarginLayout_layout_marginLeft,
                            UNDEFINED_MARGIN);
                    if (leftMargin == UNDEFINED_MARGIN) {
                        mMarginFlags |= LEFT_MARGIN_UNDEFINED_MASK;
                        leftMargin = DEFAULT_MARGIN_RESOLVED;
                    }
                    rightMargin = a.getDimensionPixelSize(
                            R.styleable.ViewGroup_MarginLayout_layout_marginRight,
                            UNDEFINED_MARGIN);
                    if (rightMargin == UNDEFINED_MARGIN) {
                        mMarginFlags |= RIGHT_MARGIN_UNDEFINED_MASK;
                        rightMargin = DEFAULT_MARGIN_RESOLVED;
                    }
                }

                startMargin = a.getDimensionPixelSize(
                        R.styleable.ViewGroup_MarginLayout_layout_marginStart,
                        DEFAULT_MARGIN_RELATIVE);
                endMargin = a.getDimensionPixelSize(
                        R.styleable.ViewGroup_MarginLayout_layout_marginEnd,
                        DEFAULT_MARGIN_RELATIVE);

                if (verticalMargin >= 0) {
                    topMargin = verticalMargin;
                    bottomMargin = verticalMargin;
                } else {
                    topMargin = a.getDimensionPixelSize(
                            R.styleable.ViewGroup_MarginLayout_layout_marginTop,
                            DEFAULT_MARGIN_RESOLVED);
                    bottomMargin = a.getDimensionPixelSize(
                            R.styleable.ViewGroup_MarginLayout_layout_marginBottom,
                            DEFAULT_MARGIN_RESOLVED);
                }

                if (isMarginRelative()) {
                   mMarginFlags |= NEED_RESOLUTION_MASK;
                }
            }

            final boolean hasRtlSupport = c.getApplicationInfo().hasRtlSupport();
            final int targetSdkVersion = c.getApplicationInfo().targetSdkVersion;
            if (targetSdkVersion < JELLY_BEAN_MR1 || !hasRtlSupport) {
                mMarginFlags |= RTL_COMPATIBILITY_MODE_MASK;
            }

            // Layout direction is LTR by default
            mMarginFlags |= LAYOUT_DIRECTION_LTR;

            a.recycle();
        }

可以看見 MarginLayoutParams獲取了margin 屬性 并且優(yōu)先獲取margin 其次才是 marginleft marginright等屬性

實現(xiàn)FlowLayout 布局

給子view設置通用的style style.xml文件下添加代碼

 <style name="text_flag_01">

        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:layout_margin">10dp</item>
        <item name="android:background">#ff0000</item>
        <item name="android:textColor">#ffffff</item>
    </style>

布局文件代碼

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:MyTextView="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical">

<com.bhb.cutomview.view.View12FlowLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:text = "第一個view"
        style="@style/text_flag_01"/>


    <TextView
        android:text = "第二個view"
        style="@style/text_flag_01"/>

    <TextView
        android:text = "第三個view"
        style="@style/text_flag_01"/>

    <TextView
        android:text = "第四個view"
        style="@style/text_flag_01"/>

    <TextView
        android:text = "第五個view"
        style="@style/text_flag_01"/>

    <TextView
        android:text = "第六個view"
        style="@style/text_flag_01"/>

    <TextView
        android:text = "第七個view"
        style="@style/text_flag_01"/>

    <TextView
        android:text = "第八個view"
        style="@style/text_flag_01"/>

    <TextView
        android:text = "第九個view"
        style="@style/text_flag_01"/>

</com.bhb.cutomview.view.View12FlowLayout>

</LinearLayout>

自定義view 代碼

package com.bhb.cutomview.view

import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup

/**
 *  create by BHB on 7/10/21
 *    自定義 FlowLayout 布局
 *
 */

//在xml標簽上添加margin屬性 發(fā)現(xiàn)運行之后沒有效果  這是因為測量和布局都是我們自己實現(xiàn)的沒有根據(jù)margin布局

class View12FlowLayout : ViewGroup {

    constructor(context: Context?, attrs: AttributeSet?):super(context, attrs){

    }

    override fun generateLayoutParams(p: LayoutParams?): LayoutParams {
        return MarginLayoutParams(p)
    }

    override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
        return MarginLayoutParams(context , attrs)
    }

    override fun generateDefaultLayoutParams(): LayoutParams {
        return MarginLayoutParams(LayoutParams.MATCH_PARENT , LayoutParams.MATCH_PARENT)
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val widthMode = MeasureSpec.getMode(widthMeasureSpec)
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)
        val widthSize = MeasureSpec.getSize(widthMeasureSpec)
        val heightSize = MeasureSpec.getSize(heightMeasureSpec)

        var lineWidth = 0   // 當前行的寬度
        var lineHeight = 0  // 當前行的高度
        var width = 0    // 控件總寬度
        var height = 0  //控件總高度

        for(i in 0 until childCount){
            var child = getChildAt(i)
            // 先測量子view 才能獲取寬度 高度
            measureChild(child , widthMeasureSpec , heightMeasureSpec)
            var lp = child.layoutParams as MarginLayoutParams
            var childWidth = child.measuredWidth + lp.leftMargin + lp.rightMargin
            var childHeight = child.measuredHeight + lp.topMargin + lp.bottomMargin

            // 繪制第一個view 的時候  height  和lineheight 的值就是  子view的高度
            if(i == 0){
                height = childHeight
                lineHeight = childHeight
            }
            if(lineWidth + childWidth > widthSize){
                //需要換行
                width = Math.max(lineWidth , width)
                height+= lineHeight

                //換行之后 當前行的寬高 就是當前子view的寬高
                lineHeight += childHeight
                lineWidth = childWidth
            }else{
                //在當前行繼續(xù)排列
                lineHeight = Math.max(lineHeight , childHeight)
                lineWidth += childWidth
            }

            setMeasuredDimension( if(widthMode == MeasureSpec.EXACTLY) widthSize else width ,
                if(heightMode == MeasureSpec.EXACTLY) heightSize else height)

        }


    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        var top = 0
        var left = 0
        var lineWith = 0
        var lineHeight = 0
        var count = childCount
        for (index in 0 until count){

            var child = getChildAt(index)

            //            獲取margin
            var lp = child.layoutParams as MarginLayoutParams

            var childheight = child.measuredHeight + lp.topMargin + lp.bottomMargin
            var childwidth = child.measuredWidth + lp.leftMargin + lp.rightMargin

            if(childwidth + lineWith > measuredWidth){
                top += lineHeight
                left = 0

                lineHeight  = childheight
                lineWith = childwidth
            }else{
                lineHeight = Math.max(lineHeight , childheight)
                lineWith += childwidth
            }

            //計算childview 的left top right bottom
            var lc = left + lp.leftMargin
            var tc = top + lp.topMargin
            var rc = lc + child.measuredWidth
            var bc = tc + child.measuredHeight

            child.layout(lc, tc , rc , bc )

            //將left 置為下一個子控件的起點
            left += childwidth
        }
    }


}

顯示效果


flowlayout.jpg
GestureDetector 手勢檢測

我們知道 View類有一個 onTouchListener 內(nèi)部接口 通過重寫它的 onTouch函數(shù)可以處理一些touch事件
但是這個函數(shù)太過簡單 如果需要處理一些復雜的手勢 使用這個接口就會很麻煩
android 給我們提供了GestureDetector 這個類可以識別很多手勢 在識別出手勢之后 具體的業(yè)務邏輯由開發(fā)者自己實現(xiàn)

class MainActivity13 : AppCompatActivity(), View.OnTouchListener{


    lateinit var gestureDetector :GestureDetector
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main13)

        // 設置監(jiān)聽  構造函數(shù)有多個
        gestureDetector = GestureDetector(GestureListener())
        gestureDetector.setOnDoubleTapListener(DoubleTapListener())

        // GestureDetector 還有一個監(jiān)聽函數(shù)
        gestureDetector = GestureDetector(object : GestureDetector.SimpleOnGestureListener() {
            //這里包含了GestureListener 和  DoubleTapListener 的所有函數(shù)
//            需要就進行重寫  不需要則不處理

            override fun onDown(e: MotionEvent?): Boolean {
                return super.onDown(e)
            }

            override fun onDoubleTap(e: MotionEvent?): Boolean {
                return super.onDoubleTap(e)
            }

            // 判斷用戶是左滑 還是 右滑
            // 判斷標準  用戶向左滑動距離超100像素 且移動速度超過200像素/秒 即認為是左滑  右滑同理
            override fun onFling(
                e1: MotionEvent?,
                e2: MotionEvent?,
                velocityX: Float,
                velocityY: Float
            ): Boolean {
                if(e1 == null || e2 == null){
                    return super.onFling(e1, e2, velocityX, velocityY)
                }
                Log.e("view13" , "velocityX =====  $velocityX")

                if( (e1.x - e2.x )> 100  && Math.abs(Math.abs(velocityX)) > 200 ){
                    Log.e("view13" , "fling left")
                }else if((e2.x - e1.x )> 100  && Math.abs(Math.abs(velocityX)) > 200 ){
                    Log.e("view13" , "fling right")
                }
                return true
            }

        })

        textview.setOnTouchListener(this)
        textview.isFocusable = true
        textview.isClickable = true
        textview.isLongClickable = true

    }

    // 將事件交給GestureDetector 處理
    override fun onTouch(v: View?, event: MotionEvent?): Boolean {
        return gestureDetector.onTouchEvent(event)
    }


    class GestureListener : GestureDetector.OnGestureListener{
        override fun onShowPress(e: MotionEvent?) {
           // 按壓事件  且按下的時間超過瞬間
            Log.e("view13", "onShowPress")
        }

        override fun onLongPress(e: MotionEvent?) {
            // 長按事件
            Log.e("view13", "onLongPress")
        }

        override fun onSingleTapUp(e: MotionEvent?): Boolean {
            // 一次單獨的輕擊抬起事件
            Log.e("view13", "onSingleTapUp")
            return true
        }

        override fun onDown(e: MotionEvent?): Boolean {
            // 按下屏幕就會觸發(fā)
            Log.e("view13", "onDown")
            return true
        }

        override fun onFling(
            e1: MotionEvent?,
            e2: MotionEvent?,
            velocityX: Float,
            velocityY: Float
        ): Boolean {
            // 滑屏事件   用戶按下 滑動后在松開
            Log.e("view13", "onFling")
            return true
        }

        override fun onScroll(
            e1: MotionEvent?,
            e2: MotionEvent?,
            distanceX: Float,
            distanceY: Float
        ): Boolean {
            // 滑動事件  在屏幕上拖動控件或者以拋的動作 都會多次觸發(fā)
            Log.e("view13", "onScroll")
            return true
        }

    }

    class DoubleTapListener : GestureDetector.OnDoubleTapListener{
        override fun onDoubleTap(e: MotionEvent?): Boolean {
            // 雙擊事件
            Log.e("view13", "onDoubleTap")
            return true
        }

        override fun onDoubleTapEvent(e: MotionEvent?): Boolean {
            // 雙擊間隔中發(fā)生的事件  包含 down up move
            Log.e("view13", "onDoubleTapEvent")
            return true
        }

        override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
            // 用于判斷是否是單擊還是雙擊事件
            Log.e("view13", "onSingleTapConfirmed")
            return true        }

    }

}

Window 與 WindowManager

WindowManager 可通過getSystemService(Context.WINDOW_SERVICE)
WindowManager.addView(View view, ViewGroup.LayoutParams params) 可向界面添加view
WindowManager.LayoutParams 構造函數(shù)
public LayoutParams(int w, int h, int _type, int _flags, int _format)
type 用來表示 Window 的類型
window 有三種類型
應用window 1-99
子window 1000-1999
系統(tǒng)window 2000-2999
層級大的window會覆蓋在層級小的window上面

_flags 用來控制window 的顯示特性  常用的幾個選項
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL     表示window區(qū)域內(nèi)的事件自己處理 區(qū)域外的時間傳遞給底層的window處理
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE       表示window不需要獲取焦點 不接收各種輸入事件
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED    讓window顯示在鎖屏上


// 不設置這個彈出框的透明遮罩顯示為黑色
params.format = PixelFormat.TRANSLUCENT;
class MainActivity13Windowmanager : AppCompatActivity(), View.OnClickListener,
    View.OnTouchListener {

    // 延遲初始化變量
    lateinit var mImageView : ImageView
    lateinit var mLayoutparams : WindowManager.LayoutParams
    lateinit var mWindowManager : WindowManager


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main13windowmanager)

        // 清單文件添加權限 與 申請權限
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
            var intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
            startActivityForResult(intent ,100)
        }else{
            init()
        }

    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if(requestCode == 100){
            init()
        }
    }

    fun init(){
        btn_add.setOnClickListener(this)
        btn_remove.setOnClickListener(this)

        mWindowManager = applicationContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager
    }


    override fun onClick(v: View?) {

        when (v?.id){
            R.id.btn_add ->{
                // 添加控件
                mImageView = ImageView(this)
                mImageView.setBackgroundResource(R.mipmap.ic_launcher)
                mLayoutparams = WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT ,WindowManager.LayoutParams.WRAP_CONTENT ,
                    2099 ,
                    WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED ,
                    PixelFormat.TRANSPARENT)

                mLayoutparams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR

                // 設置初始位置
                mLayoutparams.gravity = Gravity.TOP or Gravity.LEFT
                mLayoutparams.x = 0
                mLayoutparams.y = 300

                mImageView.setOnTouchListener(this)
                mWindowManager.addView(mImageView , mLayoutparams)
            }

            R.id.btn_remove ->{
                // 移除控件
                mImageView?.let {
                    mWindowManager.removeViewImmediate(mImageView)
                }
            }
        }
    }

    override fun onTouch(v: View?, event: MotionEvent?): Boolean {
        //監(jiān)聽觸摸事件 讓添加的view 跟隨手指移動
        event?.let {
            mImageView?.let {
                var rawX = event.rawX
                var rawY = event.rawY
                when (event.action){
                    MotionEvent.ACTION_MOVE -> {
                        mLayoutparams.x = rawX.toInt() - mImageView.width/2
                        mLayoutparams.y = rawY.toInt() - mImageView.height
                        mWindowManager.updateViewLayout(mImageView , mLayoutparams)
                    }

                }
            }

        }
        return false
    }

}
類似小火箭控件.gif
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末背率,一起剝皮案震驚了整個濱河市话瞧,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌寝姿,老刑警劉巖交排,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異饵筑,居然都是意外死亡埃篓,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門根资,熙熙樓的掌柜王于貴愁眉苦臉地迎上來架专,“玉大人同窘,你說我怎么就攤上這事〔拷牛” “怎么了想邦?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長委刘。 經(jīng)常有香客問我丧没,道長,這世上最難降的妖魔是什么钱雷? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任骂铁,我火速辦了婚禮,結果婚禮上罩抗,老公的妹妹穿的比我還像新娘拉庵。我一直安慰自己,他們只是感情好套蒂,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布钞支。 她就那樣靜靜地躺著,像睡著了一般操刀。 火紅的嫁衣襯著肌膚如雪烁挟。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天骨坑,我揣著相機與錄音撼嗓,去河邊找鬼。 笑死欢唾,一個胖子當著我的面吹牛且警,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播礁遣,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼斑芜,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了祟霍?” 一聲冷哼從身側響起杏头,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎沸呐,沒想到半個月后醇王,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡崭添,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年厦画,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片滥朱。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡根暑,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出徙邻,到底是詐尸還是另有隱情排嫌,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布缰犁,位于F島的核電站淳地,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏帅容。R本人自食惡果不足惜颇象,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望并徘。 院中可真熱鬧遣钳,春花似錦、人聲如沸麦乞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽姐直。三九已至倦淀,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間声畏,已是汗流浹背撞叽。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留插龄,地道東北人愿棋。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像辫狼,于是被迫代替她去往敵國和親初斑。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

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