2014年5月26日 星期一

Android 開發 (四十八) performance optimization (ListView)

最近在寫project時遇到listview滑動非常不順暢的情形,通常只有在view包了太多層時才會出現這樣的情形,然而我已經將整個layout 攤平了....即使如此在滑動的過程依然是非常的頓。

在我蒐集了相關的資料後,大致上有了初步debug的方向,


讓 adapter在 getview時少做點事情

根據android developer文件描述

Your code might call findViewById() frequently during the scrolling of ListView, which can slow down performance. Even when the Adapter returns an inflated view for recycling, you still need to look up the elements and update them. A way around repeated use of findViewById() is to use the "view holder" design pattern.

意思就是使用viewholder pattern來減少 findviewById的動作,

範例如下

  @Override
  public View getView(int position, View convertView, ViewGroup parent) {
   ViewHolder holder ;
   if(convertView==null){
    LayoutInflater inflater =  LayoutInflater.from(getContext());
    convertView =  inflater.inflate(R.layout.listviewLayout, null,false);
    holder = new ViewHolder();
    holder.title = (TextView)convertView.findViewById(R.id.title);
    convertView.setTag(holder);    
   }else{
    holder = (ViewHolder)convertView.getTag();
   }
   holder.title.setText("ha");
   
   return convertView;
  }

  static class ViewHolder{
   TextView title;
  }



除了使用viewholder pattern之外,當然也要極力避免在getview時做init的動作,
所有的事情最好在第一次  if(convertView == null) 時就將事情做掉,
例如background的設置,文字顏色設置,view的size,時間的計算...etc,總之能不在getview裡做的就別在裡面做


避免GC頻繁的發生

當我們在getView create Object 並且將他們destroy ,這會造成GC頻繁的被觸發,我們只需要看debug log,就可以知道GC是否在getview時不斷的觸發。


Load Image

根據android developer的建議是使用async task的方式去讀取圖片,
// Using an AsyncTask to load the slow images in a background thread
new AsyncTask<ViewHolder, Void, Bitmap>() {
    private ViewHolder v;

    @Override
    protected Bitmap doInBackground(ViewHolder... params) {
        v = params[0];
        return mFakeImageLoader.getImage();
    }

    @Override
    protected void onPostExecute(Bitmap result) {
        super.onPostExecute(result);
        if (v.position == position) {
            // If this item hasn't been recycled already, hide the
            // progress and set and show the image
            v.progress.setVisibility(View.GONE);
            v.icon.setVisibility(View.VISIBLE);
            v.icon.setImageBitmap(result);
        }
    }
}.execute(holder);
其實網路上有一些 solution,PicassoUniversal Image Loader,使用他們可以快速解決讀取圖片的問題。


使用scrollingCache 和 animateCache

使用scrollingCache其實就是個DrawingCache,使用他可以避免在每個frame重畫view,進而達到速度的提升,當然他的缺點就是會花費memory。

animationCache 就是layout animation時是否需要使用drawingCache,使用animationCache 需要花費較多的memory,但是可以換來較好的performance。
範例如下

    <ListView
        android:id="@android:id/list"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:animationCache="true"
        android:scrollingCache="true" />

盡量將layout攤平


看一下下面的範例

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="a" />

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical" >

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="b" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="c" />
    </LinearLayout>

</LinearLayout>

其實上面的layout使用relativeLayout 一層就可以搞定了,越多層layout會使getview花越多時間,基本上三層以上就會很有感覺了


如果能使用圖片,盡量使用圖片

有些時候會為了客制某些圖樣,我們必須使用如下的方式去繪製圖片

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical" >

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/a" />

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/b" />

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/c" />

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/d" />
    </LinearLayout>

example of a.xml

        <item>
        <layer-list>
            <item android:bottom="4dp" android:bottom="4dp">
                ...
            </item>
            <item android:left="4dp" android:top="4dp">
                ...
            </item>
        </layer-list>
    </item>

自己畫的好處是,即使螢幕變寬變長,layout不至於被影響,缺點是當所有的layout都用畫的,其實getview會花費較多的時間,我會發現這個的原因,是由於我客製的layout不過是一個linearlayout裡面包含分隔線還有一些文字,但是只要這個layout出現,畫面就會變得很頓,在經過了多方嘗試之後,我決定使用圖片,所有的問題就迎刃而解了。

最後附上 reference
在android 中要讓 listview平順的滑動真的需要花很多時間去tune啊...

2014年5月22日 星期四

Android 開發 (四十七) rebound animation library

想要客製出順暢的動畫,對RD來說其實算是一大考驗,
必須花很多額外的時間去tune才能得到不錯的效果,
今天要來介紹一個library,使用他之後就可以讓我們輕鬆達到動態的效果,
 先看一下效果吧..





 接著看一下該如何實做出這麼神奇的功能吧

使用rebound library

			mSpring = SpringSystem.create().createSpring()
					.setSpringConfig(ORIGAMI_SPRING_CONFIG)
					.addListener(new SimpleSpringListener() {
						@SuppressLint("NewApi")
						@Override
						public void onSpringUpdate(Spring spring) {
							doUpdate();
						}
					});

這邊是先做init的動作,設定參數,當 mSpring的value更改時 listener就會被trigger,


				@Override
				public void onClick(View v) {
					Log.d("Ted", "val " + mSpring.getEndValue());
					if (mSpring.getEndValue() == 0) {
						mSpring.setEndValue(1);
					} else {
						mSpring.setEndValue(0);
					}
				}

當我在click時執行 setEndValue 1 或 0 ,
就會trigger  listener, 接著我可以利用下面的function 取得動畫所需要的數值,並且執行動畫效果


			double val = mSpring.getCurrentValue();
			float Translate = (float) SpringUtil
					.mapValueFromRangeToRange(val, 0, 1, 300, 0);
			float Scale = (float) SpringUtil
					.mapValueFromRangeToRange(val, 0, 1, 3, 1);
			mImg.setTranslationX(Translate);
			mImg.setTranslationY(Translate);
			mImg.setScaleX(Scale);
			mImg.setScaleY(Scale);

如果仔細去看 Translate的值,以及Scale的值,可以發現他並不是線性的在變化,利用這些數值的變化,就可以產生特別的動畫效果,簡單的說 Spring其實只不過是個數值產生器(當然實作起來是有一定難度的),利用這個數值產生器,我們就可以輕易的製作出動畫效果。


2014年5月21日 星期三

Android 開發 (四十六) Decorate Pattern and SpannableString

在開發app的時候常常會遇到一些特殊的文字,例如
$2000 , $2000 or...

通常遇到這些需求時,會用最簡單的做法去解決問題,使用Utils class專為這個需求
寫一個method,然後呢...然後就完成了,就這樣過了一陣子之後會發現,越來越多的Utils class
在處理這些特殊字元,然後等到要做refactor時就發現,這邊一些code,那邊一些code,以為改完了,結果發現又漏了一些沒改到,其實我們可以在開始的時候就將問題先narrow down,


Decorate Pattern

此pattern主要的特色就是你可以在你原本的東西上面任意加上想要的配料。
怎麼說呢?  例如手搖飲料店,今天我想喝珍珠 + 椰果 + 仙草 + 奶茶。
我總不可能為了他寫一個Utils class吧。那如果有人要點
珍珠 + 椰果 + 仙草 + 奶茶 去 仙草, 那我的程式碼不就重複了。

最好的做法是甚麼?

珍珠,椰果,仙草,這些配料我可以隨意添加,然後最後在配上我的茶類即可。
這就是Decorate Pattern的概念。

回到原本的需求,為了達到這個目標,我目前會有三個class
RedString , Image, 可以把這些當作任意添加的配料,然後最終還是得配上 String

所以,依照這樣的配置,我會寫出類似下面的code


public interface EndDecorate {
 public CharSequence getCharSequence();

 public static class MyEndDecorate implements EndDecorate{
  private String str;
  public MyEndDecorate(String _str){
   str = _str;
  }
  
  @Override
  public CharSequence getCharSequence() {
   SpannableString span = new SpannableString(str);   
   return span;
  }
  
 }
 
 public static class StringDecorate implements EndDecorate {
  private EndDecorate nexDecorate;
  public StringDecorate(EndDecorate _nexDecorate){
   nexDecorate = _nexDecorate;
  }
  
  @Override
  public CharSequence getCharSequence() {

   return nexDecorate.getCharSequence();
  }

 }
 
 public static class AddStringDecorate extends StringDecorate{
  private String newString;
  public AddStringDecorate(EndDecorate _nexDecorate,String _newString) {
   super(_nexDecorate);
   newString = _newString;
  }
  
  @Override
  public CharSequence getCharSequence() {
   SpannableString span = new SpannableString(newString);   
   return TextUtils.concat(super.getCharSequence(),span); 
  }  
 }
 
 public static class RedStringDecorate extends StringDecorate{
  private String RedString;
  public RedStringDecorate(EndDecorate _nexDecorate,String _RedString) {
   super(_nexDecorate);
   RedString = _RedString;
  }
  
  @Override
  public CharSequence getCharSequence() {
   SpannableString span = new SpannableString(RedString);
   span.setSpan(new ForegroundColorSpan(Color.RED), 0, span.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
   return TextUtils.concat(super.getCharSequence(),span); 
  }
 }
 
 public static class ImageDecorate extends StringDecorate{
  Drawable drawable;
  public ImageDecorate(EndDecorate _nexDecorate,Drawable _drawable) {
   super(_nexDecorate);
   drawable = _drawable;
  }
  
  @Override
  public CharSequence getCharSequence() {
   SpannableString spannable = new SpannableString(" ");
   ImageSpan span = new ImageSpan(drawable, ImageSpan.ALIGN_BASELINE);
   spannable.setSpan(span, 0,
     spannable.length(),
     Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
   return TextUtils.concat(super.getCharSequence(),spannable);
  }
  
 }
}


接著要怎麼使用呢

   Drawable draw = getResources().getDrawable(android.R.drawable.ic_menu_search);    
   draw.setBounds(0, 0, 70, 70);   
   EndDecorate img = new ImageDecorate(myend,draw);
   EndDecorate add = new AddStringDecorate(img," searchButton ");
   ((EditText)rootView.findViewById(R.id.textview)).setHint(add.getCharSequence());

這樣就可以將想要的圖片和字套用到我的textview中了,
用起來很方便,而且維護起來也很容易,每個class 都只負責自己該添加的東西,

不過目前有遇到圖片無法自動置中的問題,我也還沒有解決的方法,或許之後會找到解決方法再提供給大家。

2014年5月13日 星期二

Android 開發 (四十五) 簡單的動畫使用,insert


最近由於有需求,所以花了一點時間研究如何使用動畫,
像是上面有點像是插入的動畫是怎麼做的呢?
原理其實很簡單,首先先找出兩個物體的位置,接著利用 move animation 以及 scale animation就可以完成。

動畫的範例程式碼如下

public class AnimationHelper {
 private int[] startPosition = new int[2];
 private int[] endPosition = new int[2];
 public void moveView(View view1 , View view2,AnimationListener listener) {

  startPosition = getPostion(view1);
  endPosition = getPostion(view2);

  AnimationSet bringToLeftFromTopAnimationSet = new AnimationSet(true);
  Animation translateAnimation = new TranslateAnimation(0,
    endPosition[0]-startPosition[0], 0, endPosition[1]-startPosition[1]);
  translateAnimation.setDuration(1000);
  float from = 1;

  ScaleAnimation scaleAnimation = new ScaleAnimation(from,
    0, from,0);
  scaleAnimation.setDuration(1000);
  bringToLeftFromTopAnimationSet.addAnimation(scaleAnimation);
  bringToLeftFromTopAnimationSet.addAnimation(translateAnimation);
  bringToLeftFromTopAnimationSet.setAnimationListener(listener);
  view1.startAnimation(bringToLeftFromTopAnimationSet);
 }

 private int[] getPostion(View view){
  int[] position = new int[2]; 
  view.getLocationOnScreen(position);
  Log.d("Ted", "L "+ String.valueOf(position[0]));
  Log.d("Ted", "T "+String.valueOf(position[1]));
  return position;
 }
 
}

我寫了一個簡單的helper,使用方式很簡單只需要如下設置即可,

AnimationHelper helper = new AnimationHelper();
    helper.moveView(btn1,btn2,PlaceholderFragment.this);


小結

適當的動畫可以提升使用者經驗,但是動畫也需要花額外的時間才能完成,如何在設計與開發時間之間取得平衡,我想也是在開發專案時必須考量的,當然.....要刻一些有的沒的動畫,RD也要夠強才行 XDDD

2014年5月12日 星期一

Android 開發 (四十四) facebook v2.0(3.14SDK) invitable_friends

在facebook 2.0 版本,要送遊戲邀請給朋友,目前只能使用 invitable_friends 的API,
使用方法如下
/* make the API call */
new Request(
    session,
    "/me/invitable_friends",
    null,
    HttpMethod.GET,
    new Request.Callback() {
        public void onCompleted(Response response) {
            /* handle the result */
        }
    }
).executeAsync();
接著將會回傳如下的資料
 {
      "id": "AVlzYTkXshfBqLe58zR9tY5dZ7L0wltTUkWKT0Z5V87zkwv-39...", 
      "name": "Guy Cross", 
      "picture": {
        "data": {
          "is_silhouette": false, 
          "url": "https://fbcdn-profile-a.akamaihd.net/hprofile-ak-prn1/t5.0-1/623782_622770420_2109148508_q.jpg" 
        }
      }
    }
可以發現,我們再也無法取得使用者真實的fbId,相對的我們只能取得 friend Token,
接著就可以使用Request Dialog 發送訊息,然後在to 的param中填入  friend Token即可
Bundle params = new Bundle();

params.putString("message", "Take this bomb to blast your way to victory!");

// Optionally provide a 'to' param to direct the request at a specific user
params.putString("to", "RECIPIENT_USER_ID");

// Give the structured request information
params.putString("action_type", "send");
params.putString("object_id", "YOUR_OBJECT_ID");  

WebDialog requestsDialog = (
        new WebDialog.RequestsDialogBuilder(getActivity(),
            Session.getActiveSession(),
            params))
            .build();
    requestsDialog.show();

在發送成功之後可以得到一組

ParameterDescription
requestThe request object ID. To get the full request ID, concatenate this with a user ID from the to field: <request_object_id>_<user_id>
toAn array of the recipient user IDs for the request that was created.

接著將 requestId and UserId 組起來,就可以得到完整的資料
http://graph.facebook.com/<REQUEST_OBJECT_ID>_<USER_ID>?access_token=APP_ACCESS_TOKEN
在這邊可以得到朋友 app scope Id。
{
  "id": "REQUEST_OBJECT_ID", 
  "application": {
    "name": "APP_DISPLAY_NAME", 
    "namespace": "APP_NAMESPACE", 
    "id": "APP_ID"
  }, 
  "to": {
    "name": "RECIPIENT_FULL_NAME", 
    "id": "RECIPIENT_USER_ID"
  }, 
  "from": {
    "name": "SENDER_FULL_NAME", 
    "id": "SENDER_USER_ID"
  }, 
  "message": "ATTACHED_MESSAGE", 
  "created_time": "2014-01-17T16:39:00+0000"
}
目前好像還找不到較好的方法取得friends list , 或許過陣子facebook會提供相關的api

2014年5月11日 星期日

Android 開發 (四十三) facebook v2.0(3.14SDK) chat api

根據facebook v2.0的文件描述

The Chat/XMPP service and API, located at chat.facebook.com, will not be available once 1.0 is deprecated.

也就是說利用xmpp send message的功能,在明年的4/30將會失效,
然而facebook也有提供新的解法,也就是 message Dialog,
但是message Dialog必須安裝 Facebook Messager app 才能夠使用,
也就是說,假設使用者未安裝此APP,則呼叫該api 將會失效.....,
然而目前也還未看到其他的解決方法,這邊就先簡單的介紹一下 Message Dialog的用法,
FacebookDialog.MessageDialogBuilder builder = new FacebookDialog.MessageDialogBuilder(getActivity())
    .setLink("https://www.google.com")
    .setName("goo")
    .setCaption("it's a caption")
    .setPicture("image_url")
    .setDescription("some description")
    .setFragment(this);


if (builder.canPresent()) {
  builder.build().present()
}
使用方法非常簡單,只需使用MessageDialogBuilder,之後他會導向Facebook Messager app,
好處是我們不必再像以前以樣必須先建立xmpp的connection之後,
再利用intent service去send message,缺點是目前似乎沒看到background send message的方法,
只能希望facebbok之後會提供其他的api,讓我們可以更方便的傳送訊息了。

2014年5月10日 星期六

Android 開發 (四十二) facebook v2.0(3.14SDK) Scope app match to other app using Business api

最近 facebook 發佈了 sdk 3.14版本,其中最主要的修改是 facebook 在 4/30號之後註冊的app再也無法使用原本的api取得 friend list ,必須使用invitable api 來取得資料,並且限定app類型為GAME才能取得,取得的資料也有做一些變更,不過這部分不會在這裡多做說明(或許之後吧....),
以及新的app取得的fb_id不再是使用者真實的fb id ,而是scope  id,也就是說這個id僅可在我的app 裡面使用,也就是說同一個使用者在不同的app裡面擁有的fb id 不再相同。

假設我今天擁有多個app,我希望在多個app中確認該登入的使用者是否已經在我其他的app中登入過,我就必須使用 business api來map scope id。

在這裡我會稍微講解如何使用 Business api來 map Scope app,
要使用Business api 首先必須前往
設定 -> 進階

並完成註冊,只需要填一些基本資料即可,
如果卡在 You're not a Page Admin on any Facebook Pages.
他的意思是說,你必須在一個粉絲頁中擁有 Admin權限,最快的做法就是
只需要在自己的帳號中創建一個粉絲頁即可。

接著將我所有的app 加入我的business中

接著再使用facebook 範例的sample code
new Request(
    session,
    "/me/ids_for_business",
    null,
    HttpMethod.GET,
    new Request.Callback() {
        public void onCompleted(Response response) {
            /* handle the result */
        }
    }
).executeAsync();
就會取得相關的資料了,以下是我這邊取得的結果

  1. {
    • "id":"149102310",
    • "app":{
      • "id":"test1_fb_id",
      • "namespace":"test1",
      • "name":"test1"
      }
    },
  2. {
    • "id":"10202864123396999",
    • "app":{
      • "id":"test2_fb_id",
      • "name":"test2"
      }
    }
其中149102310為app test1 的scope app id ,10202864123396999 為app test2的scope app id,
這樣我們就可以match到該使用者是否有在我其他的app中使用facebook登入過了。


附註:
我們也可以使用 "/{friend_id}/ids_for_business" 來做朋友Id的mapping,這個功能目前是可用,
希望過陣子還是可以使用(極度不信任facebook api XD)

2014年5月3日 星期六

Android 開發(四十一) Spotlight

這是由 Romain Guy 提供的作法
下面是他的網址
http://www.curious-creature.org/2012/12/13/android-recipe-2-fun-with-shaders/

他的做法其實是利用 canvas  draw bitmap 以及 paint來完成,
首先他將背景設置到 paint,
接著將mask 轉成 alpha 接著在canvas上繪製就完成了。

知道大概要怎麼做之後,我們來看code

  • 將背景設置到 paint

 private void createShader() {
  View target = getRootView().findViewById(mTargetId);
  mTargetBitmap = createBitmap(target);
  Shader targetShader = createShader(mTargetBitmap);
  mPaint.setShader(targetShader);
 }

 private static Shader createShader(Bitmap b) {
  return new BitmapShader(b, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
 }

 private static Bitmap createBitmap(View target) {
  Bitmap b = Bitmap.createBitmap(target.getWidth(), target.getHeight(), Bitmap.Config.ARGB_8888);
  Canvas c = new Canvas(b);
  target.draw(c);
  return b;
 }


  • 接著將mask圖轉成alpha


        mMask = convertToAlphaMask(BitmapFactory.decodeResource(getResources(), maskId));
 
        private static Bitmap convertToAlphaMask(Bitmap b) {
  Bitmap a = Bitmap.createBitmap(b.getWidth(), b.getHeight(), Bitmap.Config.ALPHA_8);
  Canvas c = new Canvas(a);
  c.drawBitmap(b, 0.0f, 0.0f, null);
  return a;
 }


  • 接著在canvas上繪製

canvas.drawBitmap(mMask, 0.0f, 0.0f, mPaint);

基本的功能這樣就完成了,如下圖



接著該如何讓spotlight移動以及放大是另一個問題,
假設我們希望將剛剛的spotlight放大3倍,並且移至較中間的位置 100,100 我們應該怎麼做呢?

  mShaderMatrix.setScale(1.0f / 3, 1.0f / 3);
  mShaderMatrix.preTranslate(-100, -100);

  mPaint.getShader().setLocalMatrix(mShaderMatrix);
  
  canvas.translate(100, 100);
  canvas.scale(3, 3);
  canvas.drawBitmap(mMask, 0.0f, 0.0f, mPaint);

可以發現,作者的做法是

  1. 先將背景先縮小並反向位移
  2. 接著再將整個畫布放大並位移


也就是原先看到的mask 被放大的並位移到較中間的位置,而背景的縮放以及位移被抵銷掉
結果的圖如下


由於作者的範例包含太多動畫,所以我寫了一個更簡單的sample,由兩個按鈕來控制
postion以及 scale  ,讓大家比較容易trace,附上 sample code