บทเรียนนี้เราจะมาประยุกต์ใช้ Volley.jar ในการเก็บ Cache รูปภาพจากเว็บเซอร์วิสร่วมกับดึงข้อมูลรายการโทรทัศน์ หรือ Online TV ดูสดผ่านแอพพลิเคชันบนระบบปฏิบัติการ Android
สิ่งที่ต้องเรียนรู้ก่อนหน้าคือ
ถ้าหากว่าได้เรียนรู้มาหมดแล้ว และพร้อมแล้วก็มาเริ่มพัฒนาแอพพลิเคชันดูทีวีของเรากันดีกว่า
เริ่มต้นให้สร้าง New Project ขึ้นมาเป็น Basic Activity ตั้งชื่อตามใจชอบว่าเราจะให้แอพพลิเคชันของเราชื่ออะไรก็ตามใจครับ หลังจากนั้นให้เราไปดาวน์โหลด Volley.jar เป็น Library ในการจัดการ Resource จาก Web Services ตัวเล็กๆ ตัวหนึ่งจากที่นี่ครับ (ผมอัพโหลดไว้ให้)
https://drive.google.com/file/d/0B08PZSOd4UmOTFBLbW1sQUlYVHc/view?usp=sharing
หรือหาตาม Google ก็ได้
เมื่อ Project ของเราบน Android Studio พร้อมแล้วให้เปิดโหมด Project View (ถ้าเป็น version 2.2 ก็จะอยู่ข้างๆ) เอา volley.jar ไปวางในโฟลเดอร์ libs ให้เรียบร้อยถ้าไม่มี ก็ออกไปที่ Windows Explorer เปิดไล่เอาครับ
หลังจากวางตัว volley.jar ลงใน libs ของตัว Android Studio เป็นที่เรียบร้อยแล้วให้ทำการ แก้ไข Gradle ใหม่ โดยเพิ่มส่วนของ libs/volley.jar ลงไปแล้วทำการ Sync
โดยแก้ไขเพิ่มเป็นตามนี้
dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) testCompile 'junit:junit:4.12' compile 'com.android.support:appcompat-v7:23.1.1' compile 'com.android.support:design:23.1.1' compile files('libs/volley.jar') }
นั่นคือการเพิ่มส่วนของการเรียกใช้งาน volley.jar เข้าไป
compile files('libs/volley.jar')
หลังจากทำการ Sync Gradle เป็นที่เรียบร้อยแล้ว ให้ทำการสร้าง Package ขึ้นมาใน Project ของเราครับ โดยสร้าง Package เป็นส่วนที่ทำงานร่วมกับ Directory Structure ของตัว main/java เท่านั้นครับ
Package ที่เราจะสร้างนั้นประกอบไปด้วย adapter, controller, model, util
แล้วทำการสร้าง Java Class File ตามรูปข้างบน นั่นคือ
- สร้างไฟล์ AppController.java อยู่ใน Package “controller“
- สร้างไฟล์ LruBitmapCache.java ใน Package “util“
- สร้างไฟล์ TvModel.java ใน Package “model“
- สร้างไฟล์ CustomListAdapter.java ใน Package “adapter“
เปิดไฟล์ LruBitmapCache.java ทำการ extends LruCache<String, Bitmap> implements ImageLoader.ImageCache ต่อท้ายแล้ว Implement Method ให้เรียบร้อย
ในตัวอย่างนี้ผมจะทำการปรับ CacheSize ไว้เป็นที่เรียบร้อยแล้ว
package util; import android.graphics.Bitmap; import android.util.LruCache; import com.android.volley.toolbox.ImageLoader; /** * Created by Banyapon on 6/26/2016. */ public class LruBitmapCache extends LruCache<String, Bitmap> implements ImageLoader.ImageCache { //TODO Create getDefaultCacheSize() method and setup cacheSize public static int getDefaultLruCacheSize() { final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); final int cacheSize = maxMemory / 8; return cacheSize; } public LruBitmapCache(){ this(getDefaultLruCacheSize()); } public LruBitmapCache(int maxSize) { super(maxSize); } //TODO insert protected sizeof() @Override protected int sizeOf(String key, Bitmap value) { return value.getRowBytes() * value.getHeight() / 1024; } @Override public Bitmap getBitmap(String s) { return null; } @Override public void putBitmap(String url, Bitmap bitmap) { put(url, bitmap); } }
ทำการเตรียม API ของส่วน JSON สำหรับดูทีวีให้เรียบร้อย ประกอบไปด้วย ชื่อช่อง ไฟล์รูปภาพช่องรายการ และ URL ของ M3u8 ที่เป็น Streaming Video ดูออนไลน์ โดยตัวอย่างนั้นจะประกอบไปด้วย Pattern JSON ตามนี้
[ { _id: { $oid: "5712eff7e4b065a8c4d71825" }, id: 3, title: "CH 3", detail: "URL File รูปภาพ", source: "URL Filee m3u8" }, { _id: { $oid: "5712f072e4b065a8c4d7182b" }, id: 5, title: "CH 5", detail: "URL File รูปภาพ", source: "URL Filee m3u8" } ]
แก้ไขไฟล์ TVModel.java ให้มีการเรียก Mapping Key Value ของ JSON ตามนี้
package model; /** * Created by Banyapon on 6/26/2016. */ public class TVmodel { private String id,title,detail,source; public TVmodel(){ } public TVmodel(String id, String title, String detail,String source){ this.id = id; this.title = title; this.detail = detail; this.source = source; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getDetail() { return detail; } public void setDetail(String detail) { this.detail = detail; } public String getSource() { return source; } public void setSource(String source) { this.source = source; } }
เพื่อรับค่า String ทุกตัวใน JSON มาอยู่ในรูปแบบการทำงาน แบบ adapter, controller และ model ต่อมาให้แก้ไขไฟล์ AppController.java โดยมีการเรียก Extend ตัว Application ทำการ Implement Method ให้เรียบร้อย ทำการเขียนคำสั่งในการ Load ไฟล์ Images ทั้งหมดเก็บใน Cache ผ่าน Class ของ LruBitmapCache
package controller; import android.app.Application; import android.text.TextUtils; import com.android.volley.Request; import com.android.volley.RequestQueue; import com.android.volley.toolbox.ImageLoader; import com.android.volley.toolbox.Volley; import util.LruBitmapCache; /** * Created by Banyapon on 6/26/2016. */ public class AppController extends Application { public static final String TAG = AppController.class.getSimpleName(); private RequestQueue mRequestQueue; private ImageLoader mImageLoader; private static AppController mInstance; @Override public void onCreate() { super.onCreate(); mInstance = this; } public static synchronized AppController getInstance() { return mInstance; } public RequestQueue getRequestQueue() { if (mRequestQueue == null) { mRequestQueue = Volley.newRequestQueue(getApplicationContext()); } return mRequestQueue; } public ImageLoader getImageLoader() { getRequestQueue(); if (mImageLoader == null) { mImageLoader = new ImageLoader(this.mRequestQueue, new LruBitmapCache()); } return this.mImageLoader; } public <T> void addToRequestQueue(Request<T> req, String tag) { // set the default tag if tag is empty req.setTag(TextUtils.isEmpty(tag) ? TAG : tag); getRequestQueue().add(req); } public <T> void addToRequestQueue(Request<T> req) { req.setTag(TAG); getRequestQueue().add(req); } public void cancelPendingRequests(Object tag) { if (mRequestQueue != null) { mRequestQueue.cancelAll(tag); } } }
ให้ทำการเพิ่ม Layout ใหม่ขึ้นมาชื่อว่า grid_layout.xml ใส่รูปแบบต่อไปนี้
<?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"> <com.android.volley.toolbox.NetworkImageView android:id="@+id/thumbnail" android:layout_width="100dp" android:layout_height="100dp" android:layout_alignParentTop="true" android:layout_centerHorizontal="true" android:scaleType="fitCenter" /> <TextView android:id="@+id/text" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginTop="5dp" android:gravity="center" android:textSize="14sp" android:textStyle="bold" android:textColor="#000000" /> </LinearLayout>
เป็นการเก็บส่วนของ component widget ภายใน Grid View เพื่อแสดงรูปภาพ และชื่อช่อง ดังนั้นต้องไปแก้ไขไฟล์ content_main.xml อีกทีโดยการเพิ่ม gridView เข้าไป
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:context="com.daydev.tutorialtv.MainActivity" tools:showIn="@layout/activity_main"> <GridView android:id="@+id/gridView" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_margin="5dp" android:columnWidth="100dp" android:drawSelectorOnTop="true" android:gravity="center" android:numColumns="auto_fit" android:stretchMode="columnWidth" android:verticalSpacing="5dp" android:focusable="true" android:clickable="true" android:background="#ffffff" /> </RelativeLayout>
หมายเหตุ: ในตัวอย่างผมตัดส่วนของ Fab Button ออกจากตัวอย่างไปเป็นที่เรียบร้อยจาก activity_main.xml
แก้ไขไฟล์ CustomListAdapter.java เพิ่มส่วนของการ ดึงข้อมูลจาก TVmodel แต่ละ Key ไปผูกค่ากับ id แต่ละตัวของ grid_layout.xml
package adapter; import android.app.Activity; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.TextView; import com.android.volley.toolbox.ImageLoader; import com.android.volley.toolbox.NetworkImageView; import com.daydev.tutorialtv.R; import java.util.List; import controller.AppController; import model.TVmodel; /** * Created by Banyapon on 6/26/2016. */ public class CustomListAdapter extends BaseAdapter { private Activity activity; private LayoutInflater inflater; private List<TVmodel> dataItems; ImageLoader imageLoader = AppController.getInstance().getImageLoader(); public CustomListAdapter(Activity activity, List<TVmodel> dataItems) { this.activity = activity; this.dataItems = dataItems; } @Override public int getCount() { return dataItems.size(); } @Override public Object getItem(int position) { return dataItems.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { if (inflater == null) inflater = (LayoutInflater) activity .getSystemService(Context.LAYOUT_INFLATER_SERVICE); if (convertView == null) convertView = inflater.inflate(R.layout.grid_layout, null); if (imageLoader == null) imageLoader = AppController.getInstance().getImageLoader(); NetworkImageView thumbNail = (NetworkImageView) convertView .findViewById(R.id.thumbnail); TextView title = (TextView) convertView.findViewById(R.id.text); // getting movie data for the row TVmodel m = dataItems.get(position); // thumbnail image thumbNail.setImageUrl(m.getDetail(), imageLoader); // title title.setText(m.getTitle()); return convertView; } }
เราจะสามารถผูกค่า Key ทั้งหลายที่รับมาจาก JSON มาลงที่ Activity ได้แล้วทีนี้ก็เหลือแค่การดึงข้อมูลจาก URL ของ API ไปปรากฏบน Gridview เปิดไฟล์ MainActivity.java ขึ้นมาเขียนคำสั่งลงไป
package com.daydev.tutorialtv; import android.app.ProgressDialog; import android.content.Intent; import android.os.Bundle; import android.support.design.widget.Snackbar; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.util.Log; import android.view.View; import android.view.Menu; import android.view.MenuItem; import android.widget.AdapterView; import android.widget.GridView; import com.android.volley.Response; import com.android.volley.VolleyError; import com.android.volley.VolleyLog; import com.android.volley.toolbox.JsonArrayRequest; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.List; import adapter.CustomListAdapter; import controller.AppController; import model.TVmodel; public class MainActivity extends AppCompatActivity { // Log tag private static final String TAG = MainActivity.class.getSimpleName(); // Movies json url private static final String url = "<<ใส่ URL ของ API>>"; private ProgressDialog pDialog; private List<TVmodel> dataList = new ArrayList<TVmodel>(); private GridView gridView; private CustomListAdapter adapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); gridView = (GridView) findViewById(R.id.gridView); adapter = new CustomListAdapter(this, dataList); gridView.setAdapter(adapter); pDialog = new ProgressDialog(this); pDialog.setMessage("Loading Data..."); pDialog.show(); JsonArrayRequest movieReq = new JsonArrayRequest(url, new Response.Listener<JSONArray>() { @Override public void onResponse(JSONArray response) { Log.d(TAG, response.toString()); hidePDialog(); for (int i = 0; i < response.length(); i++) { try { JSONObject obj = response.getJSONObject(i); TVmodel movie = new TVmodel(); movie.setTitle(obj.getString("title")); movie.setDetail(obj.getString("detail")); movie.setSource(obj.getString("source")); dataList.add(movie); } catch (JSONException e) {e.printStackTrace(); } } adapter.notifyDataSetChanged(); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { VolleyLog.d(TAG, "Error: " + error.getMessage()); hidePDialog(); } }); AppController.getInstance().addToRequestQueue(movieReq); } private void hidePDialog() { if (pDialog != null) { pDialog.dismiss(); pDialog = null; } } @Override public void onDestroy() { super.onDestroy(); hidePDialog(); } @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; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); //noinspection SimplifiableIfStatement if (id == R.id.action_settings) { return true; } return super.onOptionsItemSelected(item); } }
ค่าในการรับค่า JSON นั้นอยู่ที่คำสั่ง ปรกติคือ getJSONObject();
JsonArrayRequest movieReq = new JsonArrayRequest(url, new Response.Listener<JSONArray>() { @Override public void onResponse(JSONArray response) { Log.d(TAG, response.toString()); hidePDialog(); for (int i = 0; i < response.length(); i++) { try { JSONObject obj = response.getJSONObject(i); TVmodel movie = new TVmodel(); movie.setTitle(obj.getString("title")); movie.setDetail(obj.getString("detail")); movie.setSource(obj.getString("source")); dataList.add(movie); } catch (JSONException e) {e.printStackTrace(); } } adapter.notifyDataSetChanged(); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { VolleyLog.d(TAG, "Error: " + error.getMessage()); hidePDialog(); } }); AppController.getInstance().addToRequestQueue(movieReq);
ต่อมาให้สร้าง Activity อีกตัวชื่อว่า MainAcctivity2.java ใหม่เพิ่มเข้าไปเพื่อสร้างหน้าดูทีวีออนไลน์
โดยทำการเพิ่มส่วนของ Intent กดเพื่อส่งค่า source และ param อื่นๆ จาก MainActivity.java ไปยัง MainActivity2.java ตามนี้
gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { int itemPosition = position; Intent showPage = new Intent(MainActivity.this,MainActivity2.class); showPage.putExtra("title",""+dataList.get(position).getTitle()+""); showPage.putExtra("source",""+dataList.get(position).getSource()+""); startActivity(showPage); } });
คำสั่งหน้า MainActivity.java จะเป็นดังนี้
package com.daydev.tutorialtv; import android.app.ProgressDialog; import android.content.Intent; import android.os.Bundle; import android.support.design.widget.Snackbar; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.util.Log; import android.view.View; import android.view.Menu; import android.view.MenuItem; import android.widget.AdapterView; import android.widget.GridView; import com.android.volley.Response; import com.android.volley.VolleyError; import com.android.volley.VolleyLog; import com.android.volley.toolbox.JsonArrayRequest; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.List; import adapter.CustomListAdapter; import controller.AppController; import model.TVmodel; public class MainActivity extends AppCompatActivity { // Log tag private static final String TAG = MainActivity.class.getSimpleName(); // Movies json url private static final String url = "<<URL ของ API>>"; private ProgressDialog pDialog; private List<TVmodel> dataList = new ArrayList<TVmodel>(); private GridView gridView; private CustomListAdapter adapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); gridView = (GridView) findViewById(R.id.gridView); adapter = new CustomListAdapter(this, dataList); gridView.setAdapter(adapter); pDialog = new ProgressDialog(this); // Showing progress dialog before making http request pDialog.setMessage("Loading Data..."); pDialog.show(); gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { int itemPosition = position; Intent showPage = new Intent(MainActivity.this,MainActivity2.class); showPage.putExtra("title",""+dataList.get(position).getTitle()+""); showPage.putExtra("source",""+dataList.get(position).getSource()+""); startActivity(showPage); } }); JsonArrayRequest movieReq = new JsonArrayRequest(url, new Response.Listener<JSONArray>() { @Override public void onResponse(JSONArray response) { Log.d(TAG, response.toString()); hidePDialog(); for (int i = 0; i < response.length(); i++) { // Parsing json try { JSONObject obj = response.getJSONObject(i); TVmodel movie = new TVmodel(); movie.setTitle(obj.getString("title")); movie.setDetail(obj.getString("detail")); movie.setSource(obj.getString("source")); dataList.add(movie); } catch (JSONException e) {e.printStackTrace(); } } adapter.notifyDataSetChanged(); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { VolleyLog.d(TAG, "Error: " + error.getMessage()); hidePDialog(); } }); AppController.getInstance().addToRequestQueue(movieReq); } private void hidePDialog() { if (pDialog != null) { pDialog.dismiss(); pDialog = null; } } @Override public void onDestroy() { super.onDestroy(); hidePDialog(); } @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; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); //noinspection SimplifiableIfStatement if (id == R.id.action_settings) { return true; } return super.onOptionsItemSelected(item); } }
เปิดไฟล์ MainActivity2.java ขึ้นมาเขียนคำสั่งในการรับค่าจาก Intent มาแสดงข้อมูลผ่าน VideoView ดังนั้นต้องไปแก้ไขไฟล์ content_main2.xml ก่อนดังนี้
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="fill_parent" android:layout_height="fill_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:context="com.daydev.tutorialtv.MainActivity2" tools:showIn="@layout/activity_main2"> <VideoView android:id="@+id/VideoPlayer" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_centerVertical="true" android:layout_centerHorizontal="true" /> </RelativeLayout>
แล้วแก้ไข MainActivity2.java ดังนี้
package com.daydev.tutorialtv; import android.content.Intent; import android.content.res.Configuration; import android.net.Uri; import android.os.Bundle; import android.support.design.widget.Snackbar; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.view.View; import android.widget.MediaController; import android.widget.VideoView; import java.net.URL; public class MainActivity2 extends AppCompatActivity { private String urlStream; private VideoView myVideoView; private URL url; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main2); Intent intent= getIntent(); setTitle(intent.getStringExtra("title")); VideoView videoView = (VideoView) findViewById(R.id.VideoPlayer); String httpLiveUrl = intent.getStringExtra("source"); videoView.setVideoURI(Uri.parse(httpLiveUrl)); videoView.setMediaController(new MediaController(this)); videoView.requestFocus(); videoView.start(); } public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); if(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { getActionBar().hide(); } else { getActionBar().hide(); } } }
ขั้นตอนสุดท้ายแล้วคือเปิด Permission และแก้ไข android:name เล็กน้อย นั่นคือบรรทัดต่อไปนี้ใน AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
และ
android:name="controller.AppController"
ไฟล์ AndroidManifest.xml จะเป็นดังนี้ เปิดเทียบดูนะครับ
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.daydev.tutorialtv"> <uses-permission android:name="android.permission.INTERNET" /> <application android:name="controller.AppController" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity" android:label="@string/app_name" android:theme="@style/AppTheme.NoActionBar" android:configChanges="orientation|screenSize"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".MainActivity2" android:label="@string/title_activity_main2" android:theme="@style/AppTheme.NoActionBar"></activity> </application> </manifest>
ตกแต่งความสวยงานของ res/values/color.xml ตามนี้
<?xml version="1.0" encoding="utf-8"?> <resources> <color name="colorPrimary">#49e15d</color> <color name="colorPrimaryDark">#059e02</color> <color name="colorAccent">#fd8b00</color> <color name="backgroundBlack">#000</color> </resources>
ทดสอบแอพพลิเคชันของเราหน่อยดีกว่า เสียบ USB เปิด USB Debugging แล้ว Run ตัวแอพพลิเคชันไปเลย
ลองปรับแนวนอนดู
ก็แปลว่าใช้ได้แล้ว เอาไปประยุคใช้หากินกันต่อไปแล้วกันนะครับ ทุกคน แนะนำว่าอย่าทำ ฟรีทีวีเลยแต่ทำเป็นช่องทีวีของตัวเองก็ยังดีกว่านะครับ ไปลงทุนส่วนของ Server ก็ไม่มีปัญหาแล้ว