2014/07/21

個人でもできるGooglePlay検索最適化(ダウンロードを増やすためのASO)

最近、アプリを出しただけでは全くダウンロードされなくなり、ランキング上位は広告をバシバシ打てる企業が占めるようになって、個人の開発者が戦っていくのはほんとに厳しくなりました。

そこで、先月リリースした「通信量モニター」というアプリで、検索最適化について色々試して、知見がたまってきたのでここにまとめておこうと思います。
ダウンロードが思うように伸びなくて困っている開発者のお役に立てれば嬉しいです。

なぜ検索最適化?

GooglePlayの中の人によると、ほとんどのアプリはGoogle Playの検索結果からのダウンロードが一番多いらしい。
12%のユーザが毎日GooglePlayで検索し、50%のユーザが毎週GooglePlayで検索する。
検索でアプリを探しているユーザは目的があって検索しているので、問題を解決できるアプリを見つけたら長期的に使ってくれる優良ユーザになりやすい。
なので検索結果で上位に表示される事が、多くのユーザにアプリをダウンロードしてもらえることにつながります。

※広告を打てないデベロッパーは新着などのランキングにはこだわらないほうがいいかもしれません。
多くの開発者がリリースして1ヶ月の新着ランキング上位を狙いますが、TOP20ぐらいにならない限りダウンロードはそんなに増えません。「通信量モニター」も最終的に新着ランキング100位ぐらいまでいきましたが、新着ランキングから消えた前と後でのデイリーDLに差は無かったです。多分デイリーDL50もないです。


検索で上位表示するためにやること


キーワードの選定

自分のアプリを使って欲しいターゲットユーザがどんなキーワードで検索するかを考えて、キーワードを選びます。
アプリの特徴を表すキーワードはもちろんですが、ビッグワードな場合、それだけで上位表示を狙うのは難しいので、複合キーワードも狙います。
Google Playの検索機能は優秀なのでキーワードを打つとレコメンドしてくれます。レコメンドされるキーワードは他にもその検索結果を使っているユーザがいるということなので、それらのキーワードで上位表示されるようにします。
(検索マークと履歴マークがあるが重要なのは検索マークの方)

また、そのキーワードがどれくらいの検索ボリュームなのか調べます。
正確に計測する手段はありませんが、Googleトレンドで調べるとどのキーワードが検索ボリュームが多いか推測できます。(Webの検索ボリュームとGooglePlayの検索ボリュームは同じではないので参考程度に)

タイトルや説明文にキーワードをいれる

選定したキーワードをタイトルや説明文に加えます。
ここで重要なのは濃度です。現在GooglePlayではタイトルに30文字、説明文に4000文字含めることができますが、限界まで含めてもあまり良いことはなく、キーワードの濃度を重視しているようです。
「通信量モニター」で同じ日に次の2つを試したところ、
『通信量・通信速度モニター』
→通信速度:6位、通信量オーバー:圏外
『通信量・通信速度モニター:通信量オーバー対策に最適!』
→通信速度:8位、通信量オーバー:2位
となりました。
どちらをとるかはケースバイケースですが、一つのキーワードで上位を狙いたい場合は、そのキーワードを含めつつ、タイトルを短くすると上位に表示されやすくなります。
タイトルほど大きな影響力はありませんが、説明文も同様で濃度が重要です。取りたいキーワードをいれつつコンパクトにまとめた説明文を作りましょう。
開発者コンソールで変更して2時間ぐらいで検索結果に反映されるので、どのキーワードで上位を狙えるかいろいろ試すのがベストです。

※検索ボリュームが多いからといって、アプリに関係のないキーワードをタイトルや説明文にいれるのは垢バンされる可能性が高いのでやめましょう。説明文の下のほうにキーワードを羅列していたり、競合アプリの名前を入れたりすると危険です。すぐには発見されませんが、GooglePlayの中の人は人力でもチェックしているようなので、目立ってくると消されます。

レビューサイトにレビュー依頼

最近はレビューサイトの影響も2年ほど前に比べて落ちてきたように思いますが、初期ユーザを増やすためには重要です。検索結果にはインストール数、アンインストール数もすこし考慮されるため、初期の優良ユーザは多いほうがいいです。
またGooglePlayは外からのリンクも考慮しているようなので、レビューサイトで取り上げられることはダウンロード数もリンク数も増えるので一石二鳥です。
レビューサイトは沢山あるので、以下から探してください。
またレビュー依頼を代行してくれるサービスもありますが、無料のものは殆ど効果ないのでやってもやらなくても一緒です。
有料のものは使ったことがなかったので、今回試しにAPPリリースさんのプレミアム(49800円)サービスを使ってみました。
結果として「通信量モニター」の場合は2社に取り上げてもらい、そこからのインストールは大体1000件ぐらいです。CPI50円ぐらいになった結果なので、少しお金があるなら使ってみるのも悪くないと思います。

事前登録サービスを使う(リリース前限定)

リリース予定のアプリを予約しておく事前登録サービスというものがあります。
いくつかありますが、個人でも参加できる無料の予約トップ10がオススメです。
登録されているアプリはソーシャルゲームが多いですが、カジュアルゲームやツール系のアプリも登録できます。
通信量モニターの場合は予約登録数700ぐらいで、リリース後に300件ぐらいダウンロードしてくれました。貴重な初期ユーザを獲得できるのでオススメです。

平均評価を高くする

良いアプリは上位に表示されやすくなるため、アプリの平均評価を高めることも重要です。ユーザに評価をお願いするダイアログを表示することが、わりと当たり前になってきましたが、そのタイミングと表示方法も重要です。
1. ユーザがアプリを良いと思ったタイミングで表示する
そのアプリがユーザの困っていることを解決した時や、ゲームでハイスコアを出した時など、ユーザの満足度が高いタイミングで出すと良い評価をしてくれる可能性が高くなります。
2. ユーザに★5の評価を促す
1.で評価してくれる可能性は高くなりますが、★5の評価の可能性を高くするためにユーザに具体的に訴えることも重要です。
「CleanMaster」はこの辺がとても上手く、平均評価も4.7ととても高いです。
(キャッシュやメモリを掃除した後に出るダイアログ)
※評価でインセンティブを与えるのはダメです。(★5の評価でポイント付与など)

+1を増やす

+1を積極的に増やしていくこともポイントです。
+1の数が順位にも影響するのはもちろんですが、+1をすると、その友達のストアのトップにおすすめアプリとして表示される可能性が高くなります。
アプリのなかに+1ボタンを置くこともそこまで難しくないのでオススメです。
実装方法は「Qiita:+1ボタンを設置する」にまとめてるので参考にしてください。
導入してから+1される数が3倍ぐらいに増えました。

結果


以上が、「通信量モニター」でやった検索最適化です。
現在の検索でのランキングはこのようになっています。
(App Annieのデータ)
(デベロッパーコンソールのデータ)

現在のデイリーのDLは400~500ぐらいで、殆ど検索流入です。
検索結果の上位に表示されることでこれぐらいのダウンロードは稼げるようになります。
ご自身のアプリの検索最適化に役立てば幸いです。頑張ってダウンロードを増やしましょう。

※「こういうのも有効だよ」というものがあれば、ぜひ教えて下さい。

参考にしたページ
Y.A.M の 雑記帳: Google I/O 2013 - Android : Getting Discovered on Google Play - 
基礎から学ぶアプリストア最適化「ASO」

2013/03/27

Androidでスプラッシュ画面を表示する

何番煎じかわかりませんが、Androidでスプラッシュを表示する方法です。

スプラッシュを実現するにあたって、編集するファイルは

  • 一番最初に表示するActivity(MainActivity.java)
  • アクティビティのxmlファイル(activity_main.xml)

の2つです。

まずxmlファイルの方を編集していきます。
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:animateLayoutChanges="true"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical" >

        // アクティビティに表示するコンポーネントをここに書く
        
    </LinearLayout>

    <ImageView
        android:id="@+id/splash_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:contentDescription="@string/content_description_splash"
        android:scaleType="fitXY"
        android:src="@drawable/splash" />

</FrameLayout>

一番外側をFrameLayoutでラップして、通常の画面の上にSplash画像が重なるようにします。

android:animateLayoutChanges="true"

これは、レイアウトが変更されるときにアニメーションするかどうかを設定しています。
フェードアウトしたほうが綺麗なので、trueに設定しています。(APILevel11以上の時なので,2.xではアニメーションしません)

次に、MainActivity.javaの方を編集します。
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.actiivty_main);
        
        initSplashView();
        
        SplashClearTask splashClearTask = new SplashClearTask();
        splashClearTask.execute();
    }
    
    private void initSplashView() {
        ImageView splashView = (ImageView)findViewById(R.id.splash_view);
        // スプラッシュ表示中はクリックを無視する
        splashView.setOnClickListener(new View.OnClickListener() {
            
            @Override
            public void onClick(View v) {
                return;
            }
        });
    }

    private void clearSplash() {
        ImageView splashView = (ImageView)findViewById(R.id.splash_view);
        splashView.setVisibility(View.GONE);
    }

    private class SplashClearTask extends AsyncTask<Void, Void, Void> {

        @Override
        protected Void doInBackground(Void... params) {
            try {
                // 一定時間待機
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return null;
        }

        @Override
        protected void onPostExecute(Void result) {
            clearSplash();
        }
    }

ポイントは、スプラッシュ表示中のクリックを無効化している部分と、SplashClearTaskの部分です。

FrameLayoutでViewを重ねるだけでは、その下にあるViewをクリックできてしまうので、スプラッシュ画像が表示されている間、その下にあるコンポーネントをクリックできないようにしています。

SplashClearTaskでは、スプラッシュを一定時間表示してからスプラッシュを消すためにdoInBackgroundでThread.sleep()を使っています。

アクティビティのレイアウトの階層が1段深くなりますが、スプラッシュ用にアクティビティを使わなくてすみます。

注意するポイントとしては、アクションバーやメニューバーは表示されてしまうので、スプラッシュの前後で表示の切替が必要です。

2013/03/12

アプリで個人として独立してやっていくためにいくら稼げばいいのか

そろそろ確定申告の締切が迫って来ました。

独立されている方や、副業でそこそこ稼いでいる方、もう確定申告は済ませました?

青色申告で65万円控除できる締切は3/15日です。

僕は今日、提出してきました。(昨年はこんなにギリギリじゃなかったのに)

いつもはこのブログでは、Androidの備忘録的なのをぽろぽろ書いているのですが、今日はちょっと違ったことを書きます。

それは、

「アプリで食べていくためにはどれくらい稼がないとダメなのか。」

です。

これを書こうと思った理由は、
  • 今までアプリの売上で2回確定申告してきて、だいたいどのくらい稼げば大丈夫なのか感じがつかめてきたから
  • 新年度を迎えるにあたり、今の会社をやめて独立しようとか思っている人の参考になればとか
そんな感じです。

まだ2年ぐらいですが、そんなに的はずれな内容にはならないと思います。

例として「都内一人暮らし」で出してみます。

まず、ひと月にかかる生活費ですが、だいたいこんな感じだと思います。

家賃:6~7万ぐらい
食費:3~4万ぐらい
水道光熱費:1万ぐらい
通信費:1万ぐらい
その他:1~2万ぐらい

で大体月に15万ぐらいかな。自炊とかもっと安いところに住んでたりすると12万ぐらいで済むかもしれません。家族を養う必要があるひとはもっとかかってきますね。とりあえず一例なので。合計は180万ぐらいです。

でアプリを開発ってことでその他必要なものを書いていきます。(1年でかかる費用)
PCとか設備費:10万ぐらい(ノートPC買ったり、PCのパーツ買ったり)
書籍費:1~5万ぐらい(ネット主体の人は安く済む)
開発者登録とか:1万
検証端末購入費とか:0~10万

大体20万ぐらいです。

その他にかかってくる費用(1年でかかる費用)
国民健康保険:前年の稼いだ額に依存するけど大体20~30万ぐらい
年金:14980円x12=18万ぐらい
奨学金返済:1~2万x12=12~24万ぐらい

社会保険料+αでだいたい60~80万ぐらいです。奨学金借りてこなかった人はその分安くなります。両親に感謝ですね。

ここまでで「一年に必要な金額」はだいたい180+20+70で270万円です。じゃあこの額を1年で稼げばいいやって思って270/12で大体月に23万ぐらい稼げばいいなって試算がでますが、そうはいきません。

そうです。「税金」です。

生活している以上、その国に税金を納めなければいけません。
で、かかってくる税金の種類は次のとおりです。

  • 所得税
  • 個人事業税(アプリ開発の場合、地方によって課税されたり非課税だったり)
  • 住民税
  • 消費税(売上が1000万以上あった場合、その2年後から課税)

所得税は「総売上-必要経費」の金額が高くなるほど、税金が高くなります。細かい計算は省いて、だいたい15%ぐらいかかってきます。

次に個人事業税が大体5%ぐらいです。知り合いの方でも課税されてたり、非課税だったりします。ただ、まだアプリで稼ぐ人が現れ始めたのがここ2~3年なので、今後アプリ開発で稼ぐ人がもっと増えると制度が変わって課税されると思います。

次に住民税ですが、均等割という全員にかかるものが3000円で、市民税が6%、県民税が4%です。なので10%ととしておきます。

次に消費税ですが、これは1期目2期目ではまず課税されません。1期目で1000万以上売り上げた場合、3期目から消費税を納めなければいけません。今は5%ですが、一年後は8%、二年後の10月からは10%です。
見なし仕入率とかあるので実際にはその半分ぐらいです。その分を所得から計算し、申告納税します。

税金は以上ですが、だいたい稼いだ金額のうち30%ぐらいは持っていかれると見ていたほうがいいです。
経費を積むと納税額を抑えられますが、アプリ開発自体はそんなに経費がかかる事業じゃないので、積むにも限界があります。

これらから、簡単ながら計算式が作れます。

「稼がなければいけない金額」-「税金(稼いだ金額の30%)」=「一年に必要な金額(270万)」

「稼がなければいけない金額」× 0.7 = 「一年に必要な金額(270万)」

「稼がなければいけない金額」 = 「一年に必要な金額(270万)」× 1.43

「稼がなければいけない金額」 = 386万

になります。これを12ヶ月で割ると大体32万円ぐらいです。

ただ、独立してやっていくと、会社員のような保証は無いので、体を壊して稼げなくなった時とかのための保険として貯蓄するようにもっと稼いでないとまずいです。
年金も、国民年金だけで、会社員のように厚生年金に入らないので、もらえる年金も少ないです。(年金システム自体が無くなるかもしれないけど)
なので余裕をもてるように一年に100万ぐらい貯蓄していくとして、だいたい月に40~50万ぐらいは稼いでおかないとやっていくのは厳しいです。

結論

アプリで食べていくためには月40~50万ぐらい稼がないと厳しい。

アプリで独立しようとか考えている人の参考になれば。

2013/03/03

ListViewのlayout_heightにwrap_contentを使ってはいけない

androidの開発でよく使うListViewのコンポーネントですが、使うときに注意する事があります。

レイアウト指定で、ListViewのlayout_heightの値にwrap_contentの値を使わないことです。

これをwrap_contentとかにしておくと、listviewにつけるadapterのgetView()メソッドが何度も呼び出されて無駄な処理が発生します。

同じListViewでlayout_heightの属性を変えた場合のLogを出してみました。

layout_height="match_parent"のとき

layout_height="wrap_content"のとき

実際にはcountが54まで繰り返し処理されていました。

原因はよくわからないですが、wrap_contentだと高さの値がコンテンツのサイズになるのでそこら辺が関係してるのかなと思います。

ちなみに,「layout_height="0dp" layout_weight="1"」でも 「layout_height="match_parent"」と同じ振る舞いで問題はありませんでした。

2013/02/28

Androidの自作CalendarView

AndroidでCalendarViewを使うにはいくつか方法があります。
  1. Android公式のandroid.widget.CalendarViewを使う
  2. CalendarView 公開しましたのCalendarViewを使う
  3. 自分で作る
1.に関してはAPIが11以上でないと使えないので、2.x系以上をサポートするアプリでは使えません。

2.に関してはページ送り、祝日情報などが含まれていてかなり有用なCalendarViewだと思います。

3.本当に最低限のCalendarViewでいいとか、後で色々細かいところを調整したいとかはこっちの方がいいのかなと思います。

今作っているアプリにCalendarView使いたいなってことで、上記の3件を考慮したのですが、今回作っているアプリに一番しっくりきそうなのは3.の形だったので、


のソースコードを元に作りました。(主にリファクタリングと、少しの調整)
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.view.Gravity;
import android.widget.LinearLayout;
import android.widget.TextView;

import <package_name>.R;

/**
 * 指定した年月日のカレンダーを表示するクラス
 */
public class CalendarView extends LinearLayout {
    @SuppressWarnings("unused")
    private static final String TAG = CalendarView.class.getSimpleName();
    
    private static final int WEEKDAYS = 7;
    private static final int MAX_WEEK = 6;
    
    // 週の始まりの曜日を保持する
    private static final int BIGINNING_DAY_OF_WEEK = Calendar.SUNDAY;
    // 今日のフォント色 
    private static final int TODAY_COLOR = Color.RED;
    // 通常のフォント色
    private static final int DEFAULT_COLOR = Color.DKGRAY;
    // 今週の背景色 
    private static final int TODAY_BACKGROUND_COLOR = Color.LTGRAY;
    // 通常の背景色 
    private static final int DEFAULT_BACKGROUND_COLOR = Color.TRANSPARENT;
    
    // 年月表示部分
    private TextView mTitleView; 
    
    // 週のレイアウト
    private LinearLayout mWeekLayout;
    private ArrayList<Linearlayout> mWeeks = new ArrayList<Linearlayout>();
    
    /**
     * コンストラクタ
     * 
     * @param context context
     */
    public CalendarView(Context context) {
        this(context, null);
    }
    
    /**
     * コンストラクタ
     * 
     * @param context context
     * @param attrs attributeset
     */
    @SuppressLint("SimpleDateFormat")
    public CalendarView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.setOrientation(VERTICAL);
        
        createTitleView(context);
        createWeekViews(context);
        createDayViews(context);
    }

    /**
     * 年月日表示用のタイトルを生成する
     * 
     * @param context context
     */
    private void createTitleView(Context context) {
        float scaleDensity = context.getResources().getDisplayMetrics().density;
        
        mTitleView = new TextView(context);
        mTitleView.setGravity(Gravity.CENTER_HORIZONTAL); // 中央に表示
        mTitleView.setTextSize((int)(scaleDensity * 14));
        mTitleView.setTypeface(null, Typeface.BOLD); // 太字
        mTitleView.setPadding(0, 0, 0, (int)(scaleDensity * 16));
        
        addView(mTitleView, new LinearLayout.LayoutParams(
            LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
    }

    /**
     * 曜日表示用のビューを生成する
     * 
     * @param context context
     */
    private void createWeekViews(Context context) {
        float scaleDensity = context.getResources().getDisplayMetrics().density;
        // 週表示レイアウト
        mWeekLayout = new LinearLayout(context);
        
        Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.DAY_OF_WEEK, BIGINNING_DAY_OF_WEEK); // 週の頭をセット
        
        for (int i = 0; i < WEEKDAYS; i++) {
            TextView textView = new TextView(context);
            textView.setGravity(Gravity.RIGHT); // 中央に表示
            textView.setPadding(0, 0, (int)(scaleDensity * 4), 0);
            
            LinearLayout.LayoutParams llp = 
                    new LinearLayout.LayoutParams(0, LayoutParams.WRAP_CONTENT);
            llp.weight = 1;
            
            mWeekLayout.addView(textView, llp);
            
            calendar.add(Calendar.DAY_OF_MONTH, 1);
        }
        addView(mWeekLayout, new LinearLayout.LayoutParams(
            LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
    }

    
    /**
     * 日付表示用のビューを生成する
     * 
     * @param context context
     */
    private void createDayViews(Context context) {
        float scaleDensity = context.getResources().getDisplayMetrics().density;
        
        // カレンダー部 最大6行必要
        for (int i = 0; i < MAX_WEEK; i++) {
            LinearLayout weekLine = new LinearLayout(context);
            mWeeks.add(weekLine);
            
            // 1週間分の日付ビュー作成
            for (int j = 0; j < WEEKDAYS; j++) {
                TextView dayView = new TextView(context);
                dayView.setGravity(Gravity.TOP | Gravity.RIGHT); 
                dayView.setPadding(0, (int)(scaleDensity * 4), (int)(scaleDensity * 4), 0);
                LinearLayout.LayoutParams llp = 
                        new LinearLayout.LayoutParams(0, (int)(scaleDensity * 48));
                llp.weight = 1;
                weekLine.addView(dayView, llp);
            }
            
            this.addView(weekLine, new LinearLayout.LayoutParams(
                LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
        }
    }
    
    /**
     * 年と月を指定して、カレンダーの表示を初期化する
     * 
     * @param year 年の指定
     * @param month 月の指定
     */
    public void set(int year, int month) {
        setTitle(year, month);
        setWeeks();
        setDays(year, month);
    }

    /**
     * 指定した年月日をタイトルに設定する
     * 
     * @param year 年の指定
     * @param month 月の指定
     */
    @SuppressLint("SimpleDateFormat")
    private void setTitle(int year, int month) {
        Calendar targetCalendar = getTargetCalendar(year, month);
        
        // 年月フォーマット文字列
        String formatString = mTitleView.getContext().getString(R.string.format_month_year);
        SimpleDateFormat formatter = new SimpleDateFormat(formatString);
        mTitleView.setText(formatter.format(targetCalendar.getTime()));
    }

    /**
     * 曜日を設定する
     */
    @SuppressLint("SimpleDateFormat")
    private void setWeeks() {
        Calendar week = Calendar.getInstance();
        week.set(Calendar.DAY_OF_WEEK, BIGINNING_DAY_OF_WEEK); // 週の頭をセット
        SimpleDateFormat weekFormatter = new SimpleDateFormat("E"); // 曜日を取得するフォーマッタ
        for (int i = 0; i < WEEKDAYS; i++) {
            TextView textView = (TextView) mWeekLayout.getChildAt(i);
            textView.setText(weekFormatter.format(week.getTime())); // テキストに曜日を表示
            week.add(Calendar.DAY_OF_MONTH, 1);
        }
    }

    /**
     * 日付を設定していくメソッド
     * 
     * @param year 年の指定
     * @param month 月の指定
     */
    private void setDays(int year, int month) {
        Calendar targetCalendar = getTargetCalendar(year, month);
        
        int skipCount = getSkipCount(targetCalendar);
        int lastDay = targetCalendar.getActualMaximum(Calendar.DATE);
        int dayCounter = 1;
        
        Calendar todayCalendar = Calendar.getInstance();
        int todayYear  = todayCalendar.get(Calendar.YEAR);
        int todayMonth = todayCalendar.get(Calendar.MONTH);
        int todayDay   = todayCalendar.get(Calendar.DAY_OF_MONTH);
        
        for (int i = 0; i < MAX_WEEK; i++) {
            LinearLayout weekLayout = mWeeks.get(i);
            weekLayout.setBackgroundColor(DEFAULT_BACKGROUND_COLOR);
            for (int j = 0; j < WEEKDAYS; j++) {
                TextView dayView = (TextView) weekLayout.getChildAt(j);
                
                // 第一週かつskipCountが残っていれば
                if (i == 0 && skipCount > 0) {
                    dayView.setText(" ");
                    skipCount--;
                    continue;
                }
                
                // 最終日より大きければ
                if (lastDay < dayCounter) {
                    dayView.setText(" ");
                    continue;
                }
                
                // 日付を設定
                dayView.setText(String.valueOf(dayCounter));
                
                boolean isToday = todayYear  == year  && 
                                  todayMonth == month && 
                                  todayDay   == dayCounter;
                
                if (isToday) {
                    dayView.setTextColor(TODAY_COLOR); // 赤文字
                    dayView.setTypeface(null, Typeface.BOLD); // 太字
                    weekLayout.setBackgroundColor(TODAY_BACKGROUND_COLOR); // 週の背景グレー
                } else {
                    dayView.setTextColor(DEFAULT_COLOR);
                    dayView.setTypeface(null, Typeface.NORMAL);
                }
                dayCounter++;
            }
        }
    }

    /**
     * カレンダーの最初の空白の個数を求める
     * 
     * @param targetCalendar 指定した月のCalendarのInstance
     * @return skipCount
     */
    private int getSkipCount(Calendar targetCalendar) {
        int skipCount; // 空白の個数
        int firstDayOfWeekOfMonth = targetCalendar.get(Calendar.DAY_OF_WEEK); // 1日の曜日
        if (BIGINNING_DAY_OF_WEEK > firstDayOfWeekOfMonth) {
            skipCount = firstDayOfWeekOfMonth - BIGINNING_DAY_OF_WEEK + WEEKDAYS;
        } else {
            skipCount = firstDayOfWeekOfMonth - BIGINNING_DAY_OF_WEEK;
        }
        return skipCount;
    }

    private Calendar getTargetCalendar(int year, int month) {
        Calendar targetCalendar = Calendar.getInstance();
        targetCalendar.clear(); // カレンダー情報の初期化
        targetCalendar.set(Calendar.YEAR, year);
        targetCalendar.set(Calendar.MONTH, month);
        targetCalendar.set(Calendar.DAY_OF_MONTH, 1);
        return targetCalendar;
    }
}

実際に使うときはこんなかんじで

CalendarView calendarView = (CalendarView) findViewById(R.id.carendar);
calendarView.set(2013, 3-1); // 2013年3月にセット

実際に生成されるViewは参考元とほぼ同じです。

後は、日付部分にListenerつけたり、ViewPagerにセットするなりで色々カスタマイズすれば、独自のカレンダーが作れます。

2013/02/07

SimpleCursorAdapterのListViewが即座に更新されない時に確認すべき箇所

SimpleCursorAdapterを使っているLIstViewが更新されなくて色々調べたので書く。

問題だったのは、ListViewのアイテムをクリックした時にSQLiteの該当データを削除する機能を実装していたんだけど、押しても即座に更新されなくてどーすんのかなーっと思って調べた。

よくあるのは
mAdapter.notifyDataSetChanged();
とかで更新するってあるけどこれはどうもArrayAdapterだけの時っぽい。

SimpleCursorAdapterのListView更新について
とかも試したけど、Cursor#requery()ってのがUIスレッドでの更新処理なので使うなって怒られたので、これもちがうなーと。

で、さらに調べてたら次の記事を見つけた。

CursorAdapterとContentProviderの関係

どうやら正しいContentProviderを作ってないとSimpleCursorAdapterが変更を検知できないらしい。

で、自分の作ったContentProvider見てみると、ContentProvider#query()で次のコードが抜けてた。
cursor.setNotificationUri(getContext().getContentResolver(), uri);
上のコードをqueryBuilderからcursorを取得した後に書いておかないとダメだった。

作るときに参考にしたサンプルにはちゃんと書かれていたので普通に自分のミスでした。

で、修正すると、即座に更新されるようになった。

他にもありそうなのはContentProvider#insert()とかで
getContext().getContentResolver().notifyChange(uri, null);
とかかな。

SimpleCursorAdapterを使っているListViewのアイテム要素にListenerをつける

AndroidでSimpleCursorAdapterを使っているListViewでレイアウトの中にListenerをつけたので、その備忘録として。

やりたかったのは、SimpleCursorAdapterを使っているListViewのItem要素の中に複数のclickできるViewがあり、それぞれのclickで異なる動作をすること。

ポイントは

  • ListViewの拡張
  • SimpleCursorAdapterの拡張

サンプルとして使うItem要素のxmlはこんな感じ
(今回はTextViewにlistenerをつける。
@stringと@dimensは別に定義してるもの)

list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingTop="8dp"
    android:paddingLeft="16dp"
    android:paddingRight="16dp"
    android:paddingBottom="8dp"
    android:orientation="vertical" >
    
    <TextView
        android:id="@+id/name_textview"
        android:layout_alignParentTop="true"
        android:layout_alignParentLeft="true"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="@dimen/text_size_large" />
    
    <TextView
        android:id="@+id/point_textview"
        android:layout_below="@id/name_textview"
        android:layout_alignParentLeft="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="@dimen/text_size_small" />
    
    <TextView
        android:id="@+id/delete_textview"
        android:layout_below="@id/name_textview"
        android:layout_alignParentRight="true"
        android:layout_width="48dp"
        android:layout_height="24dp"
        android:gravity="center"
        android:text="@string/textview_label_delete"
        android:textSize="@dimen/text_size_small" />
    
    <View
        android:id="@+id/separator_view_to_left_of_delete"
        android:layout_below="@id/name_textview"
        android:layout_toLeftOf="@id/delete_textview"
        android:layout_width="2dp"
        android:layout_height="24dp"
        android:background="@android:color/darker_gray" />
    
    <TextView
        android:id="@+id/edit_textview"
        android:layout_below="@id/name_textview"
        android:layout_toLeftOf="@id/separator_view_to_left_of_delete"
        android:layout_width="48dp"
        android:layout_height="24dp"
        android:gravity="center"
        android:text="@string/textview_label_edit"
        android:textSize="@dimen/text_size_small" />
    
    <View
        android:id="@+id/separator_view_to_left_of_edit"
        android:layout_below="@id/name_textview"
        android:layout_toLeftOf="@id/edit_textview"
        android:layout_width="2dp"
        android:layout_height="24dp"
        android:background="@android:color/darker_gray" />
    
    <TextView
        android:id="@+id/record_textview"
        android:layout_below="@id/name_textview"
        android:layout_toLeftOf="@id/separator_view_to_left_of_edit"
        android:layout_width="48dp"
        android:layout_height="24dp"
        android:gravity="center"
        android:text="@string/textview_label_record"
        android:textSize="@dimen/text_size_small" />
    
</RelativeLayout>

次にListViewを拡張したMyListView。
/**
 * リスト内にボタンを配置して、ボタンが押された時にonItemClickを通知するListView
 */
public class MyListView extends ListView implements OnClickListener {
    
    /**
     * コンストラクタ
     */
    public MyListView(Context context) {
        super(context);
    }
    
    /**
     * コンストラクタ
     */
    public MyListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public void onClick(View v) {
        int pos = (Integer)v.getTag();
        this.performItemClick(v, pos, v.getId());
    }
}

次にSimpleCursorAdapterを拡張したMyCursorAdapter
private class MyCursorAdapter extends SimpleCursorAdapter {

        public MyCursorAdapter(Context context, int layout, Cursor c,
                String[] from, int[] to, int flags) {
            super(context, layout, c, from, to, flags);
        }
        
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            // viewのセットなどはスーパークラスのメソッドに任せる
            View view = super.getView(position, convertView, parent);
            
            /*
             * それぞれのTextViewにpositionTagと
             * MyListViewのlistenerをつける
             */
            TextView deleteTextView = (TextView)view.findViewById(
                    R.id.delete_textview);
            deleteTextView.setTag(position);
            deleteTextView.setOnClickListener((MyListView)parent);
            
            TextView editTextView = (TextView)view.findViewById(
                    R.id.edit_textview);
            editTextView.setTag(position);
            editTextView.setOnClickListener((MyListView)parent);

            TextView recordTextView = (TextView)view.findViewById(
                    R.id.record_textview);
            recordTextView.setTag(position);
            recordTextView.setOnClickListener((MyListView)parent);
            return view;
        }
    }

これで、アイテム要素の中のonClick()でListViewのonItemClick()が呼ばれるようになるので、
onItemClick()の中でidで処理を切り替えればOK。
        @Override
        public void onItemClick(AdapterView<?> parent, View view, int pos, long id) {

            switch(view.getId()) {
                case R.id.delete_textview:
                    // 消去の時の処理
                    break;
                case R.id.edit_textview:
                    // 編集の時の処理
                    break;
                case R.id.record_textview:
                    // 記録の時の処理
                    break;
                default:
                    // 通常のonItemClick()の時の処理
                    break;
            }
        }

参考にした記事
ListViewの中のボタンのクリックイベントをActivityに通知する

2013/02/06

Androidのテキストサイズはdimens.xmlに書いておく

アプリ作ってる時のレイアウトで結構次のように書く場合が多い。

android:textSize="18sp"

直に書いてもいいんだけど、Googleさんが公式:Typography
 「テキストの大きさは限定したほうがいいよ。一つのアプリでテキストサイズがバラバラだと見づらいしね。Android Frameworkでは12sp、14sp、18sp、22spの4つに限定してる」
って言ってて、俺もGoogleさんに従うかな〜と最近はその4種類だけ使うようにしてます。

で、いっつもレイアウトファイルいじっているわけじゃないので、たまに数値を忘れるんだけど、こうすれば忘れないなーってのを思いついたのでメモ。

dimens.xml

    12sp
    14sp
    18sp
    22sp
あとは
android:textSize="@dimen/text_size_medium"
って書くだけでいい。

別にこの値じゃなくてもいいんだけど、種類は絞った方がいいのでdimens.xmlに書いて限定しておくといいと思う。

2013/01/27

Androidのアイデアシート作ってみた

Fireworksの使い方勉強してて、ある程度使い方覚えたので、練習兼ねてアイデアシート作ってみた。

iPhoneのアイデアシートは頂いたものはあるんだけど、Androidの無かったし、探してもGalaxyNexusのは無かったっぽいので、GalaxyNexusのアイデアシート。

こんな感じ


欲しい方はPDFどーぞ

2013/01/19

AndroidのLogを使う時のちょっと便利な方法

Androidで開発している時に、よく使うのがandroid.util.Logクラスですよね。

みなさんもよく使っていると思います。
    Log.d(TAG, "hogehoge");
こんな風に使うのが一般的なのですが、これをいろんな場所に組み込んでログを出力してると、後でめんどうなことになります。

リリースビルドした時も、このログ出力は消えないので、リリース前にはLogの部分を削除したりコメントアウトしたりといった事が必要になってきます。

Logを組み込んでる場所が5箇所とか10箇所ぐらいなら問題ありませんが、100箇所を超えるようになると、消す作業そのものが大変ですし、消し忘れなども出てきます。

そして、この問題を解決できる便利な方法があります。

ADT17から/genに生成されるようになったBuildConfig.javaを利用します。
public final class BuildConfig {
    public final static boolean DEBUG = true;
}
このDEBUGはデバッグビルドの時はtrueのままですが、リリースビルドの時falseになります。

なのでログを出力する際に以下のようにすると、開発してる時はログが出力され、リリースするときはログが出力されないようになります。
if (BuildConfig.DEBUG) {
    Log.d(TAG, "hogehoge");
}

さらに、いちいちログ出力するのに3行も書くのはちょっとめんどうなので、Loggerクラスを作りました。
public class Logger {
    
    private static final String TAG = "MyApplication";
    
    public static final void d(String msg) {
        if (BuildConfig.DEBUG) {
            Log.d(TAG, msg);
        }
    }
    
    public static final void d(String tag, String msg) {
        if (BuildConfig.DEBUG) {
            Log.d(tag, msg);
        }
    }
    
    public static final void e(String msg) {
        if (BuildConfig.DEBUG) {
            Log.e(TAG, msg);
        }
    }
    
    public static final void e(String tag, String msg) {
        if (BuildConfig.DEBUG) {
            Log.e(tag, msg);
        }
    }
    
    public static final void i(String msg) {
        if (BuildConfig.DEBUG) {
            Log.i(TAG, msg);
        }
    }
    
    public static final void i(String tag, String msg) {
        if (BuildConfig.DEBUG) {
            Log.i(tag, msg);
        }
    }
    
    public static final void v(String msg) {
        if (BuildConfig.DEBUG) {
            Log.v(TAG, msg);
        }
    }
    
    public static final void v(String tag, String msg) {
        if (BuildConfig.DEBUG) {
            Log.v(tag, msg);
        }
    }
    
    public static final void w(String msg) {
        if (BuildConfig.DEBUG) {
            Log.w(TAG, msg);
        }
    }
    
    public static final void w(String tag, String msg) {
        if (BuildConfig.DEBUG) {
            Log.w(tag, msg);
        }
    }
    
    public static final void heap(){
        heap(TAG);
    }
    
    public static final void heap(String tag) {
        if (BuildConfig.DEBUG){
            String msg = "heap : Free=" + Long.toString(Debug.getNativeHeapFreeSize() / 1024) + "kb" + 
                    ", Allocated=" + Long.toString(Debug.getNativeHeapAllocatedSize() / 1024) + "kb" + 
                    ", Size=" + Long.toString(Debug.getNativeHeapSize() / 1024) + "kb";
            
            Log.v(tag, msg);
        }
    }
}
このクラスを適当な場所に置いて、ログ出力したい箇所で
Logger.d("hogehoge");
// もしくは
Logger.d(TAG, "hogehoge");
とすると、いつもと同じようにログ出力ができます。もちろんリリースビルド時にはログが出力されません。
あと、heap()メソッドはヒープメモリの情報をログに出力するので、メモリ関連の調査をしてる時に役に立つかなとおもいます。

2012/12/18

これからのAndroidアプリはxhdpiとhdpi

今まではアプリの画像リソースを用意する時は、hdpiをメインに余力があれば、mdpiも用意するというのが主流でしたが、Android4.0以上の端末がたくさん発売されて、それにともない、高解像度の端末も増えてきました。

公式の情報によると2012年12月3日時点でldpiが約2%、mdpiが約18%、hdpiが約51%、xhdpiが約29%になっています。
これからはldpi、mdpiの端末が減っていき、xhdpiの端末が増えていくはずなので、画像リソースはhdpi、xhdpi用の2種類を用意するのがよさそうです。

xhdpiの端末でもhdpiの画像リソースがあれば、表示されるようになっていますが、表示する上で問題があります。

わざとhdpiにだけ画像をおいて、試してみます。
今回はAndroidのマーク(280x280の大きさ)の画像を用いました。
左が、hdpiで、右がxhdpiのキャプチャー画像です。


見た目の大きさを同じにするためにxhdpiの端末では画像がシステムによって拡大されています。
拡大するということは、画面に描画する際に画像処理が入るので、バッテリーとメモリを消費することになります。
今回のテストでは、画像が一つなので、あまり問題ありませんが、アプリをフルデザインしていると、アプリが強制終了する原因にもなります。

最近はアプリのデザイン自体は1280x720(xhdpi)で作ることが理想的(詳細は「デザイナーがコードから読み解く、Androidアプリのデザインの幅を広げるコツとTips」)なのですが、その大きさで作った画像素材を何も考えずにhdpiに入れて実装し、苦労してレイアウトを調整(viewのサイズをハードコーディングしたり)して、作ってたことがありました。そのアプリは、リリース後、リソースが見つからないなどのバグ報告がちょこちょこ上がってきていました。

そしてリソース画像の用意ですが、デザイナーさんがxhdpiとhdpiの2種類を用意してくれる時はそれでいいのですが、1280x720でデザインして、プログラマーのもとにやってくるのはxhdpi用の画像だけという場合があります。
自分が関わっているプロジェクトでも、xhdpi用の画像だけしか渡されませんでした。

なのでこちらですべての画像をリサイズしなければいけませんでした。

自分が画像をリサイズする際に、一つ一つ変換するのはとてもめんどくさかったので、macに標準で入っているAutomatorを使いました。

Windowsで開発されている方は、画像を一括変換できるフリーソフトがあると思うので、そちらを使ってください。

Automatorはあまり複雑なことはできませんが、単純な作業を任せるのに向いているので、まだ使ったことない人は色々ためしてみるといいですよ。

今回はxhdpiの画像をhdpiの画像に変換するための使い方を紹介します。
mdpiを1とした時に、hdpiは1.5、xhdpiは2の割合なので、xhdpiの画像を75%にしたものがhdpiの画像になります。

その作業をAutomatorでやってみます。
  1. まずは、変換作業に使うフォルダを作ります。どこでもいいので「xhdpi」と「hdpi」のフォルダをつくり「xhdpi」に画像リソースをすべて入れます。
  2. Automatorを起動し、「ワークフロー」を選択します。
  3. 左のメニューから「ファイルとフォルダ」→「Finder項目を検索」を選び、右のスペースにドラッグ&ドロップします。
  4. 「Finder項目を検索」の詳細を設定します。検索を「xhdpi」にして、「ファイル拡張子」が「png」「である」に設定します。
  5. 左のメニューから「写真」→「イメージをサイズ調整」を選び、右のスペースの「Finder項目を検索」の下にドラッグ&ドロップします。
  6. 途中「Finder項目のコピー」を追加するかどうかのダイアログが表示されるので、「追加」を選択します。
  7. 「Finder項目をコピー」の保存先を「hdpi」に設定します。
  8. 「イメージサイズを調整」で「比率(パーセント)指定」「75」に設定します。

上記の手順でxhdpiの画像からhdpiの画像を複製するワークフローの完成です。
あとは右上の「実行」ボタンをクリックすれば、処理が走って、すべての画像を複製してくれます。Automator便利!
xhdpiとhdpiで同じ名前の画像ファイルができるので、Androidプロジェクトのresフォルダにそのまま入れて大丈夫です。

2012/12/02

海外のサイトにレビュー依頼してみた。

本エントリーはAndroid Advent Calendar 2012の12/2裏エントリーです。

12/2の表エントリーは@rkisatoさんの「NOT専門家向け・Android一問一答」です。そちらもあわせて御覧ください。

今回はAndroidアプリを海外に向けて宣伝するために海外のレビューサイトに記事掲載依頼をしたのでそのレポートという形式になっています。

個人で作っているデベロッパーさん、また会社でアプリを作っている企業さんで海外に向けて宣伝したい人は参考になると思われます。

このエントリーを見ている人は個人でAndroidアプリを作ったことがあったり、会社でAndroidやってる人だと思います。

どうせなら、自分が作った、もしくは製作に関わったアプリはできるだけ多くのユーザに使ってもらいたいですよね。

現在、アプリを宣伝する方法は次のようにいろいろあります。

レビューサイト

OCTOBAさんとかAndroiderさんとかに依頼してレビュー記事を掲載してもらいます。
依頼して確実に掲載されるわけではないですが、無料です。

プレスリリースを打つ

個人のデベロッパーさんはなかなか打つのは難しいと思います。無料のもありますが無料のはそんなに効果ないです。

アドネットワーク

デベロッパーコンソールで「このアプリを宣伝する」ってやつですね。AdMobのバナーを通して、Androidユーザに向けて宣伝できます。以前5000円ぐらい突っ込んだことがありますが、一瞬でなくなって効果もあんまりなかったです。

成果報酬型広告(リワード広告とか)

成果報酬型の広告です。だいたい1DLあたり200~350円ぐらいが相場のようです。

この他にもいろいろ宣伝する方法はあるかと思いますが、たいていは多くのお金がかかるものです。(記事広告など)

比較的大きな会社や、お金が余ってるところなら、成果報酬型の広告とか記事広告とか売ってバンバンDL稼ぐこともできるかもしれませんが、僕のような個人デベロッパーには無理な話です。

そうなると、個人のデベロッパーはレビュー記事ぐらいしかありません。

日本のアプリレビューサイトへの依頼はすでにやってるよって方も多いかもしれませんが、海外のレビューサイトってなると、なかなかやっていない方も多いんじゃないでしょうか。

やらない理由としては2つの理由があると思います。
  1. どのサイトにレビュー依頼したらいいのかわからない。
  2. 英語が得意ではないので依頼の英文が書けない。
1.については英語のレビューサイトをまとめた資料がSlideShareに上がっているのでそちらを利用します。

2.については、僕も英文を書くとか全くできないので、格安で日本語を各言語に翻訳してくれるWebサービス「gengo」を使いました。


10月末ごろに、制作したランチャーアプリ「FlipLauncher」のレビュー記事依頼を海外のレビューサイトにお願いしたのでその流れと結果です。
(FlipLauncherのローカライズは英語だけ行なっています。)

まず、レビュー記事をお願いする英文を「gengo」で作成します。
gengo」は会員登録する必要がありますが、facebookのアカウントやgoogleのアカウントでもOKなので、開発用のアカウントですませばいいと思います。

gengoは翻訳にグレードが4つあります。

スタンダード
ネイティブスピーカーによる迅速かつ確かな翻訳

ビジネス
一般公開用および正式な用途に、さらにレベルの高い翻訳を

ウルトラ
最高のクオリティをお求めの方に、ビジネスレベルの翻訳+校正を

専門分野
専門分野のスペシャリストによる翻訳

レビュー記事の掲載依頼なのでスタンダードで十分だと思います。

で、スタンダードで訳しました。

日本文(1行目は件名として)
=======================================================
Androidアプリ[[[FlipLauncher]]]のレビューのお願い

こんにちは。

私は日本のAndroidアプリ開発者です。

サイトにレビュー記事を載せて頂きたく連絡しました。

レビュー記事に取り上げていただきたいアプリは以下です。

アプリ名:
[[[FlipLauncher]]]

アプリURL:
[[[https://play.google.com/store/apps/details?id=com.andcreate.app.fliplauncher]]]

アプリ概要:
[[[ ]]]

ユーザの使いやすさを第一に考えて作ったアプリです。

よろしくお願いします。
=======================================================

この日本文がこうなります。

gengo」で英訳された文
=======================================================
A request for a review of the android application [[[FlipLauncher]]]

Hello.

I am a Japanese android application developer.

I have contacted you to ask if our site could publish a review article.

The application I would like you to review is listed below.

Application name:
[[[FlipLauncher]]]

Application URL:
[[[https://play.google.com/store/apps/details?id=com.andcreate.app.fliplauncher]]]

Application outline:
[[[ ]]]

This is an application designed with ease of use in mind.

Thank you for your consideration.

=======================================================

英語に翻訳してほしくない箇所、翻訳する必要のない箇所は[[[ ]]]で囲めば翻訳されません。

この英訳を依頼してから、1時間ぐらいで英訳された文章が提案されてきました。修正して欲しい箇所がある場合は部分的に修正依頼することもできます。
この文章でかかった翻訳料金は351円でした。

アプリ概要の部分には、予め英訳しているGoogle Play のアプリ紹介文のところをそのまま突っ込みます。

アプリ紹介文の方は日本語419文字で翻訳料金1089円でした。

で英訳した文章をもとに各英語レビューサイトに依頼していきます。

まずは乗っている全メールアドレスにメールしていきます。

自分がやったときは一件一件クリックしてメーラーが立ち上がったり、リンクが無効だったりで大変めんどうだったので、コピーや手打ちでメールアドレスを持ってきてBCCでメールを送信しました。

メールアドレスはまとめてあるので使いたい人は「ここ」からどうぞ。
(いくつかエラーメールがかえってきました。すでに無効のメールアドレスもいくつかあるようです。)

次に、乗っているリンクをたどってフォーム上でレビューの依頼をしていきます。
「inquiry」をクリックするとそれぞれのサイトの申請フォームに直接飛ぶので必要事項を入力してポストします。(結構大変です)

大体どのフォームも氏名、メールアドレス、件名、本文なので、一時的にどこかにメモっておいてひたすらコピペしました。

以上で申請作業は終わりです。「FlipLauncher」のときは10/29日のお昼ぐらいにやりました。

ここから今まででどれくらい効果があったかを見てみます。

紹介されたサイト一覧

Android Police 10/29

smartdroid 10/29

noticias android 10/29

Android Spin 10/29

gizmode australia (list-up) 10/30

Android apps olicious 10/30

Droid LIFE 10/31

your life updated 11/01

letsgodroid 11/01

Android Headlines (list-up) 11/01

AndroidVZLA 11/02

makeuseof 11/03

一番早いところで、お昼に出したものが夜の9時ごろに紹介されてました。
それから続々といろんなサイトに取り上げてもらえて確認できているぶんで12サイトでした。

ゲームアプリレビューサイトを除く80サイトぐらいに依頼を出したので、打率は15%ぐらいです。今回のアプリがランチャーアプリだったので、もっと一般的なツールやゲームアプリだともっと取り上げてくれるかもしれません。

次にダウンロードの推移がどの程度変化したかを示します。
(図の赤線は2012/10/29を表しています。)


海外でレビュー記事が紹介されはじめたところからダウンロード数が増加しています。

そして、12/2現在で確認できる統計での総ダウンロード数は下図になります。



日本以外の合計は約19000インストールなのでかなり効果があったことがわかります。

今では1日の海外のダウンロード数も大分下がってきましたが、それでも一日平均100DLぐらいあります。海外のレビュー記事が掲載される前は一日平均5DLぐらいでした。

掛かったのは翻訳料金の約1500円と掲載依頼作業2時間ほどなので、海外ユーザにも使って欲しいアプリを作っている人は十分やる価値はあるとおもいます。

掲載依頼したところから「俺のところは今一日50件以上の依頼が来るから全てに対応するのは難しいんだよ。30ドルくれたら、優先してトップにレビュー記事掲載してあげるよ」みないな英文のメールが幾つかきてましたが、やりませんでした。それもやるともっとDL数が伸びるかもしれません。

2012/09/06

9patchの画像をdraw9patch以外で作成する方法

Androidのデザインで伸びて欲しい部分を指定できる便利な9patchという方法があります。

そしてこれはAndroidSDKの中に入っているdraw9patchで作るのですが、これがあまり使いやすいとは言えない。

そして今日

デザイナーがコードから読み解く、 Androidアプリのデザインの幅を広げるコツとTips【第1回】

に参加してきました。

その中で、発表者がdraw9patch以外では9patch画像をうまく作れないといっていたので、

ちょっと試してみました。

使った画像はこれです。

ちょっと前までプロジェクトの新規作成で自動生成されていたやつですね。



9patchは、画像の周囲1pixelの部分に黒点を描画することで、その部分が伸びるように認識させる事ができます。

なので72x72のものを74x74に拡大し、こんなふうに黒点を描画してみます。



上と左の黒点の交わった部分が伸びる部分で、右と下の黒点(もはや黒い線)の交わった部分が、内容を表示できるエリアです。

これを、PhotoShop CS5で作成しました。

それをリソースファイルに入れて、ImageViewの背景に指定し画面いっぱいに広げたらこんな風になりました。



ちゃんと伸びてますね。

ということで結論として、9patchの画像をdraw9patchで作ることは可能です。

ただ、気をつけなければいけない点があります。

一つ目はAndroidに9patchの画像として認識させるには次のような条件があります。

周囲1pixelを9patch専用の領域とする。

9patch専用の領域には完全な黒と完全な透明の2つ以外、存在してはいけない。

拡張子は「.9.png」とする

上の勉強会の発表者がうまく作れなかった原因は主に2つ目が原因ではないかなと思います。

PhotoshopやFireworksなどで9patch用の黒点を打つときに大きさ1pixelのペンで書くと思われますが、位置が少しずれると隣のpixelに少し影響が出ます。その影響で完全な黒でない部分や、うっすらと黒が描画されている部分が発生し、9patchの画像として認識されなかったのではと思っています。

で、今回この画像を作るときにはペンを使わず、9patchとして指定したい部分を選択範囲にし、その部分を完全な黒で塗りつぶすことによって作成しています。

あと、これは9patchの生成には関係のないことですが、

ImageViewのsrc属性に画像を設定しても、伸びてくれませんでした。

ImageViewのbackground属性に画像を設定したときに、上の画像のように9patchとして認識してくれました。

9patchは背景画像にしか使えないのかも知れないです。あ、でも背景に使えれば十分か。


以上、9patchの生成に関する記事でした。デザイナーさんはいつも使っている環境で9patchの画像を作ることができるので、挑戦してみてください。


2012/08/02

[Android]OutOfMemoryError(メモリリーク)対策

AndroidアプリのOutOfMemoryError(メモリリーク)の対策としてやったことのまとめ。

Androidアプリを作っている人ならOutOfMemoryErrorに悩まされた事のある人も多いと思います。

自分も悩まされて色々試行錯誤したので、そのへんをまとめておきます。

まず、OutOfMemoryErrorに関してですが、エラーレポートには大抵、画像の描画、ロード時に発生したと書かれていることが多いですが、画像が直接の原因じゃないことも結構多いです。(もちろん、単純に画像が大きすぎる、使用する画像の数が多すぎることもありますが、その場合はサイズの縮小などを行なってください。)

自分も最初は画像が原因だと思い込んでいたので、BitmapのConfigを変更してアルファ情報を抜いたりとかして使用するメモリ容量を減らして、「対策できた!」とか思ってました。

でも、あんまりエラーレポート減りませんでした。

で、色々試行錯誤していくうちに画像が原因じゃないなとわかりました。

アプリを起動すると、様々なオブジェクトが生成されます。色々動かしたあと、アプリを終了します。アプリを終了したあとに、端末のメモリが少なくなってきたらGCが走って使っていないオブジェクトを削除してくれるのですが、アプリを終了した時に色々オブジェクトの参照が残っていたりすると、GCで開放されません。

そして、次回にアプリを起動した時に、残っているオブジェクトが再利用されるかというと、全く再利用されずに新しいオブジェクトが次々に生成されます。

これの繰り返しで、ごみオブジェクトがどんどん溜まっていき、それと比例して、アプリが使用するメモリ領域を圧迫していきます。

そして、何回目かの起動時にメモリを大きく使用する処理(画像のロードとか)がきっかけでOutOfMemoryErrorが発生します。

なので開発時のデバッグとかでは、ほとんど見つからなく(実機にインストールする段階でアプリが使用するメモリ領域が初期化されるので)、リリース後にエラーレポートが送られてくることになります。やっかいですね。

で、今回はごみオブジェクトをなるべく残さないようにするためにごみオブジェクトの調べ方と参照の消し方とかを書いておきます。

まず、調べ方についてですが、Memory Analyzerを使います。

インストールについては次のページを参照してください
Eclipseで使うMemory Analyzerのインストール

まず、調べたいアプリを起動して、色々動かしてから戻るボタンなどでアプリを終了させます。

そしてDDMSを開いて、Memory Analyzerを起動します。

「Dump HPROF file」をクリックすると初回起動時はダイアログが表示されます。とりあえず、一番上の「Leak Suspects Report」を押します。

すると、次のような画面が表示されるので、図の部分をクリックして「Histogram」を表示します。



Histogramでは、アプリが保持しているオブジェクトの一覧が次の図のような感じで表示されます。


<Regex>のところにアプリのパッケージ名を入れると、アプリのオブジェクトがどのくらい開放されずに残っているかが表示されます。


アプリが終了したのに、オブジェクトの参照が残っているとこんなかんじになります。


起動と終了を繰り返すとどんどん溜まっていきます。


このようにどんどん、ごみオブジェクトを保持し続けていき、メモリを圧迫していきます。

アプリの終了時に参照が残っていなければ、何回起動と終了を繰りかえしても、ごみオブジェクトは残りません。

なので、アプリが終了した時に参照が残らないようにします。

メモリリークの原因として有名なのはActivityの参照が残り続けることですが、Activity単体が残るだけであれば、そこまで大きな問題ではありません。
問題はActivityが参照しているクラスのインスタンスが残り続けることです。複雑な作りのアプリほど残りやすいです。

自分が行ったのはActivity#onDestroy()やFragment#onDestroyView()で次のように明示的に参照を切りました。
自分が使ってたのが、Fragment, ViewPager, ListViewぐらいなので、それらの参照の切り方だけ書いています。

/**
 * onDestroy()やonDestroyView()で
 */

//Fragmentのとき
fragment = null;

//ViewPagerのとき
viewPager.setAdapter(null);
adapter = null;

//ListViewのとき
listView.setAdapter(null);
adapter = null;

//各種リスナー
button.setOnClickListener(null);

特に、カスタムしたView,Adapter,Listenerなどは明示的に参照を切らないと残り続けるようです。

自分の場合、このように明示的に参照を切ってあげる事でほとんどオブジェクトが残らなくなりました。

検索かけてもあまりこの辺の具体的な情報はあまり見つからなかったので(見つけられなかっただけかもしれない...)、OutOfMemoryError(メモリリーク)で困っている人の手助けになれば幸いです。

ちなみに今回対策したアプリはこちらです。「顔文字BANK」
良ければ、DLして使ってください。

2012/04/30

ViewFlipperを使ってリストビューのレイアウトを切り替える

Androidのアプリでよく使われるListViewについて。

すべての要素が同じなら普通にListAdapterをセットすればいいんだけど、間になにか要素を挟みたいときとかなかなか苦労する。ListViewの要素を再利用するためにViewHolderとか使ってるとレイアウトが固定されてしまうので。

今回はViewHolderを使いつつある箇所でListViewの要素のレイアウトを切り替えることができたのでメモ。

ビューの切り替え自体はViewFlipperを使う。

リストビューの1つの要素のXMLはこんな感じ(object.xml)
<?xml version="1.0" encoding="utf-8"?>
<ViewFlipper xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/flipper"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent" >

    <!-- ViewFlipperの初期画面(1ページ目) -->
    <LinearLayout 
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" >

        <TextView
            android:id="@+id/text_view"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent" />
        
    </LinearLayout>

    <!-- ViewFlipperの2ページ目 -->
    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" >

        <Button
            android:id="@+id/button"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent" />
        
    </LinearLayout>
</ViewFlipper>

テスト用のアクティビティのXML(main.xml)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <ListView
        android:id="@+id/listview"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" >
        
    </ListView>

</LinearLayout>

そしてサンプルコードはこんな感じ(MainActivity.java)
package com.andcreate.sample.viewflipperlistview;

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

import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.ViewFlipper;

public class MainActivity extends Activity{
    
    @Override
    public void onCreate(Bundle bundle){
        super.onCreate(bundle);
        setContentView(R.layout.main);
        
        ListView listView = (ListView)findViewById(R.id.listview);
        
        List<ObjectData> objects = new ArrayList<ObjectData>();
        for(int i = 0; i < 50; i++){
            if(i % 5 == 0){
                objects.add(new ObjectData("text:" + i, "button:" + i, false));
            }
            else{
                objects.add(new ObjectData("text:" + i, "button:" + i, true));
            }
        }
        
        listView.setAdapter(new SampleAdapter(this, objects));
    }
    
}

class ObjectData{
    String textStr;
    String buttonStr;
    boolean isBtn; //ボタンを表示するかどうか
    
    public ObjectData(String textStr, String buttonStr, boolean isBtn){
        this.textStr = textStr;
        this.buttonStr = buttonStr;
        this.isBtn = isBtn;
    }
}

class ViewHolder{
    ViewFlipper flipper;
    TextView textView;
    Button button;
}

class SampleAdapter extends ArrayAdapter<ObjectData>{
    private LayoutInflater inflater;
    
    public SampleAdapter(Context context, List<ObjectData> objects){
        super(context, 0, objects);
        this.inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }
    
    @Override
    public View getView(int position, View convertView, ViewGroup parent){
        ViewHolder holder;
        if(convertView == null){
            convertView = inflater.inflate(R.layout.object, parent, false);
            holder = new ViewHolder();
            holder.flipper = (ViewFlipper)convertView.findViewById(R.id.flipper);
            holder.textView = (TextView)convertView.findViewById(R.id.text_view);
            holder.button = (Button)convertView.findViewById(R.id.button);
            convertView.setTag(holder);
        }else{
            holder = (ViewHolder)convertView.getTag();
        }
        ObjectData data = getItem(position);
        if(data.isBtn){
            //ボタンなら
            holder.button.setText(data.buttonStr);
            holder.flipper.setDisplayedChild(1); //2ページ目を表示
        }else{
            //テキストビューなら
            holder.textView.setText(data.textStr);
            holder.flipper.setDisplayedChild(0); //1ページ目を表示
        }
        return convertView;
    }
}


ViewHolderの切り替え自体はViewHolder#showNext()やViewHolder#showPrevious()でやることが多いけど、今回の場合それでやると表示がうまくいかなかったのでViewHolder#setDisplayChild()を使った。
今回の場合、ViewFlipperなので、動的に要素の高さを変更することはできない。できたらなかなかすごいけど何かいい方法があるのかな?

2012/02/23

Windows7でGitのインストール

Windows7でのGitのインストール方法のメモ


Gitはソースコードのバージョン管理システムの一つ。ほかにも有名なものでSubversionとかCVSとかあるけど、いまからやるならGit一択っぽい感じ。


そして、Windows7の環境でGit環境を構築する正式な方法はCygwinを使う方法なんだけど、自分を含めて初心者には敷居が高いっぽい。


手軽にやる方法として、「msysgit」ってのがあるのでそちらを使ってGit環境を構築する。


まずはインストーラをダウンロードする。


ここから「Downloads」ページへジャンプし、「Git-1.7.9-preview20120201.exe」をダウンロード。


インストールウィザードに従って進めていく。


途中の「Adjusting your PATH environment」画面で、





「Run Git from the Windows Command Prompt」を選択。


次の「Configuring the line ending conversions」はデフォルトのままでOK。


でインストールが終わる。


正しくインストールできたかどうかはコマンドプロンプトで


git --version


と打ち込んで確認できます。


次のような画面がでれば成功です。


2012/02/07

学生スタートアップカンファレンスに参加してきた

今日参加してきたのはコレ→http://atnd.org/events/24340
学生スタートアップカンファレンス

ハッシュタグは#gakucon

学生で起業してる人たちが自分たちの事業紹介して色々質問も受けますよってやつ。

全部じゃないけどメモれた分だけ書いてみます。

今日発表してたのはfacematch、Creatty、hhhungry、ヒルカツ、tetolの5つ。

【facematch】
facebookの情報持ってきて、異性との出会いを手助けするアプリ。
例えばユーザが男性なら、友達の友達の女の子とかどっかのイベントに参加してた女の子がアプリに表示されて、プロフィールなどが見られる。
プロフィール見て気に入った女の子がいたらハートボタンを押す。
押された女の子の方では、ユーザを含む9人の男性が画面に表示されて、この中の誰かがあなたの事を気になっています。って表示されるらしい。
それで、女の子がその中から選んだりして、両思いになれたら、その二人だけで使えるクーポンが発行されるらしい。

で、今はコアメンバー4人とインターン16人で活動中で人材募集中のようです。

求めてる人材は
・自分たちのビジョンに共感できる人
・私たちと一緒に働きたいと思える人
・私たちの持っていない何かの高い能力を持っている事
らしいです。

人と人とのつながりを濃くしていきたいって言ってました。
日本でのリリースは3月予定。


【Creatty】
物を作っている人向けのサービス
クリエイターや美大生なんかで物を作っている人がweb上でサイトやギャラリーを簡単に持てるようにするサービス。
そしてそれらを欲しい人が購入できるようにするらしい。
作る人にとっては、自分の作品をより多くの人に見てもらえたりするし、見る人にとっては世界に1つしかない作品のカタログを見る事ができると。
マネタイズはプレミアム課金、売買手数料、企業タイアップでやっていくらしい。

ベータ版を2月に公開予定

サーバー/フロントサイド/iOS/Androidなどのエンジニアを募集してた。


【hhhungry】
食事行くときのめんどくささを解決するサービス。
今おなかが減っている近くにいる友達が表示されて、簡単にさそって一緒にご飯に行く事ができるサービス。
今開催されているランチを友達の人は見る事ができて、そこに合流したければ簡単にできるよってのも言ってた。

おなかが減ってるとかどうやって調べるのとか色々疑問あったし、あまり流行りそうではないなって印象を発表を見て思った。


【ヒルカツ】
昼食の時間を使ってOBOG訪問できるサービス。
学生は友人とペアを組んで、志望している業界の人とかとマッチングしてくれる。
新しいタイプの就職活動と言っていた。

ただもうソーシャルランチとかのサービスがあるから、正直厳しいんじゃないかなって思った。もしかしたらヒルカツ側でのすごい差別点とかあるのかもしれないけど。

あと、発表が残念すぎた。笑いをとろうとしてやってたけどそこまで笑いとれてなかったし、無駄な内容に時間を割きすぎてた。最終的には来てる視聴者の方に振りだしたりでひどい印象をうけた。


【tetol】
GPSとスマートフォンを利用したソーシャル助け合いプラットフォーム
スマートフォンのアプリから依頼内容と報酬と期限をGooglemapに投稿して、近くのユーザがそれを見て、依頼を受けたりできるサービス。
大学などの場合は「代返願い」や、「ノートコピーさせて」などで、それに対して報酬500円など。(紹介の例としてどうなのと思ったが。)
労働力のCtoCと言っていた。(BtoCはタウンワークみたいなバイト紹介システム)
収益は手数料や、フリーミアム、広告料でと言っていた。
2月末にクローズドβ版を公開予定。
発表者が「日本を変えるのは学生」って言ってた。今日一番印象に残ってる。

〜最後に〜
自分と同じぐらいの学生がこうやってベンチャーキャピタルからお金をもらって事業を起こしてるのはやっぱりすごい。
そんなにうまくいかないんじゃないかと思ったのもあったけど、何かのサービスを考えだしてそれに全力で取り組んでるってところはなかなかできるものじゃない。
彼らの方が行動力も技術力も格段に上なんだろうと感じた。自分はまだまだ。
よりいっそうの努力が必要だなと感じたので、勉強と開発頑張っていく。

2012/01/19

進路について

大学もそろそろ卒業となり、進路についてずっと悩んでましたが、やっと自分なりの答えを出したので、ここにまとめておこうと思います。
大学4回生のときに、経済的な理由から大学を退学になりました。
ここから一年間、実家に戻って親の仕事を手伝いながら、Androidアプリの開発の勉強を始めました。そして2011年1月に初めてアプリを公開し、その後もいくつかアプリを開発しマーケットに公開してきました。
そんな中、大学へ復学するための資金をなんとか準備する事ができ、2011年の9月に大学へ復学する事ができました。
しかし、就職活動を全くしてなかったので、何となく自分はこのまま大学院へ行くのがベストなのかなと思っていました。大学院へ進めば一年後に問題なく就職活動ができるので。
ただ、最近いろんな本を読んだり、twitterなどでいろんなツイートを見て、「働く」ってなんなんだろうなって感じてました。
日本の会社でのプログラマーってのは降りてきた仕様通りにプログラムするのがお仕事で、だんだん出世していくとSEやマネージャーになってどんどんプログラムを書く事から離れていくイメージを持っています。
会社に属してプログラムを書いていくのは正直楽しくなさそう。
そんなふうに色々考えていく中で、自分の中で「働く」ってことは、「誰かの役に立つ」「価値を生み出す」これが大切なんじゃないかなと。
こう思ってから、必ずどこかに就職しなきゃいけないって思いが無くなりました。
なので、研究して知識を深めたいってのが3割、就活のためってのが7割だった大学院の進学も考え直そうと思いました。
復学してから約半年、卒業研究をやってきて、研究というスタイルは自分に合っていなかった。これを後2年も続けるのはあまり意味を見いだせないし面白くないなと。なにより今はアプリ事業にもっと専念したいなと思っていました。
なので卒業後はアプリ事業に専念しつつ、技術力の向上を目指していきます。
一人でやってるとなにかと病みそうなので、勉強会に参加して様々な人に会って刺激もらいつつやっていきたいなと思っています。
「誰かの役に立つ」「価値を生み出す」そんなアプリやサービスを創り出していきたい。
しばらくは関東方面で活動していこうと思っていますので、関東方面にいらっしゃる方はよろしくお願いします。

2011/07/18

Androidの広告をまとめてみた

最近Androidもどんどんユーザー数が増えていき、アプリ開発者にとってはそれなりに広告収入を得ている人もいると思います。

これから広告を載せてみたいけど、どれを使えばいいのかわからない。とか、他の広告に乗り換えてみようかなってデベロッパーの為に、いろんな広告会社を簡単にまとめてみようと思います。


自分が使ったことのある、または組み込んだことのある広告会社のみです。
また、私のアプリが日本専用アプリなので、海外での効果はわかりません。

まとめるのは、
「AdMob」
「AdMaker」
「AdLantis」
「AD-STA」
「TG-Ad」
「Rin」
の6つです。

AdMob

メリット
・google adsenseの広告も配信しているので表示される広告の種類が多い。
・アプリのメインカラーに合わせて広告の背景色の変更が可能。
・管理画面が見やすく、カテゴリごとにフィルタをかけられる。
デメリット
・手数料がかかる電信送金かpaypalでの支払いしかない。
・eCPMの変化が激しい(2011/7でeCPMが2.78→0.57へ)

スマートフォンの広告といえばこれが一番有名かと思われます。
ただ広告のインプレッション数は広告表示依頼数が高くなればなるほど多くなるようで、あまり依頼数が多くないアプリでは広告の表示回数が少なくなる傾向があるようです。

AdMaker

メリット
・高い広告単価(ちょっと前まで9.3円で固定。現在は7~9.3円ぐらい)
・medibaとの提携で広告の種類が多い。(バナー広告も豊富)
・銀行口座に直接振り込み
デメリット
・レポート画面や広告配信システムで不具合が多い。
・人の審査が入るので、広告配信の許可が降りるまで時間がかかる。

ほぼ100パーセントのインプレッション率を誇るアドネットワーク。
広告の表示も速く、単価が高いので有名です。請求書などをこちらで発行しなくても、銀行に振り込んでくれる手軽さが良いです。

AdLantis

メリット
・高い広告単価(ちょっと前まで7.5円で固定だったかが、現在は7.5~15円ぐらい)
デメリット
・広告収益の請求方法がHPに掲載されておらず、メールでのお知らせ。
・請求書を発送しないと収益が振り込まれない。

広告配信システムなどではAdMakerといい勝負をします。ただ、請求方法に難ありです。メールで「請求書を発送していただいた翌月末支払い、発送されなかった場合は翌々月末支払い」とあり、請求書を送っていないので、翌々月末支払いされるかどうか待っている段階です。
個人的にはAdMakerと比べて、請求書を作成して送るのが非常にめんどくさいです。

ここからは組み込んでみたものの、公開までいたらなかったものです。

AD-STA

メリット
・どんな広告が表示されるのか管理画面で見ることができる。
・広告ごとに配信の可否が設定可能。
デメリット
・広告表示までが少し遅め。
・広告の種類が他と比べて少ない(2011/7/18現在、広告主はmobageのみ)

広告掲載依頼があったアドネットワーク。
まだ一般には公開されていないようで、まだまだ様子見の段階です。
一般に公開されるようになり、広告の種類が増えてきたら化けるかも。

TG-Ad

メリット
・唯一のインプレッション毎に広告単価が発生するアドネットワーク。
デメリット
・審査が多い

このアドネットワークはアプリを登録するときに審査。
アプリに広告配信システムを組み込んでからまた審査。
と、とにかく審査が多く、公開に踏み切れませんでした。
もし、TG-Ad使っている方がいたら、どんなかんじなのか教えていただきたいです。

Rin

メリット
・興味関心連動型広告(らしいです。)
デメリット
・テキスト広告しかない。
・定期的に広告を更新し、その際にアプリの処理が0.5~1秒とまる。
・2011/7/18現在、美容液の広告一つだけ。

ホームページがかっこよく、興味関心連動型広告ということで期待していたアドネットワーク。
今の段階では、まったく使えません。
自動的に広告の更新が行われ、その際の処理がUIスレッドで行われるようで、アプリが一瞬止まります。
この仕様が改善され、広告の種類が増えれば使えるようにはなるかも。


以上、自分が使ってみた経験を参考にまとめてみました。



国内限定で考えると、私が現時点でお勧めするのはAdMakerです。
いろいろ不具合も多いですが、問い合わせをすれば、今のところ丁寧に対応してもらっています。
不具合が発生しないように改善してもらうのが一番なんですが。
あと、特にこちらからのアクションなしで自動的に銀行の口座に振り込んでもらえるのが非常に楽です。

最近出てきた、AD-STAとRinには結構期待しています。
これから広告の種類が増加し、安定してきたら試してみようと思います。

このまとめが、どこかの誰かの役に立てば幸いです。

2011/06/15

顔文字入力補助

「顔文字入力補助」専用のページです。

「顔文字入力補助」は顔文字を簡単に入力することができるマッシュルームアプリです。

質問や要望などはお気軽にコメントへお願いします。