258 Lab アプリ開発日記

Andorid,iOSアプリ開発してます。

【Android】画像保存の際、カメラからの画像か保存されている画像かを選ばせる方法

仕事の方が忙しく、全く書けていませんでした。。

最近は、色々と落ち着いてきて、またアプリ開発の方に着手ができてきました。

先日、MyReviewの機能追加を行いました。 MyReviewは、自身で好きなもののレビュー記録を付けることができるアプリです。 レビュー記録を作成する際、画像を一緒に付けることができます。

play.google.com

以前はカメラの機能が使えなかったのですが、使えるようにしました!

・カメラを起動させて撮影した画像を保存

・予め保存している画像を保存

どちらかを選ばせる様な挙動にしています。

こんな感じです。(画質が粗いですが、アプリ上は問題ありません。)

f:id:dev_258lab:20191006155353g:plain
ギャラリーからかカメラからか選ばせる

流れとしては、以下のイメージです。

  1. ギャラリーからのIntentを生成
  2. カメラ起動のIntentを生成
  3. createChooserメソッドに1と2を渡し、1と2を選ばせる画面を表示

今回はKotlinで書きました。

まずは、Intentの生成

// ギャラリー、カメラのIntentを入れるList
val targets: MutableList<Intent> = mutableListOf()

// ギャラリーのIntent
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "image/*"

targets.add(intent)

// カメラのIntent
val intent2 = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
      addCategory(Intent.CATEGORY_DEFAULT)
      putExtra(MediaStore.EXTRA_OUTPUT, createSaveFileUri())
      }

// ギャラリーとカメラのIntentをListに追加
targets.add(intent2)

val chooserIntent = Intent.createChooser(Intent(), "選択して下さい").apply {
      putExtra(Intent.EXTRA_INITIAL_INTENTS, targets.toTypedArray())
      }

startActivityForResult(chooserIntent, requestCode)

画像が選ばれた後のコールバック処理

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    if (resultCode == Activity.RESULT_OK) {
        // ここの中で受け取った画像に対して処理する
    }
}

では〜🐶

【Android】TextViewをリンク化する

TextViewをリンク化することがあったので、備忘録📝

        TextView lblPolicy = findViewById(R.id.lblPolicy);
        // リンクさせる
        lblPolicy.setMovementMethod(LinkMovementMethod.getInstance());
        String url = "https://dev-258lab.hatenablog.com/entry/2019/03/16/154038";
        String link = "<a href=" + url + ">" + "プライバシーポリシーについて" + "</a>";
        CharSequence textLink;

        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {//APIレベル24以上(Android OS 7.0以上)
            textLink = fromHtml(link, FROM_HTML_MODE_COMPACT);
        } else {
            textLink = fromHtml(link);
        }
        lblPolicy.setText(textLink);

f:id:dev_258lab:20190602224849p:plain

f:id:dev_258lab:20190602224931p:plain
TextViewをクリックするとリンク先に飛ぶ

塩サウナのススメ

突然ですが、皆さん……サウナ、入ってますか?(挨拶)

「交互浴」や「ととのい」、そんな単語を目にしたことがあるひとも少なくないはず。そう、最近のインターネットではサウナがブーム。しかし、サウナに興味を持ちつつも、あの熱さが苦手……そんな私が最近楽しんでいるサウナ。それが、「塩サウナ」です。

ということで、今回は「塩サウナ」について紹介します。

◾︎塩サウナとは?

塩サウナとは、ざっくり言えば、「熱の代わりに塩で汗をかくサウナ」のこと。肌に塩をまぶし、低濃度の塩水を体表面にまとうことで、浸透圧を使って汗をかきます。

◾︎塩サウナのメリット

調べてみると、塩を使うことで殺菌効果があるだの皮脂が排出されるだのありますが、、私としてはなにより、熱さを我慢しなくていい。これです。普通のサウナは90℃以上ありますが、塩サウナは60℃程度。初心者にも入りやすい熱さです。けれど、サウナに負けず劣らず、大量の汗をかくことができるのです。

◾︎塩サウナの入り方

□入るまでの準備

普通のサウナと同じです。汗をかきやすくするため、湯船に浸かり身体をしっかりと温めておきます。また、入る前には身体の水滴を拭き取っておきましょう。水分補給も忘れずに。

□入ってから

①座る椅子に湯を流す

前のひとが使っていた塩が残っていると、粘膜を刺激する可能性があります。

②塩を手に取り、肌に乗せる

肌に乗せる、というのが大切です。擦り込むと肌を傷つけるため、乗せる、まぶす、くらいです。場所は全身でOKです。ただし、粘膜は避けましょう。また、顔につける場合は、目より下にしましょう。額から流れる汗に塩が溶け、それが目に入ると、、地獄です。笑

③待つ

待ちます。

④塩を伸ばす

じんわりと汗をかいてきたら、その汗に塩を優しく混ぜていきます。それを全身に伸ばし、またその浸透圧の力で汗をかいていきます。

時間は、私の場合は20分くらいです。サウナの熱さや、汗のかきかたで調整しましょう。くれぐれも無理は禁物です。また、サウナを出るときは椅子を流すことを忘れずに。タオルをしぼったり、身体を流すのはサウナの外の掛け湯場でやりましょう。

◾️交互浴、ととのい

さて、塩サウナについて書きたかったことは以上ですが、、最初に書いた「交互浴」や「ととのい」に全く触れていません。。

それもそのはず、サウナ歴1ヶ月弱の私は、いまだに「ととのい」未経験者だからです。笑

また、これは初心者の疑問なのですが、、塩サウナって、交互浴やととのいに向いているでしょうか。①塩サウナは汗をかくまでに時間がかかるため、サウナ→水風呂→外気浴のルーチンに時間がかかり、また、②塩サウナはあまり温度が高くないため、水風呂が冷たい。笑

そんな理由で、塩サウナは「交互浴によるととのい」よりも「汗をかく」に特化したサウナという印象ですが、、実際のところはどうなんでしょうか。

塩サウナは塩サウナでとても気持ちのいいものですが、一方で、噂の「ととのい」を体感してみたいですね。とは言え、おそらくそう遠くないうちにサウナに足を踏み入れる予感がしています。ちなみに、昨日はスチームサウナに行ってきました。笑

ととのったらまた報告しますね。 では!

【Android】ホットペッパーグルメAPIを利用して周辺の飲食店を表示する方法

どこ食べのバージョンアップ対応で現在地から飲食店の一覧を取得することがあったので備忘録📝

今回の実装内容

今回はリクルートWEBサービスのグルメサーチAPIを使用して、指定した緯度経度や条件から周辺の飲食店を取得してくる。🍜 APIの詳細は公式サイトをご覧ください🐶

webservice.recruit.co.jp

1. APIキーの取得

ホットペッパーWEBサービスからAPIキーの申請を行う。 申請方法はリクルートWEBサービス(上URL)にアクセスし、[新規登録]ボタンからユーザー登録すれば取得することができる。

2. 実装

APIキーが取得できたら実際に実装していく。

2.1 権限の追加

インターネットを使用するため、AndroidManifest.xmlに権限を追加する。

<uses-permission android:name="android.permission.INTERNET" />
2.2 ホットペッパーAPIのパラメータクラスを作成

パラメータのクラスを作成する。

public class HotPepperGourmetSearch {
    private Double lat; // 緯度
    private Double lng; // 経度
    private int lunch; // ランチ営業有無
    private int range; // 検索範囲距離
    private int midnight_meal; // // 23時以降食事OK
    private ArrayList<String> keywordList; // キーワードのリスト
    private ArrayList<String> genreCdList; // ジャンルのリスト

    public Double getLat() {
        return lat;
    }

    public void setLat(Double lat) {
        this.lat = lat;
    }

    public Double getLng() {
        return lng;
    }

    public void setLng(Double lng) {
        this.lng = lng;
    }

    public int getLunch() {
        return lunch;
    }

    public void setLunch(int lunch) {
        this.lunch = lunch;
    }

    public int getRange() {
        return range;
    }

    public void setRange(int range) {
        this.range = range;
    }

    public int getMidnight_meal() {
        return midnight_meal;
    }

    public void setMidnight_meal(int midnight_meal) {
        this.midnight_meal = midnight_meal;
    }

    public ArrayList<String> getGenreCdList() {
        return genreCdList;
    }

    public void setGenreCdList(ArrayList<String> genreCdList) {
        this.genreCdList = genreCdList;
    }

    public ArrayList<String> getKeywordList() {
        return keywordList;
    }

    public void setKeywordList(ArrayList<String> keywordList) {
        this.keywordList = keywordList;
    }
}
2.2 APIコール後の戻り値のクラスを作成

APIをコールしてから戻ってくる飲食店の情報を格納するクラスを作成する。

public class HotPepperGourmet {
    private String name; // 飲食店の名前
    private String address; // 住所
    private Double lat; // お店の緯度
    private Double lng; // お店の経度
    private String lunch; // ランチ有無
    private String url; // お店のURL
    private String id; // お店コード

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public Double getLat() {
        return lat;
    }

    public void setLat(Double lat) {
        this.lat = lat;
    }

    public Double getLng() {
        return lng;
    }

    public void setLng(Double lng) {
        this.lng = lng;
    }

    public String getLunch() {
        return lunch;
    }

    public void setLunch(String lunch) {
        this.lunch = lunch;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }
}
2.3 APIを呼び出すクラスを作成

ActivityやFragmentからAPIをコールする処理を作成する。

    /**
     * ホットペッパーグルメAPIの呼び出し
     */
    public static void callHotPepperGourmetRestaurant(Activity activity, HotPepperGourmetSearch hotPepperGourmetSearch) {
        // URLの作成

        // ジャンルの切り取り
        StringBuilder genreSb = new StringBuilder();
        genreSb.append("&genre=");

        for (int i = 0; i < hotPepperGourmetSearch.getGenreCdList().size(); i++) {
            if (i > 0) {
                genreSb.append("&genre=");
            }

            String genreCd = hotPepperGourmetSearch.getGenreCdList().get(i);
            genreSb.append(genreCd);
        }

        // キーワードの切り取り
        StringBuilder keywordSb = new StringBuilder();
        keywordSb.append("&keyword=");

        for (int i = 0; i < hotPepperGourmetSearch.getKeywordList().size(); i++) {
            if (i > 0) {
                keywordSb.append("&keyword=");
            }

            String keyword = hotPepperGourmetSearch.getKeywordList().get(i);
            keywordSb.append(keyword);
        }

        // URLの生成
        StringBuilder urlStringBuilder = new StringBuilder();
        urlStringBuilder.append("http://webservice.recruit.co.jp/hotpepper/gourmet/v1/?key=");
        urlStringBuilder.append(API_KEY);
        urlStringBuilder.append(genreSb.toString()); // 飲食店のジャンル
        urlStringBuilder.append("&midnight_meal="); // 23時以降食事OK
        urlStringBuilder.append(hotPepperGourmetSearch.getMidnight_meal());
        urlStringBuilder.append(keywordSb.toString()); // キーワード
        urlStringBuilder.append("&lunch="); // ランチ営業
        urlStringBuilder.append(hotPepperGourmetSearch.getLunch());
        urlStringBuilder.append("&lat="); // 緯度
        urlStringBuilder.append(hotPepperGourmetSearch.getLat());
        urlStringBuilder.append("&lng="); // 経度
        urlStringBuilder.append(hotPepperGourmetSearch.getLng());
        urlStringBuilder.append("&range="); // 検索範囲距離
        urlStringBuilder.append(hotPepperGourmetSearch.getRange());
        urlStringBuilder.append("&count=100"); // 1ページあたりの取得数
        urlStringBuilder.append("&format=json") // レスポンス形式
        ; 

        URL url = null;

        try {
            url = new URL(urlStringBuilder.toString());
            // 非同期処理
            new RestaurantAsync(activity).execute(url).get(3000, TimeUnit.MILLISECONDS);
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
            mToastMessage = "接続がタイムアウトになったため、お店の情報を取得できませんでした";
            Utils.toastMake(activity, mToastMessage);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } finally {
            // プログレスバーを閉じる
            if (RestaurantAsync.sProgressDialog != null && RestaurantAsync.sProgressDialog.isShowing()) {
                RestaurantAsync.sProgressDialog.dismiss();
            }
        }
        Log.d(TAG, url.toString());
    }
2.4 非同期処理とJSONのパース

非同期処理でHTTP通信を行い、飲食店の情報を取得する。戻りの形式はJSON形式で指定したので、 非同期処理後に戻ってきたJSONをパースする。

public class RestaurantAsync extends AsyncTask<URL, Void, String> {

    private Activity mActivity;
    private StringBuffer mBuffer = new StringBuffer();

    private static final String TAG = "RestaurantAsync";

    public static ProgressDialog sProgressDialog;

    /**
     * コンストラクタ
     * @param activity
     */
    public RestaurantAsync(Activity activity) {
        mActivity = activity;
    }

    /**
     * 非同期処理の前処理
     */
    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        // プログレスバーを表示する
        sProgressDialog = new ProgressDialog(mActivity);
        sProgressDialog.setCancelable(false); // キャンセルさせない
        sProgressDialog.setMessage("お店を検索中...");
        sProgressDialog.show();
    }

    /**
     * 非同期処理
     * @param url
     * @return
     */
    @Override
    protected String doInBackground(URL... url) {
        HttpURLConnection con = null;
        URL urls = url[0];

        try {
            con = (HttpURLConnection) urls.openConnection();
            // JSONダウンロード
            con.setRequestMethod("GET");
            // タイムアウト3秒
            con.setConnectTimeout(3000);
            con.setReadTimeout(3000);
            // 接続
            con.connect();

            // レスポンスコードの確認
            int resCd = con.getResponseCode();

            if (resCd != HttpURLConnection.HTTP_OK) {
                // 接続NG
                throw new IOException("HTTP responseCode:" + resCd);
            }

            InputStream inputStream = con.getInputStream();
            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
            String line;

            while (true) {
                line = reader.readLine();

                if (line == null) {
                    break;
                }

                mBuffer.append(line);
            }

            // クローズ
            inputStream.close();
            reader.close();

        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 接続をクローズ
            con.disconnect();
        }

        Log.d(TAG, mBuffer.toString());
        return mBuffer.toString();
    }

    /**
     * 非同期処理の後処理
     * @param result
     */
    @Override
    protected void onPostExecute(String result) {
        super.onPostExecute(result);

        try {
            // JSONをパースして各飲食店の情報を取得する
            JSONObject jsonObject = new JSONObject(result);
            JSONArray jsonArray = jsonObject.getJSONObject("results").getJSONArray("shop");
            ArrayList<HotPepperGourmet> hotPepperGourmetArray = new ArrayList<>();

            for (int i = 0; i < jsonArray.length(); i++) {
                HotPepperGourmet hotPepperGourmet = new HotPepperGourmet();
                JSONObject json = jsonArray.getJSONObject(i);
                String id = json.getString("id"); // お店ID
                String name = json.getString("name"); // 店名
                String address = json.getString("address"); // 住所
                Double lat = json.getDouble("lat"); // 緯度
                Double lng = json.getDouble("lng"); // 経度
                String lunch = json.getString("lunch"); //ランチありなし
                String url = json.getJSONObject("urls").getString("pc"); // URL

                Log.d(TAG, "お店ID:" + id);
                Log.d(TAG, "店名:" + name);
                Log.d(TAG, "住所:" + address);
                Log.d(TAG, "緯度:" + lat.toString());
                Log.d(TAG, "経度:" + lng.toString());
                Log.d(TAG, "ランチありなし:" + lunch);
                Log.d(TAG, "URL:" + url);

                hotPepperGourmet.setId(id);
                hotPepperGourmet.setName(name);
                hotPepperGourmet.setAddress(address);
                hotPepperGourmet.setLat(lat);
                hotPepperGourmet.setLng(lng);
                hotPepperGourmet.setLunch(lunch);
                hotPepperGourmet.setUrl(url);

                hotPepperGourmetArray.add(hotPepperGourmet);
            }

            if (mActivity instanceof ConfirmAsyncListener) {
                // コールバック処理
                ((ConfirmAsyncListener) mActivity).onRestaurantAsyncCallBack(hotPepperGourmetArray);
            }

        } catch (JSONException e) {
            e.printStackTrace();
        } finally {
            // プログレスバーを閉じる
            if (sProgressDialog != null && sProgressDialog.isShowing()) {
                sProgressDialog.dismiss();
            }
        }
    }

    interface ConfirmAsyncListener {
        void onRestaurantAsyncCallBack(ArrayList<HotPepperGourmet> hotPepperGourmetArray);
    }

}
2.5 画面からの呼び出し

Activiyからコールしてみる。今回はonCreateで呼び出してみる。 コール後、onRestaurantAsyncCallBackにて、取得結果を確認することができる。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // HotPepperグルメAPIの呼び出し
        HotPepperGourmetSearch hotPepperGourmetSearch = new HotPepperGourmetSearch();
        hotPepperGourmetSearch.setLat(mLat); // 画面でセットした緯度
        hotPepperGourmetSearch.setLng(mLng); // 画面でセットした経度
        hotPepperGourmetSearch.setLunch(mLunch); // 画面でセットしたランチ有無 
        hotPepperGourmetSearch.setRange(mRange); // 画面でセットした検索範囲距離
        hotPepperGourmetSearch.setGenreCdList(mGenreCdList); // 画面でセットしたジャンルのリスト
        hotPepperGourmetSearch.setMidnight_meal(mMidnight_meal); // 画面でセットした23時以降食事OK
        hotPepperGourmetSearch.setKeywordList(mKeywordList); // 画面でセットしたキーワードのリスト
        // APIコール 
       HotPepperUtils.callHotPepperGourmetRestaurant(MapsActivity.this, hotPepperGourmetSearch);
    }

    /**
     * ホットペッパーAPI呼び出し後のコールバック処理
     *
     * @param hotPepperGourmetArray
     */
    @Override
    public void onRestaurantAsyncCallBack(ArrayList<HotPepperGourmet> hotPepperGourmetArray) {
        // hotPepperGourmetArrayに飲食店の情報がセットされている
    }

では!

【Android】ReleaseビルドでGoogleマップが表示されない

Googleマップを使用しているアプリは要注意!ReleaseビルドでGoogleマップが表示されない場合がある。。

先日、どこ食べのアップデート対応でGoogleマップを画面に表示させる機能を追加した。

play.google.com

しかし、どこ食べに初めて位置情報の権限を追加するので、少し心配になり、内部テストを実施していた。😅

内部テストとは、製品版と同様、Google Playストアにアプリのリリースを行うが、 テスターを招待し、招待されたユーザーのみリリースしたものを利用できるものである。 (内部テストについては今度書けたら記事にしたい。)

内部テストが上手くいき、アプリのアップデートも上手くいったが、Googleマップを開くと、、

f:id:dev_258lab:20190521001905p:plain

地図が表示されない。。😱

慌てて調べてみると以下記事を発見!ありがとうございます。

qiita.com

デバッグ時は「google_maps_api.xml」を参照させていましたが、 マニュフェストファイルにAPIキーを直書きしないといけないようです。。 なので、以下のように修正。

f:id:dev_258lab:20190521002636p:plain

        <!--<meta-data-->
            <!--android:name="com.google.android.geo.API_KEY"-->
            <!--android:value="@string/google_maps_key" />-->

        <meta-data
            android:name="com.google.android.geo.API_KEY"
            android:value="YOUR_API_KEY" />

そうすると、正常にマップが表示されました!!

f:id:dev_258lab:20190521002803p:plain

現在地から飲食店を探してルーレットができる機能を追加した「どこ食べ」🍺 良ければ使ってみてください!!

play.google.com

では〜!

【Android】EditText入力時、広告バナーがキーボードの上に表示されてしまう時の対処法

文字入力欄を選択するとキーボードが表示されるが、 画面下に配置している広告バナーがキーボードの上に来てしまう。 ManifestファイルのActivityタグに「android:windowSoftInputMode="adjustPan"」を記述することで解決する。

        <activity
            android:name=".HogeActivity"
            android:windowSoftInputMode="adjustPan"/>

f:id:dev_258lab:20190519220025p:plain
android:windowSoftInputMode="adjustPan"記述なし

f:id:dev_258lab:20190519220702p:plain
android:windowSoftInputMode="adjustPan"の記述あり

では!

【Android】FABの背景色を変える

FAB(Floating Action Button)の色をコードの中で変更する際、少しハマったので、備忘録。

fab = getActivity().findViewById(R.id.fab);
// 背景色を変えたい
fab.setBackgroundColor(getResources().getColor(R.color.colorPrimary));

で変わると思ったが、変わらず。。。

で、色々と調べたらこれで変わるようだ。

fab = getActivity().findViewById(R.id.fab);
// 背景色を変えたい
//fab.setBackgroundColor(getResources().getColor(R.color.colorPrimary));
fab.setBackgroundTintList(ColorStateList.valueOf(getResources().getColor(R.color.colorPrimary)));

では!