Android アクションバーに検索Box配置

Android のアクションバーに検索フィールドを配置する場合

良く使いそうなので、サンプルを書いておく。

res/menu で用意する search_menu.xml
アクションバーで表示する画像として action_search.png を用意して以下のように指定する。
android.widget.SearchView を指定する。

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
    <item android:id="@+id/action_search"
          android:showAsAction="always|collapseActionView"
          android:title="@string/action_search"
          android:actionViewClass="android.widget.SearchView"
          android:icon="@drawable/action_search">
    </item>
</menu>

res/xml に、searchable.xml を定義して、Manifest で Activity から指定しないとならない。

<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
    android:label="@string/app_name" 
    android:hint="@string/action_search_hint">
</searchable>

@string/app_name = アプリケーション名
@string/action_search_hint = 検索のヒント文字列

Manifest の アクティビティの記述、、、Intent filter を定義、

<activity android:name=".SearchActivity" android:launchMode="singleTop">
   <intent-filter>
       <action android:name="android.intent.action.SEARCH"/>
   </intent-filter>
   <meta-data android:resource="@xml/searchable" android:name="android.app.searchable"/>
</activity>

アクティビティ class ののメニューの記述は、、

@Override
public boolean onCreateOptionsMenu(Menu menu){
   getMenuInflater().inflate(R.menu.search_menu, menu);
   SearchManager searchManager = (SearchManager)getSystemService(Context.SEARCH_SERVICE);
   SearchableInfo searchableInfo = searchManager.getSearchableInfo(getComponentName());
   MenuItem menuItem = menu.findItem(R.id.action_search);

   SearchView searchView = (SearchView)menuItem.getActionView();
   searchView.setSearchableInfo(searchableInfo);
   searchView.setIconifiedByDefault(false);

   searchView.setImeOptions(EditorInfo.IME_ACTION_SEARCH);
   return true;
}
@Override
protected void onNewIntent(Intent intent) {
   setIntent(intent);
   if (Intent.ACTION_SEARCH.equals(intent.getAction())==true){
      String query = intent.getStringExtra(SearchManager.QUERY);
      // query = 検索文字列
   }
}


起動すると、、、

f:id:posturan:20160313192537p:plain



虫眼鏡アイコンをタップして検索入力状態は、、、


f:id:posturan:20160313192530p:plain



住所検索APIサービスを使ってautocomplete

住所入力で autocomplete 入力を手軽にできる API サービスを探してたら、
以下を見つけた。

http://geoapi.heartrails.com/api.html

利用規約をよく読み利用しなければならない。
さっそく勢いにまかせて jQuery UI で実装のサンプルを書いてみた。

adres_form.js というソース名で以下を書く。

var state_data = [
   { value: '',       label: '      '  },
   { value: '北海道', label: '北海道' },
   { value: '青森県', label: '青森県' },
   { value: '岩手県', label: '岩手県' },
    :
    :
   { value: '沖縄県', label: '沖縄県' }
];

function adres_form(stateSelector, citySelector, townSelector){
   $.each(state_data, function(){
      $(stateSelector).append($('<option>', this));
   });
   var city = ;
   var town = 
;
   $(stateSelector).change(function(){
      city = ;
      town = 
;
      $(citySelector).val('');
      $(townSelector).val('');
      var state = $(stateSelector).val();
      if (state.length==0) return;
      var url = 'http://geoapi.heartrails.com/api/json?method=getCities&prefecture=' + encodeURIComponent(state);
      $.getJSON(url, function(data, status, jqXHR){
         if (status==="success"){
            $.each(data['response']['location'], function(index, cdata){
               city[index] = cdata['city'];
            });
            $(citySelector).autocomplete({
               source : city
               , change : function(event, ui){
                  town = [];
                  $(townSelector).val('');
                  var turl = 'http://geoapi.heartrails.com/api/json?method=getTowns&prefecture='
                        + encodeURIComponent(state)
                        + '&city=' + encodeURIComponent($(this).val());

                  $.getJSON(turl, function(tdata, sts, jqXHR){
                     if (sts==="success"){
                        $.each(tdata['response']['location'], function(ix, towndata){
                           town[ix] = towndata['town'];
                        });
                        $(townSelector).autocomplete({
                           source : town
                        });
                     }
                  });
               }
            });
         }
      });
   });
}

これを jQuery UI を使うようにして、さらに以下のように書く。

<style type="text/css">
.ui-autocomplete{
   max-height: 200px;
   overflow-y: auto;
   overflow-x: hidden;
   font-size: 14px;
}
td{
   padding-left: 10px; padding-right: 10px;
   padding-bottom: 6px;
}
</style>

<script type="text/javascript" src="adres_form.js"></script>
<script type="text/javascript">
$(function(){
   adres_form($('#state'), $('#city'), $('#town'));
});
</script>

HTML body の中、、

<div>
   <table>
      <tr>
         <td><select name="state" id="state"></select></td>
         <td><input name="city" id="city"></td>
      </tr>
      <tr>
         <td colspan="2"><input name="town" id="town" size="48"></td>
      </tr>
   </table>
</div>


IEでは、以下のようになる。

f:id:posturan:20160313192619j:plain


f:id:posturan:20160313192614j:plain


複数列が重複するレコードをクレンジングする

複数列が同じ値のレコードが重複するテーブルのデータを1つだけ削除フラグをOFFにしたい。
HAVING count(*) > 1 を実行したくない。count(*) を実行してパフォーマンス劣化が心配だからである。

そんなに良いパフォーマンスではないかもしれないが以下のように考えてみた。
しかし、やっぱり遅いだろう。


(サンプル)
テーブルの定義、

DELIMITER //
DROP TABLE IF EXISTS gamma
//
CREATE TABLE gamma (
  id             INT NOT NULL AUTO_INCREMENT
, item_name      VARCHAR(20) NOT NULL
, price          INT
, mount          INT
, memo           VARCHAR(32)
, start_at       DATE
, end_at         DATE
, delete_flg     INT
, PRIMARY KEY (id) )
DEFAULT CHARACTER SET = utf8
COLLATE = utf8_bin
//
DELIMITER ;

「目的」
このテーブルの中で、id と mount 以外の列の値が、重複するレコードに、delete_flg = 1 にする。
1つの組み合わせだけ、delete_flg = 0 にする。
(データは、全て delete_flg = 0 になっていたとする)


方法は、以下のようなクエリの結果で delete_flg = 1 にすべきレコードの id を求めることで、
NULL のカラムを考慮する。

SELECT 
    x.id
FROM
    (SELECT id,
            CONCAT(item_name, '-', COALESCE(price, 'NULL'), '-', COALESCE(memo, 'NULL'), '-'
            , COALESCE(start_at, 'NULL'), '-', COALESCE(end_at, 'NULL'), '-', delete_flg) AS catdata
    FROM gamma
    WHERE delete_flg = 0) x

INNER JOIN
    (SELECT MAX(id) AS id,
            CONCAT(item_name, '-', COALESCE(price, 'NULL'), '-', COALESCE(memo, 'NULL'), '-'
            , COALESCE(start_at, 'NULL'), '-', COALESCE(end_at, 'NULL'), '-', delete_flg) AS catdata
    FROM gamma
    WHERE delete_flg = 0
    GROUP BY catdata
    HAVING COUNT(catdata) > 1) y
 
ON x.catdata = y.catdata
WHERE x.id <> y.id


ストアドプロシジャで以下のように定義する。

DELIMITER //
DROP PROCEDURE IF EXISTS sp_gamma_cleansing
//
CREATE PROCEDURE sp_gamma_cleansing()
BEGIN
   DECLARE done INT DEFAULT 0;
   DECLARE target_id INT;

   -- カーソル定義
   DECLARE c1 CURSOR FOR
   SELECT x.id FROM
   (SELECT id, CONCAT(item_name,'-',COALESCE(price,'NULL'),'-',COALESCE(memo,'NULL'),'-',COALESCE(start_at,'NULL')
   ,'-',COALESCE(end_at,'NULL'),'-',delete_flg ) AS catdata
   FROM gamma WHERE delete_flg = 0) x
   INNER JOIN
   (SELECT MAX(id) AS id, CONCAT(item_name,'-',COALESCE(price,'NULL'),'-',COALESCE(memo,'NULL'),'-',COALESCE(start_at,'NULL')
   ,'-',COALESCE(end_at,'NULL'),'-',delete_flg ) AS catdata
   FROM gamma WHERE delete_flg = 0
   GROUP BY catdata HAVING COUNT(catdata) > 1 ) y
   ON x.catdata = y.catdata
   WHERE x.id <> y.id ;

   -- ハンドラ宣言
   DECLARE EXIT HANDLER FOR NOT FOUND SET done = 0;
   DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done = 1;

   OPEN c1;
   REPEAT
      FETCH c1 INTO target_id;
      IF NOT done THEN
         UPDATE gamma SET delete_flg = 1 WHERE id = target_id;
      END IF;
   UNTIL done END REPEAT;
   CLOSE c1;

END
//
DELIMITER ;

実行は、call sp_gamma_cleansing();


ストアド実行後の検証は、以下クエリで0件であること。

SELECT MAX(id)
, CONCAT(item_name,'-',COALESCE(price,'NULL'),'-',COALESCE(memo,'NULL'),'-',COALESCE(start_at,'NULL')
,'-',COALESCE(end_at,'NULL'),'-',delete_flg ) AS catdata
FROM gamma WHERE delete_flg = 0
GROUP BY catdata HAVING COUNT(catdata) > 1

jQuery cookie を使ってアコーディオン開閉の維持

先日書いた「jQuery UI 開いたアコーディオンを再現する」 は、URLパラメータで指定する方法なので、
あまり現実的ではない。
cookie を使う方法は、ネット検索するといろいろ出てくる。

ちょっと安易だが書いてみる。


アコーディオン・バーを h3 タグで書いた場合、

$(function(){
   $("#accordion").accordion({
        collapsible: true
      , autoHeight: false
      , active: false
    });
   $("h3").click(function(){
      $.cookie('opened_menu', $("h3").index(this),{ expires: 1 });
    });
   var opened_menu = $.cookie('opened_menu');
   if (opened_menu != undefined){
      $.globalEval("$('<色:#006600>#accordion</色>').accordion({active:"+opened_menu+"});");
   }
});


HTMLは以下のとおり。

<div id="accordion">
     <h3>Chapter-1</h3>
   <div>
      ..............
      <a href="c1.html">c1</a>
   </div>
  <h3>Chapter-2</h3>
   <div>
      ...............
      <a href="c2.html">c2</a>
   </div>
   <h3>Chapter-3</h3>
   <div>
      ..........
      <a href="c3.html">c3</a>
   </div>
</div>


Parameter count exceeded allowed maximum

要件に対して基本的な設計ができてない、Tomcat を使用しているシステム、
httpリクエストで
 Parameter count exceeded allowed maximum: 512
が発生した。
作り直すような工数を貰えないことは、よくある話である。
JBoss を使用しているシステムなら、standalone.xml で以下属性の設定を行うことで回避はできる。

<property name="org.apache.tomcat.util.http.Parameters.MAX_COUNT" value="10000"/>

ちょっと MAX_COUNT = 10000 は乱暴かも。。

ただの Tomcat なら、、catalina.properties に以下を追記する。

org.apache.tomcat.util.http.Parameters.MAX_COUNT=10000

このようなことをしなくてはならないシステムに関わりたくないものだ。

jQuery UI 開いたアコーディオンを再現する

jQuery UI アコーディオンで、開いたアコーディオンの中からページ遷移して、
戻った特に元のアコーディオンを開くようにしたい。

戻るときのURLのパラメータで開くアコーディオンのインデックスを指定することで開くようにする。

(しかし、cookie を使った方がよいのかも。。

アコーディオンのページサンプル index.html

<div id="accordion">
   <h3>Chapter-1</h3>
   <div>
      ..............
      <a href="c1.html">c1</a>
   </div>
   <h3>Chapter-2</h3>
   <div>
      ...............
      <a href="c2.html">c2</a>
   </div>
   <h3>Chapter-3</h3>
   <div>
      ..........
      <a href="c3.html">c3</a>
   </div>
</div>

accord_open というパラメータで戻った時に開くアコーディオンのインデックスを指定する約束にする。
=====================
c1.html では、
<a href="index.html?accord_open=0">go back</a>

c2.html では、
<a href="index.html?accord_open=1">go back</a>

c3.html では、
<a href="index.html?accord_open=3">go back</a>
=====================
として、、、index.html の jQuery は、、

$("#accordion").accordion({
   collapsible: true
   , autoHeight: false
   , active: false
});
var parameter = location.search.substring(1);
if (parameter != ""){
   var ary = decodeURIComponent(parameter).split('&');
   $.each(decodeURIComponent(parameter).split('&'), function(index,e){
      if (e.split('=')[0]=="accord_open"){
         $.globalEval("$('#accordion').accordion({active:" +  e.split('=')[1] + "});");
      }
   });
}

Wicketバリデータを書けない時、、、

Wicket のバリデーションクラスをどうしても書けない時、
それでも、feedback の Panel にエラーを出したい場合がある。
  <div wicket:id="feedback"></div>

FeedbackPanel feedback = new FeedbackPanel("feedback");
feedback.setOutputMarkupId(true);
add(feedback);

と書いて、バリデーションクラスを書かなくても、無理やりこの feedback にエラーを出力するには、
AjaxButton の onSubmit で、入力チェック後エラーなら、jQuery を実行して、メッセージをセットする。
そのあとで、onErrorを呼び出す。
Wicket の バリデータが、必ず class="feedbackPanel" の ul タグ、
class="feedbackPanelERROR" の li タグ、
class="feedbackPanelERROE" の span タグに、メッセージを書くと
いうことを代わりに実行する。だけのことである。

Form<Void> form = new Form<Void>("form");
form.setMultiPart(true);

form.add(new AjaxButton("ajaxsubmit",form){
   @Override
   protected void onSubmit(AjaxRequestTarget target,Form<?> f){
      if ( 入力チェック ){
         target.appendJavaScript(
"$('div.feedback').html('<ul class=\"feedbackPanel\"><li class=\"feedbackPanelERROR\"><span class=\"feedbackPanelERROR\">1つ以上の項目を入力する必要があります</span></li></ul>');
         ");

         onError(target, f);
         return;

      }
      // submit の処理
   }
   @Override
   protected void onError(AjaxRequestTarget target,Form<?> f){
      // エラー処理
   }
});