CardView

CardView 也是一个FrameLayout,只是额外提供了圆角和阴影等效果,看上去会有立体的感觉。

我在此使用RecyclerView 内嵌CardView 进行演示

因为我们要使用RecyclerViewCardViewGlide
所以需要添加以下dependencies

implementation 'com.android.support:cardview-v7:24.2.1'
implementation 'com.android.support:recyclerview-v7:24.2.1'
implementation 'com.github.bumptech.glide:glide:4.9.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
加入RecyclerView 控件

activity_main.xml

<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http:///apk/res/android"
    xmlns:app="http:///apk/res-auto"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.coordinatorlayout.widget.CoordinatorLayout
        xmlns:tools="http:///tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerview"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
        </androidx.recyclerview.widget.RecyclerView>

        <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/fab"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom|end"
            android:layout_margin="16dp"
            android:src="@drawable/gou" />
    </androidx.coordinatorlayout.widget.CoordinatorLayout>

    <com.google.android.material.navigation.NavigationView
        android:id="@+id/nav_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:background="#fff"
        android:textSize="30sp"
        app:headerLayout="@layout/nav_header"
        app:menu="@menu/nav_menu" />
</androidx.drawerlayout.widget.DrawerLayout>

有了RecyclerView,然后就要配置RecyclerView 的子项布局了:

RecyclerView 的子项布局

fruit_item.xml

<androidx.cardview.widget.CardView xmlns:android="http:///apk/res/android"
    xmlns:app="http:///apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="5dp"
    android:orientation="vertical"
    app:cardCornerRadius="4dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <ImageView
            android:id="@+id/fruit_image"
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:scaleType="centerCrop" />

        <TextView
            android:id="@+id/fruit_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:layout_margin="5dp"
            android:textSize="16sp" />
    </LinearLayout>
</androidx.cardview.widget.CardView>

这里使用CardView 来作为子项的最外层布局,从而使得RecyclerView 中的每个元素都是在卡片当中。
注意:在ImageView 中我们使用了一个android:scaleType 属性,这个属性可以指定图片的缩放模式。由于各张图片的长宽比例可能都不一致,为了让所有的图片都能填充满整个ImageView,这里使用了centerCrop 模式,它可以让图片保持原有比例填 充满ImageView,并将超出屏幕的部分裁剪掉)

对子项信息的描述类

然后创建一个类用来对子项中所有资源进行描述:

public class Fruit {
    private String name;
    private String imageId;

    Fruit(String name, String imageId) {
         = name;
        this.imageId = imageId;
    }

    String getName() {
        return name;
    }

    String getImageId() {
        return imageId;
    }
}

子项中有两个资源,名字和图片的资源id

构建适配器
public static class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder> {
    private Context context;
    private List<Fruit> FruitList;

    FruitAdapter(List<Fruit> fruitList) {
        FruitList = fruitList;
    }

    @NonNull
    @Override
    public FruitAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        if (context == null) {
            context = parent.getContext();
        }
        View view = LayoutInflater.from(context)
                .inflate(R.layout.fruit_item, parent, false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull FruitAdapter.ViewHolder holder, int position) {
        Fruit fruit = FruitList.get(position);
        holder.textView.setText(fruit.getName());
        Glide.with(context).load(fruit.getImageId()).into(holder.imageView);
    }

    @Override
    public int getItemCount() {
        return FruitList.size();
    }

    static class ViewHolder extends RecyclerView.ViewHolder {
        CardView cardView;
        ImageView imageView;
        TextView textView;

        ViewHolder(View view) {
            super(view);
            cardView = (CardView) view;
            imageView = view.findViewById(.fruit_image);
            textView = view.findViewById(.fruit_name);
        }
    }
}

新建 FruitAdapter类, 让这个适配器继承自RecyclerView.Adapter,并将泛型指定为FruitAdapter.ViewHolder

onBindViewHolder() 方法中我们使用Glide 来加载图片:

android 实现卡片切换 android卡片布局_CardLayout


(官网对其中一种用法的描述,也就是从一个地址加载一张图片,然后传入一个ImageView 对象)

实例化适配器并加入信息
public class MainActivity extends AppCompatActivity {

	... ...
	
    private Fruit[] fruits = {new Fruit("Apple", "https://preview.qiantucdn.com/58pic/32/59/28/58PIC1158PICxye7fz6358PIC058PICbI_PIC2018_pngtree.png!w1024_new_small"),
            new Fruit("Banana", "https://preview.qiantucdn.com/original_origin_pic/19/01/26/08c8b06d2463f3f6c4180f32e41611b6.png!w1024_new_0"),
            new Fruit("Orange", "https://preview.qiantucdn.com/58pic/27/51/59/51W58PICUd58Najfeyn53_PIC2018.png!w1024_new_0"),
            new Fruit("Pear", "https://preview.qiantucdn.com/58pic/27/97/49/94T58PICe8Gjf6hWSXcQy_PIC2018.png!w1024_new_0"),
            new Fruit("Pineapple", "https://preview.qiantucdn.com/58pic/28/29/04/62v58PIC7XM19B5BSf5zz_PIC2018.png!w1024_new_0"),
            new Fruit("Cherry", "https://preview.qiantucdn.com/original_origin_pic/19/01/24/3083d56a27171f70dde7fcea354ed976.png!w1024_new_0")};

    private List<Fruit> fruitList = new ArrayList<>();
    private FruitAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
		... ...
		
        initFruits();
        RecyclerView recyclerView = findViewById(.recyclerview);
        GridLayoutManager layoutManager = new GridLayoutManager(this, 2);
        recyclerView.setLayoutManager(layoutManager);
        adapter = new FruitAdapter(fruitList);
        recyclerView.setAdapter(adapter);
    }

    private void initFruits() {
        fruitList.clear();
        for (int i = 0; i < 50; i++) {
            Random random = new Random();
            int index = random.nextInt(fruits.length);
            fruitList.add(fruits[index]);
        }
    }
}

(在此,我为适配器填充子项信息时,选择从千图网获取一些图片的地址)

首先定义了一个数组,数组里面存放了很多个Fruit 的实例,每个实例都代表着一种水果。
然后在initFruits() 方法中,先是清空了一下fruitList 中的数据,接着使用一个随机函数,从刚才定义的Fruit 数组中随机挑选一个水果放入到fruitList 当中,这样每次打开程序看到的水果数据都会是不同的。
另外,为了让界面上的数据多一些,这里使用了一个循环,随机挑选50个水果。

我为RecyclerView 添加布局管理器时添加的是一个GridLayoutManager 对象,该对象接受两个参数:

  1. 第一个是Context;
  2. 第二个是列数。

效果:

android 实现卡片切换 android卡片布局_Android_02


但是好像有点不对劲,Toolbar 居然被遮住了!

由于RecyclerViewToolbar 都是放置在CoordinatorLayout 中的,而前面已经说过,CoordinatorLayout 就是一个加强版的 FrameLayout,那么FrameLayout 中的所有控件在不进行明确定位的情况下,默认都会摆放在布局的左上角,从而也就产生了遮挡的现象。

AppBarLayout

在此,除了让RecyclerView 向下偏移方法之外,CoordinatorLayout 还有其他方法可以解决此问题 —— AppBarLayout

AppBarLayout 的使用也异常简单(一共就两步):

  1. Toolbar 嵌套到AppBarLayout 中;
  2. RecyclerView 指定一个布局行为。
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http:///apk/res/android"
    xmlns:app="http:///apk/res-auto"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:tools="http:///tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <com.google.android.material.appbar.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <androidx.appcompat.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:background="?attr/colorPrimary"
                android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
                app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />

        </com.google.android.material.appbar.AppBarLayout>

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_behavior="@string/appbar_scrolling_view_behavior">

        </androidx.recyclerview.widget.RecyclerView>

        <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/fab"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom|end"
            android:layout_margin="16dp"
            android:src="@drawable/gou" />
    </androidx.coordinatorlayout.widget.CoordinatorLayout>

    <com.google.android.material.navigation.NavigationView
        android:id="@+id/nav_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:background="#fff"
        android:textSize="30sp"
        app:headerLayout="@layout/nav_header"
        app:menu="@menu/nav_menu" />
</androidx.drawerlayout.widget.DrawerLayout>

android 实现卡片切换 android卡片布局_工具栏隐藏_03


虽然已经很好了,但是这也没有体现出Material Design 的设计理念。

事实上,当RecyclerView 滚动的时候就已经将滚动事件都通知给AppBarLayout 了, 只是我们还没进行处理而已。

AppBarLayout 接收到滚动事件的时候,它内部的子控件其实是可以指定如何去影响这些事件的,通过app:layout_scrollFlags 属性就能实现。

<com.google.android.material.appbar.AppBarLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        app:layout_scrollFlags="scroll|enterAlways|snap"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />

</com.google.android.material.appbar.AppBarLayout>

只给Toolbar 添加了一个app:layout_scrollFlags 属性,其值"scroll|enterAlways|snap" 分别表示:

  1. scroll 表示当RecyclerView 向上滚动的时候,Toolbar 会跟着一起向上滚动并实现隐藏;
  2. enterAlways 表示当RecyclerView 向下滚动的 时候,Toolbar 会跟着一起向下滚动并重新显示;
  3. snap 表示当Toolbar 还没有完全隐藏或显示的时候,会根据当前滚动的距离,自动选择是隐藏还是显示。

就这一行,就实现了下滑工具栏隐藏效果。

由上图下滑一点点距离:

android 实现卡片切换 android卡片布局_CardLayout_04

像这种功能,如果是使用ActionBar 的话,那就完全不可能实现了,Toolbar 的出现为我们提供了更多的可能。