一篇文章彻底理解自定义View

news2024/11/18 12:22:57

目录

 

一.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过程区别:

watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2dvbmdqZGRl,size_16,color_FFFFFF,t_70 可以看出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方法)

先看流程图:

e94b7301418c4c02917003709f682370.png

 可以看出流程图中比较简单:调用setContentView会调用到Activity中,然后Activity会调用window的setContentView,在Activity的attach方法中可以看出window的实例是phoneWindow,然后就是调用到PhoneWindow中,PhoneWindow的setContentView主要有两个重要方法分别是generateDecor创建DecorViewgenerateLayout传入参数为decor然后加载根布局xml文件,在generateLayout方法中可以看见加载了根布局screen_title.xml文件,从此文件中可以看出最后梳理下各个总的流程包含关系如下:

ed927379995e4c19bcd82a907163f13e.png

从上述的源码分析也可以看出事件的分发流程是从Activity——>PhoneWindow——>DocorView——>ContentView(最外层的ViewGroup)

2.View的工作流程

 ①.view工作流程入口

0947fcc5f5344e97996279b7dc66fe5a.png

  • 首先是在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>

③.最后实现效果

4a3c0ee0b48f40f494e2b152e9207a1d.png

 可以看见文字中间是有一条横线的

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>

ef87ae5562f14a6097d856caa499b1c2.png

 ②.将这个矩形添加对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>

4922e75d3f8145e3aea3f435db548094.png

 ③.根据前面的讲解知道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>

51738f48211848cc92d17b17374168a4.png

 ④.自定义属性

添加自定义属性,添加自定义背景色

先在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>

80be22a678ad437e8d98afba66487644.png

 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>

最后显示效果如下

8c19b07f558043299998fff89a10ad4f.png

 如果需要自定义属性可以和上述一样在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>

最终可以看出三个自定义属性都显示出来了

073b0fc969974bbdb9c06a3db595dafa.png

 

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,可以左右滑动 

21a8adf22a4a454e99a87b51cfb64fe4.gif

 

参考: 自定义view总结_龚礼鹏的博客-CSDN博客

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/55007.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何基于YAML设计接口自动化测试框架?看完秒会

在设计自动化测试框架的时候&#xff0c;我们会经常将测试数据保存在外部的文件&#xff08;如Excel、YAML、CSV&#xff09;或者数据库中&#xff0c;实现脚本与数据解耦&#xff0c;方便后期维护。目前非常多的自动化测试框架采用通过Excel或者YAML文件直接编写测试用例&…

部署高校房屋管理系统可以实现哪些目标?

数图互通房产管理 随着技术的不断进步和升级&#xff0c;以及高校房屋建筑物数量的不断扩充&#xff0c;建立房屋资产管理信息系统进行信息化、数字化、图形化房屋资产管理已经是势在必行。数图互通自主研发的FMCenterV5.0平台&#xff0c;是针对中国高校房产的管理特点和管…

工业数据与数据采集应用如何在ARM+FPGA异核架构的米尔MYC-JX8MMA7核心板应用

随着通信与网络技术、互联网的发展&#xff0c;工业管理数据化、网络化、智能化已成大势所趋&#xff0c;利用工业物联网完成工业控制是智慧工厂中必不可少的一部分。传统的控制与数据采集系统&#xff0c;主机一旦需要同时与多个数据采集设备保持高速通信&#xff0c;并要承担…

Golang【Web 入门】 07 路由 - http.ServeMux

阅读目录说明ServeMux 和 Handler重构&#xff1a;区分不同的 Handler查看 http.HandleFunc 源码重构&#xff1a;使用自定义的 ServeMuxhttp.ServeMux 的局限性URI 路径参数请求方法过滤不支持路由命名http.ServeMux 的优缺点标准库里的就是最好的&#xff1f;说明 goblog 需…

【运维心得】ApacheDirectory找不到java路径的解决方案

目录 ApacheDirectory是什么&#xff1f; 问题现象描述 解决步骤 总结 本文是因为没有在网上找到类似的问题和文章&#xff0c;只能依靠自己去解决&#xff0c;既然解决了&#xff0c;就应该分享一下&#xff0c;希望能帮到需要的朋友。 ApacheDirectory是什么&#xff1f…

火山引擎 DataTester 揭秘:字节如何用 A/B 测试,解决增长问题的?

更多技术交流、求职机会&#xff0c;欢迎关注字节跳动数据平台微信公众号&#xff0c;回复【1】进入官方交流群 上线六年&#xff0c;字节跳动的短视频产品——抖音已成为许多人记录美好生活的平台。除了抖音&#xff0c;字节跳动旗下还同时运营着数十款产品&#xff0c;从资讯…

人工智能在网络安全中的重要性

介绍&#xff1a; 人工智能&#xff08;AI&#xff09;是计算机科学的一个分支&#xff0c;基于某些独特的算法和相关数学计算&#xff0c;使机器能够拥有人类的决策能力。另一方面&#xff0c;网络安全包括保护虚拟世界免受网络攻击和威胁的安全措施。人工智能能够通过采取与…

第二十一章《万年历》第1节:万年历项目简介

万年历项目实现的是一款日历软件,它能够展示出任意年份的日历,除此之外,该软件还能以红色字体标出每个月的阳历节日。 21.1.1万年历功能简介 万年历软件的运行结果如图21-1所示。 图21-1万年历软件界面 为方便讲述,此处把这个万年历的界面分成了4个区域,每个区域当中都有…

【优化发电】基于matlab差分进化算法求解单库发电优化问题【含Matlab源码 2253期】

⛄一、差分进化算法简介 如同所有的优化算法一样, 差分进化算法基于种群的进化算法。差分进化算法主要的参数主要有种群规模NP, 解空间的维数D, 缩放因子F和交叉概率Cr。D维矢量XGi[xGi,1, xGi,2, …, xi, DG], i1, 2, …, Np, 表示G代第i个个体。变异和交叉操作在每一代中产生…

NIO-ServerSocketChannel和Tomcat

ServerSocketChannel 面向流的侦听套接字的可选通道。 通过调用此类的open方法创建服务器套接字通道。 无法为任意预先存在的ServerSocket创建通道。 新创建的服务器套接字通道已打开但尚未绑定。 尝试调用未绑定的服务器套接字通道的accept方法将导致抛出NotYetBoundExcepti…

达梦数据库通过作业实现自动备份功能

达梦数据库通过作业实现自动备份功能作业功能简介一、通过DM管理工具创建备份作业(图形化配置)1.创建代理环境2.创建作业二、命令行方式配置备份作业案例1.创建代理环境2.全量备份3.增量备份4.备份清理三、JOB 运行和日志查看作业功能简介 在管理员的工作中&#xff0c;有许多…

安卓APP源码和设计报告——好再来点餐

大作业文档 项目名称&#xff1a;好再来点餐专业&#xff1a;班级&#xff1a;学号&#xff1a;姓名&#xff1a; 目 录 一、项目功能介绍3 二、项目运行环境3 1、开发环境3 2、运行环境3 3、是否需要联网3 三、项目配置文件及工程结构3 1、工程配置文件3 2、工程结构…

STC - 同时外挂扩展RAM和12864时, C库函数失效的问题

文章目录STC - 同时外挂扩展RAM和12864时, C库函数失效的问题概述笔记原理图 - 外挂XRAM原理图 - 12864错误现象总结ENDSTC - 同时外挂扩展RAM和12864时, C库函数失效的问题 概述 在写STC15实验箱4的出厂测试程序. 发现memset(buf, 0, 256)一片256字节的xdata内存时, 无法将这…

生产型企业如何搭建进销存管理系统?低代码平台了解一下

生产型企业在激烈的市场竞争中充分意识到信息化管理的重要性&#xff0c;但限于资金压力无法购买或开发大型的ERP 系统整合企业管理的小型企业而言&#xff0c;比较多的采用部署相对独立的小型信息系统提高管理信息化水平&#xff0c;常见的包括采购管理系统、销售管理系统、库…

Ubuntu开机自动挂载SD卡到指定挂载点并将Docker默认存储路径改为SD卡

Ubuntu开机自动挂载SD卡到指定挂载点并将Docker默认存储路径改为SD卡查看磁盘信息查看磁盘原挂载点永久开机自动挂载分区——修改文件/etc/fstab应用挂载修改docker默认存储路径查看磁盘信息 sudo fdisk -l如果磁盘太多可以用 sudo fdisk -l | grep GiB只看以GB为单位的磁盘&…

【D3.js】2.2-给 Circle 元素添加属性

title: 【D3.js】2.2-给 Circle 元素添加属性 date: 2022-12-02 15:19 tags: [JavaScript,CSS,HTML,D3.js,SVG] 上章节中虽然添加了circle&#xff0c;但是因为缺少某些属性设置而显得不可见&#xff0c;在此章节中将学习circle的cx、cy、r属性。 一、学习目标 circle的x坐标与…

带你玩转序列模型之NLP与词嵌入(一)

目录 一.词汇表征 二.使用词嵌入 三.词嵌入的特性 四.嵌入矩阵 五.学习词嵌入 一.词汇表征 上周我们学习了RNN、GRU单元和LSTM单元。本周你会看到我们如何把这些知识用到NLP上&#xff0c;用于自然语言处理&#xff0c;深度学习已经给这一领域带来了革命性的变革。其中一…

嵌入式 ADC使用手册完整版 (188977万字)(附源码详细篇)

嵌入式 ADC使用手册完整版 &#xff08;188977万字&#xff09;&#x1f49c;&#xff08;附源码详细篇&#xff09;&#x1f49c;【1】ADC简介【2】ADC主要特征【3】ADC引脚和内部信号【4】ADC 模块框图【5】ADC校准&#xff08;CLB&#xff09;【6】ADC 时钟【7】ADCON 开关【…

Gomodule和GoPath

GoLang生态系统 什么是GOROOT GOROOT 是环境变量&#xff0c;它的值是 Golang 安装包路径 什么是GOPATH GOPATH 是Golang 1.5版本之前一个重要的环境变量配置&#xff0c;是存放 Golang 项目代码的文件路径。 可以通过go env命令查看 GOPATH目录 进入GOPATH目录&#xff…

【Docker】redis分片集群搭建:3主3从,容错迁移,扩缩容

Docker是基于Go语言实现的云开源项目。Docker的主要目标是**“Build&#xff0c;Ship and Run Any App,Anywhere”**。也就是通过对应用组件的封装、分发、部署、运行等生命周期的管理&#xff0c;使用户的APP&#xff08;可以是一个WEB应用或数据库应用等等&#xff09;及其运…