android自定义view【控件篇】

kotlin语法总结

自定义属性与自定义style

假如我们想要自定义textview 的属性
先在res/value 下创建文件 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)

        // 根据属性的不同format  调用TypedArray不同的函数获取对应的值
        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类型

转自 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:百分数。

(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函数对所有子控件进行布局
onDraw 根据布局的位置绘图

onMeasure函数与MeasureSpec

onMeasure 函数测量完成之后 要通过void setMeasuredDimension(int measuredWidth, int measuredHeight) 函数设置给系统 给onLayuot函数提供数值

MeasureSpec的组成
measuredWidth 和 measuredHeight 都是int 类型 在内存中都是32位
前两位代表mode 后30位代表数值
有三种模式
未指定 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 提供了函数获取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

数值提取
MeasureSpec 也提供了函数

 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);
模式与数值主要用到了 与 非 运算

xml 中定义的大小 对应的模式

xml定义 对应的模式 描述
wrap_content AT_MOST 最多得到父控件的大小
match_parent EXACTLY 得到的是父控件已经确定的大小
具体的值 EXACTLY 指定的大小

UNSPECIFIED 基本用不到

onLayout 函数

void onLayout(boolean changed, int left, int top, int right, int bottom)
在ViewGroup中这是一个抽象函数 说明凡事派生自ViewGoup 的类都必须自己去实现这个函数
像 LinearLayout RelativeLayout 等布局都重写了这个函数 然后在按照自己的规则对子视图进行布局

示例 自定义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)
        }

        // 告诉系统计算完的值
        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

现在我们把第二个view 设置margin值 运行起来之后发现并没有效果
这是因为测量和布局都是我们自己实现的没有根据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"/>

接下来我们需要重写三个函数

 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高度  根据margin计算
            child.layout(0 ,top + lp.topMargin  ,childwidth , top + lp.topMargin +childheight )
            top += childheight  + lp.topMargin + lp.bottomMargin
        }

margin布局.jpg

为什么要把 child.layoutParams 强转成 MarginLayoutParams
首先container初始化子控件时, 会调用generateLayoutParams 函数来为子控件生成对应的布局属性 但默认只生成 layout_width 和 layout_height , 即正常情况下调用generateLayoutParams函数生成的layoutparams 实例是不能获取到 margin的

如果我们还需要与margin相关的参数就只能重写generateLayoutParams函数

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函数值获取了 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 属性 并且优先获取margin 其次才是 marginleft marginright等属性

实现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属性 发现运行之后没有效果  这是因为测量和布局都是我们自己实现的没有根据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{
                //在当前行继续排列
                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 内部接口 通过重写它的 onTouch函数可以处理一些touch事件
但是这个函数太过简单 如果需要处理一些复杂的手势 使用这个接口就会很麻烦
android 给我们提供了GestureDetector 这个类可以识别很多手势 在识别出手势之后 具体的业务逻辑由开发者自己实现

class MainActivity13 : AppCompatActivity(), View.OnTouchListener{


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

        // 设置监听  构造函数有多个
        gestureDetector = GestureDetector(GestureListener())
        gestureDetector.setOnDoubleTapListener(DoubleTapListener())

        // GestureDetector 还有一个监听函数
        gestureDetector = GestureDetector(object : GestureDetector.SimpleOnGestureListener() {
            //这里包含了GestureListener 和  DoubleTapListener 的所有函数
//            需要就进行重写  不需要则不处理

            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 {
            // 按下屏幕就会触发
            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 {
            // 滑动事件  在屏幕上拖动控件或者以抛的动作 都会多次触发
            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 {
            // 双击间隔中发生的事件  包含 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 构造函数
public LayoutParams(int w, int h, int _type, int _flags, int _format)
type 用来表示 Window 的类型
window 有三种类型
应用window 1-99
子window 1000-1999
系统window 2000-2999
层级大的window会覆盖在层级小的window上面

_flags 用来控制window 的显示特性  常用的几个选项
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL     表示window区域内的事件自己处理 区域外的时间传递给底层的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 {
        //监听触摸事件 让添加的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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容