1 /*
2  * Copyright 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.plausiblesoftware.drumthumper
17 
18 import android.content.Context
19 import android.graphics.Canvas
20 import android.graphics.Color
21 import android.graphics.Paint
22 import android.graphics.RectF
23 import android.util.AttributeSet
24 import android.util.TypedValue
25 import android.view.MotionEvent
26 import android.view.View
27 
28 class TriggerPad: View {
29 
30     private val mDrawRect = RectF()
31     private val mPaint = Paint()
32 
33     private val mUpColor = Color.LTGRAY
34     private val mDownColor = Color.DKGRAY
35     private var mIsDown = false
36 
37     private var mText = "DrumPad"
38     private var mTextSizeSp = 28.0f
39 
40     private val mTextColor = Color.BLACK
41 
42     val DISPLAY_MASK        = 0x00000003
43     val DISPLAY_RECT        = 0x00000000
44     val DISPLAY_CIRCLE      = 0x00000001
45     val DISPLAY_ROUND_RECT  = 0x00000002
46 
47     private var mDisplayFlags = DISPLAY_ROUND_RECT
48 
49     interface DrumPadTriggerListener {
triggerDownnull50         fun triggerDown(pad: TriggerPad)
51         fun triggerUp(pad: TriggerPad)
52     }
53 
54     var mListeners = ArrayList<DrumPadTriggerListener>()
55 
56     constructor(context: Context) : super(context)
57 
58     constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
59         extractAttributes(attrs)
60     }
61 
62     constructor(context: Context, attrs: AttributeSet, defStyle: Int): super(context, attrs, defStyle) {
63         extractAttributes(attrs)
64     }
65 
66     //
67     // Attributes
68     //
extractAttributesnull69     private fun extractAttributes(attrs: AttributeSet) {
70         val xmlns = "http://schemas.android.com/apk/res/android"
71         val textVal = attrs.getAttributeValue(xmlns, "text")
72         if (textVal != null) {
73             mText = textVal
74         }
75     }
76 
77     //
78     // Layout Routines
79     //
calcTextSizeInPixelsnull80     private fun calcTextSizeInPixels(): Float {
81         return TypedValue.applyDimension(
82                 TypedValue.COMPLEX_UNIT_SP,
83                 mTextSizeSp,
84                 resources.displayMetrics
85         )
86     }
87 
onSizeChangednull88     override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
89         val padLeft = paddingLeft
90         val padRight = paddingRight
91         val padTop = paddingTop
92         val padBottom = paddingBottom
93 
94         mDrawRect.set(padLeft.toFloat(),
95                 padTop.toFloat(),
96                 w - padRight.toFloat(),
97                 h - padBottom.toFloat())
98 
99         // mTextSize = mDrawRect.bottom / 4.0f
100     }
101 
onMeasurenull102     override fun onMeasure (widthMeasureSpec: Int, heightMeasureSpec: Int) {
103         val width = MeasureSpec.getSize(widthMeasureSpec)
104 
105         val padTop = paddingTop
106         val padBottom = paddingBottom
107 
108         val heightMode = MeasureSpec.getMode(heightMeasureSpec)
109         var height = MeasureSpec.getSize(heightMeasureSpec)
110 
111         val textSizePixels = calcTextSizeInPixels()
112         when (heightMode) {
113             MeasureSpec.AT_MOST -> run {
114                 // mText = "AT_MOST"
115                 val newHeight = (textSizePixels.toInt() * 2) + padTop + padBottom
116                 height = minOf(height, newHeight) }
117 
118             MeasureSpec.EXACTLY -> run {
119                 /*mText = "EXACTLY"*/ }
120 
121             MeasureSpec.UNSPECIFIED -> run {
122                 // mText = "UNSPECIFIED"
123                 height = textSizePixels.toInt() }
124         }
125 
126         setMeasuredDimension(width, height)
127     }
128 
129     //
130     // Drawing Routines
131     //
onDrawnull132     override fun onDraw(canvas: Canvas) {
133         // Face
134         if (mIsDown) {
135             mPaint.color = mDownColor
136         } else {
137             mPaint.color = mUpColor
138         }
139 
140         when (mDisplayFlags and DISPLAY_MASK) {
141             DISPLAY_RECT -> canvas.drawRect(mDrawRect, mPaint)
142 
143             DISPLAY_CIRCLE -> run {
144                 val midX = mDrawRect.left + mDrawRect.width() / 2.0f
145                 val midY = mDrawRect.top + mDrawRect.height() / 2.0f
146                 val radius = minOf(mDrawRect.height() / 2.0f, mDrawRect.width() / 2.0f)
147                 canvas.drawCircle(midX, midY, radius - 5.0f, mPaint)
148             }
149 
150             DISPLAY_ROUND_RECT -> run {
151                 val rad = minOf(mDrawRect.width() / 8.0f, mDrawRect.height() / 8.0f)
152                 canvas.drawRoundRect(mDrawRect, rad, rad, mPaint)
153             }
154         }
155 
156         // Text
157         val midX = mDrawRect.width() / 2
158         mPaint.textSize = calcTextSizeInPixels()
159         val textWidth = mPaint.measureText(mText)
160         mPaint.color = mTextColor
161         val textSizePixels = calcTextSizeInPixels()
162         canvas.drawText(mText, mDrawRect.left + midX - textWidth / 2,
163                 mDrawRect.bottom/2 + textSizePixels/2, mPaint)
164 
165     }
166 
167     //
168     // Input Routines
169     //
onTouchEventnull170     override fun onTouchEvent(event: MotionEvent): Boolean {
171         if (event.actionMasked == MotionEvent.ACTION_DOWN ||
172                 event.actionMasked == MotionEvent.ACTION_POINTER_DOWN) {
173             mIsDown = true;
174             triggerDown()
175             invalidate()
176             return true
177         } else if (event.actionMasked == MotionEvent.ACTION_UP) {
178             mIsDown = false;
179             triggerUp()
180             invalidate()
181             return true
182         }
183 
184         return false
185     }
186 
187     //
188     // Event Listeners
189     //
addListenernull190     fun addListener(listener: DrumPadTriggerListener) {
191         mListeners.add(listener)
192     }
193 
triggerDownnull194     private fun triggerDown() {
195         for( listener in mListeners) {
196             listener.triggerDown(this)
197         }
198     }
199 
triggerUpnull200     private fun triggerUp() {
201         for( listener in mListeners) {
202             listener.triggerUp(this)
203         }
204     }
205 }
206