当前位置: 首页 > news >正文

Android 列表视频滑动自动播放—滑动过程自动播放(实现思路)

本文基于Exoplayer +  PlayerView 实现列表视频显示一定比例后自动播放

首先引入google media3包

implementation 'androidx.media3:media3-exoplayer:1.1.1'
implementation 'androidx.media3:media3-exoplayer-dash:1.1.1'
implementation 'androidx.media3:media3-ui:1.1.1'
implementation "androidx.media3:media3-common:1.1.1"

列表自动播放,我们需要监听recyclerView.addOnScrollListener(this),并实现对应方法

onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) 中判断滑动过程中,展示视频itemVideoView比例

1、首先我们需要拿到recyclerView显示出来的itemPosition

int firstItemPosition = linearLayoutManager.findFirstVisibleItemPosition();
int lastItemPosition = linearLayoutManager.findLastVisibleItemPosition();

2、当判断第一个(firstItemPosition)和最后一个(lastItemPosition)都大于0时候,开始遍历recyclerView数据,判断显示视频itemVideoView显示比例(因为我项目,视频播放数据是二维数组。RecyclerView1+RecyclerView2结构,所以视频播放时在里层RecyclerView2,如果不是二维,那么就RecyclerView2其实就是视频播放容器,dataItem.getVideoPosition()其实就是告诉我们RecyclerView2中第几条数据播放视频,默认是-1,表示没有视频,需要我们在网络请求后,遍历数据生成对应可以展示视频position)

 public void recyclerViewScrollVideo() {if (recyclerView == null || linearLayoutManager == null || communityPostListAdapter == null) {return;}int firstItemPosition = linearLayoutManager.findFirstVisibleItemPosition();int lastItemPosition = linearLayoutManager.findLastVisibleItemPosition();if (firstItemPosition >= 0 && lastItemPosition >= 0) {DataItem dataItem = communityPostListAdapter.getData(firstItemPosition);CommonViewHolder viewHolder = (CommonViewHolder) recyclerView.findViewHolderForAdapterPosition(firstItemPosition);if (viewHolder != null) {//当第一个显示数据,不支持播放视频时候,需要直接遍历后面,看是否有支持视频的//获取里层 RecyclerView ImageRecyclerView recyclerImage = viewHolder.getView(R.id.communal_post_header_recycler_image);if (dataItem != null && dataItem.getVideoPosition() >= 0 && recyclerImage != null) {//获取里层视频那个itemView positionint videoPosition = dataItem.getVideoPosition();CommonViewHolder itemViewHolder = (CommonViewHolder) recyclerImage.findViewHolderForAdapterPosition(videoPosition);//获取当前视频itemBenVideoAdapter videoAdapter = (CommunalImageAdapter) recyclerImage.getAdapter();//获取视频数据对象VideoDataBean videoDataBean = null;if (videoDataBean != null) {videoDataBean = videoAdapter.getData(videoPosition);}boolean isPlaying = ExoPlayerManager.getInstance().recyclerViewScrollVideo(recyclerImage, itemViewHolder, firstItemPosition, videoDataBean);if (!isPlaying) {//firstItemPosition不支持播放,则遍历RecyclerView数据判断下一个是否支持播放int newFirstItemPosition = firstItemPosition + 1;traversal(recyclerView, newFirstItemPosition, lastItemPosition);}} else {//有可能是多布局,则第一条数据,找不到视频播放view,那么我们需要遍历数据,从第二条数据开始找int newFirstItemPosition = firstItemPosition + 1;traversal(recyclerView, newFirstItemPosition, lastItemPosition);}}}}

3、当第一条不满足,开始遍历

 /*** 遍历寻找,屏幕显示可以播放视频的item** @param recycler* @param newFirstItemPosition* @param lastItemPosition*/private void traversal(RecyclerView recycler, int newFirstItemPosition, int lastItemPosition) {//标记是否找到视频-1,表示未找到视频int playPosition = -1;for (int i = newFirstItemPosition; i <= lastItemPosition; i++) {DataItem dataItem = communityPostListAdapter.getData(i);CommonViewHolder viewHolder = (CommonViewHolder) recycler.findViewHolderForAdapterPosition(i);if (viewHolder != null && dataItem != null && dataItem.getVideoPosition() >= 0) {RecyclerView recyclerImage = viewHolder.getView(R.id.communal_post_header_recycler_image);if (recyclerImage != null) {CommonViewHolder itemViewHolder = (CommonViewHolder) recyclerImage.findViewHolderForAdapterPosition(dataItem.getVideoPosition());//获取当前视频itemBenVideoAdapter videoAdapter = (CommunalImageAdapter) recyclerImage.getAdapter();VideoDataBean videoDataBean = null;if (videoAdapter != null) {videoDataBean = videoAdapter.getData(dataItem.getVideoPosition());}if (ExoPlayerManager.getInstance().recyclerViewScrollVideo(recyclerImage, itemViewHolder, i, communalImageBean)) {playPosition = i;break;}}}}if (playPosition == -1) {//都没有找到视频ExoPlayerManager.getInstance().stopVideo();}}

4、视频播放器单例(Exoplayer +  PlayerView)


public class ExoPlayerManager {private static volatile ExoPlayerManager inStance = null;ExoPlayer mExoPlayer;public ExoPlayerListener mExoPlayerListener;private UserPlayerView mUserPlayerView;//视频容器private FrameLayout mLayout;//loading viewprivate ProgressBar mLoading;//视频logoprivate ImageView mLogo;//点击播放按钮private ImageView mPlay;//计算RecyclerView显示位置private Rect mItemRect;//RecyclerView显示高度float visibleHeight;//RecyclerView内容高度float totalHeight;//RecyclerView 显示高度和高度比例float visibleRatio;int mItemPlayPosition;//视频播放数据对象VideoDataBean mVideoDataBean;//视频最先显示比例private float minRatio;/*** 监听*/private AnalyticsListener mAnalyticsListener = new AnalyticsListener() {@Overridepublic void onPlaybackStateChanged(EventTime eventTime, int state) {AnalyticsListener.super.onPlaybackStateChanged(eventTime, state);switch (state) {case Player.STATE_IDLE:  //未加载到资源,停止播放,包含无网也会执行if (mExoPlayerListener != null) {mExoPlayerListener.onVideoPlayIdle();} else {stopShowView();}showController();break;case Player.STATE_BUFFERING: //缓冲中if (mExoPlayerListener != null) {mExoPlayerListener.onVideoPlayBuffering(eventTime != null ? eventTime.currentPlaybackPositionMs : -1L);} else {loadingShowView();}break;case Player.STATE_READY:  //开始播放if (mExoPlayerListener != null) {mExoPlayerListener.onVideoPlay(eventTime != null ? eventTime.currentPlaybackPositionMs : -1L);} else {startShowView();}break;case Player.STATE_ENDED:  //播放结束,当循环播放时候,改方法不会被触发  onPositionDiscontinuity  DISCONTINUITY_REASON_AUTO_TRANSITIONif (mExoPlayerListener != null) {mExoPlayerListener.onVideoPlayerCompletion();}break;}}@Overridepublic void onPositionDiscontinuity(EventTime eventTime, Player.PositionInfo oldPosition, Player.PositionInfo newPosition, int reason) {AnalyticsListener.super.onPositionDiscontinuity(eventTime, oldPosition, newPosition, reason);if (mExoPlayerListener != null) {mExoPlayerListener.onVideoPlayerCompletion();} else {//循环播放了,显示时间if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {showController();}}}@Overridepublic void onPlayerError(EventTime eventTime, PlaybackException error) {AnalyticsListener.super.onPlayerError(eventTime, error);if (mExoPlayerListener != null) {mExoPlayerListener.onVideoPlayerError(eventTime != null ? eventTime.currentPlaybackPositionMs : -1l);} else {Toast.makeText(CommonParam.getInstance().getApplication(), "网络异常", Toast.LENGTH_LONG).show();if (eventTime != null) {long currentPlaybackPositionMs = eventTime.currentPlaybackPositionMs;if (mVideoDataBean != null && currentPlaybackPositionMs > 0) {mVideoDataBean.setSeekToPositionMs(currentPlaybackPositionMs);}}//网络异常会执行--onPlayerError---onPlaybackStateChanged(1)----onIsPlayingChanged()stopShowView();}}};public static ExoPlayerManager getInstance() {if (inStance == null) {synchronized (ExoPlayerManager.class) {if (inStance == null) {inStance = new ExoPlayerManager();}}}return inStance;}private ExoPlayerManager() {init();}private void init() {//初始化exoPlayerif (mExoPlayer == null) {mExoPlayer = new ExoPlayer.Builder(CommonParam.getInstance().getApplication()).build();mExoPlayer.addAnalyticsListener(mAnalyticsListener);}// 设置重复模式// Player.REPEAT_MODE_ALL 无限重复// Player.REPEAT_MODE_ONE 重复一次// Player.REPEAT_MODE_OFF 不重复mExoPlayer.setRepeatMode(Player.REPEAT_MODE_ALL);setVolume(CommonParam.getInstance().isVideoVolumeChange());if (mItemRect == null) {mItemRect = new Rect();}if (mUserPlayerView == null) {mUserPlayerView = new UserPlayerView(CommonParam.getInstance().getApplication());ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);mUserPlayerView.setLayoutParams(layoutParams);}//设置视频填充模式  RESIZE_MODE_FILL 拉伸视频达到没有黑边效果,RESIZE_MODE_ZOOM:这个是居中填充效果mUserPlayerView.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH);minRatio = 9f / 16f;}/*** 获取视频View** @return*/public UserPlayerView getUserPlayerView() {return mUserPlayerView;}/*** 添加监听*/public void addAnalyticsListener() {if (mExoPlayer != null) {mExoPlayer.removeAnalyticsListener(mAnalyticsListener);mExoPlayer.addAnalyticsListener(mAnalyticsListener);}}/*** 移除监听*/public void removeAnalyticsListener() {if (mExoPlayer != null) {mExoPlayer.removeAnalyticsListener(mAnalyticsListener);}}/*** 视频显示尺寸修改** @param params* @param playerView* @param maxWidth   屏幕显示最大宽* @param width      视频真实宽* @param height     视频真实高*/public void disposeVideoSize(ViewGroup.LayoutParams params, View playerView, int maxWidth, float width, float height) {if (params != null && playerView != null) {if (width > 0 && height > 0) {if (height >= width) {int showWidth = (int) (maxWidth > 0 ? maxWidth : width);params.width = showWidth;params.height = showWidth;} else {params.width = maxWidth;float ratio = height / width;params.height = (int) ((ratio >= minRatio) ? ratio * maxWidth : minRatio * maxWidth);}} else if (maxWidth > 0) {params.width = maxWidth;params.height = maxWidth;} else {params.width = ViewGroup.LayoutParams.MATCH_PARENT;params.height = ViewGroup.LayoutParams.MATCH_PARENT;}playerView.setLayoutParams(params);}}/*** 获取当前列表布局控件,一会计算时候需要用** @param recyclerImage* @param itemViewHolder* @param communalImageBean*/public boolean recyclerViewScrollVideo(RecyclerView recyclerImage, CommonViewHolder itemViewHolder, int itemPlayPosition, CommunalImageBean communalImageBean) {if (recyclerImage != null && itemViewHolder != null && communalImageBean != null) {FrameLayout frameLayout = itemViewHolder.getView(R.id.video);ImageView logo = itemViewHolder.getView(R.id.logo);ImageView play = itemViewHolder.getView(R.id.play);ProgressBar loading = itemViewHolder.getView(R.id.loading);ImageView volume = itemViewHolder.getView(R.id.volume);setVolumeViewChange(volume);setVolume(CommonParam.getInstance().isVideoVolumeChange());return calculateVideoPlay(recyclerImage, frameLayout, play, logo, loading, itemPlayPosition, communalImageBean);}return false;}/*** 计算是否滑动自动播放视频** @param view* @param frameLayout* @param play* @param logo* @param loading* @param videoDataBean*/public boolean calculateVideoPlay(View view, FrameLayout frameLayout, ImageView play, ImageView logo, ProgressBar loading, int itemPlayPosition,VideoDataBean videoDataBean ) {view.getLocalVisibleRect(mItemRect);visibleHeight = mItemRect.bottom - mItemRect.top;totalHeight = view.getHeight();visibleRatio = visibleHeight / totalHeight;if (mItemRect.top >= 0 && visibleRatio > BaseConstantValue.VIDEO_START_VISIBLE_RATIO) {playVideo(frameLayout, play, logo, loading, itemPlayPosition, videoDataBean);return true;} else {//不满足播放if (play != null) {play.setVisibility(View.VISIBLE);}if (loading != null) {loading.setVisibility(View.GONE);}if (logo != null) {logo.setVisibility(View.VISIBLE);}if (frameLayout != null) frameLayout.removeAllViews();}return false;}/*** 满足播放视频,需要再判断,是不是已经还在播放当前视频** @param layout* @param play* @param logo* @param loading* @param itemPlayPosition* @param videoDataBean*/public void playVideo(FrameLayout layout, ImageView play, ImageView logo, ProgressBar loading, int itemPlayPosition, VideoDataBean videoDataBean) {if (mItemPlayPosition != itemPlayPosition) {//不满足当前播放那么需要停止播放,做部分资源保存if (videoDataBean != null && mExoPlayer.isPlaying()) {//视频已经播放位置videoDataBean.setSeekToPositionMs(mExoPlayer.getContentPosition());}//显示暂停不播布局stopShowView();//暂停播放stopVideo();}//重新复制相关控件mLayout = layout;mPlay = play;mLogo = logo;mLoading = loading;//获取当前播放状态,如果未播放,则直接设置资源播放,如果已经播放,则不做处理if (!mExoPlayer.isPlaying()) {//设置播放布局loadingShowView();//移除播放Viewif (mUserPlayerView != null) {ViewParent parent = mUserPlayerView.getParent();if (parent != null && parent instanceof FrameLayout) {((FrameLayout) parent).removeAllViews();}}//将PlayerView 添加 FrameLayout上if (layout != null) {layout.removeAllViews();layout.addView(mUserPlayerView);}if (videoDataBean != null) {String videoUrl = videoDataBean.getVideoUrl();long seekToPositionMs = videoDataBean.getSeekToPositionMs();long durationMs = videoDataBean.getDurationMs();mUserPlayerView.setVideoPosition(seekToPositionMs, durationMs);MediaItem mediaItem = MediaItem.fromUri(videoUrl != null ? videoUrl : "");mExoPlayer.setMediaItem(mediaItem, seekToPositionMs);}mUserPlayerView.setPlayer(mExoPlayer);mExoPlayer.prepare();
//            mExoPlayer.play();mExoPlayer.setPlayWhenReady(true);}mItemPlayPosition = itemPlayPosition;mVideoDataBean = videoDataBean;}public ExoPlayer exoPlayer() {return mExoPlayer;}/*** 是否正在播放** @return*/public boolean isPlaying() {return mExoPlayer != null ? mExoPlayer.isPlaying() : false;}/*** 获取当前播放进度** @return*/public long getCurrentPositionMs() {if (mExoPlayer != null && mExoPlayer.isPlaying()) {return mExoPlayer.getCurrentPosition();}return -1;}/*** 获取总时长** @return*/public long getDurationMs() {if (mExoPlayer != null) {return mExoPlayer.getDuration();}return 0l;}/*** 设置进度** @param seekToCurrentPositionMs*/public void seekTo(long seekToCurrentPositionMs) {if (mExoPlayer != null) {mExoPlayer.seekTo(seekToCurrentPositionMs);if (!isPlaying()) {mExoPlayer.play();}}}/*** 设置是否有视频控制器** @param useController*/private void setUseController(boolean useController) {mUserPlayerView.setUseController(useController);}/*** 绑定全屏播放** @param videoUrl* @param currentPositionMs* @return*/private UserPlayerView bindVideoFullScreen(String videoUrl, long currentPositionMs) {mItemPlayPosition = -1;mExoPlayer.setMediaItem(MediaItem.fromUri(videoUrl == null ? "" : videoUrl), currentPositionMs);mUserPlayerView.setPlayer(mExoPlayer);mExoPlayer.prepare();mVideoDataBean = null;return mUserPlayerView;}/*** 开始播放*/public void startVideo() {if (mExoPlayer != null && !mExoPlayer.isPlaying()) {mExoPlayer.play();}}/*** 开始播放*/public void startVideo(long currentPositionMs) {if (mExoPlayer != null && !mExoPlayer.isPlaying()) {mExoPlayer.seekTo(currentPositionMs);mExoPlayer.play();}}/*** 开始播放*/public void startVideo(String videoUrl, long currentPositionMs) {if (mExoPlayer != null) {if (!mExoPlayer.isPlaying()) {mExoPlayer.stop();}mExoPlayer.setMediaItem(MediaItem.fromUri(videoUrl == null ? "" : videoUrl), currentPositionMs);mUserPlayerView.setPlayer(mExoPlayer);mExoPlayer.prepare();mExoPlayer.play();}}/*** 暂停播放*/public void pauseVideo() {if (mExoPlayer != null && mExoPlayer.isPlaying()) {mExoPlayer.pause();}}/*** 停止播放*/public void stopVideo() {mItemPlayPosition = -1;if (mExoPlayer != null && mExoPlayer.isPlaying()) {if (mVideoDataBean != null) {mVideoDataBean.setSeekToPositionMs(mExoPlayer.getContentPosition());}}mExoPlayer.stop();releaseView();}/*** 页面关闭,释放资源*/public void releaseVideo() {if (mExoPlayer != null) {mExoPlayer.removeAnalyticsListener(mAnalyticsListener);mExoPlayer.release();mExoPlayer = null;}releaseView();}private void releaseView() {if (mUserPlayerView != null) {ViewParent parent = mUserPlayerView.getParent();if (parent != null && parent instanceof FrameLayout) {((FrameLayout) parent).removeAllViews();}}//将PlayerView 添加 FrameLayout上if (mLayout != null) {mLayout.removeAllViews();mLayout = null;}if (mPlay != null) {mPlay = null;}if (mLogo != null) {mLogo = null;}if (mLoading != null) {mLoading = null;}mVideoDataBean = null;}/*** 设置播放器是否开启声音** @param volumeChange*/public void setVolume(boolean volumeChange) {if (volumeChange) {//设置声音if (mExoPlayer.getVolume() == 0f) {AudioManager audioManager = (AudioManager) CommonParam.getInstance().getApplication().getSystemService(Context.AUDIO_SERVICE);float streamVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);float streamMaxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);float volume = (streamMaxVolume != 0 && streamVolume != 0) ? streamVolume / streamMaxVolume : 0.3f;mExoPlayer.setVolume(volume);}} else {//设置声音为0mExoPlayer.setVolume(0f);}CommonParam.getInstance().setVideoVolumeChange(volumeChange);}/*** 设置 音量开关状态** @param volumeView*/public void setVolumeViewChange(View volumeView) {if (volumeView != null) {volumeView.setSelected(CommonParam.getInstance().isVideoVolumeChange());}}/*** 视频正在播放,设置相关布局show or hide*/public void startShowView() {if (mLoading != null) {mLoading.setVisibility(View.GONE);}if (mLogo != null) {mLogo.setVisibility(View.GONE);}if (mPlay != null) {mPlay.setVisibility(View.GONE);}}/*** 视频正在加载中,设置相关布局show or hide*/public void loadingShowView() {if (mLoading != null) {mLoading.setVisibility(View.VISIBLE);}if (mLogo != null) {mLogo.setVisibility(View.VISIBLE);}if (mPlay != null) {mPlay.setVisibility(View.GONE);}}/*** 视频停止播放,设置相关布局show or hide*/public void stopShowView() {if (mLoading != null) {mLoading.setVisibility(View.GONE);}if (mLogo != null) {mLogo.setVisibility(View.VISIBLE);}if (mPlay != null) {mPlay.setVisibility(View.VISIBLE);}}/*** 显示视频控制器*/public void showController() {if (mUserPlayerView != null) {UserPlayerControlView controller = mUserPlayerView.getController();if (controller != null) {controller.show();}}}/*** 设置监听** @param exoPlayerListener*/public void setExoPlayerListener(ExoPlayerListener exoPlayerListener) {mExoPlayerListener = exoPlayerListener;}
}

5、建议我们在开发视频时自定义PlayerView,只需要把源码中,这三个文件拷贝一份,然后自己根据具体需求,实现对应功能。

①、PlayerView

②、PlayerControlView

③、PlayerControlViewLayoutManager

6、如果项目是appBarLayout+RecyclerView的话。我们需要对appBarLayout滑动事件进行监听,从而实现appBarLayout滑动,也能播放视频

  mAppBarLayout.addOnOffsetChangedListener(new AppBarLayout.BaseOnOffsetChangedListener() {@Overridepublic void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {//appBarLayout 滑动监听int newOffset = Math.abs(verticalOffset);if (mVerticalOffset != newOffset && adapter != null) {Fragment fragment = adapter .getFragment(viewPagerShowType);if (fragment != null && fragment.isOnResume()) {fragment.startVideo();}mVerticalOffset = newOffset;}}});

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • tableau范围-线图与倾斜图绘制 - 14
  • CSS 中的 ::before 和 ::after 伪元素
  • 同三维T80006EH2-4K30编码器视频使用操作说明书:高清HDMI编码器,高清SDI编码器,4K超清HDMI编码器,双路4K超高清编码器
  • vue3项目中浏览器打开本地文档或者下载本地应用的方法(2024-07-11)
  • clean code-代码整洁之道 阅读笔记(第十七章 终章)
  • 【排序 - 快速排序】
  • 大模型/NLP/算法面试题总结9——从普通注意力换成多头注意力会导致参数暴涨吗?
  • 渔人杯——RE
  • git批量删除本地包含某字符串的特定分支
  • 04.ffmpeg打印音视频媒体信息
  • linux从入门到精通
  • 性能测试的流程(企业真实流程详解)(二)
  • 冒泡排序与其C语言通用连续类型排序代码
  • SpringBoot新手快速入门系列教程五:基于JPA的一个Mysql简单读写例子
  • [C++] 模拟实现list(二)
  • 【面试系列】之二:关于js原型
  • angular组件开发
  • CSS3 变换
  • CSS盒模型深入
  • ES6语法详解(一)
  • Idea+maven+scala构建包并在spark on yarn 运行
  • javascript 总结(常用工具类的封装)
  • Mysql5.6主从复制
  • Redis提升并发能力 | 从0开始构建SpringCloud微服务(2)
  • socket.io+express实现聊天室的思考(三)
  • weex踩坑之旅第一弹 ~ 搭建具有入口文件的weex脚手架
  • win10下安装mysql5.7
  • 半理解系列--Promise的进化史
  • 聊一聊前端的监控
  • 如何胜任知名企业的商业数据分析师?
  • 设计模式走一遍---观察者模式
  • 腾讯优测优分享 | 你是否体验过Android手机插入耳机后仍外放的尴尬?
  • 提升用户体验的利器——使用Vue-Occupy实现占位效果
  • 线上 python http server profile 实践
  • 一个普通的 5 年iOS开发者的自我总结,以及5年开发经历和感想!
  • NLPIR智能语义技术让大数据挖掘更简单
  • ​什么是bug?bug的源头在哪里?
  • ​一、什么是射频识别?二、射频识别系统组成及工作原理三、射频识别系统分类四、RFID与物联网​
  • #### go map 底层结构 ####
  • #QT项目实战(天气预报)
  • #基础#使用Jupyter进行Notebook的转换 .ipynb文件导出为.md文件
  • #前后端分离# 头条发布系统
  • #我与Java虚拟机的故事#连载19:等我技术变强了,我会去看你的 ​
  • $ git push -u origin master 推送到远程库出错
  • $forceUpdate()函数
  • (3)选择元素——(14)接触DOM元素(Accessing DOM elements)
  • (Charles)如何抓取手机http的报文
  • (c语言)strcpy函数用法
  • (ZT)北大教授朱青生给学生的一封信:大学,更是一个科学的保证
  • (附源码)ssm基于web技术的医务志愿者管理系统 毕业设计 100910
  • (计算机网络)物理层
  • (力扣记录)235. 二叉搜索树的最近公共祖先
  • (入门自用)--C++--抽象类--多态原理--虚表--1020
  • (算法)前K大的和
  • (一)utf8mb4_general_ci 和 utf8mb4_unicode_ci 适用排序和比较规则场景