[Android] RecyclerView | RecyclerView 簡介&應用

今天的topic是要來簡介一下 RecyclerView這個widget
Google Developer上的Guide裡的overview是這樣說的
The RecyclerView widget is a more advanced and flexible version of ListView.
恩...還真是直白呢XDD
沒錯!! RecyclerView 就是個進階且靈活的ListView
還不知道或不清楚ListView的同學可以Google 或參考我的一篇介紹文

[Android]ListView & ArrayAdapter介紹與使用

那怎麼個進階和靈活呢??



RecyclerView是由一些component一起作業來顯示你的資料,這些component包括LayoutManager、RecyclerView.ViewHolder和RecyclerView.Adapter

LayoutManager可以使用標準的LayoutManager(像是LinearLayoutManager、GridLayoutManager等),或是使用自定義的。

list裡的每個View都是ViewHolder物件,這些物件是你自己定義並繼承RecyclerView.ViewHolder介面的class

而RecyclerView.Adapter可以說是資料與ViewHolder之間的溝通橋樑,透過呼叫adapter的onBindViewHolder()方法,並將ViewHolder & position作為參數傳入,你必須自己繼承RecyclerView.Adapter物件並實作這個方法。

如果覺得上面字太多,他們的關係如下圖:

靈活的原因大概((?就是以RecyclerView作為容器,內部的原件都可以自由更換而不會影響到RecyclerView本身,真的是好棒棒呢!!

好的,對RecyclerView有初步的了解後,我們就來做一個小範例吧~

接下來會以okhttp 來 get政府資料開放平台裡的紫外線即時監測資料,並藉由RecyclerView將資料顯示出來。


首先要使用RecyclerView和一些其他的libraries需先在build.gradle中加入dependencies:

 dependencies {
     implementation 'com.android.support:recyclerview-v7:28.0.0'  //RecyclerView
     implementation 'com.squareup.okhttp3:okhttp:3.14.2'          //okhttp
     implementation 'com.google.code.gson:gson:2.8.5'             //Gson
     implementation 'com.squareup.okio:okio:2.2.2'                //okio
 }

先來看看get到的資料長怎樣吧~

 [
     {
         "County":"屏東縣",
         "PublishAgency":"環境保護署",
         "PublishTime":"2019-06-22 22:00",
         "SiteName":"屏東",
         "UVI":"0",
         "WGS84Lat":"22,40,23.09",
         "WGS84Lon":"120,29,16.92"
     },
     {
         "County":"高雄市",
         "PublishAgency":"環境保護署",
         "PublishTime":"2019-06-22 22:00",
         "SiteName":"橋頭",
         "UVI":"0",
         "WGS84Lat":"22,45,27.02",
         "WGS84Lon":"120,18,20.48"
     },//...
 ]

get到的資料包含一個array
array裡每個物件都有 County、PublishAgency、PublishTime、SiteName、UVI、WGS84Lat和WGS84Lon這些properties
根據這些properties我們新增一個class來存取這些資訊

 public class UviInfo {
     private String county, publishAgency, publishTime, siteName, UVI, wgs84lat, wgs84lon;

     public UviInfo(@NotNull JsonObject obj) {
         county = obj.get("County").getAsString();
         publishAgency = obj.get("PublishAgency").getAsString();
         publishTime = obj.get("PublishTime").getAsString();
         siteName = obj.get("SiteName").getAsString();
         UVI = obj.get("UVI").getAsString();
         wgs84lat = obj.get("WGS84Lat").getAsString();
         wgs84lon = obj.get("WGS84Lon").getAsString();
     }

     public String getCounty() {
         return county;
     }

     public String getPublishAgency() {
         return publishAgency;
     }

     public String getSiteName() {
         return siteName;
     }

     public String getWgs84lat() {
         return wgs84lat;
     }

     public String getWgs84lon() {
         return wgs84lon;
     }

     public String getPublishTime() {
         return publishTime;
     }

     public String getUVI() {
         return UVI;
     }

接著,知道有哪些資料後就來設計一下ViewHolder的layout
提供一下我的布局的xml:

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:layout_marginTop="2dp"
    android:layout_marginBottom="2dp"
    android:orientation="horizontal"
    android:weightSum="12">

    <TextView
        android:id="@+id/cityName_tv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="5"
        android:gravity="center"
        android:singleLine="false"
        android:text="TextView"
        android:textAlignment="center"
        android:textSize="14sp"
        android:textStyle="bold" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="2"
        android:orientation="vertical"
        android:weightSum="10">

        <TextView
            android:id="@+id/location_tv"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="5"
            android:singleLine="true"
            android:text="TextView"
            android:textSize="12sp" />

        <TextView
            android:id="@+id/agency_tv"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="5"
            android:text="TextView"
            android:textSize="10sp" />
    </LinearLayout>

    <TextView
        android:id="@+id/uvi_tv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="5"
        android:gravity="center"
        android:singleLine="false"
        android:text="TextView"
        android:textAlignment="center" />
</LinearLayout>

大概看起來會是這樣子:

好的,前置作業做得差不多了,接下來進入重點!
接著新增ItemAdapter類別並繼承RecyclerView.Adapter<>
後面泛型的類別必須傳入繼承RecyclerView.ViewHolder的類別
所以在ItemAdapter內再新增一個ViewHolder靜態類別

public class ItemAdapter extends RecyclerView.Adapter<ItemAdapter.ViewHolder> {

    private ArrayList<UviInfo> data;

    //建構子,順便傳入要顯示的資料
    public ItemAdapter(ArrayList<UviInfo> data){
        this.data = data;
    }

    /*
    * 繼承RecyclerView.Adapter 必須實作2個functions & 1 method
    * 分別為:  onCreateViewHolder、onBindViewHolder、getItemCount
    * */

    //onCreateViewHolder 在 RecyclerView 需要新的 ViewHolder時被呼叫
    //比較需要注意LayoutInflater.infalte() 的第三個參數(boolean attachToRoot)必須設為false
    //不然會拋出java.lang.IllegalStateException
    @NonNull
    @Override
    public ItemAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
        View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item_layout, viewGroup, false);
        return new ViewHolder(v);
    }

    //onBindViewHolder 在RecyclerView 在特定的位置要顯示資料時被呼叫
    //第一個參數為ViewHolder,第二個參數是位置
    //通常會在這設定layout裡對應的元件要顯示的內容
    @Override
    public void onBindViewHolder(@NonNull ViewHolder viewHolder, int i) {
        UviInfo info = data.get(i);
        viewHolder.cityName.setText(info.getCounty());
        viewHolder.location.setText("站名:"+info.getSiteName()+" WGS84Lat:"+info.getWgs84lat()+" WGS84Lon:"+ info.getWgs84lon());
        viewHolder.agency.setText("發布單位:"+info.getPublishAgency()+" 發布時間:"+info.getPublishTime());
        viewHolder.uvi.setText(info.getUVI());
    }

    //getItemCount 回傳list裡面item的總數
    @Override
    public int getItemCount() {
        return data.size();
    }

    public static class ViewHolder extends RecyclerView.ViewHolder{

        private TextView cityName, location, agency, uvi;

        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            cityName = itemView.findViewById(R.id.cityName_tv);
            location = itemView.findViewById(R.id.location_tv);
            agency = itemView.findViewById(R.id.agency_tv);
            uvi = itemView.findViewById(R.id.uvi_tv);
        }

    }
}

好的,現在我們有Adapter & ViewHolder,為了偷懶((? LayoutManager 用 LinearLayoutManager 就好
接著就是新增自訂義Adapter & LinearLayoutManager 的實例,然後指定給 RecyclerView 就OK了

public class MainActivity extends AppCompatActivity {

    //變數宣告
    private final String cert = "-----BEGIN CERTIFICATE-----\n" +
            "MIIFQzCCBCugAwIBAgIRALxxs3aEwvIH6GL2I9QlZAUwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE\n" +
            "BhMCVFcxEjAQBgNVBAoMCeihjOaUv+mZojEhMB8GA1UECwwY5pS/5bqc5oaR6K2J566h55CG5Lit\n" +
            "5b+DMB4XDTE3MDcyNDA4MTMyNVoXDTIwMDcyNDA4MTMyNVowdDELMAkGA1UEBhMCVFcxEjAQBgNV\n" +
            "BAoMCeihjOaUv+mZojEYMBYGA1UECwwP55Kw5aKD5L+d6K23572yMRwwGgYDVQQDExNvcGVuZGF0\n" +
            "YS5lcGEuZ292LnR3MRkwFwYDVQQFExAwMDAwMDAwMDEwMDM0MTg4MIIBIjANBgkqhkiG9w0BAQEF\n" +
            "AAOCAQ8AMIIBCgKCAQEAoKWi4Sgrv8eMYyAfaP3BF63WvUDkwMsMBBZSIu3Ge2tPsRx1yWO2JbxO\n" +
            "F2YfyJq45MBD2nTOXFq7pgMMHsU5PFqzjoXDiqx7POshI/Zwu6vQMDJJExuw55LV4nFjXHYYtupw\n" +
            "r7FsWHNq7sNOGZQzGDw85ooK1oXypC8TRQJ5NlYkwFyMVpvdfZDnCbgQNf3vw6ORikB4aF37LUSR\n" +
            "+inRXHJ6uOWqyp72QZfyR7Zk0/Tb1qById4aFDeD3aN6nfga6wzDO9RBCa7GksZWY1qjVBeJqN1S\n" +
            "dzwwAD2EDZf88P590p4oletIE5FJtCCQTbjT9FDONe/ekLvrdhIMPzSOaQIDAQABo4IB/jCCAfow\n" +
            "HwYDVR0jBBgwFoAU0Rhnw1f+EpqRa19fMeo+woSH+70wHQYDVR0OBBYEFCs/BwiQdYrDKwT3jFHj\n" +
            "qjPuCoZjMIGYBggrBgEFBQcBAQSBizCBiDBFBggrBgEFBQcwAoY5aHR0cDovL2djYS5uYXQuZ292\n" +
            "LnR3L3JlcG9zaXRvcnkvQ2VydHMvSXNzdWVkVG9UaGlzQ0EucDdiMD8GCCsGAQUFBzABhjNodHRw\n" +
            "Oi8vZ2NhLm5hdC5nb3YudHcvY2dpLWJpbi9PQ1NQMi9vY3NwX3NlcnZlci5leGUwDgYDVR0PAQH/\n" +
            "BAQDAgWgMB4GA1UdIAQXMBUwCQYHYIZ2ZQADAzAIBgZngQwBAgIwHgYDVR0RBBcwFYITb3BlbmRh\n" +
            "dGEuZXBhLmdvdi50dzAgBgNVHQkEGTAXMBUGB2CGdgFkAgExCgYIYIZ2AWQDAwEwgYgGA1UdHwSB\n" +
            "gDB+MD2gO6A5hjdodHRwOi8vZ2NhLm5hdC5nb3YudHcvcmVwb3NpdG9yeS9HQ0E0L0NSTDIvQ1JM\n" +
            "XzAwMDIuY3JsMD2gO6A5hjdodHRwOi8vZ2NhLm5hdC5nb3YudHcvcmVwb3NpdG9yeS9HQ0E0L0NS\n" +
            "TDIvY29tcGxldGUuY3JsMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjANBgkqhkiG\n" +
            "9w0BAQsFAAOCAQEAUUouHC1BaeHexR2lQ/lkshctinFwTZGtpPDZv2Z7E647qQWia0y1U2mVUXmm\n" +
            "tUZkFphPBOmRuzEnJ3Cmxel8YADRzj1lfajW92LFfgSXDlIJTKV73+gdzMDvrFCJaSqJ30QhFDza\n" +
            "aVSaps9XMWxEbPv0Wn7wLDYpi0wGxJtjB/W9tbnH1x+b6PAOzoL6yakYF/4vDn+npudW/oo8IaDu\n" +
            "QGHAG5i8E5V1YFRuJRxpUMfNuyXTKgqucoiBLk6dJlJ+AbvmS3046+Hvxwmw4ciHPc9yr9GsgOGW\n" +
            "5jLSmcYHzv5ydxuyWQSCXJ9obuWwwPWABrMiEuphrte2IHsiG44cwA==\n" +
            "-----END CERTIFICATE-----";
    private RecyclerView recyclerView;
    private RecyclerView.Adapter mAdapter;
    private RecyclerView.LayoutManager layoutManager;
    private ArrayList<UviInfo> data;
    private OkHttpClient mOkHttpClient = null;
    private final String TAG = "MainActivity";
    private AlertDialog.Builder builder;
    private GestureDetector gestureDetector;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //取得資料
        data = getData();

        recyclerView = findViewById(R.id.list);
        layoutManager = new LinearLayoutManager(this);
        mAdapter = new ItemAdapter(data);

        //recyclerView 設定 adapter & layoutManager
        recyclerView.setAdapter(mAdapter);
        recyclerView.setLayoutManager(layoutManager);

        //https 驗證會有問題,這裡因為主要是說明recyclerView,想更深入了解的話請google或參考setCertificates()這個函式
        mOkHttpClient = setCertificates(new Buffer().writeUtf8(cert).inputStream());

        builder = new AlertDialog.Builder(this);

        gestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener(){
            @Override
            public boolean onSingleTapUp(MotionEvent e) {
                if(recyclerView.findChildViewUnder(e.getX(), e.getY())!=null){
                    int pos = recyclerView.getChildAdapterPosition(recyclerView.findChildViewUnder(e.getX(), e.getY()));
                    UviInfo info = data.get(pos);
                    Log.d(TAG, String.valueOf(pos));
                    builder.setTitle("詳細資訊")
                            .setMessage("縣市:"+info.getCounty()+
                                    "\n站名:"+info.getSiteName()+
                                    "\nWGS84Lat:"+info.getWgs84lat()+
                                    "\nWGS84Lon:"+ info.getWgs84lon()+
                                    "\n發布單位:"+info.getPublishAgency()+
                                    "\n發布時間:"+info.getPublishTime())
                            .setPositiveButton("確認", null)
                            .create().show();
                    return true;
                }
                return false;
            }
        });
        recyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
            @Override
            public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent) {
                if(gestureDetector.onTouchEvent(motionEvent))
                    return true;
                return false;
            }

            @Override
            public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent) {

            }

            @Override
            public void onRequestDisallowInterceptTouchEvent(boolean b) {

            }
        });
    }

    private ArrayList<UviInfo> getData(){
        FutureTask<ArrayList<UviInfo>> task = new FutureTask<>(new Callable<ArrayList<UviInfo>>() {
            @Override
            public ArrayList<UviInfo> call() throws Exception {
                Request request = new Request.Builder().url("https://opendata.epa.gov.tw/ws/Data/UV/?$format=json")
                        .get()
                        .build();
                JsonArray jarr = new JsonParser().parse(mOkHttpClient.newCall(request).execute().body().string()).getAsJsonArray();
                ArrayList<UviInfo> data = new ArrayList<>();
                for(JsonElement elem : jarr){
                    data.add(new UviInfo(elem.getAsJsonObject()));
                }
                return data;
            }
        });
        new Thread(task).start();
        try {
            return task.get();
        } catch (ExecutionException e) {
            e.printStackTrace();
            return new ArrayList<>();
        } catch (InterruptedException e) {
            e.printStackTrace();
            return new ArrayList<>();
        }
    }

    public OkHttpClient setCertificates(InputStream... certificates)
    {
        try
        {
            CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            keyStore.load(null);
            int index = 0;
            for (InputStream certificate : certificates)
            {
                String certificateAlias = Integer.toString(index++);
                keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate));

                try
                {
                    if (certificate != null)
                        certificate.close();
                } catch (IOException e)
                {
                }
            }

            SSLContext sslContext = SSLContext.getInstance("TLS");

            TrustManagerFactory trustManagerFactory =
                    TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            X509TrustManager trustManager = new X509TrustManager() {
                @Override
                public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {

                }

                @Override
                public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {

                }

                @Override
                public X509Certificate[] getAcceptedIssuers() {
                    return new X509Certificate[0];
                }
            };

            trustManagerFactory.init(keyStore);
            sslContext.init
                    (
                            null,
                            trustManagerFactory.getTrustManagers(),
                            new SecureRandom()
                    );
            return new OkHttpClient.Builder().sslSocketFactory(sslContext.getSocketFactory(), trustManager)
                    .build();


        } catch (Exception e)
        {
            e.printStackTrace();
            return new OkHttpClient();
        }

    }
}

最後,因為有用到網路所以記得在AndroidManifest.xml裡面加上:

<uses-permission android:name="android.permission.INTERNET"/>

最後附上完成圖:


以上為個人淺見,還請各位大大指教~

參考資料:
https://www.javadoc.io/doc/com.google.code.gson/gson/2.8.5
https://square.github.io/okhttp/3.x/okhttp/
http://pingguohe.net/2016/02/26/Android-App-secure-ssl.html
https://github.com/square/okio
https://developer.android.com/guide/topics/ui/layout/recyclerview
https://opendata.epa.gov.tw/ws/Data/UV/?$format=json

留言

這個網誌中的熱門文章

[android]QR code掃描