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 }