Posts match “ android ” tag:

需求

今天要來講Android如何定位取得目前位置後,並且在Google Map上顯示,這是一個看似蠻簡單的範例,但是定位在StackOverflow上面一堆寫法,看了眼花撩亂,這邊是我最後可以完美運作的版本。

思路

  • Google Map如何導入在專案內就請自行參考Google Map Android API - Get Started
  • 手機的GPS和網路皆可以定位,有時候使用者不一定會開啟GPS,所以我們採用兩個定位方式依序取得位置。
  • 在取得定位並且設定好Google Map後,我們就可以把目前位置顯示在地圖上了,就這麼簡單而已。

你也許會問,為何我需要使用GPS和網路定位?
原因有三個:

  1. 使用者不一定會開GPS或網路。
  2. GPS跟Network的精準度也不一。
  3. 兩者取得位置的速度兩者也不同。

程式碼實作

  1. res/layout/activity_main.xml加入MapView

    <com.google.android.gms.maps.MapView
        android:id="@+id/mapview"
        android:layout_height="match_parent"
        android:layout_width="match_parent"
        map:cameraZoom="13"
        />
    
  2. Activity新增兩個Instance Variable,並且在onCreate()初始化Google Map。

@Bind(R.id.mapview)
MapView mMapView;
private GoogleMap mGoogleMap;

@Override
protected void onCreate(Bundle savedInstanceState) {
    /* 其他既有程式略 */
    ButterKnife.bind(this); /* 使用ButterKnife做View Injection */
    mMapView.onCreate(savedInstanceState); /* 初始化MapView */
    mGoogleMap = mMapView.getMap();
    int googlePlayStatus = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
    if (googlePlayStatus != ConnectionResult.SUCCESS) {
        GooglePlayServicesUtil.getErrorDialog(googlePlayStatus, this, -1).show();
        finish();
    } else {
        /* 設定地圖 */
        if (mGoogleMap != null) {
            mGoogleMap.setMyLocationEnabled(true); 
            mGoogleMap.getUiSettings().setMyLocationButtonEnabled(true);
            mGoogleMap.getUiSettings().setAllGesturesEnabled(true);
        }
    }
}
  1. 新增LocationListener的Instance Variable,目的用來監聽取得更新位置後的動作。
private LocationListener mLocationListener = new LocationListener() {
    @Override
    public void onLocationChanged(Location location) {
        if (location != null) {
            Logger.d(String.format("%f, %f", location.getLatitude(), location.getLongitude()));
            drawMarker(location);
            mLocationManager.removeUpdates(mLocationListener);
        } else {
            Logger.d("Location is null");
        }
    }

    @Override
    public void onStatusChanged(String s, int i, Bundle bundle) {
    }

    @Override
    public void onProviderEnabled(String s) {
    }
  
    @Override
    public void onProviderDisabled(String s) {
    }
};
  1. 重頭戲來了,取得目前位置的實作,我們由網路和GPS來取得定位,因為GPS精準度比網路來的更好,所以先使用網路定位、後續再用GPS定位,如果兩者皆無開啟,則跳無法定位的錯誤訊息。
private void getCurrentLocation() {
    boolean isGPSEnabled = mLocationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
    boolean isNetworkEnabled = mLocationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);

    Location location = null;
    if (!(isGPSEnabled || isNetworkEnabled))
        Snackbar.make(mMapView, R.string.error_location_provider, Snackbar.LENGTH_INDEFINITE).show();
    else {
        if (isNetworkEnabled) {
            mLocationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,
                    LOCATION_UPDATE_MIN_TIME, LOCATION_UPDATE_MIN_DISTANCE, mLocationListener);
            location = mLocationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
        }

        if (isGPSEnabled) {
            mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,
                    LOCATION_UPDATE_MIN_TIME, LOCATION_UPDATE_MIN_DISTANCE, mLocationListener);
            location = mLocationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
        }
    }
    if (location != null)
        drawMarker(location);
}

/* 在Google Map上放上目前位置的地標圖示。 */
private void drawMarker(Location location) {
    if (mGoogleMap != null) {
        mGoogleMap.clear();
        LatLng gps = new LatLng(location.getLatitude(), location.getLongitude());
        mGoogleMap.addMarker(new MarkerOptions()
                .position(gps)
                .title("Current Position"));
        mGoogleMap.animateCamera(CameraUpdateFactory.newLatLngZoom(gps, 12));
    }
}

最後完整程式碼大家可以自行取用,謝謝各位。
我是大白,有任何問題歡迎在FB訊息敲來問我,請大家多多支持。

學生時期的一些程式作品集,整理一下拿出來獻醜 >///<(以下圖多!!)。
主要是用Java寫的,當然還有含Android和Python,而應用主要有資料庫、網路爬蟲、Socket Programming、機器學習應用、演算法設計...等等項目。

以下是簡單列表(照完成時間排序):

  • Gmail Man - Gmail客戶端,可以跟本機端資料庫做同步更新。
  • 餐飲熱量計算程式 - 學校專題(補)飲食熱量資料庫應用。
  • 股市交易資料爬蟲 - 網路爬蟲可以抓每日股市的交易資料。
  • 圖片下載程式 - 輸入關鍵字,可以自動下載搜尋到的所有圖片。
  • 樂透網路下注程式 -- 幫學弟寫的一個網路程式設計作業,半個小時內寫完。
  • 地址顯示服務 - 輸入一個網址,提取一個網頁正文並分析出地址,然後把所有地址顯示在網頁上。
  • 在地遊App - 一支可以查詢地點資訊、顯示照片和地圖的App。
  • 地點關鍵字提取引擎 - 利用文字分析技術自動提取出一個地點的關鍵字列表。
  • 地點自動分類器 - 實作一個分類演算法來將地點自動分類為景點、餐飲和住宿。

Gmail Man

這是校內「雲端程式設計課程」的作業之一,題目是用抽的,我抽中用Gmail Contact API去寫一個應用程式,我就簡單做一個Gmail的客戶端。

功能

Gmail的客戶端,可以直接編輯通訊錄的資料,並且可以跟本機端的資料庫做同步備份。

所用技術


餐飲熱量計算程式

這程式是我的學校畢業專題,因為我大四休學,所以原本一年的專題中斷(做臉部器官偵測,是影像處理的應用),後來復學後就做這個簡單的應用當作畢業專題。

功能

程式提供各種餐飲的熱量資訊,使用者可以點選三餐所吃的餐點即可立刻計算當天攝取的熱量總和;或者可以輸入熱量和一些限制(例如:低脂、素食...等),程式會找出符合的餐飲。。

  • 主畫面1:使用者可以點選他今天三餐吃的餐點,程式就會幫忙計算今天攝取的總熱量是多少。 輸入熱量值和選擇限制,程式會幫你挑出符合的餐飲讓使用者選。

  • 主畫面2:可以直接檢視每個餐飲在資料庫的資料,必要的時候可以在程式登入資料庫然後編輯資料。

  • 主畫面3:或者更直接可以執行SQL指令和設定過濾器來做查詢資料。

所用技術


股市交易資料爬蟲

功能

這是一隻網路爬蟲,用來從網頁上抓每天股票買賣超的交易資料,並且把當日的前十名買賣超的公司+張數做成圖表顯示出來。

所用技術


圖片下載程式

功能

輸入關鍵字(例如:正妹),程式會自動幫你下載所有符合的圖片(使用Google搜尋結果)。

所用技術


樂透網路下注程式

幫學弟寫的作業程式,30分鐘搞定。

這是一個Network Socket Program,Server端可以等待多個Client來連線下單,然後接著時間截止後,Server端會停止繼續下注,並且把中獎號碼送到Client端,如果Client端接受後有中獎,則會顯示。

所用技術


地址顯示服務

功能

此網頁服務允許使用者輸入一個網頁,後端會分析出網頁正文後並且使用文字分析提取出所有地址,接著把地址標註在地圖上。 此服務主要目的在於當我們瀏覽一個美食部落格時,該文章當中可能列舉一個區域的所有美食,我們就可以在地圖上一次瀏覽所有美食店家的位置。

所用技術


在地遊App

功能

此App可以用來查詢和編輯地點資料,顯示該地點的照片和拍新照片並上傳,顯示地圖位置。

所用技術


地點關鍵字提取引擎

功能

此文字探勘引擎使用字詞加權技術,以每個地點的部落格文章作為分析來源,找出屬於該地點的關鍵字並且計算出該字詞和該地點的相關程度。如下圖所示,使用了四種不同的字詞加權計算公式來計算,我們可以比較不同的公式所算出來的精準度並且衡量計算速度。

所用技術


地點自動分類器

功能

此程式使用KNN分類演算法來將地點以它所有的部落格作為分析來源來自動分類為景點、餐飲和住宿。

所用技術

  • KNN分類演算法
  • KNN分類演算法加速器

需求

  • 當我們有大量的圖片,需要呈現圖片列表給使用者,讓使用者可以上下滑動檢視圖片。
  • 每支App都有配置一定的記憶體使用量,如何高效地載入和呈現圖片是重要關鍵。
  • 最後成果類似下圖,此圖為我自己公司內部的App截圖:

思路

  • 這功能的實作思路不困難,用一個GridView元件當作Gallery,加入Adapter當作圖片來源,然後當使用者上下滑動時,可以將圖片一張一張載入顯示,這些圖片可以是從網路下載、手機儲存或是拍照而得來的。
  • 避免OOM:隨著使用者不斷往下滑、載入的圖檔越來越多,就會有一個非常重要的問題(必)會發生,那就是圖片佔用記憶體超過App本身所配置到的記憶體大小,產生OutOfMemoryError的錯誤例外(OOM),使得App最終crash掉。為了避免此現象,我們加入Cache的觀念,在實作時就使用Android所提供的LruCache類別,這個類別是在Android 3.1.x (API 12)之後才提供的,如果是在更早的Android版本開發,則需要import android-support-v4.jar
  • 要呈現的圖片可能非常多,使用者可能不會看完所有圖片,所以我們採用「看得到多少數量的圖片,才載入那樣數量的圖片」來增加效能,因此,我們必須偵測使用者目前滑動到的區域、以及該區域可以呈現多少圖片。 程式碼中讓Adapter實作OnScrollListener介面,然後override onScrollStateChanged() onScroll()兩個方法。

程式碼實作

以下是使用Android 4.0 API開發。

首先先建立Gallery的Layout檔 activity_gallery.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">
    <GridView
      android:id="@+id/gridPhoto"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:columnWidth="150dp"
      android:stretchMode="columnWidth"
      android:numColumns="auto_fit"
      android:verticalSpacing="3dp"
      android:layout_marginBottom="3dp">
    </GridView>
</LinearLayout>

接下來我們定義GridView內每一張圖片的Layout檔 photo_view.xml,我們就使用ImageView元件來顯示一張圖。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" >
    <!-- 放置一張圖片View -->
    <ImageView
      android:id="@+id/photo"
      android:contentDescription="@string/app_name"
      android:layout_width="150dp"
      android:layout_height="150dp"
      android:src="@drawable/ic_launcher"
      android:layout_centerInParent="true"/>
</RelativeLayout>

再來,最重要GridView的Adapter PhotoGalleryAdapter.java

public class PhotoGalleryAdapter extends ArrayAdapter<String> implements OnScrollListener
{
    private static final int PHOTO_WIDTH = 120;
    private static final int PHOTO_HEIGHT = 120;
    
    /**
     * 紀錄所有正在下載圖片或等待的task.
     */
    private Set<BitmapDownloadTask> taskCollection;

    /**
     * 用來做圖片Caching
     */
    private LruCache<String, Bitmap> memoryCache;

    /**
     * Gallery View
     */
    private GridView gridViewPhoto;

    /**
     * 第一章可見圖片的索引.
     */
    private int firstVisiblePhoto;

    /**
     * 圖片位址或路徑列表。
     */
    private List<String> photoURLs;

    /**
     * 螢幕一次可以看到多少張圖片.
     */
    private int visiblePhotoCount;

    /**
     * 紀錄是否是剛進入此程式,用來解決程式不滑動螢幕,不會下載圖片的問題.
     */
    private boolean isFirstEnter = true;

    /**
     * 
     * @param context
     * @param objects 圖片來源列表
     * @param photoWall GridView元件
     */
    public PhotoGalleryAdapter( Context context, List<String> objects, GridView photoWall )
    {
        super( context, 0, objects );
        this.photoURLs = objects;
        this.gridViewPhoto = photoWall;
        this.taskCollection = new HashSet<BitmapDownloadTask>();

        // 取得app可用的最大記憶體

        int maxMemory = (int) Runtime.getRuntime().maxMemory();

        // 圖片暫存大小為app可用最大記憶體的1/8

        int cacheSize = maxMemory / 8;

        this.memoryCache = new LruCache<String, Bitmap>( cacheSize )
        {
            @Override
            protected int sizeOf( String key, Bitmap bitmap )
            {
                return bitmap.getByteCount();
            }
        };
        this.gridViewPhoto.setOnScrollListener( this );
    }

    /**
     * To use something other than TextViews for the array display, for instance, ImageViews,
     * or to have some of data besides toString() results fill the views, override getView(int, View, ViewGroup)
     * to return the type of view you want.
     */
    @Override
    public View getView( int position, View convertView, ViewGroup parent )
    {
        final String url = this.getItem( position );
        View view;
        if ( convertView == null )
            view = LayoutInflater.from( this.getContext() ).inflate( R.layout.photo_view, null );
        else
            // 從cache載入的圖

            view = convertView;

        ImageView photoView = (ImageView) view.findViewById( R.id.photo );

        // 給圖片設置一個tag已保證在做非同步載入圖片時順序不會亂掉

        photoView.setTag( url );
        setImageView( url, photoView );
        return view;
    }

    /**
     * 把圖片做caching
     * 
     * @param key LruCache的鍵,這裡是圖片的URL位址
     * @param bitmap
     */
    public void addBitmapToMemoryCache( String key, Bitmap bitmap )
    {
        if ( this.getBitmapFromMemoryCache( key ) == null )
            this.memoryCache.put( key, bitmap );
    }

    /**
     * 從LruCache中取回圖片
     * 
     * @param key 用圖片URL位址從cache中取回圖片
     * @return 圖片Bitmap或null
     */
    public Bitmap getBitmapFromMemoryCache( String key )
    {
        Bitmap bitmap = this.memoryCache.get( key );
        return bitmap;
    }

    /**
     * 當GridView靜止時才下載圖片,如果正在滑動,則取消下載工作
     */
    @Override
    public void onScrollStateChanged( AbsListView view, int scrollState )
    {
        if ( scrollState == SCROLL_STATE_IDLE )
            loadBitmaps( this.firstVisiblePhoto, this.visiblePhotoCount );
        else
            cancelAllTasks();
    }

    /*
     * (non-Javadoc)
     * 
     * @see android.widget.AbsListView.OnScrollListener#onScroll(android.widget.AbsListView, int, int, int)
     */
    @Override
    public void onScroll( AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount )
    {
        this.firstVisiblePhoto = firstVisibleItem;
        this.visiblePhotoCount = visibleItemCount;

        // 下載圖片的工作是由onScrollStateChanged()裡面啟動

        // 但第一次進入GridView時,onScrollStateChanged()不會被呼叫到所以要進行第一次進入GridView下載圖片的動作。

        if ( isFirstEnter && visibleItemCount > 0 )
        {
            loadBitmaps( firstVisibleItem, visibleItemCount );
            isFirstEnter = false;
        }
    }

    /**
     * 載入Bitmap物件,這方法會在暫存中檢查所有螢幕可見的圖片物件,如果可見但不在暫存中,則啟動非同步載入圖片工作。
     * 
     * @param firstVisiblePhoto 第一個可見圖片的位置索引
     * @param visiblePhotoCount 螢幕可見圖片的數量
     */
    private void loadBitmaps( int firstVisiblePhoto, int visiblePhotoCount )
    {
        for ( int i = firstVisiblePhoto; i < firstVisiblePhoto + visiblePhotoCount; i++ )
        {
            String imageUrl = this.photoURLs.get( i );
            System.out.println( "Load " + imageUrl );
            Bitmap bitmap = this.getBitmapFromMemoryCache( imageUrl );
            ImageView imageView = (ImageView) gridViewPhoto.findViewWithTag( imageUrl );
            if ( bitmap == null )
            {
                // 從遠端伺服器來

                if ( imageUrl.startsWith( "http" ) )
                {
                    BitmapDownloadTask bitmapTask = new BitmapDownloadTask();
                    this.taskCollection.add( bitmapTask );
                    bitmapTask.execute( imageUrl );
                }
                else
                // 從檔案系統來

                {
                    bitmap = loadBitmapFromFile( imageUrl, PHOTO_WIDTH, PHOTO_HEIGHT );
                    if ( bitmap != null )
                        this.addBitmapToMemoryCache( imageUrl, bitmap );
                    
                    if ( imageView != null && bitmap != null )
                        imageView.setImageBitmap( bitmap );
                }
            }
            else
            {
                if ( imageView != null && bitmap != null )
                    imageView.setImageBitmap( bitmap );
            }
        }
    }

    /**
     * 給ImageView設置圖片,這方法會去找暫存,如果沒有暫存圖,則直接先給定一張預設圖。
     * 
     * @param imageUrl
     * @param imageView
     */
    private void setImageView( String imageUrl, ImageView imageView )
    {
        Bitmap bitmap = getBitmapFromMemoryCache( imageUrl );
        if ( bitmap != null )
            imageView.setImageBitmap( bitmap );
        else
            imageView.setImageResource( R.drawable.ic_launcher );
    }

    /**
     * 取消所有下載中和等待中的圖片下載工作。
     */
    public void cancelAllTasks()
    {
        if ( this.taskCollection != null )
            for ( BitmapDownloadTask task : this.taskCollection )
                task.cancel( true );
    }

    /**
     * 圖片非同步下載的工作執行續
     * 
     * @author 白昌永, Engine Bai @ infinitibeat, styletrip
     * @version
     */
    class BitmapDownloadTask extends AsyncTask<String, Void, Bitmap>
    {
        private String bitmapUrl;

        @Override
        protected Bitmap doInBackground( String... params )
        {
            this.bitmapUrl = params[ 0 ];
            Bitmap bitmap = null;
            try
            {
                bitmap = downloadBitmap( this.bitmapUrl, PHOTO_WIDTH, PHOTO_HEIGHT );
            }
            catch ( IOException e )
            {
                e.printStackTrace();
            }

            // 圖片成功下載後,直接先加到暫存中

            if ( bitmap != null )
                addBitmapToMemoryCache( this.bitmapUrl, bitmap );
            return bitmap;
        }

        @Override
        protected void onPostExecute( Bitmap bitmap )
        {
            // 根據Tag找到對應的ImageView元件,把剛剛下載的圖片呈現出來

            ImageView imageView = (ImageView) gridViewPhoto.findViewWithTag( this.bitmapUrl );
            if ( imageView != null && bitmap != null )
                imageView.setImageBitmap( bitmap );
            taskCollection.remove( this );
        }
    }

    /**
     * 下載Bitmap。
     * 
     * @param bitmapUrl
     * @param width Bitmap要呈現的寬度
     * @param height Bitmap要呈現的高度
     * @return
     * @throws IOException
     */
    public static Bitmap downloadBitmap( String bitmapUrl, int width, int height ) throws IOException
    {
        Bitmap bitmap = null;
        
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        InputStream in = getInputStreamFromURL( bitmapUrl );
        try
        {
            bitmap = BitmapFactory.decodeStream( in, null, options );
        }
        finally
        {
            if ( in != null )
                in.close();
        }
        
        options.inSampleSize = calculateImageSampleSize( options, width, height );
//     System.out.printf("%d, %d, %s, %d\n", imageHeight, imageWidth, imageType, options.inSampleSize );

        options.inJustDecodeBounds = false;
        
        in = getInputStreamFromURL( bitmapUrl );
        try
        {
            bitmap = BitmapFactory.decodeStream( in, null, options );
        }
        finally
        {
            if ( in != null )
                in.close();
        }
        return bitmap;
    }

    /**
     * 從手機儲存載入圖片。
     * 
     * @param photoPath
     * @param width  Bitmap要呈現的寬度
     * @param height Bitmap要呈現的高度
     * @return
     */
    public static Bitmap loadBitmapFromFile( String photoPath, int width, int height )
    {
        BitmapFactory.Options options = new BitmapFactory.Options();
        Bitmap bitmap = null;
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile( photoPath, options );
        options.inSampleSize = calculateImageSampleSize( options, width, height );
        options.inJustDecodeBounds = false;
        bitmap = BitmapFactory.decodeFile( photoPath, options );
        return bitmap;
    }

    /**
     * 從URL建立連線後取得InputStream
     * 
     * @param URL
     * @return
     * @throws IOException
     */
    public static InputStream getInputStreamFromURL( final String URL ) throws IOException
    {
        HttpURLConnection conn = null;
        URL url = new URL( URL );
        conn = (HttpURLConnection) url.openConnection();
        conn.setConnectTimeout( 5 * 1000 );
        conn.setReadTimeout( 10 * 1000 );
        conn.setDoInput( true );
        conn.setDoOutput( true );
        return conn.getInputStream();
    }

    /**
     * 依照要顯示的長寬來計算Bitmap要sample的比例
     * 
     * @param options
     * @param reqWidth 需要的寬度
     * @param reqHeight 需要的高度
     * @return
     */
    public static int calculateImageSampleSize( BitmapFactory.Options options, int reqWidth, int reqHeight )
    {
        final int rawH = options.outHeight;
        final int rawW = options.outWidth;
        int imageSampleSize = 1;
        
        if ( rawH > reqHeight || rawW > reqWidth )
        {
            final int halfH = rawH / 2;
            final int halfW = rawW / 2;
            
            while ( ( ( halfH / imageSampleSize ) > reqHeight ) && ( ( halfW / imageSampleSize ) > reqWidth ) )
                imageSampleSize *= 2;
        }
        return imageSampleSize;
    }
}

最後實作呈現GridViewGalleryActivity.java

/**
 * @author 白昌永, Engine Bai @ infinitibeat, styletrip
 * @version
 */
public class GalleryActivity extends Activity
{
    private static final String[] IMAGES = { "http://photo1", "http://photo2" };
    
    /**
     * 用來展示圖片的Gallery
     */
    private GridView photoGallery;

    /**
     * GridView所使用的Adapter
     */
    private PhotoGalleryAdapter adapter;

    @Override
    protected void onCreate( Bundle savedInstanceState )
    {
        super.onCreate( savedInstanceState );
        setContentView( R.layout.activity_gallery );
        this.photoGallery = (GridView) findViewById( R.id.gridView );
        
        // TODO 這裡的IMAGES可以換成任何來源,不論是遠端伺服器查詢、或者是手機儲存

        this.adapter = new PhotoGalleryAdapter( this, Arrays.asList( IMAGES ), photoGallery );
        this.photoGallery.setAdapter( adapter );
    }

    @Override
    protected void onDestroy()
    {
        super.onDestroy();
        // 結束Activity時同時也取消所有圖片下載的工作

        this.adapter.cancelAllTasks();
    }

}

因為我們這個範例是從網路上下載圖片,使用到網路功能,所以需要在AndroidManifest.xml加入使用網路的權限。

<manfiest>
    <application>
    ... 略
    </application>
    
  <uses-permission android:name="android.permission.INTERNET" />
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>

實作說明

  • PhotoGalleryAdapter初始化LruCache類別,設置App配置最大可用記憶體容量的8分之一作為最大暫存容量,因為我們使用了LruCache類別來暫存圖片,所以不需要擔心會發生OOM現象發生,因為LruCache暫存圖片的總容量到達暫存上限時,自動會把最近最少顯示的圖片從暫存中移除。
  • 圖片下載是在實作onScrollStateChanged()方法進行,程式偵測使用者是否進行螢幕滑動,在螢幕靜止時才進行下載圖片,偵測目前可看見幾個ImageView數量,去載入多少圖片(從網路、手機暫存下載或是直接從暫存取回),圖片載入後也同時加入到暫存。當使用者滑動螢幕時,則取消所有正在進行的圖片載入工作,這樣可以提昇GridView滑動的流暢性。
  • Overriden的getView()初始化GridView顯示每一張圖片的ImageView,同時比較重要的是,我們為每一個ImageView設定了一個Tag(這邊我們可以用圖片的遠端伺服器URL或是在手機儲存上的路徑當作Tag),這個Tag可以讓我們正確的找到顯示的ImageView,在進行下載圖片的工作時,可以依照Tag來找到對應的ImageView元件,如此一來不會發生圖片顯示錯亂的問題。

參考連結

Caching Bitmaps http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html
GridView元件
LruCache類別

今天來講Android如何使用Toolbar並且加入Navigation Drawer,首先Navigation Drawer可以直接在Android Studio(以下簡稱AS)新建專案的時候,選擇 Navigation Drawer Activity範例來建立新專案。建立完成之後,AS會自動幫你產生下列檔案:

res/layout資料夾

  • activity_main.xml
  • fragment_main.xml
  • fragment_navigation_drawer.xml

java/package-name資料夾(package-name是你專案的package名稱)

  • MainActivity.java
  • NavigationDrawerFragment.java

接下來要開始修改這些檔案,加入Toolbar而且把Navigation Drawer改成Material Design,讓我們開始吧~


新增Toolbar Layout檔案

我們從Toolbar開始,先在build.gradle裡面加入

compile 'com.android.support:appcompat-v7:22.2.0'

讓gradle同步完後,建立新的layout檔案叫做toolbar.xml,裡面加入

<android.support.v7.widget.Toolbar
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="?attr/colorPrimary"
    app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
    app:popupTheme="@style/Theme.AppCompat.Light.DarkActionBar"
    android:fitsSystemWindows="true"
    />
  • 記得是使用v7的Toolbar而非android.widget.Toolbar!!!
  • android:background="?attr/colorPrimary 這邊是使用@style/colorPrimary來設定背景顏色,這是Android 5.0以後的寫法。
  • android:fitsSystemWindows="true" 這是讓Toolbar可以向上延伸到狀態列,主要是用在Android 4.4可以使用透明的狀態列。

加入Toolbar到主介面

再來activity_main.xml裡面我們要把剛剛建立的toolbar.xml引入,所以用一個RelativeLayoutToolbar用include的方式和主要內容的FrameLayout包起來如下

<android.support.v4.widget.DrawerLayout
    android:id="@+id/drawer_layout"
    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"
    tools:context=".MainActivity">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <include
            android:id="@+id/toolbar"
            layout="@layout/toolbar"/>

        <FrameLayout
            android:id="@+id/container"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_below="@id/toolbar"/>
    </RelativeLayout>

    <fragment
        android:id="@+id/navigation_drawer"
        android:name="com.moviebomber.ui.fragment.NavigationDrawerFragment"
        android:layout_width="@dimen/navigation_drawer_width"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        tools:layout="@layout/fragment_navigation_drawer"/>
</android.support.v4.widget.DrawerLayout>

修改style檔案

因為我們要用Toolbar取代Actionbar,所以需要把原本的styles.xml修改如下

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">

改成

<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    <!-- Customize your theme here. -->
    <item name="colorPrimary">@color/primary</item>
    <item name="colorPrimaryDark">@color/primary_dark</item>
    <item name="colorAccent">@color/accent</item>
    <item name="android:windowNoTitle">true</item>
    <item name="windowActionBar">false</item>
    <item name="drawerArrowStyle">@style/DrawerArrowStyle</item>
</style>

<style name="DrawerArrowStyle" parent="Widget.AppCompat.DrawerArrowToggle">
    <item name="spinBars">true</item>
    <item name="color">@android:color/white</item>
</style>
  • colorPrimary, colorPrimaryDak, colorAccent顏色對應關係可以見下圖,這是讓Android 5.0可以抓到對應的顏色。

  • android:windowNoTitle把視窗標題移除, windowActionBar不使用Actionbar。
  • drawerArrowStyle和下面DrawerArrowStyle是做出開啟Navigation Drawer漢堡變箭頭的動畫(如下圖)


在MainActivity.java使用Toolbar

MainActivity.java要加入Toolbar並且把Actionbar指向Toolbar,然後設定Statusbar為透明的。

public class MainActivity extends AppCompatActivity 
    implements NavigationDrawerFragment.NavigationDrawerCallbacks {

    @InjectView(R.id.toolbar)
    Toolbar mToolbar;

    /**
     * Fragment managing the behaviors, interactions and presentation of the navigation drawer.
     */
    private NavigationDrawerFragment mNavigationDrawerFragment;

    /**
     * Used to store the last screen title. For use in {@link #restoreActionBar()}.
     */
    private CharSequence mTitle;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            Window w = getWindow(); // in Activity's onCreate() for instance

            w.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
        }
        setContentView(R.layout.activity_main);
        ButterKnife.inject(this);
        this.setSupportActionBar(this.mToolbar);

        mNavigationDrawerFragment = (NavigationDrawerFragment)
                getSupportFragmentManager().findFragmentById(R.id.navigation_drawer);
        mTitle = getTitle();

        // Set up the drawer.

        mNavigationDrawerFragment.setUp(
                R.id.navigation_drawer,
                this.mToolbar,
                (DrawerLayout) findViewById(R.id.drawer_layout));
    }
}
  • 增加了mToolbar的成員,這邊是import android.support.v7.widget.Toolbar;,我們都是使用v7的Toolbar!
  • if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {那一段是把Statusbar設為透明的,讓colorPrimaryDark也可以在Android 4.4顯示出來,讓Toolbar可以延伸到Statusbar。
  • 使用setSupportActionBar()把Toolbar傳入。
  • mNavigationDrawerFragment.setUp(...)這方法要傳入Toolbar,所以下列的NavigationDrawlerFragment.java要改掉API。

NavigationDrawerFragment.java使用Toolbar

NavigationDrawerFragment.java要可以知道Toolbar的狀態,得知是否有按下漢堡,所以改動的地方如下

public void setUp(int fragmentId, Toolbar toolbar, DrawerLayout drawerLayout)  {
    mDrawerToggle = new ActionBarDrawerToggle(
                getActivity(),                    /* host Activity */
                mDrawerLayout,                    /* DrawerLayout object */
                toolbar,
                R.string.navigation_drawer_open,  /* "open drawer" description for accessibility */
                R.string.navigation_drawer_close  /* "close drawer" description for accessibility */
        )
} 
  • 原本setup()API多加了Toolbar當傳入參數。
  • ActionBarDrawerToggle要從v4換成v7的,並且把Toolbar傳入。

完成結果

  • 現在上面是Toolbar,不是Actionbar。

  • 展開功能表

完成收工!!

功能需求

需要更動Android內建TabWidget元件當中文字的屬性,在此以更改文字大小、文字強制限制單行顯示(不自動換行)為例。


思路

這邊重點在於如何取得在TabWidget當中的TextView元件,一旦取得後就可以依照你的需求做更改。


程式碼

TabWidget tabWidget = this.tabHost.getTabWidget();
for ( int i = 0; i < 4; i++ )
{
    LinearLayout tabView = (LinearLayout) tabWidget.getChildAt( i );
    TextView tabTextView = (TextView) tabView.findViewById( android.R.id.title );
    tabTextView.setLayoutParams( new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT,  LinearLayout.LayoutParams.MATCH_PARENT ) );
    tabTextView.setTextSize( 12 );
    tabTextView.setSingleLine();
    tabTextView.setGravity( Gravity.CENTER );
    tabTextView.setPadding( 0, 0, 0, 0 );
}

解說和注意要點

  • 如果要一次設定完所有Tab,那此段程式碼要寫在所有tab加入之後,也就是寫在所有tabHost.addTab( newTab )之後。
  • 先從TabHost取得TabWidget,可以採用下列兩個方法取得:
    TabWidget tabWidget = this.tabHost.getTabWidget();
    
    (TabWidget)this.findViewById( android.R.id.tabs );
    
    本範例採用第一種方法取得,而第二種方法注意R是Android本身原本套件而非你專案的R檔。
  • 有for迴圈是因為有多個Tab要設定,如果只有單一個Tab要設定,可以直接使用(LinearLayout) tabWidget.getChildAt( i );來取得TabView,i一樣是從0開始的整數。
  • (TextView) tabView.findViewById( android.R.id.title );這行就是取得Tab的TextView,一樣注意到R是Android本身原本套件而非你專案的R檔。
  • 取得TextView物件後,就可以依照你的需求開始設定。

參考

http://stackoverflow.com/questions/19442084/change-the-text-size-in-tab-in-android

需求

  • Activity需要直接取得AsyncTask執行後onPostExecute()的結果,而非在onPostExecute()處理執行結果。
  • AsyncTask需要寫成獨立的class檔案,而非寫成Activity的inner class。

思路

  • Activity當中不使用AsyncTask.get()來取得結果,因為呼叫AsyncTask.get()會阻斷UI介面的操作(會有停頓的現象),故需採用別種方式來取得。
  • 宣告一個介面,在此介面當中定義取得結果的方法。
  • AsyncTask當中宣告一個此介面型態的實體變數,然後在onPostExecute()方法當中把執行結果傳給此介面型態的實體變數。
  • 在要取得結果的Activity實作此介面,如此一來此Activity就可以在實作該介面取得結果的方法來取得結果。

程式碼

宣告一個介面並且定義取得結果的方法。

public interface AsyncTaskResult<T extends Object>
{
    // T是執行結果的物件型態

    public void taskFinish( T result );
}

AsyncTask當中加入一個用此介面為型態所宣告的實體變數,並且在onPostExecute()當中把執行結果傳給此變數。
這邊採用網路連線測試做為範例程式。

public class ConnectionTestAsyncTask extends AsyncTask<String, Void, Boolean>
{
    private static final int TIME_OUT = 1000;
    private static final String TEST_URL = "http://www.google.com/blank.html";
    
    public AsyncTaskResult<Boolean> connectionTestResult = null;
  
    @Override
    protected Boolean doInBackground( String... params )
    {
        boolean connectSuccess = false;
        HttpURLConnection httpConn = null;
        try
        {
            httpConn = (HttpURLConnection) ( new URL( TEST_URL ).openConnection() );
            httpConn.setInstanceFollowRedirects( false );   
            httpConn.setConnectTimeout( TIME_OUT );
            httpConn.setReadTimeout( TIME_OUT );
            httpConn.connect();
            if ( httpConn.getResponseCode() == HttpURLConnection.HTTP_OK )
                connectSuccess = true;
        }
        catch ( IOException e )
        {
        }
        finally
        {
            if ( httpConn != null )
                httpConn.disconnect();
        }
        return connectSuccess;
    }

    @Override
    public void onPostExecute( Boolean result )
    {
        this.connectionTestResult.taskFinish( result );
    }
}

接著在要取得執行結果的Activity實作此介面,同時加入AsyncTask,記得把AsyncTask.connectionTestResult指派給this ,然後實作介面的taskFinish()來處理AsyncTask執行的結果。

public class MainActivity extends Activity implements AsyncTaskResult<Boolean>
{
    public void checkNetworkStatus()
    {
        ConnectionTestAsyncTask connectionTask = new ConnectionTestAsyncTask();
        connectionTask.connectionTestResult = this;
        connectionTask.execute( "" );
    }
  
    @Override
    public void taskFinish( Boolean result )
    {
        if ( result == true )
            Toast.makeText( this, "網路連線正常", Toast.LENGTH_LONG ).show();
        else
            Toast.makeText( this, "網路連線失敗", Toast.LENGTH_LONG ).show();
    }
  
  // ...其他Activity程式碼略!!

}  

參閱原始問答

http://stackoverflow.com/questions/12575068/how-to-get-the-result-of-onpostexecute-to-main-activity-because-asynctask-is-a

解法1 (需要用sudo執行adb)

  • 開啟終端機,先cd到android-sdk/platform-tools目錄,然後執行以下指令
    sudo ./adb kill-server
    sudo ./adb start-server
    sudo ./adb devices
    
    這樣應該可以顯示出連接的手機即成功!
    List of devices attached 
    HT2A9MG00618    device
    

解法2 (不需要sudo執行adb)

解法1是一定要用sudo,這解法設定一次後就可以不用sudo,一般使用者和 eclipse 皆可以正常執行。

  1. 開啟終端機,先cd到android-sdk/platform-tools目錄,然後執行以下指令。

  2. 把adb執行檔的擁有者和擁有群組改掉,下面的user_group是要改成你電腦上的群組名稱。

    chown root:user_group adb
    
  3. 把adb執行檔權限設定為SUID:

    chmod 4550 adb
    
  4. 接著重新啟動adb-server

    ./adb kill-server
    ./adb start-server
    
  5. 接下來你就可以用一般使用者(不用root)來執行adb,而adb會自動以root來執行。如此就可以用一般使用者開啟Eclipse然後連接手機測試App了。

    ./adb devices 
    List of devices attached 
    HT0BPPY15230    device
    

前情提要

  • 以下情況皆是在ubuntu 12.04 64位元底下操作。(以下問題可能只出現在64位元作業系統)
  • 假設android sdk是儲存在~/android-sdk目錄底下。

情況1 「adb命令找不到」

  • 解法:需要把android sdk加入PATH,在終端機執行vim ~/.bashrc把加入android sdk相關路徑加到底下。
  • .bashrc加入以下路徑 (可以直接複製底下文字)
    JAVA_HOME=/usr/lib/jvm/java-7-oracle
    ANDROID_HOME=/home/android-sdk
    ANDROID_TOOLS_HOME=/home/android-sdk/tools
    ANDROID_PLATFORM_TOOLS_HOME=/home/android-sdk/platform-tools
    export PATH=$PATH:$ANDROID_PLATFORM_TOOLS_HOME:$JAVA_HOME:$ANDROID_HOME:$ANDROID_TOOLS_HOME
    
  • 加入後登出重新登入即可。

情況2「adb沒有此一檔案或目錄」

  • 卡關情況:

    • 即使PATH設定完後,執行adb也是出現「adb命令找不到」。
    • 切換到android-sdk/platform-tools底下,即使adb檔案存在,執行./adb也是出現 沒有此一檔案或目錄: ./adb 錯誤訊息。
  • 解法:主要就是因為使用64位元系統,adb執行時期需要的shared library 無效或不存在。執行sudo apt-get install ia32-libs安裝相關的程式庫檔案,安裝完後即可執行adb。

希望各位看完這篇文章之後,你可以勇敢的關掉Eclipse(除非你跟我一樣也有另外在寫Java就不用),下載安裝並開啟Android Studio來用!!
以下中英文摻插,用中文實在翻不出一個有感覺的字詞,就直接寫英文,然後Android Studio有時會簡寫成AS。
我盡量把截圖放上,所以圖多,目的在於讓你可以更了解。

你將會知道...

  • Part1. Android Studio是?
  • Part2. 特色功能。
  • Part3. Gradle簡單介紹。

Part1. Android Studio是?

  1. Android Studio本身是Google官方推出的Android IDE。(Eclipse Android ADT確定不再發佈更新了喔!!)
  2. 是以IntelliiJ IDEA為基礎去改造的工具。
  3. 採用Gradle build工具。
  4. 更多的介紹google一下就一堆,我就不再贅述。

Part2. 特色功能

  1. Auto Complete:比Eclipse ADT更強的(有自動學習功能,會自動辨識並且顯示最相關的提示)程式碼自動提示和自動完成功能。
  2. Build Variants: 這像功能可以幫你build不同功能版本的apk,例如:debug / release,或是你的app有分免費版和付費版,兩者功能不相同,都可以同時在build variant這功能來建置。
  3. Productivity Guild:在功能表的 [Help] -> [Productivity Guild] 可以開啟,裡面有列出一些能提高生產力的功能並且統計你使用的次數和最近一次的使用時間。


  4. 多裝置除錯:在測試App階段,允許選擇多個裝置,讓你可以一次同時測試不同的裝置。

  5. 多裝置、多theme的Layout Preview:在介面設計工具,除了有即時的預覽之外,也允許讓你同時預覽不同裝置的樣子,或是你可以直接套用不同的theme。

  6. 超多的plugins:跟Eclipse一樣擁有很多外掛可以安裝使用,安裝方法為開啟設定視窗Setting -> Plugins 來檢視。

  7. DDMS:開啟方法是左下的6: Android分頁標籤點下後會顯示,DDMS的左邊有工具列,包含了很多監測的工具,像是App截圖或錄影、記憶體使用情況、執行GC、Java Heap Dump...等。

  8. 監測記憶體使用狀況:從右下的Memory Monitor分頁標籤可以開啟記憶體監測的功能視窗,你就可以直接看到記憶體使用狀況的圖表。

  9. 建立不同裝置類型的專案:我們可以直接使用AS來開發手機、平板、TV、Wearable和Glass的專案。

  10. Code Tempate:在建立新專案或是新檔案的時候,我們可以選擇預先提供的程式碼樣板,像是不同類型的Activity、Fragment、或Service...等。

  11. Layout Tools Attributes:在AS新增的Tool Attributes主要是用在設計階段,最常出現在Layout的XML檔案裡面,像是tool:context出現在Activity的Layout.xml檔裡面,是紀錄該Layout.xml檔案是屬於某個Activity。 我覺得非常好用的還有 tools:text,我們一般如果想預覽TextView實際上套用文字的樣子,我們以前會在該TextView裡面加入android:text='我是測試文字,然後才能在Design Preview當中看到實際上的樣子,然後App要發怖時才把這個測試文字移除,現在如果直接加入`tools:text='我是測試文字',我們就可以直接在Layout Preview當中看到套用文字的介面實際樣子,但是在實際的App裡面就不會出現。記得,Tools Attributes是用在設計階段,只會在設計階段看到,完全不會影響到實際執行的樣子。

更多Tools Attributes介紹可以參考這篇的介紹。

  1. Resource Preview:在資源檔當中,我們可以直接預覽,例如:在activity_main.xml當中我們寫到@string/app_name,可以直接預覽到對應的文字是什麼,或是@color/text_current_position可以直接預覽顏色。

Part 3. Gradle簡單介紹

Gradle是一種依賴管理工具,基於Groovy語言,捨棄了基於XML繁瑣的配置方法,而採用Groovy的DSL語言。
而AS導入了Project v.s. Modules的概念,下面分別作簡單的講解:

Project:
1. 是一個完整的app專案。
2. 可以包含很多的modules。
Module:
1. 模組是一個獨立的元件,可以獨立的build、測試和除錯。
2. 在AS當中有三種模組:Java Library, Android Library, Android Application。

在AS當中project structure有些更動,和原本的eclipse專案結構有所不同,大家可以在下圖看到Project的根目錄底下就兩個Modules(粗體),頂層專案目錄下個別有build.gradlesettings.gradle兩個build files, 每個Modules也個別有一個build.gradle檔案(*紅字),下面在一一做簡單的介紹。

頂層settings.gradle

用途在於告訴gradle專案底下有哪些Modules要來build。

include ':app'
include ':iconicDroid'

如果今天你的Modules沒有在這個專案底下,而是在本機電腦的其他位置,你可以先[Import Module]加入,或是用下面語法來include進來。

project(':library1').projectDir = new File('other/place/on/the/computer')
頂層build.gradle

用途在於增加所有子專案或是所有模組的共同組態,例如你的remote repository、gradle的dependencies...等。

// Top-level build file where you can add configuration options common to all sub-projects/modules.


buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.0.0'

        // NOTE: Do not place your application dependencies here; they belong

        // in the individual module build.gradle files

    }
}

allprojects {
    repositories {
        jcenter()
    }
}
各Module的build.gradle

用途在於各模組可以增加不同的Android設定(例如:compileSDKVersion, buildType)以及dependencies。這邊記得兩件事情:

  1. 一個模組,就會有一個build.gradle
  2. 每個模組的dependencies就寫在自己模組底下的build.gradle檔。
apply plugin: 'com.android.application'

android {
    compileSdkVersion 21
    buildToolsVersion "21.1.1"

    defaultConfig {
        applicationId "gov.epa"
        minSdkVersion 14
        targetSdkVersion 21
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:21.0.3'
    compile project(':iconicDroid')
}
Module Dependencies

模組的dependencies有分成下面三種:

  1. 相同專案底下的Module:這樣的寫法要確保該模組是在此專案底下,或是已經import進該專案。
    compile project(':library1')
    
  2. 在本機電腦的檔案目錄:可以用來加入*.jar檔案或是libs/資料夾。
    compile files('libs/gson.jar')
    compile fileTree(dir:'libs', include:['*.jar'])
    
  3. Remote Repositiory:加入非本機端的程式庫,gradle會自動幫你完成所有的import動作(例如:下載所需要的jar,管理該程式庫的dependencies)
    compile 'com.android.support:support-v4:21.0.3'
    

結語

以上大致上簡單的介紹到此,我個人的經驗是只有在從eclipse轉換成gradle的時候有些新東西要學不太習慣而已,其他都很OK,如果有使用上的問題歡迎留言問我,至於還沒用的趕緊來用,你會體驗到它的強大!!