kotlin语法总结
自定义属性与自定义style
假如我们想要自定义textview 的属性
先在res/value 下创建文件 attrs.xml
然后在xml根布局中添加命名空间 这里的MyTextView可以定义为任何名字xmlns:MyTextView="http://schemas.android.com/apk/res-auto"
接着就可以在具体的自定义view标签中使用 MyTextView: 属性名 = 具体的值
接下来在代码中 使用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")
}
}
效果如图所示
自定义属性的format类型
转自 https://blog.csdn.net/dj0379/article/details/49662161
1. reference:参考某一资源ID。
(1)属性定义:
(2)属性使用:
2. color:颜色值。
(1)属性定义:
(2)属性使用:
3. boolean:布尔值。
(1)属性定义:
(2)属性使用:
4. dimension:尺寸值。
(1)属性定义:
(2)属性使用:
5. float:浮点值。
(1)属性定义:
(2)属性使用:
6. integer:整型值。
(1)属性定义:
(2)属性使用:
7. string:字符串。
(1)属性定义:
(2)属性使用:
8. fraction:百分数。
(1)属性定义:
(2)属性使用:
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)属性定义:
(2)属性使用:
10. flag:位或运算。
(1)属性定义:
(2)属性使用:
注意:
属性定义时可以指定多种类型值。
(1)属性定义:
(2)属性使用:
测量与布局
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布局
获取子view控件的 margin
现在我们把第二个view 设置margin值 运行起来之后发现并没有效果
这是因为测量和布局都是我们自己实现的没有根据margin布局
接下来我们需要重写三个函数
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
}
为什么要把 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文件下添加代码
布局文件代码
自定义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
}
}
}
显示效果
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
}
}