i++

プログラム系のメモ書きなど

Android : WebView 覚え書き(ズーム関連の機能の有効化、Progress の表示、Back Key のハンドリングなど)

やりたい事 方法
ピンチ操作によるズームイン / ズームアウトの有効化 webview.settings.builtInZoomControls = true
ダブルタップによるズームイン / ズームアウトの有効化 webview.settings.useWideViewPort = true
ズームボタン(右下に表示される±のルーペ)の表示・非表示切り替え webview.settings.displayZoomControls = true/false (ズーム操作不要のページでは true でも表示されません)
プログレスの表示 webview.setWebChromeClient で onProgressChanged を override した WebChromeClient を設定(サンプルコード参照)
バックキーのハンドリング webview.setOnKeyListener で KeyEvent.BACK_KEY のイベントを処理する(サンプルコード参照)
load 時に外部ブラウザを起動しない webview.setWebViewClient(WebViewClient())

※ 方法の列は Kotlin ベースの Property Syntax を使って書いています。Java の場合 webview.getSettings().set***(Boolean) になります

サンプルコード

WebView を持っているレイアウトを使用する Fragment としての例です。
WebView 回りの設定は onCreateView で行っています。

※ こちらも Kotlin です

public class WebViewFragment : Fragment() {
    companion object {
        val ARG_KEY_URL = "ARG_KEY_URL"
        fun newInstance(url:String) : WebViewFragment {
            val fragment = WebViewFragment()
            val args = Bundle()
            args.putString(ARG_KEY_URL, url)
            fragment.arguments = args
            return fragment
        }
    }

    var url:String = ""

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if(arguments != null) {
            url = arguments.getString(ARG_KEY_URL) ?: ""
        }
    }

    override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val view = inflater?.inflate(R.layout.fragment_webview_root, container, false)

        val progressbar = view?.findViewById(R.id.fragment_webview_progressbar) as ProgressBar
        val webview = view?.findViewById(R.id.fragment_webview_webview) as WebView

        webview.settings?.builtInZoomControls = true;
        webview.settings?.useWideViewPort = true
        webview.settings?.displayZoomControls = false
        webview.settings?.javaScriptEnabled = true;

        webview.setWebChromeClient(object : WebChromeClient(){
            override fun onProgressChanged(view:WebView, progress:Int) {
                progressbar.progress = progress
                if(progress == 100) {
                    progressbar.animate().setDuration(1000).alpha(0.0f)
                } else {
                    progressbar.alpha = 1.0f
                }
            }
        })

        webview.isFocusableInTouchMode = true
        webview.setOnKeyListener { view, keyCode, keyEvent ->
            if(keyEvent.action == KeyEvent.ACTION_DOWN) {
                if (keyCode == KeyEvent.KEYCODE_BACK) {
                    if (webview.canGoBack() == true) {
                        webview.goBack()
                        return@setOnKeyListener true
                    }
                }
            }
            return@setOnKeyListener false
        }

        webview.setWebViewClient(WebViewClient())
        webview.loadUrl(url)

        return view
    }
}

参考

(本当に stackoverflow 様様だなぁ)

Android : SwipeRefreshLayout で ListView に EmptyView を設定すると ListView が空の時に Refresh 表示が上手くされない問題を解消する

ListView.setEmptyView で Empty View を設定していると、ListView が空の状態で SwipeRefresh (PullToRefresh) を実行すると Refresh の Indicator (上の方でくるくる回る、setRefreshing でコントロールされるもの)が、更新が終わった瞬間辺りに一瞬表示されるだけになるなど、不自然な見た目になってしまいました。

これはどうやら

  • SwipeRefreshView の Refresh 表示は ListView の Visibility に依存している
  • Empty View を設定していると、ListView は空になると自動で非表示にされる

という組み合わせで起こる挙動のようです。

この挙動を「Empty View をセットしても、空の状態で非表示にならない ListView」を作成・使用することで回避します。

カスタム ListView

カスタムといっても、setVisibility を override して Visibility の変更を防ぐ、ただそれだけです。

私の用途では ListView の Visibility を変える予定がない(常に Visible で問題ない)ので、全く何もしないようにしていますが、「空である / 空ではない」以外の要件で Visibility を自分でコントロールする必要がある場合は、参考先(下記記載)のように項目数で if 分岐をつけると良いようです。

※ Kotlin

public class ListViewForSwipeRefreshLayout : ListView {
    public constructor(context: Context) : super(context) {}
    public constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {}
    public constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {}

    override fun setVisibility(visibility: Int) {}
}

layout.xml

ListView を使っているところを、上記のカスタム ListView で置き換えます(パッケージ名は適当です。自分のものに合わせて下さい)。それ以外は SwipeRefreshLayout の標準的な使い方と同じだと思います。

<RelativeLayout
    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">

    <TextView
        android:id="@android:id/empty"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="@string/empty"
        android:textAlignment="center"
        android:textColor="@android:color/primary_text_light"
        android:layout_centerInParent="true"/>

    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/swipe_refresh_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <com.example.swiperefresh.ListViewForSwipeRefreshLayout 
            android:id="@android:id/list"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:clipToPadding="false">
        </com.example.swiperefresh.ListViewForSwipeRefreshLayout>
    </android.support.v4.widget.SwipeRefreshLayout>

</RelativeLayout>

参考

Android : Warning:Conflict with dependency 'com.android.support:support-annotations'. Resolved versions for app (23.1.0) and test app (22.2.0) differ.

エラーメッセージ全文は以下のとおりです。
解決のために参考となる URL が記載されているなんて素晴らしい!

Warning:Conflict with dependency 'com.android.support:support-annotations'. Resolved versions for app (23.1.0) and test app (22.2.0) differ. See http://g.co/androidstudio/app-test-app-conflict for details.

具体的には、実際のアプリ用のコードとテスト用のコードで、同じライブラリのバージョン違いを使ってしまっていることが問題になっているので、同じバージョンを使うように設定をすれば良いということでした。

gradle (app) の dependencies 内に

androidTestCompile 'com.android.support:support-annotations:23.1.0'

という一行を追加して、テストの方で使う support-annotations のバージョンを明示的に指定することでエラー解消です。

最終的な dependencies の全体は以下の様な具合に。

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])

    testCompile 'junit:junit:4.12'

    androidTestCompile 'com.android.support.test:runner:0.3'
    androidTestCompile 'com.android.support.test:rules:0.3'
    androidTestCompile 'com.android.support:support-annotations:23.1.0'

    compile 'com.android.support:appcompat-v7:23.1.0'
    compile 'com.android.support:support-v4:23.1.0'
    compile 'com.android.support:design:23.1.0'
}

Kotlin : as, !, ? 周りのチートシート

キャストや、型や変数の後ろにつける ! 記号、 ? 記号の意味についてのまとめです。

?

var b : String? = "abc"
b = null

型宣言に付けた場合は nullable (Kotlin は明示的に ? を付けないと null を代入できない)。

var l = b?.length

変数に付けた場合は Safe Call(安全な呼び出し)。 b が null でない場合にのみ関数を実行し、b が null の場合は null を返す。

?:

val l = b?.length ?: -1

?: の左側にくる式の値が null の場合は右側の値を返す。Elvis Operator。

!

※ Kotlin コードを書いている際に直接使用することはありません。

Java コード呼び出し時の引数や返り値に型の後ろについていて、T!T もしくは ? を表します。

例えば Android の Context.getFileStreamPath は IDE の補完機能では context.getFileStreamPath(name : String!) : File! と表示されますが、! がついていることにより、引数や返り値に nullable な型でもそうでない型でもどちらでも使用できることがわかります。

var p0 : String? = null;
var f0 : File? = context.getFileStreamPath(p0)
var p1 : String = ""
var f1 : File = context.getFileStreamPath(p1)

!!

var l = b!!.length()

b が Null の場合 Null Pointer Exception が発生します。

as

キャスト。キャストに失敗した場合は例外が発生します。

var x:String = y as String

as?

安全なキャスト。キャストが失敗した場合は null を返します。

var aInt : Int? = a as? Int

参考

Git : fatal: Failed to lock ref for update: No such file or directory

エラーが発生した手順

  1. sandbox/foo というブランチを作成する
  2. sandbox/foo/bar というブランチを作成しようとする

エラーの原因

  • sandbox/foo というブランチを作成しようとした時点で、 git が sandbox 以下に foo というファイルを作成している
  • sandbox/foo/bar というブランチを作成しようとした際、git は sandbox 以下に foo というフォルダを作成してその下に bar とういファイルを作成しようとするが、sandobox 以下に既に foo というファイルが存在しており、同名のファイルとフォルダは作成できないのでエラーが発生する

解決方法

sandbox/foo ブランチを sandbox/foo/hoge などにリネームしてから sandbox/foo/bar を作成するなど。

参考

Android : SlidingTabLayout を使う + タブの幅をウィンドウ幅に合わせる

f:id:tkyjhr:20151028211621p:plain:w360

SlidingTabsBasic | Android Developers のコードをダウンロードして(右上のボタンで可能)Android - SlidingTabsBasicをプロジェクトに導入する - Qiita に沿って適宜ファイルをコピーしていけば SlidingTabLayout を使えます。

ただし、SlidingTabsBasic | Android Developers でダウンロードできるコードの SlidingTabLayout にはタブの幅をウィンドウの幅に合わせられる API setDistributeEvenly(Boolean) が含まれていないので、 google/iosched · GitHubGoogle I/O スケジュールアプリ) から新しいものをダウンロードしてそちらを使用します。

以下、導入までの簡単なまとめと導入後のカスタマイズ方法。

ダウンロードするファイル

自分のプロジェクトに入れるファイル

ダウンロードしたもののパッケージパスを維持するか、自分のプロジェクトのパッケージに変更するかはお好みで。

  • SlidingTabsBasic のサンプルから(Application/src/main 以下)
    • java/com/example/android/slidingtabsbasic/SingleTabsBasicFragment.java
    • res/layout/fragment_sample.xml
    • res/layout/paper_item.txml
  • ioched から
    • SlidingTabStrip.java
    • SlidingTabLayout.java

タブの幅をウィンドウ幅に合わせる

SlidingTabsBasicFragment.java 内の onViewCreated で以下のように、SlidingTabLayout の setDistributeEvenly(true)setViewPager を呼ぶ前に呼びます。後から効果はありません。

mSlidingTabLayout = (SlidingTabLayout) view.findViewById(R.id.sliding_tabs);
mSlidingTabLayout.setDistributeEvenly(true);
mSlidingTabLayout.setViewPager(mViewPager);

タブ(ページ)の中身を変更する

SlidingTabsBasicFragment.java の SamplePagerAdapter クラス辺りが編集の対象になります。 自分が表示するもののの内容に合わせて、SlidingTabsBasicFragment クラスを参考に自作クラス・レイアウトを別途作るのが良いかと思います。

タブの色やテキストを変更する

SlidingTabLayout にテキストサイズが、SlidingTabStrip に選択中のタブの下に付く色が定義されています。

選択中のタブの下の色は SlidingTabLayout.TabColorizer インターフェースによって各タブ毎に設定が可能で、デフォルトで定義されている SimpleTabColorizer では以下のようにすると、2番目のタブの下線の色は黒になります。

mDefaultTabColorizer.setIndicatorColors(DEFAULT_SELECTED_INDICATOR_COLOR, 0xff000000);

参考

Kotlin : Unresolved reference: field

Kotlin の custome setter で $プロパティ名 を使っていたところ、$プロパティ名 という書き方は deprecated になったので代わりに field を使ってね、というメッセージが出たので素直に従ったところ、Unresolved reference Error が発生しました。Kotlin のバージョンを手動で上げなくてはならなかったようです。

Gradle の

buildscript {
    ext.kotlin_version = '0.13.1514'

となっていたところを

buildscript {
    ext.kotlin_version = '1.0.0-beta-1038'

に変更したらビルドが通るようになりました。 ちなみに、このバージョン変更で size() は size に、length() は length にといった対応が追加で必要でした。