使用 RecyclerView
使用 RecyclerView
概述
RecyclerView
是一个 ViewGroup
,它用于渲染任何基于适配器的 View
。它被官方定义为 ListView
和 GridView
的取代者,是在 Support V7
包中引入的。使用该组件的一个理由是:它有一个更易于扩展的框架,尤其是它提供了横向和纵向两个方向滚动的能力。当数据集合根据用户的操作或网络状态的变化而变化时,你很需要这个控件。
要使用 RecyclerView
,需要以下的几个元素:
RecyclerView.Adapter
:用于处理数据集合,并把数据绑定到视图上LayoutManager
:用于定位列表项ItemAnimator
:用于让常见操作(例如添加或移除列表项)变得活泼
此处,当 RecyclerView
添加或移除列表项时,它还提供了动画支持,动画操作在当前的实现下,是非常困难的事情。并且,ViewHolder
在 RecyclerView
中被深度集成,不再只是一个推荐方式。
与 ListView 的对比
因为以下几个理由,RecyclerView
与它的前辈 ListView
是不相同的:
适配器中需要 ViewHolder :
ListView
中,要提升性能,你可以不实现ViewHolder
,可以尝试其它的选择;但是在RecyclerView
的适配器中,ViewHolder
是必须要使用的。自定义列表项布局 :
ListView
只能把列表项以线性垂直的方式进行安排,并且不能自定义;RecyclerView
的RecyclerView.LayoutManager
类,可以让任何列表项在水平方向排列,或是以交错的网格模式排列。简单的列表项动画 :关于添加或移除列表项的操作,
ListView
并没有添加任何的规定;对于RecyclerView
来说,它有一个RecyclerView.ItemAnimator
类,可以用来处理列表项的动画。手动的数据源 :对于不同类型的数据源来说,
ListView
有着不同的适配器与之对应,例如ArrayAdapter
,CursorAdapter
。与此相反,RecyclerAdapter
需要开发者自己实现提供给适配器的数据。手动的列表项装饰 :
ListView
有android:divider
属性,用于设置列表项之间的分隔。与此相反,要给RecyclerView
设置分隔线的装饰,需要手动使用RecyclerView.ItemDecoration
对象。手动监测点击事件 :
ListView
为列表上的每个列表项的点击事件都使用AdapterView.OnItemClickListener
接口进行了绑定。与之不同的是,RecyclerView
只提供了RecyclerView.OnItemTouchListener
接口,它可以管理单个的touch
事件,而不再内嵌点击事件的处理。
RecyclerView 的组件
LayoutManager
LayoutManager
用于在 RecyclerView
中管理列表项的位置,对于不再对用户可见的视图来说,它还能决定什么时候重用这些视图。
RecyclerView
提供了以下几种内嵌的布局管理器:
LinearLayoutManager
:在水平或垂直的滚动列表上显示列表项GridLayoutManager
:在网格中显示列表项StaggeredGridLayoutManager
:在交错的网格中显示列表项
继承 RecyclerView.LayoutManager
就可以创建自定义的布局管理器。
RecyclerView.Adapter
RecyclerView
包含一种新的适配器,它与你之前使用过的适配器很类似,只是包含了一些特殊之处,例如必须的 ViewHolder
等。要使用这些适配器,需要重写两个方法:1. 用于渲染视图和 ViewHolder
的方法;2. 用于把数据绑定到视图的方法。每当需要创建一个新的视图时,都会调用第一个方法,不再需要检测视图是否被回收。
ItemAnimator
RecyclerView.ItemAnimator
将会使要通知适配器的 ViewGroup
的改变(例如添加/删除/选择列表项)动起来。DefaultItemAnimator
可以用于基本的默认动画,并且表现不俗。
使用 RecyclerView
使用 RecyclerView
需要遵循下面的关键步骤:
在
gradle
构建文件中添加RecyclerView
的支持库定义作为数据源使用的
model
类在
Activity
中添加RecyclerView
创建用于展现列表项的自定义行布局文件
把数据源绑定到适配器中,用于填充
RecyclerView
安装
在 app/build.gradle
文件中申明:
- dependencies {
- ...
- compile 'com.android.support:support-v4:24.2.1'
- compile 'com.android.support:recyclerview-v7:24.2.1'
- }
定义 Model
每个 RecyclerView
都有一个数据源支持。在本示例中,将定义一个 Contact
类,用于定义 RecyclerView
显示的数据模型:
- public class Contact {
- private String mName;
- private boolean mOnline;
- public Contact(String name, boolean online) {
- mName = name;
- mOnline = online;
- }
- public String getName() {
- return mName;
- }
- public boolean isOnline() {
- return mOnline;
- }
- private static int lastContactId = 0;
- public static ArrayList<Contact> createContactsList(int numContacts) {
- ArrayList<Contact> contacts = new ArrayList<Contact>();
- for (int i = 1; i <= numContacts; i++) {
- contacts.add(new Contact("Person " + ++lastContactId, i <= numContacts / 2));
- }
- return contacts;
- }
- }
在布局文件中创建 RecycleView
在 res/layout/activity_user.xml
文件中,添加 RecyclerView
:
- <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent" >
- <android.support.v7.widget.RecyclerView
- android:id="@+id/rvContacts"
- android:layout_width="match_parent"
- android:layout_height="match_parent" />
- </RelativeLayout>
创建自定义的行布局
在创建适配器之前,先定义列表用于每行显示数据的布局。当前的列表项是一个水平的线性布局,布局中有一个文本框、一个按钮:
行布局文件保存在 res/layout/item_contact.xml
中,列表中每一行都会用到它。
注意:LinearLayout 的 layout_height 参数的值应该设置为 wrap_content,这是因为早于
23.2.1
版本的RecyclerView
将会忽略布局参数。
- <?xml version="1.0" encoding="utf-8"?>
- <LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="horizontal"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:paddingTop="10dp"
- android:paddingBottom="10dp"
- >
- <TextView
- android:id="@+id/contact_name"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- />
- <Button
- android:id="@+id/message_button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:paddingLeft="16dp"
- android:paddingRight="16dp"
- android:textSize="10sp"
- />
- </LinearLayout>
创建 RecyclerView.Adapter
接着,创建一个适配器,用于把数据填充到 RecyclerView
中。适配器的角色是:用于把某个位置上的对象转换为列表项,并显示。
当然,RecyclerView
的适配器需要 ViewHolder
对象,用于描述和访问每个列表项中的所有视图。在 ContactsAdapter.java
类中,先创建一个基本的、没有实际内容的适配器:
- // Create the basic adapter extending from RecyclerView.Adapter
- // Note that we specify the custom ViewHolder which gives us access to our views
- public class ContactsAdapter extends
- RecyclerView.Adapter<ContactsAdapter.ViewHolder> {
- // Provide a direct reference to each of the views within a data item
- // Used to cache the views within the item layout for fast access
- public static class ViewHolder extends RecyclerView.ViewHolder {
- // Your holder should contain a member variable
- // for any view that will be set as you render a row
- public TextView nameTextView;
- public Button messageButton;
- // We also create a constructor that accepts the entire item row
- // and does the view lookups to find each subview
- public ViewHolder(View itemView) {
- // Stores the itemView in a public final member variable that can be used
- // to access the context from any ViewHolder instance.
- super(itemView);
- nameTextView = (TextView) itemView.findViewById(R.id.contact_name);
- messageButton = (Button) itemView.findViewById(R.id.message_button);
- }
- }
- }
适配器和 ViewHolder
已经定义好了,接下来该填充它们了。首先,定义一个成员变量,用于存储联系人数据,它在构造方法中被赋值:
- public class ContactsAdapter extends
- RecyclerView.Adapter<ContactsAdapter.ViewHolder> {
- // ... view holder defined above...
- // Store a member variable for the contacts
- private List<Contact> mContacts;
- // Store the context for easy access
- private Context mContext;
- // Pass in the contact array into the constructor
- public ContactsAdapter(Context context, List<Contact> contacts) {
- mContacts = contacts;
- mContext = context;
- }
- // Easy access to the context object in the recyclerview
- private Context getContext() {
- return mContext;
- }
- }
每个适配器都有三个主要的方法:1. onCreateViewHolder
:实例化列表项布局、创建 Holder
;2. onBindViewHolder
:基于数据设置视图属性;3. getItemCount
:检测列表项的数量。为了完成适配器,需要实现它们:
- public class ContactsAdapter extends
- RecyclerView.Adapter<ContactsAdapter.ViewHolder> {
- // ... constructor and member variables
- // Usually involves inflating a layout from XML and returning the holder
- @Override
- public ContactsAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- Context context = parent.getContext();
- LayoutInflater inflater = LayoutInflater.from(context);
- // Inflate the custom layout
- View contactView = inflater.inflate(R.layout.item_contact, parent, false);
- // Return a new holder instance
- ViewHolder viewHolder = new ViewHolder(contactView);
- return viewHolder;
- }
- // Involves populating data into the item through holder
- @Override
- public void onBindViewHolder(ContactsAdapter.ViewHolder viewHolder, int position) {
- // Get the data model based on position
- Contact contact = mContacts.get(position);
- // Set item views based on your views and data model
- TextView textView = viewHolder.nameTextView;
- textView.setText(contact.getName());
- Button button = viewHolder.messageButton;
- button.setText("Message");
- }
- // Returns the total count of items in the list
- @Override
- public int getItemCount() {
- return mContacts.size();
- }
- }
把适配器绑定到 RecyclerView
在 Activity
中,给 RecyclerView
填充用于测试的用户数据:
- public class UserListActivity extends AppCompatActivity {
- ArrayList<Contact> contacts;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- // ...
- // Lookup the recyclerview in activity layout
- RecyclerView rvContacts = (RecyclerView) findViewById(R.id.rvContacts);
- // Initialize contacts
- contacts = Contact.createContactsList(20);
- // Create adapter passing in the sample user data
- ContactsAdapter adapter = new ContactsAdapter(this, contacts);
- // Attach the adapter to the recyclerview to populate items
- rvContacts.setAdapter(adapter);
- // Set layout manager to position the items
- rvContacts.setLayoutManager(new LinearLayoutManager(this));
- // That's all!
- }
- }
编译运行应用,你将看到类似于下图的结果。如果创建了足够多的元素,列表就可以滚动了,你将会发现它的滚动远比 ListView
流畅的多。
通知适配器
与 ListView
不同的是,通过 RecyclerView
的适配器,没有办法直接添加或移除列表项。你需要做的是:直接操作数据源,然后通知适配器数据源发生了变化。同时,每当添加或移除元素时,总是应该对已经存在的列表进行操作。例如,下面这句的改变,并不会影响适配器,这是因为适配器保存了旧数据的引用,但是生成的新数据的引用发生了变化。
- // do not reinitialize an existing reference used by an adapter
- contacts = Contact.createContactsList(5);
相反,你需要在已经存在的引用上直接进行操作:
- // add to the existing list
- contacts.addAll(Contact.createContactsList(5));
如果要给适配器通知不同类型的改变,可以使用下面的这些方法:
方法 | 描述 |
---|---|
notifyItemChanged(int pos) |
某个位置上元素改变的通知 |
notifyItemInserted(int pos) |
某个位置上插入元素的通知 |
notifyItemRemoved(int pos) |
某个位置上元素移除的通知 |
notifyDataSetChanged() |
数据集改变的通知 |
在 Activity
或 Fragment
中,应该这样使用:
- // Add a new contact
- contacts.add(0, new Contact("Barney", true));
- // Notify the adapter that an item was inserted at position 0
- adapter.notifyItemInserted(0);
每当我们想给 RecyclerView
添加或移除数据时,我们需要显式地通知适配器正在发生的事件是什么。与 ListView
的适配器不同,RecyclerView
的适配器不应该依赖于 notifyDataSetChanged()
,应该使用粒度更细的动作。
如果你打算更新一个已经存在的数据列表,请在改变之前,务必要获取当前元素的数量。例如,应该调用适配器的 getItemCount()
方法来记录改变前的索引:
- // record this value before making any changes to the existing list
- int curSize = adapter.getItemCount();
- // replace this line with wherever you get new records
- ArrayList<Contact> newItems = Contact.createContactsList(20);
- // update the existing list
- contacts.addAll(newItems);
- // curSize should represent the first element that got added
- // newItems.size() represents the itemCount
- adapter.notifyItemRangeInserted(curSize, newItems.size());
定义大的改变
实际生产中,列表的改变往往更为复杂一些(例如:对已经存在的数据排序),改变一复杂,就不能精确对改变进行一个分类。这种情况下,通常,需要在整个适配器上调用 notifyDataSetChanged()
方法来更新整个屏幕,这样,使用动画来展示改变的能力,就会被减弱。
在 support v7
库的 v24.2.0
版本中,新增了 DiffUtil
类,用于计算新旧数据之间的不同。对于比较大的数据列表来说,推荐使用后台线程执行计算。
要使用 DiffUtil
类,需要实现该类并实现 DiffUtil.Callback
回调,该回调可以接收新、旧的数据列表:
- public class ContactDiffCallback extends DiffUtil.Callback {
- private List<Contact> mOldList;
- private List<Contact> mNewList;
- public ContactDiffCallback(List<Contact> oldList, List<Contact> newList) {
- this.mOldList = oldList;
- this.mNewList = newList;
- }
- @Override
- public int getOldListSize() {
- return mOldList.size();
- }
- @Override
- public int getNewListSize() {
- return mNewList.size();
- }
- @Override
- public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
- // add a unique ID property on Contact and expose a getId() method
- return mOldList.get(oldItemPosition).getId() == mNewList.get(newItemPosition).getId();
- }
- @Override
- public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
- Contact oldContact = mOldList.get(oldItemPosition);
- Contact newContact = mNewList.get(newItemPosition);
- if (oldContact.getName() == newContact.getName() && oldContact.isOnline() == newContact.isOnline()) {
- return true;
- }
- return false;
- }
- }
接着,要在适配器中实现 swapItems()
方法用于执行差异化比较。比较过后,如果有数据被插入、移除、移动或删除时,调用 dispatchUpdates()
方法来通知适配器:
- public class ContactsAdapter extends
- RecyclerView.Adapter<ContactsAdapter.ViewHolder> {
- public void swapItems(List<Contact> contacts) {
- // compute diffs
- final ContactDiffCallback diffCallback = new ContactDiffCallback(this.mContacts, contacts);
- final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback);
- // clear contacts and add
- this.mContacts.clear();
- this.mContacts.addAll(contacts);
- diffResult.dispatchUpdatesTo(this); // calls adapter's notify methods after diff is computed
- }
- }
完整的示例代码请参见 mrmike/DiffUtil-sample。
滚动到新的列表项
把列表项插入到列表的前部分,但是当前列表视图处于列表的后部分,这时,如果你想把列表显示顶部的位置,可以通过下列代码实现:
- adapter.notifyItemInserted(0);
- rvContacts.scrollToPosition(0); // index 0 position
如果把列表项添加到列表的底部,我们可以这样做:
- adapter.notifyItemInserted(contacts.size() - 1); // contacts.size() - 1 is the last element position
- rvContacts.scrollToPosition(mAdapter.getItemCount() - 1); // update based on adapter
实现连续的滚动
当用户滚动到列表的底部时,想要实现加载数据,把新加载的数据添加到列表的尾部的功能,需要使用 RecyclerView
的 addOnScrollListener()
方法,并添加 onLoadMore
方法。具体的实现参见另一篇教程:EndlessScrollViewScrollListener。
配置 RecyclerView
RecyclerView
可以自定制,它非常的灵活,可以从以下几个方面得以体现:
性能
如果数据源是静态、不会发生变化的,那我们可以启用最优化,来显著的提升平滑滚动的效果:
- recyclerView.setHasFixedSize(true);
布局
使用 layout manager
可以配置列表项的位置。我们可以在 LinearLayoutManager
、GridLayoutManager
和 StaggeredGridLayoutManager
这几个布局管理器中进行选择。
列表项在水平或垂直方向进行线性排列的示例:
- // Setup layout manager for items with orientation
- // Also supports `LinearLayoutManager.HORIZONTAL`
- LinearLayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
- // Optionally customize the position you want to default scroll to
- layoutManager.scrollToPosition(0);
- // Attach layout manager to the RecyclerView
- recyclerView.setLayoutManager(layoutManager);
在网格或交错网格上显示列表项的示例:
- // First param is number of columns and second param is orientation i.e Vertical or Horizontal
- StaggeredGridLayoutManager gridLayoutManager =
- new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
- // Attach the layout manager to the recycler view
- recyclerView.setLayoutManager(gridLayoutManager);
交错的网格布局可能如下所示:
要自定义 layout manager
,可以参见 这篇文章。
装饰
我们可以使用与 RecyclerView
相关的一些装饰来修饰列表项,例如使用 DividerItemDecoration
:
- RecyclerView.ItemDecoration itemDecoration = new
- DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST);
- recyclerView.addItemDecoration(itemDecoration);
装饰的作用是在列表项之间显示分隔:
Grid 空间装饰
在 Grid
或 StaggeredGrid
中,修饰可以在显示的元素之间添加一致的空间。把 SpacesItemDecoration.java 这个修饰拷贝到你的项目中,看一下效果吧。要想知道更多的细节,请参考 这篇教程 。
动画
当 RecyclerView
的列表项进入、添加或删除时,使用 ImteAnimator
可以给列表项添加自定义的动画。DefaultItemAnimator
用于定义默认的动画,它源码(源码地址)中复杂的实现说明了要确保以某个序列执行的动画效果所需要的必须的逻辑。
目前,在 RecyclerView
上实现列表项动画的最快方式就是使用第三方库。wasabeef/recyclerview-animators 库中包含了大量可以使用的动画。引入方式如下:
- repositories {
- jcenter()
- }
- //If you are using a RecyclerView 23.1.0 or higher.
- dependencies {
- compile 'jp.wasabeef:recyclerview-animators:2.2.3'
- }
- //If you are using a RecyclerView 23.0.1 or below.
- dependencies {
- compile 'jp.wasabeef:recyclerview-animators:1.3.0'
- }
代码中的使用:
- recyclerView.setItemAnimator(new SlideInUpAnimator());
下面是效果:
新的动画接口
从 support v23.1.0
开始,RecyclerView.ItemAnimator
新增了一个接口。该库添加了 ItemHolderInfo
类,它的表现与 MoveInfo
类似,但是在动画过渡状态之间,它能更为优雅的传递状态信息。
不同布局的列表项
如果在单个的 RecyclerView
中,你想展示多种类型的列表项,那你应该参这篇文章。
处理 Touch 事件
- recyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
- @Override
- public void onTouchEvent(RecyclerView recycler, MotionEvent event) {
- // Handle on touch events here
- }
- @Override
- public boolean onInterceptTouchEvent(RecyclerView recycler, MotionEvent event) {
- return false;
- }
- });
Snap to Center 效果
在某些情况下,我们可能需要一个横向滚动的 RecyclerView
。当用户滚动时,如果某个列表项被暴露了,我们想让它 snap to center
,如下图所示:
LinearSnapHelper
当用户滑动时,如要实现上图的效果,在 support v24.2.0
及以上版本中,可以使用内嵌的 LinearSnapHelper
类:
- SnapHelper snapHelper = new LinearSnapHelper();
- snapHelper.attachToRecyclerView(recyclerView);
使用 RecyclerView的更多相关文章
- RecyclerView使用大全
RecylerView介绍 RecylerView是support-v7包中的新组件,是一个强大的滑动组件,与经典的ListView相比,同样拥有item回收复用的功能,这一点从它的名字recyler ...
- 带你实现开发者头条APP(五)--RecyclerView下拉刷新上拉加载
title: 带你实现开发者头条APP(五)--RecyclerView下拉刷新上拉加载 tags: -RecyclerView,下拉刷新,上拉加载更多 grammar_cjkRuby: true - ...
- 安卓易学,爬坑不易——腾讯老司机的RecyclerView局部刷新爬坑之路
针对手游的性能优化,腾讯WeTest平台的Cube工具提供了基本所有相关指标的检测,为手游进行最高效和准确的测试服务,不断改善玩家的体验.目前功能还在免费开放中. 点击地址:http://wetest ...
- Android Studio开发RecyclerView遇到的各种问题以及解决(二)
开发RecyclerView时候需要导入别人的例子,我的是从github导入的,下载下github的压缩包之后解压看你要导入的文件是priject还是Module.(一般有app文件夹的大部分是pro ...
- Android Studio开发RecyclerView遇到的各种问题以及解决(一)
以前一直在用ListView,,,最近才看RecyclerView发现好强大.RecyclerView前提是Android版本在5.0以上,本人以前用的是eclipse只支持到4.4.索性就安装一个A ...
- Android的Kotlin秘方(II):RecyclerView 和 DiffUtil
作者:Antonio Leiva 时间:Sep 12, 2016 原文链接:http://antonioleiva.com/recyclerview-diffutil-kotlin/ 如你所知,在[支 ...
- Android RecyclerView 实现支付宝首页效果
Android RecyclerView 实现支付宝首页效果 [TOC] 虽然我本人不喜欢支付宝的,但是这个网格本身其实还是不错的,项目更新中更改了一个布局为网格模式,类似支付宝.(估计是产品抄袭的= ...
- Android开发学习之路-RecyclerView滑动删除和拖动排序
Android开发学习之路-RecyclerView使用初探 Android开发学习之路-RecyclerView的Item自定义动画及DefaultItemAnimator源码分析 Android开 ...
- 打造android偷懒神器———RecyclerView的万能适配器
转载请注明出处谢谢:http://www.cnblogs.com/liushilin/p/5720926.html 很不好意思让大家久等了,本来昨天就应该写这个的,无奈公司昨天任务比较紧,所以没能按时 ...
- 安卓v7支持包下的ListView替代品————RecyclerView
RecyclerView这个控件也出来很久了,相信大家也学习的差不多了,如果还没学习的,或许我可以带领大家体验一把这个艺术般的控件. 项目已经同步至github:https://github.com/ ...
随机推荐
- ZFS(一):ZFS在Debian GNU/Linux上的安装
以下内容翻译自https://pthree.org/2012/04/17/install-zfs-on-debian-gnulinux/,并附有原文,由于是第一次翻译,如有任何翻译不恰当之处,欢迎指出 ...
- linux项目-之系统安装部署-cobbler
http://cobbler.github.io/manuals/2.6.0/1/1_-_Release_Notes.html http://www.osyunwei.com/archives/760 ...
- 【C】 05 - 声明和定义
仅从形式上看,C程序就是由各种声明和定义组成的.它们是程序的骨架和外表,不仅定义了数据(变量),还定义了行为(函数).规范中的纯语言部分,声明和定义亦花去了最多的篇幅.完全说清定义的语法比较困难,这里 ...
- JavaScript 在不刷新或跳转页面的情况下改变当前浏览器地址栏上的网址
JavaScript 在不刷新或跳转页面的情况下改变当前浏览器地址栏上的网址 var stateObject = {}; var title = "改变后的网址的标题"; var ...
- A*算法入门
来源:http://www.cppblog.com/mythit/archive/2009/04/19/80492.aspx 在看下面这篇文章之前,先介绍几个理论知识,有助于理解A*算法. 启发式搜索 ...
- CRM JS 设置lookup字段 setSimpleLookupValue
function setSimpleLookupValue(LookupId, Type, Id, Name) { /// <summary> /// Sets the value for ...
- 数据库 MySql(二)
MySQL(二) 1.外键及连表 外键 一个特殊的索引,只能是指定内容 CREATE TABLE part1 ( nid INT NOT NULL auto_increment PRIMARY KEY ...
- Android 获取设备信息 异常
/**获取设备信息 * @param c * @return */ public static void setDeviceInfo(Context c,RequestParams params){ ...
- 创建线程方式-pthread
*:first-child { margin-top: 0 !important; } body > *:last-child { margin-bottom: 0 !important; } ...
- 12. Linux在线升级yum
软件包仓库源 将yum源放在/etc/yum.repo.d root@cfm880 Packages]# cd /etc/yum.repos.d/[root@cfm880 yum.repos.d]# ...