Android用(ちょっと)本格的なミュージックプレーヤの開発 part6

ミュージックプレーヤの開発 part6です。

ライブラリパートはこれでとりあえず完了です。
実はプレイリスト管理という大仕事が残っていますが

プレイリストについては、音楽再生サービスが出来たあとのほうが
整理しやすいのでひとまずおいておきます。

検索機能

検索機能の実装はライブラリ関連機能のなかでは一番大規模です。
ただし実装の難易度はそんなに高くありません。
順に作っていきます。

呼び出し

呼び出し部分はいつもの通りです。
Mainアクティビティにて

enum     FrgmType { fRoot, fAlbum, fArtist, fSearch }

まずはフラグメントタイプに登録して、
検索ワード保持用に

public   static String     searchWord;
public String getSearchWord()          {return searchWord;}

最後に
任意の場所にテキストフィールドと検索ボタンを設置、
ボタンが押されると以下の関数が実行されるようにします。

onClick(View v)で、
vが検索ボタンならばソフトウェアキーボードを終了して
setSearch() を呼び出します。

        InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
        inputMethodManager.hideSoftInputFromWindow(v.getWindowToken(), 0);
        setSearch();

setSearch()は以下の通りです。
searchTextは設置したEditTextウィジェットです。

        public void setSearch(){
                searchWord = searchText.getText().toString();
                if(searchWord.isEmpty()) return;
                setNewFragment(FrgmType.fSearch);
        }

空入力でないことを確認してから setNewFragmentが呼ばれるようにします。
例によって

        switch(CallFragment)
        {
         case fRoot   : ft.replace(R.id.root, new RootMenu(),     "Root"); break;
         case fAlbum  : ft.replace(R.id.root, new AlbumMenu(),   "album"); break;
         case fArtist : ft.replace(R.id.root, new ArtistMenu(), "artist"); break;
         case fSearch : ft.replace(R.id.root, new SearchMenu(), "search"); break;
        }

setNewFragmentに検索用画面呼び出し用の処理を追記します。
当然 SearchMenuはまだできていないので赤波線になるので必要ならダミーを使って
ください。

検索アイテム

検索画面では、 入力された文字列を含む
 トラック、アルバム、アーティスト
を検索して表示します。
ことなる3パターンのデータをうまい具合に1つのリスト内に表示したいので、
従来の Track Album Artist のアイテムリストでは不都合です。
新たに Search アイテムを追加します。中身はシンプルです。

public class Search {
        enum ItemType {
                TRACK,
                ALBUM,
                ARTIST
        }
        public ItemType itemType;
        public Track    trackItem;
        public Album    albumItem;
        public Artist   artistItem;
        public String   title;
        public String   sub;

        public Search(ItemType type, Cursor cursor)
        {
                itemType = type;
                switch(type)
                {
                        case TRACK:
                                trackItem = new Track(cursor);
                                title = trackItem.title;
                                sub   = trackItem.artist;
                                break;
                        case ALBUM:
                                albumItem = new Album(cursor);
                                title = albumItem.album;
                                sub   = albumItem.artist;
                                break;
                        case ARTIST:
                                artistItem = new Artist(cursor);
                                title = artistItem.artist;
                                sub   = "Artist" ;
                                break;

                }

        }

}

保持しているデータのタイプとそのデータを保持します。

Searchを扱うリストのアダプタも追加します。
これもシンプルです。

まずはxmlでリストアイテムのデザインを組みます。

このレイアウトに合わせてListSearchAdapterを実装します。

public class ListSearchAdapter extends ArrayAdapter{
LayoutInflater mInflater;

        public ListSearchAdapter(Context context, List item){
                super(context, 0, item);
                mInflater =  (LayoutInflater) context.getSystemService( Context.LAYOUT_INFLATER_SERVICE );
        }

        @Override
        public View getView(int position, View convertView,ViewGroup parent){

                Search item = getItem(position);        
                ViewHolder holder;

                if(convertView==null){
                        convertView = mInflater.inflate(R.layout.item_search, null);
                        holder = new ViewHolder();
                        holder.titleTextView  = (TextView)convertView.findViewById(R.id.title);
                        holder.subTextView    = (TextView)convertView.findViewById(R.id.sub_title);
                        holder.typeImageView  = (ImageView)convertView.findViewById(R.id.typeart);
                        convertView.setTag(holder);
                }else{
                        holder = (ViewHolder) convertView.getTag();
                }

                holder.titleTextView.setText(item.title);
                holder.subTextView.setText(item.sub);

                switch(item.itemType){
                        case  TRACK: holder.typeImageView.setImageResource(R.drawable.search_track);  break;
                        case  ALBUM: holder.typeImageView.setImageResource(R.drawable.search_album);  break;
                        case ARTIST: holder.typeImageView.setImageResource(R.drawable.search_artist); break;
                }

                return convertView;     
        }

        static class ViewHolder{
                TextView   titleTextView;
                TextView   subTextView;
                ImageView  typeImageView;
        }
}

基本的にトラックやアルバム、アーティストのアダプタと同じです。

        switch(item.itemType){
                case  TRACK: holder.typeImageView.setImageResource(R.drawable.search_track);  break;
                case  ALBUM: holder.typeImageView.setImageResource(R.drawable.search_album);  break;
                case ARTIST: holder.typeImageView.setImageResource(R.drawable.search_artist); break;
        }

この部分は検索アイテムのデータタイプ識別アイコンの切り替えです。

こんなかんじのアイコンをGIMPで作成しました。
アイテムのタイプで表示するアイコンを変更します。

以上で検索機能に必要なパーツが揃いました。

検索

では早速検索画面を作ります。
例によってレイアウトを組みます。

前回作成した spinnerをここでも使用します。
spinnerには 全て、トラック、アルバム、アーティスト の4つの抽出モードを
登録しておいて切り替えられるようにします。

レイアウトに合わせて SearchMenu を作成します。
長いので部分ごとに掲載します。

まずは宣言

                private static Main activity;
                private String searchWord;
                private String[] SearchSelecter = 
                                {"全て" ,"トラック","アルバム" , "アーティスト"};

                private static List<Search>  results = null;
                private static ListSearchAdapter result_adapter = null;

前回同様ソースに直接 全て〜 を書き込んでいますが
res/value/strings.xml を使ったほうがベターです。
onCreateViewでは、いつものように

 activity = (Main)getActivity();
 searchWord = activity.getSearchWord();

次は検索結果表示用に ListViewを初期化します。

ListView resultList = (ListView) searchView.findViewById(R.id.list);
                 result_adapter = new ListSearchAdapter(activity,results);
                 resultList.setAdapter(result_adapter);
                 resultList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
                        @Override
                        public void onItemClick(AdapterView parent, View view,
                                        int position, long id) {
                                                 ListView lv = (ListView)parent;
                                                 Search item = (Search)lv.getItemAtPosition(position);
                                                 switch(item.itemType)
                                                 {
                                                 case TRACK:
                                                         activity.focusTrack(item.trackItem);
                                                         break;
                                                 case ALBUM:
                                                         activity.focusAlbum(item.albumItem);
                                                         activity.setNewFragment(FrgmType.fAlbum);
                                                         break;
                                                 case ARTIST:
                                                         activity.focusArtist(item.artistItem);
                                                         activity.setNewFragment(FrgmType.fArtist);
                                                 }
                                        } 
                                        });
                 resultList.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
                        @Override
                        public boolean onItemLongClick(AdapterView parent, View view,
                            int position, long id){
                                                ListView lv = (ListView)parent;
                                                Track item = (Track)lv.getItemAtPosition(position);
                                                Toast.makeText((Main)getActivity(), "LongClick:"+item.title, Toast.LENGTH_LONG).show();
                                        return true;    
                        }
                        });

アイテムが選択されると、アイテムタイプを識別して
選択されたアイテムをフォーカスして setNewFragment を呼び出します。

最後にsipnnerを設定します。

                ArrayAdapter<String> adapter = new ArrayAdapter<String>(activity,R.layout.spinner_item);
                adapter.setDropDownViewResource(R.layout.spinner_dropdown_item);
                adapter.addAll(SearchSelecter);
                Spinner spinner = (Spinner) searchView.findViewById(R.id.option_spinner);
                 spinner.setAdapter(adapter);
                 spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
                    @Override
                    public void onItemSelected(AdapterView<?> parent, View view,
                            int position, long id) {
                        Spinner spinner = (Spinner) parent;
                        String item = (String)spinner.getSelectedItem();
                        result_adapter.clear();
                        setList(item);
                                result_adapter.notifyDataSetChanged();
                    }
                    @Override
                    public void onNothingSelected(AdapterView<?> arg0) {
                    }
                });

前回用意したデザイン(xml)を使ってスピナを登録します。
スピナでモードが選択されると、 setList が呼ばれます。

setListはシンプルです。

        private void setList(String item){
                results.clear();
                if(item.equals(SearchSelecter[0])) //全て
                {
                        searchArtist();
                        searchAlbum();
                        searchTrack();

                }else if(item.equals(SearchSelecter[1])) //トラック
                {
                        searchTrack();
                }else if(item.equals(SearchSelecter[2])) //アルバム
                {
                        searchAlbum();
                }else if(item.equals(SearchSelecter[3])) //アーティスト
                {
                        searchArtist();
                }

        }

選択された内容ごとに検索機能を呼び出します。
searchArtist(), searchAlbum(), searchTrack() は名前の通り、
searchWord
に基づいてアーティスト、アルバム、トラックを検索します。

それぞれの実装はほぼ同じですが、
検索の範囲だけが少し違います。

アーティストの検索では
 指定された文字列を含むアーティスト
だけを探しますが、

アルバムの検索では
 指定された文字列を含むアルバム
 指定された文字列を含むアーティストが登録されたアルバム
も探します。

また
トラックの検索では
 指定された文字列を含むトラック
 指定された文字列を含むアルバムに登録されたトラック
 指定された文字列を含むアーティストに登録されたトラック

を検索するようにします。

        private void searchTrack(){

                 HashSet set = new HashSet();
                ContentResolver resolver = activity.getContentResolver();       
                String[] SELECTION_ARG = {"%"+searchWord+"%", 
                                          "%"+searchWord+"%",
                                          "%"+searchWord+"%" };
                Cursor cursor = resolver.query(
                                MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 
                                Track.COLUMNS, 
                                MediaStore.Audio.Media.TRACK + " like ? OR "
                             +MediaStore.Audio.Media.ALBUM + " like ? OR "
                             +MediaStore.Audio.Media.ARTIST+ " like ?",
                                SELECTION_ARG,
                                null
                                );
        while( cursor.moveToNext() ){
                if( cursor.getLong(cursor.getColumnIndex( MediaStore.Audio.Media.DURATION)) < 3000 ){continue;}
                if( set.contains(cursor.getString( cursor.getColumnIndex( MediaStore.Audio.Media.TITLE ))))     {continue;}
                        results.add(new Search(Search.ItemType.TRACK,cursor));
                        set.add(cursor.getString( cursor.getColumnIndex( MediaStore.Audio.Media.TITLE )));
        }
        cursor.close();
        }

ということでトラックの検索はこんなかんじです。
通常の検索では 完全一致 のデータしか持ってこないので 一部一致でヒットするように
 like そして OR でつないで 上記の3つの条件で検索しています。

album artist については検索オプションの OR の数が違う以外はほぼ同じです。

以上で全実装完了です。
エミュレータで起動してみます。


検索できました!

"a" で検索すると、条件にマッチする
トラック、アルバム、アーティストが表示されており
spinnerから抽出モードを切り替えることで
アーティストのみの検索になったのが確認できます。

以上で基本的な音楽ライブラリ機能の実装が完了しました。

次回からいよいよ音を出すための機能を作っていきます。

コメントを追加する

コメント

なつめ さん
失礼しました。もっと解析しながらやっていきます。。
( Thu, 01 Dec 2016 22:06:51 )
hiroumauma(管理人) さん
何ども同じことを書くようで心苦しいのですが、
自分の書いているプログラムの意味を考えてから作業を行って下さい。
どうも、AndroidのActivityやFragmentもそうですがJavaの
オブジェクトからしてその意味がよくわかっていないように見受けられます。

part5補足記事で説明したとおり XMLレイアウトはinflaterが
Viewの大枠を作るための素材であり、画面の実体ではありません。
おそらくHOME画面から検索画面に飛ぶような動きがさせたかったのだと
推察しますが全然的はずれです。HomeSectionFragmentのonCreateViewには
スピナやリストを登録するコードなんてないので画像のようになるのは
当然で、不具合でもなんでもありません。


part6で作ったものは、Mainアクティビティに保持してある 検索文字列を
貰って、その検索結果を表示する画面(フラグメント) ですよね?
フラグメントは Mainアクティビティから個別に呼び出されるので
今RootMenu内の子フラグメントHomeは全然関係ありません。
呼び出しに必要な手順は
1,Mainアクティビティの searchWord に検索したい文字列を登録
2,setNewFragment() で検索画面を呼び出し
以上です。
試しにMainアクティビティの onCreate の最後に
 searchWord = "a";
 setNewFragment(FrgmType.fSearch);
と書き加えてみて下さい。
アプリ起動直後に "a" の検索画面が表示されるはずです。

あとはこの2行を応用して任意の検索機能にしてください。
partX記事のようにポップアップで文字列を受け取るなり、
適当な EditTextを設置するなりして文字列を受け取り
 = "a" の代わりに EditText内の文字列を使えばいいだけです。
( Wed, 30 Nov 2016 02:28:55 )
なつめ さん
ありがとうございます。
xmlであることを忘れていました。簡単な問題でした。。

ちなみに今回の検索画面を表示させるために、RootMenuにあるHomeSectionFragment内のlayout.の次を変更しますか?
それが原因か、直接的な原因が分からないのですが、エミュレーター、実機で開いてもこの画像のような(http://i.imgur.com/dweK04w.png)
表示になり検索画面が出てこないのですがなぜですかね?
logcatを見てもそれらしきエラーなどは見つからず。歯が立たない状態です。。
( Wed, 30 Nov 2016 00:29:32 )
hiroumauma(管理人) さん
>なつめさん
作成中のプログラムの意味をよく考えてみて下さい。
たしかにテキストとして option_spinner という名前の説明は冗長になるだけ
なので省略していますが、明らかに指しているのは特定のViewです。

 Spinner spinner = (Spinner) searchView.findViewById(R.id.option_spinner);

この部分のことだと思いますが、これは part5 補足ページでも何度も登場した
inflaterが作った雛形の一部Viewを捕まえるコードです。
(ここがわからない場合は part5補足ページをもう一度ゆっくり読んでみてください)
今searchViewのテンプレート上にスピナーは1つしかないので
つまりその部分を指定すればいいわけです。

毎回テンプレートのXMLを掲載しても意味がないので省略しているだけ
ですので、その部分は自分のテンプレートで使用しているidに書き換えて
使用してください。
( Tue, 29 Nov 2016 01:26:03 )
なつめ さん
失礼します。

今回のSearchMenuのspinner登録のところですが、option_spinnerはどの場面で作られたものですか?
過去記事を見渡してもoption_spinnerが出てきていないのでそこだけエラーになってしまって。。。
( Mon, 28 Nov 2016 23:07:00 )