教你輕鬆實現Material Design風格的知乎主頁(詳解多種控件的綜合使用)

本文講主要來說說Toolbar、RecyclerView、CardView、DrawerLayout、以及SwiperefreshLayout的綜合使用,其中Toolbar和RecyclerView在前幾篇博客已經詳細講述其用法了,有興趣的可以去看看。現在利用這幾個控件實現Material Design風格的知乎主頁(Android v3.3版 知乎),在講到相關控件的時候,我也會提及一下這個控件的用法。那麼,讓我們開始控件之旅吧。

示例效果

在動手寫代碼之前,我們先要看看最終的實現效果是什麼:
仿知乎
首先,頂部導航欄是Toolbar,下面是RecyclerView,而RecyclerView內部的item view則是一個個CardView。
仿知乎
接着,向右滑動,會出現一個側邊菜單欄,這個是用DrawerLayout實現的,下面會詳細說明。
仿知乎.gif
最後,還實現了下拉刷新功能,這個是用SwiperefreshLayout實現的。
以上效果大致和手機上的知乎效果一樣(v3.3版),但最新的知乎已經更新到了4.0,改動很多,就不是以上的效果了。但無礙我們接下來的學習。

動手實踐

part 1.側滑菜單的佈局:DrawerLayout

根據以上的效果,我們首先要創建一個DrawerLayout佈局,它是一個ViewGroup,因此能放下其他的View,顯然左邊滑出來的部分放我們的側滑菜單選項,而右邊則是我們的主頁部分,新建activity_main.xml文件:

<android.support.v4.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <!-- main content -->
    <include layout="@layout/content_main"></include>

    <!-- The navigation drawer-->
    <include layout="@layout/drawermenu"></include>
</android.support.v4.widget.DrawerLayout>

這裏簡單介紹一下DrawerLayout的用法:在佈局內部放兩個子佈局,分別是content_main和drawermenu,放在上面的佈局會默認顯示爲主頁,而下面的佈局則會隱藏,需要滑動才能顯示。因此,主頁的view一般放在drawerlayout的第一個子View位置,同時寬高設置應爲match_parent。而側滑菜單的高度爲match_parent,但寬度應該設置爲一個固定的值(一般側滑菜單不會完全覆蓋主內容),同時它的layout_gravity要指定一個值,比如本例,是從左滑出的,因此要設置爲android:layout_gravity=”left”,表示在父佈局的左側,否則會失去側滑的效果。更多DrawerLayout的具體使用方法可以參考安卓官方指南:Creating a Navigation Drawer

Part 2.側滑菜單:ListView

爲了方便起見,側滑菜單採用了listView的列表呈現,新建drawermenu.xml文件,代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_gravity="left"
    android:background="#ffffff">

    <LinearLayout
        android:id="@+id/admininfo"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#58a5ff"
        android:orientation="vertical">
        <ImageView
            android:id="@+id/adminicon"
            android:layout_width="80dp"
            android:layout_height="80dp"
            android:layout_margin="5dp"
            android:src="@mipmap/ic_usericon"/>
        <TextView
            android:id="@+id/adminname"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:paddingLeft="10dp"
            android:text="Chen Yu"
            android:textSize="23sp"/>
    </LinearLayout>
    <ListView
        android:id="@+id/listview"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/admininfo"
        android:padding="15dp"
        android:divider="@null" />
    <ImageView
        android:id="@+id/divider"
        android:layout_width="match_parent"
        android:layout_height="2dp"
        android:background="?android:attr/listDivider"
        android:layout_above="@+id/qiehuanzhuti"
        android:layout_alignParentStart="true" />
    <TextView
        android:id="@+id/qiehuanzhuti"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="切換主題"
        android:textSize="20sp"
        android:layout_marginLeft="15dp"
        android:layout_marginTop="15dp"
        android:layout_below="@id/listview"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="設置"
        android:textSize="20sp"
        android:layout_marginLeft="15dp"
        android:layout_marginTop="15dp"
        android:layout_below="@id/qiehuanzhuti"/>

</RelativeLayout>

同時,需要爲listview的每一個Item創建一個item view,因此,新建list_item.xml:

<?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">
    <ImageView
        android:id="@+id/actionicon"
        android:layout_width="30dp"
        android:layout_height="30dp"
        android:src="@mipmap/ic_format"
        android:layout_margin="5dp" />
    <TextView
        android:id="@+id/choice"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="選項"
        android:textSize="18sp"
        android:layout_gravity="center_vertical"
        android:layout_marginLeft="15dp"/>

</LinearLayout>

既然有了listView,那麼就需要一個適配器,因此我們新建MenuAdapter.java:

public class MenuAdapter extends BaseAdapter {
    private LayoutInflater mInflater;
    private List<String> mData;
    private List<Integer> mDataIcon;

    public MenuAdapter(Context context, List<String> data,List<Integer> dataicon) {
        this.mInflater = LayoutInflater.from(context);
        this.mData = data;
        this.mDataIcon = dataicon;
    }

    @Override
    public int getCount() {
        return mData.size();
    }

    @Override
    public Object getItem(int position) {
        return mData.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View view = mInflater.inflate(R.layout.list_item,null);
        TextView textView = (TextView) view.findViewById(R.id.choice);
        ImageView imageView = (ImageView) view.findViewById(R.id.actionicon);
        //爲每一個item設置一個圖標和相應的文字
        imageView.setImageResource(mDataIcon.get(position));
        textView.setText(mData.get(position));
        return view;
    }
}

該adapter繼承自BaseAdapter,相信熟悉ListView的都對適配器比較熟悉了,因此這裏不再展開來講了。

Part 3.主頁佈局

1、實現下拉刷新:SwiperefreshLayout

先新建主頁佈局文件:content_main.xml,我們看一下上面的圖,主頁佈局主要由如下三者組成:一個toolbar導航欄,一個RecyclerView用於展示數據,以及一個刷新的小圓圈。那麼我們的實現思路如下:首先toolbar應該是位於最頂層的,接着利用swiperefreshlayout佈局,裝載一個RecycleView。代碼如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:toolbar="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="?attr/colorPrimary"
        toolbar:titleTextColor="@android:color/white"
        toolbar:subtitleTextColor="@android:color/white"
        toolbar:popupTheme="@style/ToolbarPopupTheme">
    </android.support.v7.widget.Toolbar>
    <android.support.v4.widget.SwipeRefreshLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:id="@+id/swiperefreshlayout">
        <android.support.v7.widget.RecyclerView
            android:id="@+id/recyclerview"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </android.support.v4.widget.SwipeRefreshLayout>
</LinearLayout>

關於Toolbar的使用,如果還沒用過的可以參考我之前的一篇文章Android ToolBar 使用完全解析,這裏就省略了關於Toolbar中的menu的xml文件,可以參考完整的源碼。接下來我們看看SwiperefreshLayout這個組件的使用:
這個組件是在v4包裏面的,它實際上是一個ViewGroup,但是它內部只能有一個子View,因此,我們可以把RecyclerView放在裏面,在佈局文件裏面的寫法很簡單,那麼我們在Activity內怎麼初始化這個控件呢?
與一般控件一樣,它都是使用如下方法來獲取實例:

swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swiperefreshlayout);

接着我們看它幾個常用的方法:

  //該方法用於指定刷新時進度條的顏色變化,第一個顏色表示進度條出現的初始顏色,這裏引用的是資源文件
  public void setColorSchemeResources (int... colorResIds)

  /**該方法用於設置刷新監聽器,用於監聽用戶下拉刷新的操作
   * 接收的參數是SwipeRefreshLayout.OnRefreshListener,並需要重寫該接口的onRefresh()方法。
   * 設置該監聽器後,可以使得用戶在下拉刷新的時候回調該方法
   */
  public void setOnRefreshListener (SwipeRefreshLayout.OnRefreshListener listener)

  //該方法用於設置刷新狀態,比如設置爲false,那麼是停止刷新圓圈的轉動
  public void setRefreshing (boolean refreshing)

2、數據內容:CardView

CardView是v7的一個控件,在使用它的時候需要先引入,在build.gradle中修改如下:

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    compile 'com.android.support:appcompat-v7:23.1.1'
    compile 'com.android.support:cardview-v7:23.1.1'
    compile 'com.android.support:recyclerview-v7:23.1.1'
}

這個控件的優點在於,它是一個卡片式的佈局,能輕鬆實現卡片式效果,並且能實現陰影、圓角效果。我們先看它的幾個重要的xml屬性:

app:cardElevation 陰影的大小
app:cardMaxElevation 陰影最大高度
app:cardBackgroundColor 卡片的背景色
app:cardCornerRadius 卡片的圓角大小
app:contentPadding 卡片內容於邊距的間隔

以上app是自定義的命名空間。通過設置以上的屬性,就能配置好一個漂亮的卡片了,接下來爲卡片添加各種子View內容,新建item_cardview.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="5dp"
    app:cardElevation="15dp"
    app:contentPaddingTop="2dp"
    app:contentPaddingBottom="4dp">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <RelativeLayout
            android:id="@+id/relativelayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <ImageView
                android:id="@+id/usericon"
                android:layout_height="35dp"
                android:layout_width="35dp"
                android:src="@mipmap/ic_launcher"
                android:layout_marginLeft="4dp"
                android:layout_centerVertical="true"/>

            <TextView
                android:id="@+id/username"
                android:layout_height="50dp"
                android:layout_width="wrap_content"
                android:text="羅傑斯"
                android:textSize="17sp"
                android:textColor="#8cc3ff"
                android:gravity="center"
                android:layout_toRightOf="@id/usericon"/>

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="50dp"
                android:text="贊同了該答案"
                android:textSize="17sp"
                android:gravity="center_vertical"
                android:layout_alignParentTop="true"
                android:layout_toRightOf="@id/username"/>

            <TextView
                android:id="@+id/count"
                android:layout_width="50dp"
                android:layout_height="25dp"
                android:text="1.0k"
                android:textSize="17sp"
                android:textColor="#408aff"
                android:background="#a9d3ff"
                android:gravity="center"
                android:layout_centerVertical="true"
                android:layout_alignParentRight="true"
                android:layout_marginRight="15dp"
                />
        </RelativeLayout>

        <TextView
            android:id="@+id/title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="18sp"
            android:text="Android開發測試Demo示例"
            android:layout_below="@id/relativelayout"
            android:padding="4dp"/>

        <TextView
            android:id="@+id/content"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="17sp"
            android:lines="3"
            android:ellipsize="end"
            android:text="測試內容測試內容測試內容測試內容測試內容測試內容測試內容測試內容測試內容測試內容測試內容測試內容測試內容測試內容測試內容"
            android:layout_below="@id/title"
            android:padding="4dp"/>
    </RelativeLayout>

</android.support.v7.widget.CardView>

3、數據列表:RecyclerView

接着,我們繼續完善主頁,我們需要一個數據呈現的控件,RecyclerView,該控件的詳細使用方法可以參考我的一篇文章:揭開RecyclerView的神祕面紗(一):RecyclerView的基本使用上面所說的CardView正是這裏RecyclerView的一個子Item view,我們新建一個MyAdapter.java,即RecyclerView的數據適配器:

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {

    private List<MessageObj> mData;
    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
        View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item_cardview,viewGroup,false);
        MyViewHolder vh = new MyViewHolder(v);
        return vh;
    }

    public MyAdapter(List<MessageObj> data){
        mData = data;
    }
    @Override
    public void onBindViewHolder(MyViewHolder myViewHolder, int i) {
        myViewHolder.mUsername.setText(mData.get(i).getUsername());
        myViewHolder.mUserIcon.setImageResource(mData.get(i).getIcon());
        myViewHolder.mCount.setText(mData.get(i).getCount());
        myViewHolder.mTitle.setText(mData.get(i).getTitle());
        myViewHolder.mContent.setText(mData.get(i).getContent());
    }

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

    public class MyViewHolder extends RecyclerView.ViewHolder {

        public TextView mUsername;
        public TextView mCount;
        public TextView mTitle;
        public TextView mContent;
        public ImageView mUserIcon;
        public MyViewHolder(View itemView) {
            super(itemView);
            mUsername = (TextView) itemView.findViewById(R.id.username);
            mUserIcon = (ImageView) itemView.findViewById(R.id.usericon);
            mCount = (TextView) itemView.findViewById(R.id.count);
            mTitle = (TextView) itemView.findViewById(R.id.title);
            mContent = (TextView) itemView.findViewById(R.id.content);
        }
    }
}

接着,我們要寫一個消息實體類,因爲每一個ItemView的內容都是通過網絡加載或者本地緩存都獲得的,因此,我們新建MessageObj.java:

public class MessageObj {
    private String username;
    private String count;
    private String title;
    private String content;
    private int icon;

    public MessageObj(String username,int icon ,String count, String title, String content) {
        this.username = username;
        this.count = count;
        this.title = title;
        this.content = content;
        this.icon = icon;
    }

    public String getUsername() {
        return username;
    }

    public String getContent() {
        return content;
    }

    public String getTitle() {
        return title;
    }

    public String getCount() {
        return count;
    }

    public int getIcon() {
        return icon;
    }
}

到目前爲止,通過使用RecyclerView+CardView實現了數據列表的呈現,通過SwiperefreshLayout實現了下拉刷新的功能,通過Toolbar實現了導航的功能,通過DrawerLayout實現了側滑菜單的功能。那麼最後,我們需要在Activity中,對以上一系列控件初始化以及準備數據的提供。

Part 4.MainActivity

public class MainActivity extends AppCompatActivity {

    private RecyclerView recyclerView;
    private SwipeRefreshLayout swipeRefreshLayout;
    private DrawerLayout drawerLayout;
    private ActionBarDrawerToggle actionBarDrawerToggle;
    private ListView listView;
    private List<MessageObj> mData;
    private Toolbar toolbar;
    private List<String> choices;
    private List<Integer> choiceIcon;
    private MyAdapter recyclerAdapter;

    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what){
                case 1:
                    swipeRefreshLayout.setRefreshing(false);
                    recyclerAdapter.notifyDataSetChanged();
                    break;
                default:
                    break;
            }
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initData();
        initViews();
    }

    private void initViews() {
        /**
         *  初始化Toolbar
         */
        toolbar = (Toolbar) findViewById(R.id.toolbar);
        toolbar.setTitle("首頁");
        //設置導航圖標、添加菜單點擊事件要在setSupportActionBar方法之後
        setSupportActionBar(toolbar);
        toolbar.setNavigationIcon(R.mipmap.ic_drawer_home);
        toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() {
            @Override
            public boolean onMenuItemClick(MenuItem item) {
                switch (item.getItemId()) {
                    case R.id.action_search:
                        Toast.makeText(MainActivity.this, "Search !", Toast.LENGTH_LONG).show();
                        break;
                    case R.id.action_notifications:
                        Toast.makeText(MainActivity.this, "Notification !", Toast.LENGTH_LONG).show();
                        break;
                }
                return true;
            }
        });

        /**
         *  初始化RecyclerView
         */
        recyclerView = (RecyclerView) findViewById(R.id.recyclerview);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        recyclerAdapter = new MyAdapter(mData);
        recyclerView.setAdapter(recyclerAdapter);

        /**
         *  初始化swipeRefreshLayout
         */
        swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swiperefreshlayout);
        swipeRefreshLayout.setColorSchemeResources(R.color.color_blue);
        swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        Collections.reverse(mData);
                        try {
                            Thread.sleep(1000); //模擬耗時操作
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        mHandler.sendEmptyMessage(1);
                    }
                }).start();
            }
        });

        /**
         *  初始化側滑菜單 DrawerLayout
         */
        drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
        actionBarDrawerToggle = new ActionBarDrawerToggle(
                this,
                drawerLayout,toolbar,R.string.drawer_open,R.string.drawer_close
        );
        drawerLayout.setDrawerListener(actionBarDrawerToggle);
        listView = (ListView) findViewById(R.id.listview);
        ListAdapter adapter = new MenuAdapter(this,choices,choiceIcon);
        listView.setAdapter(adapter);
    }

    private void initData() {
        mData = new ArrayList<MessageObj>();
        MessageObj obj1 = new MessageObj("神盾局",R.mipmap.shield,"5.6K","神盾局是怎麼樣的一個組織?",
                "神盾局,全稱爲國土戰略防禦攻擊與後勤保障局,由斯坦·李與傑克·科比聯合創造。神盾局是國際安全理事會專門用於處理各種奇異事件的特殊部隊");
        mData.add(obj1);
        MessageObj obj2 = new MessageObj("Stark",R.mipmap.stark,"7.8K","鋼鐵俠是誰?",
                "託尼·斯塔克(小羅伯特·唐尼飾)是“斯塔克工業”的董事長,作爲鋼鐵俠 官方劇照鋼鐵俠軍火商他譭譽不一,但還是過着上流生活。此時,");
        mData.add(obj2);
        MessageObj obj3 = new MessageObj("索爾",R.mipmap.thor,"7.8K","雷神索爾的能力如何?",
                "北歐神話裏揮舞着大鐵錘、掌控着風暴和閃電的天神,還能用鐵錘打開時空之門。暴脾氣的他因爲自大魯莽的行爲重新點燃了一場古老戰爭的戰火,之後被貶到凡間被迫與人類一起生活。");
        mData.add(obj3);
        MessageObj obj4 = new MessageObj("羅傑斯",R.mipmap.steven,"7.8K","怎麼評價美國隊長3?",
                "該片根據漫威2006年出版的漫畫大事件《內戰》改編,背景故事承接於《復仇者聯盟2:奧創紀元》事件的餘波中,講述了奧創事件後引發的");
        mData.add(obj4);
        MessageObj obj5 = new MessageObj("黑寡婦",R.mipmap.widow,"7.8K","黑寡婦是一個怎麼樣的角色?",
                "1928年出生於前蘇聯的斯大林格勒,自幼被前蘇聯特工人員訓練成特工,身體經前蘇聯政府基因改造後大大延緩了其衰老速度,並增強其免疫系統以及抗擊打能力,加上本身多年的各種體能及精神上的訓練");
        mData.add(obj5);
        choices = new ArrayList<String>();
        choiceIcon = new ArrayList<>();
        choices.add("首頁");
        choices.add("發現");
        choices.add("關注");
        choices.add("收藏");
        choices.add("圓桌");
        choices.add("私信");
        choiceIcon.add(R.mipmap.ic_main);
        choiceIcon.add(R.mipmap.ic_find);
        choiceIcon.add(R.mipmap.ic_attention);
        choiceIcon.add(R.mipmap.ic_collect);
        choiceIcon.add(R.mipmap.ic_circle);
        choiceIcon.add(R.mipmap.ic_message);


    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

}

最後,可以運行一下,就會發現結果和一開始的效果是一樣的了。

源碼地址:https://github.com/chenyua1995/androiddemo

發佈了61 篇原創文章 · 獲贊 283 · 訪問量 31萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章