RecycleView与TabLayout联动展示更多功能列表页面的实现

news2024/11/15 9:41:22

一.前言

  • 对于更多功能页面,使用RecycleView与TabLayout联动方式实现是比较常见的,先上效果图(请大佬们忽略gif的水印)

在这里插入图片描述

  • 单独使用TabLayout和RecycleView都是比较容易的,这里就不做举例了;gif中的列表实际上是RecycleView嵌套了RecycleView,嵌套的RecycleView设置了间距(不是本文的重点,代码会在下方贴出来),实现item均分;
  • 列表的实现借助了开源库:com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.4;
  • 这里个人先讲解实现思路(会配上局部代码,不要在意代码实现),最后再贴出全部的代码;

二.联动效果的实现

  • 联动效果的实现核心在于两个监听的设置。
  • 其一:RecycleView需要设置setOnScrollChangeListener,实现滑动RecyclerView列表的时候,根据最上面一个Item的position来切换TabLayout的tab;
mBinding.recyclerView.setOnScrollChangeListener { _, _, _, _, _ ->
            mBinding.tabLayout.setScrollPosition(
                mManager!!.findFirstVisibleItemPosition(),
                0F,
                true
            )
        }
  • 其二:TabLayout需要设置addOnTabSelectedListener,点击tab的时候,RecyclerView自动滑到该tab对应的item位置;
mBinding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
            override fun onTabSelected(tab: TabLayout.Tab) {
mManager!!.scrollToPositionWithOffset(tab.position, 0)
            }

            override fun onTabUnselected(tab: TabLayout.Tab) {

            }
            override fun onTabReselected(tab: TabLayout.Tab) {
                mManager!!.scrollToPositionWithOffset(tab.position, 0)
            }
        })

三.细节补充

  • 当滑动到RecycleView最后一个item的时候,需要让最后一个item能滑动到
    TabLayout的下方位置,这里的处理方式是:
    • 将RecycleView定义两种不同类型的布局
override fun getItemViewType(position: Int): Int {
        return if (position == mAllFuncationInfos.size) {
            2
        } else {
            mViewTypeItem
        }
    }

  • 同时RecycleView的item数量额外+1
 override fun getItemCount(): Int {
        return mAllFuncationInfos.size + 1
    }
  • 在onCreateViewHolder方法中针对两种不同的item分别返回不同的布局
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
        return if (viewType == mViewTypeItem) {
            val view = LayoutInflater.from(parent.context).inflate(mLayoutResId, parent, false)
            view.post {
                parentHeight = mRecyclerView.height
                itemHeight = view.height
                if (itemTitleHeight == 0) {
                    val childNumber = (view as ViewGroup).childCount
                    if (childNumber > 0) {
                        itemTitleHeight = view.getChildAt(0).height
                    }
                }
            }
            ItemViewHolder(view)
        } else {
            //Footer是最后留白的位置,以便最后一个item能够出发tab的切换
            //需要考虑一个问题,若二级列表中有数据和没有数据 Footer的高度计算存在区别
            val view = View(parent.context)
            if (lastItemChildrenEmpty) {
                view.layoutParams =
                    ViewGroup.LayoutParams(
                        ViewGroup.LayoutParams.MATCH_PARENT,
                        parentHeight - itemTitleHeight
                    )
            } else {
                view.layoutParams =
                    ViewGroup.LayoutParams(
                        ViewGroup.LayoutParams.MATCH_PARENT,
                        parentHeight - itemHeight
                    )
            }
            ItemViewHolder(view)
        }
    }
  • 到此,基本上关键的点都已经完成了,但是呢,还是会有细节。其一:对于TabLayout的addOnTabSelectedListener,如果TabLayout的tab是选中状态,当再次点击的时候,不会执行onTabSelected回调。老规矩,还是上图:
    在这里插入图片描述

  • 最开始TabLayout选中的tab是索引为0的tab,当列表滑动了,再次点击索引为0的tab,没有出现联动效果,因为这次执行的回调不是onTabSelected,而是onTabReselected,所以对应的处理方案应该很清楚了;

  • 接着讲解其它细节,其二:列表的数据源问题,当传递给嵌套的RecycleView的列表数据为空时,且是最后一个item为空,那么底部留白的高度需要重新计算,在前面onCreateViewHolder方法代码已经贴出相关的代码了。

四.代码环节

  • 相关的全部代码
//界面
@Route(path = RouterPathFragment.HomeFour.PAGER_HOME_FOUR)
class ModuleFragment04 :
    BaseSimpleFragment<ModuleFragment04FragmentHome04Binding>(ModuleFragment04FragmentHome04Binding::inflate) {
    private val mSpace = DensityU.dip2px(6F)
    private var mAllFuncationRvAdapter: AllFuncationRvAdapter? = null
    private var mManager: LinearLayoutManager? = null

    private var mAllFuncationInfos: MutableList<AllFunctionInfoRes>? = null
    override fun titBarView(view: View): View = mBinding.funcationTitleBar

    override fun perpareWork() {
        super.perpareWork()
        mBinding.funcationTitleBar.leftView.isVisible = false
    }

    override fun prepareListener() {
        super.prepareListener()
        //滑动RecyclerView list的时候,根据最上面一个Item的position来切换tab
//        mBinding.recyclerView.setOnScrollChangeListener { v, scrollX, scrollY, oldScrollX, oldScrollY ->
        mBinding.recyclerView.setOnScrollChangeListener { _, _, _, _, _ ->
            mBinding.tabLayout.setScrollPosition(
                mManager!!.findFirstVisibleItemPosition(),
                0F,
                true
            )
        }

        mBinding.tabLayout.setSelectedTabIndicatorColor(
            ContextCompat.getColor(
                requireContext(),
                R.color.color_000000
            )
        )
        mBinding.tabLayout.setTabTextColors(
            ContextCompat.getColor(requireContext(), R.color.color_ff585858),
            ContextCompat.getColor(requireContext(), R.color.color_000000)
        )
        mBinding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
            override fun onTabSelected(tab: TabLayout.Tab) {
                //点击tab的时候,RecyclerView自动滑到该tab对应的item位置
                //当tab是选中状态,再次点击是不会回调该方法,将下方代码在onTabReselected回调中添加即可解决问题
                mManager!!.scrollToPositionWithOffset(tab.position, 0)
            }

            override fun onTabUnselected(tab: TabLayout.Tab) {
                
            }
            override fun onTabReselected(tab: TabLayout.Tab) {
                mManager!!.scrollToPositionWithOffset(tab.position, 0)
            }
        })

        mAllFuncationRvAdapter!!.setOpenFunctionActivityInterface(object :
            AllFuncationRvAdapter.OpenFunctionActivityInterface{
            override fun openFunctionActivity(childrenBean: AllFunctionInfoRes.ChildrenBean) {
                openActivityByFunction(childrenBean)
            }
        })
    }

    private fun openActivityByFunction(childrenBean: AllFunctionInfoRes.ChildrenBean) {
        val attributesBean: AttributesBean? = childrenBean.attributes

        if(attributesBean != null){
            if(attributesBean.appFunctionName == "CardLayout"){
                openActivityByARouter(RouterPathActivity.SimpleRv.PAGER_SIMPLE_RV);
            }
        }
    }

    private fun initAdapter() {
        mAllFuncationInfos = mutableListOf()

        val jsonListInfos = JsonU.json2List(
            jsonFileName = "treeListInfo.json",
            clazz = AllFunctionInfoRes::class.java
        )

        if (!jsonListInfos.isNullOrEmpty()) {
            mAllFuncationInfos!!.addAll(jsonListInfos)
        }

        if (!mAllFuncationInfos.isNullOrEmpty()) {
            val itemChildren =
                mAllFuncationInfos!![mAllFuncationInfos!!.size - 1].children
            lastItemChildrenEmpty = itemChildren!!.isEmpty()
        }
    }

    var lastItemChildrenEmpty = false

    @SuppressLint("NotifyDataSetChanged")
    private fun setAllFuncationData() {
        mAllFuncationRvAdapter = AllFuncationRvAdapter(
            mAllFuncationInfos!!, lastItemChildrenEmpty,
            mBinding.recyclerView, mSpace, R.layout.item_all_funcation
        )
        mManager = LinearLayoutManager(context)
        mBinding.recyclerView.layoutManager = mManager
        mBinding.recyclerView.adapter = mAllFuncationRvAdapter
        RecycleViewU.setMaxFlingVelocity(mBinding.recyclerView, 10000)
        initTablayout()
        mAllFuncationRvAdapter!!.notifyDataSetChanged()
    }

    override fun prepareData() {
        super.prepareData()
        initAdapter()
        setAllFuncationData()
    }

    private fun initTablayout() {
        mBinding.tabLayout.tabMode = TabLayout.MODE_SCROLLABLE
        for (i in mAllFuncationInfos!!.indices) {
            val allFunctionInfoRes = mAllFuncationInfos!![i]
            mBinding.tabLayout.addTab(
                mBinding.tabLayout.newTab().setText(allFunctionInfoRes.name).setTag(i)
            )
        }
    }

}

//适配器
class AllFuncationRvAdapter(
    allFunctionInfoRes: MutableList<AllFunctionInfoRes>,
    private var lastItemChildrenEmpty: Boolean,
    recyclerView: RecyclerView,
    space: Int,
    layoutResId: Int
) : BaseQuickAdapter<AllFunctionInfoRes, BaseViewHolder>(layoutResId, data = allFunctionInfoRes) {

    private val mViewTypeItem = 1
    private var parentHeight = 0
    private var itemHeight = 0
    private var itemTitleHeight = 0
    private var mSpace: Int = space
    private var mRecyclerView: RecyclerView = recyclerView
    private var mAllFuncationInfos: List<AllFunctionInfoRes> = allFunctionInfoRes
    private var mLayoutResId = layoutResId

    override fun convert(holder: BaseViewHolder, item: AllFunctionInfoRes) {
        //负责将每一个将每一个子项holder绑定数据
        if (holder.itemViewType == mViewTypeItem) {
            holder.setText(R.id.item_title_tv, item.name)
            holder.setImageResource(R.id.item_titie_iv, R.drawable.icon_three)
            val recyclerView = holder.getView<RecyclerView>(R.id.item_recycler_view)
            recyclerView.setHasFixedSize(true)
            recyclerView.layoutManager =
                GridLayoutManager(
                    ContextU.context(), 4,
                    GridLayoutManager.VERTICAL, false
                )

            if (recyclerView.itemDecorationCount == 0) {    //只能设置一次
                recyclerView.addItemDecoration(
                    GridSpacingItemDecoration(
                        4,
                        mSpace,
                        true
                    )
                )
            }

//            当我们确定Item的改变不会影响RecyclerView的宽高的时候可以设置setHasFixedSize(true)
//            https://blog.csdn.net/wsdaijianjun/article/details/74735039
            recyclerView.setHasFixedSize(true);

            //可以做一下缓存 避免每次滑动都重新设置
            val itemRecyclerViewAdapter =
                ItemRecyclerViewAdapter(R.layout.item_recycle_inner_content)
            recyclerView.adapter = itemRecyclerViewAdapter
            itemRecyclerViewAdapter.setNewInstance(item.children)

            itemRecyclerViewAdapter.setOnItemClickListener { adapter, _, position ->
                val childrenBean = adapter.getItem(position) as ChildrenBean
                if (mOpenFunctionActivityInterface != null) {
                    mOpenFunctionActivityInterface!!.openFunctionActivity(childrenBean)
                }
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
        return if (viewType == mViewTypeItem) {
            val view = LayoutInflater.from(parent.context).inflate(mLayoutResId, parent, false)
            view.post {
                parentHeight = mRecyclerView.height
                itemHeight = view.height
                if (itemTitleHeight == 0) {
                    val childNumber = (view as ViewGroup).childCount
                    if (childNumber > 0) {
                        itemTitleHeight = view.getChildAt(0).height
                    }
                }
            }
            ItemViewHolder(view)
        } else {
            //Footer是最后留白的位置,以便最后一个item能够出发tab的切换
            //需要考虑一个问题,若二级列表中有数据和没有数据 Footer的高度计算存在区别
            val view = View(parent.context)
            if (lastItemChildrenEmpty) {
                view.layoutParams =
                    ViewGroup.LayoutParams(
                        ViewGroup.LayoutParams.MATCH_PARENT,
                        parentHeight - itemTitleHeight
                    )
            } else {
                view.layoutParams =
                    ViewGroup.LayoutParams(
                        ViewGroup.LayoutParams.MATCH_PARENT,
                        parentHeight - itemHeight
                    )
            }
            ItemViewHolder(view)
        }
    }

    override fun getItemCount(): Int {
        return mAllFuncationInfos.size + 1
    }

    //若使用Java语言开发,则不需要做该处理
    override fun getItem(position: Int): AllFunctionInfoRes {
        //需要重写一下该方法做特殊处理
        if (position == mAllFuncationInfos.size) {       //做拦截处理 避免 super.getItem(position)执行时出现索引越界
            return AllFunctionInfoRes()                  //返回一个空的AllFunctionInfoRes即可
        }
        return super.getItem(position)
    }

    override fun getItemViewType(position: Int): Int {
        return if (position == mAllFuncationInfos.size) {
            2
        } else {
            mViewTypeItem
        }
    }

    internal inner class ItemViewHolder(itemView: View) : BaseViewHolder(itemView)

    //使用接口回调
    private var mOpenFunctionActivityInterface: OpenFunctionActivityInterface? = null

    interface OpenFunctionActivityInterface {
        fun openFunctionActivity(childrenBean: ChildrenBean)
    }

    fun setOpenFunctionActivityInterface(openFunctionActivityInterface: OpenFunctionActivityInterface) {
        mOpenFunctionActivityInterface = openFunctionActivityInterface
    }
}

//适配的布局文件
<?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="wrap_content"
    tools:ignore="ResourceName">

    <LinearLayout
        android:id="@+id/item_title"
        android:layout_width="match_parent"
        android:layout_height="@dimen/dp_30"
        android:orientation="horizontal"
        android:gravity="center_vertical"
        android:layout_marginLeft="@dimen/dp_7"
        android:layout_marginRight="@dimen/dp_7"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <ImageView
            android:id="@+id/item_titie_iv"
            android:layout_width="@dimen/dp_10"
            android:layout_height="@dimen/dp_10"
            android:src="@drawable/icon_three"
            android:layout_marginLeft="@dimen/dp_8" />

        <TextView
            android:id="@+id/item_title_tv"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="@dimen/dp_4"
            android:textSize="@dimen/sp_15" />

    </LinearLayout>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/item_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginTop="@dimen/dp_7"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/item_title"/>

</androidx.constraintlayout.widget.ConstraintLayout>

//Rv间距设置工具类
public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration {
    private int     spanCount;
    private int     spacing;
    private boolean includeEdge;

    public GridSpacingItemDecoration(int spanCount, int spacing, boolean includeEdge) {
        this.spanCount = spanCount;
        this.spacing = spacing;
        this.includeEdge = includeEdge;
    }

    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, RecyclerView parent, @NonNull RecyclerView.State state) {
        int position = parent.getChildAdapterPosition(view); // 获取view 在adapter中的位置
        int column = position % spanCount; // view 所在的列

        if (includeEdge) {
            outRect.left = spacing - column * spacing / spanCount; // spacing - column * ((1f / spanCount) * spacing)
            outRect.right = (column + 1) * spacing / spanCount; // (column + 1) * ((1f / spanCount) * spacing)
            if (position < spanCount) { // 第一行
                outRect.top = spacing;
            }
            outRect.bottom = spacing;
        } else {
            //等间距需满足两个条件:
            //1.各个模块的大小相等,即 各列的left+right 值相等;
            //2.各列的间距相等,即 前列的right + 后列的left = 列间距;

            //公式是需要推演的[演示了当列数为2或者3的时候,验证了公式是成立的]: 资料---https://blog.csdn.net/JM_beizi/article/details/105364227
            //注:这里用的所在列数为从0开始
            outRect.left = column * spacing / spanCount; //某列的left = 所在的列数 * (列间距 * (1 / 列数))
            outRect.right = spacing - (column + 1) * spacing / spanCount; //某列的right = 列间距 - 后列的left = 列间距 -(所在的列数+1) * (列间距 * (1 / 列数))
            if (position >= spanCount) {    //说明不是在第一行
                outRect.top = spacing;
            }
        }
    }
}

五.总结

  • TabLayout和RecycleView的联动关键在于两个监听的设置,同时将上方提及的几个细节注意一下即可;

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

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

相关文章

权限控制导入到项目中

在项目中应用 进行认证和授权需要前面课程中提到的权限模型涉及的7张表支撑&#xff0c;因为用户信息、权限信息、菜单信息、角色信息、关联信息等都保存在这7张表中&#xff0c;也就是这些表中的数据是进行认证和授权的依据。所以在真正进行认证和授权之前需要对这些数据进行…

( “树” 之 BST) 501. 二叉搜索树中的众数 ——【Leetcode每日一题】

二叉查找树&#xff08;BST&#xff09;&#xff1a;根节点大于等于左子树所有节点&#xff0c;小于等于右子树所有节点。 二叉查找树中序遍历有序。 ❓501. 二叉搜索树中的众数 难度&#xff1a;简单 给你一个含重复值的二叉搜索树&#xff08;BST&#xff09;的根节点 root…

Leetcode每日一题——“合并两个有序数组”

各位CSDN的uu们你们好呀&#xff0c;又到小雅兰的愉快题解时候啦&#xff0c;今天&#xff0c;我们的题目内容是合并两个有序数组&#xff0c;下面&#xff0c;让我们进入合并两个有序数组的世界吧 示例 1&#xff1a; 输入&#xff1a;nums1 [1,2,3,0,0,0], m 3, nums2 [2,…

C++内联/构造函数详解

内联函数 宏&#xff1a; 宏的优缺点&#xff1f; 优点&#xff1a; 1.增强代码的复用性。 2.提高性能。 缺点&#xff1a; 1.展开后会使得代码长度变长&#xff0c;使可执行程序变大 2.不方便调试宏。&#xff08;因为预编译阶段进行了替换&#xff09; 3.导致代码可读性差…

Python 查看数据常用函数

Python 查看数据常用函数&#xff08;以 iris 数据集为例&#xff09; 1、查看前后几行数据&#xff1a;head 和 tail2、查看数据基本信息&#xff1a;info3、查看数据统计信息&#xff1a;describe 查看数据可以用很多函数&#xff0c;这里就挑选几个最常用的进行简单展示&…

SpringBoot自动配置原理、手写一个xxx-spring-boot-starter

SpringBoot的自动配置是&#xff1a;当项目中使用了一个第三方依赖&#xff0c;如何将第三方依赖中的Bean加载到Spring的IOC容器中&#xff0c;我们就可以做到无需额外的配置&#xff0c;直接使用第三方jar中的Bean。 SpringBoot的理念是“约定大于配置”&#xff0c;只要按照S…

【下载器篇】IDM下载记录分析(简)

【下载器篇】IDM下载记录分析&#xff08;简&#xff09; IDM下载记录分析-未完待续—【蘇小沐】 文章目录 【下载器篇】IDM下载记录分析&#xff08;简&#xff09;1.实验环境 &#xff08;一&#xff09;IDM下载记录分析-未完待续临时文件夹下载痕迹 总结 1.实验环境 系统版…

【内网渗透】春秋云镜Intitle WP

前言 第一次正式接触内网渗透的东西&#xff0c;写的很新手&#xff0c;也适合新手观看&#xff0c;有问题可以私信或评论&#xff0c;接下来会持续更新 信息收集 拿到地址先nmap扫端口 没什么发现&#xff0c;直接访问80端口&#xff0c;看到图标知道是thinkphp 第一台Th…

leetcode刷题(8)二叉树(2)

各位朋友们&#xff0c;大家好&#xff01;今天我为大家分享的是关于二叉树leetcode刷题的第二篇&#xff0c;我们一起来看看吧。 文章目录 1.对称二叉树题目要求示例做题思路代码实现 2.二叉树的最大深度题目要求示例做题思路代码实现 3.翻转二叉树题目要求示例做题思路代码实…

WebSocket入门

WebSocket 1.1websoket介绍 websocket是一种网络通信协议&#xff0c;RFC6455定义了它的通信标准 websocket是Html5开始提供的一种在单个TCP连接上进行全双工通讯的协议 Http协议是一种无状态、无连接、单向的应用层协议&#xff0c;它采用了请求/响应模型&#xff0c;通信…

Tomcat多实例部署实验

引言 本文主要内容是tomcat的多实例配置实验。 一、实验准备 Tomcat多实例是指在一台设备上运行多个Tomcat服务&#xff0c;这些Tomcat相互独立&#xff0c;互不影响。多实例与虚拟主机不同&#xff0c;虚拟主机的本质是在一个服务下有多个相对独立的目录&#xff0c;但是多实…

OFA(One-For-All)阿里达摩院实现架构、模态、任务的三个统一之Image Captioning

OFA(One-For-All) 通用多模态预训练模型&#xff0c;使用简单的序列到序列的学习框架统一模态&#xff08;跨模态、视觉、语言等模态&#xff09;和任务&#xff08;如图片生成、视觉定位、图片描述、图片分类、文本生成等&#xff09; 架构统一&#xff1a;使用统一的transfo…

何谓SRIO——RapidIO之旅从这里开始

何谓SRIO——RapidIO之旅从这里开始 SRIO&#xff08;Serial RapidIO&#xff09;协议是一种用于高速串行通信的协议&#xff0c;旨在连接数字信号处理器&#xff08;DSP&#xff09;、网络处理器、FPGA等芯片&#xff0c;以及它们之间的互连。SRIO协议具有低延迟、高带宽&…

【单链表】

单链表 1. 函数的声明部分2. 函数的实现部分&#xff08;1&#xff09;打印链表&#xff08;2&#xff09;头插&#xff08;3&#xff09;尾插&#xff08;3&#xff09;头删&#xff08;4&#xff09;尾删&#xff08;5&#xff09;单链表的查找&#xff08;6&#xff09;删除…

leetcode 879. Profitable Schemes(有利润的计划)

有几个工程&#xff0c;每个工程需要group[ i ]个人去做&#xff0c;做完了可以得到profit[ i ]的利润。 现有2个限制条件&#xff1a; 人数上限是n, 且参加了一个工程的人不能再参加其他工程。 利润下限minProfit, 至少要获得minProfit的利润。 问有多少种工程的选法&#xff…

Zuul源码解析(一)

说在前面 我们公司有一个线上服务报错通知群&#xff0c;经常报网关服务的一个 EOFException 异常。这个异常报出来好久了&#xff0c;如下图所示&#xff0c;艾特相关的人也不去处理&#xff0c;大概是不重要异常吧&#xff0c;反正看样子是不影响线上核心业务流程。 然后我上…

FreeRTOS学习笔记(一)——初识FreeRTOS

FreeRTOS官网&#xff1a;FreeRTOS - 适用于具有物联网扩展功能的嵌入式系统的市场领先 RTOS&#xff08;实时操作系统&#xff09; FreeRTOS源码下载&#xff1a;FreeRTOS Real Time Kernel (RTOS) - Browse /FreeRTOS at SourceForge.net 目录 0x01 FreeRTOS编程风格 一…

用CentOS服务器自己搭建部署个Discuz论坛网站,网站搭建教程

Linux系统CentOS服务器使用堡塔搭建论坛网站全套教程。服务器大本营&#xff0c;技术文章内容集合站发车啦&#xff01; 操作系统&#xff1a;Centos 7.6 网站程序&#xff1a;Discuz-X3.4 前言 首先&#xff0c;搭建一个网站需要准备&#xff1a;服务器、域名、网站程序。 …

PWM控制直流电机

一&#xff0c;TB6612电机驱动模块 直流电机属于大功率器件&#xff0c;GPIO无法直接驱动&#xff0c;需要电机驱动模块配合&#xff0c;才能驱动直流电机. TB6612可以驱动2个直流电机。由IN1&#xff0c;IN2控制电机旋转方向&#xff0c;由PWM控制电机旋转速度。 二&#xf…

基于Oracle VM VirtualBox的ubuntu的安装

基于Oracle VM VirtualBox的ubuntu的安装 感谢詹老师的帮助使我得以完成本次安装&#xff0c;以下为本次安装的一个小小的记录。 目录 基于Oracle VM VirtualBox的ubuntu的安装Oracle VM VirtualBox的下载与安装ubuntu的下载Oracle VM VirtualBox下安装ubuntu安装 ROS Melodi…