Android 实现菜单拖拽排序

news2024/11/18 3:36:43

效果图

简介

本文主角是ItemTouchHelper。

它是RecyclerView对于item交互处理的一个「辅助类」,主要用于拖拽以及滑动处理。

以接口实现的方式,达到配置简单、逻辑解耦、职责分明的效果,并且支持所有的布局方式。

功能拆解

功能实现

4.1、实现接口

自定义一个类,实现ItemTouchHelper.Callback接口,然后在实现方法中根据需求简单配置即可。

class DragCallBack(adapter: DragAdapter, data: MutableList<String>) : ItemTouchHelper.Callback() {
}

ItemTouchHelper.Callback必须实现的3个方法:

  • getMovementFlags

  • onMove

  • onSwiped

其他方法还有onSelectedChanged、clearView等。

4.1.1、getMovementFlags

用于创建交互方式,交互方式分为两种:

1. 拖拽,网格布局支持上下左右,列表只支持上下(LEFT、UP、RIGHT、DOWN)。

2. 滑动,只支持前后(START、END)。

最后,通过makeMovementFlags把结果返回回去,makeMovementFlags接收两个参数,dragFlags和swipeFlags,即上面拖拽和滑动组合的标志位。

override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
    var dragFlags = 0
    var swipeFlags = 0
    when (recyclerView.layoutManager) {
        is GridLayoutManager -> {
            // 网格布局
            dragFlags = ItemTouchHelper.LEFT or ItemTouchHelper.UP or ItemTouchHelper.RIGHT or ItemTouchHelper.DOWN
            return makeMovementFlags(dragFlags, swipeFlags)
        }
        is LinearLayoutManager -> {
            // 线性布局
            dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
            swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
            return makeMovementFlags(dragFlags, swipeFlags)
        }
        else -> {
            // 其他情况可自行处理
            return 0
        }
    }
}

4.1.2、onMove

拖拽时回调,这里我们主要对起始位置和目标位置的item做一个数据交换,然后刷新视图显示。

override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
    // 起始位置
    val fromPosition = viewHolder.adapterPosition
    // 结束位置
    val toPosition = target.adapterPosition
    // 固定位置
    if (fromPosition == mAdapter.fixedPosition || toPosition == mAdapter.fixedPosition) {
        return false
    }
    // 根据滑动方向 交换数据
    if (fromPosition < toPosition) {
        // 含头不含尾
        for (index in fromPosition until toPosition) {
            Collections.swap(mData, index, index + 1)
        }
    } else {
        // 含头不含尾
        for (index in fromPosition downTo toPosition + 1) {
            Collections.swap(mData, index, index - 1)
        }
    }
    // 刷新布局
    mAdapter.notifyItemMoved(fromPosition, toPosition)
    return true
}

4.1.3、onSwiped

滑动时回调,这个回调方法里主要是做数据和视图的更新操作。

override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
        if (direction == ItemTouchHelper.START) {
            Log.i(TAG, "START--->向左滑")
        } else {
            Log.i(TAG, "END--->向右滑")
        }
        val position = viewHolder.adapterPosition
        mData.removeAt(position)
        mAdapter.notifyItemRemoved(position)
    }

4.2、绑定RecyclerView

上面接口实现部分我们已经简单写好了,逻辑也挺简单,总共不超过100行代码。

接下来就是把这个辅助类绑定到RecyclerView。

RecyclerView显示的实现就是基础的样式,就不展开了,可以查看源码。

val dragCallBack = DragCallBack(mAdapter, list)
val itemTouchHelper = ItemTouchHelper(dragCallBack)
itemTouchHelper.attachToRecyclerView(mBinding.recycleView)

绑定只需要调用attachToRecyclerView就好了。

至此,简单的效果就已经实现了。下面开始优化和进阶的部分。

4.3、设置分割线

RecyclerView网格布局实现等分,我们一般先是自定义ItemDecoration,然后调用addItemDecoration来实现的。

但是我在实现效果的时候遇到一个问题,因为我加了布局切换的功能,在每次切换的时候,针对不同的布局分别设置layoutManager和ItemDecoration,这就导致随着切换次数的增加,item的间隔就越大。

addItemDecoration,顾名思义是添加,通过查看源码发现RecyclerView内部是有一个ArrayList来维护的,所以当我们重复调用addItemDecoration方法时,分割线是以递增的方式在增加的,并且在绘制的时候会从集合中遍历所有的分割线绘制。

部分源码:

@Override
public void draw(Canvas c) {
    super.draw(c);

    final int count = mItemDecorations.size();
    for (int i = 0; i < count; i++) {
        mItemDecorations.get(i).onDrawOver(c, this, mState);
    }
    //...
}

既然知道了问题所在,也大概想到了3种解决办法:

1. 调用addItemDecoration前,先调用removeItemDecoration方法remove掉之前所有的分割线。

2. 调用addItemDecoration(@NonNull ItemDecoration decor, int index),通过index来维护。

3. add时通过一个标示来判断,添加过就不添加了。

好像可行,实际上并不太行...因为始终都有两个分割线实例。

我们再来梳理一下:

  • 两种不同的布局

  • 都有分割线

  • 分割线只需设置一次

我想到另外一个办法,不对RecyclerView做处理了,既然两种布局都有分割线,是不是可以把分割线合二为一了,然后根据LayoutManager去绘制不同的分割线?

理论上是可行的,事实上也确实可以...

自定义分割线:

class GridSpaceItemDecoration(private val spanCount: Int, private val spacing: Int = 20, private var includeEdge: Boolean = false) :
    RecyclerView.ItemDecoration() {

    override fun getItemOffsets(outRect: Rect, view: View, recyclerView: RecyclerView, state: RecyclerView.State) {
        recyclerView.layoutManager?.let {
            when (recyclerView.layoutManager) {
                is GridLayoutManager -> {
                    val position = recyclerView.getChildAdapterPosition(view) // 获取item在adapter中的位置
                    val column = position % spanCount // item所在的列
                    if (includeEdge) {
                        outRect.left = spacing - column * spacing / spanCount
                        outRect.right = (column + 1) * spacing / spanCount
                        if (position < spanCount) {
                            outRect.top = spacing
                        }
                        outRect.bottom = spacing
                    } else {
                        outRect.left = column * spacing / spanCount
                        outRect.right = spacing - (column + 1) * spacing / spanCount
                        if (position >= spanCount) {
                            outRect.top = spanCount
                        }
                        outRect.bottom = spacing
                    }
                }
                is LinearLayoutManager -> {
                    outRect.top = spanCount
                    outRect.bottom = spacing
                }
            }
        }
    }

}

4.4、选中放大/背景变色

为了提升用户体验,可以在拖拽的时候告诉用户当前拖拽的是哪个item,比如选中的item放大、背景高亮等。

  • 网格布局,选中变大。

  • 列表布局,背景变色。

这里用到ItemTouchHelper.Callback中的两个方法,onSelectedChanged和clearView,我们需要在选中时改变视图显示,结束时再恢复。

4.4.1、onSelectedChanged

拖拽或滑动 发生改变时回调,这时我们可以修改item的视图。

override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
    if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
        viewHolder?.let {
            // 因为拿不到recyclerView,无法通过recyclerView.layoutManager来判断是什么布局,所以用item的宽度来判断
            // itemView.width > 500 用这个来判断是否是线性布局,实际取值自己看情况
            if (it.itemView.width > 500) {
                // 线性布局 设置背景颜色
                val drawable = it.itemView.background as GradientDrawable
                drawable.color = ContextCompat.getColorStateList(it.itemView.context, R.color.greenDark)
            } else {
                // 网格布局 设置选中放大
                ViewCompat.animate(it.itemView).setDuration(200).scaleX(1.3F).scaleY(1.3F).start()
            }
        }
    }
    super.onSelectedChanged(viewHolder, actionState)
}

actionState:

  • ACTION_STATE_IDLE 空闲状态。

  • ACTION_STATE_SWIPE 滑动状态。

  • ACTION_STATE_DRAG 拖拽状态。

4.4.2、clearView

拖拽或滑动 结束时回调,这时我们要把改变后的item视图恢复到初始状态。

override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
    // 恢复显示
    // 这里不能用if判断,因为GridLayoutManager是LinearLayoutManager的子类,改用when,类型推导有区别
    when (recyclerView.layoutManager) {
        is GridLayoutManager -> {
            // 网格布局 设置选中大小
            ViewCompat.animate(viewHolder.itemView).setDuration(200).scaleX(1F).scaleY(1F).start()
        }
        is LinearLayoutManager -> {
            // 线性布局 设置背景颜色
            val drawable = viewHolder.itemView.background as GradientDrawable
            drawable.color = ContextCompat.getColorStateList(viewHolder.itemView.context, R.color.greenPrimary)
        }
    }
    super.clearView(recyclerView, viewHolder)
}

4.5、固定位置

在实际需求中,交互可能要求我们第一个菜单不可以变更顺序,只能固定,比如效果中的第一个菜单「推荐」固定在首位这种情况。

4.5.1、修改adapter

定义一个固定值,并设置不同的背景色和其他菜单区分开。

class DragAdapter(private val mContext: Context, private val mList: List<String>) : RecyclerView.Adapter<DragAdapter.ViewHolder>() {

    val fixedPosition = 0 // 固定菜单

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.mItemTextView.text = mList[position]

        // 第一个固定菜单
        val drawable = holder.mItemTextView.background as GradientDrawable
        if (holder.adapterPosition == 0) {
            drawable.color = ContextCompat.getColorStateList(mContext, R.color.greenAccent)
        }else{
            drawable.color = ContextCompat.getColorStateList(mContext, R.color.greenPrimary)
        }
    }
    //...
}

4.5.1、修改onMove回调

在onMove方法中判断,只要是固定位置就直接返回false。

class DragCallBack(adapter: DragAdapter, data: MutableList<String>) : ItemTouchHelper.Callback() {
    /**
     * 拖动时回调
     */
    override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
        // 起始位置
        val fromPosition = viewHolder.adapterPosition
        // 结束位置
        val toPosition = target.adapterPosition

        // 固定位置
        if (fromPosition == mAdapter.fixedPosition || toPosition == mAdapter.fixedPosition) {
            return false
        }
        // ...
        return true
    }
}

虽然第一个菜单无法交换位置了,但是它还是可以拖拽的。

效果实现了吗,好像也实现了,可是又好像哪里不对,就好像填写完表单点击提交时你告诉我格式不正确一样,你不能一开始就告诉我吗?

为了进一步提升用户体验,可以让固定位置不可以拖拽吗?

可以,ItemTouchHelper.Callback中有两个方法:

1. isLongPressDragEnabled 是否可以长按拖拽。

2. isItemViewSwipeEnabled 是否可以滑动。

这俩方法默认都是true,所以即使不能交换位置,但默认也是支持操作的。

4.5.3、重写isLongPressDragEnabled

以拖拽举例,我们需要重写isLongPressDragEnabled方法把它禁掉,然后在非固定位置的时候去手动开启。

override fun isLongPressDragEnabled(): Boolean {
    //return super.isLongPressDragEnabled()
    return false
}

禁掉之后什么时候再触发呢?

因为我们现在的交互是长按进入编辑,那就需要在长按事件中再调用startDrag手动开启。

mAdapter.setOnItemClickListener(object : DragAdapter.OnItemClickListener {
    //...
    override fun onItemLongClick(holder: DragAdapter.ViewHolder) {
        if (holder.adapterPosition != mAdapter.fixedPosition) {
            itemTouchHelper.startDrag(holder)
        }
    }
})

ok,这样就完美实现了。

4.6、其他

4.6.1、position

因为有拖拽操作,下标其实是变化的,在做相应的操作时,要取实时位置。

holder.adapterPosition

4.6.2、重置

不管是拖拽还是滑动,其实本质都是对Adapter内已填充的数据进行操作,实时数据通过Adapter获取即可。

如果想要实现重置功能,直接拿最开始的原始数据重新塞给Adapter即可。

源码探索

看源码时,找对一个切入点,往往能达到事半功倍的效果。

这里就从绑定RecyclerView开始吧。

val dragCallBack = DragCallBack(mAdapter, list)
val itemTouchHelper = ItemTouchHelper(dragCallBack)
itemTouchHelper.attachToRecyclerView(mBinding.recycleView)

实例化ItemTouchHelper,然后调用其attachToRecyclerView方法绑定到RecyclerView。

5.1、attachToRecyclerView

public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
    if (mRecyclerView == recyclerView) {
        return; // nothing to do
    }
    if (mRecyclerView != null) {
        destroyCallbacks();
    }
    mRecyclerView = recyclerView;
    if (recyclerView != null) {
        final Resources resources = recyclerView.getResources();
        mSwipeEscapeVelocity = resources.getDimension(R.dimen.item_touch_helper_swipe_escape_velocity);
        mMaxSwipeVelocity = resources.getDimension(R.dimen.item_touch_helper_swipe_escape_max_velocity);
        setupCallbacks();
    }
}

这段代码其实有点意思的,解读一下:

1. 第一个if判断,避免重复操作,直接return。

2. 第二个if判断,调用了destroyCallbacks,在destroyCallbacks里面做了一些移除和回收操作,说明只能绑定到一个RecyclerView;同时,注意这里判断的主体是mRecyclerView,不是我们传进来的recyclerView,而且我们传进来的recyclerView是支持Nullable的,所以我们可以传个空值走到destroyCallbacks里来做解绑操作。

3. 第三个if判断,当我们传的recyclerView不为空时,调用setupCallbacks。

5.2、setupCallbacks

private void setupCallbacks() {
    ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());
    mSlop = vc.getScaledTouchSlop();
    mRecyclerView.addItemDecoration(this);
    mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);
    mRecyclerView.addOnChildAttachStateChangeListener(this);
    startGestureDetection();
}

这个方法里已经大概可以看出内部实现原理了。

两个关键点:

  • addOnItemTouchListener

  • startGestureDetection

通过触摸和手势识别来处理交互显示。

5.3、mOnItemTouchListener

private final OnItemTouchListener mOnItemTouchListener = new OnItemTouchListener() {
    @Override
    public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent event) {
        mGestureDetector.onTouchEvent(event);
        if (action == MotionEvent.ACTION_DOWN) {
            //...
            if (mSelected == null) {
                if (animation != null) {
                    //...
                    select(animation.mViewHolder, animation.mActionState);
                }
            }
        } else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
            select(null, ACTION_STATE_IDLE);
        } else if (mActivePointerId != ACTIVE_POINTER_ID_NONE) {
            //...
            if (index >= 0) {
                checkSelectForSwipe(action, event, index);
            }
        }
        return mSelected != null;
    }

    @Override
    public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent event) {
        mGestureDetector.onTouchEvent(event);
        //...
        if (activePointerIndex >= 0) {
            checkSelectForSwipe(action, event, activePointerIndex);
        }
        switch (action) {
            case MotionEvent.ACTION_MOVE: {
                if (activePointerIndex >= 0) {
                    moveIfNecessary(viewHolder);
                }
                break;
            }
            //...
        }
    }

    @Override
    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        select(null, ACTION_STATE_IDLE);
    }
};

这段代码删减之后还是有点多,不过没关系,提炼一下,核心通过判断MotionEvent调用了几个方法:

  • select

  • checkSelectForSwipe

  • moveIfNecessary

5.3.1、select

void select(@Nullable ViewHolder selected, int actionState) {
    if (selected == mSelected && actionState == mActionState) {
        return;
    }
    //...
    if (mSelected != null) {
        if (prevSelected.itemView.getParent() != null) {
            final float targetTranslateX, targetTranslateY;
            switch (swipeDir) {
                case LEFT:
                case RIGHT:
                case START:
                case END:
                    targetTranslateY = 0;
                    targetTranslateX = Math.signum(mDx) * mRecyclerView.getWidth();
                    break;
                //...
            }
            //...
        } else {
            removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);
            mCallback.clearView(mRecyclerView, prevSelected);
        }
    }
    //...
    mCallback.onSelectedChanged(mSelected, mActionState);
    mRecyclerView.invalidate();
}

这里面主要是在拖拽或滑动时对translateX/Y的计算和处理,然后通过mCallback.clearView和mCallback.onSelectedChanged回调给我们,最后调用invalidate()实时刷新。

5.3.2、checkSelectForSwipe

void checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) {
    //...
    if (absDx < mSlop && absDy < mSlop) {
        return;
    }
    if (absDx > absDy) {
        if (dx < 0 && (swipeFlags & LEFT) == 0) {
            return;
        }
        if (dx > 0 && (swipeFlags & RIGHT) == 0) {
            return;
        }
    } else {
        if (dy < 0 && (swipeFlags & UP) == 0) {
            return;
        }
        if (dy > 0 && (swipeFlags & DOWN) == 0) {
            return;
        }
    }
    select(vh, ACTION_STATE_SWIPE);
}

这里是滑动处理的check,最后也是收敛到select()方法统一处理。

5.3.3、moveIfNecessary

void moveIfNecessary(ViewHolder viewHolder) {
    if (mRecyclerView.isLayoutRequested()) {
        return;
    }
    if (mActionState != ACTION_STATE_DRAG) {
        return;
    }
    //...
    if (mCallback.onMove(mRecyclerView, viewHolder, target)) {
        // keep target visible
        mCallback.onMoved(mRecyclerView, viewHolder, fromPosition,
                target, toPosition, x, y);
    }
}

这里检查拖拽时是否需要交换item,通过mCallback.onMoved回调给我们。

5.4、startGestureDetection

private void startGestureDetection() {
    mItemTouchHelperGestureListener = new ItemTouchHelperGestureListener();
    mGestureDetector = new GestureDetectorCompat(mRecyclerView.getContext(),
            mItemTouchHelperGestureListener);
}

5.4.1、ItemTouchHelperGestureListener

private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener {
    //...
    @Override
    public void onLongPress(MotionEvent e) {
        //...
        View child = findChildView(e);
        if (child != null) {
            ViewHolder vh = mRecyclerView.getChildViewHolder(child);
            if (vh != null) {
                //...
                if (pointerId == mActivePointerId) {
                    //...
                    if (mCallback.isLongPressDragEnabled()) {
                        select(vh, ACTION_STATE_DRAG);
                    }
                }
            }
        }
    }
}

这里主要是对长按事件的处理,最后也是收敛到select()方法统一处理。

5.5、源码小结

1. 绑定RecyclerView。

2. 注册触摸手势监听。

3. 根据手势,先是内部处理各种校验、位置计算、动画处理、刷新等,然后回调给ItemTouchHelper.Callback。

事儿大概就是这么个事儿,主要工作都是源码帮我们做了,我们只需要在回调里根据结果处理业务逻辑即可。

源码地址

https://github.com/yechaoa/MaterialDesign

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

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

相关文章

【员工管理系统】

员工管理系统前言需求分析系统设计系统框图所需技术系统实现编写代码测试前言 这是一个使用epoll实现TCP并发服务器&#xff0c;并让客户端登录服务器可以进行员工的管理&#xff0c;员工的信息存储在sqlite数据库中&#xff0c;对数据库进行增删改查实现对员工的添加&#xf…

一文理解服务端渲染SSR的原理,附实战基于vite和webpack打造React和Vue的SSR开发环境

SSR和CSR 首先&#xff0c;我们先要了解什么是SSR和CSR&#xff0c;SSR是服务端渲染&#xff0c;CSR是客户端渲染&#xff0c;服务端渲染是指 HTTP 服务器直接根据用户的请求&#xff0c;获取数据&#xff0c;生成完整的 HTML 页面返回给客户端&#xff08;浏览器&#xff09;展…

嵌入式 STM32 通讯协议--MODBUS

目录 一、自定义通信协议 1、协议介绍 2、网络协议 3、自定义的通信协议 二、MODBUS通信协议 1、概述 2、MODBUS帧结构 协议描述 3、MODBUS数据模型 4、MODBUS事务处理的定义 5、MODBUS功能码 6、功能码定义 7、MODBUS数据链路层 8、MODBUS地址规则 9、MO…

SpringBoot 2.x ——使用 mail 实现邮件发送

文章目录前言环境、版本等pom依赖引入springboot项目配置文件获取邮箱授权码配置properties文件定义接口信息接收类编写邮件发送服务类编写接口swagger测试1、简单邮件发送2、html格式发送(支持附件)前言 最近再看xxl-job的源码&#xff0c;其中在邮件告警通知中使用到了告警信…

Go调用dll 解决方案 dll查看工具

准备工作 Go需要1.10版本&#xff0c;即支持动态链接库 基本调用代码 lib : syscall.NewLazyDLL("lib/plugin.dll") // 读取dll f : lib.NewProc("Sum") // 调用dll函数 res, _, _ : f.Call(param) // 传值 fmt.Println(res)可能出现的问题 %1 is not a …

移动硬盘不显示怎么办?恢复硬盘的方法汇总

在日常工作和生活中&#xff0c;移动硬盘是非常重要的存储设备&#xff0c;它们可以储存大量的数据&#xff0c;比如照片、音乐、视频、文档等。但是&#xff0c;有时候你可能会遇到移动硬盘不显示的问题。这个问题通常会让人感到困惑&#xff0c;因为你无法访问移动硬盘里的数…

Appium自动化测试之启动时跳过初始化设置

Appium每次启动时都会检查和安装Appium Settings&#xff0c;这是完全没有必要的&#xff0c;在首次使用Appium连接设备是Appium Settings便已经安装好。怎样跳过安装Appium Settings呢&#xff1f;之前的做法是修改appium中的源文件中的android-helpers.js实现&#xff0c;如M…

足球俱乐部管理系统

技术&#xff1a;Java、JSP等摘要&#xff1a;网站是一种主要的渠道。人们通过互联网快速、准确的发布信息、获取信息。而足球俱乐部是足球职业化、专业化的一个标志&#xff0c;是足球运动员以足球谋生时&#xff0c;所被聘用的机构&#xff0c;应运时代发展&#xff0c;规模、…

观测云产品更新|新增用户访问监测自动化追踪;新增 CDN 质量分析;新增自定义查看器导航菜单等

观测云更新 用户访问监测优化 新增用户访问监测自动化追踪 用户访问监测新增自动化追踪&#xff0c;通过“浏览器插件”的实现方式&#xff0c;使用浏览器记录用户访问行为&#xff0c;创建无代码的端到端测试。更多详情可参考文档【 自动化追踪 】https://docs.guance.com/…

本机安装docker,redis并进行连接实战

1、背景 win10系统&#xff0c;想要在本机搭建一套开发环境&#xff0c;需要安装zk&#xff0c;redis等组件&#xff0c;一个个的安装显然效率太低且复杂&#xff0c;这里考虑安装docker及相关镜像 2、 docker安装 docker官网下载:https://docs.docker.com/desktop/install/…

Git的简述

Git 文章目录GitGit概述版本控制工具集中式管理控制工具分步式管理控制工具控制机制Git和代码托管中心安装Git软件Git常用命令Git概述 Git是一个免费的、开源的分步式版本控制系统&#xff0c;可以快速的处理从小型到大型的各种项目 Git 易于学习&#xff0c;占地面积小&…

【Linux | ELK 8.2】搭建ELKB集群Ⅱ—— 安装 Logstash 和 Kibana

目录2.3 安装Logstash&#xff08;1&#xff09;检查系统jdk版本&#xff08;2&#xff09;下载logstash&#xff08;3&#xff09;安装logstash&#xff08;4&#xff09;配置logstash&#xff08;5&#xff09;启动与测试方法1方法2&#xff08;主要的使用方式&#xff09;&a…

Python--深入浅出的装饰器--1

本章一起深入浅出一下装饰器。前面我们讲过一章装饰器了。不知道各位看懂了多少。每太看懂也没关系&#xff0c;本章就一起实操一下。简单的例子例1例2上述的两个例子&#xff0c;执行结果为&#xff1a;1423.为什么呢&#xff1f;&#xff1f;&#xff1f;解析语法糖&#xff…

sed 功能详解

介绍sedsed是一种流编辑器&#xff0c;它一次处理一行内容&#xff0c;把当前处理的行存储在临时缓冲区中&#xff08;buffer&#xff09;,称为"模式空间"&#xff0c;接着sed命令处理缓冲区中的内容&#xff0c;处理完成后&#xff0c;把缓冲区的内容送往屏幕&#…

面向Elasticsearch的高性能应用网关INFINI Gateway的介绍

1.微服务的API网关介绍网关的含义很多&#xff0c;应用范围也很广&#xff0c;不同的领域理解也不一样&#xff0c;站在分布式领域基于微服务的架构风格中&#xff0c;API网关其实就是一个微服务系统的统一入口。往往微服务是指由多个应用组成的一个个独立的服务系统&#xff0…

【C/C++】类型限定符extern、const、Volatile、register

1、extern&#xff1a; 声明一个变量&#xff0c;extern声明的变量没有建立存储空间。 extern int a ; //变量在定义的时候创建存储空间。 ①当我们在编译器中试图运行以下代码&#xff0c;系统会报错。 错误原因是“无法解析外部符号_a”.系统认为变量a是没有开辟内存空间的…

【3】MyBatis+Spring+SpringMVC+SSM整合一套通关

三、SpringMVC 1、SpringMVC简介 1.1、什么是MVC MVC是一种软件架构的思想&#xff0c;将软件按照模型、视图、控制器来划分 M&#xff1a;Model&#xff0c;模型层&#xff0c;指工程中的JavaBean&#xff0c;作用是处理数据 JavaBean分为两类&#xff1a; 一类称为实体…

网络应用之静态Web服务器-多任务版

静态Web服务器-多任务版学习目标能够写出多线程版的多任务web服务器程序1. 静态Web服务器的问题目前的Web服务器&#xff0c;不能支持多用户同时访问&#xff0c;只能一个一个的处理客户端的请求&#xff0c;那么如何开发多任务版的web服务器同时处理 多个客户端的请求?可以使…

ROS进行深度相机的标定

前言 自己使用标定板对深度相机进行标定。 参考&#xff1a;http://wiki.ros.org/camera_calibration/Tutorials/MonocularCalibration 一、准备标定板 在下面的网站中可下载棋盘格标定板&#xff0c;可用A4纸打印下来。 http://wiki.ros.org/camera_calibration/Tutorials/…

Spring Boot整合Kaptcha实现验证码功能

目录一、前言1.Kaptcha 简介2.Kaptcha 详细配置表二、实现1.整合kaptcha&#xff0c;创建kaptcha的工具类1.1 添加依赖1.2 创建KaptchaConfig工具类2 编写接口&#xff0c;在接口中使用 kaptcha 工具类来生成验证码图片&#xff08;验证码信息&#xff09;并返回3 登录时从sess…