カテゴリー別アーカイブ: Java

SharedPreferencesでputStringSetを使う時の注意点

こんな感じでgetStringSetで得た結果に直接要素を追加して、
putStringSetしてもダメっぽい。

SharedPreferences pref 
  = context.getSharedPreferences("name",Context.MODE_PRIVATE);

String key = "keyA";

// Setを取得する
Set<String> set1 = pref.getStringSet(key, new HashSet<String>());

// Setに文字列を追加
set1.add("追加する文字列");

// そのまま保存
SharedPreferences.Editor editor = pref.edit();
editor.putStringSet(key, set1);
editor.apply();

 

この状態でgetStringSetするとちゃんと設定されてて正常に動いているように見えるんだけど、
プロセスが再起動されたあとに消えてます。(2件目以降)

対応としては下記のように別のSetを作ってコピーしてあげるとちゃんと動いてくれた。

// Setをクローンする
HashSet<String> set2 = new HashSet<>();
set2.addAll(set1);

SharedPreferences.Editor editor = pref.edit();
editor.putStringSet(key, set2);
editor.apply();

モヤモヤしますな。

Javaで青空文庫のルビをパースする

Swiftで作ったサンプルのJava版です。
仕様などはSwift版と同様なので→Swift版

こちらもJavaで正規表現を利用するサンプルにもなってます。
 

使い方

String sampleText = "てめえらの|血《ち》は|何色《なにいろ》だーっ!!";
AozoraRubyParser parser = new AozoraRubyParser(sampleText);
List<AozoraRubyParser.Result> tokens = parser.parse();
for (AozoraRubyParser.Result r : tokens){
  Log.d("", r.text +  (r.ruby != null ? "[" + r.ruby + "]" : "") );
}

 

パーサーのコード

public class AozoraRubyParser {

  static public class Result {
    public String text;
    public String ruby;

    public Result(String t, String r) {
      this.text = t;
      this.ruby = r;
    }
  }


  private String text;

  public AozoraRubyParser(String text) {
    this.text = text;
  }

  public List<Result> parse() {

    ArrayList<Result> result = new ArrayList<>();

    String patternStr = "|(.+?)《(.+?)》";
    Pattern ptn = Pattern.compile(patternStr);

    int position = 0;

    Matcher matcher = ptn.matcher(text);
    while (matcher.find()) {
      if (position < matcher.start()) {
        String subText = this.text.substring(position, matcher.start());
        result.add(new Result(subText, null));
      }
      position = matcher.end();

      // 文字と読み仮名を抽出
      result.add(new Result(matcher.group(1), matcher.group(2)));
    }

    // 文末の残りを追加
    if (this.text.length() > position) {
      String subText = this.text.substring(position, this.text.length());
      result.add(new Result(subText, null));
    }

    return result;
  }
}

AndroidでQRコードをリーダーを作る(超シンプル版)

GooglePlayサービス 7.8以降では「Mobile Vision API」ってのが追加されていて、これを使うとQRコードの認識が行えるようです。
ググってみると色々とサンプルはあるのですが、いかんせん複雑になっており「QRコードを認識する!」って箇所がわかり辛く思いました。
そこで超シンプルなサンプルを作成しましたよ。

簡単にQRコードが認識できると思いますよ。

まずは準備から。

build.gradleへ依存ライブラリを追記します。

dependencies {
  compile 'com.google.android.gms:play-services-vision:10.2.0'
}

次にManifestへ権限などを追記します。
下記の uses-feature、uses-permission、meta-dataを追記してください。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.eyewhale.sample">
  
  <!-- ↓ この2行を追記  -->
  <uses-feature android:name="android.hardware.camera" android:required="true"/>
  <uses-permission android:name="android.permission.CAMERA" />
  
  <application ・・ >
    
      <!-- ↓ このmeta-dataを追記  -->
      <meta-data
          android:name="com.google.android.gms.version"
          android:value="@integer/google_play_services_version" />
      <meta-data
          android:name="com.google.android.gms.vision.DEPENDENCIES"
          android:value="barcode" />

  </application>
</manifest>

これは説明がいるか不明ですが、、超シンプルなレイアウトファイル。
SurfaceViewにカメラから撮影された画像が表示されます。
(今回のサンプルではFragmentにしてみてます。よくあるサンプルがActivityだったので)

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <SurfaceView
        android:id="@+id/camera_preview"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</FrameLayout>

ようやく実装です。
実際のコードは最後に記述しておきますが、
大きな流れはこんなかんじです。

1. SurfaceViewにコールバックを設定
2. BarcodeDetectorを作ってProcessorを設定する
3. CameraSourceを作成してBarcodeDetectorを設定
4. SurfaceViewの準備ができたらCameraSourceを開始
  →バーコード認識も開始される
5. バーコードが認識されたらProcessorのreceiveDetectionsが呼ばれるので適当に処理

あと、Pause/Resumeでカメラの停止/再開を行っています。

import com.google.android.gms.vision.CameraSource;
import com.google.android.gms.vision.Detector;
import com.google.android.gms.vision.barcode.Barcode;
import com.google.android.gms.vision.barcode.BarcodeDetector;

public class BarcodeSampleFragment extends Fragment {
  private final static String TAG = "BarcodeSampleFragment";
    
  private SurfaceView _surfaceView;
  private CameraSource _cameraSource;
  private boolean _surfaceCreated = false;
  
  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_barcode_sample, container, false);

    // SurfaceViewにコールバックを設定
    _surfaceView = (SurfaceView)view.findViewById(R.id.camera_preview);
    _surfaceView.getHolder().addCallback(_surfaceCallback);
    return view;
  }

  @Override
  public void onResume() {
    super.onResume();
      
    // CameraSourceが未作成 or 破棄されていたら作成
    if (_cameraSource == null) {
        setupCameraSource();
    }
        
    // SurfaceViewが準備できていたらキャプチャを開始
    if (_surfaceCreated){
        startCameraSource(_surfaceView.getHolder());
    }
  }

  @Override
  public void onPause() {
    super.onPause();
    // キャプチャを停止
    if (_cameraSource != null){
      _cameraSource.stop();
    }
  }
    
  private void setupCameraSource(){
    // QRコードを認識させるためのBarcodeDetectorを作成
    BarcodeDetector barcodeDetector = new BarcodeDetector.Builder(this.getContext())
        .setBarcodeFormats(Barcode.QR_CODE)
        .build();
    // DetectorにProcessorというコールバックを設定
    barcodeDetector.setProcessor(_detactorProcessor);

    // CameraSourceを作成
    _cameraSource = new CameraSource.Builder(this.getContext(), barcodeDetector)
        .setAutoFocusEnabled(true)
        .setFacing(CameraSource.CAMERA_FACING_BACK)
        .build();
  }

  private void startCameraSource(SurfaceHolder holder){
    try {
      _cameraSource.start(holder);
    } catch (IOException e){
        Log.e(TAG,"",e);
    }
  }
  
  private Detector.Processor<Barcode> _detactorProcessor = new Detector.Processor<Barcode>(){
    @Override
    public void release() {
    }
        
    @Override
    public void receiveDetections(Detector.Detections<Barcode> detections) {
      // 認識された結果がdetectionsに入ってる
      for(int i=0;i<detections.getDetectedItems().size();i++){
        int key = detections.getDetectedItems().keyAt(i);
        Barcode barcode = detections.getDetectedItems().get(key);
        String barcodeValue = barcode.rawValue;
        Rect rect = barcode.getBoundingBox();
        // 認識結果をログへ出力
        Log.d(TAG,"detect barcode:" + barcodeValue + " " + rect.toString());
      }
    }
  };

  private SurfaceHolder.Callback _surfaceCallback = new SurfaceHolder.Callback() {
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
      try {
          // カメラのキャプチャを開始
          _cameraSource.start(holder);
      } catch (IOException e){
          Log.e(TAG,"",e);
      }
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
      _cameraSource.stop();
      _cameraSource.release();
      _cameraSource = null;
    }
  }; 
}

※ 注意!
このサンプルはシンプルにするため色々と省いてます
– カメラへアクセスするための権限チェック
– GooglePlayServicesが利用可能であるかのチェック
– BarcodeDetectorが利用可能であるかのチェック
– その他もろもろ
実際に使う時は注意してくださいね!

「javaでファイルのリストを取得したら遅いよ」問題への対策

javaでファイルのリストを取得したら遅いよ。
ローカルでも遅いんだけど、ネットワーク越しだともうね。。

あまりに遅いので対策を考えてみました。

例えば指定されたディレクトリ以下のファイル名を全て取得するとこんなコードになったりしますよね。


private listFiles(File dir){
    File[] files = dir.listFiles();  // (a)
    for (File f : files) {
        if (f.isFile()){  // (c)
            String filename = f.getName();  // (b)
            System.out.println(filename);
        } else {
            listFile(f);
        }
    }
}

単純ですね。
ただ、これだと遅いんです。

Fileのインスタンスから取得している情報は下記の3つなのですが、どうやらそれぞれについて毎回システムコールを行っているようなのです。
それがネットワーク越しだと、ネットワークアクセスが発生します(たぶん)
・ディレクトリに含まれるディレクトリ/ファイルのFileインスタンス (a)
・ファイルの名前 (b)
・Fileインスタンスがディレクトリであるか (c)

で、代替手段として次のようにしました。
1. File.listFiles() の代わりにFile.list() を使う
listFilesはFileインスタンスを返しますが、listはファイル名を返します。
ファイル名からFileインスタンスを作成することで 上記の(a)と(b)が同時に取得できます。

2. isFile()の代わりに File.list() の戻り値がnullであるか判定する
対象のFileインスタンスがファイルの時、list()はnullを返すようです。
ですので、上記1でlist()を実行した際の戻り値を判定することで isFile を呼ぶ必要がなくなります。

3. (これは代替とはちょっと違いますが) Fileのメソッドの戻り値をキャッシュする
例えば File.getName() を複数回呼んでいる場合、変数に保存しておいてFileのメソッドは1回しか呼ばない

上記の処理を抜粋すると


// ファイル名のリストを取得する  
File f = new File(省略);  
String[] filenames = f.list();  
  
// ファイルであるか判定  
boolean isFile = (filenames == null);  
  
Fileのインスタンスを取得する  
File[] subfiles = Arrays.stream(filenames).map(name -> new File(f,name)).toArray(File[]::new);  

こんな感じです。

環境や処理内容にもよると思いますが、これらの対策で処理時間が1/3になってくれましたよ。

移行アシスタントを使うとeclipseが動かないよ(泣

macの移行アシスタントって便利ですよね。
大概の環境設定、アプリをそのまま新しいmacに移行してくれます。
乗り換えが本当にラクチンです。

でもちょっと困った事が、、、なぜか新マシンでeclipseが動かないのです。
こんな画面が表示されて。。。
eclipseerror131103

ログにはこんなメッセージが記録されています

!MESSAGE アプリケーション・エラー
!STACK 1
java.lang.IllegalStateException: アプリケーション・サービスを獲得できません。org.eclipse.core.runtime バンドルが解決済みかつ始動済みであるか確認してください (config.ini を参照)。

macで開発環境作るのは大概簡単なのですが、eclipse関係だけはちょっと面倒なんですよね。
tomcatとかプラグインとか。(面倒といっても入れるだけなんですけどね)
ぜひなんとかしたい。。。 と思ってgoogle先生に聞いてみると、
 eclipseのインストールフォルダをそのままコピーするとイイヨ!
なんと!

このeclipse.appが入っているフォルダね↓

eclipsedir131103

これを新マシンに上書きすると。。。

キレイに動き出しました(^^v
tomcatとの連携もバッチリですし。よかったよかった。

隣接したQuadKeyを求める

googleマップ、Bingマップなどで使用するクアッドキー
大変良く出来たキー体系で、設計者の頭の良さが想像できますね。

さて、ちょっと地図情報を扱う上で、並んだQuadKeyを求める必要がありましたので
プログラムを書いてみました。

今回はjavaですが、簡単なソースなので他の言語へも適用しやすいと思います。

/**
 * 指定されたキーの隣にあたるQuadKeyを求める
 * @param srcKey 元のQuadKey
 * @param direction 0:左 1:右 2:上 3:下
 * @return
 */
private static String calcNextQuadKey(String srcKey,int direction)
{
     int lv = srcKey.length();

     long latbits = 0;
     long lonbits = 0;
     char c;
     int val;
     int bit;

     for (int i = 0; i < lv; i++) {
          c = srcKey.charAt(i);
          val = c - '0';

          // valの2ビット目を縦位置を示すlatbitsに格納
          bit = val >> 1;
          latbits += (bit << (lv - i - 1));

          // valの1ビット目を横位置を示すlonbitsに格納
          bit = val & 1;
          lonbits += (bit << (lv - i - 1));
     }

     // 隣接したQuadKeyを求めるためlat、lonを変更
     switch (direction) {
     case 0:// 左
          lonbits -= 1;
          break;
     case 1:// 右
          lonbits += 1;
          break;
     case 2:// 上
          latbits -= 1;
          break;
     case 3:// 下
          latbits += 1;
          break;
     }

     // lv以上の精度に繰り上がった場合には最上位ビットをクリアする
     long floodmask = 1 << lv;
     if ((lonbits & floodmask) != 0) lonbits ^= floodmask;
     if ((latbits & floodmask) != 0) latbits ^= floodmask;

     // latbits、lonbitsからクアッドキーを求める
     StringBuilder sb = new StringBuilder(lv);
     long mask = 1 << (lv - 1);
     boolean lonbit, latbit;
     for (int i = 0; i < lv; i++) {

          lonbit = ((lonbits & mask) != 0);
          latbit = ((latbits & mask) != 0);

          if (latbit) {
               c = lonbit ? '3' : '2';
          } else {
               c = lonbit ? '1' : '0';
          }
          sb.append(c);

          mask >>>= 1;
     }
     return sb.toString();

}

以上.