mirror of
https://git.sr.ht/~thestr4ng3r/chiaki
synced 2025-08-20 13:33:13 -07:00
Use SurfaceView on Android
This commit is contained in:
parent
3a90ef0a65
commit
510064c899
6 changed files with 206 additions and 80 deletions
|
@ -26,13 +26,11 @@ ChiakiErrorCode android_chiaki_video_decoder_init(AndroidChiakiVideoDecoder *dec
|
||||||
return chiaki_mutex_init(&decoder->codec_mutex, false);
|
return chiaki_mutex_init(&decoder->codec_mutex, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void android_chiaki_video_decoder_fini(AndroidChiakiVideoDecoder *decoder)
|
static void kill_decoder(AndroidChiakiVideoDecoder *decoder)
|
||||||
{
|
{
|
||||||
if(decoder->codec)
|
|
||||||
{
|
|
||||||
chiaki_mutex_lock(&decoder->codec_mutex);
|
chiaki_mutex_lock(&decoder->codec_mutex);
|
||||||
decoder->shutdown_output = true;
|
decoder->shutdown_output = true;
|
||||||
ssize_t codec_buf_index = AMediaCodec_dequeueInputBuffer(decoder->codec, -1);
|
ssize_t codec_buf_index = AMediaCodec_dequeueInputBuffer(decoder->codec, 1000);
|
||||||
if(codec_buf_index >= 0)
|
if(codec_buf_index >= 0)
|
||||||
{
|
{
|
||||||
CHIAKI_LOGI(decoder->log, "Video Decoder sending EOS buffer");
|
CHIAKI_LOGI(decoder->log, "Video Decoder sending EOS buffer");
|
||||||
|
@ -48,7 +46,14 @@ void android_chiaki_video_decoder_fini(AndroidChiakiVideoDecoder *decoder)
|
||||||
chiaki_mutex_unlock(&decoder->codec_mutex);
|
chiaki_mutex_unlock(&decoder->codec_mutex);
|
||||||
}
|
}
|
||||||
AMediaCodec_delete(decoder->codec);
|
AMediaCodec_delete(decoder->codec);
|
||||||
}
|
decoder->codec = NULL;
|
||||||
|
decoder->shutdown_output = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void android_chiaki_video_decoder_fini(AndroidChiakiVideoDecoder *decoder)
|
||||||
|
{
|
||||||
|
if(decoder->codec)
|
||||||
|
kill_decoder(decoder);
|
||||||
chiaki_mutex_fini(&decoder->codec_mutex);
|
chiaki_mutex_fini(&decoder->codec_mutex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,6 +61,16 @@ void android_chiaki_video_decoder_set_surface(AndroidChiakiVideoDecoder *decoder
|
||||||
{
|
{
|
||||||
chiaki_mutex_lock(&decoder->codec_mutex);
|
chiaki_mutex_lock(&decoder->codec_mutex);
|
||||||
|
|
||||||
|
if(!surface)
|
||||||
|
{
|
||||||
|
if(decoder->codec)
|
||||||
|
{
|
||||||
|
kill_decoder(decoder);
|
||||||
|
CHIAKI_LOGI(decoder->log, "Decoder shut down after surface was removed");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if(decoder->codec)
|
if(decoder->codec)
|
||||||
{
|
{
|
||||||
#if __ANDROID_API__ >= 23
|
#if __ANDROID_API__ >= 23
|
||||||
|
|
|
@ -91,7 +91,7 @@ private class ChiakiNative
|
||||||
@JvmStatic external fun sessionStart(ptr: Long): Int
|
@JvmStatic external fun sessionStart(ptr: Long): Int
|
||||||
@JvmStatic external fun sessionStop(ptr: Long): Int
|
@JvmStatic external fun sessionStop(ptr: Long): Int
|
||||||
@JvmStatic external fun sessionJoin(ptr: Long): Int
|
@JvmStatic external fun sessionJoin(ptr: Long): Int
|
||||||
@JvmStatic external fun sessionSetSurface(ptr: Long, surface: Surface)
|
@JvmStatic external fun sessionSetSurface(ptr: Long, surface: Surface?)
|
||||||
@JvmStatic external fun sessionSetControllerState(ptr: Long, controllerState: ControllerState)
|
@JvmStatic external fun sessionSetControllerState(ptr: Long, controllerState: ControllerState)
|
||||||
@JvmStatic external fun sessionSetLoginPin(ptr: Long, pin: String)
|
@JvmStatic external fun sessionSetLoginPin(ptr: Long, pin: String)
|
||||||
@JvmStatic external fun discoveryServiceCreate(result: CreateResult, options: DiscoveryServiceOptions, javaService: DiscoveryService)
|
@JvmStatic external fun discoveryServiceCreate(result: CreateResult, options: DiscoveryServiceOptions, javaService: DiscoveryService)
|
||||||
|
@ -350,7 +350,7 @@ class Session(connectInfo: ConnectInfo, logFile: String?, logVerbose: Boolean)
|
||||||
event(RumbleEvent(left.toUByte(), right.toUByte()))
|
event(RumbleEvent(left.toUByte(), right.toUByte()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setSurface(surface: Surface)
|
fun setSurface(surface: Surface?)
|
||||||
{
|
{
|
||||||
ChiakiNative.sessionSetSurface(nativePtr, surface)
|
ChiakiNative.sessionSetSurface(nativePtr, surface)
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,8 @@ class StreamSession(val connectInfo: ConnectInfo, val logManager: LogManager, va
|
||||||
private val _rumbleState = MutableLiveData<RumbleEvent>(RumbleEvent(0U, 0U))
|
private val _rumbleState = MutableLiveData<RumbleEvent>(RumbleEvent(0U, 0U))
|
||||||
val rumbleState: LiveData<RumbleEvent> get() = _rumbleState
|
val rumbleState: LiveData<RumbleEvent> get() = _rumbleState
|
||||||
|
|
||||||
var surfaceTexture: SurfaceTexture? = null
|
private var surfaceTexture: SurfaceTexture? = null
|
||||||
|
private var surface: Surface? = null
|
||||||
|
|
||||||
init
|
init
|
||||||
{
|
{
|
||||||
|
@ -61,9 +62,9 @@ class StreamSession(val connectInfo: ConnectInfo, val logManager: LogManager, va
|
||||||
_state.value = StreamStateConnecting
|
_state.value = StreamStateConnecting
|
||||||
session.eventCallback = this::eventCallback
|
session.eventCallback = this::eventCallback
|
||||||
session.start()
|
session.start()
|
||||||
val surfaceTexture = surfaceTexture
|
val surface = surface
|
||||||
if(surfaceTexture != null)
|
if(surface != null)
|
||||||
session.setSurface(Surface(surfaceTexture))
|
session.setSurface(surface)
|
||||||
this.session = session
|
this.session = session
|
||||||
}
|
}
|
||||||
catch(e: CreateError)
|
catch(e: CreateError)
|
||||||
|
@ -92,6 +93,26 @@ class StreamSession(val connectInfo: ConnectInfo, val logManager: LogManager, va
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun attachToSurfaceView(surfaceView: SurfaceView)
|
||||||
|
{
|
||||||
|
surfaceView.holder.addCallback(object: SurfaceHolder.Callback {
|
||||||
|
override fun surfaceCreated(holder: SurfaceHolder) { }
|
||||||
|
|
||||||
|
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int)
|
||||||
|
{
|
||||||
|
val surface = holder.surface
|
||||||
|
this@StreamSession.surface = surface
|
||||||
|
session?.setSurface(surface)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun surfaceDestroyed(holder: SurfaceHolder)
|
||||||
|
{
|
||||||
|
this@StreamSession.surface = null
|
||||||
|
session?.setSurface(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fun attachToTextureView(textureView: TextureView)
|
fun attachToTextureView(textureView: TextureView)
|
||||||
{
|
{
|
||||||
textureView.surfaceTextureListener = object: TextureView.SurfaceTextureListener {
|
textureView.surfaceTextureListener = object: TextureView.SurfaceTextureListener {
|
||||||
|
@ -100,6 +121,7 @@ class StreamSession(val connectInfo: ConnectInfo, val logManager: LogManager, va
|
||||||
if(surfaceTexture != null)
|
if(surfaceTexture != null)
|
||||||
return
|
return
|
||||||
surfaceTexture = surface
|
surfaceTexture = surface
|
||||||
|
this@StreamSession.surface = Surface(surfaceTexture)
|
||||||
session?.setSurface(Surface(surface))
|
session?.setSurface(Surface(surface))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
package com.metallic.chiaki.stream
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
|
||||||
|
// see ExoPlayer's AspectRatioFrameLayout
|
||||||
|
class AspectRatioFrameLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null): FrameLayout(context, attrs)
|
||||||
|
{
|
||||||
|
companion object
|
||||||
|
{
|
||||||
|
private const val MAX_ASPECT_RATIO_DEFORMATION_FRACTION = 0.01f
|
||||||
|
}
|
||||||
|
|
||||||
|
var aspectRatio = 0f
|
||||||
|
set(value)
|
||||||
|
{
|
||||||
|
if(field != value)
|
||||||
|
{
|
||||||
|
field = value
|
||||||
|
requestLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var mode: TransformMode = TransformMode.FIT
|
||||||
|
set(value)
|
||||||
|
{
|
||||||
|
if(field != value)
|
||||||
|
{
|
||||||
|
field = value
|
||||||
|
requestLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int)
|
||||||
|
{
|
||||||
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||||
|
if(aspectRatio <= 0)
|
||||||
|
{
|
||||||
|
// Aspect ratio not set.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var width = measuredWidth
|
||||||
|
var height = measuredHeight
|
||||||
|
val viewAspectRatio = width.toFloat() / height
|
||||||
|
val aspectDeformation = aspectRatio / viewAspectRatio - 1
|
||||||
|
if(Math.abs(aspectDeformation) <= MAX_ASPECT_RATIO_DEFORMATION_FRACTION)
|
||||||
|
return
|
||||||
|
when(mode)
|
||||||
|
{
|
||||||
|
TransformMode.ZOOM ->
|
||||||
|
if(aspectDeformation > 0)
|
||||||
|
width = (height * aspectRatio).toInt()
|
||||||
|
else
|
||||||
|
height = (width / aspectRatio).toInt()
|
||||||
|
TransformMode.FIT ->
|
||||||
|
if(aspectDeformation > 0)
|
||||||
|
height = (width / aspectRatio).toInt()
|
||||||
|
else
|
||||||
|
width = (height * aspectRatio).toInt()
|
||||||
|
TransformMode.STRETCH -> {}
|
||||||
|
}
|
||||||
|
super.onMeasure(
|
||||||
|
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
||||||
|
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,12 +7,12 @@ import android.animation.AnimatorListenerAdapter
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.graphics.Matrix
|
import android.graphics.Matrix
|
||||||
import android.os.*
|
import android.os.*
|
||||||
import android.view.KeyEvent
|
import android.transition.TransitionManager
|
||||||
import android.view.MotionEvent
|
import android.view.*
|
||||||
import android.view.TextureView
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.constraintlayout.widget.ConstraintSet
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
@ -44,6 +44,7 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe
|
||||||
|
|
||||||
private lateinit var viewModel: StreamViewModel
|
private lateinit var viewModel: StreamViewModel
|
||||||
private val uiVisibilityHandler = Handler()
|
private val uiVisibilityHandler = Handler()
|
||||||
|
private val streamView: View get() = surfaceView
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?)
|
override fun onCreate(savedInstanceState: Bundle?)
|
||||||
{
|
{
|
||||||
|
@ -94,15 +95,17 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe
|
||||||
if (displayModeToggle.checkedButtonId == -1)
|
if (displayModeToggle.checkedButtonId == -1)
|
||||||
displayModeToggle.check(checkedId)
|
displayModeToggle.check(checkedId)
|
||||||
|
|
||||||
adjustTextureViewAspect()
|
adjustStreamViewAspect()
|
||||||
showOverlay()
|
showOverlay()
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.session.attachToTextureView(textureView)
|
//viewModel.session.attachToTextureView(textureView)
|
||||||
|
viewModel.session.attachToSurfaceView(surfaceView)
|
||||||
viewModel.session.state.observe(this, Observer { this.stateChanged(it) })
|
viewModel.session.state.observe(this, Observer { this.stateChanged(it) })
|
||||||
textureView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
/*streamView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
||||||
adjustTextureViewAspect()
|
adjustStreamViewAspect()
|
||||||
}
|
}*/
|
||||||
|
adjustStreamViewAspect()
|
||||||
|
|
||||||
if(Preferences(this).rumbleEnabled)
|
if(Preferences(this).rumbleEnabled)
|
||||||
{
|
{
|
||||||
|
@ -306,73 +309,84 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun adjustTextureViewAspect()
|
private fun adjustTextureViewAspect(textureView: TextureView)
|
||||||
{
|
{
|
||||||
val displayInfo = DisplayInfo(viewModel.session.connectInfo.videoProfile, textureView)
|
val trans = TextureViewTransform(viewModel.session.connectInfo.videoProfile, textureView)
|
||||||
val resolution = displayInfo.computeResolutionFor(displayModeToggle.checkedButtonId)
|
val resolution = trans.resolutionFor(TransformMode.fromButton(displayModeToggle.checkedButtonId))
|
||||||
|
|
||||||
Matrix().also {
|
Matrix().also {
|
||||||
textureView.getTransform(it)
|
textureView.getTransform(it)
|
||||||
it.setScale(resolution.width / displayInfo.viewWidth, resolution.height / displayInfo.viewHeight)
|
it.setScale(resolution.width / trans.viewWidth, resolution.height / trans.viewHeight)
|
||||||
it.postTranslate((displayInfo.viewWidth - resolution.width) * 0.5f, (displayInfo.viewHeight - resolution.height) * 0.5f)
|
it.postTranslate((trans.viewWidth - resolution.width) * 0.5f, (trans.viewHeight - resolution.height) * 0.5f)
|
||||||
textureView.setTransform(it)
|
textureView.setTransform(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun adjustSurfaceViewAspect()
|
||||||
|
{
|
||||||
|
val videoProfile = viewModel.session.connectInfo.videoProfile
|
||||||
|
aspectRatioLayout.aspectRatio = videoProfile.width.toFloat() / videoProfile.height.toFloat()
|
||||||
|
aspectRatioLayout.mode = TransformMode.fromButton(displayModeToggle.checkedButtonId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun adjustStreamViewAspect() = adjustSurfaceViewAspect()
|
||||||
|
|
||||||
override fun dispatchKeyEvent(event: KeyEvent) = viewModel.input.dispatchKeyEvent(event) || super.dispatchKeyEvent(event)
|
override fun dispatchKeyEvent(event: KeyEvent) = viewModel.input.dispatchKeyEvent(event) || super.dispatchKeyEvent(event)
|
||||||
override fun onGenericMotionEvent(event: MotionEvent) = viewModel.input.onGenericMotionEvent(event) || super.onGenericMotionEvent(event)
|
override fun onGenericMotionEvent(event: MotionEvent) = viewModel.input.onGenericMotionEvent(event) || super.onGenericMotionEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class TransformMode
|
||||||
class DisplayInfo constructor(val videoProfile: ConnectVideoProfile, val textureView: TextureView)
|
|
||||||
{
|
{
|
||||||
val contentWidth : Float get() = videoProfile.width.toFloat()
|
FIT,
|
||||||
val contentHeight : Float get() = videoProfile.height.toFloat()
|
STRETCH,
|
||||||
|
ZOOM;
|
||||||
|
|
||||||
|
companion object
|
||||||
|
{
|
||||||
|
fun fromButton(displayModeButtonId: Int)
|
||||||
|
= when (displayModeButtonId)
|
||||||
|
{
|
||||||
|
R.id.display_mode_stretch_button -> STRETCH
|
||||||
|
R.id.display_mode_zoom_button -> ZOOM
|
||||||
|
else -> FIT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TextureViewTransform(private val videoProfile: ConnectVideoProfile, private val textureView: TextureView)
|
||||||
|
{
|
||||||
|
private val contentWidth : Float get() = videoProfile.width.toFloat()
|
||||||
|
private val contentHeight : Float get() = videoProfile.height.toFloat()
|
||||||
val viewWidth : Float get() = textureView.width.toFloat()
|
val viewWidth : Float get() = textureView.width.toFloat()
|
||||||
val viewHeight : Float get() = textureView.height.toFloat()
|
val viewHeight : Float get() = textureView.height.toFloat()
|
||||||
val contentAspect : Float get() = contentHeight / contentWidth
|
private val contentAspect : Float get() = contentHeight / contentWidth
|
||||||
|
|
||||||
fun computeResolutionFor(displayModeButtonId: Int) : Resolution
|
fun resolutionFor(mode: TransformMode): Resolution
|
||||||
|
= when(mode)
|
||||||
{
|
{
|
||||||
when (displayModeButtonId)
|
TransformMode.STRETCH -> strechedResolution
|
||||||
{
|
TransformMode.ZOOM -> zoomedResolution
|
||||||
R.id.display_mode_stretch_button -> return computeStrechedResolution()
|
TransformMode.FIT -> normalResolution
|
||||||
R.id.display_mode_zoom_button -> return computeZoomedResolution()
|
|
||||||
else -> return computeNormalResolution()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun computeStrechedResolution(): Resolution
|
private val strechedResolution get() = Resolution(viewWidth, viewHeight)
|
||||||
{
|
|
||||||
return Resolution(viewWidth, viewHeight)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun computeZoomedResolution(): Resolution
|
private val zoomedResolution get() =
|
||||||
{
|
if(viewHeight > viewWidth * contentAspect)
|
||||||
if (viewHeight > viewWidth * contentAspect)
|
|
||||||
{
|
{
|
||||||
val zoomFactor = viewHeight / contentHeight
|
val zoomFactor = viewHeight / contentHeight
|
||||||
return Resolution(contentWidth * zoomFactor, viewHeight)
|
Resolution(contentWidth * zoomFactor, viewHeight)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
val zoomFactor = viewWidth / contentWidth
|
val zoomFactor = viewWidth / contentWidth
|
||||||
return Resolution(viewWidth, contentHeight * zoomFactor)
|
Resolution(viewWidth, contentHeight * zoomFactor)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun computeNormalResolution(): Resolution
|
private val normalResolution get() =
|
||||||
{
|
if(viewHeight > viewWidth * contentAspect)
|
||||||
if (viewHeight > viewWidth * contentAspect)
|
Resolution(viewWidth, viewWidth * contentAspect)
|
||||||
{
|
|
||||||
return Resolution(viewWidth, viewWidth * contentAspect)
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
Resolution(viewHeight / contentAspect, viewHeight)
|
||||||
return Resolution(viewHeight / contentAspect, viewHeight)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,24 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<FrameLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/mainStreamLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
tools:context=".stream.StreamActivity"
|
tools:context=".stream.StreamActivity"
|
||||||
android:keepScreenOn="true">
|
android:keepScreenOn="true">
|
||||||
|
|
||||||
<TextureView
|
<com.metallic.chiaki.stream.AspectRatioFrameLayout
|
||||||
android:id="@+id/textureView"
|
android:id="@+id/aspectRatioLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"/>
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="center">
|
||||||
|
<SurfaceView
|
||||||
|
android:id="@+id/surfaceView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
</com.metallic.chiaki.stream.AspectRatioFrameLayout>
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/progressBar"
|
android:id="@+id/progressBar"
|
||||||
|
@ -34,7 +42,6 @@
|
||||||
android:id="@+id/overlay"
|
android:id="@+id/overlay"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_gravity="bottom"
|
|
||||||
android:fitsSystemWindows="true">
|
android:fitsSystemWindows="true">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue