2012年2月24日金曜日

Android TextView の setTextSize() は sp 単位だよ!

いままでも sp という単位が用意されていましたが、標準の設定アプリでシステムの文字スケールを 設定できないかったので、ほぼ dp と同じような振る舞いでした。
Android 4.0 の設定アプリではシステム全体の文字スケールが設定できるようになっています。

[Display] - [Font size]



これによって sp 単位で指定した値はシステム全体の文字スケールに応じて実際のピクセル数が変わります。

Small


Normal


Large


Extra large


で、ここからが本題。

TextView の setTextSize() メソッドの引数は sp 単位として処理されます。

---
public void setTextSize (float size)

Set the default text size to the given value, interpreted as "scaled pixel" units. This size is adjusted based on the current density and user font size preference.

Related XML Attributes
android:textSize

Parameters
size The scaled pixel size.
---

コンテンツの本文とかはこれでいいのですが、ヘッダー部分などシステム全体の文字スケールに影響されたくない部分もあります。 そういう場合に dp など sp 以外の単位で指定するには、引数を2つとる setTextSize() を使います。

---
public void setTextSize (int unit, float size)

Set the default text size to a given unit and value. See TypedValue for the possible dimension units.

Related XML Attributes
android:textSize

Parameters
unit The desired dimension unit.
size The desired size in the given units.
---

第1引数で単位として TypedValue の定数を指定します。

  • COMPLEX_UNIT_DIP : dp, dip
  • COMPLEX_UNIT_MM : mm
  • COMPLEX_UNIT_PT : pt
  • COMPLEX_UNIT_PX : raw pixels
  • COMPLEX_UNIT_SP : sp
public class MainActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); DisplayMetrics matrics = getResources().getDisplayMetrics(); float density = matrics.density; float scaledDensity = matrics.scaledDensity; ((TextView)findViewById(R.id.density)).setText("density : " + density); ((TextView)findViewById(R.id.scaledDensity)).setText("scaledDensity : " + scaledDensity); TextView tv1 = (TextView) findViewById(R.id.text1); tv1.setText("setTextSize(20)"); tv1.setTextSize(20); TextView tv2 = (TextView) findViewById(R.id.text2); tv2.setText("setTextSize(TypedValue.COMPLEX_UNIT_DIP, 20)"); tv2.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 20); } }




■ sp, から pt, px に変換する

dp から pt に変換する方法(Android dip, dp, から pt, px に変換する)と同じように

getContext().getResources().getDisplayMetrics().scaledDensity

で文字スケールも考慮された比率がとれるので、 これを sp 単位の値にかければ pt, px 単位になります。


2012年2月23日木曜日

Android DrawableState でリストアイテムの背景を変える ViewGroup を作る

アイテムを選択可能なリスト用として、 CheckedTextView を使った

android.R.layout.simple_list_item_single_choice

<?xml version="1.0" encoding="utf-8"?> <!-- Copyright (C) 2008 The Android Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --> <CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/text1" android:layout_width="match_parent" android:layout_height="?android:attr/listPreferredItemHeightSmall" android:textAppearance="?android:attr/textAppearanceListItemSmall" android:gravity="center_vertical" android:checkMark="?android:attr/listChoiceIndicatorSingle" android:paddingLeft="?android:attr/listPreferredItemPaddingLeft" android:paddingRight="?android:attr/listPreferredItemPaddingRight" />



android.R.layout.simple_list_item_multiple_choice

<?xml version="1.0" encoding="utf-8"?> <!-- Copyright (C) 2008 The Android Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --> <CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/text1" android:layout_width="match_parent" android:layout_height="?android:attr/listPreferredItemHeight" android:textAppearance="?android:attr/textAppearanceListItem" android:gravity="center_vertical" android:checkMark="?android:attr/listChoiceIndicatorMultiple" android:paddingLeft="8dip" android:paddingRight="8dip" />

が用意されていて

public class MainActivity extends ListActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); String[] data = { "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data", }; ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_single_choice, android.R.id.text1, data); setListAdapter(adapter); ListView lv = getListView(); lv.setChoiceMode(ListView.CHOICE_MODE_SINGLE); } }

のように使うことで



のようにできます。


さて、

1行のレイアウトを複雑にしたい場合、内部に CheckBox を持ってその表示を変える方法に ついては Android Layout Cookbook に書きました。

CheckBox ではなく、単に1行の ViewGroup の背景色を変えて選択状態を変えたい場合は Checkable を implements した ViewGroup を作ります。

例えばリストの1行を LinearLayout にして、選択されたら背景を変えるようにするには、 このように LinearLayout を継承したクラスを作って Checkable を実装します。

CheckedLinearLayout.java public class CheckedLinearLayout extends LinearLayout implements Checkable { private boolean mChecked; private static final int[] CHECKED_STATE_SET = { android.R.attr.state_checked }; public CheckedLinearLayout(Context context) { this(context, null); } public CheckedLinearLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CheckedLinearLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override public void toggle() { setChecked(!mChecked); } @Override public boolean isChecked() { return mChecked; } @Override public void setChecked(boolean checked) { if (mChecked != checked) { mChecked = checked; refreshDrawableState(); } } @Override protected int[] onCreateDrawableState(int extraSpace) { final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); if (isChecked()) { mergeDrawableStates(drawableState, CHECKED_STATE_SET); } return drawableState; } }

Checkable インタフェースのメソッドは toggle(), isChecked(), setChecked() だけなのですが、 これを実装するだけでは背景は変わってくれません。
キモは onCreateDrawableState() で、ここで super.onCreateDrawableState() で LinearLayout の drawableState を取得し、それにチェック状態の state を mergeDrawableStates() で追加したものを返す必要があります。

MainActivity.java public class MainActivity extends ListActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); String[] data = { "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data", }; ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, R.layout.list_item, android.R.id.text1, data); setListAdapter(adapter); ListView lv = getListView(); lv.setChoiceMode(ListView.CHOICE_MODE_SINGLE); } }

list_item.xml <?xml version="1.0" encoding="utf-8"?> <yanzm.example.drawablestatesample.CheckedLinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/list_item_bg" android:gravity="center_vertical" android:orientation="vertical" android:padding="8dip" > <TextView android:id="@android:id/text1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceListItem" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="sub text" android:textAppearance="?android:attr/textAppearanceListItemSmall" /> </yanzm.example.drawablestatesample.CheckedLinearLayout>

list_item.bg <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_checked="true"> <shape> <solid android:color="#666666" /> </shape></item> <item> <shape> <solid android:color="#00000000" /> </shape> </item> </selector>

こんな感じにタップした行の背景が変わるようになります。





2012年2月15日水曜日

Android コードから PopupWindow を生成するときは setBackgroundDrawable() したほうがいいよ

こんにちは。お久しぶりです。
原稿とか原稿とかメルマガとかで、ご無沙汰してたのですが、ぼちぼち再開します。
原稿の行方についてはそのうちご報告できると思います。(早くはじめにを書けよ、というry)

---


PopupWindow には、setOutsideTouchable(true) を指定すると、PopupWindow の外の領域をタップしたときにポップアップを閉じる処理が実装されています。

PopupWindow.java#1578 1578 @Override 1579 public boolean onTouchEvent(MotionEvent event) { 1580 final int x = (int) event.getX(); 1581 final int y = (int) event.getY(); 1582 1583 if ((event.getAction() == MotionEvent.ACTION_DOWN) 1584 && ((x < 0) || (x >= getWidth()) || (y < 0) || (y >= getHeight()))) { 1585 dismiss(); 1586 return true; 1587 } else if (event.getAction() == MotionEvent.ACTION_OUTSIDE) { 1588 dismiss(); 1589 return true; 1590 } else { 1591 return super.onTouchEvent(event); 1592 } 1593 }

また、setTouchInterceptor() で View.OnTouchListener を指定すると、ポップアップのタッチを取得することができます。
ちなみに、setOutsideTouchable(true) を指定しないと、ポップアップの外側をタッチしたときにリスナーが呼ばれません。

PopupWindow.java#1570 1570 @Override 1571 public boolean dispatchTouchEvent(MotionEvent ev) { 1572 if (mTouchInterceptor != null && mTouchInterceptor.onTouch(this, ev)) { 1573 return true; 1574 } 1575 return super.dispatchTouchEvent(ev); 1576 }

問題は、これらポップアップのタッチイベントが PopupWindow の mBackground 変数が null の時は呼ばれない!!!ということなのです。

レイアウトXMLファイルから PopupWindow を生成した場合は

177 public PopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 178 mContext = context; 179 mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); 180 181 TypedArray a = 182 context.obtainStyledAttributes( 183 attrs, com.android.internal.R.styleable.PopupWindow, defStyleAttr, defStyleRes); 184 185 mBackground = a.getDrawable(R.styleable.PopupWindow_popupBackground);

のようにデフォルトの背景が指定されるので問題ないのですが、コードから生成する場合は PopupWindow の setBackgroundDrawable() メソッドを読んで mBackground に Drawable をセットしないと

312 public Drawable getBackground() { 313 return mBackground; 314 }

setOutsideTouchable(true) を指定してもポップアップの周りをタップしても自動で dismiss() してくれないし、さらには setTouchInterceptor() で指定したリスナーの onTouch() も呼ばれなくなります!

実は、これらのタッチイベントの処理 (onTouchEvent や dispatchTouchEvent) は PopupWindow のサブクラスの PopupViewContainer の中に実装されています。この PopupViewContainer が mBackground が null 以外のときだけ使うような実装になっているのです。

PopupWindow.java#944 944 private void preparePopup(WindowManager.LayoutParams p) { ... 950 if (mBackground != null) { ... 960 PopupViewContainer popupViewContainer = new PopupViewContainer(mContext); 961 PopupViewContainer.LayoutParams listParams = new PopupViewContainer.LayoutParams( 962 ViewGroup.LayoutParams.MATCH_PARENT, height 963 ); 964 popupViewContainer.setBackgroundDrawable(mBackground); 965 popupViewContainer.addView(mContentView, listParams); 966 967 mPopupView = popupViewContainer; 968 } else { 969 mPopupView = mContentView; 970 } ... 973 }

別に背景なくても PopupViewContainer 使ってくれればいいのにー。

ということで、とりあえず PopupWindow は背景指定したほうがいいよー。