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

生年月日の入力で、絶対に使っていけないデザイン。カレンダー型のDatePickerについて

androidの生年月日の入力

androidの生年月日の入力フォームのデザインは、主に二つあります。

  • カレンダー型
  • ドラムロール型

スクリーンショット 2020-02-18 20.33.20.png

生年月日の入力で、左のようなカレンダー型の生年月日の入力フォームは絶対に使ってはいけません。
なぜだかわかりますか?

デザイナーや、アプリ開発をしてきた人は、一瞬でわかると思いますが。。

わからない人は、こちらの記事を見てください。

https://satoriku.com/android-uidesign/

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

コーディング中の警告解消時の注意点

はじめに

みなさん!コーディング時の警告はちゃんと解消してますよね?

ただ、「はいはいいつものやついつものやつ」とか「またこれねハイハイ」とか容易にソースを修正してませんか?

特にテストやらなんやら終わった後のクラスとかで「あれ?警告でてるじゃん、消しとこ」とかで軽い気持ちで修正しちゃったりとかありませんか?

その修正が思わぬ不具合につながるかもしれませんよ?

まえおき

理解してる人はあたりまえの話なのでそこはあたたかい目で読んでいただければと思います

例を作るにあたり、いろいろ突っ込みどころがあると思いますがご容赦を・・・!

ほんだい

極端な例ですが、例えばこんなclassがあったとします。

public class TestClass {

    TestClass() {

        hogeInt hogeInt = new hogeInt();

        ...

        if (特定の条件) {

            Log.d("TAG", "前");

            Boolean flg = hogeInt.getIntParam().equals(1);

            Log.d("TAG", "後");

            Toast.makeText(context,"サーバーに truncate table * のクエリを投げました",Toast.LENGTH_LONG).show();
        }

        ...

    }

}

class hogeInt {

    private Integer intParam = null;

    @Nullable
    public Integer getIntParam() {
        return this.intParam;
    }

    public void setIntParam(Integer intParam) {
        this.intParam = intParam;
    }
}

そして特定の条件内に入るのが極稀なケースだったとします。

ここでひとつPointなのですが、これを何もせずに実行した場合、

Toast.makeText(context,"サーバーに truncate table * のクエリを投げました",Toast.LENGTH_LONG).show();

実行される前にクラッシュします。
( hogeInt.getIntParam() はNull なのに equals を実行しようとしているので)

そして本題。

Boolean flg = hogeInt.getIntParam().equals(1);

こいつは警告として表示されます。
今回だと「’NullPointerException’を生成する可能性があります」ってやつですね。わりかしよく見かける部類の警告だと思います。

これをアプリ改良中に「あれ?ここ警告でてるからついでに修正しておこう」とか「そういえばたまにクラッシュ報告あるけどこいつかも」
といった軽い感じで何も考えずにソースを修正した場合

ソースがこう変化します

public class TestClass {

    TestClass() {

        hogeInt hogeInt = new hogeInt();

        ...

        if (特定の条件) {

            Log.d("TAG", "前");

            if (hogeInt != null) {
                Boolean flg = hogeInt.getIntParam().equals(1);
            }

            Log.d("TAG", "後");

            Toast.makeText(context,"サーバーに truncate table * のクエリを投げました",Toast.LENGTH_LONG).show();
        }

        ...

    }

}

今までクラッシュしていた部分をnullチェックでスルーする事によって後続が流れるようになり、めでたくサーバにTruncate文が投げられる事になりました。

ちなみに今回のお話は警告は残しておいてもいいよ!という事ではなく、小さな警告を消す場合でも慎重になれという事です。

何かしら修正を入れる場合、その一文だけを見ずに、全体の流れをちゃんと把握した上で修正しましょう!

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

何もしてないのに READ_PHONE_STATE が付与されてしまった!(解決)

まあ何もしてない訳は無いんですよね。
いくつかライブラリをバージョンアップしていました。
1行ずつ戻しながら検証しました。

build.gradle
implementation 'com.android.installreferrer:installreferrer:1.0'
↓
implementation 'com.android.installreferrer:installreferrer:1.1.1'

これが原因でした。

https://blog.danlew.net/2020/02/11/install-referrer-library-permissions-snafu/

こちらで詳細を見つけました。
とりあえず 1.0 に戻して解決!

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

Androidの戻るキー対応をなるべく簡単にする提案

意外と面倒くさいAndroid戻るキー対応

ね。面倒くさいですよね。(iOSには無いわけですし・・・)
さしあたり一番簡単なのは「画面上の『ネガティブ的』なボタンと同等の機能をつける」事です。

ダイアログを開いたのであれば、戻るキーが「閉じるボタン」や「戻るボタン」相当になればよいですし。
image.png

画面遷移したのであれば、戻るキーが「Backボタン」(前の画面に戻る)相当になればよいわけです。
image.png

そう書くと簡単のように聞こえますが、
画面遷移した次の画面で、ダイアログを表示した場合は?
image.png

戻るキーを押したら、ダイアログも閉じてしまう+画面も前の画面に戻ってしまう

では困るわけです。

これをまともに対応しようとすると、

  • 優先順位スタックマネージャ的なクラスを作成
  • 画面遷移したら、「戻るボタン」の処理(Actionとか?)を(上記)スタックマネージャにPush
  • ダイアログを開いたら「ダイアログ閉じるボタン」の処理(Actionとか?)をスタックマネージャにPush
  • 戻るキーを押したら、スタックマネージャにスタックされている処理の一番上(Peek)を処理
  • ダイアログを閉じたら「ダイアログ閉じるボタン」の処理をスタックマネージャからRemove
  • もう一度戻るキーを押したら・・・・

といった、管理が必要になります。 はい面倒臭いですね!

もっとシンプルに考える

そもそも、上記例の「画面遷移した次の画面で、ダイアログを表示した場合」って、普通はダイアログがモーダル的に表示されていて、後ろの「戻るボタン」は押せないようにしているのがほとんどのはず。(わざわざ後ろのボタンのintaractiveをfalseにしているのか、「タッチガード」的な全画面Panelを一枚噛ませてタッチイベントを遮断しているかのどちらかがほとんどでしょう)

問題なのは、

単純に戻るキーとボタンが押されたときの処理を関連付けてしまうと、uguiのイベントとは関係無しに処理が呼ばれてしまう

ことです。

なので、徹底的にuguiのイベントを倣い、戻るキーの押下を指定ボタンへのマウスクリックへとすり替えてあげれば解決です。

作ってみた

KeyBind.cs
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

[RequireComponent(typeof(Button))]
public class KeyBind : MonoBehaviour
{
    [SerializeField]
    private Button _targetButton;

    public KeyCode _bindKey;

    private void Reset()
    {
        _targetButton = GetComponent<Button>();
    }

    private static List<RaycastResult> raycastResultList = new List<RaycastResult>();
    private PointerEventData _pointerEventData;

    private void Update()
    {
        //指定したキーの押下
        if (Input.GetKeyDown(_bindKey))
        {
            _pointerEventData = new PointerEventData(EventSystem.current)
            {
                button = PointerEventData.InputButton.Left,
                position = _targetButton.transform.position //指定したボタンの位置にマウスがある体
            };
            EventSystem.current.RaycastAll(_pointerEventData , raycastResultList);
            var validGameObject = raycastResultList.Select(result => result.gameObject).FirstOrDefault(gameObject => gameObject != null);//一番最初にぶつかっている有効なGameObject取得
            raycastResultList.Clear();
            if (validGameObject == null)
            {
                return;
            }
            var currentPointerDownHandlerObject = ExecuteEvents.GetEventHandler<IPointerDownHandler>(validGameObject); //ボタン位置にあるGameObjectからIPointerDownHandlerを保持しているGameObjectを取得
            if (currentPointerDownHandlerObject != _targetButton.gameObject){
                return;    //ボタン位置から得られたGameObjectとボタンのGameObjectが異なる=別のもので遮られている ので処理しない
            }

            _pointerEventData.pointerPress = currentPointerDownHandlerObject;
            ExecuteEvents.Execute(currentPointerDownHandlerObject, _pointerEventData, ExecuteEvents.pointerDownHandler);
        }

        //指定したキーの押上
        if (_pointerEventData != null && _pointerEventData.pointerPress != null && Input.GetKeyUp(_bindKey))
        {
            ExecuteEvents.Execute(_pointerEventData.pointerPress, _pointerEventData, ExecuteEvents.pointerUpHandler);
            ExecuteEvents.Execute(_pointerEventData.pointerPress, _pointerEventData, ExecuteEvents.pointerClickHandler);
            _pointerEventData = null;
        }
    }
}

(よくわからんなりに調べて作ったので、大分力業ですが・・・)

使い方

このScriptをButtonコンポーネントが乗っているGameObjectに追加します。
image.png

Target Button は勝手に同GameObjectButtonがセットされます。
そして
Bind Key には割り当てたいハードキー を指定します(KeyCode の一覧が候補で出ます)
Androidの戻るキーは KeyCode.Escape で割り当たります。
image.png

なんと、これだけで、ボタンのタップとAndroidの戻るキーが同等になります! シンプル!!

注意

ボタンを疑似的にクリックした相当なので、(利点でも欠点でもあるんですが)ボタンのTransitionがそのまま効きます。
↑の動画をよく見ると分かるんですが、戻るキーを押した時でもボタンの色が変化しています(戻るキーを押しっぱなしにすると、ボタンも押されっぱなしになる)
それが嫌! という場合には使えないです。 悪しからず・・・。

補足

今回、 Androidの戻るキー対応 と銘打っては居ますが、既に書いた通りボタンには KeyCodeで割り当てるキーを指定することができます。
もう一つの使い道として、入力処理の一元化があります。

こちらの動画のゲームで今回のKeyBind.csが実際に使われており、前半はマウスでボタンをクリックして操作ですが、後半はそれぞれのボタンに割り当てられたキーボードで操作しています。

このように「複数方法の入力処理を制御」するには

  • ベタでボタンがクリックされた場合の処理とキーボード入力の処理の場合を分けて書いてしまうスタイル
  • IInput のような入力処理を抽象化したインタフェースを切り、IInput を実装した KeyboardInputButtonInput のようなクラスをそれぞれ実装するスタイル
  • 神Inputクラスに想定される全インプット処理分の条件分岐をぶち込んでいくGODスタイル

などなど。方法はありますがそれなりに面倒で。

対して、↑の動画のゲームでは入力制御は「ボタン処理」のみ対象に記述しています。
そして、KeyBind.csはあくまでもボタンの疑似クリック処理なので、キーボード操作を増やしても入力制御処理は何も手を入れずに済んでいます。 つまり、ボタンによる入力処理で「処理の一元化」がされている状態です。

もちろん、これは画面上にバーチャルパッド的なものをuguiで置いているから出来るだけなので適用範囲はそう広くは無いですが、使える人も少なくないのではないでしょうか。

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

jetpack navigationを使ってViewPager2上のFragmentから別Fragmentに画面遷移した後、navigateUpをするとStateAdapterがあれで落ちる話

どういうこと

まずはこれを見てください。
自作Connpassのイベント一覧表示するやつ
お勉強も兼ねて、ちくちく作ってるアプリです。

このアプリのメイン画面は一覧表示のFragmentを2つもっており、それらをViewPager2で保持しています。
(ちなみにFragmentStateAdapterはdaggerでInjectしています)
一覧表示のアイテムをクリックして詳細画面へ遷移する腹積もりです。
そして、詳細画面でfindnavcontroller().navigateUp()をしたときに掲題の件にぶつかります。


構成イメージ

App
    ├ MainFragment
    │   ├ List1Fragment
    │   └ List2Fragment
    │
    └ DetailFragment

どんなエラーがでるのかその①

単純に上記のプログラムを実行して、一覧画面->詳細画面->戻るボタンを実行すると下記クラッシュを起こします。

2020-02-18 00:37:34.858 4022-4022/com.ryunen344.connpasssearch.debug E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.ryunen344.connpasssearch.debug, PID: 4022
    java.lang.IllegalArgumentException
        at androidx.core.util.Preconditions.checkArgument(Preconditions.java:36)
        at androidx.viewpager2.adapter.FragmentStateAdapter.onAttachedToRecyclerView(FragmentStateAdapter.java:132)
        at com.ryunen344.connpasssearch.feature.main.MainFragmentStateAdapter.onAttachedToRecyclerView(MainFragmentStateAdapter.kt:32)
        at androidx.recyclerview.widget.RecyclerView.setAdapterInternal(RecyclerView.java:1209)
        at androidx.recyclerview.widget.RecyclerView.setAdapter(RecyclerView.java:1161)
        at androidx.viewpager2.widget.ViewPager2.setAdapter(ViewPager2.java:461)
        at com.ryunen344.connpasssearch.feature.main.MainFragment.initViewPager(MainFragment.kt:43)
        at com.ryunen344.connpasssearch.feature.main.MainFragment.onViewCreated(MainFragment.kt:35)
        at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:322)
        at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1185)
        at androidx.fragment.app.FragmentManager.addAddedFragments(FragmentManager.java:2222)
        at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:1995)
        at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1951)
        at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1847)
        at androidx.fragment.app.FragmentManager$4.run(FragmentManager.java:413)
        at android.os.Handler.handleCallback(Handler.java:883)
        at android.os.Handler.dispatchMessage(Handler.java:100)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7356)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)

FragmentStateAdapter#onAttachedToRecyclerViewで落ちていますね。
何をしているかというと

  // FragmentStateAdapter.java
    @CallSuper
    @Override
    public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
        checkArgument(mFragmentMaxLifecycleEnforcer == null);
        mFragmentMaxLifecycleEnforcer = new FragmentMaxLifecycleEnforcer();
        mFragmentMaxLifecycleEnforcer.register(recyclerView);
    }


    // Preconditions.java
    public static void checkArgument(boolean expression) {
        if (!expression) {
            throw new IllegalArgumentException();
        }
    }

こうなっています。
mFragmentMaxLifecycleEnforcer がMainFragmentとFragmentStateAdapterのLifecycleを紐づけていて、
一覧から詳細画面に遷移した際に一覧のFragmentは破棄されないため、mFragmentMaxLifecycleEnforcerはnon nullとなっています。
落ちます。

どんなエラーがでるのかその②

自分はFragmentStateAdapterのonDetachedFromRecyclerViewが呼ばれていないから起こるのではと予想し、下記コードをMainFragment.ktに追加しました。
Fragment#onDestroyViewのタイミングでViewPager2へのadapterの参照を外してあげています。

MainFragment.kt
    override fun onDestroyView() {
        binding.viewPagerContainer.adapter = null
        super.onDestroyView()
    }

するとこうなる。

2020-02-18 00:59:17.535 4455-4455/com.ryunen344.connpasssearch.debug E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.ryunen344.connpasssearch.debug, PID: 4455
    java.lang.IllegalStateException: Expected the adapter to be 'fresh' while restoring state.
        at androidx.viewpager2.adapter.FragmentStateAdapter.restoreState(FragmentStateAdapter.java:536)
        at androidx.viewpager2.widget.ViewPager2.restorePendingState(ViewPager2.java:350)
        at androidx.viewpager2.widget.ViewPager2.dispatchRestoreInstanceState(ViewPager2.java:375)
        at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3864)
        at android.view.View.restoreHierarchyState(View.java:19793)
        at androidx.fragment.app.Fragment.restoreViewState(Fragment.java:573)
        at androidx.fragment.app.FragmentStateManager.restoreViewState(FragmentStateManager.java:346)
        at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1187)
        at androidx.fragment.app.FragmentManager.addAddedFragments(FragmentManager.java:2222)
        at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:1995)
        at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1951)
        at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1847)
        at androidx.fragment.app.FragmentManager$4.run(FragmentManager.java:413)
        at android.os.Handler.handleCallback(Handler.java:883)
        at android.os.Handler.dispatchMessage(Handler.java:100)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7356)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)

どういうこっちゃ:thinking: :thinking: :thinking: :thinking: :thinking:

Expected the adapter to be 'fresh' while restoring state
ふーむ、Adapterが再生成されてないことに怒っているのかな・・・?

それもそのはず。
一覧画面 -> 詳細画面への遷移を行ってもMainFragmentは破棄されていないので当然FragmentStateAdapterも破棄されていない。
だが、onCreateViewは走るのでViewPagerとViewPager内のRecyclerViewは再生成されている。
再生成されたRecyclerViewに対して前の状態を保持しているFragmentStateAdapterをrestore, attachしようとすると参照が噛み合わないので落ちてる(と思います)。

あれ!?FragmentStateAdaterって再生成されてないの!?!?

MainFragmentは破棄されていないので当然FragmentStateAdapterも破棄されていない。
先述のこれ、後になって気付きました。じゃあ毎回FragmentStateAdapterのコンストラクタを走らせるしかあるまい・・・・と思い、DaggerさんにSpring Frameworkでいう @Scope("prototype")のやつちょーだいって書くことにしました。
(別にDIする必要ないんですが、できるものならなんでもDIしたいお年頃なので・・・・)

MainFragmentModule.kt
// includesにProvidersを追加
@Module(includes = [MainFragmentModule.Providers::class])
abstract class MainFragmentModule {

    @FragmentScope
    @ContributesAndroidInjector(modules = [EventListFragmentModule::class])
    abstract fun contributeEventListFragment(): EventListFragment

    @FragmentScope
    @ContributesAndroidInjector
    abstract fun contributeSearchFragment(): SearchFragment

  // includesにProvidersを追加
    @Module
    internal object Providers {
        @Provides
        internal fun provideMainFragmentStateAdapter(mainFragment: MainFragment) =
            MainFragmentStateAdapter(mainFragment)
    }
}

書いて安心してはいけません。DaggerはComponentの死活が再生成と同義です。
つまりこのスコープではFragmentStateAdapterは死なないので再生成されないのです。

えっ、だからSpring Frameworkでいう @Scope("prototype")はどうやるんだってばよ:thinking:

FragmentStateAdapterのobjectをinjectするのではなくMainFragmentModule.Providersをinjectすることによって実現しました。
つまりこうなります。

MainFragment.kt
    // objectをinjectしていた部分を下記に書き換える
    @Inject
    lateinit var mainFragmentStateAdapterProvider: Provider<MainFragmentStateAdapter>

   ~ ~ ~ ~ ~
  // binding.viewPagerContainer.adapter = mainFragmentStateAdapter
   // これを
   binding.viewPagerContainer.adapter = mainFragmentStateAdapterProvider.get()
   // こうする

こうすることでProviderが毎回FragmentStateAdapterのinstanceを生成してくれます。
※onDestroyView()でviewpager.adapter = nullするといいと思います。

終わりに

daggerは俺に微笑んでくれない、誰も俺を愛してくれない・・・・・・
koinは俺を愛してくれるだろうか・・・・・

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

[Android]Screen に Viewが表示されたか判定する

はじめに

View が Screen に表示されたか判定する処理を作成します。例えば Player が Screen に表示されたら動画の再生を開始したい、TextView が表示されたらテキストカラーを変えたい、といった処理に使えるサンプルを作成してみたいと思います。

Feb-18-2020 00-47-23.gif

TL;DR

表示されたか判定する方法はシンプルです、 Screen の Rectと View の Rect を取得して、 Screen の Rectに View のRect が含まれているか判定するだけです。

1. Screen の範囲(Rect)を取得する

MainActivity.kt
private fun getScreenRect() : Rect {
    val displayMetrics = DisplayMetrics()
    windowManager.defaultDisplay.getMetrics(displayMetrics)
    return Rect(0, 0, displayMetrics.widthPixels,displayMetrics.heightPixels)
}

2. View の範囲(Rect)を取得する

MainActivity.kt
private fun View.getViewRect() : Rect {
    val array = IntArray(2)
    this.getLocationOnScreen(array)
    return Rect(this.width, this.height, array[0], array[1])
}

3. Screen に View に表示されているか判定する

MainActivity.kt
  scroll_view.viewTreeObserver.addOnScrollChangedListener {
    val screenRect = getScreenRect()

    val oneRect = one.getViewRect()
    if (screenRect.contains(oneRect)) {
      // 含まれているときの処理
    }
    else {
      // 含まれていないときの処理
    }

レイアウトの準備する

判定処理を記述するためにスクロールしなければスクリーンに表示できないようなレイアウトを作成します。ScrollView の中に TextView が3つ含まれているレイアウトを準備しておきましょう。

Feb-17-2020 21-54-33.gif

activity_main.xml
<ScrollView android:id="@+id/scroll_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <LinearLayout
        android:id="@+id/linear_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <TextView
            android:id="@+id/one"
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:textAlignment="center"
            android:textSize="80sp"
            android:text="One" /></LinearLayout>
</ScrollView>

Screen の範囲(Rect)を取得する

Screen の位置(サイズ)は windowsManager.defaultDisplay.getMetrics()に取得できます。このgetMetricsで取得した幅と高さを利用して、Screen の範囲を示す Rect オブジェクトを作成しましょう。(ここで取得できるサイズは Pixel になります)

Screen no.png

MainActivity.kt
private fun getScreenRect() : Rect {
    val displayMetrics = DisplayMetrics()
    windowManager.defaultDisplay.getMetrics(displayMetrics)
    return Rect(0, 0, displayMetrics.widthPixels,displayMetrics.heightPixels)
}

View の範囲(Rect)を取得する

View の左上の座標をView.getLocationInWindowで取得できます。また View の高さと幅は View.widthView.heightで取得できます。これらの値を組み合わせて View が表示されている範囲を示す Rectオブジェクトを作成しましょう。(ここも取得できるサイズは Pixel になります)

Viewno.png

MainActivity.kt
private fun View.getViewRect() : Rect {
    val array = IntArray(2)
    this.getLocationOnScreen(array)
    return Rect(this.width, this.height, array[0], array[1])
}

Screen に View に表示されているか判定する

あとは ScrollView のスクロール位置が更新された時、Screen に View が表示されているか判定する処理を記述すれば完成となります。判定処理を記述する箇所は ScrollViewviewTreeObserveraddOnScrollChangedListenerになります。そこに Screen の範囲(Rect)に View の範囲(Rect)が含まれるか判定する処理を記述していきます。(含まれるか判定するには Rect の contains が便利です、含まているかそうでないかを Boolean で返すメソッドになります。)

Feb-18-2020 00-47-23.gif

MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val oneTextView = one
    val twoTextView = two
    val threeTextView = three

    scroll_view.viewTreeObserver.addOnScrollChangedListener {
        val screenRect = getScreenRect()

        val oneRect = one.getViewRect()
        val oneColor = if (screenRect.contains(oneRect)) {
            Color.RED
        } else {
            Color.WHITE
        }
        oneTextView.setBackgroundColor(oneColor)

        val twoRect = twoTextView.getViewRect()
        val twoColor = if (screenRect.contains(twoRect)) {
            Color.RED
        } else {
            Color.WHITE
        }
        twoTextView.setBackgroundColor(twoColor)

        val threeRect = threeTextView.getViewRect()
        val threeColor = if (screenRect.contains(threeRect)) {
            Color.RED
        } else {
            Color.WHITE
        }
        threeTextView.setBackgroundColor(threeColor)
    }
}

参考文献

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

「Swift」でマルチプラットフォーム開発をしよう!

前置き

この記事自体は2019年10月に書き終わっていたのですが、、、

DroidKaigi2020に応募したCFP
「Scadeを使って「Swift」で始めるAndroidアプリ開発」
が通ったため、だいぶ温めてからの公開になりました、、笑

残念ながら開催は見送られてしまいましたが、、、
開催週に登壇の前座資料として、元々この記事を公開予定だったので公開しています!
(登壇資料はまだ開催予定を信じて公開は致しませんmm)

内容

登壇内容ほど詳しくは載せていませんが、みなさんがSwiftを使ってScadeでアプリ開発できるように

- Scadeのライトな説明
- 環境設定方法
- 開発トラベルシューティング

周りを記載しています。

What is Scade ?

スクリーンショット 2020-02-17 23.26.10.png

SwiftをベースにiOS/Android向けのアプリを開発できる統合開発環境です。

どんな物が作れる?

DroidKaigi2020のコピーアプリをScadeを使って作りました。

74605697-c2a15880-510d-11ea-8edf-7c0883e3eaa2.png

といっても、検索フィルターなど一部の画面・機能は未実装です。
(単純に自分のコミット不足で実装しきれなかった)

また、リリースしたいと考えていたので(結局してないけど笑)
申請のリジェクトリスクを恐れてデザインは少し変更しています。
デザイン自体はDroidKaigi2020のアプリをベースにしています。

アプリのリポジトリを用意してあるので、詳しくはそのReadmeを見れいただけると!

(一部動作はぬるっとしてたりします。特にAndroidは、、泣)

開発環境構築

Scade公式のMacへのインストール方法

公式サイトにあるものは、英語なのと古いので、改めて以下の手順で行うと良いかと。

1. インストール

多いですが、iOS/Android双方へアプリをビルドする必要があるため、以下を全てインストールします。

複数のJREがインストールされている場合は、指定してあげるための対応が必要です。
(上記の公式サイトを参照してください)

2. Androidでビルドできるようにする

① Android Studioを起動

00.png

プロジェクトを開くか or 空のプロジェクトを作成してください

② 対応バージョンを入れる

11.png

[AndroidStudio] > [Preferences] > [Android SDK]
と進み、Android 6.0 にチェックを入れてインストールします。

③ 必要なSDKをインストールする

22.png

いくつかはデフォルトでインストールされているが、以下はないのでインストールします。
- Google Play services
- NDK

④ シュミレータのインストール

Android Studioをインストールしただけでは、何もシュミレータが入っていないので、自分で入れる必要があります。例では、Pixelを選択していますが、好きな端末を選んでインストールしていただければ。

a.png

3. iOSでビルドできるようにする

(こちらは後で行うことになるので一旦飛ばしても大丈夫です)

Xcode周りでは特に設定は必要ないです。

ただ、証明書がないとビルドできません。
(回避方法があるかも?現状見つけていない)

スクリーンショット 2020-01-13 19.02.50.png

Scadeのプロジェクト作成時にできるbuild.yamlに証明書を設定する部分があるので設定します。
(証明書ファイル名前は適当ですので、確実すきな名前にしてください)

スクリーンショット 2020-01-14 0.52.14.png

とりあえず動かしたいという方は、証明書を作成する際にApple Developerでアプリ名をワイルドカードにすると良いでしょう。

4. Scadeのセットアップ

1. パスの設定をする

Scadeの設定画面から以下を設定します。

scade.png

  • Android SDK はUsers配下にあるので、各自のパスを指定
  • Android NDK は最初にインストールしたものを任意の場所に設置して、その場所を各自が指定

Android SDKの場所はAndroid Studioの設定画面からパスを確認することができます。

スクリーンショット 2020-01-12 21.13.53.png

5. 「HelloWorld」する

XcodeのGUIのように開発が可能です!

①新規プロジェクトの作成

h-1 2.png

②ラベルの設置

h-3.png

③ビルドする端末を選択

h-4.png

④HelloWorld!

h-5.png

設定は少し面倒ですが、起動してしまえばすぐに開発を始めることができます。

※初回ビルドは失敗するので2回ビルドすること(端末が起動前だとエラーになる。特にAndroid)
※同じ端末をマルチビルドできないため、1端末ごとで行いましょう

その他

開発環境トラブルシューティング

実際に出くわして解決したものを載せておきます。

1. Androidでビルドできない時

DLツールの許可が必要な場合

インストールしたNDK内のコンパイラがビルド最中に弾かれてしまう場合があります。
(外部MacOS Catalinaだと発生する模様)

スクリーンショット 2020-01-12 22.12.10.png

その場合は一度キャンセルし、Mac本体のシステム環境設定 > セキュリティとプライバシーから許可を行ってください。
「ゴミ箱に入れる」は絶対にしない でください。やってしまった場合は、NDKを再インストールして入れ替えてください)

スクリーンショット 2020-01-12 22.12.18.png

ちなみに4つほど弾かれるので、許可→再ビルド というのを4回ほど行う必要があるので注意

サポートライブラリが読み込めない場合

普通にAndroidの開発をしていた場合でも発生するエラーですが、、
Androidでのビルドが止まってしまう

実際にScade上で発生するエラーは以下

スクリーンショット 2020-01-13 2.29.36.png

プロジェクトをクリーンをして、以下のandroidディレクトリを削除し
Library > Android > sdk > android

スクリーンショット 2020-01-13 3.33.10.png

Scadeでビルドし直すことで解決できます。
そうすることで、サポートライブラリが再インストールされます。
(ただし、数分時間がかかるので注意)

2.iOSでビルドできない時

Scadeのアップデートの影響

最新版のScadeにアップデートした際に、Xcodeを10系->11系にする必要があります。
(現状Scadev0.9.17以降はXcode11.x系でないとビルドできなくなった)

その際、プロジェクトファイルのbuild.yamlファイルが変わっているためにビルドできなくなります。
アップデートすべき項目をこちらです。

スクリーンショット 2020-01-13 22.30.22.png

新規プロジェクトを作成し、その中にあるyamlファイルを参考に古いプロジェクトにも項目を追加すると良いでしょう。

3.ビルドが通らない時

コンパイルが通らない

Xcode/Android Studio同様に、Scadeではビルド高速化のために、一度ビルドしたものをキャッシュしています。
が、その部分がたまに壊れてしまいビルドできなくなることがあります。

プロジェクトをクリーンし、吐き出されている[products]配下をすべて削除して、再びビルドすると通ることが多いです。

インストールされない・起動しない

Androidでよく起こりますが、端末によってはインストールできないアーキテクチャがあるようなので、
x86, arm64などいずれかでビルドできないか色々試してみましょう。

また、XcodeやAndroid Studioでもあるあるですが、再起動プロジェクトクリーンで起動することもあります笑

git管理

リポジトリが肥大化しないために、gitignoreに以下を追加します。
(書き出されたipa, apkが含まれないようにします)

.build/
.target/
products/
*.ipa
*.scadeapp
.DS_Store
CMakeLists.txt
fonts/

終わりに

タイトルにSwiftってあるのに記事に1文字もSwiftコードねえじゃねえか!

すみません、書き終わって気づきました笑
(登壇の前座資料なので、あまり触れるわけにもいかず、、)

実際にどんな感じに書いているのかは、作成したアプリのリポジトリをみていただくか、
公式サイトにあるサンプルコードリポジトリをみていただくのが良いかと!

iOSとAndroidで実装分けるために汚いコードも一部ありますが、、、
それも含めてご覧になっていただけると笑

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