2013年6月28日 星期五

[Android] Retrofit + Signpost : 替Retrofit加上OAuth支援

看到Square發表的這個Retrofit - http://square.github.io/retrofit/ 蠻有趣的, 它的目的似乎是試圖的想要去簡化開發REST client, 開發者不用寫太多的邏輯, 只要寫一個Interface跟利用annotation就可以完成一個簡單的REST client:

public interface GitHubService {
  @GET("/users/{user}/repos")
  List<Repo> listRepos(@Path("user") String user);
}

因為開發者只需要寫interface和annotation, 實質上並不用寫任何的code, 真正實作的部份他用了Proxy class的技巧包裝起來了, 這作法讓我想起來很久之前我在之前的工作幫公司寫的一個legacy系統的wrapper, 那時有很多機器產生的interface, 如果人工一個個實作很浪費時間, Proxy class可以解決掉這一部分的問題, 同樣的在retrofit似乎也是想用這技巧節省實作

但可惜的是, 現在的retrofit並還沒加入OAuth的支援, 因此送出去的API部分並沒被oauth簽章過,不過所幸要解決這一部分也不難, 寫一個Client class搭配Signpost還是可以做到, 這邊範例繼承了OkClient(使用OkHttp) :


package com.htc.blinkfeed.sample.vimeo.api;
import java.io.IOException;
import java.net.HttpURLConnection;
import oauth.signpost.OAuthConsumer;
import oauth.signpost.basic.DefaultOAuthConsumer;
import oauth.signpost.exception.OAuthCommunicationException;
import oauth.signpost.exception.OAuthExpectationFailedException;
import oauth.signpost.exception.OAuthMessageSignerException;
import oauth.signpost.http.HttpRequest;
import retrofit.client.OkClient;
import retrofit.client.Request;
public class SignedOkClient extends OkClient {
OAuthConsumer mConsumer =null;
public SignedOkClient(OAuthConsumer consumer) {
super();
mConsumer = consumer;
}
@Override
protected HttpURLConnection openConnection(Request request)
throws IOException {
HttpURLConnection connection = super.openConnection(request);
try {
HttpRequest signedReq = mConsumer.sign(connection);
} catch (OAuthMessageSignerException e) {
//Fail to sign, ignore
e.printStackTrace();
} catch (OAuthExpectationFailedException e) {
//Fail to sign, ignore
e.printStackTrace();
} catch (OAuthCommunicationException e) {
//Fail to sign, ignore
e.printStackTrace();
}
return connection;
}
}

因為OkHttp也是一種HttpURLConnection, 因此Signpost搭配DefaultOAuthProvider和DefaultOAuthConsumer即可, 另外初始化RestAdapter時加上這個新的Client即可:

RestAdapter restAdapter = new RestAdapter.Builder() .setClient(new SignedOkClient(mConsumer))

2013年6月26日 星期三

[Android] 自定義View屬性

前一篇寫了一個自訂義的layout - SimpleCellLayout, 前一個版本的問題就是, 必須是寫程式把child view加進這個layout之中, 而且針對像是欄與行的數目也必須在程式裡設定, 並無法寫到layout xml中, 所以這次的目邊就是要讓這個layout可以像下面這樣用layout xml來擺佈:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:celllayout="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.jlnshen.widget.celllayout.SimpleCellLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
celllayout:col="5"
celllayout:row="6"
celllayout:gapsize="3dp"
android:id="@+id/celllayout">
<TextView
android:text="@string/app_name"
android:layout_width="match_parent"
android:layout_height="match_parent"
celllayout:cellX="2"
celllayout:cellY="4"
celllayout:colspan="2"
android:id="@+id/test1"
/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/imageView"
celllayout:cellX="0"
celllayout:cellY="4"
celllayout:colspan="2"
celllayout:rowspan="2"/>
</com.jlnshen.widget.celllayout.SimpleCellLayout>
</RelativeLayout>

在這範例之中, 用到幾個像是col, row, gapsize, cellX這些在原生Android並不存在的屬性, 為了這些屬性, 就需要定義一個attrs.xml在res/values目錄內, attrs.xml 裡面要定義的就是這些樣式描述屬性, 這邊定義了: 給SimpleCellLayout本身用的col(欄數), row(行數), gapsize(間距大小), 以及給他的Child views用的cellX(格子的橫軸位置), cellY(格子的縱軸位置), colspan(格子寬), rowspan(格子高), 除了gapsize我們需要的跟實際螢幕上的大小有關, 所以格式定義為dimension外(就是可以用3dp, 1px這類的值), 其他都是整數就可

<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SimpleCellLayout">
<attr name="col" format="integer"/>
<attr name="row" format="integer"/>
<attr name="gapsize" format="dimension"/>
<attr name="colspan" format="integer"/>
<attr name="rowspan" format="integer"/>
<attr name="cellX" format="integer"/>
<attr name="cellY" format="integer"/>
</declare-styleable>
</resources>
view raw attrs.xml hosted with ❤ by GitHub
這些屬性, 到時候就是要放在xml標籤內的屬性

要用到這些屬性, 需要先在tag裡面定義一個新的name space, 如同在前面範例寫的:

xmlns:celllayout="http://schemas.android.com/apk/res-auto"
文件內的範例大多是apk/後面接著package name, 不過Android Studio卻是建議使用"res-auto", 定義了這個name space後, 便可以在後面的tag內使用像是"celllayout:col"這樣的屬性了

那在layout內又要怎處理這些屬性? ViewGroup有兩個建構子所需要帶的參數含有AttributeSet, 基本上只要從這兩個建構子處理就好

public SimpleCellLayout(Context context, AttributeSet attrs) {
super(context, attrs);
initFromAttributes(context, attrs);
}
public SimpleCellLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initFromAttributes(context, attrs);
}
private void initFromAttributes(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SimpleCellLayout);
for (int i = 0; i < a.getIndexCount(); i++) {
int attr = a.getIndex(i);
switch (attr) {
case R.styleable.SimpleCellLayout_col:
mColCount = a.getInt(attr, mColCount);
break;
case R.styleable.SimpleCellLayout_row:
mRowCount = a.getInt(attr, mRowCount);
break;
case R.styleable.SimpleCellLayout_gapsize:
mGap = a.getDimensionPixelSize(attr, mGap);
break;
}
}
a.recycle();
}
這範例內的initFromAttributes就是用來從AttributeSet擷取這些屬性, 前面attrs.xml有定義stylable, 所以基本上有一堆相對應的Resource ID可以使用,這邊只處理了col, row, 和gapsize, 因為只有這三個屬性直接跟這layout相關, 至於這layout的child views該怎處理呢? 它就要靠LayoutParams了, 我在SimpleCellLayout內定義了一個CellLayoutParams, 基本上這類別也有一個建構子是用來處理AttributeSet的


@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new CellLayoutParams(getContext(), attrs);
}
public static class CellLayoutParams extends MarginLayoutParams {
public int cellX = 0;
public int cellY = 0;
public int cellRowSpan = 1;
public int cellColSpan = 1;
int x = 0;
int y = 0;
int width = 0;
int height = 0;
public CellLayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.SimpleCellLayout);
for (int i = 0; i < a.getIndexCount(); i++) {
int attr = a.getIndex(i);
switch (attr) {
case R.styleable.SimpleCellLayout_colspan:
cellColSpan = a.getInt(attr, cellColSpan);
if (cellColSpan < 1) {
cellColSpan = 1;
}
break;
case R.styleable.SimpleCellLayout_rowspan:
cellRowSpan = a.getInt(attr, cellRowSpan);
if (cellRowSpan < 1) {
cellRowSpan = 1;
}
break;
case R.styleable.SimpleCellLayout_cellX:
cellX = a.getInt(attr, cellX);
if (cellX < 0) {
cellX = 0;
}
break;
case R.styleable.SimpleCellLayout_cellY:
cellY = a.getInt(attr, cellY);
if (cellY < 0) {
cellY = 0;
}
break;
}
}
a.recycle();
}
public CellLayoutParams(int width, int height) {
super(width, height);
}
public CellLayoutParams(MarginLayoutParams source) {
super(source);
}
public CellLayoutParams(LayoutParams source) {
super(source);
}
public void saveMeasureResult(int x, int y, int width, int height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
}
這邊比較重要的就是要overwrite掉generateLayoutParams, 因為ViewGroup原有的generateLayoutParams實作的是回傳ViewGroup.LayoutParams, 他並不認得CellLayoutParams, 但SimpleCellLayout靠的是CellLayoutParams來儲存layout所需的資訊, 原本用程式來加入view的範例部份, 我是手動產生一個CellLayoutParams來給child view, 但如果是放在layout xml, 並無法指定要用哪個LayoutParams類別, 這時候就要靠generateLayoutParams了 有趣的是, 雖然是我自訂的layout, 但在我做完這些後, Android studio的UI編輯器, 卻知道怎去依據我的layout把元件畫到正確的位子上面:

2013年6月25日 星期二

[Android] 一個簡單的自製 layout - SimpleCellLayout

寫Android寫了這麼久, 才想到自己沒寫過custom layout, 剛剛花了點時間(主要時間還是花在跟Android studio和gradle搏鬥)寫了一個簡單的CellLayout (還很陽春) :

https://github.com/julianshen/SimpleCellLayout

寫custom layout還蠻簡單的, 在onMeasure決定自己和child views的大小, 在onLayout時把每個child views放到適當的位置, 目前還沒加入attibrtues的支援, 所以暫時還只能寫code自己把view加進去, 另外也還沒加檢查是不是會有重疊的views, 之後再來寫

這個layout跟GridView不同, 可以不用每個格子都是同一大小, 以下範例是把畫面劃成4x4的格子, 左上角的那張圖大小是2x2, 下方則是 4x2



2013年6月21日 星期五

[Android] 圓形大頭貼 - 使用Picasso的Transformation

現在不管是Google+也好, 或是Facebook (Home), 似乎都很喜歡用圓形的大頭貼像這樣 (左上角圖形,

但在server端存的大頭照其實都是都是方形的, 所以必須抓下來後再轉畫成圓的, 最近從網路抓圖的部分, 我還蠻喜歡用Picasso

沒啥別的原因, 就是它簡單, 雖然說, 它似乎還是有一些小小問題, 不過它可以用這樣短短一行解決圖檔下載並顯示到ImageView的一連串動作:

Picasso.with(context).load("http://i.imgur.com/DvpvklR.png").into(imageView);

不過, 簡單的代價可能在其他的地方來說彈性就不高了, 不過, 做這樣一個小玩意兒的彈性倒是還有, 在Picasso中, 可以實作Transformation把下載下來的圖檔再作一次後處理, 在它的網頁有一個CropSquareTransformation的範例, 圓形的大頭貼可以視為這一個的延伸:

/*
* Copyright 2014 Julian Shen
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.picassodemo;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Paint;
import com.squareup.picasso.Transformation;
/**
* Created by julian on 13/6/21.
*/
public class CircleTransform implements Transformation {
@Override
public Bitmap transform(Bitmap source) {
int size = Math.min(source.getWidth(), source.getHeight());
int x = (source.getWidth() - size) / 2;
int y = (source.getHeight() - size) / 2;
Bitmap squaredBitmap = Bitmap.createBitmap(source, x, y, size, size);
if (squaredBitmap != source) {
source.recycle();
}
Bitmap bitmap = Bitmap.createBitmap(size, size, source.getConfig());
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
BitmapShader shader = new BitmapShader(squaredBitmap, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP);
paint.setShader(shader);
paint.setAntiAlias(true);
float r = size/2f;
canvas.drawCircle(r, r, r, paint);
squaredBitmap.recycle();
return bitmap;
}
@Override
public String key() {
return "circle";
}
}
這邊利用了BitmapShader重畫了一張圓形的大頭貼, 出來的結果就會像是這樣:


2013年6月20日 星期四

[Android] 一閃一閃亮晶晶的BlinkLayout

Androids Do Daydream裡有提到Romain Guy要他提(大概假的吧)在layout裡面放"<blink>"可以做到view閃爍的效果而不用寫到一行code

查了一下AOSP的原始碼也的確有這東西, 它是一個躲在LayoutInflater的一個叫BlinkLayout的東西, 這樣一來就可以配合TextClock (API Level 17)來做一個閃爍的時鐘了

<blink android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextClock
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/textClock"
android:textSize="60sp"
android:textColor="#b3b5b5"/>
</blink>

不過, 這樣, 是整個TextClock在閃, 而不是像一般數字鐘一樣是只有":"閃, 如果要做到只有":"在閃, 那只好把"時"跟"分"給拆開, 像這樣:

<TextClock
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/textClock"
android:textSize="60sp"
android:textColor="#b3b5b5"
android:format24Hour="k"/>
<blink android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text=":"
android:id="@+id/textView"
android:textSize="60sp"
android:textColor="#b3b5b5"/>
</blink>
<TextClock
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/textClock2"
android:textSize="60sp"
android:textColor="#b3b5b5"
android:format24Hour="mm"/>

2013年6月16日 星期日

[筆記][Android Studio]Git push

本想說有直接整合了Git, GitHub是件不錯的事, 沒想到要把新project push到remote git就栽了....

Android Studio/Intellij 似乎沒介面設定remote (搜尋了半天沒發現這種答案, 啃了半天的使用手冊也沒發現), 現在如果要把既有的project給匯出到遠端的git (如bitbucket), 就得利用command line (以bitbucket 為例):

  1. 在bitbucket上create一個新的project
  2. 在Android studio選"VCS"->"Import into version control"->"Create Git repository", 這樣就可以建立一個local的repository
  3. "VCS"->"Git"->"Add to VCS"
  4. "VCS"->"Git"->"Commit File"
  5. 到command line下切換到project目錄下"git remote add origin ssh://git@bitbucket.org/xxx/xxx.git"
  6. 回到Android studio, "VCS"->"Git"->"Push"
要注意的是, 如果你已經有一個remote repository,"VCS"->"Import into version control"->"Share project on GitHub"會失敗


2013年6月15日 星期六

[筆記] Android Studio, Gradle & Native libraries

最近試用Android studio開發, 可能有些習慣被以往Eclipse慣壞了, 剛剛發現放在libs/底下的東西不像以前會被自動建置到apk內, 有點小訝異, 對jar file來說, 可能還好不算太難, 在build.gradle的dependencies內補上一筆就夠了, 但對native library (jni)那些.so檔, 就沒辦法

用這樣解決了 Google了一下, 並且在Stack overflow上找到些解法來試, 可能因為Android studio跟Gradle Android plugin還一直在變化中, 在現在這版一直沒成功, 後來小改一下成底下這樣就成功了:
task copyNativeLibs(type: Copy) {
from fileTree(dir: 'jni', include: '**/*.so' ) into 'build/native-libs'
}
tasks.withType(Compile) { compileTask -> compileTask.dependsOn copyNativeLibs }
clean.dependsOn 'cleanCopyNativeLibs'
tasks.withType(com.android.build.gradle.tasks.PackageApplication) { pkgTask ->
pkgTask.jniDir file('build/native-libs')
}
view raw build.gradle hosted with ❤ by GitHub


原本找到的解法是pkgTask.jniDir new File('build/native-libs') , 這樣會得到
> Directory 'build/native-libs' specified for property 'jniDir' does not exist.
把"new File"改成"file"就沒問題了,不過現在一切應該都還在開發中, 未來可能會有原生的native lib 支援也不一定, 未來可能這方法也不管用了吧