Android视频客户端的设计与实现

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)项目的目录结构

 

(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.csdn.net/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


发布了41 篇原创文章 · 获赞 114 · 访问量 68万+

猜你喜欢

转载自blog.csdn.net/hil2000/article/details/20654151