Android视频客户端的设计与实现
笔者最近正在给网站视频模块开发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请求
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,并对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 = "where 1=1 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
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。