Android RecyclerView详解
介绍
RecyclerView用于在有限的窗口展现大量的数据,其实早已经有了类似的控件,如ListView、GridView,那么相比它们,RecyclerView有什么样优势呢?
RecyclerView标准化了ViewHolder,而且异常的灵活,可以轻松实现ListView实现不了的样式和功能,通过布局管理器LayoutManager可控制Item的布局方式,通过设置Item操作动画自定义Item添加和删除的动画,通过设置Item之间的间隔样式,自定义间隔。
可实现效果
设置布局管理器以控制Item的布局方式,横向、竖向以及瀑布流方式。
可设置Item操作的动画(删除或者添加等)
可设置Item的间隔样式(可绘制)
关于Item的点击和长按事件,需要用户自己去实现
使用
- 使用RecyclerView时候,必须指定一个适配器Adapter和一个布局管理器LayoutManager。
- 适配器继承RecyclerView.Adapter类,具体实现类似ListView的适配器,取决于数据信息以及展示的UI。
- 布局管理器用于确定RecyclerView中Item的展示方式以及决定何时复用已经不可见的Item,避免重复创建以及执行高成本的findViewById()方法
用法
示例
mRecyclerView = (RecyclerView) findViewById(R.id.my_recycler_view);
LinearLayoutManager mLayoutManager=new LinearLayoutManager(this);
// 设置布局管理器
mRecyclerView.setLayoutManager(mLayoutManager);
// 设置adapter
mRecyclerView.setAdapter(mAdapter);
// 设置Item添加和移除的动画
mRecyclerView.setItemAnimator(new DefaultItemAnimator());
// 设置Item之间间隔样式
mRecyclerView.addItemDecoration(mDividerItemDecoration);
基本使用
首先需要在在 build.gradle 文件中引入 RecyclerView 类
compile 'com.android.support:recyclerview-v7:23.4.0'
Fragment代码
package com.demo.fragment;import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;import com.demo.R;
import com.demo.adapter.VideoRecyclerViewAdapter;
import com.demo.bean.VideoBean;import java.util.ArrayList;
import java.util.List;public class ListViewFragment extends Fragment{public static ListViewFragment newInstance() {return new ListViewFragment();}@Nullable@Overridepublic View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {View view = inflater.inflate(R.layout.activity_recycler_view, container, false);initView(view);return view;}private void initView(View view) {RecyclerView recyclerView = view.findViewById(R.id.rv);LinearLayoutManager layoutManager=new LinearLayoutManager(getActivity());layoutManager.setOrientation(LinearLayoutManager.VERTICAL);recyclerView.setLayoutManager(layoutManager);// 设置布局管理器DefaultItemAnimator itemAnimator = new DefaultItemAnimator();recyclerView .setItemAnimator(itemAnimator);// 设置Item添加和移除的动画itemAnimator.setSupportsChangeAnimations(false);recyclerView.setAdapter(new VideoRecyclerViewAdapter(getVideoList(), getActivity()));}public List<VideoBean> getVideoList() {List<VideoBean> videoList = new ArrayList<>();//...添加数据return videoList;}
}
R.layout.activity_recycler_view
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical" android:layout_width="match_parent"android:layout_height="match_parent"><android.support.v7.widget.RecyclerView
android:id="@+id/rv"android:layout_width="match_parent"android:layout_height="match_parent"/></LinearLayout>
RecyclerView适配器Adapter代码
package com.demo.adapter;import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;import com.bumptech.glide.Glide;
import com.demo.R;
import com.demo.bean.VideoBean;
import com.demo.view.CustomVideoView ;import java.util.List;public class VideoRecyclerViewAdapter extends RecyclerView.Adapter<VideoRecyclerViewAdapter.VideoHolder> {private List<VideoBean> videos;private Context context;public VideoRecyclerViewAdapter(List<VideoBean> videos, Context context) {this.videos = videos;this.context = context;}@Overridepublic VideoHolder onCreateViewHolder(ViewGroup parent, int viewType) {View itemView = LayoutInflater.from(context).inflate(R.layout.item_video_auto_play, parent, false);return new VideoHolder(itemView);}@Overridepublic void onBindViewHolder(final VideoHolder holder, int position) {VideoBean videoBean = videos.get(position);holder.title.setText(videoBean.getTitle());holder.videoView .setPlayUrl(videoBean.getUrl());}@Overridepublic int getItemCount() {return videos.size();}public class VideoHolder extends RecyclerView.ViewHolder {private CustomVideoView videoView;private TextView title;VideoHolder(View itemView) {super(itemView);videoView= (CustomVideoView )itemView.findViewById(R.id.video_player);int widthPixels = context.getResources().getDisplayMetrics().widthPixels;videoView .setLayoutParams(new LinearLayout.LayoutParams(widthPixels, widthPixels / 16 * 9));title = itemView.findViewById(R.id.tv_title);}}}
布局管理器:RecyclerView.LayoutManager
上述代码中mLayoutManager 对象是布局管理器,RecyclerView提供了三种布局管理器:
- LinerLayoutManager(线性):以垂直或者水平列表方式展示Item
- GridLayoutManager (网格):以网格方式展示Item
- StaggeredGridLayoutManager(瀑布流): 以瀑布流方式展示Item
适配器:RecyclerView.Adapter
RecyclerView.Adapter的使用方式和ListView的ListAdapter 类似,向RecyclerView提供显示的数据。
但是RecyclerView.Adapter做了一件了不起的优化,那就是RecyclerView.Adapter的
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
方法能够保证当前RecyclerView是确实需要你创建一个新的ViewHolder对象。而ListView的ListAdapter 对应的方法
@Overridepublic View getView(int i, View view, ViewGroup viewGroup)
需要在方法内部判断是重新创建一个View还是刷新一个View的数据,而不明所以的客户可能每次都会返回一个新创建的View造成ListView的卡顿和资源的浪费;创建和刷新作为两个不同的功能本来就应该在两个方法中实现—庆幸的是RecyclerView.Adapter解决了这个问题
视图容器:RecyclerView.ViewHolder
RecyclerView中强制客户使用ViewHolder,谈及ListView的时候就经常说到使用ViewHolder来进行优化。使用ViewHolder其中一点好处是能够避免重复调用方法findViewById(),对当前item的View中的子View进行管理。
ViewHolder 描述RecylerView中某个位置的itemView和元数据信息,属于Adapter的一部分。其实该类通常用于保存 findViewById 的结果
ViewHolder的mFlags属性
- FLAG_BOUND ——ViewHolder已经绑定到某个位置,mPosition、mItemId、mItemViewType都有效
- FLAG_UPDATE ——ViewHolder绑定的View对应的数据过时需要重新绑定,mPosition、mItemId还是一致的
- FLAG_INVALID ——ViewHolder绑定的View对应的数据无效,需要完全重新绑定不同的数据
- FLAG_REMOVED ——ViewHolder对应的数据已经从数据集移除
- FLAG_NOT_RECYCLABLE ——ViewHolder不能复用
- FLAG_RETURNED_FROM_SCRAP ——这个状态的ViewHolder会加到scrap list被复用。
- FLAG_CHANGED ——ViewHolder内容发生变化,通常用于表明有ItemAnimator动画
- FLAG_IGNORE ——ViewHolder完全由LayoutManager管理,不能复用
- FLAG_TMP_DETACHED ——ViewHolder从父RecyclerView临时分离的标志,便于后续移除或添加回来
- FLAG_ADAPTER_POSITION_UNKNOWN ——ViewHolder不知道对应的Adapter的位置,直到绑定到一个新位置
- FLAG_ADAPTER_FULLUPDATE ——方法 addChangePayload(null) 调用时设置
间隔样式:RecyclerView.ItemDecoration
- 用于绘制itemView之间的一些特殊UI,比如在itemView之前设置空白区域、画线等。
- RecyclerView 将itemView和装饰UI分隔开来,装饰UI即 ItemDecoration ,主要用于绘制item间的分割线、高亮或者margin等
- 通过recyclerView.addItemDecoration(new DividerDecoration(this))对item添加装饰;对RecyclerView设置多个ItemDecoration,列表展示的时候会遍历所有的ItemDecoration并调用里面的绘制方法,对Item进行装饰。
- public void onDraw(Canvas c, RecyclerView parent) 装饰的绘制在Item条目绘制之前调用,所以这有可能被Item的内容所遮挡
- public void onDrawOver(Canvas c, RecyclerView parent) 装饰的绘制在Item条目绘制之后调用,因此装饰将浮于Item之上
- public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) 与padding或margin类似,LayoutManager在测量阶段会调用该方法,计算出每一个Item的正确尺寸并设置偏移量
展示效果和ListView基本上无差别,但是Item之间并没有分割线,在xml去找divider属性的时候,发现RecyclerView没有divider属性,当然也可以在Item布局中加上分割线,但是这样做并不是很优雅。
其实RecyclerView是支持自定义间隔样式的。通过
mRecyclerView.addItemDecoration()
来设置我们定义好的间隔样式
自定义间隔样式需要继承RecyclerView.ItemDecoration类,该类是个抽象类,主要有三个方法
- onDraw(Canvas c, RecyclerView parent, State state):在Item绘制之前被调用,该方法主要用于绘制间隔样式
- onDrawOver(Canvas c, RecyclerView parent, State state):在Item绘制之前被调用,该方法主要用于绘制间隔样式
- getItemOffsets(Rect outRect, View view, RecyclerView parent, State state):设置item的偏移量,偏移的部分用于填充间隔样式,在RecyclerView的onMesure()中会调用该方法
onDraw()和onDrawOver()这两个方法都是用于绘制间隔样式,我们只需要复写其中一个方法即可。直接来看一下自定义的间隔样式的实现,参考官方实例
public class MyDividerItemDecoration extends RecyclerView.ItemDecoration {private static final int[] ATTRS = new int[]{android.R.attr.listDivider};public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;/*** 用于绘制间隔样式*/private Drawable mDivider;/*** 列表的方向,水平/竖直*/private int mOrientation;public MyDividerItemDecoration(Context context, int orientation) {// 获取默认主题的属性final TypedArray a = context.obtainStyledAttributes(ATTRS);mDivider = a.getDrawable(0);a.recycle();setOrientation(orientation);}@Overridepublic void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {// 绘制间隔if (mOrientation == VERTICAL_LIST) {drawVertical(c, parent);} else {drawHorizontal(c, parent);}}@Overridepublic void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {if (mOrientation == VERTICAL_LIST) {outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());} else {outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);}}private void setOrientation(int orientation) {if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {throw new IllegalArgumentException("invalid orientation");}mOrientation = orientation;}/*** 绘制间隔*/private void drawVertical(Canvas c, RecyclerView parent) {final int left = parent.getPaddingLeft();final int right = parent.getWidth() - parent.getPaddingRight();final int childCount = parent.getChildCount();for (int i = 0; i < childCount; i++) {final View child = parent.getChildAt(i);final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();final int top = child.getBottom() + params.bottomMargin +Math.round(ViewCompat.getTranslationY(child));final int bottom = top + mDivider.getIntrinsicHeight();mDivider.setBounds(left, top, right, bottom);mDivider.draw(c);}}/*** 绘制间隔*/private void drawHorizontal(Canvas c, RecyclerView parent) {final int top = parent.getPaddingTop();final int bottom = parent.getHeight() - parent.getPaddingBottom();final int childCount = parent.getChildCount();for (int i = 0; i < childCount; i++) {final View child = parent.getChildAt(i);final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();final int left = child.getRight() + params.rightMargin +Math.round(ViewCompat.getTranslationX(child));final int right = left + mDivider.getIntrinsicHeight();mDivider.setBounds(left, top, right, bottom);mDivider.draw(c);}}
}
然后在代码中设置RecyclerView的间隔样式
mRecyclerView.addItemDecoration(new MyDividerItemDecoration(this, LinearLayoutManager.VERTICAL));
动画:RecyclerView.ItemAnimator
RecyclerView可以设置列表中Item删除和添加的动画,在v7包中给我们提供了一种默认的Item删除和添加的动画,如果没有特殊的需求,默认使用这个动画即可
// 设置Item添加和移除的动画
mRecyclerView.setItemAnimator(new DefaultItemAnimator());
设置的动画用于在 item 项数据变化时的动画效果
当调用 Adapter 的 notifyItemChanged、notifyItemInserted、notifyItemMoved 等方法,会触发该对象显示相应的动画。
RecyclerView 的 ItemAnimator 使得 item 的动画实现变得简单而样式丰富,我们可以自定义 item 项不同操作(如添加,删除)的动画效果;
ItemAnimator 触发于以下三种事件:
- 某条数据被插入到数据集合中 ,对应 public final void notifyItemInserted(int position) 方法
- 从数据集合中移除某条数据 ,对应 public final void notifyItemRemoved(int position) 方法
- 更改数据集合中的某条数据,对应 public final void notifyItemChanged(int position) 方法
注意:notifyDataSetChanged(),会触发列表的重绘,并不会出现任何动画效果
使用:Animator使用到的逻辑比较多,因此最方便的就是使用第三方库:https://github.com/wasabeef/recyclerview-animators
点击事件
RecyclerView并没有像ListView一样暴露出Item点击事件或者长按事件处理的api,也就是说使用RecyclerView时候,需要我们自己来实现Item的点击和长按等事件的处理。
实现方法有很多
- 可以监听RecyclerView的Touch事件然后判断手势做相应的处理
- 可以通过在绑定ViewHolder的时候设置监听,然后通过Apater回调出去
- 使用点击、长按事件支持类
第二种方法:在绑定ViewHolder的时候设置监听,通过Apater回调出去 Adapter 的完整代码
public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter .ViewHolder>{/*** 展示数据*/private ArrayList<String> mData;/*** 事件回调监听*/private RecyclerViewAdapter.OnItemClickListener onItemClickListener;public RecyclerViewAdapter(ArrayList<String> data) {this.mData = data;}public void updateData(ArrayList<String> data) {this.mData = data;notifyDataSetChanged();}/*** 添加新的Item*/public void addNewItem() {if(mData == null) {mData = new ArrayList<>();}mData.add(0, "new Item");notifyItemInserted(0);}/*** 删除Item*/public void deleteItem() {if(mData == null || mData.isEmpty()) {return;}mData.remove(0);notifyItemRemoved(0);}/*** 设置回调监听* * @param listener*/public void setOnItemClickListener(MyAdapter.OnItemClickListener listener) {this.onItemClickListener = listener;}@Overridepublic ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {// 实例化展示的viewView v = LayoutInflater.from(parent.getContext()).inflate(R.layout.view_rv_item, parent, false);// 实例化viewholderViewHolder viewHolder = new ViewHolder(v);return viewHolder;}@Overridepublic void onBindViewHolder(final ViewHolder holder, int position) {// 绑定数据holder.mTv.setText(mData.get(position));holder.itemView.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(final View v) {if(onItemClickListener != null) {int pos = holder.getLayoutPosition();onItemClickListener.onItemClick(holder.itemView, pos);}}});holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {@Overridepublic boolean onLongClick(View v) {if(onItemClickListener != null) {int pos = holder.getLayoutPosition();onItemClickListener.onItemLongClick(holder.itemView, pos);}//表示此事件已经消费,不会触发单击事件return true;}});}@Overridepublic int getItemCount() {return mData == null ? 0 : mData.size();}public static class ViewHolder extends RecyclerView.ViewHolder {TextView mTv;public ViewHolder(View itemView) {super(itemView);mTv = (TextView) itemView.findViewById(R.id.item_tv);}}public interface OnItemClickListener {void onItemClick(View view, int position);void onItemLongClick(View view, int position);}
}
Activity 设置 Adapter 事件监听
mAdapter.setOnItemClickListener(new MyAdapter.OnItemClickListener() {@Overridepublic void onItemClick(View view, int position) {Toast.makeText(MyActivity.this,"click " + position + " item", Toast.LENGTH_SHORT).show();}@Overridepublic void onItemLongClick(View view, int position) {Toast.makeText(MyActivity.this,"long click " + position + " item", Toast.LENGTH_SHORT).show();}
});
第三种方法:使用点击、长按事件支持类代码
参考 Hugo 的文章:Getting your clicks on RecyclerView
- 先要准备一份resources
res -> values -> ids.xml ->
<?xml version="1.0" encoding="utf-8"?>
<resources><item name="item_click_support" type="id" />
</resources>
- 具体的支持类
public class ItemClickSupport {private final RecyclerView mRecyclerView;private OnItemClickListener mOnItemClickListener;private OnItemLongClickListener mOnItemLongClickListener;private View.OnClickListener mOnClickListener = new View.OnClickListener() {@Override public void onClick(View v) {if (mOnItemClickListener != null) {RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);mOnItemClickListener.onItemClicked(mRecyclerView, holder.getAdapterPosition(), v);}}};private View.OnLongClickListener mOnLongClickListener = new View.OnLongClickListener() {@Override public boolean onLongClick(View v) {if (mOnItemLongClickListener != null) {RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);return mOnItemLongClickListener.onItemLongClicked(mRecyclerView, holder.getAdapterPosition(), v);}return false;}};private RecyclerView.OnChildAttachStateChangeListener mAttachListener = new RecyclerView.OnChildAttachStateChangeListener() {@Override public void onChildViewAttachedToWindow(View view) {if (mOnItemClickListener != null) {view.setOnClickListener(mOnClickListener);}if (mOnItemLongClickListener != null) {view.setOnLongClickListener(mOnLongClickListener);}}@Override public void onChildViewDetachedFromWindow(View view) {}};private ItemClickSupport(RecyclerView recyclerView) {mRecyclerView = recyclerView;mRecyclerView.setTag(R.id.item_click_support, this);mRecyclerView.addOnChildAttachStateChangeListener(mAttachListener);}public static ItemClickSupport addTo(RecyclerView view) {ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support);if (support == null) {support = new ItemClickSupport(view);}return support;}public static ItemClickSupport removeFrom(RecyclerView view) {ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support);if (support != null) {support.detach(view);}return support;}public ItemClickSupport setOnItemClickListener(OnItemClickListener listener) {mOnItemClickListener = listener;return this;}public ItemClickSupport setOnItemLongClickListener(OnItemLongClickListener listener) {mOnItemLongClickListener = listener;return this;}private void detach(RecyclerView view) {view.removeOnChildAttachStateChangeListener(mAttachListener);view.setTag(R.id.item_click_support, null);}// 点击接口public interface OnItemClickListener {void onItemClicked(RecyclerView recyclerView, int position, View v);}// 长按接口public interface OnItemLongClickListener {boolean onItemLongClicked(RecyclerView recyclerView, int position, View v);}
}
- 在setAdapter()之后调用
// 点击
ItemClickSupport.addTo(rv).setOnItemClickListener(new ItemClickSupport.OnItemClickListener() {@Override public void onItemClicked(RecyclerView recyclerView, int position, View v) {Toast.makeText(MainActivity.this, mDatas.get(position), Toast.LENGTH_SHORT).show();}
});// 长按
ItemClickSupport.addTo(rv).setOnItemLongClickListener(new ItemClickSupport.OnItemLongClickListener() {@Override public boolean onItemLongClicked(RecyclerView recyclerView, int position, View v) {Toast.makeText(MainActivity.this, "长按" + mDatas.get(position) + "已删除", Toast.LENGTH_SHORT).show();// 需要自己处理position在集合中的位置(需考虑头、身、脚布局数量)mDatas.remove(position);if (lastVisible + 1 == mAdapter.getItemCount()) {addmore();}mAdapter.notifyItemRemoved(position);// 消耗事件return true;}
});
总结
目前而言,我们已经知道RecyclerView的一些功能如下
- 水平列表展示,设置LayoutManager的方向性
- 竖直列表展示,设置LayoutManager的方向性
- 自定义间隔,RecyclerView.addItemDecoration()
- Item添加和删除动画,RecyclerView.setItemAnimator()
所以在项目中如果再遇见列表类的布局,就可以优先考虑使用 RecyclerView,更灵活更快捷的使用方式会给编码带来不一样的体验
Android RecyclerView详解相关推荐
- 【转】Android菜单详解——理解android中的Menu--不错
原文网址:http://www.cnblogs.com/qingblog/archive/2012/06/08/2541709.html 前言 今天看了pro android 3中menu这一章,对A ...
- Android菜单详解——理解android中的Menu
前言 今天看了pro android 3中menu这一章,对Android的整个menu体系有了进一步的了解,故整理下笔记与大家分享. PS:强烈推荐<Pro Android 3>,是我至 ...
- Android LayoutInflater详解
Android LayoutInflater详解 在实际开发中LayoutInflater这个类还是非常有用的,它的作用类 似于findViewById().不同点是LayoutInflater是用来 ...
- android Fragments详解
android Fragments详解一:概述 android Fragments详解二:创建Fragment 转载于:https://my.oschina.net/liangzhenghui/blo ...
- android WebView详解,常见漏洞详解和安全源码(下)
上篇博客主要分析了 WebView 的详细使用,这篇来分析 WebView 的常见漏洞和使用的坑. 上篇:android WebView详解,常见漏洞详解和安全源码(上) 转载请注明出处:http ...
- android WebView详解,常见漏洞详解和安全源码(上)
这篇博客主要来介绍 WebView 的相关使用方法,常见的几个漏洞,开发中可能遇到的坑和最后解决相应漏洞的源码,以及针对该源码的解析. 由于博客内容长度,这次将分为上下两篇,上篇详解 WebView ...
- android子视图无菜单,Android 菜单详解
Android中菜单分为三种,选项菜单(OptionMenu),上下文菜单(ContextMenu),子菜单(SubMenu) 选项菜单 可以通过两种办法增加选项菜单,一是在menu.xml中添加,该 ...
- Android StateFlow详解
转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/121913352 本文出自[赵彦军的博客] 文章目录 系列文章 一.冷流还是热流 S ...
- Android SharedFlow详解
转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/121911675 本文出自[赵彦军的博客] 文章目录 系列文章 什么是SharedF ...
最新文章
- Java项目:朴素风个人博客系统(前后端分离+java+vue+Springboot+ssm+mysql+maven+redis)
- dedecms模板中首页实现分页的方法
- 索引,表增删改统计,加锁查具体情况(推荐)
- Cloud for Customer Fiori client导航栏里工作中心层级显示设定
- 国产数据库技术全面破冰,金融核心系统打破国外巨头垄断指日可待
- 程序员又迎一利器,联想 LeapIOT 工业互联网平台大曝光
- 非资深玩家留言频率限制(1024秒限制)
- 新兴的人工智能服务器,5个新兴人工智能物联网应用
- java 切图_分布式切图服务——切图篇
- hadoop版本和java版本不一致的问题
- MySQL初始密码忘记了怎么办
- zmud之汉字转换为数字
- 基于Android的学生信息管理大作业
- 【VR】一直困扰虚拟现实的VAC现象,真的无解么?
- 国产系统-Deepin安装图文(VIP典藏2022版)
- 一周学python系列(7)——面向对象
- 银行间市场的USDCNY即期一天交易量到底有多少?
- Oracle数据库配置
- java中StringTokenizer使用
- swagger生成对应的客户端、服务端代码