1 /*
2  * Copyright (C) 2020 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.android.test.silkfx.materials
17 
18 import android.content.Context
19 import android.graphics.Bitmap
20 import android.graphics.BitmapFactory
21 import android.graphics.BitmapShader
22 import android.graphics.BlendMode
23 import android.graphics.Canvas
24 import android.graphics.Color
25 import android.graphics.Outline
26 import android.graphics.Paint
27 import android.graphics.Rect
28 import android.graphics.RenderEffect
29 import android.graphics.RenderNode
30 import android.graphics.Shader
31 import android.hardware.Sensor
32 import android.hardware.SensorEvent
33 import android.hardware.SensorEventListener
34 import android.hardware.SensorManager
35 import android.util.AttributeSet
36 import android.view.View
37 import android.view.ViewOutlineProvider
38 import android.widget.FrameLayout
39 import com.android.test.silkfx.R
40 import kotlin.math.sin
41 import kotlin.math.sqrt
42 
43 class GlassView(context: Context, attributeSet: AttributeSet) : FrameLayout(context, attributeSet) {
44 
45     private val textureTranslationMultiplier = 200f
46 
47     private var gyroXRotation = 0f
48     private var gyroYRotation = 0f
49 
50     private var noise = BitmapFactory.decodeResource(resources, R.drawable.noise)
51     private var materialPaint = Paint()
52     private var scrimPaint = Paint()
53     private var noisePaint = Paint()
54     private var blurPaint = Paint()
55 
56     private val src = Rect()
57     private val dst = Rect()
58 
59     private val sensorManager = context.getSystemService(SensorManager::class.java)
60     private val sensorListener = object : SensorEventListener {
61 
62         // Constant to convert nanoseconds to seconds.
63         private val NS2S = 1.0f / 1000000000.0f
64         private val EPSILON = 0.000001f
65         private var timestamp: Float = 0f
66 
67         override fun onSensorChanged(event: SensorEvent?) {
68             // This timestep's delta rotation to be multiplied by the current rotation
69             // after computing it from the gyro sample data.
70             if (timestamp != 0f && event != null) {
71                 val dT = (event.timestamp - timestamp) * NS2S
72                 // Axis of the rotation sample, not normalized yet.
73                 var axisX: Float = event.values[0]
74                 var axisY: Float = event.values[1]
75                 var axisZ: Float = event.values[2]
76 
77                 // Calculate the angular speed of the sample
78                 val omegaMagnitude: Float = sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ)
79 
80                 // Normalize the rotation vector if it's big enough to get the axis
81                 // (that is, EPSILON should represent your maximum allowable margin of error)
82                 if (omegaMagnitude > EPSILON) {
83                     axisX /= omegaMagnitude
84                     axisY /= omegaMagnitude
85                     axisZ /= omegaMagnitude
86                 }
87 
88                 // Integrate around this axis with the angular speed by the timestep
89                 // in order to get a delta rotation from this sample over the timestep
90                 // We will convert this axis-angle representation of the delta rotation
91                 // into a quaternion before turning it into the rotation matrix.
92                 val thetaOverTwo: Float = omegaMagnitude * dT / 2.0f
93                 val sinThetaOverTwo: Float = sin(thetaOverTwo)
94                 gyroXRotation += sinThetaOverTwo * axisX
95                 gyroYRotation += sinThetaOverTwo * axisY
96 
97                 invalidate()
98             }
99             timestamp = event?.timestamp?.toFloat() ?: 0f
100         }
101 
102         override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { }
103     }
104 
105     var backgroundBitmap: Bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
106     set(value) {
107         field = value
108         invalidate()
109     }
110 
111     var noiseOpacity = 0.0f
112     set(value) {
113         field = value
114         noisePaint.alpha = (value * 255).toInt()
115         invalidate()
116     }
117 
118     var materialOpacity = 0.0f
119     set(value) {
120         field = value
121         materialPaint.alpha = (value * 255).toInt()
122         invalidate()
123     }
124 
125     var scrimOpacity = 0.5f
126         set(value) {
127             field = value
128             scrimPaint.alpha = (value * 255).toInt()
129             invalidate()
130         }
131 
132     var zoom = 0.0f
133         set(value) {
134             field = value
135             invalidate()
136         }
137 
138     var color = Color.BLACK
139     set(value) {
140         field = value
141         var alpha = materialPaint.alpha
142         materialPaint.color = color
143         materialPaint.alpha = alpha
144 
145         alpha = scrimPaint.alpha
146         scrimPaint.color = color
147         scrimPaint.alpha = alpha
148         invalidate()
149     }
150 
151     var blurRadius = 150f
152     set(value) {
153         field = value
154         renderNode.setRenderEffect(
155                 RenderEffect.createBlurEffect(value, value, Shader.TileMode.CLAMP))
156         invalidate()
157     }
158 
159     private var renderNodeIsDirty = true
160     private val renderNode = RenderNode("GlassRenderNode")
161 
162     override fun invalidate() {
163         renderNodeIsDirty = true
164         super.invalidate()
165     }
166 
167     init {
168         setWillNotDraw(false)
169         materialPaint.blendMode = BlendMode.SOFT_LIGHT
170         noisePaint.blendMode = BlendMode.SOFT_LIGHT
171         noisePaint.shader = BitmapShader(noise, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
172         scrimPaint.alpha = (scrimOpacity * 255).toInt()
173         noisePaint.alpha = (noiseOpacity * 255).toInt()
174         materialPaint.alpha = (materialOpacity * 255).toInt()
175         outlineProvider = object : ViewOutlineProvider() {
176             override fun getOutline(view: View?, outline: Outline?) {
177                 outline?.setRoundRect(Rect(0, 0, width, height), 100f)
178             }
179         }
180         clipToOutline = true
181     }
182 
183     override fun onAttachedToWindow() {
184         sensorManager?.getSensorList(Sensor.TYPE_GYROSCOPE)?.firstOrNull().let {
185             sensorManager?.registerListener(sensorListener, it, SensorManager.SENSOR_DELAY_GAME)
186         }
187     }
188 
189     override fun onDetachedFromWindow() {
190         sensorManager?.unregisterListener(sensorListener)
191     }
192 
193     override fun onDraw(canvas: Canvas?) {
194         updateGlassRenderNode()
195         canvas?.drawRenderNode(renderNode)
196     }
197 
198     fun resetGyroOffsets() {
199         gyroXRotation = 0f
200         gyroYRotation = 0f
201         invalidate()
202     }
203 
204     private fun updateGlassRenderNode() {
205         if (renderNodeIsDirty) {
206             renderNode.setPosition(0, 0, getWidth(), getHeight())
207 
208             val canvas = renderNode.beginRecording()
209 
210             src.set(-width / 2, -height / 2, width / 2, height / 2)
211             src.scale(1.0f + zoom)
212             val centerX = left + width / 2
213             val centerY = top + height / 2
214             val textureXOffset = (textureTranslationMultiplier * gyroYRotation).toInt()
215             val textureYOffset = (textureTranslationMultiplier * gyroXRotation).toInt()
216             src.set(src.left + centerX + textureXOffset, src.top + centerY + textureYOffset,
217                     src.right + centerX + textureXOffset, src.bottom + centerY + textureYOffset)
218 
219             dst.set(0, 0, width, height)
220             canvas.drawBitmap(backgroundBitmap, src, dst, blurPaint)
221             canvas.drawRect(dst, materialPaint)
222             canvas.drawRect(dst, noisePaint)
223             canvas.drawRect(dst, scrimPaint)
224 
225             renderNode.endRecording()
226 
227             renderNodeIsDirty = false
228         }
229     }
230 }