ActionBarSherlockでSearchView
android.widget.SearchViewなメニューのActionView自体が多分Android3.xくらいからサポートされたんだと思うんだけど、ActionBarSherlockにそれと同等な感じのWidgetが存在する模様。という事でAndroidAnnotations+ActionBarSherlockな構成でやってみた
要件
検索メニューを選択すると
んな感じで上に検索入力が出る。でまぁ「f」だけ入力してみると
みたいな感じでインクレメンタル検索的な事が出来て、✕を押すと
まぁフィルターから元に戻る的な感じな事をやる
※上記スクリーンショットのAndroidバージョンは2.2.1
サーバー側な処理に検索機能をぶちこむ
サーバー側はRailsなので
App::Application.routes.draw do
resources :entry do
match "search/:query", :action => :search, :on => :collection
end
end
な感じでconfig/routes.rbを設定。rake routesすると
- /entry/search/:query(.:format) entry#search
な感じで処理出来るように。でコントローラーにアクションを定義するのだけど、検索方式はとりまぁLIKE検索で
class EntryController < ApplicationController
def search
query = params[:query]
samples = Sample.where("name like ?", "%#{query}%")
render :json => samples
end
end
な感じで。これでサーバー側な仕込み完了。AndroidAnnotationsなところにもちょっとした仕込みが必要
AndroidAnnotations Restな仕込み
上記でサーバー側にAPIを追加したので@Restアノテーションなインターフェースにも追加せばならぬ
package kinjouj.sample;
import com.googlecode.androidannotations.annotations.rest.Accept;
import com.googlecode.androidannotations.annotations.rest.Get;
import com.googlecode.androidannotations.annotations.rest.Post;
import com.googlecode.androidannotations.annotations.rest.Rest;
import org.springframework.http.converter.json.GsonHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import static com.googlecode.androidannotations.api.rest.MediaType.APPLICATION_JSON;
@Rest(converters = { GsonHttpMessageConverter.class })
public interface AAService {
public void setRootUrl(String rootUrl);
public RestTemplate getRestTemplate();
@Get("/entry")
@Accept(APPLICATION_JSON)
public Sample[] getSamples();
// 追加
@Get("/entry/search/{query}")
@Accept(APPLICATION_JSON)
public Sample[] search(String query);
@Get("/entry/{id}")
@Accept(APPLICATION_JSON)
public Sample getSample(long id);
@Post("/entry")
@Accept(APPLICATION_JSON)
public Long save(Sample sample);
}
んまぁそんな感じで。でこいつをブリッジしちゃう@EBeanアノテーションなクラスも実装追加するんだけど、これはSample[] -> List<Sample>にするだけなので省略
本題はここから
res/menu/main.xmlにactionViewClassを持つメニューアイテムを追加
<?xml version="1.0" ?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_item_search"
android:showAsAction="always|collapseActionView"
android:icon="@drawable/search"
android:title="@string/menu_search_label"
android:actionViewClass="com.actionbarsherlock.widget.SearchView" />
<!-- 以下省略 -->
</menu>
Android本式だとandroid.widget.SearchViewなんだけど、ActionBarSherlockにあるのはcom.actionbarsherlock.widget.SearchView。これを指定する。あと本式はどうかは知らないけど、ActionBarSherlockでタイトルを指定しないとぬるぽでオチるので注意
MainActivity.java にSearchViewな処理を実装する
でAndroidAnnotationsを使う場合は基本的にonCreateOptionsMenuは実装しない。@OptionsMenuを使ってメニューをinflateする訳なんだけど、SearchViewを使う為にはそれが必要なのでオーバーライドする
package kinjouj.sample;
import android.annotation.SuppressLint;
import android.support.v4.app.FragmentManager;
import android.text.TextUtils;
import android.util.Log;
import com.actionbarsherlock.app.SherlockFragmentActivity;
import com.actionbarsherlock.view.ActionMode;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuItem;
import com.actionbarsherlock.widget.SearchView;
import com.googlecode.androidannotations.annotations.AfterInject;
import com.googlecode.androidannotations.annotations.AfterViews;
import com.googlecode.androidannotations.annotations.EActivity;
import com.googlecode.androidannotations.annotations.OptionsItem;
import com.googlecode.androidannotations.annotations.OptionsMenu;
import com.jeremyfeinstein.slidingmenu.lib.SlidingMenu;
@EActivity(R.layout.main)
@OptionsMenu(R.menu.main)
public class MainActivity extends SherlockFragmentActivity {
private static final String TAG = MainActivity.class.getName();
private SlidingMenu mSlidingMenu;
private ActionMode mMode;
private MainListFragment_ fragment;
@AfterInject
public void init() {
Log.v(TAG, "init");
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
@AfterViews
public void initViews() {
Log.v(TAG, "initViews");
mSlidingMenu = new SlidingMenu(this);
mSlidingMenu.setTouchModeAbove(SlidingMenu.TOUCHMODE_FULLSCREEN);
mSlidingMenu.setBehindOffsetRes(R.dimen.sliding_offset);
mSlidingMenu.setShadowWidthRes(R.dimen.shadow_width);
mSlidingMenu.setShadowDrawable(R.drawable.shadow);
mSlidingMenu.setFadeDegree(0.35f);
mSlidingMenu.attachToActivity(this, SlidingMenu.SLIDING_CONTENT);
mSlidingMenu.setMenu(R.layout.menu);
}
@SuppressLint("InlinedApi")
@OptionsItem(android.R.id.home)
public void onMenuHome() {
Log.v(TAG, "onMenuHome");
if (mSlidingMenu != null) {
mSlidingMenu.toggle();
}
}
@OptionsItem(R.id.menu_item_reload)
protected void onMenuReload() {
Log.v(TAG, "onMenuReload");
getFragment().loadSamples();
}
@OptionsItem(R.id.menu_item_add)
public void onMenuAdd() {
Log.v(TAG, "onMenuAdd");
AddFormDialogFragment dialogFragment = new AddFormDialogFragment();
dialogFragment.show(getSupportFragmentManager(), "add_form_dialog_fragment");
}
@OptionsItem(R.id.menu_item_destroy)
public void onMenuDestroy() {
Log.v(TAG, "onMenuDestroy");
getFragment().destroySelected();
}
@OptionsItem(R.id.menu_item_info)
public void onMenuExit() {
Log.v(TAG, "onMenuExit");
mMode = startActionMode(new ActionMode.Callback() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
Log.v(TAG, "onCreateActionMode");
getSupportMenuInflater().inflate(R.menu.action_mode, menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
Log.v(TAG, "onPrepareActionMode");
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
Log.v(TAG, "onActionItemClicked");
switch (item.getItemId()) {
case R.id.action_mode_app_info:
AppInfoDialogFragment_ fragment = (AppInfoDialogFragment_)AppInfoDialogFragment_.builder().build();
fragment.show(getSupportFragmentManager(), "app_info_dialog_fragment");
break;
case R.id.action_mode_app_exit:
finish();
break;
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
Log.v(TAG, "onDestroyActionMode");
mMode = null;
}
});
}
// 追加
@Override
public boolean onCreateOptionsMenu(Menu menu) {
Log.v(TAG, "onCreateOptionsMenu");
SearchView searchView = (SearchView)menu.findItem(R.id.menu_item_search).getActionView();
searchView.setSubmitButtonEnabled(false);
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
if (!TextUtils.isEmpty(newText)) {
getFragment().getListView().setFilterText(newText);
} else {
getFragment().getListView().clearTextFilter();
}
return false;
}
});
return super.onCreateOptionsMenu(menu);
}
public void onBackPressed() {
Log.v(TAG, "onBackPressed");
if (mSlidingMenu != null && mSlidingMenu.isMenuShowing()) {
mSlidingMenu.toggle();
return;
}
finish();
}
public MainListFragment_ getFragment() {
if (fragment == null) {
FragmentManager fragmentManager = getSupportFragmentManager();
fragment = (MainListFragment_)fragmentManager.findFragmentById(R.id.main_fragment);
}
return fragment;
}
public SlidingMenu getSlidingMenu() {
return mSlidingMenu;
}
public ActionMode getActionMode() {
return mMode;
}
}
ListView自体はListFragmentで処理しているので、そのFragmentなインスタンスを取得してgetListViewしつつのsetFilterTextをするっつー感じ。でonQueryTextChangeが発生する毎にそれをやるっていう所
MainListFragment.java にフィルターする処理を実装する
上記でサーバー側にクエリーをぶん投げる事でマッチする結果をJSONで取って、それをSample[]でデコードしつつ返してくれるのは実装したのでそれを利用する。方式はAdapterクラスのgetFilterメソッドでandroid.widget.Filterを返す事で出来るのでそこに実装する。
package kinjouj.sample;
import java.util.ArrayList;
import java.util.List;
import android.support.v4.app.ListFragment;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseBooleanArray;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Filter;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import com.googlecode.androidannotations.annotations.AfterInject;
import com.googlecode.androidannotations.annotations.AfterViews;
import com.googlecode.androidannotations.annotations.Background;
import com.googlecode.androidannotations.annotations.Bean;
import com.googlecode.androidannotations.annotations.EFragment;
import com.googlecode.androidannotations.annotations.UiThread;
import org.springframework.web.client.RestClientException;
@EFragment(R.layout.main_fragment_layout)
public class MainListFragment extends ListFragment {
private static final String TAG = MainListFragment.class.getName();
private List<Sample> samples = new ArrayList<Sample>();
private Filter filter;
private Toast errorToast;
@Bean
protected AAServiceBean serviceBean;
@AfterInject
public void init() {
Log.v(TAG, "init");
loadSamples();
}
@AfterViews
public void initViews() {
Log.v(TAG, "initViews");
getListView().setTextFilterEnabled(true);
getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
registerAdapter();
}
public void registerAdapter() {
ArrayAdapter<Sample> adapter = new ArrayAdapter<Sample>(
getActivity(),
android.R.layout.simple_list_item_checked,
android.R.id.text1,
samples
) {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Log.v(TAG, "getView");
View view = super.getView(position, convertView, parent);
if (view instanceof TextView) {
Sample sample = getItem(position);
((TextView)view).setText(sample.getName());
}
return view;
}
// ここから追加
@Override
public Filter getFilter() {
Log.v(TAG, "getFilter");
if (filter == null) {
Filterクラスのインスタンス生成時に初期状態を取っておく。だけど、こんな風にやるのはNG
final List<Sample> _samples = new ArrayList<Sample>(samples);
filter = new Filter() {
@Override
protected FilterResults performFiltering(CharSequence constraint) {
Log.v(TAG, "performFiltering");
FilterResults results = new FilterResults();
List<Sample> filterItems = new ArrayList<Sample>();
if (!TextUtils.isEmpty(constraint)) {
// サーバー側にsearch APIを要求して結果を取る
List<Sample> samples = serviceBean.search(constraint.toString());
filterItems.addAll(samples);
} else {
filterItems = _samples;
}
results.values = filterItems;
results.count = filterItems.size();
return results;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
Log.v(TAG, "publishResults");
if (results.count > 0) {
@SuppressWarnings("unchecked")
List<Sample> filterItems = (ArrayList<Sample>)results.values;
notifyDataSetChanged();
clear();
for (Sample filterItem : filterItems) {
add(filterItem);
}
}
}
};
}
return filter;
}
};
setListAdapter(adapter);
}
@Background
public void loadSamples() {
Log.v(TAG, "loadSamples");
try {
List<Sample> _samples = serviceBean.getSamples();
samples.clear();
samples.addAll(_samples);
} catch (RestClientException e) {
handleError(e);
}
notifyAdapter();
}
@Background
public void addSample(Sample sample) {
Log.v(TAG, "addSample");
if (sample == null) {
throw new IllegalArgumentException("argument(sample) is null");
}
try {
serviceBean.save(sample);
samples.add(sample);
notifyAdapter();
} catch (RestClientException e) {
handleError(e);
}
}
public void destroySelected() {
Log.v(TAG, "destroySelected");
SparseBooleanArray checkedArray = getListView().getCheckedItemPositions();
int checkedSize = checkedArray.size();
List<Sample> selectedSamples = new ArrayList<Sample>();
for (int i = 0; i < checkedSize; i++) {
int key = checkedArray.keyAt(i);
boolean checked = checkedArray.get(key);
if (!checked) {
continue;
}
Object o = getListView().getItemAtPosition(key);
if (o instanceof Sample) {
Sample sample = (Sample)o;
if (!selectedSamples.contains(sample)) {
selectedSamples.add(sample);
}
}
}
if (selectedSamples.size() > 0) {
try {
serviceBean.destroy(selectedSamples);
samples.removeAll(selectedSamples);
notifyAdapter();
} catch (RestClientException e) {
handleError(e);
}
}
}
@UiThread
public void handleError(Throwable t) {
Log.v(TAG, "handleError");
clearChoices();
if (t == null) {
throw new IllegalArgumentException("argument(Throwable) is null");
}
errorToast = Toast.makeText(
getActivity(),
"ERROR: " + t.getMessage(),
Toast.LENGTH_LONG
);
errorToast.show();
}
@UiThread
public void notifyAdapter() {
Log.v(TAG, "notifyAdapter");
clearChoices();
notifyDataSetChanged();
}
private void clearChoices() {
Log.v(TAG, "clearChoices");
ListView listView = getListView();
if (listView != null) {
getListView().clearChoices();
}
}
@SuppressWarnings("unchecked")
private void notifyDataSetChanged() {
Log.v(TAG, "notifyDataSetChanged");
ListAdapter adapter = getListAdapter();
if (adapter != null) {
((ArrayAdapter<Sample>)adapter).notifyDataSetChanged();
}
}
}
んまぁ今回のと関係ないコード多すぎだけどね。要はsetListAdapterで指定するAdapterクラスのgetFilterメソッドをオーバーライドして処理する。performFilteringで受け取った検索キーワードをサーバーにリクエストして結果を取る。でその結果をListViewにバインドする。まぁそんだけ
ってな感じで本題な所は短かったけど、ActionBarSherlockなSearchViewを使えばAndroid3.x以下系でも同様な感じの事は出来るんでねーかと
※Android4.x向けに検証していないのでずっこける可能性もあります