使用RecyclerView打造QQ条目侧滑效果

news/2024/11/29 17:31:31/

说起这个功能,先吐槽一下,刚来不久的一个产品经理,自己虽然使用的是IOS手机,但也不能什么效果都是说人家IOS的效果交互设计的号,我们就按照它的效果做!

IOS自带的这个破侧滑功能,看着丑的要死,干嘛非要这个干,干,干……….啊…………啊………….

如果她不是个女的,我就……………………………….

好了!看看效果吧!
这里写图片描述

这里写图片描述

大致效果就是模仿QQ条目侧滑自己写了一个:

一、首先我们要自定义一个SwipeLayout继承自FrameLayout
来包装item的子view,子view分为两部分,一部分实在默认状态下(就是没侧滑)我们看的的内容,另一部分被隐藏在最右侧。

  • 那好我们来看看SwipeLayout具体内容:
package cn.hnshangyu.swipelayout.view;import android.content.Context;
import android.graphics.Rect;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;import cn.hnshangyu.swipelayout.R;/*** =========================================* 版权所有 违法必究* 作者: huangxiaoguo.* =========================================*/
public class SwipeLayout extends FrameLayout {/*** 滑动状态*/public enum SwipeState {OPEN, CLOSE, SWIPING}private SwipeState swipeState = SwipeState.CLOSE;//默认关闭状态private OnSwipeChangeListener onSwipeChangeListener;public OnSwipeChangeListener getOnSwipeChangeListener() {return onSwipeChangeListener;}public void setOnSwipeChangeListener(OnSwipeChangeListener onSwipeChangeListener) {this.onSwipeChangeListener = onSwipeChangeListener;}public interface OnSwipeChangeListener {void onOpen(SwipeLayout layout);void onClose(SwipeLayout layout);void onSwiping(SwipeLayout layout);// 将要打开     当前是 关闭状态 ----> 拖动void onStartOpen(SwipeLayout layout);//将要关闭    当前是 打开i状态--->拖动void onStartClose(SwipeLayout layout);}private ViewDragHelper mViewDragHelper;private ViewGroup mBackLayout;private ViewGroup mFrontLayout;private int mWidth;private int mHeight;private int mRange;public SwipeLayout(Context context) {this(context, null);}public SwipeLayout(Context context, AttributeSet attrs) {this(context, attrs, 0);}public SwipeLayout(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);//1 .初始化 ViewDragHelper对象mViewDragHelper = ViewDragHelper.create(this, 1.0f, callBack);}//2. 将touch 事件 转交给 mViewDragHelper@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {// 交给 mViewDragHelper 决定是否拦截return mViewDragHelper.shouldInterceptTouchEvent(ev);}@Overridepublic boolean onTouchEvent(MotionEvent event) {switch (event.getAction()) {case MotionEvent.ACTION_UP:Log.e("Log","onTouchEvent---ACTION_UP");if (swipeState == SwipeState.CLOSE) {Log.e("Log","onTouchEvent---ACTION_UP--swipeState");return false;}break;}// 让 mViewDragHelper 接收到 触摸事件try {mViewDragHelper.processTouchEvent(event);} catch (Exception e) {e.printStackTrace();}return true;}//测量 会调用很多次@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);}// 测量完成后   值改变后才会调用@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);mWidth = getMeasuredWidth();mHeight = getMeasuredHeight();//拖动范围mRange = mBackLayout.getMeasuredWidth();}/*** 放置 子view** @param changed* @param left* @param top* @param right* @param bottom*/@Overrideprotected void onLayout(boolean changed, int left, int top, int right, int bottom) {super.onLayout(changed, left, top, right, bottom);layoutInit(false);}private void layoutInit(boolean isOpen) {Rect frontRect = computeFrontRect(isOpen);//f放置  mFrontLayoutmFrontLayout.layout(frontRect.left, frontRect.top, frontRect.right, frontRect.bottom);Rect backRect = computeBackRect(frontRect);mBackLayout.layout(backRect.left, backRect.top, backRect.right, backRect.bottom);//将 控件前置bringChildToFront(mFrontLayout);}/*** 计算 mBackLayout 矩形位置** @param frontRect* @return*/private Rect computeBackRect(Rect frontRect) {int left = frontRect.right;return new Rect(left, frontRect.top, left + mRange, frontRect.bottom);}/*** 计算 mFrontLayout矩形位置** @param isOpen* @return*/private Rect computeFrontRect(boolean isOpen) {int left = 0;if (isOpen) {left = -mRange;} else {left = 0;}return new Rect(left, 0, left + mWidth, 0 + mHeight);}@Overrideprotected void onFinishInflate() {super.onFinishInflate();//添加健壮性 判断//1 比如有 两个 或者 两个以上子viewint childCount = getChildCount();if (childCount < 2) {throw new IllegalStateException("You must have 2 children at least!! 你得有 至少两个子view!!");}//2 校验都是viewGroupif (getChildAt(0) == null || !(getChildAt(0) instanceof ViewGroup) || getChildAt(1) == null && !(getChildAt(1) instanceof ViewGroup)) {throw new IllegalArgumentException("your child must be instance of ViewGroup! 你的view 必须是 viewgroup 的子类 ");}//  后边菜单mBackLayout = (ViewGroup) findViewById(R.id.layout_back);//前置条目mFrontLayout = (ViewGroup) findViewById(R.id.layout_front);}//    3  mViewDragHelper 解析完 touch事件  ----》CallBackViewDragHelper.Callback callBack = new ViewDragHelper.Callback() {/***返回值决定是否 可以拖动* @param child   拖拽的view对象  子view* @param pointerId  多指   手指的id* @return*/@Overridepublic boolean tryCaptureView(View child, int pointerId) {//            return child == mFrontLayout;return true;}/*** 当 view 被捕获的时候调用* @param capturedChild* @param activePointerId*/@Overridepublic void onViewCaptured(View capturedChild, int activePointerId) {super.onViewCaptured(capturedChild, activePointerId);}/*** 获取横向  拖拽范围   不决定 能否拖动* 做伴随动画 计算执行时长   ,计算敏感度  >0* @param child* @return*/@Overridepublic int getViewHorizontalDragRange(View child) {//返回实际的拖动范围return mRange;}/***  1.   修正  位置   left  2. 没有 开始真正的移动* @param child* @param left* @param dx* @return*/@Overridepublic int clampViewPositionHorizontal(View child, int left, int dx) {//child  正在拖动的子view//left  建议达到的位置//dx  deltaX 水平方向的瞬间变化量
//            int currentLeft = mFrontLayout.getLeft();
//            System.out.println( "currentLeft = "+currentLeft+"dx"+dx+" =? "+left);if (child == mFrontLayout) {left = fixedFrontLeft(left);} else if (child == mBackLayout) {left = fixedBackLeft(left);}return left;}private int fixedFrontLeft(int left) {if (left < -mRange) {left = -mRange;} else if (left > 0) {left = 0;}return left;}private int fixedBackLeft(int left) {if (left < (mWidth - mRange)) {left = mWidth - mRange;} else if (left > mWidth) {left = mWidth;}return left;}/*** 位置改变的时候调用  1. 伴随动画 2. 状态变化 3.  添加回调* @param changedView* @param left* @param top* @param dx* @param dy*/@Overridepublic void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
//            System.out.println("onViewPositionChanged>>>mBackLayout " + mBackLayout.getLeft());//changedView 当前正在拖动的子view//left  clampViewPositionHorizontal  的返回值
//            top
//            dx   横向的瞬间变化量if (changedView == mFrontLayout) {  //拖动mFrontLayout  让 mBackLayout跟着出来mBackLayout.offsetLeftAndRight(dx);} else if (changedView == mBackLayout) {//mBackLayout 转交 给mFrontLayoutmFrontLayout.offsetLeftAndRight(dx);}dispatchEvent();//手动 刷新  重新绘制invalidate();}//        @Override
//        public int clampViewPositionVertical(View child, int top, int dy) {
//            return top;
//        }// 当 拖动的view 释放的时候调用@Overridepublic void onViewReleased(View releasedChild, float xvel, float yvel) {//releasedChild  释放的view//xvel   释放时横向的速度 向左  -  +    停止后释放  0//yvel 释放时纵向的速度
//            System.out.println(" releasedChild = " + releasedChild + "::xvel = " + xvel);// 释放时 位置小于 -mRange 丙炔速度为0if (mFrontLayout.getLeft() < -mRange * 0.5f && xvel == 0) {open();} else if (xvel < 0) { // 向左快速滑动open();} else {close();}}};/*** 1. 更新状态 2.添加回调*/private void dispatchEvent() {SwipeState preState = swipeState;swipeState = updateState();if (onSwipeChangeListener != null) {onSwipeChangeListener.onSwiping(this);if (swipeState != preState) { //当前状态和上一个状态不一样if (swipeState == SwipeState.OPEN) {onSwipeChangeListener.onOpen(this);} else if (swipeState == SwipeState.CLOSE) {onSwipeChangeListener.onClose(this);} else if (preState == SwipeState.OPEN) {onSwipeChangeListener.onStartClose(this);} else if (preState == SwipeState.CLOSE) {onSwipeChangeListener.onStartOpen(this);}}}}/*** 获取当前 最新状态** @return*/private SwipeState updateState() {if (mFrontLayout.getLeft() == -mRange) { // 打开return SwipeState.OPEN;} else if (mFrontLayout.getLeft() == 0) {return SwipeState.CLOSE;}return SwipeState.SWIPING;}//  scroller 执行会调用此方法     computeScroll 会调用很多次@Overridepublic void computeScroll() {super.computeScroll();//  是否继续触发动画if (mViewDragHelper.continueSettling(true)) {//执行动画ViewCompat.postInvalidateOnAnimation(this);}}public void open(boolean isSmooth) {if (isSmooth) {int finalLeft = -mRange;// 最终的位置//  返回值 决定是否触发动画boolean b = mViewDragHelper.smoothSlideViewTo(mFrontLayout, finalLeft, 0);if (b) {// 执行动画ViewCompat.postInvalidateOnAnimation(this);}} else {layoutInit(true);}}/*** 打开*/public void open() {open(true);//默认平滑状态}public void close(boolean isSmooth) {if (isSmooth) {int finalLeft = 0;// 最终的位置//  返回值 决定是否触发动画boolean b = mViewDragHelper.smoothSlideViewTo(mFrontLayout, finalLeft, 0);if (b) {// 执行动画ViewCompat.postInvalidateOnAnimation(this);}} else {layoutInit(false);}}/*** 关闭*/public void close() {close(true);// 默认平滑关闭}
}

代码里面解释的很详细,再让我解释,都不知道该说啥了!

  • 我们再来看看item的布局
<cn.hnshangyu.swipelayout.view.SwipeLayout xmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/swipelayout"android:layout_width="match_parent"android:layout_height="60dp"><LinearLayoutandroid:id="@+id/layout_back"android:layout_width="wrap_content"android:layout_height="match_parent"android:descendantFocusability="blocksDescendants"android:orientation="horizontal"><TextViewandroid:id="@+id/placed_top"android:layout_width="60dp"android:layout_height="match_parent"android:background="#bdbdbd"android:gravity="center"android:text="置顶"android:textColor="@android:color/white"android:textSize="16dp" /><TextViewandroid:id="@+id/no_read"android:layout_width="100dp"android:layout_height="match_parent"android:background="#e6be62"android:gravity="center"android:text="标为未读"android:textColor="@android:color/white"android:textSize="16dp" /><TextViewandroid:id="@+id/delete"android:layout_width="60dp"android:layout_height="match_parent"android:background="#e92f2f"android:gravity="center"android:text="删除"android:textColor="@android:color/white"android:textSize="16dp" /></LinearLayout><RelativeLayoutandroid:id="@+id/layout_front"android:layout_width="match_parent"android:layout_height="match_parent"android:descendantFocusability="blocksDescendants"android:padding="8dp"><ImageViewandroid:id="@+id/icon"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_centerVertical="true"android:gravity="center_vertical"android:src="@mipmap/icon_head" /><TextViewandroid:id="@+id/textview"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_marginLeft="8dp"android:layout_toRightOf="@+id/icon"android:gravity="center_vertical"android:text="姓名"android:textSize="16dp" /></RelativeLayout>
</cn.hnshangyu.swipelayout.view.SwipeLayout>

可以看到item分为前后两部分,

  • mainActivity的布局很简单只有一个RecyclerView
<?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:id="@+id/activity_main"android:layout_width="match_parent"android:layout_height="match_parent"tools:context="cn.hnshangyu.swipelayout.MainActivity"><android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"android:layout_width="match_parent"android:layout_height="match_parent" /></RelativeLayout>

好了布局我们完成了,现在要看看我们的adapter了

package cn.hnshangyu.swipelayout.adapter;import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;import java.util.concurrent.CopyOnWriteArrayList;import butterknife.ButterKnife;
import butterknife.InjectView;
import cn.hnshangyu.swipelayout.R;
import cn.hnshangyu.swipelayout.view.SwipeLayout;public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {private Context mContext;private CopyOnWriteArrayList<String> mNameList;private SwipeLayout preLayout;//记录上一个打开public SwipeLayout getPreLayout() {return preLayout;}public MyAdapter(Context context, CopyOnWriteArrayList<String> nameList) {this.mContext = context;this.mNameList = nameList;}private OnItemClickListener onItemClickListener;public interface OnItemClickListener {void onOpen(SwipeLayout layout);void onClose(SwipeLayout layout);void onSwiping(SwipeLayout layout);void onStartOpen(SwipeLayout layout);void onStartClose(SwipeLayout layout);void onpLacedTop(int position);void onNoRead(int position);void onDelete(int position);void onItemClick(int position);}public void setOnItemClickListener(OnItemClickListener onItemClickListener) {this.onItemClickListener = onItemClickListener;}@Overridepublic ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {View view = LayoutInflater.from(mContext).inflate(R.layout.item_swipe, parent, false);return new ViewHolder(view);}@Overridepublic void onBindViewHolder(ViewHolder holder, final int position) {holder.textview.setText(mNameList.get(position));holder.swipelayout.setOnSwipeChangeListener(new SwipeLayout.OnSwipeChangeListener() {@Overridepublic void onOpen(SwipeLayout layout) {preLayout = layout;if (onItemClickListener != null) {onItemClickListener.onOpen(layout);}}@Overridepublic void onClose(SwipeLayout layout) {if (onItemClickListener != null) {onItemClickListener.onClose(layout);}}@Overridepublic void onSwiping(SwipeLayout layout) {if (onItemClickListener != null) {onItemClickListener.onSwiping(layout);}}@Overridepublic void onStartOpen(SwipeLayout layout) {if (preLayout != null) {preLayout.close();}if (onItemClickListener != null) {onItemClickListener.onStartOpen(layout);}}@Overridepublic void onStartClose(SwipeLayout layout) {if (onItemClickListener != null) {onItemClickListener.onStartClose(layout);}}});holder.layoutFront.setOnTouchListener(new View.OnTouchListener() {@Overridepublic boolean onTouch(View v, MotionEvent event) {return false;}});holder.placedTop.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {if (onItemClickListener != null) {onItemClickListener.onpLacedTop(position);}}});holder.noRead.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {if (onItemClickListener != null) {onItemClickListener.onNoRead(position);}}});holder.delete.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {if (onItemClickListener != null) {onItemClickListener.onDelete(position);}}});holder.layoutFront.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {if (onItemClickListener != null) {onItemClickListener.onItemClick(position);}}});}@Overridepublic int getItemCount() {return mNameList.size();}class ViewHolder extends RecyclerView.ViewHolder {@InjectView(R.id.placed_top)TextView placedTop;@InjectView(R.id.no_read)TextView noRead;@InjectView(R.id.delete)TextView delete;@InjectView(R.id.layout_back)LinearLayout layoutBack;@InjectView(R.id.icon)ImageView icon;@InjectView(R.id.textview)TextView textview;@InjectView(R.id.layout_front)RelativeLayout layoutFront;@InjectView(R.id.swipelayout)SwipeLayout swipelayout;public ViewHolder(View itemView) {super(itemView);ButterKnife.inject(this, itemView);}}}

在adapter中自定义监听,便于与Activity的交互

  • Activity的实现
package cn.hnshangyu.swipelayout;import android.content.Context;
import android.os.Bundle;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.widget.AbsListView;import java.util.concurrent.CopyOnWriteArrayList;import butterknife.ButterKnife;
import butterknife.InjectView;
import cn.hnshangyu.swipelayout.adapter.MyAdapter;
import cn.hnshangyu.swipelayout.utils.ToastUtil;
import cn.hnshangyu.swipelayout.view.RecycleViewDivider;
import cn.hnshangyu.swipelayout.view.SwipeLayout;public class MainActivity extends AppCompatActivity {@InjectView(R.id.recyclerView)RecyclerView mRecyclerView;private LinearLayoutManager manager;private Context mContext;private MyAdapter mAdapter;private CopyOnWriteArrayList<String> NameList = new CopyOnWriteArrayList<>();@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);mContext = this;ButterKnife.inject(this);initData();initView();initListener();}private void initData() {for (int i = 0; i < 108; i++) {NameList.add("huangxiaoguo" + i);}}private void initView() {manager = new LinearLayoutManager(this);manager.setOrientation(LinearLayoutManager.VERTICAL);mRecyclerView.setLayoutManager(manager);int mColor = ContextCompat.getColor(mContext, R.color.light_gray);mRecyclerView.addItemDecoration(new RecycleViewDivider(mContext, LinearLayoutManager.HORIZONTAL, 2, mColor));mAdapter = new MyAdapter(mContext, NameList);mRecyclerView.setAdapter(mAdapter);mRecyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {@Overridepublic void onScrollStateChanged(RecyclerView recyclerView, int newState) {if (newState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {SwipeLayout preLayout = mAdapter.getPreLayout();if (preLayout != null) {preLayout.close();}}}@Overridepublic void onScrolled(RecyclerView recyclerView, int dx, int dy) {super.onScrolled(recyclerView, dx, dy);}});}private void initListener() {mAdapter.setOnItemClickListener(new MyAdapter.OnItemClickListener() {@Overridepublic void onOpen(SwipeLayout layout) {ToastUtil.showToast(mContext, "打开");}@Overridepublic void onClose(SwipeLayout layout) {ToastUtil.showToast(mContext, "关闭");}@Overridepublic void onSwiping(SwipeLayout layout) {ToastUtil.showToast(mContext, "正在移动");}@Overridepublic void onStartOpen(SwipeLayout layout) {ToastUtil.showToast(mContext, "开始打开");}@Overridepublic void onStartClose(SwipeLayout layout) {ToastUtil.showToast(mContext, "开始关闭");}@Overridepublic void onpLacedTop(int position) {ToastUtil.showToast(mContext, "置顶"+NameList.get(position));}@Overridepublic void onNoRead(int position) {ToastUtil.showToast(mContext, "标记未读"+NameList.get(position));}@Overridepublic void onDelete(int position) {ToastUtil.showToast(mContext, "删除"+NameList.get(position));}@Overridepublic void onItemClick(int position) {ToastUtil.showToast(mContext, NameList.get(position));}});}
}

Activity中有相对应的监听回调,这样就可以进行我们下一步操作了!

Demo下载地址:http://download.csdn.net/download/huangxiaoguo1/9731545


http://www.ppmy.cn/news/278595.html

相关文章

vue2.0 侧滑菜单栏

本文参考的是 https://github.com/IFmiss/vue-music 的一个云音乐app项目&#xff0c;觉得侧栏菜单不错就模仿敲了一遍&#xff0c;顺便做下记录加深印象。效果图如下&#xff1a; 1.sideBar.vue <template> <div class"home"> <!-- 对transition…

Android RecyclerView实现侧滑删除

距上次写博客有半年多了&#xff0c;回忆起来都觉得不可思议&#xff0c;中间也想憋俩大招&#xff0c;总是被耽误&#xff0c;这俩月忙完之后&#xff0c;终于空下来了&#xff0c;恰好新项目我和UI俩人商量一下&#xff0c;用MD来实现app。中间有个需求是RecyclerView中侧滑显…

Android实现侧滑抽屉菜单,Android实现侧滑抽屉菜单(DrawerLayout+NavigationView+toolbar)

目录: 1.概述 2.实现过程与代码 1.概述 在android开发中,咱们常常会有需求开发抽屉菜单,纵观当下的主流APP,大多首页都会有侧滑抽屉菜单,它的好处就在于,它能够 在有限的空间内显示尽可能多的控件。 抽屉菜单的实现方式也有不少种方式,下面我仅仅是经过官方的DrawerLayo…

Android 侧滑抽屉菜单

侧滑抽屉菜单 前言正文一、创建项目二、添加滑动菜单三、UI美化四、添加导航视图五、菜单分类六、动态菜单七、源码 运行效果图&#xff1a; 前言 滑动菜单相信都不会陌生&#xff0c;你可能见过很多这样的文章&#xff0c;但我的文章会给你不一样的阅读和操作体验。 正文 写…

Android侧滑原来可以这么优雅

前言 侧滑手势在Android App应用得非常广泛&#xff0c;常见的使用场景包括&#xff1a;滑动抽屉、侧滑删除、侧滑返回、下拉刷新以及侧滑封面等。由于这些使用场景实在是太通用了&#xff0c;各路大神们八仙过海各显神通&#xff0c;每种侧滑场景都开源出了很多非常实用的框架…

Android 使用DrawerLayout快速实现侧滑菜单

一、概述 DrawerLayout是一个可以方便的实现Android侧滑菜单的组件&#xff0c;我最近开发的项目中也有一个侧滑菜单的功能&#xff0c;于是DrawerLayout就派上用场了。如果你从未使用过DrawerLayout&#xff0c;那么本篇博客将使用一个简单的案例带你迅速掌握DrawerLayout的用…

android侧滑菜单 动画,Android 打造完美的侧滑菜单/侧滑View控件

一.概述 在App中,经常会出现侧滑菜单,侧滑滑出View等效果,虽然说Android有很多第三方开源库,但是实际上 咱们可以自己也写一个自定义的侧滑View控件,其实不难,主要涉及到以下几个要点: 1.对Android中Window类中的DecorView有所了解 2.对Scroller类实现平滑移动效果 3.自…

Android RecyclerView —— 实现侧滑菜单

RecyclerView侧滑删除可以通过ItemTouchHelper来实现&#xff0c;但侧滑菜单栏没有原生的实现方式&#xff0c;我就尝试重写RecyclerView的onInterceptEvent和onTouchEvent方法来实现侧滑菜单&#xff0c;下面来讲下我的实现思路。文章底部有源码&#xff0c;已封装可直接使用。…