Jamendo学习笔记

来源:互联网 发布:天津大学网络远程教育 编辑:程序博客网 时间:2024/06/05 23:59

http://mikewang.blog.51cto.com/3826268/737713

1:欢迎页面(SplashView)的实现

实现方法有两种:
1,使用Activity的跳转,SplashView为第一个Activity,解析放在此Activity,解析完成后,跳转至MainActivity,在此不赘述。
 
2,  使用帧布局,让splash view覆盖在main view之上。先显示splash view(在此过程中执行耗时操作),后显示main view。注意的问题是:多线程之间的切换。(刷UI只能在Main Thread)。
 
Splash view 可以用一个线性布局(嵌套一个ImageView和一个动态显示),Image为公司logo,动态显示可以选择ProgressBar或者Animation
 
以下为截图:
 
代码如下:
 
new Thread() { 
      public void run() { 
        String strJSON = getJSON(); 
        if (strJSON != null) { 
          mAlbums = parseAlbumJSON(strJSON); 
          runOnUiThread(new Runnable() { 
            public void run() { 
              mGalleryAdapter.notifyDataSetChanged(); 
              mSplashView.setVisibility(View.GONE); 
              mMainView.setVisibility(View.VISIBLE); 
              initGallery(); 
            } 
          }); 
        } else { 
          // 得不到专辑地址! 
          return
        } 
      }; 
    }.start();
 
注释图片:在此不能完整显示,可以下载后看,很清楚。 
 

2:ListView分栏的实现
ListView分栏(Columnar ListView)的好处在于,区分内容,便于用户操作选择。核心思想是:在指定位置插入标题内容。
 
实现方法如下:
 
 
1,  实现AdapterareAllItemsEnabled()方法和isEnabled()方法,确定内容
 
显然,ListViewItem中,标题不可被操作,内容部分可响应点击(click)和长按(longClick)时间。设置如下:
@Override 

                                     public boolean areAllItemsEnabled() { 

                                                        // TODO Auto-generated method stub 

                                                        return false

                                     } 

    

                                     @Override 

                                     public boolean isEnabled(int position) { 

                                                        // TODO Auto-generated method stub 

                                                        if (position == 0 || position == 4) {// 

                                                                         return false

                                                        } else { 

                                                                         return true

                                                        } 

                                     } 


 
2,ListView要显示的内容中,添加两个标题内容。如增加数组长度或StringArray长度等。
3,  getView()方法中,做判断,代码如下:
 
if (position == 0 || position == 4) { 
        TextView textView = new TextView(MainActivity.this); 
        textView.setText(mMainListContents[position]); 
        textView.setBackgroundColor(android.R.color.black); 
        return textView; 
      }
4,效果如下:

三:欢迎页面的实现Plus

按:之前写了几篇Jamendo的学习笔记,最近准备全面的将Jamendo完整的分析学习一遍。我将参考GaoMatrix的相关博文:http://blog.csdn.net/gaomatrix/article/category/900092。

学习并补充。

整体的文章结构将按照app的界面功能先后展开。

本讲涉及的类:SplashActivity HomeActivity,定位至activity包找到即可。

(一) App第一个Activity的定位

显然,manifest文件中,搜索关键词”MAIN”即可定位。另外,也可参考log信息,搜索“Displayed”信息,第一个显示的即为入口Activity,本App显然为SplashActivity

manifest.xml定位如下:

  1. <activity android:name=".activity.SplashscreenActivity" > 
  2.             <intent-filter> 
  3.                 <action android:name="android.intent.action.MAIN" > 
  4.                 </action> 
  5.  
  6.                 <category android:name="android.intent.category.LAUNCHER" > 
  7.                 </category> 
  8.             </intent-filter> 
  9.         </activity> 

(二) Activity风格的代码设置:无标题

代码如下:

  1. requestWindowFeature(Window.FEATURE_NO_TITLE); 

(三) 动画的使用:新属性

AnimationUtils的使用:通过上下文(Context)和资源id加载动画

属性setFillAfter,如果设置为true,代表动画播放完毕停留在最后一帧的画面。

代码如下:

  1. endAnimation = AnimationUtils.loadAnimation(this, R.anim.fade_out); 
  2.         endAnimation.setFillAfter(true);//动画播放完毕,停在最后一帧 

更多动画内容,参看我的动画博文:

(四) 监听

A, Dialog结束的监听

代码:

  1. final TutorialDialog dlg = new TutorialDialog(this); 
  2.             dlg.setOnDismissListener(new DialogInterface.OnDismissListener() { 
  3.                 @Override 
  4.                 public void onDismiss(DialogInterface dialog) { 
  5.                     CheckBox cb = (CheckBox) dlg.findViewById(R.id.toggleFirstRun); 
  6.                     if (cb != null && cb.isChecked()) { 
  7.                         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SplashscreenActivity.this); 
  8.                         prefs.edit().putBoolean(FIRST_RUN_PREFERENCE, false).commit(); 
  9.                     } 
  10.  
  11.                      
  12.                     int activeCount = Thread.activeCount(); 
  13.                     Log.d("animation""activeCount before is:" + activeCount); 
  14.                     endAnimationHandler.removeCallbacks(endAnimationRunnable); 
  15.                     activeCount = Thread.activeCount(); 
  16.                     Log.d("animation""activeCount after is:" + activeCount); 
  17.                     endAnimationHandler.postDelayed(endAnimationRunnable, 2000); 
  18.                 } 
  19.             }); 
  20.             dlg.show(); 

B, Animation结束的监听

代码:

  1. endAnimation.setAnimationListener(new AnimationListener() { 
  2.             @Override 
  3.             public void onAnimationStart(Animation animation) { } 
  4.              
  5.             @Override 
  6.             public void onAnimationRepeat(Animation animation) { } 
  7.              
  8.             @Override 
  9.             public void onAnimationEnd(Animation animation) { 
  10.                 HomeActivity.launch(SplashscreenActivity.this); 
  11.                 SplashscreenActivity.this.finish(); 
  12.             } 
  13.         }); 

(五) 初始Activity的销毁

显然,以下代码完成SplashActivity的销毁和HomeActivity的启动,代码如下:

SplashActivity中:

  1. @Override 
  2.             public void onAnimationEnd(Animation animation) { 
  3.                 HomeActivity.launch(SplashscreenActivity.this); 
  4.                 SplashscreenActivity.this.finish(); 
  5.             } 

HomeActivity中:

  1. public static void launch(Context c){ 
  2.         Intent intent = new Intent(c, HomeActivity.class); 
  3.         intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP ); 
  4.         c.startActivity(intent); 
  5.     } 

 

本应用使用到了Android的加载模式(launch mode),可参看前文安卓加载模式(Android LauncherMode)》:

http://mikewang.blog.51cto.com/3826268/708600

除了设置Intent的FLAG之外,也可以如Jamendo通过代码实现。

文档翻译地址:待完成

SharePreference将以键值对的形式存储我们的设置,文档中的示例代码如下:

  1. public class Calc extends Activity { 
  2.     public static final String PREFS_NAME = "MyPrefsFile"
  3.  
  4.     @Override 
  5.     protected void onCreate(Bundle state){ 
  6.        super.onCreate(state); 
  7.        . . . 
  8.  
  9.        // Restore preferences 
  10.        SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0); 
  11.        boolean silent = settings.getBoolean("silentMode"false); 
  12.        setSilent(silent); 
  13.     } 
  14.  
  15.     @Override 
  16.     protected void onStop(){ 
  17.        super.onStop(); 
  18.  
  19.       // We need an Editor object to make preference changes. 
  20.       // All objects are from android.context.Context 
  21.       SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0); 
  22.       SharedPreferences.Editor editor = settings.edit(); 
  23.       editor.putBoolean("silentMode", mSilentMode); 
  24.  
  25.       // Commit the edits! 
  26.       editor.commit(); 
  27.     } 

本身实现比较简单,可参考文档学习。

(七) 自定义Dialog:

适用于显示指定内容的view,关于(About)页可使用。

定位到:TutorialDialog,核心代码:

  1. private final void initialize(final Context context) { 
  2.         setContentView(R.layout.tutorial); 
  3.         setTitle(R.string.tutorial_title); 
  4.          
  5.         Button mCloseButton = (Button)findViewById(R.id.closeTutorial); 
  6.         if (mCloseButton != null) { 
  7.             mCloseButton.setOnClickListener(new View.OnClickListener() { 
  8.                  
  9.                 @Override 
  10.                 public void onClick(View v) { 
  11.                     dismiss(); 
  12.                 } 
  13.             }); 
  14.         } 
  15.     } 

实际显示的应用提示如下图:

(八) 动画的显示

代码如下:

  1. endAnimationHandler.removeCallbacks(endAnimationRunnable); 
  2.             endAnimationHandler.postDelayed(endAnimationRunnable, 1500); 

疑问:不太明白,之前并未调用callback,为什么要首先Remove掉callback,再postDelay。


四:Home页面的实现

本讲涉及的类:

HomeActivity

RequestCache

JamendoGet2ApiImpl

JamendoGet2Api

WSError

AlbumClickedListener

PurpleListener

PurpleEntity

Caller

 

Home页面UI如下图所示:

(一) TitleBar的实现:自定义ViewFilpper

自定义的ViewFlipper中,包含三个自定义组件:

Xml文件显示如下:

  1. <com.teleca.jamendo.util.FixedViewFlipper 
  2.         android:id="@+id/ViewFlipper" 
  3.         android:layout_width="fill_parent" 
  4.         android:layout_height="75dip" 
  5.         android:background="@drawable/gradient_dark_purple" 
  6.         android:orientation="vertical" > 
  7.  
  8.         <!-- (0) Loading --> 
  9.  
  10.         <LinearLayout 
  11.             android:layout_width="fill_parent" 
  12.             android:layout_height="fill_parent" 
  13.             android:layout_marginLeft="15dip" 
  14.             android:gravity="left|center_vertical" 
  15.             android:orientation="vertical" > 
  16.  
  17.             <com.teleca.jamendo.widget.ProgressBar 
  18.                 android:id="@+id/ProgressBar" 
  19.                 android:layout_width="wrap_content" 
  20.                 android:layout_height="wrap_content" > 
  21.             </com.teleca.jamendo.widget.ProgressBar> 
  22.         </LinearLayout> 
  23.  
  24.         <!-- (1) Gallery --> 
  25.  
  26.         <LinearLayout 
  27.             android:layout_width="fill_parent" 
  28.             android:layout_height="fill_parent" 
  29.             android:gravity="center" 
  30.             android:orientation="vertical" > 
  31.  
  32.             <Gallery 
  33.                 android:id="@+id/Gallery" 
  34.                 android:layout_width="fill_parent" 
  35.                 android:layout_height="wrap_content" 
  36.                 android:spacing="0px" /> 
  37.         </LinearLayout> 
  38.  
  39.         <!-- (2) Failure --> 
  40.  
  41.         <LinearLayout 
  42.             android:layout_width="fill_parent" 
  43.             android:layout_height="fill_parent" 
  44.             android:layout_marginLeft="15dip" 
  45.             android:gravity="left|center_vertical" 
  46.             android:orientation="vertical" > 
  47.  
  48.             <com.teleca.jamendo.widget.FailureBar 
  49.                 android:id="@+id/FailureBar" 
  50.                 android:layout_width="wrap_content" 
  51.                 android:layout_height="wrap_content" > 
  52.             </com.teleca.jamendo.widget.FailureBar> 
  53.         </LinearLayout> 
  54.     </com.teleca.jamendo.util.FixedViewFlipper> 

1,自定义ProgressBar

   如下图所示:

2,自定义FailureBar

   如下图所示:

简化起见,只列出FailureBar的代码:

  1. public class FailureBar extends LinearLayout{ 
  2.      
  3.     protected TextView mTextView; 
  4.     protected Button mRetryButton; 
  5.      
  6.     public FailureBar(Context context, AttributeSet attrs) { 
  7.         super(context, attrs); 
  8.         init(); 
  9.     } 
  10.  
  11.     public FailureBar(Context context) { 
  12.         super(context); 
  13.         init(); 
  14.     } 
  15.      
  16.     /** 
  17.      * Sharable code between constructors 
  18.      */ 
  19.     private void init(){ 
  20.         LayoutInflater.from(getContext()).inflate(R.layout.failure_bar, this); 
  21.          
  22.         mTextView = (TextView)findViewById(R.id.ProgressTextView); 
  23.         mRetryButton = (Button)findViewById(R.id.RetryButton); 
  24.     } 
  25.      
  26.     /** 
  27.      * Sets informative text 
  28.      *  
  29.      * @param resid 
  30.      */ 
  31.     public void setText(int resid){ 
  32.         mTextView.setText(resid); 
  33.     } 
  34.      
  35.     /** 
  36.      * Sets action to be performed when Retry button is clicked  
  37.      *  
  38.      * @param l 
  39.      */ 
  40.     public void setOnRetryListener(OnClickListener l){ 
  41.         mRetryButton.setOnClickListener(l); 
  42.     } 
  43.  

关键代码在init()方法中,即组合式自定义布局,很简单。按键的监听命名很好。适合我们的习惯 。

 

1,AlbumClickedListener

   我靠,这个接口在HomeActivity里根本没有用到,虽然实现了这个接口。这个接口看了好一会,觉得比较怪,打了log,Album点击事件其实是放在Gallery的点击事件里完成的。代码如下:

  1. private OnItemClickListener mGalleryListener = new OnItemClickListener(){ 
  2.  
  3.         @Override 
  4.         public void onItemClick(AdapterView<?> adapterView, View view, int position, 
  5.                 long id) { 
  6.             Album album = (Album) adapterView.getItemAtPosition(position); 
  7.             PlayerActivity.launch(HomeActivity.this, album); 
  8.             Log.d("album_click""done in GalleryListener"); 
  9.         } 
  10.          
  11.     }; 

2,PurpleListener:监听主ListView每一栏点击事件

代码如下:

  1. public interface PurpleListener { 
  2.      
  3.     /** 
  4.      * Callback to be invoked when PurpleEntry is selected 
  5.      */ 
  6.     public void performAction(); 

 主ListView内容的封装类PurpleEntry中的构造方法,其中一个参数是PurpleListener,代码如下:

  1. public PurpleEntry(Integer drawable, Integer text, PurpleListener listener) { 
  2.         mDrawable = drawable; 
  3.         mTextId = text; 
  4.         mListener = listener; 
  5.     } 

 主Activity中,PurpleEntity通过以下的方式实现了PurpleListener,代码如下:

  1. browseListEntry.add(new PurpleEntry(R.drawable.list_search, R.string.search, new PurpleListener(){ 
  2.             @Override 
  3.             public void performAction() { 
  4.                 SearchActivity.launch(HomeActivity.this); 
  5.             } 
  6.         })); 

(三) 手势

Xml布局如下:

  1. <android.gesture.GestureOverlayView 
  2.         xmlns:android="http://schemas.android.com/apk/res/android" 
  3.         android:id="@+id/gestures" 
  4.         android:layout_width="fill_parent" 
  5.         android:layout_height="fill_parent" 
  6.         android:eventsInterceptionEnabled="false" 
  7.         android:gestureStrokeType="multiple" 
  8.         android:orientation="vertical" > 
  9.  
  10.         <ListView 
  11.             android:id="@+id/HomeListView" 
  12.             android:layout_width="fill_parent" 
  13.             android:layout_height="fill_parent" 
  14.             android:divider="#000" /> 
  15.     </android.gesture.GestureOverlayView> 

代码如下:

首先:onResume()方法中获取手势的设置:是否使用手势,代码如下:

  1. @Override 
  2.     protected void onResume() { 
  3.         fillHomeListView(); 
  4.         boolean gesturesEnabled = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("gestures"true); 
  5.         mGestureOverlayView.setEnabled(gesturesEnabled); 
  6.         super.onResume(); 
  7.     } 

再在onCreate()方法中调用相关方法,Jamendo中在JamendoApp中做了封装,本文暂不涉及其封装,会在未来分析,只列出HomeActivity中的代码:

  1. mGestureOverlayView = (GestureOverlayView) findViewById(R.id.gestures); 
  2.         mGestureOverlayView.addOnGesturePerformedListener(JamendoApplication 
  3.                 .getInstance().getPlayerGestureHandler()); 

 

如果要单独实现手势,具体的实现步骤很简单,我不在此赘述,只列出顺序:

1, 创建gesture lib

2, 导入项目

    如下图所示:

3, 代码引入并实现



(四) 封装:以TitleBar为例展开

在本例中,图片的加载使用异步AsyncTask完成。代码如下:

AsyncTask的方法进行的顺序依次为,参看下图,单击放大:

绿色部分在主线程,黑色部分在其他线程。

1, 错误提示:

WSError继承Throwable封装了错误信息。通过publishProgress()方法,刷新UI错误提示部分。在通过http请求的过程中,捕获到相关异常,初始化一个WSError对象,抛出,即可在关联的TextView上显示出来。

WSError代码如下:

  1. public class WSError extends Throwable { 
  2.  
  3.     /** 
  4.      *  
  5.      */ 
  6.     private static final long serialVersionUID = 1L; 
  7.      
  8.     private String message; 
  9.  
  10.     public WSError() { 
  11.     } 
  12.      
  13.     public WSError(String message) { 
  14.         this.message = message; 
  15.     } 
  16.      
  17.     public void setMessage(String message) { 
  18.         this.message = message; 
  19.     } 
  20.  
  21.     public String getMessage() { 
  22.         return message; 
  23.     } 
  24.  

当捕获到相关异常时,抛出WSError即可,如下示例代码(Caller类中执行HttpGet连接):

  1. try { 
  2.             // execute request 
  3.             try { 
  4.                 httpResponse = httpClient.execute(httpGet);//apache http step3: 
  5.             } catch (UnknownHostException e) { 
  6.                 throw new WSError("Unable to access " + e.getLocalizedMessage()); 
  7.             } catch (SocketException e){ 
  8.                 throw new WSError(e.getLocalizedMessage()); 
  9.             } 

 



2, 数据获取:

我注意到,JamendoGet2Api类的作者为:Lukasz Wisniewski,JamendoGet2ApiImpl类的作者为 Lukasz Wisniewski,Marcin Gil。显然为了提高效率,有人定义了方法,有人去实现。在这里,我的体会是接口体现架构的意味。

缓存类代码如下:

  1. public class RequestCache { 
  2.      
  3.     // TODO cache lifeTime 
  4.      
  5.     private static int CACHE_LIMIT = 10
  6.      
  7.     @SuppressWarnings("unchecked"
  8.     private LinkedList history; 
  9.     private Hashtable<String, String> cache; 
  10.      
  11.     @SuppressWarnings("unchecked"
  12.     public RequestCache(){ 
  13.         history = new LinkedList(); 
  14.         cache = new Hashtable<String, String>(); 
  15.     } 
  16.      
  17.      
  18.     @SuppressWarnings("unchecked"
  19.     public void put(String url, String data){ 
  20.         history.add(url); 
  21.         // too much in the cache, we need to clear something 
  22.         if(history.size() > CACHE_LIMIT){ 
  23.             String old_url = (String) history.poll(); 
  24.             cache.remove(old_url); 
  25.         } 
  26.         cache.put(url, data); 
  27.     } 
  28.      
  29.     public String get(String url){ 
  30.         return cache.get(url); 
  31.     } 

显然,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响应

代码如下,标记出了顺序,第一步第二步先后没有关系:

  1. public static String doGet(String url) throws WSError{ 
  2.          
  3.         String data = null
  4.         if(requestCache != null){ 
  5.             data = requestCache.get(url); 
  6.             if(data != null){ 
  7.                 Log.d(JamendoApplication.TAG, "Caller.doGet [cached] "+url); 
  8.                 return data; 
  9.             } 
  10.         } 
  11.          
  12.         URI encodedUri = null
  13.         HttpGet httpGet = null
  14.          
  15.         try { 
  16.             encodedUri = new URI(url); 
  17.             httpGet = new HttpGet(encodedUri);//apache http step2: 
  18.              
  19.         } catch (URISyntaxException e1) { 
  20.             // at least try to remove spaces 
  21.             String encodedUrl = url.replace(' ''+'); 
  22.             httpGet = new HttpGet(encodedUrl); 
  23.                      
  24.             e1.printStackTrace(); 
  25.         } 
  26.          
  27.         // initialize HTTP GET request objects 
  28.         HttpClient httpClient = new DefaultHttpClient();//apache http step1: 
  29.         HttpResponse httpResponse; 
  30.          
  31.          
  32.         try { 
  33.             // execute request 
  34.             try { 
  35.                 httpResponse = httpClient.execute(httpGet);//apache http step3: 
  36.             } catch (UnknownHostException e) { 
  37.                 throw new WSError("Unable to access " + e.getLocalizedMessage()); 
  38.             } catch (SocketException e){ 
  39.                 throw new WSError(e.getLocalizedMessage()); 
  40.             } 
  41.             //apche http step4: 
  42.              
  43.             // request data 
  44.             HttpEntity httpEntity = httpResponse.getEntity();//apache http step5: 
  45.              
  46.             if(httpEntity != null){ 
  47.                 InputStream inputStream = httpEntity.getContent(); 
  48.                 data = convertStreamToString(inputStream); 
  49.                 // cache the result 
  50.                 if(requestCache != null){ 
  51.                     requestCache.put(url, data); 
  52.                 } 
  53.             } 
  54.              
  55.         } catch (ClientProtocolException e) { 
  56.             e.printStackTrace(); 
  57.         } catch (IOException e) { 
  58.             e.printStackTrace(); 
  59.         } 
  60.          
  61.         Log.d(JamendoApplication.TAG, "Caller.doGet "+url); 
  62.         return data; 
  63.     } 

D, 其他

缓存也可以使用线程池来完成,以后我会整理相关内容。对于缓存的处理更加简单。

3, 主UI

效果如下图:

A, 在onResume()方法中加载主ListView和Gesture,代码如下:

  1. @Override 
  2.     protected void onResume() { 
  3.         fillHomeListView(); 
  4.         boolean gesturesEnabled = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("gestures"true); 
  5.         mGestureOverlayView.setEnabled(gesturesEnabled); 
  6.         super.onResume(); 
  7.     } 

   在onCreate()方法中,执行标题栏的异步加载(AsyncTask),代码如下:

  1. new NewsTask().execute((Void)null); 

 

B, TitleBar通过自定义的ViewFlipper实现,主ListView通过自定义SeparatedListAdapter实现。Jamendo将(标题+标准ListView<自定义 PurpleAdapter>)封装成一个方法,每次调用Add方法,加入一栏及其内容。

 

C, 开始我觉得不够清晰化。Adapter太多,其实只需要一个Adapter。但是:看了项目其他部分对PurpleAdapter的引用之后,觉得Jamendo是想实现代码的复用。另外,Jamendo将点击事件封装在PurpleEntry中也不错。清晰,设置ItemOnClick方法,实现PurpleListener方法。代码如下:

  1. /** 
  2.      * Fills ListView with clickable menu items 
  3.      */ 
  4.     private void fillHomeListView(){ 
  5.         mBrowseJamendoPurpleAdapter = new PurpleAdapter(this); 
  6.         mMyLibraryPurpleAdapter = new PurpleAdapter(this); 
  7.         ArrayList<PurpleEntry> browseListEntry = new ArrayList<PurpleEntry>(); 
  8.         ArrayList<PurpleEntry> libraryListEntry = new ArrayList<PurpleEntry>(); 
  9.          
  10.         // BROWSE JAMENDO 
  11.          
  12.         browseListEntry.add(new PurpleEntry(R.drawable.list_search, R.string.search, new PurpleListener(){ 
  13.             @Override 
  14.             public void performAction() { 
  15.                 SearchActivity.launch(HomeActivity.this); 
  16.             } 
  17.         })); 
  18.  
  19.         browseListEntry.add(new PurpleEntry(R.drawable.list_radio, R.string.radio, new PurpleListener(){ 
  20.             @Override 
  21.             public void performAction() { 
  22.                 RadioActivity.launch(HomeActivity.this); 
  23.             } 
  24.         })); 
  25.          
  26.         browseListEntry.add(new PurpleEntry(R.drawable.list_top, R.string.most_listened, new PurpleListener(){ 
  27.             @Override 
  28.             public void performAction() { 
  29.                 new Top100Task(HomeActivity.this, R.string.loading_top100, R.string.top100_fail).execute(); 
  30.             } 
  31.         })); 
  32.          
  33.         // MY LIBRARY 
  34.          
  35.         libraryListEntry.add(new PurpleEntry(R.drawable.list_playlist, R.string.playlists, new PurpleListener(){ 
  36.             @Override 
  37.             public void performAction() { 
  38.                 BrowsePlaylistActivity.launch(HomeActivity.this, Mode.Normal); 
  39.             } 
  40.         })); 
  41.          
  42.         // check if we have personalized client then add starred albums 
  43.         final String userName = PreferenceManager.getDefaultSharedPreferences(this).getString("user_name"null); 
  44.         if(userName != null && userName.length() > 0){ 
  45.             libraryListEntry.add(new PurpleEntry(R.drawable.list_cd, R.string.albums, new PurpleListener(){ 
  46.                 @Override 
  47.                 public void performAction() { 
  48.                     StarredAlbumsActivity.launch(HomeActivity.this, userName); 
  49.                 } 
  50.             })); 
  51.         } 
  52.          
  53.         /* following needs jamendo authorization (not documented yet on the wiki) 
  54.          * listEntry.add(new PurpleEntry(R.drawable.list_mail, "Inbox"));  
  55.          */ 
  56.  
  57.         // show this list item only if the SD Card is present 
  58.         if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){ 
  59.             libraryListEntry.add(new PurpleEntry(R.drawable.list_download, R.string.download, new PurpleListener(){ 
  60.                 @Override 
  61.                 public void performAction() { 
  62.                     DownloadActivity.launch(HomeActivity.this); 
  63.                 } 
  64.             })); 
  65.         } 
  66.          
  67. //      listEntry.add(new PurpleEntry(R.drawable.list_star, R.string.favorites, new PurpleListener(){ 
  68. // 
  69. //          @Override 
  70. //          public void performAction() { 
  71. //              Playlist playlist = new DatabaseImpl(HomeActivity.this).getFavorites(); 
  72. //              JamendroidApplication.getInstance().getPlayerEngine().openPlaylist(playlist); 
  73. //              PlaylistActivity.launch(HomeActivity.this, true); 
  74. //          } 
  75. // 
  76. //      })); 
  77.          
  78.         // attach list data to adapters 
  79.         mBrowseJamendoPurpleAdapter.setList(browseListEntry); 
  80.         mMyLibraryPurpleAdapter.setList(libraryListEntry); 
  81.          
  82.         // separate adapters on one list 
  83.         SeparatedListAdapter separatedAdapter = new SeparatedListAdapter(this); 
  84.         separatedAdapter.addSection(getString(R.string.browse_jamendo), mBrowseJamendoPurpleAdapter); 
  85.         separatedAdapter.addSection(getString(R.string.my_library), mMyLibraryPurpleAdapter); 
  86.          
  87.         mHomeListView.setAdapter(separatedAdapter); 
  88.         mHomeListView.setOnItemClickListener(mHomeItemClickListener); 
  89.     } 

备注:不考虑复用,单一Adapter也可完成分栏式Adapter,见本人博客:

http://mikewang.blog.51cto.com/3826268/737715

(五) 菜单的实现

可以指定条件显示或隐藏菜单的某一项,代码如下:

  1. @Override 
  2.     public boolean onPrepareOptionsMenu(Menu menu) { 
  3.         if(JamendoApplication.getInstance().getPlayerEngineInterface() == null || JamendoApplication.getInstance().getPlayerEngineInterface().getPlaylist() == null){ 
  4.             menu.findItem(R.id.player_menu_item).setVisible(false); 
  5.         } else { 
  6.             menu.findItem(R.id.player_menu_item).setVisible(true); 
  7.         } 
  8.         return super.onPrepareOptionsMenu(menu); 
  9.     } 

 


五:Adpter框架

Jamendo中大量使用了Adaper,Adaper的继承关系如下图所示:

 

为了解其设计的过程,将分别沿几条线路分析。

(一) HomeActivity中TitleBar(ImageAdapter)的加载过程,涉及相关Adapter如下图所示:

 

从服务端获取数据之后,将数据给Gallery,代码如下:

  1. public void onPostExecute(Album[] albums) { 
  2.  
  3.             if(albums != null && albums.length > 0){ 
  4.                 mViewFlipper.setDisplayedChild(1); 
  5.                 ImageAdapter albumsAdapter = new ImageAdapter(HomeActivity.this); 
  6.                 albumsAdapter.setList(albums); 
  7.                 mGallery.setAdapter(albumsAdapter); 
  8.                 mGallery.setOnItemClickListener(mGalleryListener); 
  9.                 mGallery.setSelection(albums.length/2true); // animate to center 
  10.  
  11.             } else { 
  12.                 mViewFlipper.setDisplayedChild(2); 
  13.                 mFailureBar.setOnRetryListener(new OnClickListener(){ 
  14.  
  15.                     @Override 
  16.                     public void onClick(View v) { 
  17.                         new NewsTask().execute((Void)null); 
  18.                     } 
  19.  
  20.                 }); 
  21.                 mFailureBar.setText(R.string.connection_fail); 
  22.             } 
  23.             super.onPostExecute(albums); 
  24.         } 

1, 单击setList(),进入ArrayListAdapter中,代码如下:

  1. public void setList(T[] list){ 
  2.         ArrayList<T> arrayList = new ArrayList<T>(list.length);   
  3.         for (T t : list) {   
  4.             arrayList.add(t);   
  5.         }   
  6.         setList(arrayList); 
  7.     } 

A,使用了泛型,扩大了此类的使用范围,其继承类复用很多代码。

B, setList()方法重载,先将解析封装好的T[]传进来,再将T[]中每一个元素存入ArrayList中。代码如下:

  1. public void setList(ArrayList<T> list){ 
  2.         this.mList = list; 
  3.         notifyDataSetChanged();//刷新ui 
  4.     } 

C, 定义了抽象方法getView()。继承类中只需要实现其即可。经常写adapter就回知道,继承BaseAdapter需要implements四个方法,其中最主要的code内容在getView()方法中完成。在ArrayListAdapter中写一个抽象方法,继承类只需要实现getView方法,并完成其内部的代码即可。如上面的adpter tree diagram所示,ArrayListAdapter有六个子类,简化了代码。

  1. @Override 
  2.     abstract public View getView(int position, View convertView, ViewGroup parent);//抽象方法 

D,设置数据之后,即notifyDataSetChanged()刷新。显然是在重新封装成arrayList之后。

  1. public void setList(ArrayList<T> list){ 
  2.         this.mList = list; 
  3.         notifyDataSetChanged();//刷新ui 
  4.     } 

E, 此setListView()方法,暂时还没发现有什么用。如果以后遇到,再补充。

2, ImageAdapteritem其实很简单。就一个图片。继承AlbumAdapter,直接重载getView()方法。在内部写实现代码。仅在于简化代码。两者关系并不密切。AlbumAdapter仅比ImageAdapter复杂一点,ImageAdapter中getView()代码如下:

  1. @Override 
  2.     public View getView(int position, View convertView, ViewGroup parent) { 
  3.         RemoteImageView i; 
  4.  
  5.         if (convertView == null) { 
  6.             i = new RemoteImageView(mContext); 
  7.             i.setScaleType(RemoteImageView.ScaleType.FIT_CENTER); 
  8.             i.setLayoutParams(new Gallery.LayoutParams(mIconSize, mIconSize)); 
  9.         } else { 
  10.             i = (RemoteImageView) convertView; 
  11.         } 
  12.  
  13.         i.setDefaultImage(R.drawable.no_cd); 
  14.         i.setImageUrl(mList.get(position).getImage()); 
  15.  
  16.         return i; 
  17.     } 

3, 显然2中,Jamendo自定义了一个RemoteImageView,用来实现图片的下载和显示。

  1. public void setImageUrl(String url){ 
  2.          
  3. //      Log.d("img_url", "img_url is :" + url); 
  4.          
  5.         if (mUrl != null && mUrl.equals(url) && (mCurrentlyGrabbedUrl == null ||//1:url赋给全局变量mUrl,两者相等且都为空,但是未执行,所以mCurrentlyGrabbedUrl为空 
  6.                 (mCurrentlyGrabbedUrl != null && !mCurrentlyGrabbedUrl.equals(url)))) {//2:第n(n>1)次执行时,并未完成downloadTask 
  7.             mFailure++;          
  8.             if(mFailure > MAX_FAILURES){//超过指定重连次数 
  9.                 Log.e(JamendoApplication.TAG, "Failed to download "+url+", falling back to default image"); 
  10.                 loadDefaultImage(); 
  11.                 return
  12.             } 
  13.         } else { 
  14.             mUrl = url; 
  15.             mFailure = 0
  16.         } 
  17.  
  18.         updateCacheSize(); 
  19.          
  20.         if (mCacheSize>0 && (url.contains(ALBUMS) || url.contains(RADIOS))) {//只有两类路径图片需要缓存          
  21.             String fileName = convertUrlToFileName(url); 
  22.             String dir = getDirectory(fileName); 
  23.             Log.d("img_url""dir is :" + dir); 
  24.             String pathFileName = dir + "/" + fileName; 
  25.             Log.d("img_url""pathFileName is :" + pathFileName); 
  26.             Bitmap tbmp = BitmapFactory.decodeFile(pathFileName);//从指定文件保存路径解码处图片 
  27.             if (tbmp == null) { 
  28.                 Log.d(JamendoApplication.TAG, "Image is not present, try to download"); 
  29.                 try
  30.                     new DownloadTask().execute(url); 
  31.                 } catch (RejectedExecutionException e) { 
  32.                     // do nothing, just don't crash 
  33.                 } 
  34.             } else { 
  35.                 Log.i(JamendoApplication.TAG, "Loading album cover from file"); 
  36.                 this.setImageBitmap(tbmp); 
  37.                 updateFileTime(dir,fileName );               
  38.             } 
  39.              
  40.             removeAlbumCoversCache(dir, fileName);//对专辑图片的缓存处理:比较大120k 
  41.             removeRadioCoversCache(dir, fileName);//对广播图片的缓存处理:比较小8k 
  42.              
  43.         } 
  44.         else { 
  45.             Log.i(JamendoApplication.TAG, "File not cached supported" + url); 
  46.             ImageCache imageCache = JamendoApplication.getInstance() 
  47.                     .getImageCache(); 
  48.             if (imageCache.isCached(url)) {              
  49.                 this.setImageBitmap(imageCache.get(url)); 
  50.             } else { 
  51.                 try { 
  52.                     Log.i(JamendoApplication.TAG, "Image is not present, try to download"); 
  53.                     new DownloadTask().execute(url); 
  54.                 } catch (RejectedExecutionException e) { 
  55.                     // do nothing, just don't crash 
  56.                 } 
  57.             } 
  58.         } 
  59.     } 

依照顺序,分析如下:

1st. 重连的判断

指定重连次数,不断请求,大于重连次数,设置为默认图片

2nd. 更新设置中的缓存大小

updateCacheSize();

3rd. 设置图片(先从缓存中找,没有再找SDCard,最后再下载)

缓存的问题将单独写一篇文章

至此:TitleBar的相关Adapter解析完毕。给人印象最深的仍然是面向对象的思想。A: 继承BaseAdapter所实现的4个方法中,3个都基本一样,所以构建父类 ArrayListAdapter。子类.

B: 封装RemoteImageView,可移植性增强。

Adapter的分析先到这里,后面如果发现比较特殊的adpter,会继续补充。


六:缓存

从Jamendo加载到进入主页,Jamendo中所涉及的两种缓存都已经涉及到。

(一) RequestCache(服务器请求缓存)

从服务器上下载数据非常耗时,并且耗电。所以避免重复下载很有必要。Jamendo的RequestCache的原则是:保留最近10次(这个值可以自己设定)的网络请求。如果超过,清除最早的缓存内容。在调用Call获取服务器数据时,首先在RequestCache中查找,是否存在,如果不存在,向服务器请求,并将请求到的数据加入缓存中。很好理解的流程。RequestCache的代码如下:

  1. /* 
  2.  * Copyright (C) 2009 Teleca Poland Sp. z o.o. <android@teleca.com> 
  3.  * 
  4.  * Licensed under the Apache License, Version 2.0 (the "License"); 
  5.  * you may not use this file except in compliance with the License. 
  6.  * You may obtain a copy of the License at 
  7.  * 
  8.  *      http://www.apache.org/licenses/LICENSE-2.0 
  9.  * 
  10.  * Unless required by applicable law or agreed to in writing, software 
  11.  * distributed under the License is distributed on an "AS IS" BASIS, 
  12.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
  13.  * See the License for the specific language governing permissions and 
  14.  * limitations under the License. 
  15.  */ 
  16.  
  17. package com.teleca.jamendo.api.util; 
  18.  
  19. import java.util.Hashtable; 
  20. import java.util.LinkedList; 
  21.  
  22. /** 
  23.  * @author Lukasz Wisniewski 
  24.  */ 
  25. public class RequestCache { 
  26.      
  27.     // TODO cache lifeTime 
  28.      
  29.     private static int CACHE_LIMIT = 10;//保存最近十次的请求数据,key为url 
  30.      
  31.     @SuppressWarnings("unchecked"
  32.     private LinkedList history; 
  33.     private Hashtable<String, String> cache; 
  34.      
  35.     @SuppressWarnings("unchecked"
  36.     public RequestCache(){ 
  37.         history = new LinkedList(); 
  38.         cache = new Hashtable<String, String>(); 
  39.     } 
  40.      
  41.      
  42.     @SuppressWarnings("unchecked"
  43.     public void put(String url, String data){ 
  44.         history.add(url); 
  45.         // too much in the cache, we need to clear something 
  46.         if(history.size() > CACHE_LIMIT){ 
  47.             String old_url = (String) history.poll(); 
  48.             cache.remove(old_url); 
  49.         } 
  50.         cache.put(url, data); 
  51.     } 
  52.      
  53.     public String get(String url){ 
  54.         return cache.get(url); 
  55.     } 

Note:RequestCache使用的存储集合是HashTable,它不允许Null的key和value,并且是同步安全的。因为存储的数量较少,且是最耗时的操作,存储空值无意义,所以选用HashTable。

HashTable相关内容,参看:http://mikewang.blog.51cto.com/3826268/856865

(二) ImageCache缓存(图片缓存)

图片缓存的流程图如下:

 

代码如下:

  1. public class ImageCache extends WeakHashMap<String, Bitmap> { 
  2.  
  3.     private static final long serialVersionUID = 1L; 
  4.      
  5.     public boolean isCached(String url){ 
  6.         return containsKey(url) && get(url) != null
  7.     } 

显然,ImageCache继承了WeakHashmap。

WeakHashmap非常重要。我专门做了一个整理,相关内容我的博文:http://mikewang.blog.51cto.com/3826268/880775

当然,我们仍然对ImageCache缓存的取舍不是很清楚。回到RemoteImageView的setImageUrl方法。代码如下:

  1. public void setImageUrl(String url){ 
  2.          
  3. //      Log.d("img_url", "img_url is :" + url); 
  4.          
  5.         if (mUrl != null && mUrl.equals(url) && (mCurrentlyGrabbedUrl == null ||//1:url赋给全局变量mUrl,两者相等且都为空,但是未执行,所以mCurrentlyGrabbedUrl为空 
  6.                 (mCurrentlyGrabbedUrl != null && !mCurrentlyGrabbedUrl.equals(url)))) {//2:第n(n>1)次执行时,并未完成downloadTask 
  7.             mFailure++;          
  8.             if(mFailure > MAX_FAILURES){//超过指定重连次数 
  9.                 Log.e(JamendoApplication.TAG, "Failed to download "+url+", falling back to default image"); 
  10.                 loadDefaultImage(); 
  11.                 return
  12.             } 
  13.         } else { 
  14.             mUrl = url; 
  15.             mFailure = 0
  16.         } 
  17.  
  18.         updateCacheSize(); 
  19.          
  20.         if (mCacheSize>0 && (url.contains(ALBUMS) || url.contains(RADIOS))) {//只有两类路径图片需要缓存          
  21.             String fileName = convertUrlToFileName(url); 
  22.             String dir = getDirectory(fileName); 
  23.             Log.d("img_url""dir is :" + dir); 
  24.             String pathFileName = dir + "/" + fileName; 
  25.             Log.d("img_url""pathFileName is :" + pathFileName); 
  26.             Bitmap tbmp = BitmapFactory.decodeFile(pathFileName);//从指定文件保存路径解码处图片 
  27.             if (tbmp == null) { 
  28.                 Log.d(JamendoApplication.TAG, "Image is not present, try to download"); 
  29.                 try
  30.                     new DownloadTask().execute(url); 
  31.                 } catch (RejectedExecutionException e) { 
  32.                     // do nothing, just don't crash 
  33.                 } 
  34.             } else { 
  35.                 Log.i(JamendoApplication.TAG, "Loading album cover from file"); 
  36.                 this.setImageBitmap(tbmp); 
  37.                 updateFileTime(dir,fileName );               
  38.             } 
  39.              
  40.             removeAlbumCoversCache(dir, fileName);//对专辑图片的缓存处理:比较大120k 
  41.             removeRadioCoversCache(dir, fileName);//对广播图片的缓存处理:比较小8k 
  42.              
  43.         } 
  44.         else { 
  45.             Log.i(JamendoApplication.TAG, "File not cached supported" + url); 
  46.             ImageCache imageCache = JamendoApplication.getInstance() 
  47.                     .getImageCache(); 
  48.             if (imageCache.isCached(url)) {              
  49.                 this.setImageBitmap(imageCache.get(url)); 
  50.             } else { 
  51.                 try { 
  52.                     Log.i(JamendoApplication.TAG, "Image is not present, try to download"); 
  53.                     new DownloadTask().execute(url); 
  54.                 } catch (RejectedExecutionException e) { 
  55.                     // do nothing, just don't crash 
  56.                 } 
  57.             } 
  58.         } 
  59.     } 

1. 对两类图片(AlbumCover和RadioCover)进行处理,打了log之后,AlbumCover的大小在120k左右,主要用在播放页面做背景。RadioCover的大小在8k左右,主要用来显示图标。对两者的处理也不同。大小是15倍左右的关系。代码如下:

  1. removeAlbumCoversCache(dir, fileName);//对专辑图片的缓存处理:比较大120k 
  2.             removeRadioCoversCache(dir, fileName);//对广播图片的缓存处理:比较小8k 

 

对AlbumCover,因为它的文件大小比较大,所以统计是否超出总的缓存容量时,以它的总大小计算就ok。代码如下:

  1. private void removeAlbumCoversCache(String dirPath, String filename) { 
  2.  
  3.         if (!filename.contains(ALBUM_COVER_MARKER)) { 
  4.             return
  5.         } 
  6.  
  7.         File dir = new File(dirPath); 
  8.         File[] files = dir.listFiles(); 
  9.  
  10.         if (files == null) { 
  11.             // possible sd card is not present/cant write 
  12.             return
  13.         } 
  14.  
  15.         int dirSize = 0
  16.  
  17.         for (int i = 0; i < files.length; i++) { 
  18.             if (files[i].getName().contains(ALBUM_COVER_MARKER)) {//计算出指定路径指定类型文件大小:只计算出大图片的大小,小的不用计算,简化 
  19.                 dirSize += files[i].length(); 
  20.             } 
  21.         } 
  22.          
  23.          
  24.         if (dirSize > mCacheSize * MB || FREE_SD_SPACE_NEEDED_TO_CACHE > freeSpaceOnSd()) { 
  25.             int removeFactor = (int) ((0.4 * files.length) + 1);//移出缓存文件夹的四分之一文件 
  26.             Arrays.sort(files, new FileLastModifSort());//按照从老到新的顺序排列 
  27.             Log.i(JamendoApplication.TAG, "Clear some album covers cache files "); 
  28.             for (int i = 0; i < removeFactor; i++) {//移出选定数量的缓存文件(其中只删除占空间比较大的专辑图片,抓大舍小) 
  29.                 if (files[i].getName().contains(ALBUM_COVER_MARKER)) { 
  30.                     files[i].delete();               
  31.                 } 
  32.             } 
  33.         } 
  34.  
  35.     } 

对RadioCover,因为其所占空间很小,所以按照时间来清理。超过指定日期的文件就清理掉。清除缓存(下载)超过指定天数的文件,只保存最近45天内的文件。代码如下:

  1. private void removeRadioCoversCache(String dirPath, String filename) { 
  2.  
  3.         if (filename.contains(ALBUM_COVER_MARKER)) {//筛选出非专辑图片类图片 
  4.             return
  5.         } 
  6.  
  7.         File file = new File(dirPath, filename); 
  8.         if (file.lastModified() != 0 
  9.                 && System.currentTimeMillis() - file.lastModified() > mTimeDiff) {//清除缓存(下载)超过指定天数的文件,即所有的Radio类图片,只保存最近45天内的文件 
  10.                          
  11.             Log.i(JamendoApplication.TAG, "Clear some album or radio thumbnail cache files "); 
  12.             file.delete(); 
  13.         } 
  14.  
  15.     } 

2. 思考

这让做的好处是,抓住了真正耗时的关键,有的放矢。


七:搜索页面实现及数据加载

Mike按:

前一段时间,在工作学习方面有点迷茫。请教了两个高人。分别给出了两个互补的建议,受益匪浅,感谢!一是学习开源项目,二是在应用的实际开发中学习东西(按照自己的思路写一个app)。最近主要是优化重构之前的代码,将开源项目中比较好的部分应用到自己的项目中。本文就是基于此。

 

(一) 应用场景:刷新数据,显示加载进度条,数据准备,数据准备完毕,进度条消失,显示结果。

可能出现的状况:

1, 无网络,无法获取数据,有网络,显示提示

2, 一切正常,显示数据

3, 搜索无结果,各种错误:协议错误,超时

之前的思路,先显示Dialog,新启线程加载数据,数据加载完毕后,Handler发送消息,UI中刷新,Dialog消失。结果的显示使用帧布局。功能实现上ok,但是封装上不好,感觉代码分散,混乱。

 

(二) Jamendo的处理

将这个处理过程封装成LoadingDialog(继承AsyncTask),结果的显示上使用ViewFlipper。思路非常清晰。

a) Jamendo将加载的处理均使用LoadingDialog的实现子类。相关继承树(extend tree)如下图所示:

b) LoadingDialog继承AsyncTask,这样的好处是容易控制流程的先后顺序,而且UI线程和其他线程的切换非常的平滑。其类的结构如下图所示:

 

c) 定义

泛型的使用,代码如下:

  1. public abstract class LoadingDialog<Input, Result> extends AsyncTask<Input, WSError, Result> 

d) 流程

 绘制其流程图如下:

 

 

i. onPreExecute()

  1. @Override 
  2.     public void onPreExecute() { 
  3.         String title = ""
  4.         String message = mActivity.getString(mLoadingMsg); 
  5.         mProgressDialog = ProgressDialog.show(mActivity, title, message, truetruenew OnCancelListener(){ 
  6.  
  7.             @Override 
  8.             public void onCancel(DialogInterface dialogInterface) { 
  9.                 LoadingDialog.this.cancel(true); 
  10.             } 
  11.  
  12.         }); 
  13.         super.onPreExecute(); 
  14.     } 

显示Dialog,这个创建方法第一次见到,Dialog可被cancel掉,代码如下:

  1. @Override 
  2.     public void onCancelled() {      
  3.          
  4.         if( mActivity instanceof PlayerActivity) 
  5.             { 
  6.             PlayerActivity pa = (PlayerActivity)mActivity; 
  7.             pa.doCloseActivity(); 
  8.             } 
  9.          
  10.         failMsg(); 
  11.         super.onCancelled(); 
  12.     } 

ii. doInBackground()-------抽象方法

这里是处理数据的地方,Jamendo在这里定义了抽象方法doInBackGround(),以应对不同的需求,子类各自实现即可。代码如下:

  1. @Override 
  2.     public abstract Result doInBackground(Input... params); 

iii. onPostExecute()

代码如下:

  1. @Override 
  2.     public void onPostExecute(Result result) { 
  3.         super.onPostExecute(result); 
  4.  
  5.         mProgressDialog.dismiss(); 
  6.  
  7.         if(result != null){ 
  8.             doStuffWithResult(result); 
  9.         } else { 
  10.              
  11.             if( mActivity instanceof PlayerActivity) 
  12.                 { 
  13.                 PlayerActivity pa = (PlayerActivity)mActivity; 
  14.                 pa.doCloseActivity(); 
  15.                 } 
  16.             failMsg(); 
  17.  
  18.         } 
  19.     } 

数据获取结束之后,Dialog dismiss掉。数据获取有两种情况:

l 获取数据正常

调用doStuffWithResult(Result result)方法处理数据,此方法为抽象方法,需要子类实现,按照自己的需求处理,代码如下:

  1. /** 
  2.      * Very abstract function hopefully very meaningful name, 
  3.      * executed when result is other than null 
  4.      *  
  5.      * @param result 
  6.      * @return 
  7.      */ 
  8.     public abstract void doStuffWithResult(Result result); 

l 获取数据异常

调用failMsg()方法。即Toast,代码如下:

  1. protected void failMsg(){ 
  2.         Toast.makeText(mActivity, mFailMsg, 2000).show(); 
  3.     } 

iv. onProgressUpdate()

显然,doInBackGround()方法中,可以实时的将一些信息(错误信息即WSError的Message)publish到本方法 。一旦出现问题:首先,toast,其次,取消本次异步任务,最后,Dialog dismiss掉。代码如下:

  1. @Override 
  2.     protected void onProgressUpdate(WSError... values) { 
  3.         Toast.makeText(mActivity, values[0].getMessage(), Toast.LENGTH_LONG).show(); 
  4.         this.cancel(true); 
  5.         mProgressDialog.dismiss(); 
  6.         super.onProgressUpdate(values); 
  7.     } 

2, 实现类SearchDialog(以此为例,说明其实现,只说明最重要的)

a) SearchActvity内容部分的布局,效果图,参看上面彩图。

布局虽是自定义ViewFlipper,我在实际使用中使用原生ViewFlipper也ok,Xml文件如下:

  1. <com.teleca.jamendo.util.FixedViewFlipper 
  2.             android:id="@+id/SearchViewFlipper" 
  3.             android:layout_width="fill_parent" 
  4.             android:layout_height="fill_parent" 
  5.             android:layout_weight="1" 
  6.             android:background="#fff" > 
  7.  
  8.             <ListView 
  9.                 android:id="@+id/SearchListView" 
  10.                 android:layout_width="fill_parent" 
  11.                 android:layout_height="fill_parent" 
  12.                 android:divider="#000" /> 
  13.  
  14.             <TextView 
  15.                 android:layout_width="wrap_content" 
  16.                 android:layout_height="wrap_content" 
  17.                 android:layout_gravity="center" 
  18.                 android:text="@string/no_results" > 
  19.             </TextView> 
  20.  
  21.             <TextView 
  22.                 android:layout_width="wrap_content" 
  23.                 android:layout_height="wrap_content" 
  24.                 android:layout_gravity="center" 
  25.                 android:text="@string/search_list_hint" > 
  26.             </TextView> 
  27.         </com.teleca.jamendo.util.FixedViewFlipper> 

b) 多态的使用

我是在这个类中深深体会多态的妙处。搜索有很多的分类,每一类可能对应不同的Adapter,那么对结果的处理就成了一个问题。在SearchDialog中定义BaseAdapter。对于所有的搜索结果的处理就smooth了。代码如下:

  1. AlbumAdapter albumAdapter = new AlbumAdapter(SearchActivity.this);  
  2.                 albumAdapter.setList(albums); 
  3.                 albumAdapter.setListView(mSearchListView); 
  4.                 mAdapter = albumAdapter; 

c) 错误信息的捕获

Jamendo自己定义了Throwable类WSError,捕获Exception之后(就是主要的网络连接错误:请求异常,连接超时,下载超时等),将错误信息封装之后,向上抛出。分两步处理方式(以AlbumSearch为例):

第一步:catch

第二步:publishProgress

代码如下:

  1. catch (JSONException e) { 
  2.                 e.printStackTrace(); 
  3.             } catch (WSError e) { 
  4.                 publishProgress(e); 
  5.                 this.cancel(true); 
  6.             } 

d) 对结果的处理

多态的使用,使得ListView的数据填充很easy。代码很清晰,不用作解释,代码如下:

  1. @Override 
  2.         public void doStuffWithResult(Integer result) { 
  3.             mSearchListView.setAdapter(mAdapter); 
  4.  
  5.             if(mSearchListView.getCount() > 0){ 
  6.                 mViewFlipper.setDisplayedChild(0); // display results 
  7.             } else { 
  8.                 mViewFlipper.setDisplayedChild(1); // display no results message 
  9.             } 
  10.  
  11.             // results are albums 
  12.             if(mSearchMode.equals(0) || mSearchMode.equals(1) ||  mSearchMode.equals(3)){ 
  13.                 mSearchListView.setOnItemClickListener(mAlbumClickListener); 
  14.             } 
  15.  
  16.             // results are playlists 
  17.             if(mSearchMode.equals(2)){ 
  18.                 mSearchListView.setOnItemClickListener(mPlaylistClickListener); 
  19.             } 
  20.         } 

3, 搜索状态的保存

Jamendo对搜索状态的保存,是基于其的搜索页面是Activity之间的跳转。所以有必要保存。不做解释了,代码如下:

  1. @SuppressWarnings("unchecked"
  2.     @Override 
  3.     protected void onRestoreInstanceState(Bundle savedInstanceState) { 
  4.         mSearchMode = (SearchMode) savedInstanceState.getSerializable("mode"); 
  5.         if(mSearchMode != null){ 
  6.             if(mSearchMode.equals(SearchMode.Artist)  
  7.                     || mSearchMode.equals(SearchMode.Tag) 
  8.                     || mSearchMode.equals(SearchMode.UserStarredAlbums)){ 
  9.                 AlbumAdapter adapter = new AlbumAdapter(this); 
  10.                 adapter.setList((ArrayList<Album>) savedInstanceState.get("values")); 
  11.                 mSearchListView.setAdapter(adapter); 
  12.                 mSearchListView.setOnItemClickListener(mAlbumClickListener); 
  13.             } 
  14.              
  15.             if(mSearchMode.equals(SearchMode.UserPlaylist)) { 
  16.                 PlaylistRemoteAdapter adapter = new PlaylistRemoteAdapter(this);  
  17.                 adapter.setList((ArrayList<PlaylistRemote>) savedInstanceState.get("values")); 
  18.                 mSearchListView.setAdapter(adapter); 
  19.                 mSearchListView.setOnItemClickListener(mPlaylistClickListener); 
  20.             } 
  21.              
  22.             mViewFlipper.setDisplayedChild(savedInstanceState.getInt("flipper_page")); 
  23.         } 
  24.         super.onRestoreInstanceState(savedInstanceState); 
  25.     } 
  26.  
  27.     @Override 
  28.     protected void onSaveInstanceState(Bundle outState) {//保存上次的搜索结果,返回时还在 
  29.         if(mSearchMode != null){ 
  30.             outState.putSerializable("mode", mSearchMode); 
  31.             if(mSearchMode.equals(SearchMode.Artist)  
  32.                     || mSearchMode.equals(SearchMode.Tag)  
  33.                     || mSearchMode.equals(SearchMode.UserStarredAlbums)){ 
  34.                 AlbumAdapter adapter = (AlbumAdapter)mSearchListView.getAdapter(); 
  35.                 outState.putSerializable("values", adapter.getList()); 
  36.             } 
  37.  
  38.             if(mSearchMode.equals(SearchMode.UserPlaylist)) { 
  39.                 PlaylistRemoteAdapter adapter = (PlaylistRemoteAdapter)mSearchListView.getAdapter(); 
  40.                 outState.putSerializable("values", adapter.getList()); 
  41.             } 
  42.              
  43.             outState.putInt("flipper_page", mViewFlipper.getDisplayedChild()); 
  44.         } 
  45.         super.onSaveInstanceState(outState); 
  46.     } 

(三) 思考

前面的博文分析提到,Jamendo中有一个RequestCache,缓存10次请求。觉得意义不大,因为点开任意一个页面,滚动一下,很快就超过10个url请求。在Search这里,发现RequestCache的好处。搜索在短时间内,结果基本不会发生差别,所以阻止重复搜索很有必要。在实时性要求特别高的weibo里,RequestCache的使用也不用影响其刷新到最新数据,因为10次很容易超过,之前保留的刷新最新数据的老数据将被remove掉。

 

注:重构代码时,发现一个奇怪的问题,当前Activity有EditText(即有组件被focus到时),其继承的BaseActivity的无法显示showDialog()所对应的Dialog不会显示,但new出的Dialog可以显示。查看文档后,showDialog()方法不建议使用。文档如下:


八:播放流程

这一次主要是研究Jamendo的播放流程,相对比较繁复一些。播放显然要启动Service来实现,在这之前,Jamendo是怎么处理的。本文将简单展开。

(一) 基本流程图

 

(二) 实现

1. 启动

在Jamendo中,开发者习惯用被启动Activity的静态方法来完成这个功能。Gallery的Item点击事件之后,代码如下:

  1. Album album = (Album) adapterView.getItemAtPosition(position); 
  2.             PlayerActivity.launch(HomeActivity.this, album); 
  3.              

2. 数据加载封装

这一部分的处理思路在笔记之七中已经详细的说明。只在此列出代码,如下:

  1. public class PlayerAlbumLoadingDialog extends LoadingDialog<Album, Track[]>{ 
  2.      
  3.     private Album mAlbum; 
  4.  
  5.     public PlayerAlbumLoadingDialog(Activity activity, int loadingMsg, int failMsg) { 
  6.         super(activity, loadingMsg, failMsg); 
  7.     } 
  8.  
  9.     @Override 
  10.     public Track[] doInBackground(Album... params) { 
  11.         mAlbum = params[0]; 
  12.          
  13.         JamendoGet2Api service = new JamendoGet2ApiImpl(); 
  14.         Track[] tracks = null
  15.          
  16.         try { 
  17.             tracks = service.getAlbumTracks(mAlbum, JamendoApplication.getInstance().getStreamEncoding()); 
  18.             Log.d("play_workflow""doInBackground tracks is:" + tracks.toString()); 
  19.         } catch (JSONException e) { 
  20.             e.printStackTrace(); 
  21.             return null
  22.         } catch (WSError e) { 
  23.             publishProgress(e); 
  24.             cancel(true); 
  25.         } 
  26.         return tracks; 
  27.          
  28.     } 
  29.  
  30.  
  31.     @Override 
  32.     public void doStuffWithResult(Track[] tracks) { 
  33.          
  34.         Intent intent = new Intent(mActivity, PlayerActivity.class); 
  35.         Playlist playlist = new Playlist(); 
  36.         mAlbum.setTracks(tracks); 
  37.         playlist.addTracks(mAlbum); 
  38.          
  39.         intent.putExtra("playlist", playlist); 
  40.         Log.d("play_workflow""doStuffWithResult playlist is:" + playlist.toString()); 
  41.         mActivity.startActivity(intent); 
  42.     } 
  43.  

封装的示意图如下:

 

3. 播放列表加载及准备

数据封装完毕后,开始进行各种播放器的准备。我猜因为是开源软件的原因,部分代码写的不是很完善。比如if的某个条件,打了log之后,始终并未执行。我在代码中已经做了注释。handleIntent()方法里边处理已经封装好的PlayList类。代码如下:

  1. private void handleIntent(){ 
  2.         Log.i(JamendoApplication.TAG, "PlayerActivity.handleIntent"); 
  3.  
  4.         // This will be result of this intent handling 
  5.         Playlist playlist = null
  6.  
  7.         Log.d("play_workflow""data is:" + getIntent().getData());//本身并未给data赋值,所以执行的都是data==null的情形 
  8.         // We need to handle Uri 
  9.         if(getIntent().getData() != null){//不执行此条判断 
  10.              
  11.             // Check if this intent was already once parsed  
  12.             // we don't need to do that again 
  13.             if(!getIntent().getBooleanExtra("handled"false)){ 
  14.                 Log.d("play_workflow""handled is true" ); 
  15.                 mUriLoadingDialog = (LoadingDialog) new UriLoadingDialog(this, R.string.loading, R.string.loading_fail).execute(); 
  16.             } 
  17.                  
  18.         } else { 
  19.             Log.d("play_workflow""handled is false" ); 
  20.             playlist = (Playlist) getIntent().getSerializableExtra("playlist"); 
  21.             Log.d("play_workflow""handle_intent playlist is:" + playlist); 
  22.             loadPlaylist(playlist); 
  23.         } 
  24.     } 

其中loadPlaylist()方法的代码如下:

  1. private void loadPlaylist(Playlist playlist){ 
  2.         Log.i(JamendoApplication.TAG, "PlayerActivity.loadPlaylist"); 
  3.         if(playlist == null
  4.             return
  5.          
  6.         mPlaylist = playlist; 
  7.         if(mPlaylist != getPlayerEngine().getPlaylist()){//播放列表不相等,就要重新加载 
  8.             //getPlayerEngine().stop(); 
  9.             getPlayerEngine().openPlaylist(mPlaylist); 
  10.             getPlayerEngine().play(); 
  11.         } 
  12.     } 

其实,jamendo的处理是使用了两步来实现的,示意图如下:

按照先后顺序,以openPlayList()为例,代码如下:

Step1:在handleIntent中执行

       代码如上所示

Step2:在IntentPlayEngine中

  1. @Override 
  2.         public void openPlaylist(Playlist playlist) { 
  3.             mPlaylist = playlist; 
  4.             if(mServicePlayerEngine != null){ 
  5.                 mServicePlayerEngine.openPlaylist(playlist); 
  6.             } 
  7.         } 

Step3:在PlayEngineImpl中

  1. @Override 
  2.     public void openPlaylist(Playlist playlist) { 
  3.         if(!playlist.isEmpty()) 
  4.             mPlaylist = playlist; 
  5.         else 
  6.             mPlaylist = null
  7.     } 

 

 

4. 启动服务播放

从3中, play方法最终调用到的也是PlayEngine中的。

这样,整个流程就ok了。但是整个流程下来,给人的感觉是,冗繁。

(三) 播放的控制

对于播放控制的处理是(以play键为例):

1, 监听按键

代码如下:

  1. /** 
  2.      * on click play/pause and open playlist if necessary 
  3.      */ 
  4.     private OnClickListener mPlayOnClickListener = new OnClickListener(){ 
  5.  
  6.         @Override 
  7.         public void onClick(View v) { 
  8.             if(getPlayerEngine().isPlaying()){ 
  9.                 getPlayerEngine().pause(); 
  10.             } else { 
  11.                 getPlayerEngine().play(); 
  12.             } 
  13.         } 
  14.  
  15.     }; 

2, 调用接口,启动intent

代码如下:

  1. @Override 
  2.         public void play() { 
  3.             if (mServicePlayerEngine != null) { 
  4.                 Log.d("play_workflow""mServicePlayerEngine is not null:" ); 
  5.                 playlistCheck(); 
  6.                 mServicePlayerEngine.play(); 
  7.             } else { 
  8.                 Log.d("play_workflow""mServicePlayerEngine is null"); 
  9.                 startAction(PlayerService.ACTION_PLAY); 
  10.             } 
  11.         } 

3, 执行Service指定操作

代码如下:

  1. if(action.equals(ACTION_PLAY)){  
  2.             mPlayerEngine.play(); 
  3.             return
  4.         } 

最终执行的代码如下:

  1. @Override 
  2.     public void play() { 
  3.          
  4.         if( mPlayerEngineListener.onTrackStart() == false ){ 
  5.             return// apparently sth prevents us from playing tracks 
  6.         } 
  7.  
  8.         // check if there is anything to play 
  9.         if(mPlaylist != null){ 
  10.  
  11.             // check if media player is initialized 
  12.             if(mCurrentMediaPlayer == null){ 
  13.                 mCurrentMediaPlayer = build(mPlaylist.getSelectedTrack()); 
  14.             } 
  15.  
  16.             // check if current media player is set to our song 
  17.             if(mCurrentMediaPlayer != null && mCurrentMediaPlayer.playlistEntry != mPlaylist.getSelectedTrack()){ 
  18.                 cleanUp(); // this will do the cleanup job               
  19.                 mCurrentMediaPlayer = build(mPlaylist.getSelectedTrack()); 
  20.             } 
  21.              
  22.             // check if there is any player instance, if not, abort further execution  
  23.             if(mCurrentMediaPlayer == null
  24.                 return
  25.  
  26.             // check if current media player is not still buffering 
  27.             if(!mCurrentMediaPlayer.preparing){ 
  28.  
  29.                 // prevent double-press 
  30.                 if(!mCurrentMediaPlayer.isPlaying()){ 
  31.                     // i guess this mean we can play the song 
  32.                     Log.i(JamendoApplication.TAG, "Player [playing] "+mCurrentMediaPlayer.playlistEntry.getTrack().getName()); 
  33.                      
  34.                     // starting timer 
  35.                     mHandler.removeCallbacks(mUpdateTimeTask); 
  36.                     mHandler.postDelayed(mUpdateTimeTask, 1000); 
  37.                      
  38.                     mCurrentMediaPlayer.start(); 
  39.                 } 
  40.             } else { 
  41.                 // tell the mediaplayer to play the song as soon as it ends preparing 
  42.                 mCurrentMediaPlayer.playAfterPrepare = true
  43.             } 
  44.         } 
  45.     } 

 

(四) 思考

在播放准备及播放的流程上,有些冗余,代码不够清晰。我之前做过一个音乐播放器,采用的是bind service,回调让控制比较方便。Jamendo对于IntentPlayerEngine和PlayEngineImpl做出了相应的解释。未绑定的原因是适应代码的原始版本,减少重构量。

l IntentPlayerEngine

  1. /** 
  2.      * Since 0.9.8.7 we embrace "bindless" PlayerService thus this adapter. No 
  3.      * big need of code refactoring, we just wrap sending intents around defined 
  4.      * interface   
  5.      *  
  6.      * 意思是:因为jamendo使用的是无绑定的PlayService,所以有着这个适配器。不需要对代码 
  7.      * 进行太多的重构。我们只需要通过这个指定的接口发送Intent就ok了。 
  8.      * @author Lukasz Wisniewski 
  9.      */ 
  10.     private class IntentPlayerEngine implements PlayerEngine  

l PlayEngineImpl

  1. /** 
  2.  * Player core engine allowing playback, in other words, a 
  3.  * wrapper around Android's <code>MediaPlayer</code>, supporting 
  4.  * <code>Playlist</code> classes 
  5.  *  
  6.  * @author Lukasz Wisniewski 
  7.  */ 
  8. public class PlayerEngineImpl implements PlayerEngine  

 



0 0
原创粉丝点击