20200913のAndroidに関する記事は7件です。

TextViewをHTMLのように下線を引いたり着色したりする

android.core.textSpannableStringBuilder拡張関数を見つけたので使ってみる

また、TextViewの拡張関数としているが、BindingAdapterでも使えるようにしている。

下線

SpannableStringBuilder().underline {}を使うパターン

/**
 * TextViewに下線付きで文字を表示する (SpannableStringBuilder 版)
 */
@BindingAdapter("textWithUnderLine")
fun TextView.textWithUnderLine(text: String?) {
    if (text.isNullOrEmpty()) return
    this.text = SpannableStringBuilder().underline {
        append(text)
    }
}

HtmlCompatを使うパターン

/**
 * TextViewに下線付きで文字を表示する(HtmlCompat 版)
 */
@BindingAdapter("textWithUnderLine")
fun TextView.textWithUnderLine(text: String?) {
    text ?: return
    this.text = HtmlCompat.fromHtml("<u>$text</u>", HtmlCompat.FROM_HTML_MODE_COMPACT)
}

BindingAdapter で使うとき

app:textWithUnderLineで使える

<TextView
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:layout_marginBottom="16dp"
     app:textWithUnderLine='@{"under line"}'
     tools:text="under line"
     />

文字色

SpannableStringBuilder().color() {}を使うパターン

/**
 * TextViewに文字色をつける(SpannableStringBuilder 版)
 */
@BindingAdapter("textWithRedColor2")
fun TextView.textWithRedColor2(text: String?) {
    if (text.isNullOrEmpty()) return
    val color = ContextCompat.getColor(this@textWithRedColor2.context, R.color.colorAccent)
    this.text = SpannableStringBuilder().color(color) { append(text) }
}

HtmlCompatパターン

/**
 * TextViewに文字色をつける(HtmlCompat 版)
 */
@BindingAdapter("textWithRedColor")
fun TextView.textWithRedColor(text: String?) {
    if (text.isNullOrEmpty()) return
    this.text = HtmlCompat.fromHtml("ここは<font color='red'>$text</font>", HtmlCompat.FROM_HTML_MODE_COMPACT)
}

背景色

/**
 * 文字のBackgroundに色をつける(SpannableStringBuilder 版)
 */
@BindingAdapter("textWitBackgroundColor")
fun TextView.textWitBackgroundColor(text: String?) {
    if (text.isNullOrEmpty()) return
    val color = ContextCompat.getColor(this.context, R.color.colorAccent)
    this.text = SpannableStringBuilder().backgroundColor(color) {
        append(text)
    }
}

GitHub

https://github.com/ikemura23/Android-Kotlin-Lab/blob/text_decoration_html/app/src/main/java/com/ikemura/android_kotlin_lab/extention/TextExt.kt

備考

SpannableStringBuilderで複数パターンの装飾をするにはsetSpan()を使うとできるはず。

HtmlCompatはタグを書いて装飾するため、自由度とカスタム製が高そう

Spanの公式ドキュメント
https://developer.android.com/guide/topics/text/spans?hl=ja

SpannableStringBuilderの拡張関数
https://developer.android.com/reference/kotlin/androidx/core/text/package-summary?hl=ja#extension-functions

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Androidスタジオ始めました

以前からスマホアプリを作ってみたいと思っていたが、
javaを学びつつandroidアプリ作れば一石二鳥なのでは?という
非常に安直な理由を思いつき始めてみました。

と思い立ったのも束の間、ダウンロードしていきなりしょうもないところで壁にぶつかってしまったので忘備録として残しておきます。

まずは定番のメモアプリを作ろうとして"text view"を追加したところ、
テキストサイズの変更がわからない...(恥ずかしい)
スクリーンショット 2020-09-13 16.59.49.png
ググってみたとろ”Properties Window”なるものがあるらしいのだがそんなものはない。
(多分バージョン違う?)

探してみたとろ、"Attuributes Window"の"All Attuributes"直下に一応"text size"がありました。 
スクリーンショット 2020-09-13 16.56.48.png

もっとわかりやすいところに機能がありそうなので引き続き探してみます。
なんとなく3DCADソフトとかに操作感が似ているので
3Dソフトをいじっていた時を思い出して楽しい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HMS CoreのMap KitとSite KitにおけるAPI Keyの利用の注意点

背景

ファーウェイのHMSを使って、GMSなしでGoogleマップアプリと同じものを作ってみました。最終的には無事に作れました。しかし、作っているときにはまった部分があったので、その原因を書こうと思います。

アプリのターゲット機能

機能 キット
地図表示 Map Kit
ジェスチャー Map Kit
コンパス Map Kit
ズーム Map Kit
現在地 Location Kit
キーワード検索 Site Kit
ロケーション情報検索 Site Kit

APIキーの受け渡し

特定なキットを利用するのに、APIキーを渡さなければなりません。
今回利用するキットはMap Kit、Location Kit、Site Kitです。Map KitとSite KitではAPIキーの受け渡しが必要です。
下記はHMSの公式ドキュメントに書かれたAPIキーの渡し方です。

Map Kit

方法1

// In the entrance class (inherited from android.app.Application) of the app,
// call the setApiKey method in the overridden onCreate() method. 
public class MyApp extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        // Set the API key.
        MapsInitializer.setApiKey("Your API Key");
    }
}

方法2

// FragmentまたはMapViewの中でAPIキーを渡す
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        Log.i(TAG, "onCreate: ");
        super.onCreate(savedInstanceState);
        // Set the API key before calling setContentView.
        MapsInitializer.setApiKey("Your API Key");
        setContentView(R.layout.basic_demo);
    }

簡単にいうと、MapsInitializer.setApiKey("Your API Key")を実行するというわけです。

Site Kit

// Declare a SearchService object.
private SearchService searchService; 
// Instantiate the SearchService object.
searchService = SearchServiceFactory.create(this, "API key");

実行結果(失敗)

上記の方法に従って実行してみると…

マップは表示できました。
device-2020-09-13-164940.png

しかし、キーワード検索機能は動きませんでした。
device-2020-09-13-165051.png

原因

原因はSite Kitに渡すAPIキーです。Map KitではAPIキーをそのまま渡せばよいですが、Site Kitの場合、APIキーをエンコードしてから渡さなければなりません。

実行結果(成功)

次のようにソースコードを直して試してみたら…

searchService = SearchServiceFactory.create(this, URLEncoder.encode("API key", "utf-8"));

検索結果が出ました。
device-2020-09-13-170255.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コマンドラインから画像の撮影時刻のズレを補正してAndroid端末に転送する

撮影時刻がなぜかズレている画像ファイルのexifを、Macのコマンドラインから修正し、最後にそれをAndroid端末に転送してついでファイル変更日にも反映することで、Android端末のGoogleフォトアプリで正しい日時でタイムライン表示されるようにしたメモ。

前提

  • 4年前に行ったキャニオニングで、スタッフさんがデジカメで撮ってくれた写真をCD-Rで購入してたので、バックアップのためにそれを再度PCに取り込んでみたら、以下の問題があった
    • なぜか途中で撮影時刻が1日ずれていた
    • 中には動画もあり、それらの動画ファイル内にexif的な撮影日時情報が(カメラが古くて?)存在しないようだった
    • 複数のデジカメから取り込まれているようだったが(ファイル名が違った)、その1つのデジカメデータが撮影日よりも1年以上古いファイル作成日になっていた(おそらく取り込んだPCの時刻設定の問題)
  • その結果、それらをGoogleフォトにアップロードすると、各ズレがそのまま反映され、タイムライン上で画像や動画がばらけて表示された

という状況で、

  • 撮影日時のずれてい静止画ファイルと、撮影日時情報を持たない動画ファイルを、正しい日付に補正したい
  • それをAndroid端末に取り込んで、正しい日付で表示したい

を目標とした。

環境

Mac

% sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.5
BuildVersion:   19F101

% uname -v
Darwin Kernel Version 19.5.0: Tue May 26 20:41:44 PDT 2020; root:xnu-6153.121.2~2/RELEASE_X86_64

% echo $ZSH_VERSION
5.7.1

% exiftool -ver 
12.00

Android

% adb shell getprop ro.build.version.release
10
% adb shell getprop ro.build.id
QQ3A.200805.001
 % adb shell getprop ro.product.model
Pixel 3

1日のズレを補正する

Macのプレビュー.appで撮影日時を調べると、なぜか撮影の途中(連番ファイル名の途中)で日付が1日巻き戻っているようだったので、それらを検知して+1日を加算することにした。

まず、ズレてる画像だけを抜き出す

具体的なズレ幅が事前に分かっている(正しくは2016年10月1日だが、1日前の9月30日になっている)ので、Exifの撮影日タグをexiftoolで取り出し、2016年9月かどうかで判別し、一時フォルダに入れる。

mkdir date_converted
for file in `\find . -maxdepth 1 -type f -name '*.JPG'`; do
    DATETIME=`exiftool $file -s -s -s -DateTimeOriginal`
    if [[ $DATETIME =~ "2016:09:" ]] then
        cp $file date_converted
        echo "$file $DATETIME"
    fi
done

撮影タグの日付フォーマットは YYYY:MM:DD HH:MM:SS
see: https://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif/datetimeoriginal.html

-s -s -s というオプションはタグの値のみを取り出すオプションで、sを重ねるごとに出力が短くなるようだ。 -s3 でもよかったみたい。

% exiftool RIMG1465.JPG -EncodingProcess    
Encoding Process                : Baseline DCT, Huffman coding
% exiftool RIMG1465.JPG -EncodingProcess -s
EncodingProcess                 : Baseline DCT, Huffman coding
% exiftool RIMG1465.JPG -EncodingProcess -s -s
EncodingProcess: Baseline DCT, Huffman coding
% exiftool RIMG1465.JPG -EncodingProcess -s -s -s
Baseline DCT, Huffman coding
% exiftool RIMG1465.JPG -EncodingProcess -s3     
Baseline DCT, Huffman coding

取り出したフォルダ内の全ファイルに対して、撮影日時を1日進める

相対指定で1日を加算する。
今回は使わなかったが、絶対指定もできるようだ。

exiftool "-DateTimeOriginal+=0:0:1 0:0:0" date_converted
exiftool "-CreateDate+=0:0:1 0:0:0" date_converted

mv date_converted/*.JPG .

CreateDate とは DateTimeOriginalのことで、Macのプレビュー.appでは「デジタル化日時」として表示されるもの。これも同じ日時にしておく。

処理後、オリジナルファイルは _original というsuffixつきで保存されてるのでそれはそのままに、変換済みファイルを元フォルダに書き戻す。

ファイルの変更日(ついでにファイル名にも)を、撮影日で更新する

  • 撮影日とファイル変更日がズレているものを合わせる
  • 動画ファイルの撮影日を、直前の静止画の撮影日から計算し、それをファイル変更日に設定する
  • 後のために、その日時をファイル名にも使用しておく

がやること。

mkdir filename_converted
for file in `\find . -maxdepth 1 -type f -name '*.JPG' -or -name '*.mp4' | sort`; do
    FILENAME=`basename -- $file`
    DATETIME=`exiftool $file -s -s -s -DateTimeOriginal`
    if [[ $DATETIME =~ "0000:00:00" ]] then
        TIMESTAMP=`date -r $PREV_TIMESTAMP -v+1S "+%s"`
    else
        TIMESTAMP=`date -j -f "%Y:%m:%d %H:%M:%S" "$DATETIME" "+%s"`
    fi
    echo "$file $TIMESTAMP"
    cp $file "filename_converted/${FILENAME:t:r}_$TIMESTAMP.${FILENAME:t:e}"

    PREV_TIMESTAMP=$TIMESTAMP
done

findで対象ファイルを絞り込みつつ、sortでファイル名順でforを回し、
撮影日がないファイル(動画ファイルのみのはず)は直前ファイルの1秒後とみなしつつ、
dateコマンドで日時文字列をUNIX timeに変換して、
それをファイル名に使う形で、別フォルダにコピー。

その後、別フォルダ上で、

cd filename_converted
for file in `\find . -maxdepth 1 -type f -name '*.JPG' -or -name '*.mp4' | sort`; do
    FILENAME=`basename -- $file`
    TIMESTAMP=`echo ${FILENAME:t:r} | cut -d _ -f2`
    touch -t `date -r $TIMESTAMP "+%Y%m%d%H%M"` $file
    echo $TIMESTAMP
done

ファイル名の中のタイムスタンプをcutコマンドで取り出しつつ、
dateコマンドでtouchコマンド用の日付文字列に変換して、touchコマンドでファイル変更日を更新。

これで完成。

ちなみに、単に撮影日時をファイル変更日に設定するけなら、exiftoolのコマンド一発で行ける。
https://qiita.com/suin/items/04f08c3d1c410264e94f

Android端末に転送して、Android上のファイルのタイムスタンプも更新

% adb push * /path/to/push/
% adb shell

adb pull には -a: preserve file timestamp and mode というタイムスタンプを維持するオプションがあるが、pushにはないで、push後にファイル名から撮影日時を特定してtouchで更新する。

adb-shell
for file in `\find . -maxdepth 1 -type f`; do
    FILENAME=`basename -- $file`
    TIMESTAMP=`echo $FILENAME | cut -d . -f1 | cut -d _ -f2`
    touch -t `date -d "@$TIMESTAMP" "+%Y%m%d%H%M"` $file
    echo $TIMESTAMP
done

※ せっかくCD-RのデータをGoogleフォトに入れておくなら高画質のままとしたかったので、Pixel3からのアップロードは静止画も動画も全て無制限という特典を活用したく、このような面倒な手順となった。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Kotlinの画面遷移

Android Kotlin画面遷移方法について調べてみた。

Manifestへの追加

AndroidManifest.xml
 <activity android:name=".MoveApp" android:label="Move" ></activity>

xml画面作成

デザインモードで作成した物をコード化すると以下になる。

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".MainActivity">

    <Button
        android:id="@+id/moveButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="移動する"
        tools:layout_editor_absoluteX="154dp"
        tools:layout_editor_absoluteY="426dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
app_move.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    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">

    <Button
        android:id="@+id/returnButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Return"
        tools:layout_editor_absoluteX="148dp"
        tools:layout_editor_absoluteY="266dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

画面遷移コード

MainActivity.kt
package com.example.moveapp

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.content.Intent
import kotlinx.android.synthetic.main.activity_main.*


class MainActivity:AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setScreenMoveApp()
    }

    private fun setScreenMoveApp(){
        setContentView(R.layout.activity_main)
        setScreenMain()
    }

    private fun setScreenMain() {
        setContentView(R.layout.activity_main)
        moveButton.setOnClickListener{
            val intent = Intent(application, MoveApp::class.java)
            startActivity(intent)
        }
    }
}
MoveApp.kt
package com.example.moveapp
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.app_move.*


class MoveApp:AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.app_move)
        setScreenMoveApp()
    }

    private fun setScreenMoveApp(){
        setContentView(R.layout.app_move)
        setScreenMain()
    }

    private fun setScreenMain(){
        setContentView(R.layout.app_move)
        returnButton.setOnClickListener{
            val intent = Intent(application, MainActivity::class.java)
            startActivity(intent)
        }
    }
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Raspberry Pi 4BにAndroid TV 10.0をインストール (LineageOS)

はじめに

近年、Android TVを搭載したTVが多数発売されています
少し興味があっても、様々な理由で購入に踏み切れない方もいらっしゃるのではないでしょうか。
そんな方におすすめしたいのが、"Android TV on Raspberry Pi"の方法です!

そこそこお安くご家庭の環境に合わせたAndroid TVが手に入ります。
既存のテレビを簡単にAndroid TV化することもできますよね!

というわけで、今回は最新版Raspberry Pi4にAndroid TVをインストールするために、筆者が行った方法をご紹介します!

注意点

  • 質問欄を通して可能な限りのサポートはしますが、故障の他、一切の事柄に対して筆者は責任を負いません。
  • 商用利用できません。ROMイメージの一部に表示-非営利-継承4.0国際ライセンスが含まれています。
  • 地上波放送・BS/CS放送は見られません。
  • できること/できないことは作者であるKonstaTさんのポストに説明があるため、参照されるとよいかと思います。

筆者の環境

  • Raspberry Pi 4B
  • 64GB Micro SDカード: 容量は最低16GBあれば良いらしいです。
  • 16GBUSBメモリ:容量は1GBもあれば十分かと思います。
  • Windows10 (イメージをSDカードに書き込むために用います。)
  • USB接続のマウス・キーボード

方法

まずはファイルの準備

  1. KonstaTさんのポストから必要なイメージをダウンロードしてください。3つ必要です。

    • ① OSのイメージ: lineage-17.1-20200815-UNOFFICIAL-KonstaKANG-rpi4-atv.zip というファイルです。
    • ② Google関係のアプリイメージ:Q&A欄 "How to install Google apps?"項にリンクがあります。
    • ③ リカバリーから復帰するためのイメージ:Q&A欄 "How to boot out of TWRP recovery?"項にリンクがあります。
  2. パソコンから、先ほどDLした①OSイメージをSDカードに書き込んでください。
    書き込み方法はRaspberryPi公式にありますが、私はその中でbalenaEtcherを用いました。

  3. パソコンで、先ほどDLした②Google関係のアプリイメージ③リカバリー復帰用イメージ
    USBメモリにコピーしてください。

そしてラズパイに書き込み、そしてリカバリ画面へ

  1. ラズパイにSDカード・USBマウス・キーボードを挿し、起動します。

  2. ひとまず適当にセットアップしてホーム画面を出してください。
    後で設定情報が消えるので適当で結構です。
    ※ 今後、十字キーと、Enter→タップ、F2→戻る、でリモコンの代用ができます。

  3. 開発者モードを有効化します。
    設定画面(右上の歯車を押す)→「デバイス設定」→「デバイス情報」に移動し、
    ビルド欄をEnter連打してください。

  4. リカバリー画面に移動します。
    設定画面→「開発者モード」に移動し、"Local Terminal"と"Root access"を有効化します。
    ※ Root Accessの有効化の際にセキュリティリスクが高まる旨の警告が出ます。
    その後、ホーム画面「アプリ」内にTerminalがあるはずです。それを起動します。
    Terminal内で以下のコマンドを打ってください。
    この後、自動で青っぽいデザインの画面が表示されるはずです。
    マウスを用いてスライドすれば、リカバリ画面(Install, Wipe, Mount等のボタンがある画面)に入ることができます。

$ su
# rpi4-recovery.sh
# reboot

リカバリ画面で追加インストール

  1. USBの読み出し
    まず、イメージを書き込んだUSBメモリをラズパイに挿します。
    ラズパイ画面→"Install"→"Select Storage"→"USB OTG"を選択すると、
    先ほどUSBに書き込んだ2ファイル②③が表示されるはずです。

  2. パッケージのインストール
    ②、③の順でインストールを行います。ここでもスライドすれば開始します。
    その後、前の画面に戻りWipeを押して実行します。

これらが完了したら、Rebootボタンを押してください。
再度セットアップを済ませれば、晴れてAndroid TVの準備完了です。

Android TVの世界をぜひお楽しみください!

参考文献

LineageOS 17.1 Android TV (Android 10) for Raspberry Pi 4
How to Install Android TV on a Raspberry Pi with LineageOS

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android ボタンの共通化

フラグメントで共通ボタン記述。

フラグメント内のstartボタンでリスナーを記述。

CommonButtonFragment.kt
    override fun onStart(){
        super.onStart()
        btToday.setOnClickListener{onCommonButtonClick(it)}
        btCreate.setOnClickListener{onCommonButtonClick(it)}
    }

共通ボタンファンクション

CommonButtonFragment.kt
private fun onCommonButtonClick(view: View?){
        if(view != null) {
            if(activity != null) {
                //login画面の場合
                if (activity!!.localClassName == "TodayToDoTaskLoginActivity" || activity!!.localClassName == "TaskCreateLoginActivity") {
                    when (view.id) {
                        R.id.btToday -> {
                            val intent = Intent(activity, TodayToDoTaskLoginActivity::class.java)
                            startActivity(intent)
                        }
                        R.id.btCreate -> {
                            val intent = Intent(activity, TaskCreateLoginActivity::class.java)
                            startActivity(intent)
                        }
                    }
                //login画面以外の場合
                } else {
                    when (view.id) {
                        R.id.btToday -> {
                            val intent = Intent(activity, TodayToDoTaskActivity::class.java)
                            startActivity(intent)
                        }
                        R.id.btCreate -> {
                            val intent = Intent(activity, TaskCreateActivity::class.java)
                            startActivity(intent)
                        }
                    }
                }
            }
        }
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む