2011年1月20日木曜日

アプリのデータを保存する(プリファレンス編)

前回 アプリのデータを保存する(基本編)では、ファイルの保存について説明しましたが、
今回は「設定値の保存」を可能にする「プリファレンス」について学んでいきたいと思います。

プリファレンスの形式とデータ型

プリファレンスは、「"キー" => 値」という形式でデータを保存します。
キーと値には5種類の型があり、これはJavaの「変数と値」の関係と似ています。

保存できるデータ型は以下の通りです。

1. boolean
2. float
3. int
4. long
5. String

データの保存方法

以下のように、SharedPreferences の SharedPreferences.Editor でデータを保存します。
SharedPreferences pref =
    getSharedPreferences("pref", MODE_WORLD_READABLE | MODE_WORLD_WRITEABLE);
Editor e = pref.edit();
e.putString("key", "value");
e.commit();

オペレーションモードは3種類

プリファレンスでは、MODE_APPENDを除いた3種類のオペレーションモードが指定可能です。

定数説明
MODE_PRIVATE作成したアプリのみ読み書きできる
MODE_WORLD_READABLE他アプリに読み込み権を与える
MODE_WORLD_WRITEABLE他アプリに書き込み権を与える

プリファレンスの実体

また、プリファレンスの実体は、内部記憶媒体に保存されたXMLファイルです。
中身は、以下のように、キーが<string>タグの name 属性、値が value 属性または要素値という形になっています。

※コマンドラインから表示
# adb shell
# cat /data/data/"パッケージ名"/shared_prefs/"ファイル名".xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <boolean name="key_boolean" value="false" />
    <float name="key_float" value="0.0" />
    <int name="key_int" value="0" />
    <long name="key_long" value="0" />
    <string name="key_String">value</string>
</map>

3種類のプリファレンス作成方法

代表的なプリファレンス作成方法は、以下の3種類があります。
いずれも、戻り値は SharedPreferences です。

1. Context#getSharedPreferences(String name, int mode)
2. Activity#getPreferences(int mode)
3. PreferenceManager.getDefaultSharedPreferences(Context context)

具体的な使用方法は以下になります。
package com.blogspot.androlab.example;

import android.app.Activity;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import android.preference.PreferenceManager;

public class MainActivity extends Activity {
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);

  // 代表的なプリファレンス作成方法は3種類ある
  SharedPreferences prefNameFree;
  SharedPreferences prefNameClass;
  SharedPreferences prefNamePackage;

  // 任意のファイル名で作成する方法
  prefNameFree = getSharedPreferences("shared", MODE_WORLD_READABLE | MODE_WORLD_WRITEABLE);
  Editor e = prefNameFree.edit();
  e.putString("key", "value");
  e.commit();

  // アクティビティごとに作成する方法
  prefNameClass = getPreferences(MODE_PRIVATE);
  e = prefNameClass.edit();
  e.putString("key", "value");
  e.commit();

  // デフォルトプリファレンスを作成する方法(常に MODE_PRIVATE)
  prefNamePackage = PreferenceManager.getDefaultSharedPreferences(this);
  e = prefNamePackage.edit();
  e.putString("key", "value");
  e.commit();
 }
}

※コマンドラインから表示
# adb shell
# ls /data/data/com.blogspot.androlab.example/shared_prefs/
com.blogspot.androlab.example_preferences.xml
MainActivity.xml
shared.xml

プリファレンスの削除方法

設定値の削除は、以下の2つのメソッドを用います。
SharedPreferences.Editor#remove(String key)
SharedPreferences.Editor#commit()

一括削除は、以下の2つのメソッドを用います。
SharedPreferences.Editor#clear()
SharedPreferences.Editor#commit()

なお、ファイル自体を削除するメソッドはありません。
アンインストールしてアプリごと消すか、以下のコードをご参考ください。
public static void deleteSharedPreferences(Context context) {
  try {
   ApplicationInfo info =
    context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);
   File dir = new File(info.dataDir.concat(File.separator).concat("shared_prefs").concat(File.separator));
   if (dir.isDirectory())
    for (String file : dir.list()) new File(dir, file).delete();
  } catch (NameNotFoundException e) {
   e.printStackTrace();
  }
}

プリファレンスを初期化する

最後に、プリファレンスの初期化について説明します。

まず、以下の2つのXMLファイルを用意します。
これは、ユーザによる設定が行われていない場合のプリファレンスの初期値になります。

/res/xml/default_values.xml
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
 android:key="applicationPreference" android:title="Weight Class Settings">
 <ListPreference
  android:key="weight_class"
  android:defaultValue="126"
  android:title="Hozumi Hasegawa"
  android:summary="Featherweight"
  android:entries="@array/weight_classes"
  android:entryValues="@array/weight_pounds"
  android:dialogTitle="Please set his weight class." />
</PreferenceScreen>

/res/values/arrays.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
 <string-array name="weight_classes">
  <item>Featherweight</item>
  <item>Super bantamweight</item>
  <item>Bantamweight</item>
 </string-array>
 <string-array name="weight_pounds">
  <item>126</item>
  <item>122</item>
  <item>118</item>
 </string-array>
</resources>

次に、下記のメソッドで上記のXMLファイルを指定します。

PreferenceManager.setDefaultValues(Context context, String sharedPreferencesName, int sharedPreferencesMode, int resId, boolean readAgain)

MainActivity.java
package com.blogspot.androlab.example;

import java.io.File;

import android.app.Activity;
import android.content.Context;
import android.os.Bundle;

public class MainActivity extends Activity {
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);

  PreferenceManager.setDefaultValues(this, "weight_class", MODE_PRIVATE, R.xml.default_values, true);
 }
}

以上の手順を踏むと、プリファレンスの実体で説明した形式に変換されて、内部記憶領域に保存されます。

※コマンドラインから表示
# adb shell
# cat /data/data/com.blogspot.androlab.example/shared_prefs/weight_class.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
  <string name="weight_class">126</string>
</map>

2011年1月19日水曜日

オブジェクト指向な ListView の使い方(前編)

以下のような ListView のサンプルがあったとします。
package com.blogspot.androlab.example;

import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;

import android.app.Activity;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.ListView;

/** メインアクティビティです。 */
public class MainActivity extends Activity {
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);

  List<Schedule> scheduleList = new ArrayList<Schedule>();

  // リストに値を挿入
  scheduleList.add(new Schedule("2011/01/19", "通常業務", "業務報告", "懇談会"));
  scheduleList.add(new Schedule("2011/01/20", "通常業務", "業務報告"));
  scheduleList.add(new Schedule("2011/01/21", "通常業務", "週末報告", "日本 vs カタール"));
  scheduleList.add(new Schedule("2011/01/24", "朝礼", "工場視察"));

  // リストに表示する文字列を配列に格納
  List<String> sList = new ArrayList<String>(scheduleList.size());
  for (Schedule s : scheduleList)
   sList.add(s.getDateString() + " [ " + s.size() + "件 ]");

  // 作成した配列で ArrayAdapter を作成
  ArrayAdapter<String> arrayAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, sList);

  // ListView を作成
  ListView listView = new ListView(this);
  listView.setAdapter(arrayAdapter);

  // レイアウト
  LinearLayout linearLayout = new LinearLayout(this);
  linearLayout.addView(listView, new LinearLayout.LayoutParams(
    LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
  setContentView(linearLayout);
 }
}

/** 日程クラスです。 */
class Schedule extends ArrayList<Task> {
 private static final long serialVersionUID = 1L;
 private Date mDate;
 Schedule(String date, String... tasks) {
  mDate = new Date(date);
  for (String task : tasks) add(new Task(task));
 }
 String getDateString() {
  DateFormat df = DateFormat.getDateInstance(DateFormat.LONG, Locale.JAPANESE);
  return df.format(mDate);
 }
}

/** 仕事の最小単位です。 */
class Task {
 private String mSubject;
 Task(String subject) {
  mSubject = subject;
 }
 public String toString() {
  return mSubject;
 }
}

実行結果

スケジュールの日にち(Schedule#getDateString())と件数(Schedule#size())が、一行づつ出力されています。
このほかに、内容(Task t = Schedule#get(n))を含めて表示させたいときは、どうしたらよいでしょうか。

強引に30行目付近を、
sList.add(s.getDateString() + " [ " + s.size() + "件 ]\n" + s.get(0) + ", " + s.get(1));
のようにして、文字列を連結してしまうことも可能ですが、ちょっとかっこ悪いですよね。

こういうときは、ArrayAdapter#getView(int, View, ViewGroup) をオーバライドして対応させると、非常にスマートになります。

package com.blogspot.androlab.example;

import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;

import android.app.Activity;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.TwoLineListItem;

public class MainActivity extends Activity {
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);

  List<Schedule> scheduleList = new ArrayList<Schedule>();

  // リストに値を挿入
  scheduleList.add(new Schedule("2011/01/19", "通常業務", "業務報告", "懇談会"));
  scheduleList.add(new Schedule("2011/01/20", "通常業務", "業務報告"));
  scheduleList.add(new Schedule("2011/01/21", "通常業務", "週末報告", "日本 vs カタール"));
  scheduleList.add(new Schedule("2011/01/24", "朝礼", "工場視察"));

  // 作成した配列で ArrayAdapter を作成
  ArrayAdapter<Schedule> arrayAdapter = new ArrayAdapter<Schedule>(this, 0, scheduleList) {
   private LayoutInflater mInflater;
   // 初期化子(コンストラクタの代わり)
   {
    mInflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
   }
   // 表示する「一行分のView」を返すメソッド
   @Override
   public View getView(int position, View convertView, ViewGroup parent) {
    // 表示する一行分のViewには、android.R.layout.simple_list_item_2
    // (中身は TwoLineListItem) を利用する
    Schedule s = getItem(position);
    TwoLineListItem view = (TwoLineListItem) mInflater.inflate(android.R.layout.simple_list_item_2, null);

    // 1/2行分のTextView(上部)
    TextView text1 = view.getText1();
    text1.setText(s.getDateString() + " [ " + s.size() + "件 ]");

    // 1/2行分のTextView(下部)
    TextView text2 = view.getText2();
    text2.setSingleLine(true);
    String tasks = ""; for (Task t : s) tasks += t + ", ";
    text2.setText(tasks.substring(0, tasks.length() - 2));

    return view;
   }
  };

  // ListView を作成
  ListView listView = new ListView(this);
  listView.setAdapter(arrayAdapter);

  // レイアウト
  LinearLayout linearLayout = new LinearLayout(this);
  linearLayout.addView(listView, new LinearLayout.LayoutParams(
    LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
  setContentView(linearLayout);
 }
}

/** 日程クラスです。 */
class Schedule extends ArrayList<Task> {
 private static final long serialVersionUID = 1L;
 private Date mDate;
 Schedule(String date, String... tasks) {
  mDate = new Date(date);
  for (String task : tasks) add(new Task(task));
 }
 String getDateString() {
  DateFormat df = DateFormat.getDateInstance(DateFormat.LONG, Locale.JAPANESE);
  return df.format(mDate);
 }
}

/** 仕事の最小単位です。 */
class Task {
 private String mSubject;
 Task(String subject) {
  mSubject = subject;
 }
 public String toString() {
  return mSubject;
 }
} 

実行結果
うまくいきました。
プログラムの保守性も考えて、なるべく、オブジェクト単位で処理していきたいですね。

次回は、リスト選択時のアイテムの扱い方をご紹介します。

2011年1月10日月曜日

アプリのデータを保存する(基本編)

Androidは、永続的なアプリケーションのデータを保存するため、以下の記憶媒体に関するAPIを端末に提供しています。

1. 内部記憶媒体(デバイスメモリ)
Androidがインストールされている場所です。 一般には端末に内蔵されています。
2. 外部記憶媒体
SDカードなどの共有された外部ストレージです。
3. ネットワークサーバ
ネットワーク内のホストコンピュータです。 インターネット経由でWebサービスを読み込んだりなどの用途で利用します。

1 と 2 は、OSのファイルシステムにより一元化されますが、プログラム上は区別して扱います。
3 は、サーバの有無や都合に左右されますので、一般にアプリのデータ保存として用いられる記憶媒体としては 1 と 2 でしょう。

注意したいのは、Androidはアプリごとにユーザ登録することで、アプリのシステムに対するアクセスを制御しており、
1 の「内部記憶媒体」にアプリがデータを保存できる場所は、特定の場所に限られるということです。

間違って、システムにとって重要なファイルを破壊してしまったら大変ですから、セキュリティ上そのような仕組みになっているようです。

そのため、1 の「内部記憶媒体」にデータを保存する場合、安全にシステム領域にアクセスするために android.context.Context#openFileOutput(String, int) を使います。
このメソッドの戻り値は、java.io.FileOnputStream オブジェクトとなっています。
以下に引数を示します。

第1引数は、ファイル名を指定します。
パスではないのでセパレータは含みません。

第2引数は、ファイルのパーミッションになっています。
複数の値を合わせて指定したい場合は、値の論理和を指定します。

定数 説明
MODE_APPEND 追記モードで開く
MODE_PRIVATE 作成したアプリのみ読み書きできる
MODE_WORLD_READABLE 他アプリに読み込み権を与える
MODE_WORLD_WRITEABLE 他アプリに書み込み権を与える

ファイルに保存したデータを読み込む場合、android.context.Context#openFileInput(String) を使います。
第1引数は、ファイル名を指定します。こちらもパス名ではないのでセパレータを含めないようにしてください。

package com.blogspot.androlab.example;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.Date;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends Activity {
 final String FILE_NAME = "app_data.txt";

 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);

  /* データの書き込み */
  try {
   // MODE_PRIVATE と MODE_APPEND を指定
   FileOutputStream fos = openFileOutput(FILE_NAME, MODE_PRIVATE|MODE_APPEND);
   PrintWriter pw = new PrintWriter(fos);
   pw.println(new Date());
   pw.flush();
   fos.close();
   pw.close();
  } catch (FileNotFoundException e) {
   // ファイルが開けなかった -> openFileOutput(String, int)
   e.printStackTrace();
  } catch (IOException e) {
   // ストリームの切断に失敗した -> fos.close()
   e.printStackTrace();
  }

  /* データの読み込み */
  try {
   FileInputStream fis = openFileInput(FILE_NAME);
   BufferedReader br = new BufferedReader(new InputStreamReader(fis));
   String line;
   while ((line = br.readLine()) != null)
    Log.e("Debug", line);
   fis.close();
   br.close();
  } catch (FileNotFoundException e) {
   // ファイルが開けなかった -> openFileInput(String)
   e.printStackTrace();
  } catch (IOException e) {
   // テキスト行が読み込めなかった -> br.readLine()
   // ストリームの切断に失敗した -> fis.close(), br.close()
   e.printStackTrace();
  }
 }
}

2 の外部記憶媒体にデータを保存する場合、下準備として AndroidManifest.xml にパーミッションを記述します。
以下を参考にしてください。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      android:versionCode="1"
      android:versionName="1.0" package="com.blogspot.androlab.example">
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".MainActivity" 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>
    <uses-sdk android:minSdkVersion="4" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest>

ファイルを開くときは、直接パスを指定して出力ストリームを開きます。
このときに指定するパスは、バージョンによって変わる場合がありますので、
Environment#getExternalStorageDirectory() など、可能な限りAPIとして提供されたものを使うようにします。

package com.blogspot.androlab.example;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;

import android.app.Activity;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;

public class MainActivity extends Activity {
 final String FILE_NAME = "app_data.txt";

 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);

  // SDカードのディレクトリオブジェクトを取得
  File dir = Environment.getExternalStorageDirectory();

  // 新規のファイルオブジェクトを作成
  File file = new File(dir, FILE_NAME);

  /* データの書き込み */
  try {
   // 追記モードでストリームを開く
   FileWriter fw = new FileWriter(file, true);
   PrintWriter pw = new PrintWriter(fw);
   pw.println(new Date());
   pw.flush();
   fw.close();
   pw.close();
  } catch (IOException e) {
   // ファイルが開けなかった -> new FileWriter(File, boolean)
   // ストリームの切断に失敗した -> fw.close()
   e.printStackTrace();
  }

  /* データの読み込み */
  try {
   FileReader fr = new FileReader(file);
   BufferedReader br = new BufferedReader(fr);
   String line;
   while ((line = br.readLine()) != null)
    Log.e("Debug", line);
   fr.close();
   br.close();
  } catch (FileNotFoundException e) {
   // ファイルが開けなかった -> new FileReader(File)
   e.printStackTrace();
  } catch (IOException e) {
   // テキスト行が読み込めなかった -> br.readLine()
   // ストリームの切断に失敗した -> fr.close(), br.close()
   e.printStackTrace();
  }
 }
}

3 のネットワークサーバにデータを保存する場合は、サーバとの都合がありますので今回は省略しますが、
サーバ上にある、データを保存してくれるなんらかのファイル(LLスクリプトなど)」にアクセスして、データを渡すというのが一般的な方法でしょう。

読込みの場合は、「CSVやXML、JSON、YAMLなどにフォーマットされたデータ列を返してくれるなんらかのファイル」にアクセスしてデータを取得(いわゆるアプリとWebのマッシュアップ)という具合です。

参考
Android Developers Data Strage
1. Using the Internal Storage
2. Using the External Storage

2011年1月9日日曜日

Logcat のログが表示されないときの対処法

Android SDK 標準のデバッグツール「Logcat」ですが、
開発中に、以下のようなエラーが表示され、アプリケーションのログが表示されなくなる場合があります。

logcat read: Invalid argument

こんなときは、コマンドラインから「adb logcat -c」と入力し、
Logcat に溜まったログを消すことで解決します。

PATHが設定されていない環境では、以下のディレクトリに移動してから上記のコマンド入力してください。

SDK Tools, Revision 7 以前) "Android SDK ディレクトリ"/tools/
SDK Tools, Revision 8 以降) "Android SDK ディレクトリ"/platform-tools/

SDK Tools, Revision 7 以前) ./android-sdk-windows/tools/adb.exe logcat -c
SDK Tools, Revision 8 以降) ./android-sdk-windows/platform-tools/adb.exe logcat -c

こんな感じです。

2010年12月6日月曜日

Activityの状態を保存

主に、システムにActivityが強制終了させられる(システムにActivityが殺される)場合は、下記の3通りになります。

1.メモリ不足
2.長期間利用なし
3.画面の向き変更

この後、Activityは再起動される時があります(3番はすぐに再起動されます)。

通常は再開時にオブジェクトのフィールド値など、全てのデータは初期化されてしまいますが、何事もなかったかのようにActivityを再開する目的で一時的にデータを退避させておく仕組みがBundleです。

public class MainActivity extends Activity {

    int mInteger;
    ArrayList<Integer> mArray;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        ArrayList<Integer> mArray = (ArrayList<Integer>) getLastNonConfigurationInstance();
        if ( mArray == null ) {
            mArray = new ArrayList<Integer>();
            mArray.add(mInteger);
        }
    }

    // onCreateより後に呼ばれる。初期起動時は呼ばれない
    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {  
        super.onRestoreInstanceState(savedInstanceState);

        // Bundleよりデータを復元する
        // int getInt("キー名", 存在しない場合のデフォルト値);
        mInteger = saveInstanceState.getInt("integer", 0);
            mArray.add(mInteger);
    }

    // Activity終了時に呼ばれる
    @Override
    public Object onRetainNonConfigurationInstance() {
        // 保存オブジェクトを返す
        return mArray;
    }

    // Activity終了時に呼ばれる
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        // Activity再開時に渡すデータをBundle
        outState.putInt("integer", mInteger);
    }
}

2010年12月4日土曜日

Viewで文字列を描画

 // Viewを継承したクラスのメソッド
@Override
public void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    Paint rubyPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    rubyPaint.setTextSize(12.0f);
    textPaint.setTextSize(24.0f);
    rubyPaint.setColor(Color.WHITE);
    textPaint.setColor(Color.WHITE);
    String ruby = "シリアライズ";
    String text = "serialize";
    float rubyX = (textPaint.measureText(text) - rubyPaint.measureText(ruby)) / 2;
    float textX = 0.0f;
    float rubyY = 32.0f;
    float textY = rubyY + textPaint.getTextSize();
    canvas.drawText(ruby, rubyX, rubyY, rubyPaint);
    canvas.drawText(text, textX, textY, textPaint);
    canvas.drawColor(Color.TRANSPARENT);
}

ScrollViewをヘッダとフッタではさむ

コンテンツはスクロールされ、ヘッダとフッタは固定されます。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <TextView
        android:id="@+id/header"
        android:layout_width="fill_parent"
        android:layout_height="32dip"
        android:layout_alignParentTop="true"
        android:background="#FFFF0000"
        android:text="header" />
    <TextView
        android:id="@+id/footer"
        android:layout_width="fill_parent"
        android:layout_height="32dip"
        android:layout_alignParentBottom="true"
        android:background="#FF0000FF"
        android:text="footer" />
    <ScrollView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/header"
        android:layout_above="@id/footer">
        <LinearLayout
            android:layout_width="fill_parent"
            android:layout_height="wrap_content">
            <EditText
                android:id="@+id/edit_text"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:inputType="text" />
            <TextView
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:background="#FF00FF00"
                android:text="@string/long_text" />
        </LinearLayout>
    </ScrollView>
</RelativeLayout>