1.前言
笔者最近正在给网站视频模块开发android手机客户端,通过手机客户端可以很方便的浏览网站的视频内容,网站的视频内容大部分是flv和mp4格式,以下为手机客户端的部分截图:
下面记录下笔者的开发过程和注意事项
2.开发工具
项目基于Android Studio IDE构建,Android Studio是2013 google I/O开发者大会推出的,基于IntelliJ idea构建,android studio一直在更新完善,今天已经到了0.4.6预览版,我估计到了今年的2014 google I/O大会会到1.0稳定版。有人担心从Eclipse迁移到Android Studio不适应,不稳定,影响开发进度,这里从笔者的亲身体验告诉大家Android Studio用起来真的很容易上手,而且大大提高开发进度,Android Studio是未来的方向!Android Studio还集成了先进的Gradle构建系统,本项目也是基于Gradle项目构建,对于Android项目中经常要依赖Library projects很方便,关于Gradle,大家可以参看google官方文档 http://tools.android.com/tech-docs/new-build-system/user-guide,Android Studio还集成了VCS版本控制系统,笔者可以很方便的将源码提交到github上。
3.Android客户端项目的构建
本项目的建立参考了代码家设计的AnimeTaste,感谢代码家的开源!下面简单介绍下实现思路:手机客户端通过向服务器端发送http请求,服务器端api接口返回json数据,然后手机客户端解析json数据,然后将数据展示在listview中。
(1)项目的目录结构
(4)StartActivity获得getIntent().hasExtra("LoadData")获得传递来的数据
(6)VideoListItemListener单击监听类,当用户单击条目时启动PlayActivity。
①根目录新建 libraries文件夹
②将vitamio拷贝到libraries文件夹
③修改settings.gradle
笔者最近正在给网站视频模块开发android手机客户端,通过手机客户端可以很方便的浏览网站的视频内容,网站的视频内容大部分是flv和mp4格式,以下为手机客户端的部分截图:
下面记录下笔者的开发过程和注意事项
2.开发工具
项目基于Android Studio IDE构建,Android Studio是2013 google I/O开发者大会推出的,基于IntelliJ idea构建,android studio一直在更新完善,今天已经到了0.4.6预览版,我估计到了今年的2014 google I/O大会会到1.0稳定版。有人担心从Eclipse迁移到Android Studio不适应,不稳定,影响开发进度,这里从笔者的亲身体验告诉大家Android Studio用起来真的很容易上手,而且大大提高开发进度,Android Studio是未来的方向!Android Studio还集成了先进的Gradle构建系统,本项目也是基于Gradle项目构建,对于Android项目中经常要依赖Library projects很方便,关于Gradle,大家可以参看google官方文档 http://tools.android.com/tech-docs/new-build-system/user-guide,Android Studio还集成了VCS版本控制系统,笔者可以很方便的将源码提交到github上。
3.Android客户端项目的构建
本项目的建立参考了代码家设计的AnimeTaste,感谢代码家的开源!下面简单介绍下实现思路:手机客户端通过向服务器端发送http请求,服务器端api接口返回json数据,然后手机客户端解析json数据,然后将数据展示在listview中。
(1)项目的目录结构
(2)项目的LoadActivity为app的main Activity启动界面,init()方法主要是从服务器端获取数据进行数据的初始化,服务器端返回的数据为JSONArray格式,即变量response,通过intent.putExtra("LoadData",response.toString())将数据放在intent中以便将数据传递到StartActivity.
package com.zyy360.app;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.support.v7.app.ActionBarActivity;
import com.loopj.android.http.JsonHttpResponseHandler;
import com.zyy360.app.core.DataVideoFetcher;
import com.zyy360.app.ui.StartActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import org.json.JSONArray;
/**
* @author daimajia
* @modified Foxhu
* @version 1.0
*/
public class LoadActivity extends ActionBarActivity {
private Context mContext;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if(getSupportActionBar() != null){
getSupportActionBar().hide();
}
mContext = this;
setContentView(R.layout.activity_load);
if (NetworkUtils.isWifi(mContext) == false){
AlertDialog.Builder builder = new AlertDialog.Builder(mContext)
.setTitle(R.string.only_wifi_title).setMessage(R.string.only_wifi_body);
builder.setCancelable(false);
//if user click ok then init data
builder.setPositiveButton(R.string.only_wifi_ok,
new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
init();
}
});
//if user click quit then finish()
builder.setNegativeButton(R.string.only_wifi_cancel,
new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
finish();
}
});
builder.create().show();
}else{
init();
}
}
/**
* init data
*/
private void init(){
DataVideoFetcher.instance().getList(0,new JsonHttpResponseHandler(){
/**
* The server returns data format like
* [{"name":"冬虫夏草","path":"2013/10/25_152747_61dP.flv","video_pic":"20131025/IMG_9La6_25_b.jpg","video_thumbpic":"20131025/IMG_BjTA_25_s.jpg","introduce":"冬虫夏草多种功效。","___key_id":25},
* {"name":"防风","path":"2013/10/25_152557_pmYc.flv","video_pic":"20131025/IMG_H0zO_24_b.jpg","video_thumbpic":"20131025/IMG_3b76_24_s.jpg","introduce":"解表药、祛风药","___key_id":24}]
* reference documnets
* http://loopj.com/android-async-http/doc/com/loopj/android/http/JsonHttpResponseHandler.html
* @param statusCode
* @param response
*/
@Override
public void onSuccess(int statusCode, JSONArray response) {
super.onSuccess(statusCode, response);
System.out.println("jsonArray->>"+response);
Intent intent = new Intent(LoadActivity.this,StartActivity.class);
if (statusCode == 200 && response.length()>0){
try {
intent.putExtra("LoadData",response.toString());
startActivity(intent);
finish();
}catch (Exception e) {
e.printStackTrace();
}
}else{}
}
@Override
public void onFailure(Throwable e, JSONArray errorResponse) {
super.onFailure(e, errorResponse);
System.out.println("jsonArray->>"+errorResponse);
Toast.makeText(getApplicationContext(), R.string.error_load,
Toast.LENGTH_SHORT).show();
startActivity(new Intent(mContext, StartActivity.class));
finish();
}
});
}
@Override
protected void onResume() {
super.onResume();
}
@Override
protected void onPause() {
super.onPause();
}
}
(3)DataVideoFetcher是通过使用android-async-http这个库实现想服务器端发送post或get请求,关于android-async-http的使用,请参考我之前的博文
http://blog.youkuaiyun.com/hil2000/article/details/13949513
package com.zyy360.app.core;
import com.loopj.android.http.AsyncHttpClient;
import com.loopj.android.http.JsonHttpResponseHandler;
/**
* @author daimajia
* @modified Foxhu
* @version 1.0
*/
public class DataVideoFetcher {
private static DataVideoFetcher mInstance;
//request url with parameter page
private static final String mRequestListUrl = "http://192.168.0.101:8080/action/api/videoList?page=%d";
private DataVideoFetcher() {
}
public static DataVideoFetcher instance() {
if (mInstance == null) {
mInstance = new DataVideoFetcher();
}
return mInstance;
}
/**
* get data from server by AsyncHttpClient
* @param page
* @param handler
*/
public void getList(int page,JsonHttpResponseHandler handler){
AsyncHttpClient client = new AsyncHttpClient();
String request = String.format(mRequestListUrl,page);
//get json data from server
client.get(request,null,handler);
}
}
而LoadActivity的 DataVideoFetcher.instance().getList()中的new JsonHttpResponseHandler()对onSuccess和onFailure进行了重写.
(4)StartActivity获得getIntent().hasExtra("LoadData")获得传递来的数据
if (getIntent().hasExtra("LoadData")) {
init(getIntent().getStringExtra("LoadData"));
} else {
init();
}
其中init为初始化数据
public void init(String data) {
try {
JSONArray videoList = new JSONArray(data);
if (videoList != null) {
new AddToDBThread(videoList).start();
}
mVideoAdapter = VideoListAdapter.build(mContext, videoList, true);
mVideoList.setAdapter(mVideoAdapter);
} catch (JSONException e) {
e.printStackTrace();
}
}
其中mVideoAdapter = VideoListAdapter.build(mContext, videoList, true);基于JsonArray数据构建,我们看下VideoListAdapter的builde方法build(Context context, JSONArray data,Boolean checkIsWatched)
public static VideoListAdapter build(Context context, JSONArray data,
Boolean checkIsWatched) throws JSONException {
ArrayList<VideoDataFormat> videos = new ArrayList<VideoDataFormat>();
for (int i = 0; i < data.length(); i++) {
videos.add(VideoDataFormat.build(data.getJSONObject(i)));
}
return new VideoListAdapter(context, videos, checkIsWatched);
}
其中videos.add(VideoDataFormat.build(data.getJSONObject(i)));通过VideoDataFormat的build方法解析JSONObjec对象,VideoDataFormat类如下
package com.zyy360.app.model;
import android.database.Cursor;
import org.json.JSONObject;
import org.json.JSONException;
import java.io.Serializable;
/**
* @author Foxhu
* @version 1.0
*/
public class VideoDataFormat implements Serializable {
public Integer id;
public String name;//视频名称
public String path;//视频地址
public String video_pic;//视频图片
public String video_thumbpic; //视频缩略图
public String introduce;//视频简介
public String create_time;
//缩略图地址 http://192.168.0.101:8080/uploads/videopics/20131025/IMG_BjTA_25_s.jpg
//大图地址 http://192.168.0.101:8080/uploads/videopics/20131025/IMG_BjTA_25_b.jpg
//视频地址 http://192.168.0.101:8080/uploads/videofiles/2013/10/25_152747_61dP.flv
private boolean IsWatched;
private final String VideoUrlFormat = "http://192.168.0.101:8080/uploads/videofiles/%s";
private final String PicUrlFormat = "http://192.168.0.101:8080/uploads/videopics/%s";
public static final String NONE_VALUE = "-1";
private VideoDataFormat(Integer id, String name, String path,String video_pic,
String video_thumbPic,String introduce,String create_time)
{
super();
this.id = id;
this.name = name;
this.path = String.format(VideoUrlFormat, path);//根据原始地址构建完整url地址
this.video_pic = String.format(PicUrlFormat, video_pic);
this.video_thumbpic = String.format(PicUrlFormat, video_thumbPic);
this.introduce = introduce;
this.create_time = create_time;
}
private VideoDataFormat(JSONObject object){
id = Integer.valueOf(getValue(object,"___key_id"));
name = getValue(object, "name");
path = String.format(VideoUrlFormat, getValue(object, "path"));
video_pic = String.format(PicUrlFormat, getValue(object,"video_pic"));
video_thumbpic = String.format(PicUrlFormat, getValue(object,"video_thumbpic"));
introduce = getValue(object,"introduce");
create_time = getValue(object,"create_time");
IsWatched = false;
}
private static String getValue(JSONObject object, String key) {
try {
return object.getString(key);
} catch (JSONException e) {
e.printStackTrace();
}
return NONE_VALUE;
}
public boolean isWatched() {
return IsWatched;
}
public void setWatched(Boolean watch) {
IsWatched = watch;
}
public static VideoDataFormat build(JSONObject object) {
return new VideoDataFormat(object);
}
public static VideoDataFormat build(Cursor cursor) {
int id = cursor.getInt(cursor.getColumnIndex("id"));
String name = cursor.getString(cursor.getColumnIndex("name"));
String path = cursor.getString(cursor.getColumnIndex("path"));
String video_pic = cursor.getString(cursor.getColumnIndex("video_pic"));
String video_thumbPic = cursor.getString(cursor.getColumnIndex("video_thumbpic"));
String introduce = cursor.getString(cursor.getColumnIndex("introduce"));
String create_time = cursor.getString(cursor.getColumnIndex("create_time"));
return new VideoDataFormat(id, name, path,video_pic,video_thumbPic,introduce,create_time);
}
}
(5)VideoListAdapter中getView()方法
@Override
public View getView(int position, View convertView, ViewGroup parent) {
TextView titleTextView;
TextView contentTextView;
ImageView thumbImageView;
ViewHolder holder;
if (convertView == null) {
convertView = mLayoutInflater.inflate(R.layout.video_item, parent,
false);
titleTextView = (TextView) convertView.findViewById(R.id.title);
contentTextView = (TextView) convertView.findViewById(R.id.content);
thumbImageView = (ImageView) convertView.findViewById(R.id.thumb);
holder = new ViewHolder(titleTextView, contentTextView,
thumbImageView);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
titleTextView = holder.titleText;
contentTextView = holder.contentText;
thumbImageView = holder.thumbImageView;
}
VideoDataFormat video = (VideoDataFormat) getItem(position);
Picasso.with(mContext).load(video.video_thumbpic)
.placeholder(R.drawable.placeholder_thumb)
.error(R.drawable.placeholder_fail).into(thumbImageView);
titleTextView.setText(video.name);
contentTextView.setText(video.introduce);
convertView.setOnClickListener(new VideoListItemListener(mContext,
this, video));
convertView.setOnLongClickListener(new View.OnLongClickListener() {
// 保证长按事件传递
@Override
public boolean onLongClick(View v) {
return false;
}
});
if (video.isWatched() == true) {
titleTextView.setTextColor(mWatchedTitleColor);
} else {
titleTextView.setTextColor(mUnWatchedTitleColor);
}
return convertView;
}
用于向视图控件装载数据,其中图片数据的加载采用第三方图片缓存库Picasso(picasso是Square公司开源的一个Android图形缓存库,地址
http://square.github.io/picasso/,可以实现图片下载和缓存功能),并对view条目设置监听convertView.setOnClickListener(new VideoListItemListener(mContext,this, video));以便启动播放界面PlayActivity
(6)VideoListItemListener单击监听类,当用户单击条目时启动PlayActivity。
@Override
public void onClick(View v) {
Intent intent = new Intent(mContext, PlayActivity.class);
intent.putExtra("VideoInfo", mData);
mContext.startActivity(intent);
mVideoDB.insertWatched(mData);
if (mAdapter != null) {
if (mData.isWatched() == false)
mAdapter.setWatched(mData);
}
}
(7)PlayActivity类,该类主要是利用第三方视频播放库vitamio实现视频播放,关于vitamio,请参考https://github.com/yixia/VitamioBundle,关于PlayActivity请参考笔者的github源码。这里涉及到Android项目如何引入第三方library project。Android Studio的项目由于采用Gradle构建,所以引入library project与Eclipse不同。主要步骤如下,这里以vitamio为例:
①根目录新建 libraries文件夹
②将vitamio拷贝到libraries文件夹
③修改settings.gradle
include ':app'
include(':libraries:vitamio')
④.修改app的build.gradle文件
dependencies {
compile 'com.android.support:support-v4:19.0.+'
compile 'com.android.support:appcompat-v7:+'
compile fileTree(dir: 'libs', include: '*.jar')
compile project(':libraries:vitamio')
}
以上修改完后记得Sync project with Gradle Files
4.服务器端API接口设计
服务器端接收用户的http请求,通过ctx.param获取参数,然后从数据库查询数据,利用google 的GSON库,将list数据转成JSONArray数据返回给客户端
package com.cmsis.action;
import java.io.IOException;
import java.util.List;
import javax.servlet.ServletException;
import com.cmsis.beans.Video;
import com.google.gson.Gson;
/**
*
* @author Foxhu
*
*/
public class ApiAction extends BaseAction {
private static final String homeIds = "order by id desc";
/**
* 网站视频客户端api,返回数据格式为JsonArray
* @param ctx
* @throws IOException
*/
public void videoList(RequestContext ctx) throws IOException{
int pageno = ctx.param("page", 1);//获取手机客户端请求页码
pageno = pageno <= 0 ? 1 : pageno;
List<Long> ids = Video.INSTANCE.IDs(homeIds);//从缓存中获取加载数据id
int size = ids.size();
int beginIndex = (pageno - 1) * 10;//每页记录10条
int toIndex = pageno * 10;
List<Long> returnIds = ids.subList((beginIndex > size ? size : beginIndex), (toIndex > size ? size : toIndex));
List<Video> list = Video.INSTANCE.LoadList(returnIds);//根据id加载数据
Gson gson = new Gson();
String jsonList = gson.toJson(list.toArray());
System.out.println("json->>"+jsonList);
ctx.print(jsonList);
}
}
github源码地址:
https://github.com/puma007/Zyy360