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.の形だったので、


のソースコードを元に作りました。(主にリファクタリングと、少しの調整)
  1. import java.text.SimpleDateFormat;  
  2. import java.util.ArrayList;  
  3. import java.util.Calendar;  
  4.   
  5. import android.annotation.SuppressLint;  
  6. import android.content.Context;  
  7. import android.graphics.Color;  
  8. import android.graphics.Typeface;  
  9. import android.util.AttributeSet;  
  10. import android.view.Gravity;  
  11. import android.widget.LinearLayout;  
  12. import android.widget.TextView;  
  13.   
  14. import <package_name>.R;  
  15.   
  16. /** 
  17.  * 指定した年月日のカレンダーを表示するクラス 
  18.  */  
  19. public class CalendarView extends LinearLayout {  
  20.     @SuppressWarnings("unused")  
  21.     private static final String TAG = CalendarView.class.getSimpleName();  
  22.       
  23.     private static final int WEEKDAYS = 7;  
  24.     private static final int MAX_WEEK = 6;  
  25.       
  26.     // 週の始まりの曜日を保持する  
  27.     private static final int BIGINNING_DAY_OF_WEEK = Calendar.SUNDAY;  
  28.     // 今日のフォント色   
  29.     private static final int TODAY_COLOR = Color.RED;  
  30.     // 通常のフォント色  
  31.     private static final int DEFAULT_COLOR = Color.DKGRAY;  
  32.     // 今週の背景色   
  33.     private static final int TODAY_BACKGROUND_COLOR = Color.LTGRAY;  
  34.     // 通常の背景色   
  35.     private static final int DEFAULT_BACKGROUND_COLOR = Color.TRANSPARENT;  
  36.       
  37.     // 年月表示部分  
  38.     private TextView mTitleView;   
  39.       
  40.     // 週のレイアウト  
  41.     private LinearLayout mWeekLayout;  
  42.     private ArrayList<Linearlayout> mWeeks = new ArrayList<Linearlayout>();  
  43.       
  44.     /** 
  45.      * コンストラクタ 
  46.      *  
  47.      * @param context context 
  48.      */  
  49.     public CalendarView(Context context) {  
  50.         this(context, null);  
  51.     }  
  52.       
  53.     /** 
  54.      * コンストラクタ 
  55.      *  
  56.      * @param context context 
  57.      * @param attrs attributeset 
  58.      */  
  59.     @SuppressLint("SimpleDateFormat")  
  60.     public CalendarView(Context context, AttributeSet attrs) {  
  61.         super(context, attrs);  
  62.         this.setOrientation(VERTICAL);  
  63.           
  64.         createTitleView(context);  
  65.         createWeekViews(context);  
  66.         createDayViews(context);  
  67.     }  
  68.   
  69.     /** 
  70.      * 年月日表示用のタイトルを生成する 
  71.      *  
  72.      * @param context context 
  73.      */  
  74.     private void createTitleView(Context context) {  
  75.         float scaleDensity = context.getResources().getDisplayMetrics().density;  
  76.           
  77.         mTitleView = new TextView(context);  
  78.         mTitleView.setGravity(Gravity.CENTER_HORIZONTAL); // 中央に表示  
  79.         mTitleView.setTextSize((int)(scaleDensity * 14));  
  80.         mTitleView.setTypeface(null, Typeface.BOLD); // 太字  
  81.         mTitleView.setPadding(000, (int)(scaleDensity * 16));  
  82.           
  83.         addView(mTitleView, new LinearLayout.LayoutParams(  
  84.             LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));  
  85.     }  
  86.   
  87.     /** 
  88.      * 曜日表示用のビューを生成する 
  89.      *  
  90.      * @param context context 
  91.      */  
  92.     private void createWeekViews(Context context) {  
  93.         float scaleDensity = context.getResources().getDisplayMetrics().density;  
  94.         // 週表示レイアウト  
  95.         mWeekLayout = new LinearLayout(context);  
  96.           
  97.         Calendar calendar = Calendar.getInstance();  
  98.         calendar.set(Calendar.DAY_OF_WEEK, BIGINNING_DAY_OF_WEEK); // 週の頭をセット  
  99.           
  100.         for (int i = 0; i < WEEKDAYS; i++) {  
  101.             TextView textView = new TextView(context);  
  102.             textView.setGravity(Gravity.RIGHT); // 中央に表示  
  103.             textView.setPadding(00, (int)(scaleDensity * 4), 0);  
  104.               
  105.             LinearLayout.LayoutParams llp =   
  106.                     new LinearLayout.LayoutParams(0, LayoutParams.WRAP_CONTENT);  
  107.             llp.weight = 1;  
  108.               
  109.             mWeekLayout.addView(textView, llp);  
  110.               
  111.             calendar.add(Calendar.DAY_OF_MONTH, 1);  
  112.         }  
  113.         addView(mWeekLayout, new LinearLayout.LayoutParams(  
  114.             LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));  
  115.     }  
  116.   
  117.       
  118.     /** 
  119.      * 日付表示用のビューを生成する 
  120.      *  
  121.      * @param context context 
  122.      */  
  123.     private void createDayViews(Context context) {  
  124.         float scaleDensity = context.getResources().getDisplayMetrics().density;  
  125.           
  126.         // カレンダー部 最大6行必要  
  127.         for (int i = 0; i < MAX_WEEK; i++) {  
  128.             LinearLayout weekLine = new LinearLayout(context);  
  129.             mWeeks.add(weekLine);  
  130.               
  131.             // 1週間分の日付ビュー作成  
  132.             for (int j = 0; j < WEEKDAYS; j++) {  
  133.                 TextView dayView = new TextView(context);  
  134.                 dayView.setGravity(Gravity.TOP | Gravity.RIGHT);   
  135.                 dayView.setPadding(0, (int)(scaleDensity * 4), (int)(scaleDensity * 4), 0);  
  136.                 LinearLayout.LayoutParams llp =   
  137.                         new LinearLayout.LayoutParams(0, (int)(scaleDensity * 48));  
  138.                 llp.weight = 1;  
  139.                 weekLine.addView(dayView, llp);  
  140.             }  
  141.               
  142.             this.addView(weekLine, new LinearLayout.LayoutParams(  
  143.                 LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));  
  144.         }  
  145.     }  
  146.       
  147.     /** 
  148.      * 年と月を指定して、カレンダーの表示を初期化する 
  149.      *  
  150.      * @param year 年の指定 
  151.      * @param month 月の指定 
  152.      */  
  153.     public void set(int year, int month) {  
  154.         setTitle(year, month);  
  155.         setWeeks();  
  156.         setDays(year, month);  
  157.     }  
  158.   
  159.     /** 
  160.      * 指定した年月日をタイトルに設定する 
  161.      *  
  162.      * @param year 年の指定 
  163.      * @param month 月の指定 
  164.      */  
  165.     @SuppressLint("SimpleDateFormat")  
  166.     private void setTitle(int year, int month) {  
  167.         Calendar targetCalendar = getTargetCalendar(year, month);  
  168.           
  169.         // 年月フォーマット文字列  
  170.         String formatString = mTitleView.getContext().getString(R.string.format_month_year);  
  171.         SimpleDateFormat formatter = new SimpleDateFormat(formatString);  
  172.         mTitleView.setText(formatter.format(targetCalendar.getTime()));  
  173.     }  
  174.   
  175.     /** 
  176.      * 曜日を設定する 
  177.      */  
  178.     @SuppressLint("SimpleDateFormat")  
  179.     private void setWeeks() {  
  180.         Calendar week = Calendar.getInstance();  
  181.         week.set(Calendar.DAY_OF_WEEK, BIGINNING_DAY_OF_WEEK); // 週の頭をセット  
  182.         SimpleDateFormat weekFormatter = new SimpleDateFormat("E"); // 曜日を取得するフォーマッタ  
  183.         for (int i = 0; i < WEEKDAYS; i++) {  
  184.             TextView textView = (TextView) mWeekLayout.getChildAt(i);  
  185.             textView.setText(weekFormatter.format(week.getTime())); // テキストに曜日を表示  
  186.             week.add(Calendar.DAY_OF_MONTH, 1);  
  187.         }  
  188.     }  
  189.   
  190.     /** 
  191.      * 日付を設定していくメソッド 
  192.      *  
  193.      * @param year 年の指定 
  194.      * @param month 月の指定 
  195.      */  
  196.     private void setDays(int year, int month) {  
  197.         Calendar targetCalendar = getTargetCalendar(year, month);  
  198.           
  199.         int skipCount = getSkipCount(targetCalendar);  
  200.         int lastDay = targetCalendar.getActualMaximum(Calendar.DATE);  
  201.         int dayCounter = 1;  
  202.           
  203.         Calendar todayCalendar = Calendar.getInstance();  
  204.         int todayYear  = todayCalendar.get(Calendar.YEAR);  
  205.         int todayMonth = todayCalendar.get(Calendar.MONTH);  
  206.         int todayDay   = todayCalendar.get(Calendar.DAY_OF_MONTH);  
  207.           
  208.         for (int i = 0; i < MAX_WEEK; i++) {  
  209.             LinearLayout weekLayout = mWeeks.get(i);  
  210.             weekLayout.setBackgroundColor(DEFAULT_BACKGROUND_COLOR);  
  211.             for (int j = 0; j < WEEKDAYS; j++) {  
  212.                 TextView dayView = (TextView) weekLayout.getChildAt(j);  
  213.                   
  214.                 // 第一週かつskipCountが残っていれば  
  215.                 if (i == 0 && skipCount > 0) {  
  216.                     dayView.setText(" ");  
  217.                     skipCount--;  
  218.                     continue;  
  219.                 }  
  220.                   
  221.                 // 最終日より大きければ  
  222.                 if (lastDay < dayCounter) {  
  223.                     dayView.setText(" ");  
  224.                     continue;  
  225.                 }  
  226.                   
  227.                 // 日付を設定  
  228.                 dayView.setText(String.valueOf(dayCounter));  
  229.                   
  230.                 boolean isToday = todayYear  == year  &&   
  231.                                   todayMonth == month &&   
  232.                                   todayDay   == dayCounter;  
  233.                   
  234.                 if (isToday) {  
  235.                     dayView.setTextColor(TODAY_COLOR); // 赤文字  
  236.                     dayView.setTypeface(null, Typeface.BOLD); // 太字  
  237.                     weekLayout.setBackgroundColor(TODAY_BACKGROUND_COLOR); // 週の背景グレー  
  238.                 } else {  
  239.                     dayView.setTextColor(DEFAULT_COLOR);  
  240.                     dayView.setTypeface(null, Typeface.NORMAL);  
  241.                 }  
  242.                 dayCounter++;  
  243.             }  
  244.         }  
  245.     }  
  246.   
  247.     /** 
  248.      * カレンダーの最初の空白の個数を求める 
  249.      *  
  250.      * @param targetCalendar 指定した月のCalendarのInstance 
  251.      * @return skipCount 
  252.      */  
  253.     private int getSkipCount(Calendar targetCalendar) {  
  254.         int skipCount; // 空白の個数  
  255.         int firstDayOfWeekOfMonth = targetCalendar.get(Calendar.DAY_OF_WEEK); // 1日の曜日  
  256.         if (BIGINNING_DAY_OF_WEEK > firstDayOfWeekOfMonth) {  
  257.             skipCount = firstDayOfWeekOfMonth - BIGINNING_DAY_OF_WEEK + WEEKDAYS;  
  258.         } else {  
  259.             skipCount = firstDayOfWeekOfMonth - BIGINNING_DAY_OF_WEEK;  
  260.         }  
  261.         return skipCount;  
  262.     }  
  263.   
  264.     private Calendar getTargetCalendar(int year, int month) {  
  265.         Calendar targetCalendar = Calendar.getInstance();  
  266.         targetCalendar.clear(); // カレンダー情報の初期化  
  267.         targetCalendar.set(Calendar.YEAR, year);  
  268.         targetCalendar.set(Calendar.MONTH, month);  
  269.         targetCalendar.set(Calendar.DAY_OF_MONTH, 1);  
  270.         return targetCalendar;  
  271.     }  
  272. }  

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

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

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

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

2013/02/07

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

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

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

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

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

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

CursorAdapterとContentProviderの関係

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

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

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

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

他にもありそうなのはContentProvider#insert()とかで
  1. 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
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="wrap_content"  
  5.     android:paddingTop="8dp"  
  6.     android:paddingLeft="16dp"  
  7.     android:paddingRight="16dp"  
  8.     android:paddingBottom="8dp"  
  9.     android:orientation="vertical" >  
  10.       
  11.     <TextView  
  12.         android:id="@+id/name_textview"  
  13.         android:layout_alignParentTop="true"  
  14.         android:layout_alignParentLeft="true"  
  15.         android:layout_width="match_parent"  
  16.         android:layout_height="wrap_content"  
  17.         android:textSize="@dimen/text_size_large" />  
  18.       
  19.     <TextView  
  20.         android:id="@+id/point_textview"  
  21.         android:layout_below="@id/name_textview"  
  22.         android:layout_alignParentLeft="true"  
  23.         android:layout_width="wrap_content"  
  24.         android:layout_height="wrap_content"  
  25.         android:textSize="@dimen/text_size_small" />  
  26.       
  27.     <TextView  
  28.         android:id="@+id/delete_textview"  
  29.         android:layout_below="@id/name_textview"  
  30.         android:layout_alignParentRight="true"  
  31.         android:layout_width="48dp"  
  32.         android:layout_height="24dp"  
  33.         android:gravity="center"  
  34.         android:text="@string/textview_label_delete"  
  35.         android:textSize="@dimen/text_size_small" />  
  36.       
  37.     <View  
  38.         android:id="@+id/separator_view_to_left_of_delete"  
  39.         android:layout_below="@id/name_textview"  
  40.         android:layout_toLeftOf="@id/delete_textview"  
  41.         android:layout_width="2dp"  
  42.         android:layout_height="24dp"  
  43.         android:background="@android:color/darker_gray" />  
  44.       
  45.     <TextView  
  46.         android:id="@+id/edit_textview"  
  47.         android:layout_below="@id/name_textview"  
  48.         android:layout_toLeftOf="@id/separator_view_to_left_of_delete"  
  49.         android:layout_width="48dp"  
  50.         android:layout_height="24dp"  
  51.         android:gravity="center"  
  52.         android:text="@string/textview_label_edit"  
  53.         android:textSize="@dimen/text_size_small" />  
  54.       
  55.     <View  
  56.         android:id="@+id/separator_view_to_left_of_edit"  
  57.         android:layout_below="@id/name_textview"  
  58.         android:layout_toLeftOf="@id/edit_textview"  
  59.         android:layout_width="2dp"  
  60.         android:layout_height="24dp"  
  61.         android:background="@android:color/darker_gray" />  
  62.       
  63.     <TextView  
  64.         android:id="@+id/record_textview"  
  65.         android:layout_below="@id/name_textview"  
  66.         android:layout_toLeftOf="@id/separator_view_to_left_of_edit"  
  67.         android:layout_width="48dp"  
  68.         android:layout_height="24dp"  
  69.         android:gravity="center"  
  70.         android:text="@string/textview_label_record"  
  71.         android:textSize="@dimen/text_size_small" />  
  72.       
  73. </RelativeLayout>  

次にListViewを拡張したMyListView。
  1. /** 
  2.  * リスト内にボタンを配置して、ボタンが押された時にonItemClickを通知するListView 
  3.  */  
  4. public class MyListView extends ListView implements OnClickListener {  
  5.       
  6.     /** 
  7.      * コンストラクタ 
  8.      */  
  9.     public MyListView(Context context) {  
  10.         super(context);  
  11.     }  
  12.       
  13.     /** 
  14.      * コンストラクタ 
  15.      */  
  16.     public MyListView(Context context, AttributeSet attrs) {  
  17.         super(context, attrs);  
  18.     }  
  19.   
  20.     @Override  
  21.     public void onClick(View v) {  
  22.         int pos = (Integer)v.getTag();  
  23.         this.performItemClick(v, pos, v.getId());  
  24.     }  
  25. }  

次にSimpleCursorAdapterを拡張したMyCursorAdapter
  1. private class MyCursorAdapter extends SimpleCursorAdapter {  
  2.   
  3.         public MyCursorAdapter(Context context, int layout, Cursor c,  
  4.                 String[] from, int[] to, int flags) {  
  5.             super(context, layout, c, from, to, flags);  
  6.         }  
  7.           
  8.         @Override  
  9.         public View getView(int position, View convertView, ViewGroup parent) {  
  10.             // viewのセットなどはスーパークラスのメソッドに任せる  
  11.             View view = super.getView(position, convertView, parent);  
  12.               
  13.             /* 
  14.              * それぞれのTextViewにpositionTagと 
  15.              * MyListViewのlistenerをつける 
  16.              */  
  17.             TextView deleteTextView = (TextView)view.findViewById(  
  18.                     R.id.delete_textview);  
  19.             deleteTextView.setTag(position);  
  20.             deleteTextView.setOnClickListener((MyListView)parent);  
  21.               
  22.             TextView editTextView = (TextView)view.findViewById(  
  23.                     R.id.edit_textview);  
  24.             editTextView.setTag(position);  
  25.             editTextView.setOnClickListener((MyListView)parent);  
  26.   
  27.             TextView recordTextView = (TextView)view.findViewById(  
  28.                     R.id.record_textview);  
  29.             recordTextView.setTag(position);  
  30.             recordTextView.setOnClickListener((MyListView)parent);  
  31.             return view;  
  32.         }  
  33.     }  

これで、アイテム要素の中のonClick()でListViewのonItemClick()が呼ばれるようになるので、
onItemClick()の中でidで処理を切り替えればOK。
  1. @Override  
  2. public void onItemClick(AdapterView<?> parent, View view, int pos, long id) {  
  3.   
  4.     switch(view.getId()) {  
  5.         case R.id.delete_textview:  
  6.             // 消去の時の処理  
  7.             break;  
  8.         case R.id.edit_textview:  
  9.             // 編集の時の処理  
  10.             break;  
  11.         case R.id.record_textview:  
  12.             // 記録の時の処理  
  13.             break;  
  14.         default:  
  15.             // 通常のonItemClick()の時の処理  
  16.             break;  
  17.     }  
  18. }  

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

2013/02/06

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

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

  1. android:textSize="18sp"  

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

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

dimens.xml
  1. <resources>  
  2.     <dimen name="text_size_micro">12sp</dimen>  
  3.     <dimen name="text_size_small">14sp</dimen>  
  4.     <dimen name="text_size_medium">18sp</dimen>  
  5.     <dimen name="text_size_large">22sp</dimen>  
  6. </resources>  
あとは
  1. android:textSize="@dimen/text_size_medium"  
って書くだけでいい。

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