目录
一.View的基础
1.view的基础概念
2.view的位置和事件event几种表示法
3.view的滑动
①.ScrollTo、ScrollBy:
②.布局位置(layout,offsetLeftAndRight,offsetTopAndBottom)
③.布局参数(LayoutParams)
4.view的弹性滑动
①.Scroller+computeScroll+scrollTo
②.动画
③.延时策略(Handler,view.postDelayed)
5.速度跟踪
①.VelocityTracker:速度追踪,可以判断控件或者页面是否快速滑动。
二.动画
1.view动画:平移缩放旋转淡入淡出动画(补间动画),帧动画。
2.帧动画:顺序播放一组预先定义好的图片,类似电影播放。
3.属性动画:在一段时间内完成对象的一个属性值改变成另一个属性值。
①.比如:xml方式示例如下
②.一些常用的可以直接使用的属性动画的属性值(平移,旋转,透明,缩放)
③.插值器和估值器。
④.属性动画的监听器(AnimatorUpdateListener和AnimatorListener)
⑤.对任意属性进行动画。
⑥.ValueAnimator与ObjectAnimator过程区别:
4.使用动画注意事项:帧动画oom,属性动画内存泄漏,view动画清理
三.View的事件分发机制
1.MotionEvent和TouchSlop
2.GestureDetector、OnDoubleTapListener
3.事件分发机制(重点):事件分发过程dispatchTouchEvent,onIntercepteTouchEvent,onTouchEvent;
4.事件滑动冲突
四.View的相关源码解析
1.源码解析Activity的构成(解析activity的setContentView方法)
2.View的工作流程
①.view工作流程入口
②.measure的流程
③.layout的流程
五.自定义View
1.继承系统控件的自定义View
①.先实现继承TextView的自定义TextView
②.然后在布局文件中使用
③.最后实现效果
2.继承View的自定义View
②.将这个矩形添加对padding的支持
③.根据前面的讲解知道wrap_content与match_parent对于View的效果是一样的,所以可以对wrap_content进行特殊处理
④.自定义属性
3.自定义组合控件
4.自定义ViewGroup
①.继承ViewGroup,实现构造方法和抽象方法等
②.实现测量过程,由于是类似ViewPager,所以在AT_MOST的时候高度是子布局的高度,宽度是子布局的宽度相加,测量过程实现如下
③.实现onLayout,子元素的由左向右排列,所以top为0,bottom为子元素高度不变,left和right是一直相加从左到右排列
④.处理滑动冲突
⑤.弹性滑动到其他界面
⑥.快速滑动到其他界面
⑦.再次触摸屏幕阻止页面继续滑动,在手势抬起然后界面滑动过程再次点击需要让其不在滑动。
一.View的基础
1.view的基础概念
- view:android中所有控件的基类。
- viewGroup:控件组。
2.view的位置和事件event几种表示法
- ①.view.getLeft(),view.getRight(),view.getTop(),view.getBottom() 相对父布局的位置参数
- ②.view.getX(),view.getY() 相对父布局左上角的坐标,getX() = getLeft()+getTranslationX()
- ③.view.getTranslationX(),view.getTranslationY() 相对于原始位置的偏移量,例如:属性动画让其偏移后产生的偏移坐标
- ④.event.getX(),event.getY() 触摸事件相对父布局左上角的坐标(view中motionEvent参数的位置)
- ⑤.event.getRawX,event.getRawY() 触摸事件相对屏幕左上角的坐标(view中motionEvent参数的位置)
参考:Android View坐标系详解(getTop()、getX、getTranslationX...)_张可_的博客-CSDN博客
3.view的滑动
①.ScrollTo、ScrollBy:
- (1)都是对view的内容进行滑动;
- (2)ScrollBy本质调用了ScrollTo;
- (3)注意滑动时向左(向上)为正值,向右(向下)为负值
②.布局位置(layout,offsetLeftAndRight,offsetTopAndBottom)
③.布局参数(LayoutParams)
4.view的弹性滑动
①.Scroller+computeScroll+scrollTo
- Ⅰ.新建Scroller实例;
- Ⅱ.调用scroller.startScroll()方法并将滑动耗时和起始点位置传入,并且调用invalidate()方法进行重绘,重绘调用到draw()方法,draw()方法中调用到computeScroll()方法;
- Ⅲ.重绘过程中调用到computeScroll()方法,在方法中调用scroller.computeScrollOffset()方法判断是否滑动结束,并且在调用computeScrollOffset()方法中会对scroller中的x、y进行处理得到新的滑动位置,没有结束则调用scrollTo方法并通过scroller获取滑动的位置,然后调用invalidate()方法继续重绘,轮询调用到computeScroll()方法。
②.动画
- view动画(并没有真正改变位置)
- 属性动画(真正改变位置)
③.延时策略(Handler,view.postDelayed)
5.速度跟踪
①.VelocityTracker:速度追踪,可以判断控件或者页面是否快速滑动。
二.动画
1.view动画:平移缩放旋转淡入淡出动画(补间动画),帧动画。
补间动画动画集合示例:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true" //动画结束后是否停留在结束位置
android:duration="1000" //动画持续时间
android:shareInterpolator="true"> //是否共享差值器
<alpha //透明度动画(淡入淡出动画)
android:fromAlpha="0.1" //透明度起始值 比如0.1
android:toAlpha="1.0"/> //透明度结束值 比如1
<scale //缩放动画
android:fromXScale="0.5" //水平缩放的起始值 比如:0.5
android:fromYScale="0.5" //竖直方向的起始值 比如:0.5
android:toXScale="1.0" //水平方向的结束值 比如:1.0
android:toYScale="1.0" //竖直方向的结束值 比如:1.0
android:pivotX="1.0" //缩放的轴点的x坐标。
android:pivotY="1.0"/> //缩放的轴点的y坐标。
<translate //平移动画
android:fromXDelta="0.1" //平移动画水平方向起始值
android:fromYDelta="0.1"
android:toXDelta="1.0" //平移动画水平方向结束值
android:toYDelta="1.0"/>
<rotate //旋转动画
android:fromDegrees="0" //旋转动画开始角度 比如:0
android:toDegrees="180" //旋转动画结束角度 比如:180
android:pivotX="1" //轴点x坐标值
android:pivotY="1" />
</set>
2.帧动画:顺序播放一组预先定义好的图片,类似电影播放。
对应标签<animation-list>。
缺点:容易引起oom
3.属性动画:在一段时间内完成对象的一个属性值改变成另一个属性值。
①.比如:xml方式示例如下
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:ordering="sequentially|together"> //sequentially 子动画顺序播放 together 子动画同时播放
<objectAnimator //ObjectAnimator
android:propertyName="string" //属性名
android:duration="int"
android:valueFrom="float|int|color" //表示属性的起始值
android:valueTo="float|int|color" //表示属性的结束值
android:startOffset="int" //动画延时,动画开始后多少毫秒真正播放动画
android:repeatCount="int" //动画重复次数
android:repeatMode="restart" //动画重复模式
android:valueType="colorType"/> //属性类型
<animator //ValueAnimator
android:duration="int"
android:valueFrom="float|int|color"
android:valueTo="float|int|color"
android:startOffset="int"
android:repeatCount="int"
android:repeatMode="restart"
android:valueType="colorType"/>
</set>
②.一些常用的可以直接使用的属性动画的属性值(平移,旋转,透明,缩放)
- translationX和translationY,用来沿着X轴或者Y轴进行平移。
- rolation、rolationX、rolationY:用来围绕View的支点进行旋转。
- PrivotX和PrivotY:控制View对象的支点位置,围绕这个支点进行旋转和缩放变换处理。默认的支点位置就是View对象的中心点。
- alpha:透明度,默认为1(不透明),0代表透明。
- x和y:描述View对象在其容器中的最终位置。
③.插值器和估值器。
TimeInterpolator:时间插值器。根据当前流逝时间的百分比计算出当前属性改变的百分比,可以实现Interpolator接口来实现一个自定义的时间插值器。(插值器返回值0-1之间)
TypeEveluator:类型估值器。根据当前变化的百分比(即时间插值器返回的值),计算出属性改变后的值。可以实现TypeEveluator接口来实现一个自定义的类型估值器。
④.属性动画的监听器(AnimatorUpdateListener和AnimatorListener)
- AnimatorListener监听开始结束取消和重复播放。
- AnimatorUpdateListener监听整个动画过程的进度
⑤.对任意属性进行动画。
比如对Button的width属性进行动画
ObjectAnimator.ofInt(mButton,"width",500).setDuration(500).start();
发现上述动画没有效果,因为setWidth方法不是对宽度设值的,源码中如下:
public void setWidth(int pixels) {
mMaxWidth = mMinWidth = pixels;
mMaxWidthMode = mMinWidthMode = PIXELS;
requestLayout();
invalidate();
}
对属性abc做动画并且想让动画生效需要满足以下两点:
- object必须提供setAbc方法,如果动画需要传递初始值,还需要getAbc方法,因为系统需要获取abc的属性值(如果不满足直接crash)
- object的setAbc对属性abc所做的改变必须通过某种方式反映出来,比如:ui带来改变(如果此条不满足,不会有任何效果也不会crash)
以上问题官方告诉我们三种解决方式:
- 给你的对象加set和get方法,如果你有权限的话。
- 添加包装类包装原始对象,间接提供get和set方法。
- 采用ValueAnimator,监听动画过程,自己实现属性的改变。
⑥.ValueAnimator与ObjectAnimator过程区别:
可以看出ValueAnimator和ObjectAnimator区别是最后一步ValueAnimator是通过监听器来监听当前数值,然后通过数值进行界面的布局或者绘制操作;ObjectAnimator是先根据属性值拼接对应的set函数的名字(set+属性值首字母大写),通过反射找到对应控件的set方法,并将当前值作为参数传入。
4.使用动画注意事项:帧动画oom,属性动画内存泄漏,view动画清理
- ①.oom问题:主要是帧动画中,易出现oom,可以将动画中的图片压缩或者采用surfaceView
- ②.内存泄漏:属性动画中有一类无限循环的动画,在activity退出要及时停止。
- ③.view动画问题:在view动画结束后需要clearAnimation清除动画,否则可能view调用setVisibility(GONE)失效。
- ④.view动画结束后真正的位置还是原先的位置,属性动画结束后真正的位置是动画完成的位置。
- ⑤.使用动画过程建议开启硬件加速,这样更加流畅。
深入理解动画参考:动画深入进阶_龚礼鹏的博客-CSDN博客_keyframe.offloat时间是怎么计算的
三.View的事件分发机制
1.MotionEvent和TouchSlop
MotionEvent常用的事件类型:
- ACTION_DOWN:手指刚触摸屏幕
- ACTION_MOVE:手指在屏幕上移动
- ACTION_UP:手指离开屏幕的一瞬间
- ACTION_CANCEL:被取消的事件(比如:action_down分发给了子控件,action_move和action_up没有分发给子控件,此时子控件的ACTION_CANCEL会被调用)
- ACTION_POINTER_DOWN:有非主要手指按下时
- ACTION_POINTER_UP:有非主要手指抬起时
- TouchSlop:最小滑动距离(为了提升用户体验如果滑动距离小于这个值可以默认没滑动)
2.GestureDetector、OnDoubleTapListener
GestureDetector.OnGestureListener:监听单击滑动长按等动作。方法如下:
- onDown:用户按下屏幕
- onShowPress:按下时间超过瞬间,而且按下的时候没有松开或者拖动
- onLongPress:长按触摸屏,超过一定时长
- onSingleTapUp:单击后立即抬起,除了onDown不能用任何其他操作
- onFling:滑屏,用户按下触摸屏,滑动后松开,只有最后一次触发(如果慢慢拖动不会触发)
- onScroll:在屏幕中拖动事件,可能有连续多次触发,然后以onFling结尾
GestureDetector.OnDoubleTapListener:监听双击动作。
- onSingleTapConfirmed:确认是单击事件,与onDoubleTap互斥
- onDoubleTap:双击事件
- onDoubleTapEvent:获取双击之间发生的动作(第一次点击和第二次点击之间,在onDoubleTap之后调用)
3.事件分发机制(重点):事件分发过程dispatchTouchEvent,onIntercepteTouchEvent,onTouchEvent;
- ①一个事件序列一旦被某个view拦截,那么后续的事件都交给他处理。
- ②如果view的某个事件不消耗ACTION_DOWN,那么同一个事件的其他事件都不会交给他处理,会交给他的父布局处理。
- ③如果view的某个事件不消耗除ACTION_DOWN之外的事件,那么这个事件会消失,最后消失的点击事件会交给Activity处理。
- ④viewGroup默认不拦截事件。
- ⑤view没有onInterceptTouchEvent方法,一旦点击事件传给他,就会调用他的onTouchEvent方法。
- ⑥view的ontouchevent默认消耗事件。
- ⑦onclick发生的前提是当前事件是可点击的。
- ⑧子view可以通过requestDisallowInterceptTouchEvent方法干预父元素的事件分发过程,但ACTION_DOWN除外。
- ⑨源码分析可得事件优先级onTouch---->onTouchEvent---->onClick。
- ⑩view的enabled是false不会影响onTouchEvent的返回值,但是会让此控件不可点击。
4.事件滑动冲突
- ①外部拦截法(通过父布局的OnIntercepteTouchEvent方法)
- ②内部拦截法(通过子view的OnTouchEvent方法和requestDisallowInterceptTouchEvent及父布局的OnIntercepteTouchEvent)
四.View的相关源码解析
1.源码解析Activity的构成(解析activity的setContentView方法)
先看流程图:
可以看出流程图中比较简单:调用setContentView会调用到Activity中,然后Activity会调用window的setContentView,在Activity的attach方法中可以看出window的实例是phoneWindow,然后就是调用到PhoneWindow中,PhoneWindow的setContentView主要有两个重要方法分别是generateDecor创建DecorView和generateLayout传入参数为decor然后加载根布局xml文件,在generateLayout方法中可以看见加载了根布局screen_title.xml文件,从此文件中可以看出最后梳理下各个总的流程包含关系如下:
从上述的源码分析也可以看出事件的分发流程是从Activity——>PhoneWindow——>DocorView——>ContentView(最外层的ViewGroup)
2.View的工作流程
①.view工作流程入口
- 首先是在Activity的启动过程会调用到handleLaunchActivity方法,此方法会先调用performLaunchActivity然后到Activity的onCreate方法,然后调用handleResumeActivity方法,参考:android进阶解密 第四章 android8.0四大组件的工作过程_龚礼鹏的博客-CSDN博客
- handleResumeActivity会同上先调用到onResume方法;然后调用window的getDecorView方法获取decorView,前面也说过window中持有decorView;然后通过activity获取到WindowManager实例,实现类为WindowManagerImpl;然后调用到WindowManagerImpl的addView方法并将decorView传入进去。最后调用setView会将decorView加载到WindowManageService中,关于整个流程详细介绍相关参考:android 进阶解密 第七章 理解WindowManager_龚礼鹏的博客-CSDN博客
- 在setView将docorView与ViewRoot关联的过程会调用到requestLayout方法,然后调用到scheduleTraversals方法
- 注意在scheduleTraversals方法中是通过mChoreographer发送异步消息然后消息送达后会到doTraversal方法,然后就调用到performTraversals方法,此方式就是View测量布局绘制的入口方法,此处有个异步消息同步屏障的知识点,参考:Handler的内功心法,值得拥有!
- performTraversals方法会调用到performMeasure、performLayout、performDraw方法,然后这三个方法又调用到View的measure、layout、draw方法。
②.measure的流程
Ⅰ.View的measure流程,下面的方法都是在View类中
measure——>onMeasure——>getDefaultSize——>setMeasuredDimension——>setMeasuredDimensionRaw
- 在getDefaultSize中可以看出测量模式AT_MOST与EXACTLY模式宽和高都是取决于specSize的,也就是说,对于一个直接继承View的自定义View他们的wrap_content和match_parent属性效果一样的
测量模式和测量大小参考:android view的工作原理_龚礼鹏的博客-CSDN博客_android view的工作原理
Ⅱ.ViewGroup的measure流程
measureChildren——>measureChild——>getChildMeasureSpec——>child.measure
- 在 getChildMeasureSpec方法中定义了父布局的MeasureSpec和子布局的layoutParams参数决定子布局的MeasureSpec,关系如下:
parentSpecSize
ChildLayoutParams | EXACITY | AT_MOST | UNSPECIFIED |
dp/px | EXACITY childSize | EXACITY childSize | EXACITY childSize |
match_parent | EXACITY parentSize | AT_MOST parentSize | UNSPECIFIED 0 |
wrap_content | AT_MOST parentSize | AT_MOST parentSize | UNSPECIFIED 0 |
Ⅲ.LinearLayout的measure流程
分析垂直布局过程
onMeasure——>measureVertical
其实就是主要逻辑在measureVertical方法中,如果高度设置为wrap_content,先定义一个mTotalLength用来存储LinearLayout高度,然后for循环中根据子布局的specMode然后分别计算子元素的高度,将每个子元素的高度+margin的高度加上mTotalLength并且赋值给mTotalLength。最后需要mTotalLength加上padding的值得到最终的高度值mTotalLength
③.layout的流程
Ⅰ.View的layout流程
layout(int l, int t, int r, int b)——>setFrame(l, t, r, b)——>onLayout(changed, l, t, r, b)
- layout中的参数分别表示View从左、上、右、下相对于父容器的距离。setFrame就确定了该View在父容器中的位置。然后确定位置后在layout中还有调用一个onLayout的空方法,此方法可以根据不同的控件进行不同的实现。所以此方法在View和ViewGroup中都没有实现
Ⅱ.LinearLayout的布局方法
onLayout——>layoutVertical——>setChildFrame
- 在layoutVertical的for循环中不断累加childTop,让其子元素调用setChildFrame方法是可以依次的排列下去
④.View的draw流程
- 绘制背景,重写drawBackground()方法
- 如有需要保存当前canvas层,canvas.save方法集
- 绘制View的内容,onDraw(canvas)
- 绘制子View,dispatchDraw(canvas)
- 如有需要,绘制View的褐色边缘,类似阴影效果
- 绘制装饰,比如滚动条,重写onDrawForeground(canvas)方法
- 绘制默认焦点高亮显示,重写drawDefaultFocusHighlight(canvas) 方法
五.自定义View
1.继承系统控件的自定义View
继承系统的TextView,实现在文字中间加一条横线
①.先实现继承TextView的自定义TextView
通过画笔画布实现画横线功能,代码如下:
package com.example.customview
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
class InvalidTextView : androidx.appcompat.widget.AppCompatTextView {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
constructor(context: Context) : super(context) {
initDraw()
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
initDraw()
}
constructor(context: Context, attrs: AttributeSet, def: Int) : super(context, attrs, def) {
initDraw()
}
fun initDraw() {
paint.color = Color.RED
paint.strokeWidth = 1.5F
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.drawLine(0F, height/2F, width.toFloat(), height/2f, paint)
}
}
②.然后在布局文件中使用
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.customview.InvalidTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
③.最后实现效果
可以看见文字中间是有一条横线的
2.继承View的自定义View
①.简单实现一个矩形的绘制
package com.example.customview
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
class RectView : View {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
constructor(context: Context) : super(context) {
initDraw()
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
initDraw()
}
constructor(context: Context, attrs: AttributeSet, def: Int) : super(context, attrs, def) {
initDraw()
}
fun initDraw() {
paint.color = Color.RED
paint.strokeWidth = 1.5F
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.drawRect(0F, 0F, width.toFloat(), height.toFloat(), paint)
}
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.customview.RectView
android:layout_width="200dp"
android:layout_height="200dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
②.将这个矩形添加对padding的支持
需要在绘制矩形的地方添加对padding的支持,如下
package com.example.customview
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
class RectView : View {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
constructor(context: Context) : super(context) {
initDraw()
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
initDraw()
}
constructor(context: Context, attrs: AttributeSet, def: Int) : super(context, attrs, def) {
initDraw()
}
fun initDraw() {
paint.color = Color.RED
paint.strokeWidth = 1.5F
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
val width = width - paddingLeft - paddingRight
val height = height - paddingTop - paddingBottom
canvas?.drawRect(
0F + paddingLeft,
0F + paddingTop,
width.toFloat() + paddingLeft,
height.toFloat() + paddingTop,
paint
)
}
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.customview.RectView
android:layout_width="200dp"
android:layout_height="200dp"
android:padding="20dp"
android:background="@color/black"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
③.根据前面的讲解知道wrap_content与match_parent对于View的效果是一样的,所以可以对wrap_content进行特殊处理
package com.example.customview
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
class RectView : View {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
constructor(context: Context) : super(context) {
initDraw()
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
initDraw()
}
constructor(context: Context, attrs: AttributeSet, def: Int) : super(context, attrs, def) {
initDraw()
}
fun initDraw() {
paint.color = Color.RED
paint.strokeWidth = 1.5F
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val widthSpecMode = MeasureSpec.getMode(widthMeasureSpec)
val heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)
var widthSpecSize = MeasureSpec.getSize(widthMeasureSpec)
var heightSpecSize = MeasureSpec.getSize(heightMeasureSpec)
//当是最大模式时,直接设置默认值,区分与精准模式
if (widthSpecMode == MeasureSpec.AT_MOST) {
widthSpecSize = 500
}
if (heightSpecMode == MeasureSpec.AT_MOST) {
heightSpecSize = 500
}
setMeasuredDimension(widthSpecSize, heightSpecSize)
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
val width = width - paddingLeft - paddingRight
val height = height - paddingTop - paddingBottom
canvas?.drawRect(
0F + paddingLeft,
0F + paddingTop,
width.toFloat() + paddingLeft,
height.toFloat() + paddingTop,
paint
)
}
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.customview.RectView
android:layout_width="200dp"
android:layout_height="wrap_content"
android:padding="20dp"
android:background="@color/black"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
④.自定义属性
添加自定义属性,添加自定义背景色
先在values目录下面创建attrs.xml,添加自定义属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="RectView">
<attr name="rect_color" format="color" />
</declare-styleable>
</resources>
然后在代码中获取自定义属性,注意获取的资源要回收
package com.example.customview
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
class RectView : View {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private var mColor = Color.RED
constructor(context: Context) : super(context) {
initDraw()
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
//获取自定义属性对应的组名称
val typedArray = context.obtainStyledAttributes(attrs,R.styleable.RectView)
//获取自定义属性
mColor = typedArray.getColor(R.styleable.RectView_rect_color, Color.RED)
//注意资源需要回收
typedArray.recycle()
initDraw()
}
constructor(context: Context, attrs: AttributeSet, def: Int) : super(context, attrs, def) {
initDraw()
}
fun initDraw() {
paint.color = mColor
paint.strokeWidth = 1.5F
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val widthSpecMode = MeasureSpec.getMode(widthMeasureSpec)
val heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)
var widthSpecSize = MeasureSpec.getSize(widthMeasureSpec)
var heightSpecSize = MeasureSpec.getSize(heightMeasureSpec)
//当是最大模式时,直接设置默认值,区分与精准模式
if (widthSpecMode == MeasureSpec.AT_MOST) {
widthSpecSize = 500
}
if (heightSpecMode == MeasureSpec.AT_MOST) {
heightSpecSize = 500
}
setMeasuredDimension(widthSpecSize, heightSpecSize)
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
val width = width - paddingLeft - paddingRight
val height = height - paddingTop - paddingBottom
canvas?.drawRect(
0F + paddingLeft,
0F + paddingTop,
width.toFloat() + paddingLeft,
height.toFloat() + paddingTop,
paint
)
}
}
最后在xml文件中添加自定义属性的值,可以通过自动导包的方式
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.customview.RectView
android:layout_width="200dp"
android:layout_height="wrap_content"
android:padding="20dp"
android:background="@color/black"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:rect_color="@android:color/holo_blue_light"/>
</androidx.constraintlayout.widget.ConstraintLayout>
3.自定义组合控件
自定义组合控件实现title
首先实现组合控件的xml布局文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="45dp">
<ImageView
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_centerVertical="true"
android:layout_alignParentStart="true"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:src="@drawable/icon_back"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="标题栏"
android:textColor="@color/black"
android:layout_centerInParent="true"
/>
<ImageView
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_centerVertical="true"
android:layout_alignParentEnd="true"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:src="@drawable/icon_ok"/>
</RelativeLayout>
然后代码中实现这个组合布局控件
package com.example.customview
import android.content.Context
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.ImageView
import android.widget.RelativeLayout
import android.widget.TextView
class CustomTitleBar : RelativeLayout {
private lateinit var rlContent: RelativeLayout
private lateinit var ivBack: ImageView
private lateinit var ivOk: ImageView
private lateinit var tvTitle: TextView
constructor(context: Context) : super(context) {
initView(context)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
initView(context)
}
constructor(context: Context, attrs: AttributeSet, def: Int) : super(context, attrs, def) {
initView(context)
}
fun initView(context: Context) {
LayoutInflater.from(context).inflate(R.layout.custom_title, this, true)
rlContent = findViewById(R.id.rl_content)
ivBack = findViewById(R.id.iv_back)
ivOk = findViewById(R.id.iv_ok)
tvTitle = findViewById(R.id.tv_title)
}
fun setOnBackClick(listener: OnClickListener){
ivBack.setOnClickListener(listener)
}
fun setOnOkClick(listener: OnClickListener){
ivOk.setOnClickListener(listener)
}
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.customview.CustomTitleBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
最后显示效果如下
如果需要自定义属性可以和上述一样在attrs.xml文件中添加新的属性,此处就添加标题栏,文字颜色,背景颜色三个属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CustomTitleBar">
<attr name="text_color" format="color" />
<attr name="text_content" format="string" />
<attr name="background_color" format="color" />
</declare-styleable>
</resources>
package com.example.customview
import android.content.Context
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.ImageView
import android.widget.RelativeLayout
import android.widget.TextView
class CustomTitleBar : RelativeLayout {
private lateinit var rlContent: RelativeLayout
private lateinit var ivBack: ImageView
private lateinit var ivOk: ImageView
private lateinit var tvTitle: TextView
private var textColor: Int? = null
private var backgroudColor: Int? = null
private var textContent: String? = null
constructor(context: Context) : super(context) {
initView(context)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
//获取自定义属性对应的组名称
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomTitleBar)
//获取自定义属性
textColor = typedArray.getColor(R.styleable.CustomTitleBar_text_color, Color.RED)
textContent = typedArray.getString(R.styleable.CustomTitleBar_text_content)
backgroudColor = typedArray.getColor(R.styleable.CustomTitleBar_background_color, Color.RED)
//注意资源需要回收
typedArray.recycle()
initView(context)
}
constructor(context: Context, attrs: AttributeSet, def: Int) : super(context, attrs, def) {
initView(context)
}
fun initView(context: Context) {
LayoutInflater.from(context).inflate(R.layout.custom_title, this, true)
rlContent = findViewById(R.id.rl_content)
ivBack = findViewById(R.id.iv_back)
ivOk = findViewById(R.id.iv_ok)
tvTitle = findViewById(R.id.tv_title)
textContent?.let {
tvTitle.text = it
}
textColor?.let {
tvTitle.setTextColor(it)
}
backgroudColor?.let {
rlContent.setBackgroundColor(it)
}
}
fun setOnBackClick(listener: OnClickListener) {
ivBack.setOnClickListener(listener)
}
fun setOnOkClick(listener: OnClickListener) {
ivOk.setOnClickListener(listener)
}
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.customview.CustomTitleBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:text_content="重新设置标题栏"
app:text_color="@android:color/holo_blue_bright"
app:background_color="@android:color/holo_green_light"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
最终可以看出三个自定义属性都显示出来了
4.自定义ViewGroup
自定义ViewGroup是最复杂的,此处就是简单实现一个横向滑动的类似ViewPager的一个自定义的ViewGroup
①.继承ViewGroup,实现构造方法和抽象方法等
package com.example.customview
import android.content.Context
import android.util.AttributeSet
import android.view.ViewGroup
class CustomViewGroup :ViewGroup {
constructor(context: Context) : super(context) {
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
}
constructor(context: Context, attrs: AttributeSet, def: Int) : super(context, attrs, def) {
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
}
}
②.实现测量过程,由于是类似ViewPager,所以在AT_MOST的时候高度是子布局的高度,宽度是子布局的宽度相加,测量过程实现如下
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val widthSpecMode = MeasureSpec.getMode(widthMeasureSpec)
val heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)
var widthSpecSize = MeasureSpec.getSize(widthMeasureSpec)
var heightSpecSize = MeasureSpec.getSize(heightMeasureSpec)
//测量子布局
measureChildren(widthMeasureSpec, heightMeasureSpec)
//没有元素则直接设置宽高为0
if (childCount == 0) {
setMeasuredDimension(0, 0)
return
}
//如果宽度设置为AT_MOST则宽度为所有子元素宽度之和
if (widthSpecMode == MeasureSpec.AT_MOST) {
widthSpecSize = getChildAt(0).measuredWidth * childCount
}
if (heightSpecMode == MeasureSpec.AT_MOST) {
heightSpecSize = getChildAt(0).measuredHeight
}
setMeasuredDimension(widthSpecSize, heightSpecSize)
}
③.实现onLayout,子元素的由左向右排列,所以top为0,bottom为子元素高度不变,left和right是一直相加从左到右排列
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var left = 0
var child: View
for (index in 0 until childCount) {
child = getChildAt(index)
if (child.visibility != View.GONE) {
val width = child.measuredWidth
child.layout(left, 0, left + width, child.measuredHeight)
left += width
}
}
}
④.处理滑动冲突
检测滑动手势,水平方向滑动拦截
private var lastInterceptX:Int = 0
private var lastInterceptY:Int = 0
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
var interceptor: Boolean = false
val x:Int = ev?.x?.toInt() ?: 0
val y:Int = ev?.y?.toInt() ?: 0
when (ev?.action) {
MotionEvent.ACTION_MOVE->{
val deltaX = x-lastInterceptX
val deltaY = y-lastInterceptY
if (abs(deltaX) > abs(deltaY)) {
interceptor = true
}
}
}
lastInterceptX = x
lastInterceptY = y
return interceptor
}
⑤.弹性滑动到其他界面
首先跟随手势滑动需要用到scrollBy(),然后当手势抬起,此时需要判断当前界面是否已经超过一半,然后根据判断逻辑需要使用到弹性动画滑动下一个界面或者弹回到上一个界面,此时的弹性动画需要用到scroller+computeScroll+scrollTo组合
private var lastInterceptX: Int = 0
private var lastInterceptY: Int = 0
private var lastX: Int = 0
private var lastY: Int = 0
private var currentIndex: Int = 0 //当前子元素
private var mScroller:Scroller? = null
override fun onTouchEvent(event: MotionEvent?): Boolean {
val x: Int = event?.x?.toInt() ?: 0
val y: Int = event?.y?.toInt() ?: 0
when (event?.action) {
MotionEvent.ACTION_MOVE -> {
val deltaX = x - lastX
scrollBy(-deltaX, 0)
}
MotionEvent.ACTION_UP -> {
val distance = scrollX - currentIndex * getChildAt(0).width
if (abs(distance) > getChildAt(0).width / 2) {
if (distance > 0) {
currentIndex++
} else {
currentIndex--
}
}
smoothScrollTo(currentIndex * getChildAt(0).width, 0)
}
}
lastX = x
lastY = y
return super.onTouchEvent(event)
}
override fun computeScroll() {
super.computeScroll()
mScroller?.let {
if (it.computeScrollOffset()) {
scrollTo(it.currX, it.currY)
postInvalidate()
}
}
}
fun smoothScrollTo(destX: Int, destY: Int) {
mScroller?.startScroll(scrollX, scrollY, destX - scrollY, destY - scrollY, 1000)
invalidate()
}
⑥.快速滑动到其他界面
如果速度很快的滑动是不是也要做处理,不然交互体验很差。速度是否足够快则用到了VelocityTracker进行判断
首先初始化快速滑动的变量
constructor(context: Context) : super(context) {
init(context)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init(context)
}
constructor(context: Context, attrs: AttributeSet, def: Int) : super(context, attrs, def) {
init(context)
}
fun init(context:Context){
mScroller = Scroller(context)
tracker = VelocityTracker.obtain()
}
然后在滑动手势中进行处理,主要就是在抬起手势中进行处理
MotionEvent.ACTION_UP -> {
val distance = scrollX - currentIndex * getChildAt(0).width
if (abs(distance) > getChildAt(0).width / 2) {
if (distance > 0) {
currentIndex++
} else {
currentIndex--
}
} else {
tracker?.apply {
computeCurrentVelocity(1000)//获取水平方向速度
val xV = xVelocity
if (abs(xV) > 50) {//如果速度绝对值大于50则认为快速滑动
if (xV > 0) {
currentIndex--
} else {
currentIndex++
}
}
}
}
if (currentIndex < 0) {
currentIndex = 0
}
if (currentIndex > childCount - 1) {
currentIndex = childCount - 1
}
smoothScrollTo(currentIndex * getChildAt(0).width, 0)
tracker?.clear()//重置速度计算器
}
⑦.再次触摸屏幕阻止页面继续滑动,在手势抬起然后界面滑动过程再次点击需要让其不在滑动。
可以在ACTION_DOWN直接中断scroller的滑动过程
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
var interceptor = false
val x: Int = ev?.x?.toInt() ?: 0
val y: Int = ev?.y?.toInt() ?: 0
when (ev?.action) {
MotionEvent.ACTION_DOWN -> {
interceptor = false
mScroller?.apply {
if (!isFinished) {//如果Scroller没有执行完毕直接打断执行
abortAnimation()
}
}
}
MotionEvent.ACTION_MOVE -> {
val deltaX = x - lastInterceptX
val deltaY = y - lastInterceptY
if (abs(deltaX) > abs(deltaY)) {
interceptor = true
}
}
MotionEvent.ACTION_UP -> {}
}
lastX = x//因为在ACTION_DOWN中返回false,所以onTouchEvent中无法获取DOWN事件,所以此处需要赋值
lastY = y
lastInterceptX = x
lastInterceptY = y
return interceptor
}
附上自定义的ViewGroup完整源码
package com.example.customview
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.VelocityTracker
import android.view.View
import android.view.ViewGroup
import android.widget.Scroller
import kotlin.math.abs
class CustomViewGroup : ViewGroup {
constructor(context: Context) : super(context) {
init(context)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init(context)
}
constructor(context: Context, attrs: AttributeSet, def: Int) : super(context, attrs, def) {
init(context)
}
fun init(context: Context) {
mScroller = Scroller(context)
tracker = VelocityTracker.obtain()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val widthSpecMode = MeasureSpec.getMode(widthMeasureSpec)
val heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)
var widthSpecSize = MeasureSpec.getSize(widthMeasureSpec)
var heightSpecSize = MeasureSpec.getSize(heightMeasureSpec)
//测量子布局
measureChildren(widthMeasureSpec, heightMeasureSpec)
//没有元素则直接设置宽高为0
if (childCount == 0) {
setMeasuredDimension(0, 0)
return
}
//如果宽度设置为AT_MOST则宽度为所有子元素宽度之和
if (widthSpecMode == MeasureSpec.AT_MOST) {
widthSpecSize = getChildAt(0).measuredWidth * childCount
}
if (heightSpecMode == MeasureSpec.AT_MOST) {
heightSpecSize = getChildAt(0).measuredHeight
}
setMeasuredDimension(widthSpecSize, heightSpecSize)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var left = 0
var child: View
for (index in 0 until childCount) {
child = getChildAt(index)
if (child.visibility != View.GONE) {
val width = child.measuredWidth
child.layout(left, 0, left + width, child.measuredHeight)
left += width
}
}
}
private var lastInterceptX: Int = 0
private var lastInterceptY: Int = 0
private var lastX: Int = 0
private var lastY: Int = 0
private var currentIndex: Int = 0 //当前子元素
private var mScroller: Scroller? = null
private var tracker: VelocityTracker? = null
override fun onTouchEvent(event: MotionEvent?): Boolean {
val x: Int = event?.x?.toInt() ?: 0
val y: Int = event?.y?.toInt() ?: 0
when (event?.action) {
MotionEvent.ACTION_DOWN->{
mScroller?.apply {
if (isFinished) {
abortAnimation()
}
}
}
MotionEvent.ACTION_MOVE -> {
val deltaX = x - lastX
scrollBy(-deltaX, 0)
}
MotionEvent.ACTION_UP -> {
val distance = scrollX - currentIndex * getChildAt(0).measuredWidth
if (abs(distance) > getChildAt(0).measuredWidth / 2) {
if (distance > 0) {
currentIndex++
} else {
currentIndex--
}
} else {
tracker?.apply {
computeCurrentVelocity(1000)//获取水平方向速度
val xV = xVelocity
if (abs(xV) > 50) {//如果速度绝对值大于50则认为快速滑动
if (xV > 0) {
currentIndex--
} else {
currentIndex++
}
}
}
}
Log.e("CustomView","currentIndex"+currentIndex)
if (currentIndex < 0) {
currentIndex = 0
}
if (currentIndex > childCount - 1) {
currentIndex = childCount - 1
}
Log.e("CustomView","currentIndex"+currentIndex)
smoothScrollTo(currentIndex * getChildAt(0).measuredWidth, 0)
tracker?.clear()//重置速度计算器
}
}
lastX = x
lastY = y
return super.onTouchEvent(event)
}
override fun computeScroll() {
super.computeScroll()
mScroller?.let {
if (it.computeScrollOffset()) {
scrollTo(it.currX, it.currY)
postInvalidate()
}
}
}
fun smoothScrollTo(destX: Int, destY: Int) {
mScroller?.startScroll(scrollX, scrollY, destX - scrollX, destY - scrollY, 1000)
invalidate()
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
var interceptor = false
val x: Int = ev?.x?.toInt() ?: 0
val y: Int = ev?.y?.toInt() ?: 0
when (ev?.action) {
MotionEvent.ACTION_DOWN -> {
interceptor = false
mScroller?.apply {
if (!isFinished) {//如果Scroller没有执行完毕直接打断执行
abortAnimation()
}
}
}
MotionEvent.ACTION_MOVE -> {
val deltaX = x - lastInterceptX
val deltaY = y - lastInterceptY
interceptor = abs(deltaX) > abs(deltaY)
}
MotionEvent.ACTION_UP -> {
interceptor = false
}
}
lastX = x//因为在ACTION_DOWN中返回false,所以onTouchEvent中无法获取DOWN事件,所以此处需要赋值
lastY = y
lastInterceptX = x
lastInterceptY = y
return interceptor
}
}
最后集成到界面中进行实现
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.customview.CustomViewGroup
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<ListView
android:id="@+id/lv_one"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ListView
android:id="@+id/lv_two"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.example.customview.CustomViewGroup>
</RelativeLayout>
package com.example.customview
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.ArrayAdapter
import android.widget.ListView
class MainActivity : AppCompatActivity() {
private lateinit var lvOne:ListView
private lateinit var lvTwo:ListView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
lvOne = findViewById(R.id.lv_one)
lvTwo = findViewById(R.id.lv_two)
var strs1 = arrayOf("1","2","3","4")
var strs2 = arrayOf("a","b","c","d")
var adapter1 =
ArrayAdapter<String>(this, android.R.layout.simple_expandable_list_item_1, strs1)
var adapter2 =
ArrayAdapter<String>(this, android.R.layout.simple_expandable_list_item_1, strs2)
lvOne.adapter = adapter1
lvTwo.adapter = adapter2
}
}
显示效果如下,类似ViewPager,可以左右滑动
参考: 自定义view总结_龚礼鹏的博客-CSDN博客