Add joystick press and general clean up

This commit is contained in:
Willi Ye 2020-10-03 12:10:16 +02:00 committed by ◱ PixelyIon
parent 3057e4b29a
commit e023dbbf0a
10 changed files with 77 additions and 120 deletions

View File

@ -411,9 +411,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
return true return true
} }
private fun onButtonStateChanged(buttonId : ButtonId, state : ButtonState) { private fun onButtonStateChanged(buttonId : ButtonId, state : ButtonState) = setButtonState(0, buttonId.value(), state.state)
setButtonState(0, buttonId.value(), state.state)
}
private fun onStickStateChanged(buttonId : ButtonId, position : PointF) { private fun onStickStateChanged(buttonId : ButtonId, position : PointF) {
val stickId = when (buttonId) { val stickId = when (buttonId) {
@ -423,7 +421,6 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
else -> error("Invalid button id") else -> error("Invalid button id")
} }
Log.i("blaa", "$position")
setAxisValue(0, stickId.xAxis.ordinal, (position.x * Short.MAX_VALUE).toInt()) setAxisValue(0, stickId.xAxis.ordinal, (position.x * Short.MAX_VALUE).toInt())
setAxisValue(0, stickId.yAxis.ordinal, (-position.y * Short.MAX_VALUE).toInt()) // Y is inverted setAxisValue(0, stickId.yAxis.ordinal, (-position.y * Short.MAX_VALUE).toInt()) // Y is inverted
} }

View File

@ -49,9 +49,7 @@ class MainActivity : AppCompatActivity() {
/** /**
* The adapter used for adding elements to [app_list] * The adapter used for adding elements to [app_list]
*/ */
private val adapter by lazy { private lateinit var adapter : AppAdapter
AppAdapter(layoutType = layoutType, onClick = ::selectStartGame, onLongClick = ::selectShowGameDialog)
}
private var reloading = AtomicBoolean() private var reloading = AtomicBoolean()
@ -223,6 +221,7 @@ class MainActivity : AppCompatActivity() {
val metrics = resources.displayMetrics val metrics = resources.displayMetrics
val gridSpan = ceil((metrics.widthPixels / metrics.density) / itemWidth).toInt() val gridSpan = ceil((metrics.widthPixels / metrics.density) / itemWidth).toInt()
adapter = AppAdapter(layoutType = layoutType, onClick = ::selectStartGame, onLongClick = ::selectShowGameDialog)
app_list.adapter = adapter app_list.adapter = adapter
app_list.layoutManager = when (adapter.layoutType) { app_list.layoutManager = when (adapter.layoutType) {
LayoutType.List -> LinearLayoutManager(this).also { app_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL)) } LayoutType.List -> LinearLayoutManager(this).also { app_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL)) }

View File

@ -15,12 +15,13 @@ import android.view.ViewGroup
import android.view.Window import android.view.Window
import android.widget.ImageView import android.widget.ImageView
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.TextView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import emu.skyline.R import emu.skyline.R
import emu.skyline.data.AppItem import emu.skyline.data.AppItem
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.app_item_grid_compact.*
/** /**
* This enumerates the type of layouts the menu can be in * This enumerates the type of layouts the menu can be in
@ -47,23 +48,9 @@ internal class AppAdapter(val layoutType : LayoutType, private val onClick : Int
super.addHeader(BaseHeader(string)) super.addHeader(BaseHeader(string))
} }
/** private class ItemViewHolder(override val containerView : View) : RecyclerView.ViewHolder(containerView), LayoutContainer
* The ViewHolder used by items is used to hold the views associated with an item
*
* @param parent The parent view that contains all the others
* @param icon The ImageView associated with the icon
* @param title The TextView associated with the title
* @param subtitle The TextView associated with the subtitle
*/
private class ItemViewHolder(val parent : View, var icon : ImageView, var title : TextView, var subtitle : TextView, var card : View? = null) : RecyclerView.ViewHolder(parent)
/** private class HeaderViewHolder(override val containerView : View) : RecyclerView.ViewHolder(containerView), LayoutContainer
* The ViewHolder used by headers is used to hold the views associated with an headers
*
* @param parent The parent view that contains all the others
* @param header The TextView associated with the header
*/
private class HeaderViewHolder(val parent : View, var header : TextView? = null) : RecyclerView.ViewHolder(parent)
/** /**
* This function creates the view-holder of type [viewType] with the layout parent as [parent] * This function creates the view-holder of type [viewType] with the layout parent as [parent]
@ -79,20 +66,9 @@ internal class AppAdapter(val layoutType : LayoutType, private val onClick : Int
} }
return when (ElementType.values()[viewType]) { return when (ElementType.values()[viewType]) {
ElementType.Item -> { ElementType.Item -> ItemViewHolder(view)
ItemViewHolder(view, view.findViewById(R.id.icon), view.findViewById(R.id.text_title), view.findViewById(R.id.text_subtitle)).apply {
if (layoutType == LayoutType.Grid || layoutType == LayoutType.GridCompact) {
card = view.findViewById(R.id.app_item_grid)
title.isSelected = true
}
}
}
ElementType.Header -> { ElementType.Header -> HeaderViewHolder(view)
HeaderViewHolder(view).apply {
header = view.findViewById(R.id.text_title)
}
}
} }
} }
@ -103,8 +79,8 @@ internal class AppAdapter(val layoutType : LayoutType, private val onClick : Int
val item = getItem(position) val item = getItem(position)
if (item is AppItem && holder is ItemViewHolder) { if (item is AppItem && holder is ItemViewHolder) {
holder.title.text = item.title holder.text_title.text = item.title
holder.subtitle.text = item.subTitle ?: item.loaderResultString(holder.subtitle.context) holder.text_subtitle.text = item.subTitle ?: item.loaderResultString(holder.text_subtitle.context)
holder.icon.setImageBitmap(item.icon ?: missingIcon) holder.icon.setImageBitmap(item.icon ?: missingIcon)
@ -114,13 +90,13 @@ internal class AppAdapter(val layoutType : LayoutType, private val onClick : Int
when (layoutType) { when (layoutType) {
LayoutType.List -> holder.itemView LayoutType.List -> holder.itemView
LayoutType.Grid, LayoutType.GridCompact -> holder.card!! LayoutType.Grid, LayoutType.GridCompact -> holder.card_app_item_grid
}.apply { }.apply {
setOnClickListener { onClick.invoke(item) } setOnClickListener { onClick.invoke(item) }
setOnLongClickListener { true.also { onLongClick.invoke(item) } } setOnLongClickListener { true.also { onLongClick.invoke(item) } }
} }
} else if (item is BaseHeader && holder is HeaderViewHolder) { } else if (item is BaseHeader && holder is HeaderViewHolder) {
holder.header!!.text = item.title holder.text_title.text = item.title
} }
} }

View File

@ -9,11 +9,13 @@ import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import emu.skyline.R import emu.skyline.R
import emu.skyline.data.BaseItem import emu.skyline.data.BaseItem
import emu.skyline.input.* import emu.skyline.input.*
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.controller_item.*
import kotlinx.android.synthetic.main.section_item.text_title
/** /**
* This is a class that holds everything relevant to a single item in the controller configuration list * This is a class that holds everything relevant to a single item in the controller configuration list
@ -22,14 +24,8 @@ import emu.skyline.input.*
* @param subContent The secondary line of text to show data more specific data about the item * @param subContent The secondary line of text to show data more specific data about the item
*/ */
abstract class ControllerItem(var content : String, var subContent : String) : BaseItem() { abstract class ControllerItem(var content : String, var subContent : String) : BaseItem() {
/** lateinit var adapter : ControllerAdapter
* The underlying adapter this item is contained within
*/
var adapter : ControllerAdapter? = null
/**
* The position of this item in the adapter
*/
var position : Int? = null var position : Int? = null
/** /**
@ -42,7 +38,7 @@ abstract class ControllerItem(var content : String, var subContent : String) : B
if (subContent != null) if (subContent != null)
this.subContent = subContent this.subContent = subContent
position?.let { adapter?.notifyItemChanged(it) } position?.let { adapter.notifyItemChanged(it) }
} }
/** /**
@ -149,71 +145,45 @@ class ControllerStickItem(val context : ControllerActivity, val stick : StickId)
override fun update() = update(null, getSummary(context, stick)) override fun update() = update(null, getSummary(context, stick))
} }
class ControllerCheckBox()
/** /**
* This adapter is used to create a list which handles having a simple view * This adapter is used to create a list which handles having a simple view
*/ */
class ControllerAdapter(private val onItemClickCallback : (item : ControllerItem) -> Unit) : HeaderAdapter<ControllerItem?, BaseHeader, RecyclerView.ViewHolder>() { class ControllerAdapter(private val onItemClickCallback : (item : ControllerItem) -> Unit) : HeaderAdapter<ControllerItem?, BaseHeader, RecyclerView.ViewHolder>() {
/**
* This adds a header to the view with the contents of [string]
*/
fun addHeader(string : String) { fun addHeader(string : String) {
super.addHeader(BaseHeader(string)) super.addHeader(BaseHeader(string))
} }
/**
* This functions sets [ControllerItem.adapter] and delegates the call to [HeaderAdapter.addItem]
*/
fun addItem(item : ControllerItem) { fun addItem(item : ControllerItem) {
item.adapter = this item.adapter = this
super.addItem(item) super.addItem(item)
} }
/** private class ItemViewHolder(override val containerView : View) : RecyclerView.ViewHolder(containerView), LayoutContainer
* The ViewHolder used by items is used to hold the views associated with an item
*
* @param parent The parent view that contains all the others
* @param title The TextView associated with the title
* @param subtitle The TextView associated with the subtitle
* @param item The View containing the two other views
*/
class ItemViewHolder(val parent : View, var title : TextView, var subtitle : TextView, var item : View) : RecyclerView.ViewHolder(parent)
/** private class HeaderViewHolder(override val containerView : View) : RecyclerView.ViewHolder(containerView), LayoutContainer
* The ViewHolder used by headers is used to hold the views associated with an headers
*
* @param parent The parent view that contains all the others
* @param header The TextView associated with the header
*/
private class HeaderViewHolder(val parent : View, var header : TextView? = null) : RecyclerView.ViewHolder(parent)
/** override fun onCreateViewHolder(parent : ViewGroup, viewType : Int) : RecyclerView.ViewHolder = LayoutInflater.from(parent.context).let { layoutInflater ->
* This function creates the view-holder of type [viewType] with the layout parent as [parent] when (ElementType.values()[viewType]) {
*/ ElementType.Header -> HeaderViewHolder(layoutInflater.inflate(R.layout.section_item, parent, false))
override fun onCreateViewHolder(parent : ViewGroup, viewType : Int) = when (ElementType.values()[viewType]) {
ElementType.Header -> LayoutInflater.from(parent.context).inflate(R.layout.section_item, parent, false).let { view ->
HeaderViewHolder(view).apply { header = view.findViewById(R.id.text_title) }
}
ElementType.Item -> LayoutInflater.from(parent.context).inflate(R.layout.controller_item, parent, false).let { view -> ElementType.Item -> ItemViewHolder(layoutInflater.inflate(R.layout.controller_item, parent, false))
ItemViewHolder(view, view.findViewById(R.id.text_title), view.findViewById(R.id.text_subtitle), view.findViewById(R.id.controller_item))
} }
} }
/**
* This function binds the item at [position] to the supplied [holder]
*/
override fun onBindViewHolder(holder : RecyclerView.ViewHolder, position : Int) { override fun onBindViewHolder(holder : RecyclerView.ViewHolder, position : Int) {
val item = getItem(position) val item = getItem(position)
if (item is ControllerItem && holder is ItemViewHolder) { if (item is ControllerItem && holder is ItemViewHolder) {
item.position = position item.position = position
holder.title.text = item.content holder.text_title.text = item.content
holder.subtitle.text = item.subContent holder.text_subtitle.text = item.subContent
holder.parent.setOnClickListener { onItemClickCallback.invoke(item) } holder.itemView.setOnClickListener { onItemClickCallback.invoke(item) }
} else if (item is BaseHeader && holder is HeaderViewHolder) { } else if (item is BaseHeader && holder is HeaderViewHolder) {
holder.header?.text = item.title holder.text_title.text = item.title
} }
} }
} }

View File

@ -11,11 +11,12 @@ import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import emu.skyline.R import emu.skyline.R
import emu.skyline.data.BaseItem import emu.skyline.data.BaseItem
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.log_item.*
/** /**
* This class is used to hold all data about a log entry * This class is used to hold all data about a log entry
@ -55,22 +56,9 @@ internal class LogAdapter internal constructor(val context : Context, val compac
} }
} }
/** private class ItemViewHolder(override val containerView : View) : RecyclerView.ViewHolder(containerView), LayoutContainer
* The ViewHolder used by items is used to hold the views associated with an item
*
* @param parent The parent view that contains all the others
* @param title The TextView associated with the title
* @param subtitle The TextView associated with the subtitle
*/
private class ItemViewHolder(val parent : View, var title : TextView, var subtitle : TextView? = null) : RecyclerView.ViewHolder(parent)
/** private class HeaderViewHolder(override val containerView : View) : RecyclerView.ViewHolder(containerView), LayoutContainer
* The ViewHolder used by headers is used to hold the views associated with an headers
*
* @param parent The parent view that contains all the others
* @param header The TextView associated with the header
*/
private class HeaderViewHolder(val parent : View, var header : TextView) : RecyclerView.ViewHolder(parent)
/** /**
* This function creates the view-holder of type [viewType] with the layout parent as [parent] * This function creates the view-holder of type [viewType] with the layout parent as [parent]
@ -87,14 +75,14 @@ internal class LogAdapter internal constructor(val context : Context, val compac
return when (ElementType.values()[viewType]) { return when (ElementType.values()[viewType]) {
ElementType.Item -> { ElementType.Item -> {
if (compact) { if (compact) {
ItemViewHolder(view, view.findViewById(R.id.text_title)) ItemViewHolder(view)
} else { } else {
ItemViewHolder(view, view.findViewById(R.id.text_title), view.findViewById(R.id.text_subtitle)) ItemViewHolder(view)
} }
} }
ElementType.Header -> { ElementType.Header -> {
HeaderViewHolder(view, view.findViewById(R.id.text_title)) HeaderViewHolder(view)
} }
} }
} }
@ -106,15 +94,15 @@ internal class LogAdapter internal constructor(val context : Context, val compac
val item = getItem(position) val item = getItem(position)
if (item is LogItem && holder is ItemViewHolder) { if (item is LogItem && holder is ItemViewHolder) {
holder.title.text = item.message holder.text_title.text = item.message
holder.subtitle?.text = item.level holder.text_subtitle?.text = item.level
holder.parent.setOnClickListener { holder.itemView.setOnClickListener {
clipboard.setPrimaryClip(ClipData.newPlainText("Log Message", item.message + " (" + item.level + ")")) clipboard.setPrimaryClip(ClipData.newPlainText("Log Message", item.message + " (" + item.level + ")"))
Toast.makeText(holder.itemView.context, "Copied to clipboard", Toast.LENGTH_LONG).show() Toast.makeText(holder.itemView.context, "Copied to clipboard", Toast.LENGTH_LONG).show()
} }
} else if (item is BaseHeader && holder is HeaderViewHolder) { } else if (item is BaseHeader && holder is HeaderViewHolder) {
holder.header.text = item.title holder.text_title.text = item.title
} }
} }
} }

View File

@ -25,7 +25,7 @@ class ControllerActivity : AppCompatActivity() {
/** /**
* The index of the controller this activity manages * The index of the controller this activity manages
*/ */
var id : Int = -1 val id by lazy { intent.getIntExtra("index", 0) }
/** /**
* The adapter used by [controller_list] to hold all the items * The adapter used by [controller_list] to hold all the items
@ -55,6 +55,8 @@ class ControllerActivity : AppCompatActivity() {
if (controller.type == ControllerType.None) if (controller.type == ControllerType.None)
return return
var wroteTitle = false var wroteTitle = false
for (item in GeneralType.values()) { for (item in GeneralType.values()) {
@ -128,8 +130,6 @@ class ControllerActivity : AppCompatActivity() {
override fun onCreate(state : Bundle?) { override fun onCreate(state : Bundle?) {
super.onCreate(state) super.onCreate(state)
id = intent.getIntExtra("index", 0)
if (id < 0 || id > 7) if (id < 0 || id > 7)
throw IllegalArgumentException() throw IllegalArgumentException()
@ -159,7 +159,7 @@ class ControllerActivity : AppCompatActivity() {
is ControllerTypeItem -> { is ControllerTypeItem -> {
val controller = InputManager.controllers[id]!! val controller = InputManager.controllers[id]!!
val types = ControllerType.values().filter { !it.firstController || id == 0 } val types = ControllerType.values().apply { if (id != 0) filter { !it.firstController } }
val typeNames = types.map { getString(it.stringRes) }.toTypedArray() val typeNames = types.map { getString(it.stringRes) }.toTypedArray()
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(this)

View File

@ -87,7 +87,7 @@ class OnScreenControllerView @JvmOverloads constructor(
val outerToInner = joystick.outerToInner() val outerToInner = joystick.outerToInner()
val outerToInnerLength = outerToInner.length() val outerToInnerLength = outerToInner.length()
val direction = outerToInner.normalize() val direction = outerToInner.normalize()
val duration = (150f * outerToInnerLength / radius).roundToLong() val duration = (50f * outerToInnerLength / radius).roundToLong()
joystickAnimators[joystick] = ValueAnimator.ofFloat(outerToInnerLength, 0f).apply { joystickAnimators[joystick] = ValueAnimator.ofFloat(outerToInnerLength, 0f).apply {
addUpdateListener { animation -> addUpdateListener { animation ->
val value = animation.animatedValue as Float val value = animation.animatedValue as Float
@ -106,6 +106,8 @@ class OnScreenControllerView @JvmOverloads constructor(
override fun onAnimationEnd(animation : Animator?) { override fun onAnimationEnd(animation : Animator?) {
super.onAnimationEnd(animation) super.onAnimationEnd(animation)
if (joystick.shortDoubleTapped)
onButtonStateChangedListener?.invoke(joystick.buttonId, ButtonState.Released)
joystick.onFingerUp(event.x, event.y) joystick.onFingerUp(event.x, event.y)
invalidate() invalidate()
} }
@ -123,6 +125,8 @@ class OnScreenControllerView @JvmOverloads constructor(
joystickAnimators[joystick] = null joystickAnimators[joystick] = null
joystick.touchPointerId = pointerId joystick.touchPointerId = pointerId
joystick.onFingerDown(x, y) joystick.onFingerDown(x, y)
if (joystick.shortDoubleTapped)
onButtonStateChangedListener?.invoke(joystick.buttonId, ButtonState.Pressed)
performClick() performClick()
handled = true handled = true
} }
@ -139,7 +143,7 @@ class OnScreenControllerView @JvmOverloads constructor(
} }
} }
handled.also { if (it) invalidate() } handled.also { if (it) invalidate() else super.onTouchEvent(event) }
} }
private val editingTouchHandler = OnTouchListener { _, event -> private val editingTouchHandler = OnTouchListener { _, event ->
@ -169,7 +173,7 @@ class OnScreenControllerView @JvmOverloads constructor(
} }
} }
false false
}.also { handled -> if (handled) invalidate() } }.also { handled -> if (handled) invalidate() else super.onTouchEvent(event) }
} }
init { init {

View File

@ -7,6 +7,7 @@ package emu.skyline.input.onscreen
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.PointF import android.graphics.PointF
import android.os.SystemClock
import androidx.core.graphics.minus import androidx.core.graphics.minus
import emu.skyline.R import emu.skyline.R
import emu.skyline.input.ButtonId import emu.skyline.input.ButtonId
@ -59,6 +60,11 @@ class JoystickButton(
private val innerButton = CircularButton(onScreenControllerView, buttonId, config.relativeX, config.relativeY, defaultRelativeRadiusToX * 0.75f, R.drawable.ic_stick) private val innerButton = CircularButton(onScreenControllerView, buttonId, config.relativeX, config.relativeY, defaultRelativeRadiusToX * 0.75f, R.drawable.ic_stick)
private var fingerDownTime = 0L
private var fingerUpTime = 0L
var shortDoubleTapped = false
private set
override fun renderCenteredText(canvas : Canvas, text : String, size : Float, x : Float, y : Float) = Unit override fun renderCenteredText(canvas : Canvas, text : String, size : Float, x : Float, y : Float) = Unit
override fun render(canvas : Canvas) { override fun render(canvas : Canvas) {
@ -74,12 +80,23 @@ class JoystickButton(
relativeY = (y - heightDiff) / adjustedHeight relativeY = (y - heightDiff) / adjustedHeight
innerButton.relativeX = relativeX innerButton.relativeX = relativeX
innerButton.relativeY = relativeY innerButton.relativeY = relativeY
val currentTime = SystemClock.elapsedRealtime()
val firstTapDiff = fingerUpTime - fingerDownTime
val secondTapDiff = currentTime - fingerUpTime
if (firstTapDiff in 0..500 && secondTapDiff in 0..500) {
shortDoubleTapped = true
}
fingerDownTime = currentTime
} }
override fun onFingerUp(x : Float, y : Float) { override fun onFingerUp(x : Float, y : Float) {
loadConfigValues() loadConfigValues()
innerButton.relativeX = relativeX innerButton.relativeX = relativeX
innerButton.relativeY = relativeY innerButton.relativeY = relativeY
fingerUpTime = SystemClock.elapsedRealtime()
shortDoubleTapped = false
} }
fun onFingerMoved(x : Float, y : Float) : PointF { fun onFingerMoved(x : Float, y : Float) : PointF {
@ -91,6 +108,11 @@ class JoystickButton(
finger = position.add(outerToInner.multiply(1f / distance * radius)) finger = position.add(outerToInner.multiply(1f / distance * radius))
} }
if (distance > radius * 0.075f) {
fingerDownTime = 0
fingerUpTime = 0
}
innerButton.relativeX = finger.x / width innerButton.relativeX = finger.x / width
innerButton.relativeY = (finger.y - heightDiff) / adjustedHeight innerButton.relativeY = (finger.y - heightDiff) / adjustedHeight
return finger.minus(position).multiply(1f / radius) return finger.minus(position).multiply(1f / radius)
@ -100,6 +122,7 @@ class JoystickButton(
override fun edit(x : Float, y : Float) { override fun edit(x : Float, y : Float) {
super.edit(x, y) super.edit(x, y)
innerButton.relativeX = relativeX innerButton.relativeX = relativeX
innerButton.relativeY = relativeY innerButton.relativeY = relativeY
} }

View File

@ -6,7 +6,7 @@
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView
android:id="@+id/app_item_grid" android:id="@+id/card_app_item_grid"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"

View File

@ -6,7 +6,7 @@
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView
android:id="@+id/app_item_grid" android:id="@+id/card_app_item_grid"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"