投稿者「zawata」のアーカイブ

【Android】Navigation componentを使う

概要

Android Jetpackに含まれるNavigation componentを使うと、画面間の遷移を実装する上で複雑になりがちな点を軽減できる。今回は、このコンポーネントを使って簡単な画面遷移を実装してみる。

サンプルアプリ

環境

Android Studio 3.5.1

準備

build.gradle(app)に以下の依存を追加する。

dependencies {
    ...
    implementation 'androidx.navigation:navigation-fragment-ktx:2.1.0'
    implementation 'androidx.navigation:navigation-ui-ktx:2.1.0'
    ....
}

ライブラリーのバージョンはブログ執筆時点での最新バージョンのため、必要に応じて適宜更新してほしい。

ナビゲーションの追加

プロジェクトのresフォルダの下に新たにnavigationフォルダを作成し、ナビゲーションリソースxmlを追加する。ここでは、公式サンプルに倣ってnav_graph.xmlという名前にした。このファイルを選択すると下記のような画面が立ち上がる。ここでは、Fragmentを2つ作り、1つ目のFragmentから2つ目のFragmentへ遷移を示す矢印を引いている。

xmlは以下のようになっている。

nav_graph.xml

<?xml version="1.0" encoding="utf-8"?>
<navigation 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:id="@+id/nav_graph.xml"
    app:startDestination="@id/myFragment">
    <fragment
        android:id="@+id/myFragment"
        android:name="com.zawapro.navigationcomponentlesson.MyFragment"
        android:label="fragment_my"
        tools:layout="@layout/fragment_my" >
        <action
            android:id="@+id/action_myFragment_to_myFragment22"
            app:destination="@id/myFragment2" />
    </fragment>
    <fragment
        android:id="@+id/myFragment2"
        android:name="com.zawapro.navigationcomponentlesson.MyFragment2"
        android:label="fragment_my_fragment2"
        tools:layout="@layout/fragment_my_fragment2" />
</navigation>

上記の過程で、ナビゲーションエディタから2つのFragmentが作られる。これらのxmlを下記のように編集する。

1つ目は画面右下にボタンがあるFragment。

fragment_my.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"
    android:background="@color/colorPrimary"
    tools:context=".MyFragment">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="16dp"
        android:layout_marginRight="16dp"
        android:layout_marginBottom="16dp"
        android:text="Button"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

もう一つは遷移先の画面で、特に何もない画面である。

fragment_my_frgment2.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"
    android:background="@color/design_default_color_primary"
    tools:context=".MyFragment2"/>

次に、Activityが読み込むレイアウトファイルactivity_main.xmlを下記のように変更する。

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<fragment 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:id="@+id/nav_host_fragment"
    android:name="androidx.navigation.fragment.NavHostFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    app:defaultNavHost="true"
    app:navGraph="@navigation/nav_graph">
</fragment>

fragmentタグを使い、全面にFragmentを配置している。ポイントは6行目でNavHostFragmentを指定しているところである。このようにしておくと、NavHostFragmentは11行目app:navGraphで指定したidのナビゲーションリソースを読み込み、それに従った画面とナビゲーションを構築してくれる。10行目ではapp:defaultNavHost="true"としている。このようにすることで、デフォルトの画面から遷移した後、バックキーを押すとその画面に戻ってくることができる。

ここまで出来たら画面の遷移のコードを書こう。先のnav_host.xmlにて初期画面をMyFragmentに設定していた。そこで、MyFragmentのコードを以下のように編集する。

MyFragment.kt

class MyFragment : Fragment() {
  override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
  ): View? {
    // Inflate the layout for this fragment
    return inflater.inflate(R.layout.fragment_my, container, false).also { layout ->
      layout.findViewById<Button>(R.id.button).setOnClickListener {
        // NavControllerを呼び出し、アクションを指定して遷移する
        findNavController().navigate(R.id.action_myFragment_to_myFragment22)
      }
    }
  }
}

inflater.inflate(R.layout.fragment_my, container, false)also()で処理を追加している。その中では、findNavController()でNavControllerを取得し、そのnavigate()メソッドを使いアクション先を指定する。こうすると、指定した先の画面へと遷移が行われる。

以上がNavigation componentの使い方のサンプルである。
ここまでのソースをgithubにアップしたので、参考にしてほしい。


【Android】 OssLicensesMenuActivityに表示されるライセンス情報を手動で追加する

概要

Google Play ServicesのIncluding Open Source Noticesにあるツールを使うと、プロジェクトで使用しているオープンソースライブラリのライセンスリストを自動生成し、OssLicensesMenuActivityから表示させることができる。

 しかし、このツールはgradleに記載したライブラリしかリストアップしてくれない。 例えば、material iconなどのような、gradleの依存関係に記載できないプロダクトのライセンスはリストアップの対象外である。そこで、何とかこのツールを使いつつ、ライセンス項目を追加する方法を考えてみた。

今回作成したコードの完成版はこちら

環境

Android Studio 3.2.1

手順

まずは、空のAndroidプロジェクトを作り、OssLicensesMenuActivityを表示するところまでをやってみる。ここまでの手順は以下のサイトを参考にした。

まずルートレベルのbuild.gradleにプラグインのクラスパスを追加する。

build.gradle (root-level)

buildscript {
  repositories {
    // ...
    google()
  }
  dependencies {
    // ...
    // 追加
    classpath 'com.google.android.gms:oss-licenses-plugin:0.9.3'
  }
}

アプリケーションレベルのbuild.gradleの冒頭にプラグインを適用させる旨を追記する。

build.gradle (app-level)

apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'
// 追加
apply plugin: 'com.google.android.gms.oss-licenses-plugin'

同じくアプリケーションレベルのbuild.gradleのdependenciesブロックの中に、oss-licensesライブラリーを追加する。

implementation 'com.google.android.gms:play-services-oss-licenses:16.0.2'

デフォルトのActivityに適当なButtonを追加し、ボタンをクリックするとライセンス画面を表示するようにしてみる。

MainActivity.kt

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // OssLicensesMenuActivityにインテントを投げる
        findViewById<Button>(R.id.button).setOnClickListener{
            startActivity(Intent(this, OssLicensesMenuActivity::class.java))
        }
    }
}

ここまでの手順で、gradleのdependenciesに記述されたライブラリのライセンスを表示させることができた。

ライセンス情報の追加

さて、本題はここからである。gradleの依存関係に無いプロダクトのライセンス情報を手動で追加するにはどうすればよいだろうか?GitHubに公開されているプラグインのソースを眺めながらビルド時に何が行われるかを確認していこう。処理の大まかな流れとしては、

  • preBuild前に下記のタスクが実行されるように登録
    (OssLicensesPlugin.groovy)
  • gradleの依存情報を走査し、
    app/build/generated/third_party_licenses/res/dependencies.jsonに書き込む(DependencyTask.groovy)
  • 上記のjsonファイルの内容を元に
    app/build/generated/third_party_licenses/res/raw以下にthird_party_license_metadatathird_party_licensesを生成(LicensesTask.groovy)

ここまでの流れで、app/build/generated/third_party_licenses/resの構成は以下のように自動生成される。

├── dependencies.json
└── res
    └── raw
        ├── third_party_license_metadata
        └── third_party_licenses

third_party_licensesには依存情報から取得された全てのライセンスの具体的な内容が、そしてthird_party_license_metadataには以下の形式でコンテンツのメタ情報が書かれている。

コンテンツの開始位置:コンテンツの文字数 ライセンス名

その後、OssLicensesMenuActivityは起動時に上記の2つのファイルの内容を読み込み、リストビューに表示させているのである。

さて、ということはビルドプロセス中に、これらのファイルに任意の文字列を書き込んでしまえばライセンス情報を追加できそうである。そこで、アプリレベルのbuild.gradleに以下のコードを追加した。

build.gradle (app-level)

// ライセンス情報を追加するタスク
task addMyLicenseTask {
    final String UTF_8 = "UTF-8"
    final byte[] LINE_SEPARATOR = System.getProperty("line.separator").getBytes(UTF_8)

    // generateLicensesタスクの直後に起動
    mustRunAfter tasks.findByName('generateLicenses')

    // 手動でライセンス情報を追加する
    doLast {
        def dependencyOutput = new File(project.buildDir, "generated/third_party_licenses")

        def resourceOutput = new File(dependencyOutput, "/res")
        def outputDir = new File(resourceOutput, "/raw")

        // ライセンスファイル
        def licensesFile = new File(outputDir, "third_party_licenses")
        // ライセンスファイルへの書き込み前に現在の位置を保持
        def start = licensesFile.length()

        // ライセンスファイルへ書き込み
        def licenseContent = 'This is my library license!!'
        licensesFile << licenseContent
        licensesFile << (LINE_SEPARATOR)

        // ライセンスメタデータファイルに書き込み
        def licensesMetadataFile = new File(outputDir, "third_party_license_metadata")
        // "開始位置:文字数 ライセンス名"の形式で書き込む
        licensesMetadataFile << ("${start}:${licenseContent.length()} my_library")
        licensesMetadataFile << (LINE_SEPARATOR)
    }
}

// preBuild前にライセンス情報を追加する
tasks.findByPath(':app:preBuild').dependsOn addMyLicenseTask

とりあえず今回はmy_libraryという適当な文字列を追加している。
これをビルドして実行すると、意図通りライセンス情報が追加された。

※表示の順番はコンテンツファイルに書かれていた順番ではなく、ソートされている。

あとは、必要に応じて表示する内容を調整していけばいいだろう。
ここまでの完全なコードはGitHubにプッシュしたので参考にしてほしい。


【Android】Android Architecture Componentsを使う – LifeCycle編

最近、Android Architecture Componentsの1.0安定板が公開された。
まだ情報は少なく、APIの変更も頻繁に行われているようである。しかし、このライブラリはAndroidアプリを開発していく上での新たなコーディングスタイルの指針となるものであり、ネイティブアプリの開発者にとっては今後必修項目となるように感じられる。そこで、まずは Componentsのうちのひとつ、Lifecycleについて見ていくことにしたい。

Lifecycleとは

ある任意のクラスをActivityのライフサイクルに連動させることができる。これを使うと、これまではActivityやFragmentに書きがちだったActivity本来の処理とは関係のないアプリ特有のロジックを専用のクラスに移すことが容易になる。ActivityやFragmentの肥大化を防ぎ、各ロジックを受け持つクラスの責務がはっきりすることでコードの保守性が高まる。

準備

公式サイトにbuild.gradleにライブラリの依存を追加する方法が書いてある。しかし、実は最新のサポートライブラリ(本記事では27.1.1)を導入済みであれば、特に追加の設定なく使うことができた。この記事で作成したアプリのbuild.gradleを示す。

build.gradle
apply plugin: 'com.android.application'

android {
    compileSdkVersion 27
    defaultConfig {
        applicationId "sample.com.lifecyclelesson"
        minSdkVersion 14
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:27.1.1'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.1'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
}

これは、Android Studio 3.1で新しいプロジェクトを作成した時に自動的に作られたbuild.gradleそのものである。

ライフサイクルに連動させるクラスの作成

以下のように作成した。

MyClass.java
public class MyClass implements LifecycleObserver{
    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    void handleCreate(){
        Log.d("LifecycleLesson", "ON_CREATE");
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    void handleDestroy(){
        Log.d("LifecycleLesson", "ON_DESTROY");
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    void handlePause(){
        Log.d("LifecycleLesson", "ON_PAUSE");
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    void handleResume(){
        Log.d("LifecycleLesson", "ON_RESUME");
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    void handleStart(){
        Log.d("LifecycleLesson", "ON_START");
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    void handleStop(){
        Log.d("LifecycleLesson", "ON_STOP");
    }
}

ライフサイクルに連動させるには、LifecycleObserverをimplementsする必要がある。とは言うものの、LifecycleObserverは空のインターフェースに過ぎないので実装すべきメソッドは無い。意味合いとしては「ライフサイクルを観測するクラスである」ということを明記する、といったところだろう。あとは、ライフサイクルイベントが発生した時に呼び出されてほしい任意のメソッドに@OnLifecycleEventアノテーションを付与するだけである。メソッドは引数無しvoidの形式を守っていれば、名前は何でも良い。アノテーションの引数にはライフサイクルイベントの種類を指定する。

LifeCycleObserverの登録

Activityに先ほどのクラスを登録する。コードは以下のようになる。

MainActivity.java
public class MainActivity extends AppCompatActivity {

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

        Log.d("LifecycleLesson", "Activity::onCreate");

        // Lifecycleを取得し、LifecycleObserverをimplementsしたクラスを渡す
        getLifecycle().addObserver(new MyClass());
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        Log.d("LifecycleLesson", "Activity::onDestroy");
    }

    @Override
    protected void onPause() {
        super.onPause();

        Log.d("LifecycleLesson", "Activity::onPause");
    }

    @Override
    protected void onResume() {
        super.onResume();

        Log.d("LifecycleLesson", "Activity::onResume");
    }

    @Override
    protected void onStart() {
        super.onStart();

        Log.d("LifecycleLesson", "Activity::onStart");
    }

    @Override
    protected void onStop() {
        super.onStop();

        Log.d("LifecycleLesson", "Activity::onStop");
    }
}

最新のサポートライブラリのAppCompatActivityにはLifecycleを取得するためのgetLifecycle()メソッドがある。ActivityのonCreateのタイミングでLifecycleを取得し、addObserver()で登録することができる。

実行

アプリを起動、終了させると以下のようにLogcatに出力された。


04-11 23:01:58.680 20664-20664/sample.com.lifecyclelesson D/LifecycleLesson: Activity::onCreate
04-11 23:01:58.685 20664-20664/sample.com.lifecyclelesson D/LifecycleLesson: ON_CREATE
04-11 23:01:58.686 20664-20664/sample.com.lifecyclelesson D/LifecycleLesson: Activity::onStart
    ON_START
04-11 23:01:58.687 20664-20664/sample.com.lifecyclelesson D/LifecycleLesson: Activity::onResume
04-11 23:01:58.688 20664-20664/sample.com.lifecyclelesson D/LifecycleLesson: ON_RESUME
04-11 23:02:32.518 20664-20664/sample.com.lifecyclelesson D/LifecycleLesson: ON_PAUSE
    Activity::onPause
04-11 23:02:34.134 20664-20664/sample.com.lifecyclelesson D/LifecycleLesson: Activity::onResume
    ON_RESUME
04-11 23:03:33.507 20664-20664/sample.com.lifecyclelesson D/LifecycleLesson: ON_PAUSE
    Activity::onPause
04-11 23:03:33.979 20664-20664/sample.com.lifecyclelesson D/LifecycleLesson: ON_STOP
    Activity::onStop
04-11 23:03:33.980 20664-20664/sample.com.lifecyclelesson D/LifecycleLesson: ON_DESTROY
    Activity::onDestroy

Activityのイベントに連動して独自クラスのメソッドが呼び出されたことがわかる。

まとめ

Lifecycleを使うことで、簡単にActivityのライフサイクルに連動するクラスを作成することができた。今回はログを出力しただけだったが、実際にはアプリ特有の処理をこちらのクラスに書いていくことになるだろう。これまでのサンプルやチュートリアルで見られたActivityのon〜()系メソッドをオーバーライドして専用の処理を書いていたスタイルは今後、一変すると思う。最新のサポートライブラリに含まれているこの機能を積極的に使っていかない手はない。


【Xamarin.Forms】SkiaSharpを使って描画の実装を共通化する

概要

Xamarin.Formsを使うとマルチプラットフォーム向けアプリのUI実装が共通化できるのだが、デフォルトではボタンやテキストボックス等、必要最低限なコントロールしか用意されていない。
ちょっと凝ったことをしたい場合、例えば描画処理を自分で実装しようと思ったら各プラットフォームごとに書くしかないのである。
しかし、せっかくXamarinを使うのだったら、なるべくコードは共通化したい。
そこで、SkiaSharpというライブラリを使ってPCL側で描画処理を実装するようにしてみる。

環境

macOS Sierra
Visual Studio Community 2017 for Mac

準備

Visual Studioを起動し、新しいソリューションを作成する。
ここでは、ソリューション名を「HelloSkia」とした。プロジェクトのテンプレートは
Multiplatform -> アプリ -> Xamarin.Forms -> Blank Forms App
を選択する。

ソリューションが作成されると、中にPCL, Android向け, iOS向けのプロジェクトができているので、
すべてのプロジェクトに必要なパッケージを追加する。今回は、
プロジェクトを右クリック -> 追加 -> NuGetパッケージの追加SkiaSharp for Xamarin.Formsを選ぶ。
この時点で、PCLプロジェクトにはSkiaSharp, SkiaSharp.Views.Forms, AndroidとiOSプロジェクトにはSkiaSharp, SkiaSharp.Views, SkiaSharp.View.Formsのパッケージが追加されていればよい。

実装

xamlの実装
HelloSkiaという名前でソリューションを作成した場合、PCLプロジェクトにHelloSkiaPage.xamlというファイルが生成されているはずである。まずは、このファイルを編集する。

HelloSkiaPage.xaml
<ContentPage 
    xmlns="http://xamarin.com/schemas/2014/forms" 
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
    xmlns:local="clr-namespace:HelloSkia"
    xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
    x:Class="HelloSkia.HelloSkiaPage">
    <StackLayout VerticalOptions="FillAndExpand">
        <Label Text="Welcome to Xamarin Forms!" VerticalOptions="Center" HorizontalOptions="Center" />
        <skia:SKCanvasView x:Name="canvasView" PaintSurface="Handle_PaintSurface" HeightRequest="400" />
    </StackLayout>
    
</ContentPage>

注目するのはハイライトした部分である。
xmlns:skia=”clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms”
として、以降skia:という名前でSkiaSharpのクラスにアクセスできるようにする。
そして、StackLayoutに1つのSKCanvasViewを追加している。SKCanvasViewはカスタムの描画処理を実装できるViewで、Xamarin.Formsの他のViewと同じようにレイアウトに含めたりできる。
このタグの中にあるPaintSurfaceというのはSKCanvasViewの描画イベントである。
ここにHandle_PaintSurfaceというメソッド名を指定している。このようにすると描画が要求されたタイミングでこのメソッドが呼ばれるようになる。

コードビハインドの実装
次にコードビハインドを実装していこう。xamlがHelloSkiaPage.xamlという名前であれば、コードビハインドのファイルはHelloSkiaPage.xaml.csという名前でHelloSkiaPage.xamlの一階層下にあるはずである。実装内容は以下のようにした。

HelloSkiaPage.xaml.cs
using SkiaSharp;
using Xamarin.Forms;

namespace HelloSkia
{
    public partial class HelloSkiaPage : ContentPage
    {
        private SKPaint rectPaint = new SKPaint
        {
            StrokeWidth = 4,
            IsAntialias = true,
            Color = SKColors.Blue
        };
        private SKPaint circlePaint = new SKPaint
        {
            StrokeWidth = 4,
            IsAntialias = true,
            Color = SKColors.Green
        };
        private SKPaint linePaint = new SKPaint
        {
            StrokeWidth = 4,
            IsAntialias = true,
            Color = SKColors.Red
        };


        public HelloSkiaPage()
        {
            InitializeComponent();
        }

        // 描画メソッド。
        void Handle_PaintSurface(object sender, SkiaSharp.Views.Forms.SKPaintSurfaceEventArgs e)
        {
            var canvas = e.Surface.Canvas;

            // 四角形を描画
            canvas.DrawRect(new SKRect(10, 10, 200, 200), rectPaint);
            // 円を描画
            canvas.DrawCircle(320, 110, 100, circlePaint);
            // 線を描画
            canvas.DrawLine(430, 10, 630, 210, linePaint);
            canvas.DrawLine(630, 10, 430, 210, linePaint);
        }
    }
}

描画処理は先ほどのxamlで指定しておいたHandle_PaintSurfaceメソッドに書いて行く。
ここでは、四角形、円、ラインといった簡単な図形を描画してみた。APIの使い方は、AndroidでonDrawを実装するときとほぼ一緒という感覚である。
これを各プラットフォームのシミュレータで実行すると以下のようになる。

Android
iOS

ここまでで、AndroidとiOSのプロジェクト内には何らソースコードを書いていない。このようにすることで、AndroidとiOSで描画処理を共通化することができた。

参考サイト・記事

Xamarin.FormsとSkiaSharpを組み合わせたチュートリアル。英語だが、SkiaSharpを使う上で最小限のことがまとまっていてわかりやすい。
https://blog.xamarin.com/drawing-with-skiasharp/

自サイト内記事。AndroidでのViewの描画方法について。
https://zawapro.com/?p=331


【iOS】UIViewをタッチして移動させる

使用環境
Xcode:8.0
Swift:3.0

UIViewをタッチして移動させてみよう。完成イメージは以下の通りである。
20161022_viewtouchmove
※クリックでGIFアニメーションを表示

UIViewControllerが用意したデフォルトのUIViewにサブビューを追加し、赤いビューと青いビューをタッチして動かせるようになっている。

準備

Xcodeを起動したら「Single View Application」テンプレートで新しいプロジェクトを作成する。

実装

プロジェクト内にあるViewController.swiftに処理を書いていく。
以下にコードを示す。

ViewController.swift

import UIKit

// ユーティリティメソッド CGPoint同士の足し算を+で書けるようにする
func -(_ left:CGPoint, _ right:CGPoint)->CGPoint{
    return CGPoint(x:left.x - right.x, y:left.y - right.y)
}
// ユーティリティメソッド CGPoint同士の引き算を-で書けるようにする
func +(_ left:CGPoint, _ right:CGPoint)->CGPoint{
    return CGPoint(x:left.x + right.x, y:left.y + right.y)
}

class ViewController: UIViewController {

    // タッチ開始時のUIViewのorigin
    var orgOrigin: CGPoint!
    // タッチ開始時の親ビュー上のタッチ位置
    var orgParentPoint : CGPoint!
    
    // Viewのパンジェスチャーに反応し、処理するためのメソッド
    func handlePanGesture(sender: UIPanGestureRecognizer){
        switch sender.state {
        case UIGestureRecognizerState.began:
            // タッチ開始:タッチされたビューのoriginと親ビュー上のタッチ位置を記録しておく
            orgOrigin = sender.view?.frame.origin
            orgParentPoint = sender.translation(in: self.view)
            break
        case UIGestureRecognizerState.changed:
            // 現在の親ビュー上でのタッチ位置を求める
            let newParentPoint = sender.translation(in: self.view)
            // パンジャスチャの継続:タッチ開始時のビューのoriginにタッチ開始からの移動量を加算する
            sender.view?.frame.origin = orgOrigin + newParentPoint - orgParentPoint
            break
        default:
            break
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // ビューを2つ作成し、Subviewとして追加する
        // !!注意!!
        // UIGestureRecognizerのインスタンスは複数のViewで使いまわせないので、各View毎に作成する
        let view1 = UIView(frame: CGRect(x:10,y:20,width:100,height:50))
        view1.backgroundColor = UIColor.red
        view1.addGestureRecognizer(
            UIPanGestureRecognizer(target:self, action:#selector(handlePanGesture)))
        self.view.addSubview(view1)
        
        let view2 = UIView(frame: CGRect(x:10,y:100,width:100,height:50))
        view2.backgroundColor = UIColor.blue
        view2.addGestureRecognizer(
            UIPanGestureRecognizer(target:self, action:#selector(handlePanGesture)))
        self.view.addSubview(view2)
        
    }
}

アプリ起動時にまず呼ばれるのは、38行目からのviewDidLoadである。
ここでは背景色が赤と青のビューを一つずつ作成し、親にサブビューとして追加している。

これらのビューでタッチイベントを受けられるようにするため、UIGestureRecognizerの一種であるUIPanGestureRecognizerをサブビューに追加する。これはパンジェスチャ、すなわちタッチ後にドラッグするような操作を認識するためのRecognizerである。
ここで注意が必要なのはGestureRecognizerのインスタンスは複数のUIViewで使い回せない、ということである。下のようについ書きたくなるのだが、このように書くとタッチが有効になるのは最後に追加したViewの方のみとなってしまう。

(GestureRecognizer追加時の失敗例)

let recognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture))
let view1 = UIView()
view1.addGestureRecognizer(recognizer)

// こちらのViewだけがタッチを受け取る
let view2 = UIView()
view2.addGestureRecognizer(recognizer)

タッチ時の処理を扱うのは20行目からのhandlePanGestureメソッドである。ここで行っているのは以下の処理である。

  • タッチ開始時:タッチされたビューの位置(origin)と親ビュー上のタッチ位置を記録しておく。
  • タッチ移動時:タッチ開始時点のビューの位置(origin)に、タッチ開始点から現在のタッチまでの移動量を足してビューの新しい位置(origin)とする。

タッチの移動量は
「現在の親ビューにおけるタッチ位置」-「タッチ開始時点の親ビューにおける位置」
で求めることができる。サブビューの位置はタッチの移動によってどんどん変わっていくから、計算には親ビュー上のタッチ位置の値を使うことがポイントである。

31行目でCGPointに対して+や-の演算子を使っている。本来はCGPointにこのような演算子はないのだが、4行目、6行目でユーティリティメソッドを用意しているのでこのように書くことができる。これは、ソースコードを見やすくするための工夫である。もし、普通に書くならば以下のようになるだろう。

sender.view?.frame.origin = 
    CGPoint(x:(orgOrigin.x + newParentPoint.x - orgParentPoint.x,
            y:(orgOrigin.y + newParentPoint.y - orgParentPoint.y ))

CGPointのユーティリティメソッドについて、さらに詳しく知りたいときは以下の記事が参考になる。
http://qiita.com/koher/items/7b4f23d749d973d409c9

さて、今回の内容は以前にAndroidでもやったことがある。Androidでの実装に興味がある方は以下の記事も参考にしてほしい。
https://zawapro.com/?p=305


【iOS】PlaygroundでUIViewを表示する

使用環境
Xcode:8.0
Swift:3.0

XcodeにはPlaygoundという機能が搭載されている。
iOSアプリ開発の場合、エミュレータや実機を立ち上げることなく書いたコードの実行結果がすぐに表示されるので、便利である。そこで、今回はUIViewをPlaygroundで表示させてみよう。

準備

Xcodeを起動したら File -> New -> Playground を選択し、新規Playgroundを作成する。

実装

作成されたPlaygroundに以下のように入力する。
とりあえず、UIViewを表示させたいので以下のようにした。

MyPlayground.playground

import UIKit
import PlaygroundSupport

// UIViewを生成する。大きさはPlaygroundなので適当
let view = UIView(frame: CGRect(x:0, y:0, width:360, height:480))
// 背景に薄灰色を表示
view.backgroundColor = UIColor.lightGray
// ライブビューにセット
PlaygroundPage.current.liveView = view

2行目 PlaygroundSupportというフレームワークをインポートしている。これは、9行目に出てくるPlaygroundPageを使うためのもので、かつてはXCPlaygroundという名前だった。
4-7行目 UIViewを生成する。大きさは適当な値を与えている。プロトタイピングの段階ではこれでいいだろう。また、実行結果を分かりやすくするため、背景色には薄い灰色を指定した。
9行目 UIViewを表示させるためのコードである。直前で作ったUIViewをliveViewプロパティにセットする。

ここまで入力できたら、アシスタントビューを表示させてみよう。アシスタントビューは右上の丸が二つ重なっているアイコンをクリックすると表示できる。
assistantview

うまくいくと、アシスタントビューにUIViewが表示される。
playgdound


【Android】Fragmentを使う(1) レイアウトに直接配置する

Fragmentを効果的に使うことで、画面構成が柔軟になり、UI部分のコードの再利用がしやすくなる。
しかし、使い方を理解するには少し複雑である。
そこで、Fragmentの使い方について、小さなところから順に見ていくことにする。
今回紹介するのは「Fragmentをレイアウトにタグで直接配置する方法」である。

まずはAndroidStudioで新しいプロジェクトを作る。
プロジェクトのテンプレートは、「Empty Activity」とする。
プロジェクトが作成されたら、以下の手順で進めていく。

1.FragmentのためのUIレイアウトを作る
まずは、FragmentのためのUIレイアウトを作る。
my_fragment.xmlという名前でres/layout下に保存し、内容は適当に以下のようにした。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ea6c6c">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="This is my fragment!"
        android:id="@+id/textView"
        android:layout_alignParentTop="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="New Button"
        android:id="@+id/button"
        android:layout_below="@+id/textView"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true" />
</RelativeLayout>

テキストとボタンを配置しただけの単純なレイアウトである。
配置された場所を分かりやすくするため、全体の背景色を赤系にしてある。

2.Fragmentを継承したクラスを作る
次は、Fragmentのためのソースを作成する。
ソースコードは以下のようになる。

import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class MyFragment extends Fragment {
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater,
                             @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        // 引数で渡ってきたinflaterを使ってレイアウトをinfrateする
        final View layout = inflater.inflate(R.layout.my_fragment, container);

        // 必要に応じてUIにコールバックを設定する
        layout.findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(getContext(), "Hello Fragment!!", Toast.LENGTH_SHORT).show();
            }
        });

        // infrateされたViewをそのまま返す
        return layout;
    }
}

注意が必要なのは、android.app.Fragmentではなく、android.support.v4.app.Fragmentをインポートしているところである。(3行目)
これは、サポートライブラリに含まれるFragmentである。
あえてこちらのパッケージを使う理由は、Androidのバージョンが低い端末向けにアプリを配信する場合でも、最新の機能が反映されたFragmentを使えるからである。
apkのサイズが大きくなってしまうのとトレードオフだが、特に理由が無ければサポートライブラリ版を使ったほうが良いと思う。

さて、実装については今回は必要最低限とするため、onCreateView()のオーバーライドのみとした。
引数で渡ってくるinfratorを使ってレイアウトをinfrateしてやり、必要に応じてUIにコールバックをセットした後、infrateされたViewを返すだけである。
以上がFragmentの作成である。

3.メインのレイアウトにFragmentを配置する
Activityが読み込むためのレイアウトファイルに、fragmentタグを使って直接配置する。
ソースは以下のようになる。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.zawapro.fragmentlesson.MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        android:id="@+id/textView2" />

    <fragment
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:name="com.zawapro.fragmentlesson.MyFragment"
        android:id="@+id/fragment"
        android:layout_below="@+id/textView2"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"
        android:layout_alignParentEnd="true" />
</RelativeLayout>

ハイライトした18行目がFragmentを配置している個所である。
このように、普通のViewを配置する方法と何ら変わりは無いことが分かる。

4.Activityのコード
Activityのコードは以下のようになる。今回はAndroidStudioが自動生成したものに手を加えていない。
サポートライブラリを使うため、AppCompatActivity となっている点に気を付けておけば良いだろう。

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

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

以上でアプリは完成である。
実行すると以下のようになる。
device-2016-04-21-231706

ところで、配置したFragmentはどのようにViewに置き換わっているのだろうか?
Viewのヒエラルキーを見ると以下のようになっている。
hierarchy
activity_main.xmlで宣言したfragmentの部分が、そのままmy_fragment.xmlの内容に置き換わっているのが分かる。
まずは「FragmentとはViewのまとまりをコンポーネント化する仕組みである」ということをおさえておくと良いと思う。

さて、とは言うものの、実はレイアウトファイルにタグで直接Fragmentを配置する方法は固定的であまり融通が利かないのである。
例えば、画面上のFragmentを動的に入れ替えたり、バックスタックに乗せたり、ということをするにはそのためのコードを書いていく必要がある。
そういった意味では今回紹介した方法ではあまりFragmentにする恩恵を受けているとは言えず、
実際ほとんど使われていないのではないか、という気がする。
しかし、入門編としてはこのくらいの内容が良いだろう。


【Android】ndk-buildの実行時に情報を出力する

NDKのソースをndk-buildコマンドでビルドをするとき、Android.mkに書いた内容が間違っていてビルドが上手くいかないことがある。
こうしたときはエラーメッセージを調べつつ原因を探していくことになるのだが、ビルドがどこまで進んだか、とかビルド中に使われた変数の値がどうなっているか、などが分かれば間違った原因を探るための手掛かりになる。
そうした用途に便利なのが$(error), $(warning), $(info)である。これらは元々Makefileの関数だが、Android.mkでもそのまま使える。実際には以下のようにして使う。

$(error エラー時のメッセージ)
$(warning ワーニング時のメッセージ)
$(info メッセージ)

早速試してみよう。
まずは適当なフォルダを作り、以下のような構成にする。

{project_folder}
└── jni
    └── Android.mk

jniフォルダはNDKのデフォルトのソースフォルダで、本来はこの下にc/c++で書いたソースコードを置いていく。
このフォルダが無いと、ndk-buildを実行したときに「 Could not find application project directory !」というエラーとなってしまうので、とりあえず今は空のフォルダを作っておく。

Android.mkには以下の内容を記述する。

HOGE := this is hoge!

$(info This is info mesage! HOGE:$(HOGE))
$(warning This is warning mesage!)
$(error This is error mesage!)

上記では$(info)の例として、変数HOGEにセットされた値を表示している。
ファイルを保存したら、プロジェクトのルートフォルダでndk-buildコマンドを実行する。
結果は以下のようになる。

$ pwd
{project_folder}
$ ndk-build
This is info mesage! HOGE:this is hoge!
jni/Android.mk:4: This is warning mesage!
jni/Android.mk:5: *** This is error mesage!.  中止.

この実行結果から以下のことが分かる。
・infoは単にメッセージを出力する。
・warningはメッセージに加え、ファイル名と行数を表示する。
・errorはメッセージに加え、ファイル名と行数を表示するとともに、その個所で停止する。

これらの関数を効果的に使うことで、不具合が起きている個所を特定するのが容易になるだろう。


【Android】ScaleGestureDetectorを使う

ピンチインとピンチアウトによって画像を拡大縮小させたいときは
ScaleGestureDetectorを使うと便利である。
今回はこのクラスの使い方を紹介する。

始めに、サンプルアプリの画面を示す。

device-1
device-2

画面の中央に青い四角が表示されている。これを、ピンチインとピンチアウトで拡大縮小できるようにする。

それでは、ソースコードを示す。
以下は、今回の処理を行うために作ったカスタムビューのソースコードである。
このビューの処理概要は以下の通り。
・タッチイベントを受け、それをScaleGestureDetectorに渡す
・ピンチインとピンチインの動作に応じて倍率を決定し、画面の中央に正方形を表示する
MyView.java

public class MyView extends View {
    private static final int RECT_SIZE = 64;

    private float mScale = 1.0f; // 描画する倍率
    private ScaleGestureDetector mScaleDetector;

    private Paint mRectPaint = new Paint();
    private Paint mTextPaint = new Paint();
    {
        mRectPaint.setStyle(Paint.Style.STROKE);
        mRectPaint.setColor(Color.BLUE);
        mRectPaint.setStrokeWidth(2.0f);

        mTextPaint.setTextSize(64.0f);
    }

    public MyView(Context context) {
        super(context);

        mScaleDetector = new ScaleGestureDetector(context, 
                new ScaleGestureDetector.OnScaleGestureListener() {
            @Override
            public boolean onScale(ScaleGestureDetector detector) {
                // ピンチイン・アウト中に継続して呼び出される
                // getScaleFactor()は
                // 『今回の2点タッチの距離/前回の2点タッチの距離』を返す
                Log.d("MyView", "MyView.onScale factor:" +
                 detector.getScaleFactor());

                // 表示倍率の計算
                mScale *= detector.getScaleFactor();
                invalidate();
                return true;
            }

            @Override
            public boolean onScaleBegin(ScaleGestureDetector detector) {
                Log.d("MyView", "MyView.onScaleBegin");
                return true;
            }

            @Override
            public void onScaleEnd(ScaleGestureDetector detector) {
                Log.d("MyView", "MyView.onScaleEnd");
            }
        });
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 受けたMotionEventを
        // そのままScaleGestureDetector#onTouchEvent()に渡す
        return mScaleDetector.onTouchEvent(event);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // 現在の表示倍率を表示
        canvas.drawText("Scale:" + mScale, 0, 72, mTextPaint);

        // 正方形の1辺の長さを求める
        float rectSize = RECT_SIZE * mScale;

        // 描画位置をViewの中心にする
        float left = getWidth() / 2 - rectSize / 2;
        float top = getHeight() / 2 - rectSize / 2;
        float bottom = top + rectSize;
        float right = left + rectSize;

        // 正方形の描画
        canvas.drawRect(left, top, right, bottom, mRectPaint);
    }
}

20行目 コンストラクタでScaleGestureDetectorのインスタンスを作る。
第2引数でOnScaleGestureListenerを継承したクラスを渡している。
このクラスのオーバーライドメソッドであるonScale(),onScaleBegin(),onScaleEnd()に独自の実装を書くことによって、ピンチイン・ピンチアウトが行われたときの挙動を制御することができる。

23行目 OnScaleGestureListener#onScale()はピンチイン・アウト動作が行われると、継続して呼び出される。
 getScaleFactor()は前回のonScale呼び出し時と今回のonScale呼び出し時のタッチの距離の比率である。
 scaleFactor > 1.0 ならば拡大、scaleFactor < 1.0 ならば縮小となる。
 この比率を描画倍率に掛け、画面を更新する。

57行目~ 描画処理となる。
 Viewの中心に現在の倍率で正方形を描画している。

以上でピンチイン・アウトに対応して描画の拡大縮小を行う処理ができた。
最後に、参考のため上記のカスタムビューをActivityで使うときのコード例を示す。
Activity.java

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new MyView(this));
    }

  ~ 略 ~
}

【シェル】パス文字列からディレクトリ部、ファイル名を取得する

シェルスクリプトを書いていて、
・パス文字列からディレクトリ部分だけを取得したい
・パス文字列からファイル名だけを取得したい
といったことは良くある。その方法を見ていこう。

パス文字列からディレクトリ部を取得する

・方法1 dirnameコマンドを使う

STR=/home/zawata/filename
echo $(dirname ${STR})

結果:/home/zawata

dirnameコマンドを使い、渡された文字列のディレクトリ部を取得するやり方である。

・方法2 パラメータ展開を使う

STR=/home/zawata/filename
echo ${STR%/*}

結果:/home/zawata

${parameter%word}と書くと、指定されたパラメータから
『wordのパターンに最短で後方一致する部分』を削除した値を得ることができる。
ここではwordに「/*」を指定しているので、パス文字列のうち、ファイル名部分がマッチする。

/home/zawata/filename ←赤文字部分がマッチし、削除される。

このようにすることで、ディレクトリ部を取得できる。

パス文字列からファイル名を取得する

・方法1 basenameコマンドを使う

STR=/home/zawata/filename
echo $(basename ${STR})

結果:filename
basenameコマンドを使い、渡された文字列のファイル名を取得できる。

・方法2 パラメータ展開を使う

STR=/home/zawata/filename
echo ${STR##*/}

結果:filename

${parameter##word}と書くと、指定されたパラメータから
『wordのパターンに最長で前方一致する部分』を削除した値を得ることができる。
ここではwordに「*/」を指定しているので、パス文字列のうち、ディレクトリ部分がマッチする。

/home/zawata/filename ←赤文字部分がマッチし、削除される。

このようにすることで、ファイル名を取得できる。

まとめ

以上をまとめたスクリプトを示す。

#!/bin/bash

STR="/home/zawata/filename"

# パス文字列からディレクトリ部を取得する
# 方法1. dirnameコマンドを使う
echo $(dirname ${STR})

# 方法2. パラメータ展開を使う
echo ${STR%/*}


# パス文字列からファイル名を取得する
# 方法1. basenameコマンドを使う
echo $(basename ${STR})

# 方法2. パラメータ展開を使う
echo ${STR##*/}

結果:
/home/zawata
/home/zawata
filename
filename

補足 パラメータ展開の覚え方

パラメータ展開の書き方は、記号の種類と数が色々とあってややこしい。
今回出てきたパラメータ展開の書き方に限っては、こんな風に覚えておくといいかもしれない。
・${parameter%word}の覚え方
パーセント記号はキーボード上で見ると「右側」にある。1つだけなので「短い」。
だから、「右側から最短で一致したパターン」を削除する。

・${parameter##word}の覚え方
シャープ記号はキーボード上で見ると「左側」にある。2つあるので「長い」。
だから、「左側から最長で一致したパターン」を削除する。