Kotlinでカラーピッカーの作成 – Android Studio

今回は2日くらい掛けてカラーピッカーの作成をしました。

一番上がユーザーパレットで、過去に使用した色の履歴が残ります。上下にフリックすることで順番の入れ替えが可。左右にスクロールすることができます。

真ん中がお馴染みのシークバー。

タイトル、シークバー、エディットボックスの三点セット。

一番下がデフォルトパレット、64色が一番見栄えが良かったです。

255を3分割して(0 , 85, 170, 255)の組み合わせて色を作成しています。

選んだ色のプレビューはタイトルバーの色を変化させています。

備忘録に制作手順を残しておきます。

レイアウトリソースの作成

res/layoutフォルダを右クリック、new → Layout Resource Fileを選択

ファイル名を入力(color_picker) → OKボタン

テーブルレイアウトを追加

ConstraintLayoutの下にTableLayoutを追加

TableLayoutを選択し、右側Layoutの四隅の+ボタンを押して、テーブルのサイズを決定

weightSumパラメータを100に設定(子アイテムの高さを合計100で分割する)

TableLayoutを展開し1つめのTableLowを選択

layout_weightに5を入力(親の5%の高さ)

layout_heightに0dpと入力(高さをweightに任せる)

weightSumに10を入力(子アイテムの幅を10分割)

シークバーの追加

タイトルテキストの追加

タイトルの”R”, “G”, “B”のテキストを追加する

TableLowの下にTextViewを追加

TextViewのパラメータ、layout_weightに”1″(幅の1割を使う)、layout_heightに”match_parent”(親と同じ高さにする)、layout_widthに”0dp”(幅をweightに任せる)と入力

textにR、idにTextViewRと入力

textSizeパラメータに”24sp”、textAlignmentにcenter(左右中央)、gravityにcenter(上下中央)を入力

Component TreeのtextViewの右側に付いている警告アイコンをクリックし、出てきたメッセージの下側にあるFixボタンを押す。(文字はストリンクテーブルから選んで下さいという警告)

rと打ち込みOKボタンを押す

スライドバーを追加

WidgetsからSeekBarを選択し、textViewRの下に追加する

idにSeekBarRと入力

layout_weightに7、laout_widthに0dp、layout_heightにmatch_parentと入力

エディットボックスの追加

TextのNumberを選び、シークバーの下に配置

idにeditTextNumberRと入力

layout_weightに2、layout_widthに0dp、layout_heightにmatch_parentと入力

textSizeに20sp、textAlignmentとgravityにcenterを指定

textに0と入力し、警告をクリックしてFIX

(ヒントどうこうの警告が出ますが、Hintのパラメータが空だとデザインが壊れて表示されなくなったためコードから削除)

GとBのレイアウトはコピーする

G、Bと同じように入力してくのは手間なのでコピーしてidだけを書き換えます。

右上のDesignからCodeに切り替え

xml形式でTableRowにTextView、SeekBar、EditBoxが挟まれている構造が見えるので、

TableRowごと丸々コピーして、その下に貼り付けます。

ついでに初期の空っぽのTableRow3つを消しておきます。

貼り付けたアイテムのidを書き換えます(R→Gに)

同じようにBを作成

イメージビューの追加

パレット用のイメージビューを追加します

TableLayoutの下に新たにTableRowを追加。(シークバーの上と下に1つずつ)

layout_weightに10、layout_widthにmatch_parent、layout_heightに0dpと入力

そのTableRowの子にImageViewを追加。

画像を適当に選んでOKボタン、srcCompatプロパティを削除して画像を取り除きます。

layout_weightに1、layout_widthにmatch_parent、layout_heightにmatch_parentと入力

下側のイメージビューも同じように設定(TableRowのlayout_weightを30にする)

コードを記述

ダイアログクラスを作成し、コードを記述します。

コードと説明を全部書くとなると情報が膨大になってしまうので要点だけ。

OK・キャンセルの処理

ダイアログリスナーのインターフェイスを作成してMainActivityで継承し、onOK,onCancelをオーバーライドして処理をするという方法をとっています。

AlertDialog.Builderを使ってダイアログを作成

Androidのダイアログはモードレスダイアログなので、ダイアログの入力が終わるまで処理待ちといったことが出来ません。

なのでダイアログクラスのメンバー変数に戻り値を収めておいて、OnOK処理に渡されるダイアログオブジェクトから入力結果を取得します。

ダイアログのshowメソッドを呼び出す前に、メンバー変数のパラメータを設定しておけば、初期化のコントロールが出来ます。

このダイアログでは、ユーザーパレットの配列を渡してからshowを呼び出します。

シークバーの処理

ダイアログクラスのメンバーにシークバーオブジェクトを持たせます。

private var barR:SeekBar? = null

メンバーとして持っておくことで、パレットが選択されたときなどに、シークバーの位置を変更することができます。

OnCreateDialogの処理でシークバーを読み込みます。

barR = view.findViewById(R.id.seekBarR)

null保証がないとエラーが出るので、アクセスするときはbarR!!でアクセスします。

同じく、OnCreateDialogの中で、シークバーを操作したときの処理を書きます。

setOnSeekBarChangeListenerを使用します。

barR!!.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
            override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
                if(flagChange)
                    return
                color = Color.rgb(barR!!.progress, barG!!.progress, barB!!.progress)
                updateText()
            }
            override fun onStartTrackingTouch(seekBar: SeekBar?) {}
            override fun onStopTrackingTouch(seekBar: SeekBar?) {}
        })

ここで注意しなければいけないのが、シークバーの値をプログラムから変更したときも、この処理が呼び出されてしまう点です。

プログラムから変更中のフラグをセットして、弾くようにしないと、シークバーの変更→エディットボックスに反映→エディットボックスの変更→シークバーに反映・・・と無限ループに陥ってしまいます。

シークバーの値を変更するときは

barR!!.progress = color.red

のようにアクセスします。(colorはInt型)

エディットボックスの処理

エディットボックスも、シークバーと同じようにメンバー変数として保持しておきます。

エディットボックスが変更されたときの処理はaddTextChangedListenerを使用します。

TextWatcherをオーバーライドして処理を書くのですが、GでもBでも使い回せるようにTextWatcherクラスを継承したByteTextWatcherクラスを作成して使用します。

入力は3文字まで、0~255までの入力を受け付けるといったインターフェイスにします。

ダイアログクラスのメンバーに次のコードを追加

interface ByteTextWatcher: TextWatcher{
        fun getNormalNumber(str: String): Int{
            var num = 0
            if(str != "") num = str.toInt()
            if(num < 0) num = 0
            if(num > 255) num = 255
            return num
        }
        fun action(num: Int){}
        override fun afterTextChanged(ed: Editable?) {
            var text = ed.toString()
            if(text.length > 3) {
                text = text.substring(text.length - 3)
            }
            val num = getNormalNumber(text)
            action(num)
        }
        override fun beforeTextChanged(arg1: CharSequence?, arg2: Int, arg3: Int, arg4: Int) {}
        override fun onTextChanged(arg1: CharSequence?, arg2: Int, arg3: Int, arg4: Int) {}
    }

OnCreateDialogに次のコードを追加(変数editR取得後)

editR!!.addTextChangedListener(object: ByteTextWatcher{
            override fun action(num: Int) {
                if(flagChange) return
                color = Color.rgb(num, barG!!.progress, barB!!.progress)
                updateProgress()
            }
        })

actionをオーバーライドして処理を書くだけです。(0~255の数値が保証されています)

こちらも無限ループ防止にプログラムからの書き換え中フラグを使用しています。

パレット(イメージビュー)の処理

ソースを全書き出来ない理由が、パレットの描画に自作のアイコンパッドクラスを使っているためです。

アイコンパッドクラスは画像を登録して、並べて描画するクラスです。

描画領域とカラム数をセットしてソートすれば描画位置が決まり、IsHitメソッドを呼び出すことでクリックしたアイコンを取得することが出来ます。(取得方法は名前とインデックスの2種類)

なので、それ以外の部分を解説します。

ダイアログのメンバーにイメージビューオブジェクト、描画用のビットマップを持たせます。

var pltView: ImageView? = null

var pltBmp: Bitmap? = null

OnCreateDialogでImageViewのインスタンスを生成(取得)します。

pltView = view.findViewById(R.id.imageView)

このときにビューと同じサイズのビットマップを生成したいのですが、初期化中なのでイメージビューのwidthとheightを取得しても0になってしまいます。

ビューの高さと幅が決まってからビットマップ生成を行いたいのでOnCreateDialog内で次のように書きます。

// OnCreateDialog内        
// パレットビューの初期化処理(カラーボックスアイコンの作成)
pltView = view.findViewById(R.id.imageView)
val ob = pltView!!.viewTreeObserver
ob.addOnGlobalLayoutListener {
    if(!flagInit) {
        val pw = pltView!!.width
        val ph = pltView!!.height
        val w = pw / 16
        val h = ph / 2

        // パレットのカラー一覧の作成(4 * 4 * 4)
        makeColorList(3)

        // ビュー全体のビットマップ
        pltBmp = createBitmap(pw, ph)

        // アイコンパッドの作成と領域設定
        for (i in 0 until colorList.size)
            pad.addColorBox(i.toString(), w, h, colorList[i], paint)
        pad.setPadRect(RectF(0f, 0f, (pw - 1).toFloat(), (ph - 1).toFloat()))
        pad.sortIcon(16)

        // アイコンパッドをビューのビットマップに書き込む
        val cv = Canvas(pltBmp!!)
        pad.drawPad(cv, paint)
        pltView!!.setImageBitmap(pltBmp)
               
        flagInit = true
    }
}

ビットマップを生成し、パレットを描画、イメージビューにセットするという流れです。

パレットのスクロールとタッチイベントは次のように書きます。

(※アイコンパッドクラスはスクロールさせて描画位置をずらす機能もあります)

// パレットのジェスチャー処理
val gestureListener = object : GestureDetector.SimpleOnGestureListener() {
    override fun onScroll(
        e1: MotionEvent,
        e2: MotionEvent,
        distanceX: Float,
        distanceY: Float
    ): Boolean {
        pad.scrollPad(0f, distanceY)
        val pw = pltView!!.width
        val ph = pltView!!.height
        pad.setPadRect(RectF(0f, 0f, (pw - 1).toFloat(), (ph - 1).toFloat()))
        val cv = Canvas(pltBmp!!)
        cv.drawColor(Color.WHITE)
        pad.drawPad(Paint(), cv)
        pltView!!.setImageBitmap(pltBmp)
        return true
    }

}

val mGestureDetector = GestureDetector(context,gestureListener)

pltView!!.setOnTouchListener{_, event ->
    mGestureDetector.onTouchEvent(event)
    when (event.actionMasked) {
        MotionEvent.ACTION_DOWN -> {
            val i = pad.isHitIconIndex(event.x, event.y)
            if(i != -1){
                color = colorList[i]
                updateText()
                updateProgress()
            }
        }
        MotionEvent.ACTION_MOVE -> {
        }
        MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
        }
        MotionEvent.ACTION_SCROLL ->{
        }
    }
    true
}

タイトルバーにカラーを反映

タイトルバーの色を変更するにはカスタムタイトルバーという方法を使用します。

ダイアログクラスのメンバーにテキストビュークラスを持たせます。

private var tv:TextView? = null

OnCreateDialog内でインスタンスを生成します。

tv = TextView(activity)

TextViewをカスタマイズし、AlertDialogBuilderのsetCustomTitleに渡します。

tv!!.text = resources.getString(R.string.color_select)
tv!!.textSize = 18f
tv!!.setBackgroundColor(color)
if(color.red + color.green + color.blue > 128*3)
    tv!!.setTextColor(Color.BLACK)
else
    tv!!.setTextColor(Color.WHITE)

tv!!.setPadding(0, 0, 0, 0)
tv!!.gravity = Gravity.CENTER

builder.setView(view)
    .setCustomTitle(tv)

このようにすることで、TextViewの背景色を変更すると、タイトルバーの色が変わるようになります。

終わりに

アイコンパッドクラスを使っているため、完全な説明にはなりませんが、大体の概要はこんな感じです。

ImageViewの大きさを取得できないとか、思った位置に配置できないとか結構悩みました。

でも悩んだだけ良いものが出来たと思います。

使用中の動画

予測変換にヒヤヒヤw

おすすめ

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です