本讲涉及的类:
HomeActivity
RequestCache
JamendoGet2ApiImpl
JamendoGet2Api
WSError
AlbumClickedListener
PurpleListener
PurpleEntity
Caller
Home页面UI如下图所示:
(一) TitleBar的实现:自定义ViewFilpper
自定义的ViewFlipper中,包含三个自定义组件:
Xml文件显示如下:
- <com.teleca.jamendo.util.FixedViewFlipper
- android:id="@+id/ViewFlipper"
- android:layout_width="fill_parent"
- android:layout_height="75dip"
- android:background="@drawable/gradient_dark_purple"
- android:orientation="vertical" >
- <!-- (0) Loading -->
- <LinearLayout
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:layout_marginLeft="15dip"
- android:gravity="left|center_vertical"
- android:orientation="vertical" >
- <com.teleca.jamendo.widget.ProgressBar
- android:id="@+id/ProgressBar"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content" >
- </com.teleca.jamendo.widget.ProgressBar>
- </LinearLayout>
- <!-- (1) Gallery -->
- <LinearLayout
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:gravity="center"
- android:orientation="vertical" >
- <Gallery
- android:id="@+id/Gallery"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:spacing="0px" />
- </LinearLayout>
- <!-- (2) Failure -->
- <LinearLayout
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:layout_marginLeft="15dip"
- android:gravity="left|center_vertical"
- android:orientation="vertical" >
- <com.teleca.jamendo.widget.FailureBar
- android:id="@+id/FailureBar"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content" >
- </com.teleca.jamendo.widget.FailureBar>
- </LinearLayout>
- </com.teleca.jamendo.util.FixedViewFlipper>
1,自定义ProgressBar
如下图所示:
2,自定义FailureBar
如下图所示:
简化起见,只列出FailureBar的代码:
- public class FailureBar extends LinearLayout{
- protected TextView mTextView;
- protected Button mRetryButton;
- public FailureBar(Context context, AttributeSet attrs) {
- super(context, attrs);
- init();
- }
- public FailureBar(Context context) {
- super(context);
- init();
- }
- /**
- * Sharable code between constructors
- */
- private void init(){
- LayoutInflater.from(getContext()).inflate(R.layout.failure_bar, this);
- mTextView = (TextView)findViewById(R.id.ProgressTextView);
- mRetryButton = (Button)findViewById(R.id.RetryButton);
- }
- /**
- * Sets informative text
- *
- * @param resid
- */
- public void setText(int resid){
- mTextView.setText(resid);
- }
- /**
- * Sets action to be performed when Retry button is clicked
- *
- * @param l
- */
- public void setOnRetryListener(OnClickListener l){
- mRetryButton.setOnClickListener(l);
- }
- }
关键代码在init()方法中,即组合式自定义布局,很简单。按键的监听命名很好。适合我们的习惯 。
(二) 自定义Listener接口
1,AlbumClickedListener
我靠,这个接口在HomeActivity里根本没有用到,虽然实现了这个接口。这个接口看了好一会,觉得比较怪,打了log,Album点击事件其实是放在Gallery的点击事件里完成的。代码如下:
- private OnItemClickListener mGalleryListener = new OnItemClickListener(){
- @Override
- public void onItemClick(AdapterView<?> adapterView, View view, int position,
- long id) {
- Album album = (Album) adapterView.getItemAtPosition(position);
- PlayerActivity.launch(HomeActivity.this, album);
- Log.d("album_click", "done in GalleryListener");
- }
- };
2,PurpleListener:监听主ListView每一栏点击事件
代码如下:
- public interface PurpleListener {
- /**
- * Callback to be invoked when PurpleEntry is selected
- */
- public void performAction();
- }
主ListView内容的封装类PurpleEntry中的构造方法,其中一个参数是PurpleListener,代码如下:
- public PurpleEntry(Integer drawable, Integer text, PurpleListener listener) {
- mDrawable = drawable;
- mTextId = text;
- mListener = listener;
- }
主Activity中,PurpleEntity通过以下的方式实现了PurpleListener,代码如下:
- browseListEntry.add(new PurpleEntry(R.drawable.list_search, R.string.search, new PurpleListener(){
- @Override
- public void performAction() {
- SearchActivity.launch(HomeActivity.this);
- }
- }));
(三) 手势
Xml布局如下:
- <android.gesture.GestureOverlayView
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/gestures"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:eventsInterceptionEnabled="false"
- android:gestureStrokeType="multiple"
- android:orientation="vertical" >
- <ListView
- android:id="@+id/HomeListView"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:divider="#000" />
- </android.gesture.GestureOverlayView>
代码如下:
首先:onResume()方法中获取手势的设置:是否使用手势,代码如下:
- @Override
- protected void onResume() {
- fillHomeListView();
- boolean gesturesEnabled = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("gestures", true);
- mGestureOverlayView.setEnabled(gesturesEnabled);
- super.onResume();
- }
再在onCreate()方法中调用相关方法,Jamendo中在JamendoApp中做了封装,本文暂不涉及其封装,会在未来分析,只列出HomeActivity中的代码:
- mGestureOverlayView = (GestureOverlayView) findViewById(R.id.gestures);
- mGestureOverlayView.addOnGesturePerformedListener(JamendoApplication
- .getInstance().getPlayerGestureHandler());
如果要单独实现手势,具体的实现步骤很简单,我不在此赘述,只列出顺序:
1, 创建gesture lib
2, 导入项目
如下图所示:
3, 代码引入并实现
(四) 封装:以TitleBar为例展开
在本例中,图片的加载使用异步AsyncTask完成。代码如下:
AsyncTask的方法进行的顺序依次为,参看下图,单击放大:
绿色部分在主线程,黑色部分在其他线程。
1, 错误提示:
WSError继承Throwable封装了错误信息。通过publishProgress()方法,刷新UI错误提示部分。在通过http请求的过程中,捕获到相关异常,初始化一个WSError对象,抛出,即可在关联的TextView上显示出来。
WSError代码如下:
- public class WSError extends Throwable {
- /**
- *
- */
- private static final long serialVersionUID = 1L;
- private String message;
- public WSError() {
- }
- public WSError(String message) {
- this.message = message;
- }
- public void setMessage(String message) {
- this.message = message;
- }
- public String getMessage() {
- return message;
- }
- }
当捕获到相关异常时,抛出WSError即可,如下示例代码(Caller类中执行HttpGet连接):
- try {
- // execute request
- try {
- httpResponse = httpClient.execute(httpGet);//apache http step3:
- } catch (UnknownHostException e) {
- throw new WSError("Unable to access " + e.getLocalizedMessage());
- } catch (SocketException e){
- throw new WSError(e.getLocalizedMessage());
- }
2, 数据获取:
A, JamendoGet2ApiImpl实现接口JamendoGet2Api定义的方法。
我注意到,JamendoGet2Api类的作者为:Lukasz Wisniewski,JamendoGet2ApiImpl类的作者为 Lukasz Wisniewski,Marcin Gil。显然为了提高效率,有人定义了方法,有人去实现。在这里,我的体会是接口体现架构的意味。
B, JamendoGet2ApiImpl内部定义了Caller类,封装了ApacheHttp的api。在此,涉及到缓存的问题。因为资源的使用是全局的,所以在入口处JamendoApplication类中初始化了缓存类RequestCache,此处缓存的是最近的十次请求内容。
缓存类代码如下:
- public class RequestCache {
- // TODO cache lifeTime
- private static int CACHE_LIMIT = 10;
- @SuppressWarnings("unchecked")
- private LinkedList history;
- private Hashtable<String, String> cache;
- @SuppressWarnings("unchecked")
- public RequestCache(){
- history = new LinkedList();
- cache = new Hashtable<String, String>();
- }
- @SuppressWarnings("unchecked")
- public void put(String url, String data){
- history.add(url);
- // too much in the cache, we need to clear something
- if(history.size() > CACHE_LIMIT){
- String old_url = (String) history.poll();
- cache.remove(old_url);
- }
- cache.put(url, data);
- }
- public String get(String url){
- return cache.get(url);
- }
- }
显然,Jamendo使用的是HashTable,为什么不使用HashMap,两者的不同,见http://mikewang.blog.51cto.com/3826268/856865,不在此赘述!
缓存的实现:
使用LinkedList存储url,并定义最大缓存数为10,当LinkedList的size大于10,就使用poll()方法,获取并移除此列表的头(第一个元素)。
C, Android总使用Apache的http请求主要有以下五个步骤:参考代码的注释:
Step1:创建一个HttpClient(或获取现有应用)
Step2:实例化新Http方法(三类,get,post,put)
Step3: 设置Http参数名称值,例如绑定Entity或设置延时
Step4:使用HttpClient执行Http调用
Step5:处理Http响应
代码如下,标记出了顺序,第一步第二步先后没有关系:
- public static String doGet(String url) throws WSError{
- String data = null;
- if(requestCache != null){
- data = requestCache.get(url);
- if(data != null){
- Log.d(JamendoApplication.TAG, "Caller.doGet [cached] "+url);
- return data;
- }
- }
- URI encodedUri = null;
- HttpGet httpGet = null;
- try {
- encodedUri = new URI(url);
- httpGet = new HttpGet(encodedUri);//apache http step2:
- } catch (URISyntaxException e1) {
- // at least try to remove spaces
- String encodedUrl = url.replace(' ', '+');
- httpGet = new HttpGet(encodedUrl);
- e1.printStackTrace();
- }
- // initialize HTTP GET request objects
- HttpClient httpClient = new DefaultHttpClient();//apache http step1:
- HttpResponse httpResponse;
- try {
- // execute request
- try {
- httpResponse = httpClient.execute(httpGet);//apache http step3:
- } catch (UnknownHostException e) {
- throw new WSError("Unable to access " + e.getLocalizedMessage());
- } catch (SocketException e){
- throw new WSError(e.getLocalizedMessage());
- }
- //apche http step4:
- // request data
- HttpEntity httpEntity = httpResponse.getEntity();//apache http step5:
- if(httpEntity != null){
- InputStream inputStream = httpEntity.getContent();
- data = convertStreamToString(inputStream);
- // cache the result
- if(requestCache != null){
- requestCache.put(url, data);
- }
- }
- } catch (ClientProtocolException e) {
- e.printStackTrace();
- } catch (IOException e) {
- e.printStackTrace();
- }
- Log.d(JamendoApplication.TAG, "Caller.doGet "+url);
- return data;
- }
D, 其他
缓存也可以使用线程池来完成,以后我会整理相关内容。对于缓存的处理更加简单。
3, 主UI:
效果如下图:
A, 在onResume()方法中加载主ListView和Gesture,代码如下:
- @Override
- protected void onResume() {
- fillHomeListView();
- boolean gesturesEnabled = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("gestures", true);
- mGestureOverlayView.setEnabled(gesturesEnabled);
- super.onResume();
- }
在onCreate()方法中,执行标题栏的异步加载(AsyncTask),代码如下:
- new NewsTask().execute((Void)null);
B, TitleBar通过自定义的ViewFlipper实现,主ListView通过自定义SeparatedListAdapter实现。Jamendo将(标题+标准ListView<自定义 PurpleAdapter>)封装成一个方法,每次调用Add方法,加入一栏及其内容。
C, 开始我觉得不够清晰化。Adapter太多,其实只需要一个Adapter。但是:看了项目其他部分对PurpleAdapter的引用之后,觉得Jamendo是想实现代码的复用。另外,Jamendo将点击事件封装在PurpleEntry中也不错。清晰,设置ItemOnClick方法,实现PurpleListener方法。代码如下:
- /**
- * Fills ListView with clickable menu items
- */
- private void fillHomeListView(){
- mBrowseJamendoPurpleAdapter = new PurpleAdapter(this);
- mMyLibraryPurpleAdapter = new PurpleAdapter(this);
- ArrayList<PurpleEntry> browseListEntry = new ArrayList<PurpleEntry>();
- ArrayList<PurpleEntry> libraryListEntry = new ArrayList<PurpleEntry>();
- // BROWSE JAMENDO
- browseListEntry.add(new PurpleEntry(R.drawable.list_search, R.string.search, new PurpleListener(){
- @Override
- public void performAction() {
- SearchActivity.launch(HomeActivity.this);
- }
- }));
- browseListEntry.add(new PurpleEntry(R.drawable.list_radio, R.string.radio, new PurpleListener(){
- @Override
- public void performAction() {
- RadioActivity.launch(HomeActivity.this);
- }
- }));
- browseListEntry.add(new PurpleEntry(R.drawable.list_top, R.string.most_listened, new PurpleListener(){
- @Override
- public void performAction() {
- new Top100Task(HomeActivity.this, R.string.loading_top100, R.string.top100_fail).execute();
- }
- }));
- // MY LIBRARY
- libraryListEntry.add(new PurpleEntry(R.drawable.list_playlist, R.string.playlists, new PurpleListener(){
- @Override
- public void performAction() {
- BrowsePlaylistActivity.launch(HomeActivity.this, Mode.Normal);
- }
- }));
- // check if we have personalized client then add starred albums
- final String userName = PreferenceManager.getDefaultSharedPreferences(this).getString("user_name", null);
- if(userName != null && userName.length() > 0){
- libraryListEntry.add(new PurpleEntry(R.drawable.list_cd, R.string.albums, new PurpleListener(){
- @Override
- public void performAction() {
- StarredAlbumsActivity.launch(HomeActivity.this, userName);
- }
- }));
- }
- /* following needs jamendo authorization (not documented yet on the wiki)
- * listEntry.add(new PurpleEntry(R.drawable.list_mail, "Inbox"));
- */
- // show this list item only if the SD Card is present
- if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
- libraryListEntry.add(new PurpleEntry(R.drawable.list_download, R.string.download, new PurpleListener(){
- @Override
- public void performAction() {
- DownloadActivity.launch(HomeActivity.this);
- }
- }));
- }
- // listEntry.add(new PurpleEntry(R.drawable.list_star, R.string.favorites, new PurpleListener(){
- //
- // @Override
- // public void performAction() {
- // Playlist playlist = new DatabaseImpl(HomeActivity.this).getFavorites();
- // JamendroidApplication.getInstance().getPlayerEngine().openPlaylist(playlist);
- // PlaylistActivity.launch(HomeActivity.this, true);
- // }
- //
- // }));
- // attach list data to adapters
- mBrowseJamendoPurpleAdapter.setList(browseListEntry);
- mMyLibraryPurpleAdapter.setList(libraryListEntry);
- // separate adapters on one list
- SeparatedListAdapter separatedAdapter = new SeparatedListAdapter(this);
- separatedAdapter.addSection(getString(R.string.browse_jamendo), mBrowseJamendoPurpleAdapter);
- separatedAdapter.addSection(getString(R.string.my_library), mMyLibraryPurpleAdapter);
- mHomeListView.setAdapter(separatedAdapter);
- mHomeListView.setOnItemClickListener(mHomeItemClickListener);
- }
备注:不考虑复用,单一Adapter也可完成分栏式Adapter,见本人博客:
http://mikewang.blog.51cto.com/3826268/737715
(五) 菜单的实现
可以指定条件显示或隐藏菜单的某一项,代码如下:
- @Override
- public boolean onPrepareOptionsMenu(Menu menu) {
- if(JamendoApplication.getInstance().getPlayerEngineInterface() == null || JamendoApplication.getInstance().getPlayerEngineInterface().getPlaylist() == null){
- menu.findItem(R.id.player_menu_item).setVisible(false);
- } else {
- menu.findItem(R.id.player_menu_item).setVisible(true);
- }
- return super.onPrepareOptionsMenu(menu);
- }