월별 글 목록: 2013년 6월월

안드로이드 앱 폰트 전체를 변경해 보자..

안드로이드 앱을 개발하다 보면 종종 폰트가 맘에 안 드는 경우가 발생한다. 안드로이드 4.1.x 이후부터는 새로운 폰트가 추가되서 조금 만족스러운 결과를 보여준다. 하지만 그것도 맘에 안 드는 경우나 앱 전체에 같은 폰트를 사용해서 일관성 있는 UI를 표현하고 싶은 경우도 있다. 안드로이드 개별 컴포넌트의 폰트를 변경하는 것은 어렵지 않다. 여기서는 앱 전체 폰트를 한 번에 변경하는 방법에 대해 살펴보겠다. 아래 내용은 안드로이드 2.3 이상의 버전에서 앱 전체 폰트를 변경하는 예제이다.

1. 원하는 폰트(Roboto-Light.ttf)를 assets 폴더에 카피한다.

2. Application 클래스를 상속해서 아래(FontApplication)와 같이 구현한다.

package net.sjava.example.font_reflector; 
import java.lang.reflect.Field;
import android.app.Application;
import android.content.Context;
import android.graphics.Typeface;

public class FontApplication extends Application {
	@Override
	public void onCreate() {
		setDefaultFont(this, "DEFAULT", "Roboto-Light.ttf");
		setDefaultFont(this, "SANS_SERIF", "Roboto-Light.ttf");
		setDefaultFont(this, "SERIF", "Roboto-Light.ttf");
	}

	public static void setDefaultFont(Context ctx,
			String staticTypefaceFieldName, String fontAssetName) {
		final Typeface regular = Typeface.createFromAsset(ctx.getAssets(),
				fontAssetName);
		replaceFont(staticTypefaceFieldName, regular);
	}

	protected static void replaceFont(String staticTypefaceFieldName,
			final Typeface newTypeface) {
		try {
			final Field StaticField = Typeface.class
					.getDeclaredField(staticTypefaceFieldName);
			StaticField.setAccessible(true);
			StaticField.set(null, newTypeface);
		} catch (NoSuchFieldException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		}
	}
}

이 과정은 Application 클래스(앱의 시작과 종료 사이클을 가지고 있는 클래스)를 사용한다. 앱이 시작할 때 리플렉션을 이용해서 폰트를 재설정하는 방식(http://stackoverflow.com/questions/2711858/is-it-possible-to-set-font-for-entire-application?rq=1) 이다.

3. AndroidManifest.xml파일에 위의 클래스 이름(FontApplication)을 등록한다.

4. values/styles.xml 파일을 아래의 형태로 수정한다.

<resources>
    <style name="AppBaseTheme" parent="android:Theme.Holo.Light">
        <item name="android:typeface">sans</item>
    </style>

    <style name="AppTheme.DialogWhenLarge" parent="@android:style/Theme.Holo.Light.DialogWhenLarge">
        <item name="android:typeface">sans</item>
    </style>
</resources>

위 과정으로, 앱에서 표현되는 모든 문자는 FontApplication 클래스에서 등록한 폰트로 바뀌는 것을 볼 수 있다. 아래는 안드로이드 에뮬레이터 2.3버전에서 확인한 화면이다.
 

5. 추가 테마에서도 글꼴을 바꿔보자.
위의 4.에서 본 styles.xml 예제는 안드로이드 2.3의 예제이고 정말 간단한 예이다. 아마도 하나의 Theme만을 쓰는 쓰는 앱은 거의 없을 테니, 다른 테마에서 적용하는 방법을 살펴보자. 아래의 예제는 values-v11/styles.xml을 약간 수정한 파일이다. 위에서 안드로이드의 Theme.Holo.Light.DialogWhenLarge 테마를 AppTheme.DialogWhenLarge가 상속을 받고, 폰트만 재 정의 했다. 아래에서는 위에서 정의한 AppTheme.DialogWhenLarge를 적용한 AndroidManifest.xml의 Application 영역 부분이다.

<application 
    android:allowBackup="true" 
    android:icon="@drawable/ic_launcher"             
    android:label="@string/app_name"                    
    android:name=".FontApplication"             
    android:theme="@style/AppTheme" >
    <activity 
        android:name="net.sjava.example.font_reflector.IndexActivity"
        android:theme="@style/AppTheme.DialogWhenLarge"                 
        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>

위에서 살펴본 앱 폰트를 한 번에 변경하는 예제는 아래에서 다운받을 수 있다.
cfile30.uf.2302334A51CA70FD162EF6.zip

Ubuntu에서 Adobe가 배포하는 SourceCodePro, SourceSansPro 폰트 사용하기..

Adobe에서 배포한 SourceCodePro, SourceSansPro 폰트를 우분투에서 사용하는 방법입니다. 최근에 Tweet으로 SourceCodePro가 좋다고 하신 분이 계셔서, Adobe가 Github에서 공개하는 폰트를 찾아보니 SourceSansPro 폰트가 더 있어서, 두 개다 설치하는 방법이다.

아래의 URL에서 2개의 폰트를 다운로드 받는다.
https://github.com/adobe-fonts/source-code-pro
https://github.com/adobe-fonts/source-sans-pro

위 2개의 폰트중에 SourceCodePro 파일을 풀면 아래와 같은 구조를 볼 수 있고, OTF, TTF 둘 다 가지고 있다. OTF와 TTF는 OpenType의 글꼴로, 자세한 내용은 http://en.wikipedia.org/wiki/OpenType에서 볼 수 있다.

drwxr-xr-x  4 mcsong mcsong  4096 Jun 19 10:16 ./
drwxr-xr-x 41 mcsong mcsong 20480 Jun 19 10:16 ../
-rwxr-xr-x  1 mcsong mcsong  4622 Jan 16 13:48 LICENSE.txt*
drwxr-xr-x  2 mcsong mcsong  4096 Jun 19 10:16 OTF/
-rwxr-xr-x  1 mcsong mcsong  4402 Jan 16 13:48 ReadMe.html*
-rwxr-xr-x  1 mcsong mcsong 12328 Jan 16 13:48 SourceCodeProReadMe.html*
drwxr-xr-x  2 mcsong mcsong  4096 Jun 19 10:16 TTF/

각 폰트가 제공하는 2가지 타입중에서, TTF 폰트를 사용하는 방법을 살펴본다. SourceCodePro를 사용하는 예를 살펴보면..

sudo mkdir /usr/share/fonts/truetype/sourcecodepro <– SourceSansPro는 sourcesanspro 폴더를 만든다.
sudo cp *.ttf /usr/share/fonts/truetype/sourcecodepro/ <– 위의 TTF 폴더에서 복사
sudo fc-cache -f -v <– 폰트 캐시 다시 로딩

위의 과정을 SourceSansPro에 대해서도 동일하게 적용한다. 그리고, 폰트 캐시를 리프레시하면 아래의 결과를 볼 수 있다.

/usr/share/fonts/truetype/sourcecodepro: caching, new cache contents: 7 fonts, 0 dirs
/usr/share/fonts/truetype/sourcesanspro: caching, new cache contents: 12 fonts, 0 dirs

위의 과정으로 Adobe에서 배포하는 SourceCodePro와 SourceSansPro 폰트를 사용할 수 있겠다. 개인적으로는 SourceSansPro보다는 SourceCodePro가 코딩할 때 가독성이 좋게 느껴진다.. ^^

AsyncTask의 우선순위 조정하기..

안드로이드 진저브레드 이후부터는 태스크를 처리하는 형태로 AsyncTask를 사용하도록 권장한다. 만약에 개발하는 앱이, 서버에서 100장의 사진을 수신하는 상황을 생각해 보자.

시나리오는 다음과 같다.

  1. 사진의 목록을 가져온다.
  2. 목록이 보이기 시작하면 한 장씩 사진을 웹에서 가져온다.

대부분의 앱은 이 시나리오를 기준으로 개발할 것이고, 목록을 가져오는 태스크와 사진을 한 장씩 가져오는 태스크를 처리하는 데 AsyncTask를 사용한다고 가정한다.

이 시나리오대로 개발한 앱에서, 화면을 빠르게 스크롤 하면 개별 사진을 가져오는 AsyncTask가 많이 생성된다. 그리고 페이징으로 목록을 가져오는 경우에는 목록을 가져오는 AsyncTask도 경쟁상황(Race Condition)이 돼서 목록을 보는 데 오랜 시간이 걸리는 경우가 발생할 수 있다.

이 상황에서 더 좋은 UX를 위해서 AsyncTask의 우선순위를 조정해서, 목록을 가져오는 태스크의 우선순위를 다른 태스크보다 높이는 방법에 대해서 살펴보자.

AsyncTask는 태스크를 비동기 스레드로 처리하도록 스레드와 핸들러를 추상화한 클래스이다. 그리고 Template Method 패턴과 Command 패턴의 형태를 보이고 있다. AsyncTask가 스레드 말하시는 분들이 있는데, AsyncTask 소스(https://github.com/android/platform_frameworks_base/blob/master/core/java/android/os/AsyncTask.java)를 보면, 맞기도 하지만 틀리기도 하다. 정확한 표현은 태스크를 처리하는 스레드 디스패처라고 봐야 하겠다. 하지만 AsyncTask를 상속해서 사용하게 되면, 이 스레드 디스패처를 사용하는 태스크 처리 스레드라고 보면 좀 명확(?) 하겠다.

AsyncTask는 static으로 스레드를 디스패칭하는 Executor가 있고, 이 객체가 스레드를 실행시킨다. 이 구조는 자바의 병행(Concurrent) 스레드 처리하는 형태와 같다. AsyncTask 우선순위를 조정하기 위해서 첫 번째로, 생성자 부분을 살펴보자.

public AsyncTask() {
        mWorker = new WorkerRunnable<Params, Result>() {
            public Result call() throws Exception {
                mTaskInvoked.set(true);
 
                Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                //noinspection unchecked
                return postResult(doInBackground(mParams));
            }
        };
 
        mFuture = new FutureTask<Result>(mWorker) {
            @Override
            protected void done() {
                try {
                    postResultIfNotInvoked(get());
                } catch (InterruptedException e) {
                    android.util.Log.w(LOG_TAG, e);
                } catch (ExecutionException e) {
                    throw new RuntimeException("An error occured while executing doInBackground()", e.getCause());
                } catch (CancellationException e) {
                    postResultIfNotInvoked(null);
                }
            }
        };
    }

이 생성자에서 Worker 스레드의 우선순위를 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 로 조정하는 것을 알 수 있다.

이 정보를 기준으로 간단하게 새로운 AsyncTask 클래스를 사용하는 방법을 생각해 보자. 거의 동일한 클래스이고, 위의  Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT); 로 조정한 새로운 클래스를 만들고, 이 클래스를 상속해서 사용하면 기존의 AsyncTask보다 빠르게 태스크를 처리하는 AsyncTask를 사용할 수 있다.

PriorityAsyncTask.java

그럼, 이 PriorityAsyncTask를 상속받은 클래스와 기존의 AsyncTask를 상속받은 클래스의 스레드 스케줄링에 대해서 테스트 해 보자.

	
package net.sjava.asynctest;

import android.os.AsyncTask;
import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;

public class MainActivity extends Activity {
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		
		e("MAIN THREAD PRIORITY : " + Thread.currentThread().getPriority());
		
		new BackgroundTask(1).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, "");
		new BackgroundTask(2).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, "");
		new BackgroundTask(3).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, "");
		new BackgroundTask(4).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, "");
		new BackgroundTask(5).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, "");
		new BackgroundTask(6).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, "");
		new BackgroundTask(7).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, "");
		new BackgroundTask(8).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, "");
		new ForgroundTask().executeOnExecutor(PriorityAsyncTask.THREAD_POOL_EXECUTOR, "");
		new BackgroundTask(9).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, "");
		new BackgroundTask(10).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, "");
	}


	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		return true;
	}
	// 
	public void w(String value) {
		android.util.Log.w("ThreadTest", value);
	}
	
	public void e(String value) {
		android.util.Log.e("ThreadTest", value);
	}
	
	int size = 1000000;
	class BackgroundTask extends AsyncTask<String, Integer, Boolean> {
		int x =0;
		long value = 0L;
		BackgroundTask(int x) {
			this.x = x;
		}
		
		@Override
		protected Boolean doInBackground(String... params) {
			long start = System.currentTimeMillis();
			w(x + ", BackgroundTask start : " + start );
			for(int i=0; i < size; i++) {
				value += i;
			}
			
			w(x +", BackgroundTask ended : " + value +" : " + (System.currentTimeMillis() - start));
			return null;
		}
	}
	
	class ForgroundTask extends PriorityAsyncTask<String, Integer, Boolean> {
		long value = 0L;
		
		@Override
		protected Boolean doInBackground(String... params) {
			long start = System.currentTimeMillis();
			w("ForgroundTask start : " + start );
			
			for(int i=0; i < size; i++) {
				value += i;
			}
			
			w("ForgroundTask ended : " + value +" : "+ (System.currentTimeMillis() - start));
			return null;
		}	
	}	
}

위 코드의 결과는 다음과 같다.

06-04 21:37:41.043: W/ThreadTest(7353): 1, BackgroundTask start : 1370381861052
06-04 21:37:41.053: W/ThreadTest(7353): 2, BackgroundTask start : 1370381861053
06-04 21:37:41.063: W/ThreadTest(7353): 4, BackgroundTask start : 1370381861066
06-04 21:37:41.063: W/ThreadTest(7353): 5, BackgroundTask start : 1370381861072
06-04 21:37:41.083: W/ThreadTest(7353): 3, BackgroundTask start : 1370381861083
06-04 21:37:41.133: W/ThreadTest(7353): 4, BackgroundTask ended : 499999500000 : 75
06-04 21:37:41.143: W/ThreadTest(7353): 1, BackgroundTask ended : 499999500000 : 98
06-04 21:37:41.163: W/ThreadTest(7353): 2, BackgroundTask ended : 499999500000 : 113
06-04 21:37:41.163: W/ThreadTest(7353): 6, BackgroundTask start : 1370381861169
06-04 21:37:41.172: W/ThreadTest(7353): 3, BackgroundTask ended : 499999500000 : 93
06-04 21:37:41.172: W/ThreadTest(7353): 7, BackgroundTask start : 1370381861176
06-04 21:37:41.172: W/ThreadTest(7353): 8, BackgroundTask start : 1370381861178
06-04 21:37:41.202: W/ThreadTest(7353): 5, BackgroundTask ended : 499999500000 : 132
06-04 21:37:41.202: W/ThreadTest(7353): 9, BackgroundTask start : 1370381861204
06-04 21:37:41.202: W/ThreadTest(7353): ForgroundTask start : 1370381861207
06-04 21:37:41.252: W/ThreadTest(7353): ForgroundTask ended : 499999500000 : 47
06-04 21:37:41.262: W/ThreadTest(7353): 10, BackgroundTask start : 1370381861266
06-04 21:37:41.322: W/ThreadTest(7353): 6, BackgroundTask ended : 499999500000 : 155
06-04 21:37:41.322: W/ThreadTest(7353): 10, BackgroundTask ended : 499999500000 : 62
06-04 21:37:41.333: W/ThreadTest(7353): 7, BackgroundTask ended : 499999500000 : 158
06-04 21:37:41.333: W/ThreadTest(7353): 8, BackgroundTask ended : 499999500000 : 157
06-04 21:37:41.333: W/ThreadTest(7353): 9, BackgroundTask ended : 499999500000 : 132

이 결과를 보면, AsyncTask를 상속한 BackgroundTask의 태스크는 경쟁상황에서 태스크가 실행되고 있지만, 이 PriorityAsyncTask를 상속받은 ForgroundTask의 태스크는 실행이 되자마자 바로 실행을 마치는 것을 알 수 있다.

이상, AsyncTask의 우선순위를 조정하는 방법에 대해서 살펴 보았다. 만약 태스크에 대한 스케줄링을 고민한다면, http://www.sjava.net/312 도 참조해 보세요.