2011年7月28日 星期四

在 MapView 中新增一個 View

當點擊地標後,如果需要顯示一個按鈕,那就需要另外加入一個包含著按鈕的 View ,而在本篇則會記錄一下如何利用 Layout 產生一個客製化的 View ,並加入至 MapView 中顯示。

Layout 部分 pop.xml 原始碼 :
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<TextView android:layout_height="wrap_content" android:text="TextView" android:layout_width="wrap_content" android:id="@+id/textView1"></TextView>
<Button android:text="Button" android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>

</LinearLayout>
首先必須先建立一個 Layout ,之後將在主程式內製成一個客製化的 View ,以用來點擊地標後顯示;這個 Layout 可以直接利用 XML 編寫,或是利用 Eclipse 圖形介面做設計。




exMap 原始碼 :
public class exMap extends MapActivity {
/** Called when the activity is first created. */
GeoPoint mGeoPoint01,mGeoPoint02;
ArrayList<OverlayItem> items = new ArrayList<OverlayItem>();
Context mContext;
View popView;
MapController mMapController;
MapView mMapView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

CreatPopView();

mMapView = (MapView)findViewById(R.id.mapview);
mMapView.addView(popView,new MapView.LayoutParams(MapView.LayoutParams.WRAP_CONTENT,MapView.LayoutParams.WRAP_CONTENT, null, MapView.LayoutParams.BOTTOM_CENTER));
mMapView.setBuiltInZoomControls(true);
mMapView.setTraffic(false);
mMapController = mMapView.getController();
mMapController.setZoom(17);
mGeoPoint01 = new GeoPoint(24121194,120675526);
mGeoPoint02 = new GeoPoint(24123689,120672358);
mMapController.animateTo(mGeoPoint01);


List<Overlay> list = mMapView.getOverlays();
MyOverlay mOverlay = new MyOverlay(getResources().getDrawable(R.drawable.icon),this);

OverlayItem mOverlayItem01 = new OverlayItem(mGeoPoint01,"這裡是地標01","點擊地標");
OverlayItem mOverlayItem02 = new OverlayItem(mGeoPoint02,"這裡是地標02","點擊地標");
mOverlay.addItem(mOverlayItem01);
mOverlay.addItem(mOverlayItem02);
list.add(mOverlay);
}

@Override
protected boolean isRouteDisplayed() {
// TODO Auto-generated method stub
return false;
}

public void CreatPopView(){
popView = LayoutInflater.from(this).inflate(R.layout.pop, null);
Button btn = (Button) popView.findViewById(R.id.button1);
btn.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v){
popView.setVisibility(View.GONE);
}
});
popView.setVisibility(View.GONE);
}

class MyOverlay extends ItemizedOverlay<OverlayItem>{
public MyOverlay(Drawable defaultMarker,Context context) {
super(boundCenterBottom(defaultMarker));
mContext = context;
}

@Override
protected OverlayItem createItem(int i) {
// TODO Auto-generated method stub
return items.get(i);
}

@Override
public int size() {
// TODO Auto-generated method stub
return items.size();
}

@Override
protected boolean onTap(int index) {
popView.setVisibility(View.VISIBLE);
MapView.LayoutParams geoLP = (MapView.LayoutParams)popView.getLayoutParams();
Button btn = (Button)popView.findViewById(R.id.button1);
TextView text = (TextView)popView.findViewById(R.id.textView1);

geoLP.point = items.get(index).getPoint();
geoLP.x=15;
geoLP.y=-40;
btn.setText("PUSH");
text.setText(items.get(index).getTitle());
text.setTextColor(Color.MAGENTA);
mMapController.animateTo(items.get(index).getPoint());
mMapView.updateViewLayout(popView, geoLP);
return true;
}

public void addItem(OverlayItem item){
items.add(item);
populate();
}
}
}
在主程式這邊,首先要把剛剛的 Layout 新建為一個自訂的 View ;做法則是利用 LayoutInflater 類別,用法如下程式碼,from(this) 是指定目前使用的這個 context,inflate() 方法將 reslurce 中的 pop.xml 這個 layout 格式製成一個 View , 指定的 root View 為 null ,代表使用這個製成的 View 做為 root。
popView = LayoutInflater.from(this).inflate(R.layout.pop, null);

接著就可以取得 popView 裡的物件來使用,並監聽與設定按鈕按下時該做的動作,在這邊設定為將按鈕按下後即不顯示 popView。在程式初始後沒多久,就可以靠著呼叫 CreatPopView() 這個自訂方法來創建 popView 物件,不過 popView 物件雖然已經建立,但是在創建的初始值中,是將這個 View 設為不顯示,所以在程式執行後是看不到這個建立的 View,直到點擊地標後才會顯示。

為了在點擊地標後能夠控制顯示在 MapView 中的位置,所以在創建 MapView 後還必須將 popView 物件加入,並加入 MapView 相關 Layout 參數,之後才能夠在地圖上指定顯示位置。


接著就需要來看點擊地標後的執行動作,較為重要的是取得 MapView 的 LayoutParams ,利用取得的 LayoutParams 就可以設定顯示在地圖上的哪個位置,設定的方式可以利用經緯度也可以利用 GeoPoint ,這邊的例子使用的是 GeoPoint。

點擊地標後可以利用 getPoint() 取得點擊的地標位置,並回傳給 geoLP 這個 LayoutParams 物件的 point ,之後可以用 geoLP.x 和 geoLP.y 設定其 x、y座標的位移,達成控制顯示在螢幕上的位置。

在這順便記錄一下 Android 的繪圖座標系統,螢幕左上角為原點 (0,0) ,往右 X 值增加,往下 Y 值增加,以剛剛所提到的 geoLP 為例,當取得地標的 point 位置時,如果要將 PopView 放置在地標上方,則 Y 值必須減少,又因為 geoLP.x 和 geoLP.y 為 popView 與 MapView 的相對位置,所以只需要填寫差距值即可。


執行畫面 :

X 與 Y 皆無設定的狀態


X 與 Y 皆設為 0 ,與上圖比較可看出預設值即為0


當 X 為正值,Y為負值時,View則往右上角移動

如果要改變點擊地標產生的畫面,可以輕易的改變 Layout 來完成

2011年7月27日 星期三

2011-07-27 UCampusMap 功能畫面

本篇僅是目前地圖功能的畫面與記錄,因為未來的資料庫會有變動,所以暫時將目前的狀況做一個簡單的記錄,以供之後做參考。目前程式撰寫的部分著重在未來擴充性及維護的設計面上,而非功能性或畫面美觀,所以在這個版本與前一個版本的功能上差異並不大。

Map 基礎物件簡介

目前 Map 的架構下,在新增不同類型的地標時比之前更為容易,除了原本負責向 DB 要資料的 queryDB 這個類別外,還有 MarkerOverlay 類別負責建立地標圖層和 MarkerPoint 類別負責存取地標資訊。


queryDB 類別
接收到 query 的位址或是包含需求欄位與判斷值的要求,向遠端的 AP 做查詢的要求,並取回 JSON 格式資料,此 JSON 資料將存在於 queryDB 物件中,經由操作 queryDB 物件可以取出查詢到的資料甚至於將 queryDB 中的 JSON 物件取出。

MarkerOverlay 類別
在建構 MarkerOverlay 物件時,可以依照不同類別的資料創建,代表著不同類別的資料分別放在不同的圖層中,方便未來的維護;而在點擊地標時可將地標移至畫面中央,並跳出一個可點擊的按鈕,經由點擊此按鈕得到更多相關的資訊;點擊按鈕後的相關資訊由另外的類別作維護,將建構地標的資訊以及地標本身所包含的資訊做分割,好處在於可以需要的時候再向 DB 作要求,一方面可以減少 query 時傳遞的資料量,另一方面也可以降低行動裝置內存在著許多不需要的資訊。


MarkerPoint 類別
繼承自 GeoPoint 類別,並包含了建構地標時所需要的資料;像是地標的位置或是需要顯示的標題等等,都可以經由操作此類別物件而取得。目前的版本因為資料庫還不確定,所以取得資料的操作還需要將所需欄位名稱傳入當參數,未來等資料庫確定後,將改成直接取得資料的方式而不需要再傳欄位名稱,而不同類型所產生的物件將可取得不同的資料。可做為類似功能快速開發的基礎類別。


PointInformation 類別
此類別負責地標的完整資訊,而非建構資訊,物件的建立是在點擊地標後再點擊所產生的按鈕時才產生,而不是程式執行時就建立,物件建立後會再建立一個 queryDB 物件,並經由此物件取得地標的完整訊息,並將訊息呈現於螢幕上;目前此類別是將資訊與呈現方式包在一起,未來將此兩項分開,以達到更大的彈性。

Map 功能畫面
目前畫面上會顯示所有類型的地標,藍色水滴狀的地標為餐廳類型,小的招牌形狀地標為行政單位類型;畫面右上角有一個較大類似招牌的圖示按鈕,目前功能撰寫中,將規劃為類別按鈕,點擊此按鈕後可以叫出地標類型選單以供選擇,且未來在畫面上並不會顯示所有類型的地標,而是依照使用者選擇的類型做變動。

這個圖示按鈕並不是由地圖圖層所產生,如果由圖層所繪製,則地圖移動時按鈕也會跟著移動;這邊的做法是利用 Layout 去做規劃,所以這是個圖示按鈕,也因為如此規劃才能在點擊後同時在地圖畫面上顯示一個類型列表。

另外還有一個外面包覆一圈白色環狀的小藍點,這個是由 MyLocationOverlay 這個類別所產生,藍點的位置為目前定位的位置,外面的藍色範圍為誤差範圍,誤差範圍依照不同的定位方式而有所不同,如果有 WIFI 訊號可以利用則較 3G 訊號準確,GPS 只能在室外使用。


WIFI 及 GPS 關閉,利用 3G 訊號定位,誤差範圍較大。



同時開啟 WIFI 及 3G 時 ,定位自動採取 WIFI 訊號,誤差範圍縮小,定位位置也靠近資科大樓。


點選行政單位地標,地標移至畫面中央,顯示地標標題按鈕,並可點擊按鈕


點擊按鈕之後,跳至下一個頁面,頁面為單位相關資料


返回上一頁,並點擊餐廳地標,同樣的顯示地標標題按鈕,並可點擊


依據不同類型的地標,顯示不同的地標資料,目前餐廳資料只存有電話號碼



將地圖縮小可以很清楚的看到因為比例尺的不一樣,所有地標幾乎合在一起,但右上角的類別按鈕卻同樣的保持在螢幕的右上角。

Google Map ItemizedOverlay 使用

這個部分會來簡介一下如何使用 ItemizedOverlay 來達到放至地標在地圖上,並且點選地標可以顯示相關訊息的功能。

ItemizedOverlay

ItemizedOverlay 是繼承自 Overlay 的抽象物件,所以也必須要先實做,當實做 ItemizedOverlay 時,除了建構子外有兩個必須要覆寫的方法,一個是 creamItem() 另一個是 size() ,如果需要對地標做點擊的動作,那就需要再覆寫 onTap() 這個方法,首先來看一下完整的原始碼 (此處把 import 的部分刪除) :
public class exMap extends MapActivity {
/** Called when the activity is first created. */
GeoPoint mGeoPoint01,mGeoPoint02;
ArrayList<OverlayItem> items = new ArrayList<OverlayItem>();
Context mContext;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

MapView mMapView = (MapView)findViewById(R.id.mapview);
mMapView.setBuiltInZoomControls(true);
mMapView.setTraffic(false);
MapController mMapController = mMapView.getController();
mMapController.setZoom(17);
mGeoPoint01 = new GeoPoint(24121194,120675526);
mGeoPoint02 = new GeoPoint(24121190,120675520);
mMapController.animateTo(mGeoPoint01);


List<Overlay> list = mMapView.getOverlays();
MyOverlay mOverlay = new MyOverlay(getResources().getDrawable(R.drawable.icon),this);

OverlayItem mOverlayItem01 = new OverlayItem(mGeoPoint01,"這裡是地標01","點擊地標");
OverlayItem mOverlayItem02 = new OverlayItem(mGeoPoint02,"這裡是地標02","點擊地標");
mOverlay.addItem(mOverlayItem01);
mOverlay.addItem(mOverlayItem02);
list.add(mOverlay);
}

@Override
protected boolean isRouteDisplayed() {
// TODO Auto-generated method stub
return false;
}

class MyOverlay extends ItemizedOverlay<OverlayItem>{
public MyOverlay(Drawable defaultMarker,Context context) {
super(boundCenterBottom(defaultMarker));
mContext = context;
}

@Override
protected OverlayItem createItem(int i) {
// TODO Auto-generated method stub
return items.get(i);
}

@Override
public int size() {
// TODO Auto-generated method stub
return items.size();
}

@Override
protected boolean onTap(int index) {
OverlayItem item = items.get(index);
AlertDialog.Builder dialog = new AlertDialog.Builder(mContext);
dialog.setTitle(item.getTitle());
dialog.setMessage(item.getSnippet());
dialog.show();
return true;
}

public void addItem(OverlayItem item){
items.add(item);
populate();
}
}
}
先來看一下 ItemizedOverlay 在此處的執行流程 :
  1. 實做 ItemizedOverlay 類別,覆寫 createItem() 與 size() 兩個方法。
  2. 新增 addItem() 用來將地標加入
  3. 覆寫 onTap() 方法,可執行暗地標所需動作
  4. 在 MapActivity 中新增地圖
  5. 接著取得 MapView 中存放圖層的 List
  6. 建立 ItemizedOverlay 物件,並傳入地標圖示
  7. 建立地標點
  8. 將地標點加入至 ItemizedOverlay 物件中
  9. 將 ItemizedOverlay 物件加入至存放圖層的 List

建立 ItemizedOverlay 類別

這裡實做 ItemizedOverlay 需要傳入兩個參數,一個是地標的圖示另一個是要使用的 Context,這個 Context 是之後要用來顯示點選地標時所顯示的對話框,createItem() 在這裡雖然沒有直接被呼叫,但是在 addItem 中的 populate() 會去呼叫 createItem() ,所以當我們使用 addItem() 將地標加入圖層時,也同時可以利用 createItem() 做初始化的動作。當必須要使用到點選地標的功能時,則需要覆寫 onTap() 方法,這個方法可以讀入點選的地標號碼並取得 OverlayItem 物件,取得後即可作物件的相關操作。


建立與使用 ItemizedOverlay 物件
除了要建立 ItemizedOverlay 物件外,對於地標物件的建立也是必須的,基本的地標物件包含了 GeoPoint 物件、標題以及簡短內文;地標必須加入至圖層,圖層在繪製的時候才會有此地標,並且這個圖層也必須加入至 MapView 中存放圖層的 List 中,才會真正的被繪出。



實際執行畫面


兩個地標




點選中間第一個座標




點選左上角第二個座標

2011年7月26日 星期二

Google Map Overlay 使用

當使用 Map 時需要在地圖上使用一些座標圖示或是繪圖等功能,這時候就需要在原本的地圖上加上圖層,利用圖層就可以達成並且可以針對不同的圖層給予不同的功能,方便於管理與設計。

Overlay

Overlay是一個抽象類別,所以必須在使用前,先建立一個繼承自此抽象類別的自訂圖層類別,並將需求的功能實做,這裡用一個簡單的例子來說明如何使用圖層,範例功能將利用圖示顯示 animateTo(GeoPoint) 所設定的座標。

原始碼 :

package com.nchu.exMap;

import java.util.ArrayList;
import java.util.List;

import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapActivity;
import com.google.android.maps.MapController;
import com.google.android.maps.MapView;
import com.google.android.maps.Overlay;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.os.Bundle;

public class exMap extends MapActivity {
/** Called when the activity is first created. */
GeoPoint mGeoPoint;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

MapView mMapView = (MapView)findViewById(R.id.mapview);
mMapView.setBuiltInZoomControls(true);
mMapView.setTraffic(false);
MapController mMapController = mMapView.getController();
mMapController.setZoom(17);
mGeoPoint = new GeoPoint(24121194,120675526);
mMapController.animateTo(mGeoPoint);

MyOverlay mOverlay = new MyOverlay();
List<Overlay> list = mMapView.getOverlays();
list.add(mOverlay);
}

@Override
protected boolean isRouteDisplayed() {
// TODO Auto-generated method stub
return false;
}

class MyOverlay extends Overlay{
public void draw(Canvas canvas,MapView mapView,boolean shadow){
Paint mPaint = new Paint();
Point mScreenCoords = new Point();
mapView.getProjection().toPixels(mGeoPoint, mScreenCoords);
mPaint.setStrokeWidth(1);
mPaint.setColor(Color.RED);
mPaint.setStyle(Paint.Style.STROKE);
Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.drawable.icon);
canvas.drawBitmap(bmp, mScreenCoords.x, mScreenCoords.y,mPaint);
canvas.drawText("地圖中心點", mScreenCoords.x, mScreenCoords.y, mPaint);
}
}
}
首先來看一下 MyOverlay 這個類別,繼承自 Overlay 這個抽象類別,並實作 draw() 這個方法, Paint 為畫筆物件,利用此物件可以設定筆觸的大小或式風格等;而 Point 物件則是螢幕的點,接著利用 getProjection() 所取得的 projection 物件做經緯度與螢幕位置的轉換;我們所要貼的圖可以事先放入 Resource 中,再經由 BitmapFactory 的 decodeResource 取得,最後利用drawBitmap 或是 drawText 將圖或文字繪出。

接著來看如何使用 MyOverlay 類別,建立 MyOverlay 物件的方法跟一般物件建立方法沒什麼兩樣,但是建立出來的圖層必須加入到目前所使用的 MapView 中,不然的話就只是一個毫無用途的物件,首先我們必須取得 MapView 中存放 Overlay 的容器,放在這個容器內的 Overlay 才會被繪出,方法為
List<Overlay> list = mMapView.getOverlays();
要注意的是此處 getOverlays() 所回傳的是 List<Overlay< 所以無法使用 ArrayList 做為代替,最後只要將建立的 Overlay 物件嫁入至此容器中即可。

以下為執行畫面,可以看到中心點已經繪出紅色的 "地圖中心點" 五字,並且在下方貼了一張圖。

Android 上使用 Google Map API

當新建一個 Android 的專案時,可以選擇要使用的 API 版本,要注意的是一般的 Android API 版本並不包含 Google Map API,所以當需要使用到 Google Map API 時則需要選擇 Target name 為 Google APIs 的選項,並配合所需要的版本,使用前還必須申請一個 Android Map API Key。



申請 Android Map API Key

申請 Key 必須要有系統的證明書以及一個 Google 帳號,測試用的可以利用 Debug 的證書,不過未來如果需要發佈的話,則需要一份利用所要發佈的程式產生的 MD5 證書,並利用此證明書去申請 Key 之後才能發佈。
詳細申請步驟可以參考 Google 說明頁面 : Obtaining a Maps API Key

Debug 證書的路徑在 Eclipse 中選擇 Windows > Preference > Android > Build,其中的 Default debug keystore 的值就是 debug.keystore 的路徑。得知此路徑後就可以利用 keytool 指令取得MD5值,指令為 keytool -list -keystore debug.keysotre

接著利用剛剛取得的 MD5 碼,可以至 Google Map API key 申請頁面上申請 key,只要將取得的 MD5 碼填入並按下產生即可取得。



Google Map API 簡介

Google Map API 中有幾個重要也常用到的功能,分別為:
  • MapActivity : 一般來說 Android 程式的頁面都會繼承 Activity 類別,但是要使用 Google Map 的功能時,則必須繼承這個 MapActivity 類別,並且在 onCreate() 中實做一個 MapView 實例。
  • MapView : 用於顯示地圖的 View 元件。
  • MapController : 用來控制地圖的縮放移動等動作。
  • Overlay : 用來顯示地圖上可繪製的物件。
  • GeoPoint : 包含了經緯度的物件。


建立基本地圖

因為使用 Google Map API 必須使用到網路,所以必須在 AndroidManifest.xml 中增加網路存取的權限,並且須定義使用 com.google.android.maps 這個 package,如下原始碼。
<uses-library android:name="com.google.android.maps" />
因為顯示為 MapView ,所以可以在 main.xml 將這個 MapView 加入,並將取得的 key 填入,基本的 main.xml 如下 :
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/mainlayout"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<com.google.android.maps.MapView
android:id="@+id/mapview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:clickable="true"
android:apiKey="07MiAWWKO9Na0Aa8sf9hsd6uhZNXjrRwZU2zyGQ"
/>
</RelativeLayout>
或是直接利用程式碼建立 MapView。
MapView mMapView = new MapView(this, "API Key");
在建立 MapView之前,必須先建立起 MapActivity,再經由覆寫 MapActivity的 onCreat()方法建立MapView,之後就可以做一些地圖的設定,像是透過 setBuiltInZoomControls 設定地圖是否可以縮放,還有當地圖的顯示方式可以有三種,交通模式、衛星模式還有街景模式,分別為 setTraffic、setSatellite 以及 setStreeView 三個方法;最後再建立 MapController,而建立 MapController 物件可以利用下面方式產生 :
mMapController = mMapView.getController();
MapController 除了可以控制地圖縮放倍數外,也可以利用 animateTo(GeoPoint)的方法將地圖定位至 GeoPoint 的位置。

以下為完整範例原始碼 :

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.nchu.exMap"
android:versionCode="1"
android:versionName="1.0">
<uses-sdk android:minSdkVersion="10" />
<uses-permission android:name="android.permission.INTERNET" />

<application android:icon="@drawable/icon" android:label="@string/app_name">
<uses-library android:name="com.google.android.maps" />
<activity android:name=".exMap"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

</application>
</manifest>


main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<com.google.android.maps.MapView
android:id="@+id/mapview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:clickable="true"
android:apiKey="07MiAWWKO9Na0Aa8sf9hsd6uhZNXjrRwZU2zyGQ"
/>
</LinearLayout>


exMap.java

package com.nchu.exMap;

import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapActivity;
import com.google.android.maps.MapController;
import com.google.android.maps.MapView;

import android.os.Bundle;

public class exMap extends MapActivity {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

MapView mMapView = (MapView)findViewById(R.id.mapview);
mMapView.setBuiltInZoomControls(true);
mMapView.setTraffic(false);
MapController mMapController = mMapView.getController();
mMapController.setZoom(17);
GeoPoint mGeoPoint = new GeoPoint(24121194,120675526);
mMapController.animateTo(mGeoPoint);
}

@Override
protected boolean isRouteDisplayed() {
// TODO Auto-generated method stub
return false;
}
}



實際執行畫面


一般地圖



移動




縮放

2011年7月20日 星期三

搜尋Flickr上相關照片

本篇會針對如何使用 Flickr API 做相片搜尋的動作,以及如何在 Android 平台上取用這些資料。

簡介
簡單的來講 Flickr API 是一種 WEB API,呼叫的方式是透過 Flickr 提供的網址,並在網址後面接上所需的參數;要使用這些功能前必須先向 Flickr 申請一份 key ,除了申請的 key 之外 Flickr 還會給予一個密鑰,不過在這個搜尋的過程中我們並不會使用到這個密鑰,只會使用到 Flickr 所給予的 key。申請 key 前必須先擁有 Flickr 帳號。

API KEY 申請頁面

要使用 Flickr API 除了可以直接使用 Flickr 本身提供的方式之外,也可以利用第三方所開發的 framework 做使用,在 Flickr 頁面上有提供一些第三方工具連結,但官方是宣稱不保證使用的風險,除此之外這邊所用到的 API 並不多,所以還是直接利用 Flickr 所提供的方式。

Flickr 的要求格式分別為 REST、XML-RPC 與 SOAP 三種不同的方式,在這邊我們所使用的是 REST 方式,也就是剛剛提到的直接將所需參數接在網址後面;並且使用 REST 方式呼叫的話,預設也將以 REST 的格式回覆,但回覆的格式可以利用 format 參數做設定。

回覆的格式有五種 : REST、XML-RPC、SOAP、JSON 及 PHP。
此處使用的是 JSON 格式,一方面再使用 PHP 及 JavaScript 開發時較方便,另一方面也可以讓 PHP 程式收到的 JSON 格式資料,直接轉送至 Android 平台裝置上,而不需加做其他處理。

在這個搜尋照片並顯示在 Android 裝置上的程式規劃,將會是利用行動裝置發送需求至 AP server ,經由 AP server 向 Flickr 要求資料,並將接收到的回覆轉送回行動裝置。利用 AP server 而不直接使用行動裝置做要求的規畫,在於考慮到未來使用者大量的傳送要求時,我們可以針對 AP server 做必要的設定或更改,像是建立資料庫快取或是過濾等動作。




PHP
首先來看 PHP 方面的動作
<?php

$target_url = 'http://api.flickr.com/services/rest/?';
$data = array(
'method' => 'flickr.people.findByUsername' ,
'format' => 'json' ,
'nojsoncallback' => 1 ,
'api_key' => &#API KEY; ,
'username' => 'kuso.dir'
);
$target_url .= http_build_query( $data );
$user_id_JSONdata = startQuery($target_url);
$user_id_decode = json_decode($user_id_JSONdata,true);
$user_id = $user_id_decode[user][id];


$target_url = 'http://api.flickr.com/services/rest/?';
$data = array(
'method' => 'flickr.photos.search' ,
'format' => 'json' ,
'nojsoncallback' => 1 ,
'api_key' => 'API KEY; ,
'per_page' => 5 ,
'text' => 'nchu' ,
'user_id' => $user_id
);

$target_url .= http_build_query( $data );
$photos_data = startQuery($target_url);
print_r( $photos_data );
exit;

function startQuery($url){
$ch = curl_init();
curl_setopt( $ch , CURLOPT_URL , $url );
curl_setopt( $ch , CURLOPT_RETURNTRANSFER , true );
$d = curl_exec( $ch );
curl_close( $ch );
return $d;
}

?>
$target_url 為要求 API 的網址,$data 放的是需求的參數。

$data = array(
'method' => '要呼叫哪個 method' ,
'format' => '回覆格式設定' ,
'nojsoncallback' =&gt 設定1代表僅回覆原始JSON; ,
'api_key' => &#API KEY; ,
'username' => '使用者名稱'
);

我們呼叫的是 flickr.people.findByUsername 這支 method ,主要功能為搜尋符合的使用者名稱並回傳其 id 值,回覆的格式設定為 JSON 格式,並僅回覆最原始 JSON 資料。

startQuery() 是一個自訂函式,負責的功能為接收一個需要 query 的網址,並透過 curl 取得並回傳網頁資料;因為傳回的資料為 JSON 格式,所以可以利用 json_decode() 將之解碼為陣列,並取得使用者 id,接著可以利用此 id 搜尋此人擁有的照片。

依照同樣的方式,這次呼叫 flickr.photos.search 用來搜尋照片,這裡利用了 per_page 參數設定每頁有幾張圖,參數 text 及 user_id 分別為要搜尋的內文及使用者,最後將 Flickr 回傳的資料印出,以便 Android 抓取。




Android端
在取得 JSON 資料後首先要做的就是把需要的資料解析出來,所以必須要清楚這個回傳的資料格式長的如何,這裡先來看一下 flickr.photos.search 回傳的資料格式。

JSON回傳格式:
{
"photos":
{
"page":1,
"pages":3,
"perpage":5,
"total":"12",
"photo":
[
{"id":"5933477491", "owner":"15942951@N05",
"secret":"6db77e1e7d", "server":"6130",
"farm":7, "title":"\u4e2d\u8208\u9d5d",
"ispublic":1, "isfriend":0, "isfamily":0},

{"id":"5934040444", "owner":"15942951@N05",
"secret":"7cfb1ed29a", "server":"6141",
"farm":7, "title":"\u4e2d\u8208\u9d5d",
"ispublic":1, "isfriend":0, "isfamily":0},
]
},
"stat":"ok"
}


解析 JSON 時應注意 :
{} 為 Object
[] 為 Array

所以我們可以從下面的程式碼中解析出需要的資料,由於索取的資訊並不是照片本身,而是存放位置的相關訊息,所以我們必須再額外的做一些處理才能取得相片,而負責這些處理的功能就是程式碼中的 makeImg() 這個自定函式。

解析JSON原始碼:
jsonObj = new JSONObject(result);
photosObj = jsonObj.getJSONObject("photos");
page = photosObj.getString("page");
pages = photosObj.getString("pages");
perpage = photosObj.getString("perpage");
total = photosObj.getString("total");
photoArray = photosObj.getJSONArray("photo");
photoNum = photoArray.length();
for(int n=0;n<photoNum;n++){
photoObj = photoArray.getJSONObject(n);
id = photoObj.getString("id");
owner = photoObj.getString("owner");
secret = photoObj.getString("secret");
server = photoObj.getString("server");
farm = photoObj.getString("farm");
title = photoObj.getString("title");
ispublic = photoObj.getString("ispublic");
isfriend = photoObj.getString("isfriend");
isfamily = photoObj.getString("isfamily");
makeImg(farm,server,id,secret);
}


對於相片及網頁的存放位置, Flickr 也設定了一些格式,如下 :
相片 URL 格式 : 取得照片的地方
http://farm{farm-id}.static.flickr.com/{server-id}/{id}_{secret}.jpg
or
http://farm{farm-id}.static.flickr.com/{server-id}/{id}_{secret}_[mstzb].jpg
or
http://farm{farm-id}.static.flickr.com/{server-id}/{id}_{o-secret}_o.(jpg|gif|png)

後綴分類 : 所需取得的相片尺寸
s 小正方形 75x75
t 縮圖,最長邊為 100
m 小,最長邊為 240
- 中等,最長邊為 500
z 中等尺寸 640,最長邊為 640
b 大尺寸,最長邊為 1024*
o 原始圖片, 根據來源格式可以是 jpg、gif 或 png

網頁 URL : Flickr 上顯示相片的網頁,或是個人相關訊息的網頁。
http://www.flickr.com/people/{user-id}/ - profile
http://www.flickr.com/photos/{user-id}/ - photostream
http://www.flickr.com/photos/{user-id}/{photo-id} - individual photo
http://www.flickr.com/photos/{user-id}/sets/ - all photosets
http://www.flickr.com/photos/{user-id}/sets/{photoset-id} - single photoset

簡短 URL
http://flic.kr/p/{base58-photo-id}

如需詳細的說明請至 Flickr 說明頁面

由上面的格式可以知道如果要取得照片必須先得知一些資料,而這些資料也都包含在取回的 JSON 檔案中,分別是 farm-id、server-id、id 及 secret 這四個參數值,利用這四個參數並加上相片大小的綴詞即可組成相片存放的 URL;接著將 URL 的資料讀入並利用BitmapFactory.decodeStream() 這個函式將讀入的資料建構成一個 Bitmap ,一直到現在才真正取得相片主體,以下為部分原始碼。
public void makeImg(String farm,String server,String id,String secret){
String url = null;
url = "http://farm" + farm + ".static.flickr.com/" + server + "/" + id + "_" + secret + "_m.jpg";
getContentFromHttp(url);
Bitmap bm = BitmapFactory.decodeStream(is);
imgAdapter.addPhoto(bm);
}
getContentFromHttp() 為自訂函式,主要負責讀取網頁資料。
imgAdapter.addPhoto(bm) 為自訂函式,主要功能是將剛剛產生的圖片加入至 Gallery 做播放。

結尾
以上為 Android 與 Flickr 的應用簡介,Flickr API 提供了非常完整的訊息,包括了相片的授權方式或是群組類別等等,利用上面所介紹的方法可以簡單的取得需要的訊息,並加以利用。下面提供兩張由 Android 所抓下的實機操作圖,當作本文的結尾,畫面上方顯示著一列可左右拖移的圖片列表,當點擊有興趣的圖片時,將會在畫面下方顯示完整的大圖。

圖片為本人於中興大學所拍攝的鵝照片,用來測試帳號、標籤及內文搜索功能。

2011年7月14日 星期四

關於Drupal新增content時的資料表變動

目前後台的部分是使用 Drupal 作開發,而行動裝置抓取資料必需直接向資料庫要求,所以對於 Drupal 新增 content 時相關的資料表做了一些觀察。

觀察的環境為 TWAMP 的安裝包,使用版本為 6.22 版,並且安裝了 CCK 及 Views 這兩個基本的模組,所以資料庫的結構上就是依照這樣的安裝所產生的,總共的資料表數量為61個,觀察的資料表為 content 以及 node 這兩個字串為開頭的資料表。



  • 新增 content 類型
首先來看,新增一個 content 類型後,在 node_type 中會新增一筆資料,這筆資料代表的就是剛剛新增的 content 類型,並且此類型的相關設定都會存放在這。



  • 新增 field
接著在此 content 類型中新增一個 field ,新增完這個 field 後,資料庫中會新增一個資料表,資料表的名稱為 content_type_"ContentType名稱" ,而裡面暫時不會有值,至於會有哪些欄位就視不同的 field type 而不同,最基本的欄位會有 nid 及 vid 。

譬如新增的 field type 是 Node reference 的話,那會多一個 field_"連結的node名稱"_nid 的欄位,記錄著連結到哪個 node,而有些 type 則會在未來新增內容時才做動作。

除此之外 content_node_field 及 content_node_field_instance 這兩個資料表,也會新增一筆與 field 設定相關的紀錄,而這兩個的差別在於 content_node_field 像是物件導向概念裡的類別,而 content_node_field_instance 則是經由類別創建出的實例,content_node_field 紀錄的是一般的設定,此設定可套用到其他的 content 類別,content_node_field_instance 則是與某個 content 類別綁住的獨有的設定。



  • 新增 content 內容
接下來新增一個 content 內容,類型為剛剛所創建的 content 類型,在資料庫中會在 node 資料表中寫入一筆記錄,包含了創建的相關訊息,但不包括創建內容;node_comment_statistics 資料表中會有一筆統計紀錄;node_revisions 資料表會存放未來如果有變更的資訊。而實際內容會放在以 content 字串開頭的資料表中,像是前面提到的 Node reference 類型的 field 則會將內容資料放在 content_type_"ContentType名稱" 中,而如果是 text 類型的 field 則會 新增一個專門存放內容的資料表,資料表名稱像是 content_field_name。


以上是我對於新增 content 的流程所做的觀察,但這部分會因為不同的模組及設定而有所不同,不過如果只是要從 Drupal 所建立的資料庫中尋找到自己所需的資料來說,相信已經足夠,由上面可看出,實際內容存放的地方以 content 開頭的資料表為最主要,尤其是 content_type 和 content_field,接下來與建立內容相關的統計資料為 node_comment_statis 資料表,並且可以利用 nid 或是 vid 的值去比對出所需資料。

2011年7月12日 星期二

版本控制

未來的 Campus 的開發會利用 SVN 作版本控制,目前暫時先將 SVN Server 架在自己的機器上,以後如果有架設開發主機再將這些資料轉移過去。

SVN Server 軟體使用的是 VirsualSVN SERVER Standard Edition 安裝快速使用上也很方便,Client端的軟體則使用 TortoiseSVN 做檔案總管的整合,並且在 Eclipse 上安裝了 Subclipse 插件方便專案的提交與更新等動作。

在這個環境中使用 Eclipse 開發 Android 專案,可以很方便的利用 SVN 做版本控制外,也可以利用檔案總管直間觀察是否有未更新檔,或是直接更新其他未使用 Eclipse 開發的程式或文件,像是 PHP、HTML 或是 PDF 等檔案。

這裡需要注意的是 Eclipse 裡面的設定,記得在專案的 Build Path 中設定,將 .svn 檔案排除在 Build 之外,這樣做是為了避免 Android 把版本控制的檔案當成是專案的一部分,而嘗試進行編譯的動作,導致錯誤無法執行。

實際的設定位置在 Project > Properties > Java Build Path,點選 ****/src 裡的 Excluded ,並且將 **/.svn/** 字串加入至 Excluded(字串包含 * ),如此就可以將.svn檔案排除在外。

2011年7月7日 星期四

Android 利用 PHP 連結 MySQL

網路上有許多連結資料庫的方法,像是利用JDBC直接連結資料庫或是透過 web server 代為取得資料,在這裡提到的是後者,下圖為實際的操作環境。



本篇所使用的環境套件為 Mysql、Apache、PHP,在此除了利用PHP向DB要資料外並將取回的資料轉成 JSON 格式,接著利用 HTTP 協定傳遞給 Android 程式,接下來將詳細介紹中間的執行步驟及該注意的事項。

首先我們來看一下 PHP 如何向 Mysql 取得資料:

<?php
mysql_connect("資料庫位置","使用者名稱","使用者密碼");
mysql_select_db("資料庫名稱");
mysql_query("set names utf8");
$q=mysql_query("SELECT phone_value from news where id='".$_POST['id']."'");
while($e=mysql_fetch_assoc($q))
$output[]=$e;
print(json_encode($output));
mysql_close();
?>

mysql_connect : 建立與mysql的連線。
mysql_select_db : 選擇要使用哪一個資料庫。
mysql_query : 設定語系,避免取回的文字呈現亂碼,並且設定 SQL 語法。
mysql_fetch_assoc : 利用 SQL 語法取回要求的資料。
json_encode : 利用 json 編碼將取回的資料轉換成 json 格式。
mysql_close : 最後記得要將 mysql 的連線關閉。

當然在檔案格式的選擇方面並不一定只能使用 JSON ,例如使用 XML 或是要自訂格式也都可以,差別在於使用 JSON 或是 XML 的格式,在Android端我們不需要再自己寫parser,並且Android的開發套件裡已經包含了許多工具可以直接使用。

接下來我們來看一下 Android 端該如何的發送參數與接收資訊;此處我們使用的是HTTP協定並利用POST的方式將參數傳至PHP網頁,並取得PHP傳回的資訊,最後將取得的JSON檔案解析出來使用,所以在 Android 端我們將分成三個部分來處理。

第一個部分主要的功能在於使用HTTP傳遞參數並取得資料:
原始碼如下:

1. HttpClient httpclient = new DefaultHttpClient();
2. HttpPost httppost = new HttpPost("php網址");
3. ArrayList<NameValuePair> nameValuePairs =
new ArrayList<NameValuePair>();
4. nameValuePairs.add(new BasicNameValuePair("參數名","參數值"));
5. httppost.setEntity(new UrlEncodedFormEntity(nameValuePairs));
6. HttpResponse response = httpclient.execute(httppost);
7. HttpEntity entity = response.getEntity();
8. is = entity.getContent();


第1、2行 : 使用的是Apache的功能,首先建立一個HttpClient的實體,並且使用Post的方式
傳遞參數。
第3、4行 : 建立了一個存放NameValuePair型別的ArrayList容器,並且將之後要POST的參數名和
參數值放入容器中。
第5、6行 : 實際送出請求,並取得傳回狀態等資訊。
第7、8行 : 取得收到的內容。


接下來將剛剛所取得的Content利用StringBuilder轉換為字串

BufferedReader reader = new BufferedReader(
new InputStreamReader(is, "iso-8859-1"),8
);
StringBuilder sb = new StringBuilder();
String line = null;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
is.close();
result = sb.toString();


最後取得每個JSON物件與值,先利用JSONArray.getJSONObject取得需要的JSON物件,之後再利用此物件的getString或getInt等方法取得相對應的值。

JSONArray jArray = new JSONArray(result);
while(int i = 0;i<jArray.length();i++){
JSONObject json_data = jArray.getJSONObject(i);
data1 = json_data.getString("鍵值名稱");
data2 = json_data.getInt("鍵值名稱");
data3 = json_data.getDouble("鍵值名稱");
}


以上為利用PHP取得MySQL資料的大略流程,中間還有許多可以做變化,像是最後取值的部分是不是一定要全部取?還是當有需要時再取所需的部分?這些都可以做一些規劃,讓整體系統更有擴充性或是賦予物件該負的責任。

在MAP的開發過程中,這個連線與取值的部分我是獨立成一個類別,類別中所需要POST的參數與值經由實際創立物件時再另外做設定,要對哪支PHP做呼叫甚至取回哪些值都是等到物件建立後再依照需求作建立,這樣作雖然在建立物件時會多了許多需要設定的步驟,但相對的可以將PHP頁面與Android程式做適當的分割,以減少其耦合度,除此之外這支類別的責任也就很清楚的定義為Android與PHP兩大單位的中間人,未來兩邊有變異,將可以只修改這個類別作對應動作。

Twitter Delicious Facebook Digg Stumbleupon Favorites More

 
Powered by Blogger