Wednesday, April 13, 2011

在你的Apk裡使用GoogleMap

使用GoogleMap有幾個Google要我們遵守的,一旦沒遵守會發生超多問題︰

1.SDK需使用含Google APIs [Android x.x]的,而非單純的 [Android x.x]

因為我們使用Google API s[Android 2.2]  (而非只是[Android 2.2]) ,maps.jar會直接被匯入到APIs裡,而不用自己另外匯入maps.jar

2.

 
這行要宣告在AndroidManifest.xml的區塊裡,放在別的區塊都不行



3.必須使用含有Google APIs的模擬器


4.跟Google申請APIkey
將手邊正在用的KeyStore取出MD5,跟Google申請專屬的Google Map金鑰。
並且在你的layout.xml裡宣告。
如︰

Tuesday, April 5, 2011

客製化你專屬的View - View與OnDraw()應用

文章更新時間︰2013/06/04
文章更新次數︰3

一、前提

話說人是貪婪的,
這句話用在沉浸於3C的使用者來說,
真是滿恰當的。

一開始就以這段文字做開頭,
跟今天的主題很有關係,
一般初學者剛開始學的,
大部份都是如何建立一個TextView、EditText、Button等等的元件。

想要在我們的畫面上叫出一個TextView文字框,我們有以下2種方式︰
1.在程式碼裡
TextView text = new TextView(this);
text.setText("test");
我們的Layout.addView(text);  //將TextView新增至我們的Layout
2.在xml裡宣告
<TextView
    android:layout_width="fill_parent"
    android:layout_height="warp_content"
    android:text="test"/>
於是,我們可以很順利的得到這個畫面

但是這些基礎元件用久了,我們或使用者,
開始想要搞東搞西,想要客製化

以下是這篇分享文最後會呈現的樣子︰

ㄜ...這不就是再加一行字,並且把字的大小和顏色改一改而已嗎?
有什麼好說的呢?

二、文章開始

如果你這麼想,可就大錯特錯囉!
快來看看我在layout資料夾裡面的main.xml放了些什麼︰
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res/com.test.testcustomize"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
<TextView  
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:text="@string/hello"
    />
<com.test.testcustomize.LabelView
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    app:text="Reddd" />
</LinearLayout>
有注意到嗎?多了
xmlns:app="http://schemas.android.com/apk/res/com.test.testcustomize"

app:text="Reddd" 
這時候你會問︰
app:text="Reddd"
這個標籤怎麼來的?

從這裡︰

只要在res/values/裡新增attrs.xml這份文件,宣告我們自訂的屬性,
然後在main.xml裡導入這份文件,
再加上之後會提到LabelView.class的一些設定,
就可以達到藍色字的效果了。

也許你會說︰
「天啊!
為什麼我們要把一個原本可以很簡單設定大小和顏色就能做到的事,
搞得那麼複雜?」
這就是我今天要說的「客製化」。

不知道你有沒有注意到,畫面中藍色的那行字,
只在main.xml裡很簡單的宣告文字內容,
app:text="Reddd"
卻呈現了藍色、而且更大的字體。

Android允許我們自訂View,
呈現我們自己想呈現的基本樣式。

技術文件說︰
如果我們想要有自己的View,
那我們就自己寫一個類並繼承View。
所以在這份文件裡,我多寫了一個LabelView來繼承View。

該類別Code如下(可快速跳過這段)︰
package com.test.testcustomize;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

public class LabelView extends View {
    private Paint mTextPaint;
    private String mText;
    private int mAscent;
    
    public LabelView(Context context) {
        super(context);
        initLabelView();
    }
    
    public LabelView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initLabelView();
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LabelView);
        CharSequence s = a.getString(R.styleable.LabelView_text);
        if(s!=null){
            setText(s.toString());
        }
        setTextColor(a.getColor(R.styleable.LabelView_textColor, 0xFF0000FF));

            int textSize = a.getDimensionPixelOffset(R.styleable.LabelView_textSize, 0);
            if (textSize > 0) {
                setTextSize(textSize);
            }
        a.recycle();
    }

    private void initLabelView(){
        mTextPaint = new Paint();
        mTextPaint.setAntiAlias(true);
        mTextPaint.setTextSize(24);
        mTextPaint.setColor(0xFF000000);
        setPadding(3,3,3,3);
    }

    private void setText(String str){
        mText = str;
        requestLayout();
        invalidate();  //每呼叫一次就會重新繪圖onDraw()
    }

      /**
     * Sets the text size for this label
     * @param size Font size
     */
    public void setTextSize(int size) {
        mTextPaint.setTextSize(size);
        requestLayout();
        invalidate();  //每呼叫一次就會重新繪圖onDraw()
    }

    /**
     * Sets the text color for this label.
     * @param color ARGB value for the text
     */
    public void setTextColor(int color) {
        mTextPaint.setColor(color);
        invalidate(); //每呼叫一次就會重新繪圖onDraw()
    }

    /**
     * @see android.view.View#measure(int, int)
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(measureWidth(widthMeasureSpec),
                measureHeight(heightMeasureSpec));
    }

    /**
     * Determines the width of this view
     * @param measureSpec A measureSpec packed into an int
     * @return The width of the view, honoring constraints from measureSpec
     */
    private int measureWidth(int measureSpec) {

        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        
        int specSize = MeasureSpec.getSize(measureSpec);
        
        if (specMode == MeasureSpec.EXACTLY) {
            // We were told how big to be
            result = specSize;
        } else {
            // Measure the text
            result = (int) mTextPaint.measureText(mText) + getPaddingLeft()
                    + getPaddingRight();
            if (specMode == MeasureSpec.AT_MOST) {
                // Respect AT_MOST value if that was what is called for by measureSpec
                result = Math.min(result, specSize);
            }
        }

        return result;
    }

    /**
     * Determines the height of this view
     * @param measureSpec A measureSpec packed into an int
     * @return The height of the view, honoring constraints from measureSpec
     */
    private int measureHeight(int measureSpec) {

        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        mAscent = (int) mTextPaint.ascent();
        Log.i("tag", "Height Ascent: "+mAscent);
        if (specMode == MeasureSpec.EXACTLY) {
            // We were told how big to be
            result = specSize;
        } else {
            // Measure the text (beware: ascent is a negative number)
            result = (int) (-mAscent + mTextPaint.descent()) + getPaddingTop()
                    + getPaddingBottom();
            Log.i("tag", "Height mTextPaint.descent(): "+mTextPaint.descent());
            if (specMode == MeasureSpec.AT_MOST) {
                // Respect AT_MOST value if that was what is called for by measureSpec
                result = Math.min(result, specSize);
            }
        }
        return result;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);        
        canvas.drawText(mText, getPaddingLeft(), getPaddingTop() - mAscent, mTextPaint);
    }
    
}

Code很長,請聽我一一道來。

這段Code的基本骨架是這樣的︰
1.一開始,宣告2個建構式︰
//建構式1
    public LabelView(Context context) {
        super(context);
        initLabelView();  //設定文字的最初樣式
    }

    //建構式2    
    public LabelView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initLabelView();  //設定文字的最初樣式

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LabelView);
        CharSequence s = a.getString(R.styleable.LabelView_text);
        if(s!=null){
            setText(s.toString());
        }
        setTextColor(a.getColor(R.styleable.LabelView_textColor, 0xFF0000FF));

            int textSize = a.getDimensionPixelOffset(R.styleable.LabelView_textSize, 0);
            if (textSize > 0) {
                setTextSize(textSize);
            }

        a.recycle();
    }
技術文件裡提到,
如果我們想對我們想呈現的View初始化,
那麼我們就應該在建構式裡做基本的設定

先不看第2段建構式的龐大內容,
這2個建構式都有一個共通點︰
都呼叫了
initLabelView();
該函式的內容如下︰
private void initLabelView(){
        mTextPaint = new Paint();
        mTextPaint.setAntiAlias(true); //設定反鉅齒
        mTextPaint.setTextSize(24);    //設定預設大小為24
        mTextPaint.setColor(0xFF000000); //設定預設的顏色為黑字體
        setPadding(3,3,3,3);   //設定在版面上下左右各距離3
    }
我們在輸入文字的一開始,
馬上呼叫了Paint,
並對這個我們自訂的View做了初始化.

回到第2個建構式︰
//建構式2    
    public LabelView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initLabelView();

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LabelView);
        CharSequence s = a.getString(R.styleable.LabelView_text);
        if(s!=null){
            setText(s.toString());
        }
        setTextColor(a.getColor(R.styleable.LabelView_textColor, 0xFF0000FF));

        int textSize = a.getDimensionPixelOffset(R.styleable.LabelView_textSize, 0);
        if (textSize > 0) {
             setTextSize(textSize);
        }
      
        a.recycle();
    }
第6行︰將我們自己做的attrs.xml呼叫進來
第7行︰取出我們在main.xml裡設定的字
第8行︰如果有取到字,則呼叫setText()函式
第11行︰設定文字最後會呈現的顏色,如果在main.xml裡面沒有設定的話,
就用這裡的預設值︰0xFF0000FF,設為藍色
第13行︰設定文字大小
第18行︰讓我們自訂的attrs可以重覆被使用(因為以後也可能被用到啊!)

Android在畫出一個View時,
原理是這樣子的︰
1.不斷的呼叫我們程式裡的onMeasure()函式去量測現在要畫的View的尺寸會有多大︰
/**
     * @see android.view.View#measure(int, int)
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(measureWidth(widthMeasureSpec),
                measureHeight(heightMeasureSpec));
    }
2.最後呼叫onDraw()繪圖

當我在試驗時,程式為了畫出藍色字體,
至少呼叫了3次的onMeasure()才呼叫onDraw()。

所以我們得知︰
系統在開始onDraw()前,
會不斷的呼叫onMeasure()去跟Layout問現在要畫的藍色字體,
範圍是多大。

所以,我們就要將尺寸的算法在這裡交待一下了︰
/**
     * 量測這個View的寬
     * @param measureSpec A measureSpec packed into an int
     * @return The width of the view, honoring constraints from measureSpec
     */
    private int measureWidth(int measureSpec) {

        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);       
        int specSize = MeasureSpec.getSize(measureSpec);
        
        if (specMode == MeasureSpec.EXACTLY) {
            // We were told how big to be
            result = specSize;
        } else {
            // Measure the text
            result = (int) mTextPaint.measureText(mText) + getPaddingLeft()
                    + getPaddingRight();
            if (specMode == MeasureSpec.AT_MOST) {
                // Respect AT_MOST value if that was what is called for by measureSpec
                result = Math.min(result, specSize);
            }
        }

        return result;
    }
/**
     * 量測這個View的高
     * @param measureSpec A measureSpec packed into an int
     * @return The height of the view, honoring constraints from measureSpec
     */
    private int measureHeight(int measureSpec) {

        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        mAscent = (int) mTextPaint.ascent();
        Log.i("tag", "Height Ascent: "+mAscent);
        if (specMode == MeasureSpec.EXACTLY) {
            // We were told how big to be
            result = specSize;
        } else {
            // Measure the text (beware: ascent is a negative number)
            result = (int) (-mAscent + mTextPaint.descent()) + getPaddingTop()
                    + getPaddingBottom();
            Log.i("tag", "Height mTextPaint.descent(): "+mTextPaint.descent());
            if (specMode == MeasureSpec.AT_MOST) {
                // Respect AT_MOST value if that was what is called for by measureSpec
                result = Math.min(result, specSize);
            }
        }
        return result;
    }
第6行︰一開始系統傳進來的引數measureSpec是從main.xml裡抓過來的
第9行︰在MeasureSpec.getMode()裡會抓到的尺寸模式,有以下3種
 Mode
 Value
 備註
main.xml裡相對應的設定
AT_MOST
 -2147483648
 子視圖可自行調整尺寸大小
android:layout_width或height 設為"wrap_content"時
EXACTLY     
 1073741824
 父Layout已經很明確的定義該子視圖的大小
android:layout_width或height有自訂大小或為"fill_parent"時
UNSPECIFIED
 0
 父Layout未指定大小,子視圖可自行調整。

第10行︰你在main.xml裡真實設定的尺寸。

幾次的onMeasure()呼叫完後,就跑去呼叫onDraw()開始繪圖了。

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);        
        canvas.drawText(mText, getPaddingLeft(), getPaddingTop() - mAscent, mTextPaint);
    }
挪,結束了。

三、結論

1.在res/values/新增一個自訂的attrs.xml
2.在main.xml裡使用他們
3.繼承View,並在
(1)建構式 裡宣告初始值
(2)在onMeasure()裡交待清楚這個自創的View應該有多大
(3)在onDraw()裡開始繪圖

這樣子,你就能為所欲為,畫出任何你預設想呈現出來的View了。

有點複雜,但搞懂了就簡單了。

備註︰
來源︰百度知道





1.基準點是baseline
2.ascent:是baseline之上至字符最高處的距離
3.descent:是baseline之下至字符最低處的距離
4.leading:是上一行字符的descent到下一行的ascent之間的距離,也就是相鄰行間的空白距離
5.top:指的是最高字符到baseline的值,即ascent的最大值
6.bottom:指最低字符到baseline的值,即descent的最大值

Sunday, April 3, 2011

Android從不會到會 - 一位從餐飲業跳到資訊業的心路歷程

相信很多想要跳進Android開發世界的初學者在入門的階段都無從學起,而這條路我也是走的挺辛苦的。現在有了一點小小的心得,希望在這裡分享給剛入門的朋友們。

我本來的職業是餐飲業的煮麵學徒兼副店,之所以會跳來這個領域
1.臺灣餐飲業人員逐漸大陸化
2.餐飲業越來越看不出我存在的優勢

後來想一想,手上那臺Windows Mobile手機真難用,
可是手機不應該僅止於此,於是跑去聯成學JAVA,也很確定要開發手機。

在聯成遇到貴人好老師(Thanks for 顧老師Piano Man),辛苦了半年左右得了一張基本的SCJP證照,
也很辛苦的進了一家公司開始,
對我而言,才是戰場的開始。

Android設計不用太深的JAVA理論,
而Google在讓開發人員跳槽進來,也設定了很低的門檻。

但對我而言,有一個很重要的障礙︰
「英文很爛。」

不知為何,對那隻Android機器人就是情有獨鍾,
就是想開發Android。

於是我開始從Gasolin的網路電子書下手。
Gasolin的這個網站,讓我從英文很爛 + 對Android什麼都不懂,變成了開始懂什麼叫Android Activity的生命週期等等的Android基本概念。經由這個網站製作BMI量測的專案的教學,建立了我對Android的基本認識。

註︰
2011-09-23
Gasolin其實有將這個電子書出成實體書,
大家還是可以多多買實體書,
最近他的寶寶剛誕生,
養家不容易的!

我跟你們說,在程式開發初期,你會遇到一堆超級奇怪的現象︰
1.Eclipse怎麼用?我在補習班學的是NetBeans。


2.什麼叫外掛?Eclipse怎麼掛Android都掛不起來?
3.為什麼專案一匯入Eclipse就是出現紅色警嘆號或XX
4.為什麼叫不出顏色
5.為什麼就是不能安裝Android SDK

總之,你會在剛踏入Android的初期遇到超多超多的問題,
但是你都得努力的爬文、不計任何代價、用任何方法,
都要去克服他們。
我覺得因為有這些過程,才能磨練出一位程設師之後在開發過程遇到困難時的耐力。

讀完了Gasolin的電子書後,接下來要如何下一步?
我遇到了盧育聖Sam的網站
Sam在帶領新人的一開始,
希望我們打好JAVA的基礎,
希望我們讀熟Android技術文件再來做開發。

這種建議不否認絕對是個好建議,
但對我這種英文很爛的人來說,
簡直是災難一場

這時候心裡想,
真的該堅持下去嗎?
這條路超辛苦的。

我相信當一個人有心的時候,
老天爺絕對不會給他絕路。
此時,我發現到,
有一個東西可以幫助英文很爛 + 對Android什麼都不懂的我,
那就是Mars的視頻

Mars的視頻是一個很棒的東西,
他幫你把你本來應該從技術文件看到的基礎知識,
用影像的方式一步一步帶你實作和講解,
他講的內容對我來說都不會很難。
(可能我當時手上已經買了6本左右的Android書了)

我覺得在學習的過程中,
有一點很重要,
就是要有一個練習的環境 + 好的工具書。

在一家Android環境的公司磨練 + 工具書,
幫我慢慢的建立起"模仿"的能力。

工具書我推薦一定要買SDK範例大全2

這是一本幫什麼都不懂的你,
開始學會模仿的書。

註︰
2011-09-23
現在又有新版了︰SDK開發範例大全3

所以,邊看書、邊打成Code、邊查英文、邊做專案、參與論譠(大陸最大的Android網站eoe),
就成了我程式學習的起初所做的事。

我從Mars的視頻裡,
真的是驚嘆他能一看技術文件變馬上翻譯成中文的能力,
我理解到如果Android技術如果要更上一層,
你的英文底子一定要有。

於是,下班後跑去補習班學英文、聽廣播、跟外國人聊天、看電影學英文…

但是,這麼做並沒有幫我看懂技術文件,
因為技術文件裡的字彙,
只是英文的其中一個領域。
在我學英文的範圍裡,是沒有學到這個領域的。

苦了我,拚了那麼久英文文件仍看不懂。

我又去找為什麼我看不懂技術文件的原因。

1.我完全沒有文法概念,所以一個句子遇到倒裝或子句時,我看不懂其意。
2.一堆生字,造成我一個字也看不下去。
3.句子與句子之間的介系詞片語,如︰for a while, in particular,..我看不懂。

於是又再報了一家補習班,專攻閱讀與文法這一塊。
補了1、2個月,效果也慢慢出來了。

但真正讓我理解我應該要怎麼面對外來語言的文章的,
是呂英沖的書籍︰你用對方法學英語了嗎。


我是在國家圖書總館無意間看到這本書的,
書裡提倡我們一般在聽英文MP3時,
都是先唸英文,再聽中文解釋。
但這個方式錯了。
因為英文是我們不熟的語言,
當我們聽到  Apple 蘋果  時,
對於外文完全不熟的我們,聽到Apple腦袋馬上打了個問號,
後來聽到蘋果的中文解釋,才知道原來剛剛那個字是蘋果,
此時,剛剛那個字怎麼發音你也忘了。
所以呂先生說,先唸英文再唸中文的教材,是FOR老外閱讀的。
我們應該先聽蘋果,再聽到Apple。否則會產生記憶斷點

他又提到語言一定是先聽,後講,後來產生文字,最後才寫。
所以,在我所要求的讀懂Android技術文件裡,
我得先會聽和講英文。

直覺告訴我呂先生心理+記憶學的經歷,能讓我在學英文的過程中,
得到更多的啟發,
於是又繼續往他的其他書籍追相關資料。

我追到外全外文學習法。

這本書提到我們是怎麼看懂一個句子的。

我看到小明在沙發睡覺。

1.看到 「我」這個字時,我們心裡會開始默唸「ㄨㄛˇ」,並跟大腦索取什麼叫「我」,
於是你得到我就是「自己」的意思。
然後,你將「我」這個字,存在你腦中的短暫記憶裡。
2.依此類推,你一一的將「看到」、「小明」、「在」、「沙發」、「睡覺」這些字放進了短暫記憶。
3.最後你看到了句點。
4.大腦開始將你短暫記憶裡儲存的「我」、「看到」、「小明」、「在」、「沙發」、「睡覺」組出一個句意。於是你知道什麼叫「我看到小明在沙發睡覺」。

在這個理解的過程中,如果你不知道什麼叫「看到」,
或者如果你的短暫記憶裡,漏記了「看到」,
而只記得「我」、「小明」、「在」、「沙發」、「睡覺」,
那這個句意是出不來的。

原來我們要看懂一個句子,我們經過了一個
心裡默唸→懂詞意→記憶→組詞成句
的過程。

英文也是如此。
An apple a day, keeps the doctor away.

你是不是也用一樣的原理,在理解這個句子呢?

搞懂了人類理解句子的原理,
長期的廣播 + 補習 + 背Android技術字 + 真正去寫過程式碼產生了開發經驗
我才終於慢慢看懂技術文件。

謝謝你看完這篇文章,
我相信你絕對是一位有耐心的人。

為了得到一個目的,
我付出了相當大的代價。

這個過程中,
我發現我不斷在做的事情是︰
1.我缺什麼?
2.該如何補足?
3.方法對嗎?
4.不斷檢視這3點。

寫到這邊,我覺得這不僅用在學Android,
也能用在學任何的東西上面。

如果你真的有心,我相信沒有事能難的倒你的。
一起加油!

下一篇︰Android開發者應有的認知