使用RecyclerView开发TabView

news2024/7/6 18:55:28

github链接
demo代码
效果图
在这里插入图片描述
这个功能是使用RecyclerView开发的,需要解决下面这些问题

  1. 单个item滚动的问题:左边的view需要固定、手指松开之后,惯性的处理
  2. 滑动布局子View事件分发冲突的解决
  3. 多个item联合滚动滚动
  4. header
  5. 解决itemView与RecyclerView滑动冲突的问题
  6. 横向滚动时,显示和隐藏滚动条

带着上面想到的问题,逐一写demo,最后再把编写的代码糅合在一起,完成tab view。

第1个问题还是比较复杂的,也是核心问题,所以必须最先解决。
由于我以前写过左滑显示删除按钮的功能,所以滑动部分马上就想到在LinearLayout的基础上开发。而固定的功能反而是最简单的,直接在外部套一个LinearLayout,然后写一个View在最左边就行。
简单提了一下思路,接下来是功能的开发。

单个滑动布局
先实现滑动的功能,这个是最简单的,先看一下图片。

在这里插入图片描述
代码:
这里10个TextView的代码我就不提供了,没什么好说的,直接提供GestureLayout的代码。

class GestureLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    LinearLayout(context, attrs, defStyleAttr), View.OnTouchListener {

    private var scrollState = SCROLL_STATE_IDLE

    private var lastTouchX = 0
    // 当前滑动的距离
    private var scrollOffset = 0f
    // 最大可滑动的距离
    private var maxScrollOffset = 0f
    // 大于这个值才可以滑动
    private var touchSlop = 16

    init {
        orientation = HORIZONTAL
        setOnTouchListener(this)
    }

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        val onGlobalLayoutListener = object : ViewTreeObserver.OnGlobalLayoutListener {
            override fun onGlobalLayout() {
                viewTreeObserver.removeOnGlobalLayoutListener(this)
                // 计算最大宽度
                var totalChildWith = 0
                for (i in 0 until childCount) {
                    totalChildWith += getChildAt(i).measuredWidth
                }
                // 可滑动的距离 = 最大宽度 - 当前View的宽度
                maxScrollOffset = (totalChildWith - width).toFloat()
            }
        }
        viewTreeObserver.addOnGlobalLayoutListener(onGlobalLayoutListener)
    }

    override fun onTouch(v: View?, ev: MotionEvent): Boolean {
        when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
                lastTouchX = (ev.x + 0.5f).toInt()
            }
            MotionEvent.ACTION_MOVE -> {
                val x = (ev.x + 0.5f).toInt()
                val dx = lastTouchX - x
                if (scrollState != SCROLL_STATE_DRAGGING && Math.abs(dx) > touchSlop) {
                    scrollState = SCROLL_STATE_DRAGGING
                }
                if (scrollState == SCROLL_STATE_DRAGGING) {
                    lastTouchX = x
                    // 更新offset
                    updateScrollOffset(scrollOffset + dx)
                    scrollTo(scrollOffset.toInt(), 0)
                }
            }
            MotionEvent.ACTION_UP -> {
                // 回收资源
                recycler()
            }
        }
        return true
    }

    private fun recycler(){
        scrollState = SCROLL_STATE_IDLE
    }

    private fun updateScrollOffset(scrollOffset: Float) {
        this.scrollOffset = Math.min(maxScrollOffset, Math.max(0f, scrollOffset))
        // 这段代码可能有点绕,看下面这段代码就懂了
//        if (scrollOffset < 0f){
//            this.scrollOffset = 0f
//        }else if (scrollOffset > maxScrollOffset){
//            this.scrollOffset = scrollOffset
//        }else{
//            this.scrollOffset = scrollOffset
//        }
    }

    companion object {
        private const val SCROLL_STATE_IDLE = 0
        private const val SCROLL_STATE_DRAGGING = 1
    }
}

基础代码就是上面这些。可以看到,其实是很简单的,只需调用scrollTo,就可以了。该写的注释都已经写了,没啥好说的。
但很显然,简单的滑动是不够的,还需要做松开手指之后的惯性功能,这个就有点麻烦了。

在说如何实现这个功能之前,先来介绍2个需要用到的类。
VelocityTracker:顾名思义,速度追踪器,用来追踪速度的工具类。有3个在这里需要用到的方法:

  • addMovement:记录触摸事件,用于计算出up时的xVeloctiy和yVelocity。
  • computeCurrentVelocity(int, float):在调用getXVelocity之前,需要调用该方法进行计算。
  • getXVelocity:ACTION_UP时调用获取,再将该值传递给Scroller的fling方法,让Scroller计算出实际需要滚动的距离。

OverScroller:上面提到的Scroller,就是第2个类。而在OverScroller里面,有这样一句注释。

This class is a drop-in replacement for Scroller in most cases.

大多数情况下,可以直接使用OverScroller代替Scroller。所以这里直接使用OverScroller。
OverScroller的作用就是:是一个用于模拟滑动的工具类,用它来实现平滑移动时非常有用。注意,这个类只能辅助实现,不是直接实现。
几个需要用到的方法:

  • fling(startX, startY, veloctiyX, velocityY, minX, maxX, minY, maxY, overX, overY):用于惯性的处理。将起始的x/y值、滑动速度、x/y最小最大值传递给它之后,Scroller会计算出实际的x/y值,再让View滑动起来。
  • computeScrollOffset:用来计算当前的滑动位置。如果返回true,表示当前计算还没有完成,此时调用getCurrX/getCurrY可以获取到滑动的值。如果返回false则说明滑动已经完成,无需继续处理。该方法需要在View的computeScroll方法里面调用。
  • getCurrX/getCurrY:在调用computeScrollOffset之后,需要通过该方法获取实际滚动的值,再调用View的scrollTo/scrollBy方法,实现滚动。
  • abortAnimation:用来阻止Scroller滚动,一般在ACTION_DOWN中使用。

除了上面这两个类,还有2个View自带的方法需要解释。

  • invalidate/postInvalidate/postInvalidateOnAnimation:这3个方法都是刷新方法,都会让View调用draw方法,最后会调用computeScroll方法。这里的刷新我使用的是postInvalidateOnAnimation,因为这个方法刷新的次数更少,相对另外两个方法,性能更好。而这里对刷新的要求也不高,所以够用了。
  • computeScroll:在调用刷新方法之后,就会调用这个方法。在这个方法里面,需要调用Scroller的computeScrollOffset,如果返回true,就调用scrollTo/scrollBy方法滚动,再调用刷新方法,直到computeScrollOffset返回false。

总结一下流程:ACTION_UP -> VelocityTracker.addMovement -> VelocityTracker.computeCurrentVelocity -> VelocityTracker.getXVelocity -> Scroller.fling ->postInvalidateOnAnimation -> computeScroll ->Scroller.computeScrollOffset ->Scroller.getCurrX-> scrollTo -> postInvalidateOnAnimation
调用链路有点长,接下来看看代码实现吧,刚才已经写过的大部分代码不会写在下面。

private var touchSlop = 0

private val scroller = OverScroller(context)
private var velocityTracker: VelocityTracker? = null
private var minimumFlingVelocity = 0
private var maximumFlingVelocity = 0

init{
    // 借助ViewConfiguration获取下面这3个值
    val vc = ViewConfiguration.get(context)
    minimumFlingVelocity = vc.scaledMinimumFlingVelocity
    maximumFlingVelocity = vc.scaledMaximumFlingVelocity
    touchSlop = vc.scaledTouchSlop
}

override fun onTouch(v: View?, ev: MotionEvent): Boolean {
    // 初始化VelocityTracker
    initVelocityTrackerIfNoExits()
    // 每次都将event交给VelocityTracker分析
    velocityTracker?.addMovement(ev)
    when (ev.action) {
        MotionEvent.ACTION_DOWN -> {
            // 中断Scroller的滑动
            scroller.abortAnimation()
            lastTouchX = (ev.x + 0.5f).toInt()
        }
        // ACTION_MOVE的代码和上面的一样,就不贴出来了
        MotionEvent.ACTION_UP -> {
            if (scrollState == SCROLL_STATE_DRAGGING) {
                val velocityTracker = velocityTracker
                // 让VelocityTacker开始计算速度
                velocityTracker?.computeCurrentVelocity(1000, maximumFlingVelocity.toFloat())
                // 获取x的速度
                val xVelocity = velocityTracker?.xVelocity ?: 0f
                // 如果速度大于最小的速度,就开始fling
                if (Math.abs(xVelocity) > minimumFlingVelocity.toFloat()) {
                    scroller.fling(scrollOffset.toInt(), 0, -xVelocity.toInt(), 0, 0,    maxScrollOffset.toInt(), 0, 0, 0, 0)
                    postInvalidateOnAnimation()
                }
            }
            recycler()
        }
    }
}

private fun recycler(){
    recycleVelocityTracker()
    scrollState = SCROLL_STATE_IDLE
}

override fun computeScroll() {
    // super是空实现,想去掉也可以
    super.computeScroll()
    // 判断是否还在计算offset
    if (scroller.computeScrollOffset()) {
        val curX = Math.min(Math.max(scroller.currX.toFloat(), 0f), maxScrollOffset)
        if (curX != scrollOffset){
            scrollOffset = curX
        }
        scrollTo()
        if (scrollOffset == 0f || scrollOffset == maxScrollOffset){
            scroller.abortAnimation()
        }
    }
}

private fun initVelocityTrackerIfNoExits() {
    if (velocityTracker == null) {
        velocityTracker = VelocityTracker.obtain()
    }
}

private fun recycleVelocityTracker() {
    velocityTracker?.recycle()
    velocityTracker = null
}

private fun scrollTo(){
    scrollTo(scrollOffset, 0)
    postInvalidateOnAnimation()
}

效果图:
在这里插入图片描述


接下来先在左边加一个TextView实现左边固定的功能

<LinearLayout
    android:layout_width="match_parent"
    android:orientation="horizontal"
    android:layout_height="50dp">

    <TextView
        android:layout_width="@dimen/table_item_width"
        android:textColor="@color/black"
        android:text="stick"
        android:gravity="center"
        android:textSize="@dimen/table_item_text_size"
        android:layout_height="match_parent" />

    <GestureLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <include layout="@layout/merge_table_layout" />
    </GestureLayout>
</LinearLayout>

效果我就不贴出来了,一看就知道怎么回事。至于10个TextView使用include,这是因为后面的Header需要使用同一个layout,所以这样做可以避免编写重复代码。

接下来是子View事件分发的处理。这个View是一个ViewGroup,所以需要处理好touch事件。一些可以传递给子View的事件,就传递给子View,不能传递给子View的,就自己处理。
先来一个反例

<LinearLayout
    android:layout_width="match_parent"
    android:orientation="horizontal"
    android:layout_height="50dp">

    <TextView
        android:layout_width="@dimen/table_item_width"
        android:textColor="@color/black"
        android:text="stick"
        android:gravity="center"
        android:textSize="@dimen/table_item_text_size"
        android:layout_height="match_parent" />

    <GestureLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            android:id="@+id/click_area"
            android:layout_width="wrap_content"
            android:layout_height="match_parent">

            <include layout="@layout/merge_table_layout" />
        </LinearLayout>
    </GestureLayout>
</LinearLayout>

java代码我就不贴了,只是给click_area设置了一个onClick,弹出toast,看一下效果图
在这里插入图片描述
可以看到,鼠标明明滑动了,但View却没有滑动,反而是触发了onClick,说明必须对某些事件进行处理。
而即使去掉onClick,也会出现问题。因为在onAttch方法里面,只是计算当前ViewGroup所有子View的width。此时,只有一个,计算出来的width是不正确的,导致maxScrollWidth不正确,最后没办法滑动。想要解决这个问题,就需要修改一点点代码。

override fun onAttachedToWindow() {
    super.onAttachedToWindow()
    calculateMaxScrollOffset()
}

private fun calculateMaxScrollOffset(){
    // 用于计算的ViewGroup,可能是当前View,也可能是第一个子View
    val calculateViewGroup: ViewGroup
    // 如果childCount等于0,只会返回null,不用担心越界异常
    val firstChild = getChildAt(0)
    // 如果只有一个child,并且是ViewGroup才使用该View
    if (childCount == 1 && firstChild as? ViewGroup != null){
        calculateViewGroup = firstChild
    }else{
        calculateViewGroup = this
    }
    val onGlobalLayoutListener = object : ViewTreeObserver.OnGlobalLayoutListener {
        override fun onGlobalLayout() {
            calculateViewGroup.viewTreeObserver.removeOnGlobalLayoutListener(this)
            var totalChildWith = 0
            for (i in 0 until calculateViewGroup.childCount) {
                totalChildWith += calculateViewGroup.getChildAt(i).measuredWidth
            }
            maxScrollOffset = (totalChildWith - width).toFloat()
            // 只有有子Layout时,才需要重新设置当前layout和子layout的宽度
            if (calculateViewGroup != this@GestureLayout) {
                layout(left, top, left + totalChildWith, bottom)
                calculateViewGroup.layout(calculateViewGroup.left, calculateViewGroup.top, calculateViewGroup.left + totalChildWith, calculateViewGroup.bottom)
            }
        }
    }
    calculateViewGroup.viewTreeObserver.addOnGlobalLayoutListener(onGlobalLayoutListener)
}

通过上述的代码,就不用担心其他人在这个Layout下面,再加其他的Layout了。不过如果非要加几个layout,那就没办法了。
但我泼一下冷水,上面的代码看起来好像很好,但在RecyclerView里面使用,还是有问题,最后是使用其他方式解决这个问题。
这个问题就不再讨论了,开始着手解决事件分发的问题。因为不管有几个子View,都需要解决事件分发的问题。
先思考为什么设置了onClick就会使滑动失效?原因也很简单,设置了onClick,所以子View消费了所有事件,导致事件没办法传递到GestureLayout。解决方式也很简单,重写onInterceptTouchEvent方法,如果从ACTION_DOWN到ACTION_MOVE时,x坐标变化了,而且大于touchSlop,就认为滑动生效。此时,将scrollState设置为DRAGGING并返回true。这样就子View就会拿到ACTION_CANCEL,并且还会将move事件传递给GestureLayout的dispatchTouchEvent,最后传递到onTouch方法。只要到了onTouch方法,那代码就可以正常执行下去。

private var initialTouchX: Int = 0


override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
    when(ev.action){
        MotionEvent.ACTION_DOWN -> {
            initialTouchX = (ev.x + 0.5f).toInt()
            lastTouchX = initialTouchX
            initOrResetVelocityTracker()
            velocityTracker?.addMovement(ev)
        }
        MotionEvent.ACTION_MOVE -> {
            val x = (ev.x + 0.5f).toInt()
            val dx = x - initialTouchX
            lastTouchX = x
            if (Math.abs(dx) > touchSlop) {
                initVelocityTrackerIfNoExits()
                velocityTracker?.addMovement(ev)
                scrollState = SCROLL_STATE_DRAGGING
            }
        }
        MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
            scrollState = SCROLL_STATE_IDLE
        }
    }
    return scrollState == SCROLL_STATE_DRAGGING
}

添加了这些代码之后,就可以正常滑动了,图片我就不提供了,自己试一下就知道了。

接下来是多个item联合滚动和header功能,由于功能类似,所以放在一起。
先确定一下思路。首先,想要多个联动滚动,肯定需要一个Manager来管理。能不能直接在Adapter里面的onBindViewHolder方法将View add到Manager里面,这样肯定不行,因为这样只有add没有remove,而且同一个item可能还会onBindViewHolder多次。然后我就想到了Adapter里面的onViewAttachedToWindow方法和onDetachedFromRecyclerView方法。分别对应View显示到界面和View从界面消失。最后经测试,这种方式确实可行,但每个Adapter都写同样的代码,有点烦,所以就尝试将代码放到View层的attch方法和detach方法,发现也可以,下面是简单的代码

override fun onAttachedToWindow() {
    super.onAttachedToWindow()
    scrollManager?.also {
        it.addCandidate(this)}
    }
}
override fun onDetachedFromWindow() {
    super.onDetachedFromWindow()
    scrollManager?.also {
        it.removeCandidate(this)
    }
}

接下来是设计ScrollManager。ScrollManager的作用是,统一管理所有的item。当某个item touch之后,将数据传递给Manager,让Manager统一调动所有的item。
首先,需要添加接口,通过接口将touch行为传递给ScrollManager。

interface OnScrolledChangedListener {
    // 在onInterceptTouchEvent里面调用,借助Scroller判断是否正在滚动,如果是,就返回true
    fun isScrollerFinished(): Boolean
    // 如果在onInteceptTouchEvent的ACTION_DOWN拦截了,就在onTouch组织Scroller滚动
    fun checkAndAbortAnimation()
    // ACTION_MOVE和ACTION_UP调用,更新offset
    fun onScrollChange(distanceX: Float)
    // ACTION_UP调用,让ScrollManager调用Scroller的fling
    fun onFling(velocityX: Int)
    // header的computeScroll调用。需要明确的是,不能让item去调用,因为这样做的话,会让Scroller同时调用computeScrollOffset,这样做是不合理的,也是会出问题的
    fun onComputeScroll()
    // ACTION_UP和ACTION_CANCEL调用,做一些收尾的工作
    fun draggingEnd()
}    

添加这个interface之后,就需要在GestureLayout里面添加相应的字段,让并在相应的时机调用对应的方法

// 注意,这里加了open。提供相应字段之后,再由子类自己去实现,所以改为可以继承
open class GestureLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    LinearLayout(context, attrs, defStyleAttr), View.OnTouchListener {
    
    var onScrolledChangeListener: OnScrolledChangedListener? = null
    
    // 此时,onAttachedToWindow的calculateMaxScrollOffset代码去去掉,暂时别考虑clickArea的问题
    // 这个方法直接去掉就行,不用重写,放在这里只做提醒
    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
    }
    
    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        when(ev.action){
            MotionEvent.ACTION_DOWN -> {
                ...
                scrollState = if (onScrolledChangeListener?.isScrollerFinished()
                        ?.not() == true
                ) SCROLL_STATE_DRAGGING else SCROLL_STATE_IDLE
            }
            MotionEvent.ACTION_UP, MotionEvent.ACTION_UP -> {
                scrollState = SCROLL_STATE_IDLE
                onScrolledChangeListener?.draggingEnd()
            }
        }
    }
    
    override fun onTouch(v: View?, ev: MotionEvent): Boolean {
        ...
        when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
                // 注意,上面的onInterceptTouchEvent,有一句lastTouchX,这里可以去掉,因为已经放在onInterceptTouchEvent里面了
                onScrolledChangeListener?.checkAndAbortAnimation()
            }
            MotionEvent.ACTION_MOVE -> {
                ...
                if (scrollState == SCROLL_STATE_DRAGGING) {
                    lastTouchX = x
                    // 这里的scrollTo必须去掉,放在ScrollerManager里买实现
                    onScrolledChangeListener?.onScrollChange(dx.toFloat())
                }
            }
            MotionEvent.ACTION_UP -> {
                // ACTION_UP的部分代码也更新了
                if (scrollState == SCROLL_STATE_DRAGGING) {
                    val velocityTracker = velocityTracker
                    velocityTracker?.computeCurrentVelocity(1000, maximumFlingVelocity.toFloat())
                    val initialVelocity = velocityTracker?.xVelocity?.toInt() ?: 0
                    if (Math.abs(initialVelocity) > minimumFlingVelocity) {
                        onScrolledChangeListener?.onFling(-initialVelocity)
                    } else {
                        postInvalidateOnAnimation()
                    }
                }
                recycler()
            }
        }
    }
    
    private fun recycler() {
        scrollState = SCROLL_STATE_IDLE
        onScrolledChangeListener?.draggingEnd()
        recycleVelocityTracker()
    }
    
    override fun computeScroll() {
        if (this.childCount > 1) {
            onScrolledChangeListener?.onComputeScroll()
        }
    }
}

这样,GestureLayout就改造完成,接下来编写ScrollManager。然后再分别新增item能用的和header能用的两个Layout。这个Layout设置为open,就是为了这个。
ScrollManager

class ScrollManager(context: Context){
    private val minimumScrollOffset = 0.0f
    private var maximumScrollOffset = Float.MAX_VALUE
    private var scrollOffset = minimumScrollOffset
    private var scroller = OverScroller(context)
    // 存储要scroll的View
    private val scrollCandidateList = ArrayList<View>()

    // 这两个是滚动条要用的,现在不做开发,但留出相应的接口,最后会讲怎么实现滚动条
    private var scrollBar: HorizontalScrollBar? = null
    private var scrollBarOffset: Float = 0f

    // 记录是否正在fling,阻断一些不必要的代码
    private var isFling = false

    fun addCandidate(view: View) {
        if (scrollCandidateList.contains(view).not()) {
            scrollCandidateList.add(view)
            view.scrollTo(scrollOffset.toInt(), 0)
        }
    }

    fun removeCandidate(view: View) {
        scrollCandidateList.remove(view)
    }

    // 设置最大可滚动的距离,让header统一计算,然后调用该方法设置,不要让item去计算,否则就会设置很多次
    fun setMaxScrollOffset(maxOffset: Float) {
        maximumScrollOffset = maxOffset
    }

    fun scrollSpecialView(view: View) {
        view.scrollTo(scrollOffset.toInt(), 0)
    }

    fun clearViews() {
        scrollCandidateList.clear()
        clearScrollBar()
    }

    fun setScrollBar(bar: HorizontalScrollBar) {
        scrollBar = bar
    }

    fun clearScrollBar() {
        scrollBar = null
    }

    // OnScrolledChangedListener.isScrollerFinished
    fun isScrollerFinished(): Boolean {
        return scroller.isFinished
    }

    // OnScrolledChangedListener.checkAndAbortAnimation
    fun checkAndAbortAnimation() {
        if (!scroller.isFinished) {
            scroller.abortAnimation()
        }
    }

    // OnScrolledChangedListener.onScrollChange会掉孔这个方法计算offset,再调用下面的updateScroll滚动所有的View
    fun safeUpdateScrollPosition(distanceX: Float) {
        scrollOffset += distanceX
        scrollOffset = Math.min(Math.max(scrollOffset, minimumScrollOffset), maximumScrollOffset)
        scrollBarOffset = calculateScrollBarOffset()
    }

    fun updateScroll() {
        scrollCandidateList.forEach {
            it.scrollTo(scrollOffset.toInt(), 0)
            it.postInvalidateOnAnimation()
        }
        scrollBar?.updateScrollWeight(scrollBarOffset)
    }

    // OnScrolledChangedListener.onFling
    fun fling(velocityX: Int) {
        isFling = true
        scroller.fling(
            scrollOffset.toInt(), 0, velocityX, 0, 0, maximumScrollOffset.toInt(), 0, 0, 0, 0
        )
        updateScroll()
    }

    // OnScrolledChangedListener.onComputeScroll
    fun updateScrollForScroller() {
        // 返回true表示还在计算
        if (scroller.computeScrollOffset()) {
            val curX = getSafeUpdatePosition(scroller.currX)
            if (curX != scrollOffset.toInt()) {
                scrollOffset = curX.toFloat()
                scrollBarOffset = calculateScrollBarOffset()
            }
            updateScroll()
            // 到了屏幕边缘,也可以结束fling,所以做收尾操作
            if (scrollOffset == minimumScrollOffset || scrollOffset == maximumScrollOffset) {
                scroller.abortAnimation()
                isFling = false
                scrollBar?.startCountToHide()
            }
        } else {
            // 返回false表示计算完成,fling已经结束
            if (isFling) {
                isFling = false
                scrollBar?.startCountToHide()
            }
        }
    }

    // OnScrolledChangedListener.draggingEnd
    fun draggingEnd() {
        // 这个方法是UP或CANCEL时调用的
        // 此时,可能还在fling,所以不能隐藏。而如果没有再fling,就可以隐藏
        if (isFling.not()){
            scrollBar?.startCountToHide()
        }
    }

    private fun getSafeUpdatePosition(curX: Int): Int {
        return Math.min(Math.max(curX, minimumScrollOffset.toInt()), maximumScrollOffset.toInt())
    }

    private fun calculateScrollBarOffset(): Float {
        return scrollOffset / (maximumScrollOffset - minimumScrollOffset)
    }

    interface HorizontalScrollBar {
        // 更新滚动的位置
        fun updateScrollWeight(wieght: Float)
        // 滚动完成,隐藏滚动条
        fun startCountToHide()
    }
}

然后是item的GestureLayout,ItemGestureLayout

class ItemGestureLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    GestureLayout(context, attrs, defStyleAttr) {

    private var scrollManager: ScrollManager? = null

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        scrollManager?.addCandidate(this)
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        scrollManager?.removeCandidate(this)
    }
    
    fun setScrollManager(scrollManager: ScrollManager){
        this.scrollManager = scrollManager
        onScrolleChangedListener = ItemGestureLayoutOnScrollChangedListener(scrollManager)
    }

    private class ItemGestureLayoutOnScrollChangedListener(scrollManager: ScrollManager): OnScrolledChangedListener{
        private val scrollManagerWeakRef = WeakReference(scrollManager)

        override fun isScrollerFinished(): Boolean {
            return scrollManagerWeakRef.get()?.isScrollerFinished() == true
        }

        override fun checkAndAbortAnimation() {
            scrollManagerWeakRef.get()?.checkAndAbortAnimation()
        }

        override fun onScrollChange(distanceX: Float) {
            scrollManagerWeakRef.get()?.apply {
                safeUpdateScrollPosition(distanceX)
                updateScroll()
            }
        }

        override fun onFling(velocityX: Int) {
            scrollManagerWeakRef.get()?.fling(velocityX)
        }

        // item空实现
        override fun onComputeScroll() {
        }

        override fun draggingEnd() {
            scrollManagerWeakRef.get()?.draggingEnd()
        }
    }
}

最后是header的GestureLayout,BaseHeaderGestureLayout

abstract class BaseHeaderGestureLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    FrameLayout(context, attrs, defStyleAttr) {

    private var inflated = AtomicBoolean(false)
    private var scrollManager: ScrollManager? = null

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        if (inflated.getAndSet(true).not()){
            inflate(context, getLayoutId(), this).also(::initLayout)
        }
        val scrollLayout = getScrollLayout() ?:return
        scrollManager?.also {
            it.addCandidate(scrollLayout)
        }
        scrollLayout.onScrolleChangedListener = HeaderGestureLayoutOnScrollChangedListener(scrollManager)
        val globalLayoutListener = object : ViewTreeObserver.OnGlobalLayoutListener{
            override fun onGlobalLayout() {
                scrollLayout.viewTreeObserver.removeOnGlobalLayoutListener(this)

                scrollLayout.apply {
                    var measureParent: ViewGroup = this
                    if (childCount == 1 && getChildAt(0) is ViewGroup){
                        measureParent = getChildAt(0) as ViewGroup
                    }
                    var sChildWidth = 0
                    for (i in 0 until measureParent.childCount){
                        sChildWidth += measureParent.getChildAt(i).measuredWidth
                    }
                    // 在header里面计算最大滚动offset
                    scrollManager?.setMaxScrollOffset((sChildWidth - measureParent.measuredWidth).toFloat())
                }
            }
        }
        scrollLayout.viewTreeObserver.addOnGlobalLayoutListener(globalLayoutListener)
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        clearScrollLayoutAndManager()
    }

    fun setScrollManager(scrollManager: ScrollManager){
        this.scrollManager = scrollManager
        getScrollLayout()?.also {
            if (it.onScrolleChangedListener == null){
                it.onScrolleChangedListener = HeaderGestureLayoutOnScrollChangedListener(scrollManager)
            }
        }
    }

    private fun clearScrollLayoutAndManager(){
        getScrollLayout()?.onScrolleChangedListener = null
        scrollManager?.also {
            it.removeCandidate(this)
            scrollManager = null
        }
    }

    protected abstract fun getLayoutId(): Int
    protected abstract fun initLayout(view: View)
    protected abstract fun getScrollLayout(): GestureLayout?

    private class HeaderGestureLayoutOnScrollChangedListener(scrollManager: ScrollManager?): GestureLayout.OnScrolledChangedListener{
        private val scrollManagerWeakRef = WeakReference(scrollManager)

        override fun isScrollerFinished(): Boolean {
            return scrollManagerWeakRef.get()?.isScrollerFinished() == true
        }

        override fun checkAndAbortAnimation() {
            scrollManagerWeakRef.get()?.checkAndAbortAnimation()
        }

        override fun onScrollChange(distanceX: Float) {
            scrollManagerWeakRef.get()?.apply {
                safeUpdateScrollPosition(distanceX)
                updateScroll()
            }
        }

        override fun onFling(velocityX: Int) {
            scrollManagerWeakRef.get()?.fling(velocityX)
        }

        override fun onComputeScroll() {
            // compute统一在这里做,不要让item去做
            scrollManagerWeakRef.get()?.updateScrollForScroller()
        }

        override fun draggingEnd() {
            scrollManagerWeakRef.get()?.draggingEnd()
        }
    }
}

为什么item是直接继承GestureLayout,而Header继承FrameLayout并提供一个layoutId?因为考虑到item是用在RecyclerView里面,担心需要频繁调用inflate方法,而Header是直接放到layout文件里面,稳定性比较高,一般不会出什么问题。
此时,有人会想到,scroll的那些item和header完全一样,这个要怎么办?考虑到这个问题,我建议在开发时,这些view用一个layout文件编写。最外部的layout使用merge标签,这样就不会额外增加布局。然后分别在item和header的layout里面,通过include使用这个layout文件。从我上面提供的xml代码也可以看到,我在项目中,就是这样做的。这样就可以保证scrollView的一致性和可维护性。
TabAdapter

class TableAdapter :RecyclerView.Adapter<TableAdapter.ViewHolder>(){
    var scrollManager: ScrollManager? = null

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.table_item_test, parent, false)).also{vh ->
            scrollManager?.also {
                vh.scrollLayout.setScrollManager(it)
            }
        }
    }

    override fun getItemCount(): Int = 100

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.apply {
            scrollManager?.scrollSpecialView(scrollLayout)
            stickItemTv.text = "stickItem$position"
            item1Tv.text = "item$position-1"
            item2Tv.text = "item$position-2"
            ...
        }
    }

    class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView){
        val scrollLayout: ItemGestureLayout = itemView.findViewById(R.id.scroll_layout)
        val stickItemTv: TextView = itemView.findViewById(R.id.stick_item_tv)
        val item1Tv: TextView = itemView.findViewById(R.id.data1_tv)
        val item2Tv: TextView = itemView.findViewById(R.id.data2_tv)
        ...
    }
}

table_item_test

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="@dimen/table_item_height"
    android:background="@color/white"
    android:orientation="horizontal">

    <TextView
        android:id="@+id/stick_item_tv"
        android:layout_width="@dimen/table_item_width"
        android:layout_height="match_parent"
        android:gravity="center"
        android:textColor="@color/black"
        android:textSize="@dimen/table_item_text_size" />
    <View
        android:layout_width="1dp"
        android:layout_height="match_parent"
        android:background="#cccccc" />

    <ItemGestureLayout
        android:id="@+id/scroll_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <include layout="@layout/merge_table_layout" />
    </ItemGestureLayout>
</LinearLayout>

HeaderLayout

class HeaderLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    BaseHeaderGestureLayout(context, attrs, defStyleAttr) {

    private var scrollLayout: GestureLayout? = null

    override fun getLayoutId(): Int = R.layout.table_header_test

    override fun initLayout(view: View) {
        scrollLayout = view.findViewById(R.id.scroll_layout)
        val stickView: TextView = view.findViewById(R.id.stick_item_tv)
        val data1View: TextView = view.findViewById(R.id.data1_tv)
        val data2View: TextView = view.findViewById(R.id.data2_tv)
        ...

        stickView.text = "stick"
        data1View.text = "header1"
        data2View.text = "header2"
        ...
    }

    override fun getScrollLayout(): GestureLayout? = scrollLayout

}

table_header_test

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="@dimen/table_item_height"
    android:background="@color/white"
    android:orientation="horizontal">

    <TextView
        android:id="@+id/stick_item_tv"
        android:layout_width="@dimen/table_item_width"
        android:layout_height="match_parent"
        android:gravity="center"
        android:textColor="@color/black"
        android:textSize="@dimen/table_item_text_size" />

    <View
        android:layout_width="1dp"
        android:layout_height="match_parent"
        android:background="#cccccc" />

    <GestureLayout
        android:id="@+id/scroll_layout"
        android:layout_width="wrap_content"
        android:layout_height="match_parent">

        <include layout="@layout/merge_table_layout" />
    </GestureLayout>
</LinearLayout>

Activity

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(...)
    val scrollManager = ScrollManager(this)
    header_layout.setScrollManager(scrollManager)
    val adapter = TableAdapter()
    adapter.scrollManager = scrollManager
    recycler.adapter = adapter
    recycler.layoutManager = LinearLayoutManager(this,LinearLayoutManager.VERTICAL, false)
}

activity_layout

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

    <HeaderLayout
        android:id="@+id/header_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</LinearLayout>

效果图
在这里插入图片描述

接下来解决滑动冲突的问题。为什么这个问题要憋到这里才说这么解决?因为只有将GestureLayout放到RecyclerView里面,这个问题才会特别明显,明显到影响正常使用。如果在上面编写GestureLayout就顺便加入解决的代码,那就对这个问题没什么感知。
先看一看有问题的效果图
在这里插入图片描述
可以看到,每次水平滑动时,只要有垂直滑动,就会打断水平滑动,体验起来非常糟心。解决方式也很简单,只要在GestureLayout加上requestDisallowInterceptTouchEvent就行,让RecyclerView不要拦截滑动事件。
GestureLayout

override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
    when(ev.action){
        ...
        MotionEvent.ACTION_MOVE -> {
            val x = (ev.x + 0.5f).toInt()
            val dx = x - initialTouchX
            lastTouchX = x
            if (Math.abs(dx) > touchSlop) {
                initVelocityTrackerIfNoExits()
                velocityTracker?.addMovement(ev)
                scrollState = SCROLL_STATE_DRAGGING
                // 新增的代码
                parent.requestDisallowInterceptTouchEvent(true)
            }
        }
        ...
    }
}

override fun onTouch(v: View?, ev: MotionEvent): Boolean {
   when(ev.action){
        ...
        MotionEvent.ACTION_MOVE -> {
            val x = (ev.x + 0.5f).toInt()
            val dx = lastTouchX - x
            if (scrollState != SCROLL_STATE_DRAGGING && Math.abs(dx) > touchSlop) {
                scrollState = SCROLL_STATE_DRAGGING
                // 新增的代码
                parent.requestDisallowInterceptTouchEvent(true)
            }
            if (scrollState == SCROLL_STATE_DRAGGING) {
                lastTouchX = x
                onScrolleChangedListener?.onScrollChange(dx.toFloat())
            }
        }
       ...
   } 
}

也就加了2行代码,所以我就不贴图了,自己试一下就知道了。
接下来解决一下clickArea的问题,不然这个View没办法做点击时间,所有事件都被onTouch方法消费了。而如果在onTouch里面处理onClick,又会让这个方法的代码变得比较复杂。
ItemGestureLayout

private var clickArea: View? = null

// 为什么这里要判断clickArea为空才add,因为如果click不为空,add的是clickArea,不是当前View
override fun onAttachedToWindow() {
    super.onAttachedToWindow()
    if (clickArea == null) {
        scrollManager?.addCandidate(this)
    }
}

override fun onDetachedFromWindow() {
    super.onDetachedFromWindow()
    if (clickArea == null) {
        scrollManager?.removeCandidate(this)
    }
}

// setScrollManager方法可以删掉了,换成下面这个
fun setScrollManagerAndClickArea(scrollManager: ScrollManager, clickArea: View?) {
    this.scrollManager = scrollManager
    this.clickArea = clickArea
    onScrolleChangedListener = ItemGestureLayoutOnScrollChangedListener(scrollManager)
    clickArea?.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
        override fun onViewAttachedToWindow(v: View) {
            // 当clickArea attach时,add到ScrollManager
            scrollManager.also {
                it.addCandidate(v)
            }
        }

        override fun onViewDetachedFromWindow(v: View) {
            // 当clickArea detach时,从ScrollManager remove
            scrollManager.also {
                it.removeCandidate(v)
            }
        }
    })
}

table_header_test的include代码外面,再套上一个LinearLayout,代码我就不提供了,看看adapter的代码。
TabAdapter

class TableAdapter :RecyclerView.Adapter<TableAdapter.ViewHolder>(){
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.table_item_test, parent, false)).also{vh ->
        scrollManager?.also {
            // 第2个参数传入clickArea
            vh.scrollLayout.setScrollManagerAndClickArea(it, vh.clickArea)
        }
    }
    
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.apply {
            // 这里记得改为clickArea,不要使用scrollLayout
            scrollManager?.scrollSpecialView(clickArea)
            clickArea.setOnClickListener {
                Toast.makeText(itemView.context, "toast", Toast.LENGTH_SHORT).show()
            }
        }
    }
}

    class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView){
        ...
        val clickArea: View = itemView.findViewById(R.id.click_area)
        ,,,
    }
}

好了,剩下滚动条,上面在编写ScrollManager时,我就留出了相应的接口,所以只要实现接口的功能就可以了。
滚动条我是使用RecyclerView的ItemDecoration实现,使用ItemDecoration的drawOver方法,就可以将滚动条绘制在RecyclerView的上面。
TabScrollBar

class TabScrollBar(private val recyclerView: RecyclerView) : RecyclerView.ItemDecoration(), ScrollManager.HorizontalScrollBar {
    private var barWidth = 150f
    private var barHeight = 10f
    private var barMarginHorizontal = 20f
    private var barMarginBottom = 20f
    private var barMarginFirstColumn = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 100f, recyclerView.context.resources.displayMetrics)

    private var scrollPosition = 0f
    private var isShowing = false
    private var barShowingTime = 500L

    private val rect = RectF()
    private val paint = Paint().also {
        it.isAntiAlias = true
        it.style = Paint.Style.FILL
        it.color = 0xffcccccc.toInt()
    }

    private val handler = Handler(Looper.getMainLooper())
    private val dismissAction = Runnable {
        isShowing = false
        // 必须通过RecyclerView的invalidate方法才能隐藏ScrollBar
        // 原因是:ItemDecoration是在RecyclerView的draw方法绘制的,需要让RecyclerView刷新一次界面,才不会将不想出现的内容绘制出来
        recyclerView.postInvalidateOnAnimation()
    }

    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDrawOver(c, parent, state)
        // 如果不是showing,就return
        if (isShowing.not()) {
            return
        }
        val width = parent.width
        val height = parent.height
        val offset = (width - 2 * barMarginHorizontal - barMarginFirstColumn.toInt() - barWidth) * scrollPosition

        val left = barMarginHorizontal + barMarginFirstColumn.toInt() + offset.toInt()
        val right = left + barWidth
        val bottom = height - barMarginBottom
        val top = bottom - barHeight

        rect.left = left
        rect.top = top
        rect.right = right
        rect.bottom = bottom

        val rx = barHeight / 2
        val ry = barHeight / 2

        c.drawRoundRect(rect, rx, ry, paint)
    }

    override fun updateScrollWeight(wieght: Float) {
        scrollPosition = wieght
        isShowing = true
        // 这个invalidate是我自己写的,最终是调用RecyclerView.invalidateItemDecorations刷新ItemDecoration
        // 通过这个方法,就可以实时更新scrollPosition
        invalidate()
    }

    override fun startCountToHide() {
        handler.removeCallbacks(dismissAction)
        handler.postDelayed(dismissAction, barShowingTime)
    }

    fun setBarWidth(barWidth: Float) {
        if (this.barWidth == barWidth) {
            return
        }
        this.barWidth = barWidth
        invalidate()
    }

    fun setBarHeight(barHeight: Float) {
        if (this.barHeight == barHeight) {
            return
        }
        this.barHeight = barHeight
        invalidate()
    }

    fun setBarMarginHorizontal(barMarginHorizontal: Float) {
        if (this.barMarginHorizontal == barMarginHorizontal) {
            return
        }
        this.barMarginHorizontal = barMarginHorizontal
        invalidate()
    }

    fun setBarMarginBottom(barMarginBottom: Float) {
        if (this.barMarginBottom == barMarginBottom) {
            return
        }
        this.barMarginBottom = barMarginBottom
        invalidate()
    }

    fun setBarMarginFirstColum(barMarginFirstColumn: Float) {
        if (this.barMarginFirstColumn == barMarginFirstColumn) {
            return
        }
        this.barMarginFirstColumn = barMarginFirstColumn
        invalidate()
    }

    fun setBarShowingTime(barShowingTime: Long){
        this.barShowingTime = barShowingTime
    }

    fun setBarColor(color: Int) {
        if (paint.color == color){
            return
        }
        paint.color = color
        invalidate()
    }

    private fun invalidate() {
        recyclerView.invalidateItemDecorations()
    }
}

Activity

val scrollBar = TabScrollBar(recycler)
recycler.addItemDecoration(scrollBar)
scrollManager.setScrollBar(scrollBar)

效果图:
在这里插入图片描述
可以看到,效果已经和最上面的图片一样了,实现方式还是比较简单的。

好了通过上面这么多代码,就做出TabView。再提一下我在实际开发中关于TabView的开发规范吧。
Adapter layout文件命名:我这边使用的是table_item_xxx,这样别人一看,就知道这是一个和tab item有关的layout
header文件命名:table_header_xxx,和上面一样。
而item和header的layout文件里面,stick的TextView用一个名称的textSize和textColor等参数,这样就可以保证UI改了,我们这边修改比较方便。而GestureLayout里面,使用include标签引入layout文件。item和header使用同一个layout文件,这样就可以降低维护成本。而这个layout文件的顶级标签是merge,所以在明明时,我用的是:merge_table_xxx。这样别人看到之后,就知道这是一个和table有关的merge layout。当然了,table_merge_xxx也可以,看个人或团队的具体情况。

最后再总结一下:
GestureLaout:借助VelocityTacker和Scroller实现松开手指后惯性滑动的功能。Scroller本身不具备滑动的功能,最终实现还是需要用到View的scrollTo/ScrollBy方法。在这个过程中,需要借助View的computeScroll方法来一直调用scrollTo/scrollBy方法让View滚动起来。
ScrollManager:到了RecyclerView层面,就需要用一个Manager来控制所有item。所以GestureLayout就通过interface将touch操作暴露出去,让ScrollManager来统一调用所有的item进行滚动。而ScrollManager是怎么添加和移除item?是使用View的attch方法和detach方法。当View attch时,添加到Manager里面。detach时,从Manager移除。这样就保证了Manager里面不会存在多余的item。
header:直接在RecylerView上面放一个header layout,header layout里面,使用的也是GestureLayout,并add到Manager统一管理。这样当item滚动时,也能带着header一起滚动。
拦截RecyclerView的事件:如果不对RecyclerView的事件进行拦截,在水平滑动时,进行垂直滑动就会打断item滑动的功能。这种体验是非常糟糕的, 因为在实际使用时,很容易就触发这个。不过解决方式也很简单,只需要滑动时,调用requestDisallowInterceptTouchEvent方法让RecyclerView不要拦截touch事件即可。
滚动条:使用RecyclerView.ItemDecoration的drawOver方法,在RecyclerView上面绘制内容即可。如果需要更新滚动条的位置,就使用RecyclerView.invalidateItemDecorations方法,还是比较方便的。

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

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

相关文章

【MYSQL】表的增删改查(进阶)

文章目录 &#x1f337; 1. 数据库约束⭐ 1.1 约束类型⭐ 1.2 NULL约束⭐ 1.3 UNIQUE&#xff1a;唯一约束⭐ 1.4 DEFAULT&#xff1a;默认值约束⭐ 1.5 PRIMARY KEY&#xff1a;主键约束⭐ 1.6 FOREIGN KEY&#xff1a;外键约束⭐ 1.7 CHECK约束&#xff08;了解&#xff09; …

实在智能创意沙画引爆第25届“海博会”,数字员工为电商超自动化转型加“数”

4月18日&#xff0c;由中国纺织工业联合会、中国服装协会、中国服装设计师协会、台湾纺织业拓展会主办&#xff0c;石狮市人民政府指导的第二十五届海峡两岸纺织服装博览会暨科技成果交易会&#xff08;下称“海博会”&#xff09;在石狮市举行。本届海博会以“融合创新、共享时…

Android Jetpack 应用指南 - Navigation

前言 在Android开发的过去几年中&#xff0c;在公司的项目中一直没有机会尝试单Activity多Fragment的开发模式&#xff0c;随着Google推出Navigation组件&#xff0c;我意识到&#xff0c;终于有机会学习一种全新的开发模式了。 与上一篇文章相同&#xff0c;本篇同样是Navig…

Lenovo IdeaPad S540-14IML 电脑 Hackintosh 黑苹果efi引导文件

原文来源于黑果魏叔官网&#xff0c;转载需注明出处。&#xff08;下载请直接百度黑果魏叔&#xff09; 硬件型号驱动情况 主板Lenovo IdeaPad S540-14IML 2019 (Type 81NF) 处理器Intel Core i5-10210U / i7-10510U已驱动 内存Internal 4GB 2666 Changeable 8GB 2666 memo…

Android进程间通信

在操作系统中&#xff0c;每个进程都有一块独立的内存空间。为了保证程序的的安全性&#xff0c;操作系统都会有一套严格的安全机制来禁止进程间的非法访问&#xff0c;但是&#xff0c;很多情况下进程间也是需要相互通信的 进程间通信&#xff08;Inter-process communication…

嵌入式学习笔记——SPI通信的应用

SPI通信的应用 前言屏幕分类1.3OLED概述驱动芯片框图原理图通信时序显示的方式页地址、列地址初始化指令 程序设计初始化代码初始化写数据与写命令清屏函数 初始化代码字符显示函数 总结 前言 上一篇中介绍了STM32的SPI通信&#xff0c;并根据框图和寄存器进行了SPI通信的初始…

轻松掌握k8s(使用docker)安装知识点

1、介绍 kubernetes具有以下特性&#xff1a; 服务发现和负载均衡 Kubernetes 可以使用 DNS 名称或自己的 IP 地址公开容器&#xff0c;如果进入容器的流量很大&#xff0c; Kubernetes 可以负载均衡并分配网络流量&#xff0c;从而使部署稳定。存储编排 Kubernetes 允许你自…

【数据库】— 无损连接、Chase算法、保持函数依赖

【数据库】— 无损连接、Chase算法 Chase算法Chase算法举例一种简便方法&#xff1a;分解为两个模式时无损连接和函数依赖的一个简单例子 Chase算法 形式化定义&#xff1a; 构造一个 k k k行 n n n列的表格&#xff0c;每行对应一个模式 R i ( 1 ≤ i ≤ k ) Ri (1≤i ≤ k)…

计算机组成原理汇总

提示&#xff1a;日落归山海&#xff0c;山海藏深情 文章目录 1.1 计算机的发展1.2 计算机硬件的基本组成1.3 计算机的性能指标2.1.1 进位计数制2.1.2 BCD码2.1.3 无符号整数的表示和运算2.1.4 带符号整数的表示和运算(原反补)2.1.5原反补码的特性对比2.1.6 移码2.1.7 定点小数…

【逗老师的无线电】骚活,GPS热点盒子自动上报APRS位置

逗老师最近整了个有意思的小活&#xff0c;组装了一个有4G网卡带GPS功能的热点盒子&#xff0c;让盒子基于GPS位置信息&#xff0c;自动上报APRS位置帧 全篇亮点 基于GPS和AGPS共同定位基于TCP直接上报APRS数据帧 别说&#xff0c;这小活整完之后&#xff0c;还是有点意思的&…

linux coredump

文章目录 是什么生成原理coredump 的“危害” reference: 一文读懂Coredump文件是如何生成的 GDB是什么&#xff1f; 是什么 简单的讲&#xff1a;当进程接收到某些信号而导致异常退出时&#xff0c;就会生成 coredump 文件 在程序发生某些错误而导致进程异常退出时&#x…

技术分析内核并发消杀器(KCSAN)一文解决!

一、KCSAN介绍 KCSAN(Kernel Concurrency Sanitizer)是一种动态竞态检测器&#xff0c;它依赖于编译时插装&#xff0c;并使用基于观察点的采样方法来检测竞态&#xff0c;其主要目的是检测数据竞争。 KCSAN是一种检测LKMM(Linux内核内存一致性模型)定义的数据竞争(data race…

亿发软件:中大型仓库进出货管理系统解决方案,定制软件让仓储作业高效便捷

中大型仓库出入库管理是传统厂家供应链管理流程的重要部分&#xff0c;直接关乎货物在仓库当中存储的安全&#xff0c;和员工工作的效率。一旦仓库管理当中出现了疏漏&#xff0c;那么货物的信息数据就会发生变动&#xff0c;导致实际与账目不符。人工带来的低效与不可控是传统…

软件测试行业到底有没有前景和出路?

我现在来跟你说说软件测试的真正情况。 首先一个软件做出来&#xff0c;最不能少的人是谁&#xff1f;不用说就是开发&#xff0c;因为开发是最了解软件运作的那个人&#xff0c;早期不少一人撸网站或者APP的例子&#xff0c;相当于一个人同时是产品、研发、测试、运维等等&am…

15-721 Chapter9 数据压缩

Background disk database的瓶颈在disk IO上的话&#xff08;也就是说数据压缩的好处很大&#xff0c;可以比较放心的做&#xff09;&#xff0c;那么内存数据库的瓶颈是多方面的&#xff0c;其中包含cpu。所以我们要在计算量和压缩率&#xff08;DRAM还是有点贵的&#xff0c…

需求管理实践四大流程的注意事项

需求管理实践包括四大流程&#xff1a;需求采集、需求分析、需求筛选和需求处理。 1、需求采集注意事项 需求采集需要通过多种形式对不同用户需求进行收集&#xff0c;并对需求的属性进行详细记录&#xff0c;并记录可追溯的反馈人员&#xff0c;以便后期跟踪修改。 需求管理实…

你不知道的Redis Search 以及安装指南

theme: orange 本文正在参加「金石计划」 这篇文章是为了使用Redis Search 的向量搜索功能提前做的环境准备工作。即讨论如何在准备生产的 linux 环境中安装 RediSearch 和 RedisJSON 模块。 什么是RediSearch&#xff1f; 根据RediSearch的官方文档 RediSearch是这样描述的。 …

线性表,栈和队列(2)

作者&#xff1a;额~我那个早过50了&#xff0c;忘记了 言归正传ca 什么是栈&#xff1f; 小李攒钱买了车&#xff0c;可是他家住在胡同的尽头。胡同很窄&#xff0c;只能通过一辆车&#xff0c;而且是死胡同。小李每天都为停车发愁&#xff0c;如果回家早了停在里面&#x…

pandas笔记:pandas 排序 (sort_values)

1 函数说明 DataFrame.sort_values(by,*, axis0, ascendingTrue, inplaceFalse, kindquicksort, na_positionlast, ignore_indexFalse, keyNone) 2 参数说明 by string或者一组string组成的list&#xff0c;根据什么进行排序 axis{0 or ‘index’, 1 or ‘columns’}ascendi…

其它 Composition API

1.shallowReactive 与 shallowRef shallow有浅的意思 首先在原有的页面上引入它&#xff0c;然后用shallowReactive包裹它 浅层次的意思就是像name&#xff0c;age这些是响应式的&#xff0c;而job就不处理&#xff0c;意思就是只处理第一层数据&#xff0c;像第二层的都不处理…