【Android】デフォルトのButtonをフラットなデザインに変更してみる

AndroidStudioでアプリ開発をしている際、UIのデザインは必ずぶつかる壁だと思います。
今回はButtonのデザインにフィーチャーして、デフォルトのButtonをフラットなデザインに変更してみます。

AndroidGoogleにログインする際に出てくるこの画面の、「次へボタン」のような感じを目指してみます。
f:id:sbgMahiro:20180309221836p:plain:w300

使用したAndroidStudioのバージョンは3.0.1です。
また、プロジェクトはAndroid6.0以上のバージョンに対応したアプリとして作成しました。

1.デフォルトのデザインで配置を作る

ひとまず、このように作成しました。
右下にButtonが一つあるだけです。

ここでのポイントとしては、画面の横幅目一杯のLayoutを一枚配置し、その中にButtonを置くようにすることです。

f:id:sbgMahiro:20180309221303j:plain

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.app.sbgmahiro.testbuttondesign.MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <Space
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_weight="1" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="48dp"
            android:layout_weight="0"
            android:gravity="bottom|clip_horizontal"
            android:orientation="horizontal">

            <Space
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_weight="1" />

            <Button
                android:id="@+id/button"
                android:layout_width="128dp"
                android:layout_height="48dp"
                android:layout_weight="0"
                android:text="次へ" />
        </LinearLayout>
    </LinearLayout>

</android.support.constraint.ConstraintLayout>

2.Layoutの背景色を変更する

f:id:sbgMahiro:20180309221255j:plain
Buttonが配置されているLayoutを選択し、Attributesの中にあるbackgroudを変更します。
今回はPrimaryColorにしました。

3.Buttonのstyleを変更する

f:id:sbgMahiro:20180309221248j:plain
Attributesの中のstyleを@style/Widget.AppCompat.Button.Borderless.Coloredに変更します。
このstyleは、背景色が透明で、境界線や影などがないデザインのようで、これを選択すると画像のような状態になります。

この後、ButtonのtextColorを変更して見やすい文字色にすれば完成です。

余談

app/res/values/にcolor.xmlというファイルがあります。
このファイルの中には、ステータスバーやアクションバーなどの色を設定する項目があります。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#e62f8b</color>
    <color name="colorPrimaryDark">#e62f8b</color>
    <color name="colorAccent">#5a4498</color>
</resources>

colorPrimary(アクションバー)とcolorPrimaryDark(ステータスバー)の色を、ブーゲンビリアに設定してみました。
Buttonの文字色も、白に設定しました。
f:id:sbgMahiro:20180309221259j:plain

フラットでまとまりのあるUIになっていると思います。

【Android】ServiceからBroadcastRecieverでUIを操作

タイマーアプリを開発する上で詰まったポイントです。

タイマーアプリは、他のアプリケーションを使用しているときでも、裏で動き続けている必要があります。
実際、プリインストールされているタイマーアプリでもそうなっています。

Androidアプリでバックグラウンド処理をする場合、Serviceを利用します。

しかし、ServiceはActivityとは別の場所で動作します。
ですので、ServiceからUIを操作する処理を実装する必要があります。

使用するのはLocalBroadcastManagerです。
これを使えば、ActivityとServiceの間での通信が簡単に行なえます。

タイマー処理のフローチャート的なものはこんな感じです。

f:id:sbgMahiro:20180303162431p:plain

タイマーの起動後、Service側で時間をカウントし、それをBroadcastとしてActivityへ送信します。
それを受け取ったActivityは、受信結果を元にUIを更新します。

流れとしてはこれだけです。

LocalBroadcastManagerの使い方

AndroidStudioのバージョンは3.0.1です。
Android6.0以上向けのアプリケーションとしてプロジェクトを作成しています。

レイアウトはこのように作成しました。
Serviceを起動するためのButtonと、結果を表示するTextViewがあるだけです。

f:id:sbgMahiro:20180303171040p:plain

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.app.sbgmahiro.testbroadcast.MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:id="@+id/tvText"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="10dp"
            android:layout_marginRight="10dp"
            android:layout_marginTop="10dp"
            android:padding="5dp"
            android:text="0"
            android:textSize="18sp" />

        <Button
            android:id="@+id/bt_click"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            android:fontFamily="monospace"
            android:text="start service"
            android:textSize="18sp" />
    </LinearLayout>
</android.support.constraint.ConstraintLayout>

Broadcastの受信側

まず、Activity側にBroadcastの受信処理を実装します。
今回はMainActivity内にBroadcastを受信した後の処理を行うクラスを配置しました。

package com.app.sbgmahiro.testbroadcast;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    // 他とカブっていなければなんでもいい
    public static final String ACTION_CODE = "com.app.sbgmahiro.testbroadcast";
    public static final String SEND_CODE = "SEND";

    private LocalBroadcastManager broadcastManager;
    private BroadcastReceiver broadcastReceiver;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Serviceを起動するためのIntent
        final Intent itService = new Intent(getApplication(), Service.class);

        // Serviceを起動するButton
        Button btService = (Button) findViewById(R.id.bt_click);
        btService.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                startService(itService);
            }
        });

        // 結果を表示するTextView
        tvText = (TextView) findViewById(R.id.tvText);

        // BroadcastManagerの初期化(引数はContext)
        broadcastManager = LocalBroadcastManager.getInstance(this);
        // BroadcastRecieverの初期化 ※BroadcastRecieverを継承した自作クラス
        broadcastReceiver = new testBroadcastReciever();

        // Action名を設定
        IntentFilter filter = new IntentFilter();
        filter.addAction(ACTION_CODE);

        // Recieverの登録
        broadcastManager.registerReceiver(broadcastReceiver, filter);
    }

    // Broadcastを受け取った際の処理
    public class testBroadcastReciever extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {

            // Intentから送られてきた値を取得
            String recieveText = intent.getStringExtra(SEND_CODE);

            // 結果を更新
            tvText.setText(recieveText);
            Log.d("MainActivity Log.", recieveText);
        }
    }
}

Broadcastの送信側

続いて、Service側にBroadcastの送信処理を実装します

送信側では、どこへ送信するかを指定し、送信するだけです。
受信側と違って非常にシンプルです。

今回、このService内で行っている処理は、1秒おきに変数をインクリメントし、Activityへ送信しています。
10秒後には強制的にServiceを終了しています。

package com.app.sbgmahiro.testbroadcast;

import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;

public class Service extends android.app.Service{

    private Thread thread;
    private Handler handler;
    private Runnable runnable;

    private Intent intent;

    private int count = 0;

    // 送信用のBoradcastManager
    private LocalBroadcastManager broadcastManager;

    @Override
    public void onCreate () {
        super.onCreate();

        handler = new Handler();
        runnable = new Runnable() {
            @Override
            public void run() {

                count ++;
                if (count > 10) {

                    stopService();
                    return;
                }

                // Broadcastを送信
                sendBroadcast(count);
                Log.d("Service Log.", String.valueOf(count));

                handler.postDelayed(runnable, 1000);
            }
        };

        thread = new Thread(new Runnable() {
            @Override
            public void run() {

                handler.post(runnable);
            }
        });

        // LocalBroadcastManagerの初期化(引数はContext)
        broadcastManager = LocalBroadcastManager.getInstance(this);
    }

    @Override
    public int onStartCommand (Intent intent, int flag, int id) {
        super.onStartCommand(intent, flag, id);

        this.intent = intent;
        thread.start();

        return START_NOT_STICKY;
    }

    @Override
    public void onDestroy () {
        super.onDestroy();
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    private void stopService () {

        stopService(this.intent);
    }

    // Broadcastを送信
    private void sendBroadcast (int count) {

        // 送信するAction名を指定
        Intent intent = new Intent(MainActivity.ACTION_CODE);
        // Intentに送信する変数を格納
        intent.putExtra(MainActivity.KEY_CODE, String.valueOf(count));

        // Broadcastを送信
        broadcastManager.sendBroadcast(intent);
    }
}

Manifestにrecieverを記載

最後に、ManifestにRecieverを記載します。
これをしないとBroadcastを受け取ることが出来ません。

また、Manifest内でAction名を記載します。
これはActivity内やService内で指定したものと同じである必要があります。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.app.sbgmahiro.testbroadcast">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <service android:name=".Service"></service>
        <!-- Recieverを記載 -->
        <receiver
            android:name=".MainActivity$testBroadcastReciever"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <!-- action名はActivityで指定しているものと同じでなければならない -->
                <action android:name="com.app.sbgmahiro.testbroadcast"></action>
            </intent-filter>
        </receiver>
    </application>

</manifest>

実行してみる

実行後のログはこのように表示されました。
SeriviceでBroadcastが送信された後、Activity側で受信されていることが確認できます。

03-03 17:15:34.659 2456-2456/com.app.sbgmahiro.testbroadcast D/Service Log.: 1
03-03 17:15:34.661 2456-2456/com.app.sbgmahiro.testbroadcast D/MainActivity Log.: 1
03-03 17:15:35.660 2456-2456/com.app.sbgmahiro.testbroadcast D/Service Log.: 2
03-03 17:15:35.661 2456-2456/com.app.sbgmahiro.testbroadcast D/MainActivity Log.: 2
03-03 17:15:36.662 2456-2456/com.app.sbgmahiro.testbroadcast D/Service Log.: 3
03-03 17:15:36.663 2456-2456/com.app.sbgmahiro.testbroadcast D/MainActivity Log.: 3
03-03 17:15:37.667 2456-2456/com.app.sbgmahiro.testbroadcast D/Service Log.: 4
03-03 17:15:37.670 2456-2456/com.app.sbgmahiro.testbroadcast D/MainActivity Log.: 4
03-03 17:15:38.706 2456-2456/com.app.sbgmahiro.testbroadcast D/Service Log.: 5
03-03 17:15:38.707 2456-2456/com.app.sbgmahiro.testbroadcast D/MainActivity Log.: 5
03-03 17:15:39.733 2456-2456/com.app.sbgmahiro.testbroadcast D/Service Log.: 6
03-03 17:15:39.734 2456-2456/com.app.sbgmahiro.testbroadcast D/MainActivity Log.: 6
03-03 17:15:40.759 2456-2456/com.app.sbgmahiro.testbroadcast D/Service Log.: 7
03-03 17:15:40.760 2456-2456/com.app.sbgmahiro.testbroadcast D/MainActivity Log.: 7
03-03 17:15:41.760 2456-2456/com.app.sbgmahiro.testbroadcast D/Service Log.: 8
03-03 17:15:41.762 2456-2456/com.app.sbgmahiro.testbroadcast D/MainActivity Log.: 8
03-03 17:15:42.762 2456-2456/com.app.sbgmahiro.testbroadcast D/Service Log.: 9
03-03 17:15:42.763 2456-2456/com.app.sbgmahiro.testbroadcast D/MainActivity Log.: 9
03-03 17:15:43.764 2456-2456/com.app.sbgmahiro.testbroadcast D/Service Log.: 10
03-03 17:15:43.766 2456-2456/com.app.sbgmahiro.testbroadcast D/MainActivity Log.: 10

動作中の動画です。
Activityへ復帰後に表示が更新されていることが確認できます。

若干怪しいですが…


【Android】ServiceからBroadcastRecieverでUIを操作

【Bluetoothスピーカー】AnkerのSoundCore(初代)がやってきた

f:id:sbgMahiro:20180203171836j:plain
AnkerのBluetoothスピーカー,SoundCore(初代)を今更入手しました.

年末の話になるのですが,auからメールが届きました.
内容は「au STARで使えるポイントあげる」です.

3000ポイントくらい貰いました.
せっかくなのでと,このポイントを使って注文したのです.

Anker SoundCore

SoundCoreはAnkerのBluetoothスピーカーのシリーズです.

私が入手した無印のSoundCore(初代)の他に,一つ新しいモデルのSoundCore2.
miniやnano,Sportなど…かなり種類があります.

値段は2500円くらいから7000円くらいです.
初代は4000円くらいで購入できます.

Bluetoothスピーカーの中ではかなり安い部類です.
そしてAmazonでは3600件以上のレビューがあり,4.4という評価になっています.

外観

f:id:sbgMahiro:20180203171843j:plain
音楽の再生中は正面のLEDが青色に点灯します.

f:id:sbgMahiro:20180203171814j:plain
本体上部には各種ボタンがあります.

電源オンオフと再生停止,音量操作,ペアリングなどができます.
アプリによっては,再生ボタンを複数回押すことで曲送りと曲戻しも可能です.

このボタンは押した時の感触がちょっと硬いです.
なので,曲送りと曲戻しの操作は若干難しいです.

f:id:sbgMahiro:20180203171817j:plain
本体の右側面には,充電LEDとAUX入力端子,MicroUSB type-B入力端子,マイク穴があります.

AUX端子があるのでバッテリー切れのときでも動作します.
MicroUSB入力は充電のみのようで,PCに挿しても特に入力が出来るわけではなさそうでした.

音に関して

正式な仕様として載っているわけではありませんが,コーデックはおそらくSBCのみです.

音はかなり良い方だと思います.
低音がやや貧弱ですが,値段を考えれば十分です.

少なくとも,MacbookAirの内蔵スピーカーよりは圧倒的に良いと思います.

しかし,私が音以上に驚いたのは遅延でした.
このスピーカーはコーデックがSBCのみにも関わらず,遅延がほぼないのです.

開封してPCと接続してYouTubeで動画を再生して驚愕しました.
動画が普通に見れます.

遅延がないのが逆に違和感でした.

まとめ

音は並ですが,遅延がほぼなく,動画も普通に見れます.
値段も4000円程度とかなり安い部類です.

一つ持っておいても損はないというレベルかと思います.
もしBluetoothスピーカーの購入を検討しているのであれば,候補に挙げる価値はあるんじゃないでしょうか.

【ヘッドホン】SENNHEISER HD599を購入

f:id:sbgMahiro:20180114164924j:plain
SENNHEISERの開放型ヘッドホン,HD599を購入しました.

HD599を買うに至った理由は,装着感の優れたヘッドホンが欲しくなったからです.

今まで使っていたヘッドホンはaudio-technicaのATH-M50xでした.
コイツは重く,ずれやすく,長時間付けっぱなしで作業するのには不向きな機種でした.
またモニターヘッドホン故に高音域がちょっとキツイのです.

そう言う不満点を解消したいと重い,装着感が良く,音傾向が柔らかめなヘッドホンを探した結果SENNHEISERのHD599へたどり着きました.

装着感

このヘッドホンの特筆すべき点はやはり装着感でしょう.

カップが非常に大きいため,耳に全く干渉がありません.
またイヤーパッドも非常に柔らかいので眼鏡を掛けていても違和感がりません.

重量はメーカーの仕様では250gです.
目安としては,200g以下になると”軽いヘッドホン”になるかと.

ですが以前使っていたATH-M50xは285gなので,それと比べれば格段に軽いです.

側圧は,開封したてでは少し強めです.
私はティッシュの箱を挟んで調整しました.

2,3時間は平気で付けていられるほど装着感が良いです.
これだけでも選択肢に入る価値があると思います.

音傾向

”耳に優しい音””柔らかい音”という表現が合います.
どんなジャンルの楽曲もこなせますが,これと言って得意分野もない.そんな印象です.

SENNHEISERのイヤホン,ヘッドホンは大体こう言う傾向にあると思いますが.

解像度は並ですが,音場がとても広いです.
楽器の音一つ一つが良く聞き取れるので,特定の楽器の音だけ追ってみたりする人には適していそうです.

ちなみにこのHD599はインピーダンスが50Ω,音圧感度(SPL)が106dbで,一般的には高抵抗低感度に位置します.
ですが,PC直挿しでも,スマホに直挿しでも音量は問題なくとれます.
音痩せも特に感じません.

公式で1.2mのモバイル用ケーブルを付けてくるくらいなので,SENNHEISERとしても再生機器を選ばないことを想定しているのでしょう.

まとめ

f:id:sbgMahiro:20180114171125j:plain
総評すると,このような感じです.

発売当初は30000円くらいしていたようですが,現在は値下がりして23000円程度で買うことができます.
かなりお買い得かと.

【Java】TexShopに対応したURLに変換するプログラム※改良版

mahiro-second.hatenablog.com
前回ウェブサイトのURLをTexShopで正常動作するURLに変換するプログラムを作成しました.
あれからこれを使って変換していたのですが,一々範囲選択肢てコピペすることすら面倒になってきました.

ならば変換したURLをクリップボードにコピーすれば,あとはペーストするだけです.
という訳でクリップボードにコピーするコードを追加しました.

import java.util.Arrays;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;

class transformURL {
	public static void main (String[] args) {
		System.out.println("入力されたURL" + Arrays.toString(args));
		
		String str = "";
		int length = 0;
		
		if (!args[0].isEmpty()) {			
			str = args[0];
			length = str.length();
		}
		String[] ans = new String[length];

		
		for (int i=0; i<length; i++) {
			if (str.charAt(i) == 47) {
				if (str.charAt(i+1) == 47) {					
					ans[i] = "\\" + "slash";
					ans[i+1] = "\\" + "slash{}";
				} else {
					ans[i] = "\\" + "slash{}";
				}
			} else if (str.charAt(i) == 95) {
				ans[i] = "\\_";
			} else {
				ans[i] = String.valueOf(str.charAt(i));
			}
		}
		
		StringBuffer buf = new StringBuffer();
		for (int i=0; i<length; i++) {
			buf.append(ans[i]);
		}
		
		Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
		StringSelection selection = new StringSelection(buf.toString());
		clipboard.setContents(selection, null);
		
		System.out.println("Tex形式のURL[" + buf.toString() + "]をクリップボードにコピーしました.");
	}
}

ものの数分で追加したのでやはり雑です.

$ java transformURL http://mahiro-second.hatenablog.com
入力されたURL[http://mahiro-second.hatenablog.com]
Tex形式のURL[http:\slash\slash{}mahiro-second.hatenablog.com]をクリップボードにコピーしました.

実行結果はこのように.
GUIの方では一瞬jarアプリが起動しますね.

これでもう少し楽になるだろう…