mirror of
https://github.com/skyline-emu/skyline.git
synced 2024-12-27 15:25:38 +03:00
Redesign cards in grid view
* Refactor some classes and clean up * Refresh style on the fly
This commit is contained in:
parent
b23779bda1
commit
c3e54d1abf
@ -15,10 +15,11 @@ import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import emu.skyline.adapter.AppItem
|
||||
import emu.skyline.data.AppItem
|
||||
import kotlinx.android.synthetic.main.app_dialog.*
|
||||
|
||||
/**
|
||||
@ -26,7 +27,7 @@ import kotlinx.android.synthetic.main.app_dialog.*
|
||||
*
|
||||
* @param item This is used to hold the [AppItem] between instances
|
||||
*/
|
||||
class AppDialog(val item : AppItem? = null) : BottomSheetDialogFragment() {
|
||||
class AppDialog(val item : AppItem) : BottomSheetDialogFragment() {
|
||||
|
||||
/**
|
||||
* This inflates the layout of the dialog after initial view creation
|
||||
@ -47,10 +48,9 @@ class AppDialog(val item : AppItem? = null) : BottomSheetDialogFragment() {
|
||||
dialog?.setOnKeyListener { _, keyCode, event ->
|
||||
if (keyCode == KeyEvent.KEYCODE_BUTTON_B && event.action == KeyEvent.ACTION_DOWN) {
|
||||
dialog?.onBackPressed()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
return@setOnKeyListener true
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,38 +60,35 @@ class AppDialog(val item : AppItem? = null) : BottomSheetDialogFragment() {
|
||||
override fun onActivityCreated(savedInstanceState : Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
if (item is AppItem) {
|
||||
val missingIcon = context?.resources?.getDrawable(R.drawable.default_icon, context?.theme)?.toBitmap(256, 256)
|
||||
val missingIcon = ContextCompat.getDrawable(requireActivity(), R.drawable.default_icon)!!.toBitmap(256, 256)
|
||||
|
||||
game_icon.setImageBitmap(item.icon ?: missingIcon)
|
||||
game_title.text = item.title
|
||||
game_subtitle.text = item.subTitle ?: getString(R.string.metadata_missing)
|
||||
game_icon.setImageBitmap(item.icon ?: missingIcon)
|
||||
game_title.text = item.title
|
||||
game_subtitle.text = item.subTitle ?: getString(R.string.metadata_missing)
|
||||
|
||||
game_play.setOnClickListener {
|
||||
val intent = Intent(activity, EmulationActivity::class.java)
|
||||
intent.data = item.uri
|
||||
game_play.setOnClickListener {
|
||||
val intent = Intent(activity, EmulationActivity::class.java)
|
||||
intent.data = item.uri
|
||||
|
||||
startActivity(intent)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
val shortcutManager = activity?.getSystemService(ShortcutManager::class.java)!!
|
||||
game_pin.isEnabled = shortcutManager.isRequestPinShortcutSupported
|
||||
val shortcutManager = requireActivity().getSystemService(ShortcutManager::class.java)
|
||||
game_pin.isEnabled = shortcutManager.isRequestPinShortcutSupported
|
||||
|
||||
game_pin.setOnClickListener {
|
||||
val info = ShortcutInfo.Builder(context, item.title)
|
||||
info.setShortLabel(item.meta.name)
|
||||
info.setActivity(ComponentName(context!!, EmulationActivity::class.java))
|
||||
info.setIcon(Icon.createWithAdaptiveBitmap(item.icon ?: missingIcon))
|
||||
game_pin.setOnClickListener {
|
||||
val info = ShortcutInfo.Builder(context, item.title)
|
||||
info.setShortLabel(item.meta.name)
|
||||
info.setActivity(ComponentName(requireActivity(), EmulationActivity::class.java))
|
||||
info.setIcon(Icon.createWithAdaptiveBitmap(item.icon ?: missingIcon))
|
||||
|
||||
val intent = Intent(context, EmulationActivity::class.java)
|
||||
intent.data = item.uri
|
||||
intent.action = Intent.ACTION_VIEW
|
||||
val intent = Intent(context, EmulationActivity::class.java)
|
||||
intent.data = item.uri
|
||||
intent.action = Intent.ACTION_VIEW
|
||||
|
||||
info.setIntent(intent)
|
||||
info.setIntent(intent)
|
||||
|
||||
shortcutManager.requestPinShortcut(info.build(), null)
|
||||
}
|
||||
} else
|
||||
activity?.supportFragmentManager?.beginTransaction()?.remove(this)?.commit()
|
||||
shortcutManager.requestPinShortcut(info.build(), null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,9 +25,9 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import emu.skyline.adapter.AppAdapter
|
||||
import emu.skyline.adapter.AppItem
|
||||
import emu.skyline.adapter.GridLayoutSpan
|
||||
import emu.skyline.adapter.LayoutType
|
||||
import emu.skyline.data.AppItem
|
||||
import emu.skyline.loader.RomFile
|
||||
import emu.skyline.loader.RomFormat
|
||||
import kotlinx.android.synthetic.main.main_activity.*
|
||||
@ -37,7 +37,7 @@ import java.io.IOException
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.math.ceil
|
||||
|
||||
class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClickListener {
|
||||
class MainActivity : AppCompatActivity(), View.OnClickListener {
|
||||
/**
|
||||
* This is used to get/set shared preferences
|
||||
*/
|
||||
@ -51,14 +51,14 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClick
|
||||
/**
|
||||
* This adds all files in [directory] with [extension] as an entry in [adapter] using [loader] to load metadata
|
||||
*/
|
||||
private fun addEntries(extension : String, romFormat : RomFormat, directory : DocumentFile, found : Boolean = false) : Boolean {
|
||||
private fun addEntries(romFormat : RomFormat, directory : DocumentFile, found : Boolean = false) : Boolean {
|
||||
var foundCurrent = found
|
||||
|
||||
directory.listFiles().forEach { file ->
|
||||
if (file.isDirectory) {
|
||||
foundCurrent = addEntries(extension, romFormat, file, foundCurrent)
|
||||
foundCurrent = addEntries(romFormat, file, foundCurrent)
|
||||
} else {
|
||||
if (extension.equals(file.name?.substringAfterLast("."), ignoreCase = true)) {
|
||||
if (romFormat.extension.equals(file.name?.substringAfterLast("."), ignoreCase = true)) {
|
||||
val romFd = contentResolver.openFileDescriptor(file.uri, "r")!!
|
||||
val romFile = RomFile(this, romFormat, romFd)
|
||||
|
||||
@ -111,14 +111,17 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClick
|
||||
try {
|
||||
runOnUiThread { adapter.clear() }
|
||||
|
||||
var foundRoms = addEntries("nro", RomFormat.NRO, DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!)
|
||||
foundRoms = foundRoms or addEntries("nso", RomFormat.NSO, DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!)
|
||||
foundRoms = foundRoms or addEntries("nca", RomFormat.NCA, DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!)
|
||||
foundRoms = foundRoms or addEntries("nsp", RomFormat.NSP, DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!)
|
||||
val searchLocation = DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!
|
||||
|
||||
var foundRoms = addEntries(RomFormat.NRO, searchLocation)
|
||||
foundRoms = foundRoms or addEntries(RomFormat.NSO, searchLocation)
|
||||
foundRoms = foundRoms or addEntries(RomFormat.NCA, searchLocation)
|
||||
foundRoms = foundRoms or addEntries(RomFormat.NSP, searchLocation)
|
||||
|
||||
runOnUiThread {
|
||||
if (!foundRoms)
|
||||
if (!foundRoms) {
|
||||
adapter.addHeader(getString(R.string.no_rom))
|
||||
}
|
||||
|
||||
try {
|
||||
adapter.save(File("${applicationInfo.dataDir}/roms.bin"))
|
||||
@ -172,28 +175,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClick
|
||||
open_fab.setOnClickListener(this)
|
||||
log_fab.setOnClickListener(this)
|
||||
|
||||
val layoutType = LayoutType.values()[sharedPreferences.getString("layout_type", "1")!!.toInt()]
|
||||
|
||||
adapter = AppAdapter(this, layoutType)
|
||||
app_list.adapter = adapter
|
||||
|
||||
when (layoutType) {
|
||||
LayoutType.List -> {
|
||||
app_list.layoutManager = LinearLayoutManager(this)
|
||||
app_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL))
|
||||
}
|
||||
|
||||
LayoutType.Grid -> {
|
||||
val itemWidth = 225
|
||||
val metrics = resources.displayMetrics
|
||||
val span = ceil((metrics.widthPixels / metrics.density) / itemWidth).toInt()
|
||||
|
||||
val layoutManager = GridLayoutManager(this, span)
|
||||
layoutManager.spanSizeLookup = GridLayoutSpan(adapter, span)
|
||||
|
||||
app_list.layoutManager = layoutManager
|
||||
}
|
||||
}
|
||||
setupAppList()
|
||||
|
||||
app_list.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
var y : Int = 0
|
||||
@ -202,23 +184,43 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClick
|
||||
y += dy
|
||||
|
||||
if (!app_list.isInTouchMode) {
|
||||
if (y == 0)
|
||||
toolbar_layout.setExpanded(true)
|
||||
else
|
||||
toolbar_layout.setExpanded(false)
|
||||
toolbar_layout.setExpanded(y == 0)
|
||||
}
|
||||
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun setupAppList() {
|
||||
val itemWidth = 225
|
||||
val metrics = resources.displayMetrics
|
||||
val gridSpan = ceil((metrics.widthPixels / metrics.density) / itemWidth).toInt()
|
||||
|
||||
val layoutType = LayoutType.values()[sharedPreferences.getString("layout_type", "1")!!.toInt()]
|
||||
|
||||
adapter = AppAdapter(layoutType = layoutType, gridSpan = gridSpan, onClick = selectStartGame, onLongClick = selectShowGameDialog)
|
||||
app_list.adapter = adapter
|
||||
|
||||
app_list.layoutManager = when (layoutType) {
|
||||
LayoutType.List -> LinearLayoutManager(this).also { app_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL)) }
|
||||
LayoutType.Grid, LayoutType.GridCompact -> GridLayoutManager(this, gridSpan).apply {
|
||||
spanSizeLookup = GridLayoutSpan(adapter, gridSpan).also {
|
||||
if (app_list.itemDecorationCount > 0) {
|
||||
app_list.removeItemDecorationAt(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sharedPreferences.getString("search_location", "") == "") {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||
intent.flags = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
|
||||
startActivityForResult(intent, 1)
|
||||
} else
|
||||
} else {
|
||||
refreshAdapter(!sharedPreferences.getBoolean("refresh_required", false))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -245,12 +247,11 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClick
|
||||
}
|
||||
|
||||
/**
|
||||
* This handles on-click interaction with [R.id.log_fab], [R.id.open_fab], [R.id.app_item_linear] and [R.id.app_item_grid]
|
||||
* This handles on-click interaction with [R.id.log_fab], [R.id.open_fab]
|
||||
*/
|
||||
override fun onClick(view : View) {
|
||||
when (view.id) {
|
||||
R.id.log_fab -> startActivity(Intent(this, LogActivity::class.java))
|
||||
|
||||
R.id.open_fab -> {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
@ -258,40 +259,19 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClick
|
||||
|
||||
startActivityForResult(intent, 2)
|
||||
}
|
||||
|
||||
R.id.app_item_linear, R.id.app_item_grid -> {
|
||||
val tag = view.tag
|
||||
if (tag is AppItem) {
|
||||
if (sharedPreferences.getBoolean("select_action", false)) {
|
||||
val dialog = AppDialog(tag)
|
||||
dialog.show(supportFragmentManager, "game")
|
||||
} else {
|
||||
val intent = Intent(this, EmulationActivity::class.java)
|
||||
intent.data = tag.uri
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This handles long-click interaction with [R.id.app_item_linear] and [R.id.app_item_grid]
|
||||
*/
|
||||
override fun onLongClick(view : View?) : Boolean {
|
||||
when (view?.id) {
|
||||
R.id.app_item_linear, R.id.app_item_grid -> {
|
||||
val tag = view.tag
|
||||
if (tag is AppItem) {
|
||||
val dialog = AppDialog(tag)
|
||||
dialog.show(supportFragmentManager, "game")
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
private val selectStartGame : (appItem : AppItem) -> Unit = {
|
||||
if (sharedPreferences.getBoolean("select_action", false)) {
|
||||
AppDialog(it).show(supportFragmentManager, "game")
|
||||
} else {
|
||||
startActivity(Intent(this, EmulationActivity::class.java).apply { data = it.uri })
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
private val selectShowGameDialog : (appItem : AppItem) -> Unit = {
|
||||
AppDialog(it).show(supportFragmentManager, "game")
|
||||
}
|
||||
|
||||
/**
|
||||
@ -350,4 +330,13 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClick
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
val layoutType = LayoutType.values()[sharedPreferences.getString("layout_type", "1")!!.toInt()]
|
||||
if (layoutType != adapter.layoutType) {
|
||||
setupAppList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,80 +7,42 @@ package emu.skyline.adapter
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import emu.skyline.R
|
||||
import emu.skyline.adapter.ElementType.Header
|
||||
import emu.skyline.adapter.ElementType.Item
|
||||
import emu.skyline.loader.AppEntry
|
||||
|
||||
/**
|
||||
* This class is a wrapper around [AppEntry], it is used for passing around game metadata
|
||||
*/
|
||||
class AppItem(val meta : AppEntry) : BaseItem() {
|
||||
/**
|
||||
* The icon of the application
|
||||
*/
|
||||
val icon : Bitmap?
|
||||
get() = meta.icon
|
||||
|
||||
/**
|
||||
* The title of the application
|
||||
*/
|
||||
val title : String
|
||||
get() = meta.name
|
||||
|
||||
/**
|
||||
* The string used as the sub-title, we currently use the author
|
||||
*/
|
||||
val subTitle : String?
|
||||
get() = meta.author
|
||||
|
||||
/**
|
||||
* The URI of the application's image file
|
||||
*/
|
||||
val uri : Uri
|
||||
get() = meta.uri
|
||||
|
||||
/**
|
||||
* The format of the application ROM as a string
|
||||
*/
|
||||
private val type : String
|
||||
get() = meta.format.name
|
||||
|
||||
/**
|
||||
* The name and author is used as the key
|
||||
*/
|
||||
override fun key() : String? {
|
||||
return if (meta.author != null) meta.name + " " + meta.author else meta.name
|
||||
}
|
||||
}
|
||||
import emu.skyline.data.AppItem
|
||||
|
||||
/**
|
||||
* This enumerates the type of layouts the menu can be in
|
||||
*/
|
||||
enum class LayoutType {
|
||||
List,
|
||||
Grid,
|
||||
enum class LayoutType(val layoutRes : Int) {
|
||||
List(R.layout.app_item_linear),
|
||||
Grid(R.layout.app_item_grid),
|
||||
GridCompact(R.layout.app_item_grid_compact)
|
||||
}
|
||||
|
||||
private typealias AppInteractionFunc = (appItem : AppItem) -> Unit
|
||||
|
||||
/**
|
||||
* This adapter is used to display all found applications using their metadata
|
||||
*/
|
||||
internal class AppAdapter(val context : Context?, private val layoutType : LayoutType) : HeaderAdapter<AppItem, BaseHeader, RecyclerView.ViewHolder>(), View.OnClickListener {
|
||||
private val missingIcon = context?.resources?.getDrawable(R.drawable.default_icon, context.theme)?.toBitmap(256, 256)
|
||||
private val missingString = context?.getString(R.string.metadata_missing)
|
||||
internal class AppAdapter(val layoutType : LayoutType, private val gridSpan : Int, private val onClick : AppInteractionFunc, private val onLongClick : AppInteractionFunc) : HeaderAdapter<AppItem, BaseHeader, RecyclerView.ViewHolder>() {
|
||||
|
||||
private lateinit var context : Context
|
||||
private val missingIcon by lazy { ContextCompat.getDrawable(context, R.drawable.default_icon)!!.toBitmap(256, 256) }
|
||||
private val missingString by lazy { context.getString(R.string.metadata_missing) }
|
||||
|
||||
/**
|
||||
* This adds a header to the view with the contents of [string]
|
||||
@ -89,29 +51,6 @@ internal class AppAdapter(val context : Context?, private val layoutType : Layou
|
||||
super.addHeader(BaseHeader(string))
|
||||
}
|
||||
|
||||
/**
|
||||
* The onClick handler for the supplied [view], used for the icon preview
|
||||
*/
|
||||
override fun onClick(view : View) {
|
||||
val position = view.tag as Int
|
||||
|
||||
if (getItem(position) is AppItem) {
|
||||
val item = getItem(position) as AppItem
|
||||
|
||||
if (view.id == R.id.icon) {
|
||||
val builder = Dialog(context!!)
|
||||
builder.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
builder.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
|
||||
val imageView = ImageView(context)
|
||||
imageView.setImageBitmap(item.icon ?: missingIcon)
|
||||
|
||||
builder.addContentView(imageView, RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
|
||||
builder.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The ViewHolder used by items is used to hold the views associated with an item
|
||||
*
|
||||
@ -134,38 +73,49 @@ internal class AppAdapter(val context : Context?, private val layoutType : Layou
|
||||
* This function creates the view-holder of type [viewType] with the layout parent as [parent]
|
||||
*/
|
||||
override fun onCreateViewHolder(parent : ViewGroup, viewType : Int) : RecyclerView.ViewHolder {
|
||||
context = parent.context
|
||||
|
||||
val inflater = LayoutInflater.from(context)
|
||||
var holder : RecyclerView.ViewHolder? = null
|
||||
|
||||
if (viewType == Item.ordinal) {
|
||||
val view = inflater.inflate(if (layoutType == LayoutType.List) R.layout.app_item_linear else R.layout.app_item_grid, parent, false)
|
||||
holder = ItemViewHolder(view, view.findViewById(R.id.icon), view.findViewById(R.id.text_title), view.findViewById(R.id.text_subtitle))
|
||||
|
||||
if (layoutType == LayoutType.List) {
|
||||
if (context is View.OnClickListener)
|
||||
view.setOnClickListener(context as View.OnClickListener)
|
||||
|
||||
if (context is View.OnLongClickListener)
|
||||
view.setOnLongClickListener(context as View.OnLongClickListener)
|
||||
} else {
|
||||
holder.card = view.findViewById(R.id.app_item_grid)
|
||||
|
||||
if (context is View.OnClickListener)
|
||||
holder.card!!.setOnClickListener(context as View.OnClickListener)
|
||||
|
||||
if (context is View.OnLongClickListener)
|
||||
holder.card!!.setOnLongClickListener(context as View.OnLongClickListener)
|
||||
|
||||
holder.title.isSelected = true
|
||||
}
|
||||
} else if (viewType == Header.ordinal) {
|
||||
val view = inflater.inflate(R.layout.section_item, parent, false)
|
||||
holder = HeaderViewHolder(view)
|
||||
|
||||
holder.header = view.findViewById(R.id.text_title)
|
||||
val view = when (ElementType.values()[viewType]) {
|
||||
ElementType.Item -> inflater.inflate(layoutType.layoutRes, parent, false)
|
||||
ElementType.Header -> inflater.inflate(R.layout.section_item, parent, false)
|
||||
}
|
||||
|
||||
return holder!!
|
||||
Log.i("blaa", "onCreateViewHolder")
|
||||
|
||||
return when (ElementType.values()[viewType]) {
|
||||
ElementType.Item -> {
|
||||
ItemViewHolder(view, view.findViewById(R.id.icon), view.findViewById(R.id.text_title), view.findViewById(R.id.text_subtitle)).apply {
|
||||
if (layoutType == LayoutType.List) {
|
||||
view.apply {
|
||||
if (context is View.OnClickListener) {
|
||||
setOnClickListener(context as View.OnClickListener)
|
||||
}
|
||||
if (context is View.OnLongClickListener) {
|
||||
setOnLongClickListener(context as View.OnLongClickListener)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
card = view.findViewById(R.id.app_item_grid)
|
||||
card!!.apply {
|
||||
if (context is View.OnClickListener) {
|
||||
setOnClickListener(context as View.OnClickListener)
|
||||
}
|
||||
if (context is View.OnLongClickListener) {
|
||||
setOnLongClickListener(context as View.OnLongClickListener)
|
||||
}
|
||||
}
|
||||
|
||||
title.isSelected = true
|
||||
}
|
||||
}
|
||||
}
|
||||
ElementType.Header -> {
|
||||
HeaderViewHolder(view).apply {
|
||||
header = view.findViewById(R.id.text_title)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -183,16 +133,45 @@ internal class AppAdapter(val context : Context?, private val layoutType : Layou
|
||||
holder.icon.setImageBitmap(item.icon ?: missingIcon)
|
||||
|
||||
if (layoutType == LayoutType.List) {
|
||||
holder.icon.setOnClickListener(this)
|
||||
holder.icon.tag = position
|
||||
holder.icon.setOnClickListener { showIconDialog(item) }
|
||||
}
|
||||
|
||||
holder.card?.tag = item
|
||||
holder.parent.tag = item
|
||||
when (layoutType) {
|
||||
LayoutType.List -> holder.itemView
|
||||
LayoutType.Grid, LayoutType.GridCompact -> holder.card!!
|
||||
}.apply {
|
||||
setOnClickListener { onClick.invoke(item) }
|
||||
setOnLongClickListener { true.also { onLongClick.invoke(item) } }
|
||||
}
|
||||
|
||||
// Increase margin of edges to avoid huge gap in between items
|
||||
if (layoutType == LayoutType.Grid || layoutType == LayoutType.GridCompact) {
|
||||
|
||||
holder.itemView.layoutParams = LinearLayout.LayoutParams(holder.itemView.layoutParams.width, holder.itemView.layoutParams.height).apply {
|
||||
if (position % gridSpan == 0) {
|
||||
marginStart = holder.itemView.resources.getDimensionPixelSize(R.dimen.app_card_margin) * 2
|
||||
} else if (position % gridSpan == gridSpan - 1) {
|
||||
marginEnd = holder.itemView.resources.getDimensionPixelSize(R.dimen.app_card_margin) * 2
|
||||
}
|
||||
}
|
||||
holder.itemView.requestLayout()
|
||||
}
|
||||
} else if (item is BaseHeader) {
|
||||
val holder = viewHolder as HeaderViewHolder
|
||||
|
||||
holder.header!!.text = item.title
|
||||
}
|
||||
}
|
||||
|
||||
private fun showIconDialog(appItem : AppItem) {
|
||||
val builder = Dialog(context)
|
||||
builder.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
builder.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
|
||||
val imageView = ImageView(context)
|
||||
imageView.setImageBitmap(appItem.icon ?: missingIcon)
|
||||
|
||||
builder.addContentView(imageView, RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
|
||||
builder.show()
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import android.widget.Filter
|
||||
import android.widget.Filterable
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import emu.skyline.data.BaseItem
|
||||
import info.debatty.java.stringsimilarity.Cosine
|
||||
import info.debatty.java.stringsimilarity.JaroWinkler
|
||||
import java.io.*
|
||||
@ -35,16 +36,6 @@ abstract class BaseElement constructor(val elementType : ElementType) : Serializ
|
||||
*/
|
||||
class BaseHeader constructor(val title : String) : BaseElement(ElementType.Header)
|
||||
|
||||
/**
|
||||
* This is an abstract class that all adapter item classes inherit from
|
||||
*/
|
||||
abstract class BaseItem : BaseElement(ElementType.Item) {
|
||||
/**
|
||||
* This function returns a string used for searching
|
||||
*/
|
||||
abstract fun key() : String?
|
||||
}
|
||||
|
||||
/**
|
||||
* This adapter has the ability to have 2 types of elements specifically headers and items
|
||||
*/
|
||||
@ -69,9 +60,9 @@ abstract class HeaderAdapter<ItemType : BaseItem?, HeaderType : BaseHeader?, Vie
|
||||
*/
|
||||
fun addItem(item : ItemType) {
|
||||
elementArray.add(item)
|
||||
if (searchTerm.isNotEmpty())
|
||||
if (searchTerm.isNotEmpty()) {
|
||||
filter.filter(searchTerm)
|
||||
else {
|
||||
} else {
|
||||
visibleArray.add(elementArray.size - 1)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
@ -146,93 +137,91 @@ abstract class HeaderAdapter<ItemType : BaseItem?, HeaderType : BaseHeader?, Vie
|
||||
/**
|
||||
* This returns an instance of the filter object which is used to search for items in the view
|
||||
*/
|
||||
override fun getFilter() : Filter {
|
||||
return object : Filter() {
|
||||
/**
|
||||
* We use Jaro-Winkler distance for string similarity (https://en.wikipedia.org/wiki/Jaro%E2%80%93Winkler_distance)
|
||||
*/
|
||||
private val jw = JaroWinkler()
|
||||
override fun getFilter() : Filter = object : Filter() {
|
||||
/**
|
||||
* We use Jaro-Winkler distance for string similarity (https://en.wikipedia.org/wiki/Jaro%E2%80%93Winkler_distance)
|
||||
*/
|
||||
private val jw = JaroWinkler()
|
||||
|
||||
/**
|
||||
* We use Cosine similarity for string similarity (https://en.wikipedia.org/wiki/Cosine_similarity)
|
||||
*/
|
||||
private val cos = Cosine()
|
||||
/**
|
||||
* We use Cosine similarity for string similarity (https://en.wikipedia.org/wiki/Cosine_similarity)
|
||||
*/
|
||||
private val cos = Cosine()
|
||||
|
||||
/**
|
||||
* This class is used to store the results of the item sorting
|
||||
*
|
||||
* @param score The score of this result
|
||||
* @param index The index of this item
|
||||
*/
|
||||
inner class ScoredItem(val score : Double, val index : Int) {}
|
||||
/**
|
||||
* This class is used to store the results of the item sorting
|
||||
*
|
||||
* @param score The score of this result
|
||||
* @param index The index of this item
|
||||
*/
|
||||
inner class ScoredItem(val score : Double, val index : Int) {}
|
||||
|
||||
/**
|
||||
* This sorts the items in [keyArray] in relation to how similar they are to [term]
|
||||
*/
|
||||
fun extractSorted(term : String, keyArray : ArrayList<String>) : Array<ScoredItem> {
|
||||
val scoredItems : MutableList<ScoredItem> = ArrayList()
|
||||
/**
|
||||
* This sorts the items in [keyArray] in relation to how similar they are to [term]
|
||||
*/
|
||||
fun extractSorted(term : String, keyArray : ArrayList<String>) : Array<ScoredItem> {
|
||||
val scoredItems : MutableList<ScoredItem> = ArrayList()
|
||||
|
||||
keyArray.forEachIndexed { index, item ->
|
||||
val similarity = (jw.similarity(term, item) + cos.similarity(term, item)) / 2
|
||||
keyArray.forEachIndexed { index, item ->
|
||||
val similarity = (jw.similarity(term, item) + cos.similarity(term, item)) / 2
|
||||
|
||||
if (similarity != 0.0)
|
||||
scoredItems.add(ScoredItem(similarity, index))
|
||||
}
|
||||
|
||||
scoredItems.sortWith(compareByDescending { it.score })
|
||||
|
||||
return scoredItems.toTypedArray()
|
||||
if (similarity != 0.0)
|
||||
scoredItems.add(ScoredItem(similarity, index))
|
||||
}
|
||||
|
||||
/**
|
||||
* This performs filtering on the items in [elementArray] based on similarity to [term]
|
||||
*/
|
||||
override fun performFiltering(term : CharSequence) : FilterResults {
|
||||
val results = FilterResults()
|
||||
searchTerm = (term as String).toLowerCase(Locale.getDefault())
|
||||
scoredItems.sortWith(compareByDescending { it.score })
|
||||
|
||||
if (term.isEmpty()) {
|
||||
results.values = elementArray.indices.toMutableList()
|
||||
results.count = elementArray.size
|
||||
} else {
|
||||
val filterData = ArrayList<Int>()
|
||||
return scoredItems.toTypedArray()
|
||||
}
|
||||
|
||||
val keyArray = ArrayList<String>()
|
||||
val keyIndex = SparseIntArray()
|
||||
/**
|
||||
* This performs filtering on the items in [elementArray] based on similarity to [term]
|
||||
*/
|
||||
override fun performFiltering(term : CharSequence) : FilterResults {
|
||||
val results = FilterResults()
|
||||
searchTerm = (term as String).toLowerCase(Locale.getDefault())
|
||||
|
||||
for (index in elementArray.indices) {
|
||||
val item = elementArray[index]!!
|
||||
if (term.isEmpty()) {
|
||||
results.values = elementArray.indices.toMutableList()
|
||||
results.count = elementArray.size
|
||||
} else {
|
||||
val filterData = ArrayList<Int>()
|
||||
|
||||
if (item is BaseItem) {
|
||||
keyIndex.append(keyArray.size, index)
|
||||
keyArray.add(item.key()!!.toLowerCase(Locale.getDefault()))
|
||||
}
|
||||
val keyArray = ArrayList<String>()
|
||||
val keyIndex = SparseIntArray()
|
||||
|
||||
for (index in elementArray.indices) {
|
||||
val item = elementArray[index]!!
|
||||
|
||||
if (item is BaseItem) {
|
||||
keyIndex.append(keyArray.size, index)
|
||||
keyArray.add(item.key()!!.toLowerCase(Locale.getDefault()))
|
||||
}
|
||||
|
||||
val topResults = extractSorted(searchTerm, keyArray)
|
||||
val avgScore = topResults.sumByDouble { it.score } / topResults.size
|
||||
|
||||
for (result in topResults)
|
||||
if (result.score > avgScore)
|
||||
filterData.add(keyIndex[result.index])
|
||||
|
||||
results.values = filterData
|
||||
results.count = filterData.size
|
||||
}
|
||||
|
||||
return results
|
||||
val topResults = extractSorted(searchTerm, keyArray)
|
||||
val avgScore = topResults.sumByDouble { it.score } / topResults.size
|
||||
|
||||
for (result in topResults)
|
||||
if (result.score > avgScore)
|
||||
filterData.add(keyIndex[result.index])
|
||||
|
||||
results.values = filterData
|
||||
results.count = filterData.size
|
||||
}
|
||||
|
||||
/**
|
||||
* This publishes the results that were calculated in [performFiltering] to the view
|
||||
*/
|
||||
override fun publishResults(charSequence : CharSequence, results : FilterResults) {
|
||||
if (results.values is ArrayList<*>) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
visibleArray = results.values as ArrayList<Int>
|
||||
return results
|
||||
}
|
||||
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
/**
|
||||
* This publishes the results that were calculated in [performFiltering] to the view
|
||||
*/
|
||||
override fun publishResults(charSequence : CharSequence, results : FilterResults) {
|
||||
if (results.values is ArrayList<*>) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
visibleArray = results.values as ArrayList<Int>
|
||||
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -244,7 +233,7 @@ abstract class HeaderAdapter<ItemType : BaseItem?, HeaderType : BaseHeader?, Vie
|
||||
* @param adapter The adapter which is used to deduce the type of the item based on the position
|
||||
* @param headerSpan The span size to return for headers
|
||||
*/
|
||||
class GridLayoutSpan<ItemType : BaseItem?, HeaderType : BaseHeader?, ViewHolder : RecyclerView.ViewHolder?>(val adapter : HeaderAdapter<ItemType, HeaderType, ViewHolder>, var headerSpan : Int) : GridLayoutManager.SpanSizeLookup() {
|
||||
class GridLayoutSpan<ItemType : BaseItem?, HeaderType : BaseHeader?, ViewHolder : RecyclerView.ViewHolder?>(val adapter : HeaderAdapter<ItemType, HeaderType, ViewHolder>, private val headerSpan : Int) : GridLayoutManager.SpanSizeLookup() {
|
||||
/**
|
||||
* This returns the size of the span based on the type of the element at [position]
|
||||
*/
|
||||
|
@ -10,12 +10,12 @@ import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.View.OnLongClickListener
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import emu.skyline.R
|
||||
import emu.skyline.data.BaseItem
|
||||
|
||||
/**
|
||||
* This class is used to hold all data about a log entry
|
||||
@ -32,7 +32,7 @@ internal class LogItem(val message : String, val level : String) : BaseItem() {
|
||||
/**
|
||||
* This adapter is used for displaying logs outputted by the application
|
||||
*/
|
||||
internal class LogAdapter internal constructor(val context : Context, val compact : Boolean, private val debug_level : Int, private val level_str : Array<String>) : HeaderAdapter<LogItem, BaseHeader, RecyclerView.ViewHolder>(), OnLongClickListener {
|
||||
internal class LogAdapter internal constructor(val context : Context, val compact : Boolean, private val debug_level : Int, private val level_str : Array<String>) : HeaderAdapter<LogItem, BaseHeader, RecyclerView.ViewHolder>() {
|
||||
private val clipboard : ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
|
||||
/**
|
||||
@ -72,41 +72,33 @@ internal class LogAdapter internal constructor(val context : Context, val compac
|
||||
*/
|
||||
private class HeaderViewHolder(val parent : View, var header : TextView) : RecyclerView.ViewHolder(parent)
|
||||
|
||||
/**
|
||||
* The onLongClick handler for the supplied [view], used to copy a log into the clipboard
|
||||
*/
|
||||
override fun onLongClick(view : View) : Boolean {
|
||||
val item = view.tag as LogItem
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText("Log Message", item.message + " (" + item.level + ")"))
|
||||
Toast.makeText(view.context, "Copied to clipboard", Toast.LENGTH_LONG).show()
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* This function creates the view-holder of type [viewType] with the layout parent as [parent]
|
||||
*/
|
||||
override fun onCreateViewHolder(parent : ViewGroup, viewType : Int) : RecyclerView.ViewHolder {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
var holder : RecyclerView.ViewHolder? = null
|
||||
|
||||
if (viewType == ElementType.Item.ordinal) {
|
||||
if (compact) {
|
||||
val view = inflater.inflate(R.layout.log_item_compact, parent, false)
|
||||
holder = ItemViewHolder(view, view.findViewById(R.id.text_title))
|
||||
|
||||
view.setOnLongClickListener(this)
|
||||
} else {
|
||||
val view = inflater.inflate(R.layout.log_item, parent, false)
|
||||
holder = ItemViewHolder(view, view.findViewById(R.id.text_title), view.findViewById(R.id.text_subtitle))
|
||||
|
||||
view.setOnLongClickListener(this)
|
||||
val view = when (ElementType.values()[viewType]) {
|
||||
ElementType.Item -> {
|
||||
inflater.inflate(if (compact) R.layout.log_item_compact else R.layout.log_item, parent, false)
|
||||
}
|
||||
ElementType.Header -> {
|
||||
inflater.inflate(R.layout.log_item, parent, false)
|
||||
}
|
||||
} else if (viewType == ElementType.Header.ordinal) {
|
||||
val view = inflater.inflate(R.layout.section_item, parent, false)
|
||||
holder = HeaderViewHolder(view, view.findViewById(R.id.text_title))
|
||||
}
|
||||
|
||||
return holder!!
|
||||
return when (ElementType.values()[viewType]) {
|
||||
ElementType.Item -> {
|
||||
if (compact) {
|
||||
ItemViewHolder(view, view.findViewById(R.id.text_title))
|
||||
} else {
|
||||
ItemViewHolder(view, view.findViewById(R.id.text_title), view.findViewById(R.id.text_subtitle))
|
||||
}
|
||||
}
|
||||
ElementType.Header -> {
|
||||
HeaderViewHolder(view, view.findViewById(R.id.text_title))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -121,7 +113,10 @@ internal class LogAdapter internal constructor(val context : Context, val compac
|
||||
holder.title.text = item.message
|
||||
holder.subtitle?.text = item.level
|
||||
|
||||
holder.parent.tag = item
|
||||
holder.parent.setOnClickListener {
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText("Log Message", item.message + " (" + item.level + ")"))
|
||||
Toast.makeText(holder.itemView.context, "Copied to clipboard", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
} else if (item is BaseHeader) {
|
||||
val holder = viewHolder as HeaderViewHolder
|
||||
|
||||
|
52
app/src/main/java/emu/skyline/data/AppItem.kt
Normal file
52
app/src/main/java/emu/skyline/data/AppItem.kt
Normal file
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.data
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import emu.skyline.loader.AppEntry
|
||||
|
||||
/**
|
||||
* This class is a wrapper around [AppEntry], it is used for passing around game metadata
|
||||
*/
|
||||
class AppItem(val meta : AppEntry) : BaseItem() {
|
||||
/**
|
||||
* The icon of the application
|
||||
*/
|
||||
val icon : Bitmap?
|
||||
get() = meta.icon
|
||||
|
||||
/**
|
||||
* The title of the application
|
||||
*/
|
||||
val title : String
|
||||
get() = meta.name
|
||||
|
||||
/**
|
||||
* The string used as the sub-title, we currently use the author
|
||||
*/
|
||||
val subTitle : String?
|
||||
get() = meta.author
|
||||
|
||||
/**
|
||||
* The URI of the application's image file
|
||||
*/
|
||||
val uri : Uri
|
||||
get() = meta.uri
|
||||
|
||||
/**
|
||||
* The format of the application ROM as a string
|
||||
*/
|
||||
private val type : String
|
||||
get() = meta.format.name
|
||||
|
||||
/**
|
||||
* The name and author is used as the key
|
||||
*/
|
||||
override fun key() : String? {
|
||||
return if (meta.author != null) meta.name + " " + meta.author else meta.name
|
||||
}
|
||||
}
|
19
app/src/main/java/emu/skyline/data/BaseItem.kt
Normal file
19
app/src/main/java/emu/skyline/data/BaseItem.kt
Normal file
@ -0,0 +1,19 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.data
|
||||
|
||||
import emu.skyline.adapter.BaseElement
|
||||
import emu.skyline.adapter.ElementType
|
||||
|
||||
/**
|
||||
* This is an abstract class that all adapter item classes inherit from
|
||||
*/
|
||||
abstract class BaseItem : BaseElement(ElementType.Item) {
|
||||
/**
|
||||
* This function returns a string used for searching
|
||||
*/
|
||||
abstract fun key() : String?
|
||||
}
|
@ -13,7 +13,6 @@ import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.provider.OpenableColumns
|
||||
import android.view.Surface
|
||||
import java.io.IOException
|
||||
import java.io.ObjectInputStream
|
||||
import java.io.ObjectOutputStream
|
||||
@ -23,12 +22,12 @@ import java.util.*
|
||||
/**
|
||||
* An enumeration of all supported ROM formats
|
||||
*/
|
||||
enum class RomFormat(val format: Int){
|
||||
NRO(0),
|
||||
NSO(1),
|
||||
NCA(2),
|
||||
XCI(3),
|
||||
NSP(4),
|
||||
enum class RomFormat(val extension : String) {
|
||||
NRO("nro"),
|
||||
NSO("nso"),
|
||||
NCA("nca"),
|
||||
XCI("xci"),
|
||||
NSP("nsp"),
|
||||
}
|
||||
|
||||
/**
|
||||
@ -185,7 +184,7 @@ internal class RomFile(val context : Context, val format : RomFormat, val file :
|
||||
fun getAppEntry(uri : Uri) : AppEntry {
|
||||
return if (hasAssets(instance)) {
|
||||
val rawIcon = getIcon(instance)
|
||||
val icon = if (rawIcon.size != 0) BitmapFactory.decodeByteArray(rawIcon, 0, rawIcon.size) else null
|
||||
val icon = if (rawIcon.isNotEmpty()) BitmapFactory.decodeByteArray(rawIcon, 0, rawIcon.size) else null
|
||||
|
||||
AppEntry(getApplicationName(instance), getApplicationPublisher(instance), format, uri, icon)
|
||||
} else {
|
||||
|
9
app/src/main/res/drawable/background_gradient.xml
Normal file
9
app/src/main/res/drawable/background_gradient.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:endColor="#80000000"
|
||||
android:centerColor="#00000000"
|
||||
android:startColor="#00000000"
|
||||
android:type="linear" />
|
||||
</shape>
|
@ -1,21 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.cardview.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/app_item_grid"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="15dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="@dimen/app_card_margin_half"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
card_view:cardCornerRadius="4dp">
|
||||
app:cardCornerRadius="4dp"
|
||||
app:cardElevation="@dimen/app_card_margin"
|
||||
app:cardUseCompatPadding="true">
|
||||
|
||||
<RelativeLayout
|
||||
<LinearLayout
|
||||
android:layout_width="155dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
@ -25,16 +28,13 @@
|
||||
android:id="@+id/icon"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="155dp"
|
||||
android:layout_alignParentTop="false"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:contentDescription="@string/icon"
|
||||
android:scaleType="centerCrop" />
|
||||
tools:src="@drawable/default_icon" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/icon"
|
||||
android:ellipsize="marquee"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:paddingStart="15dp"
|
||||
@ -43,14 +43,12 @@
|
||||
android:singleLine="true"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
tools:ignore="RelativeOverlap" />
|
||||
tools:text="Title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_subtitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/text_title"
|
||||
android:layout_alignStart="@id/text_title"
|
||||
android:ellipsize="marquee"
|
||||
android:fadingEdge="horizontal"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
@ -58,7 +56,8 @@
|
||||
android:singleLine="true"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
|
||||
android:textColor="@android:color/tertiary_text_light" />
|
||||
</RelativeLayout>
|
||||
android:textColor="@android:color/tertiary_text_light"
|
||||
tools:text="Subtitle" />
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
81
app/src/main/res/layout/app_item_grid_compact.xml
Normal file
81
app/src/main/res/layout/app_item_grid_compact.xml
Normal file
@ -0,0 +1,81 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/app_item_grid"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="@dimen/app_card_margin_half"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
app:cardCornerRadius="4dp"
|
||||
app:cardElevation="@dimen/app_card_margin"
|
||||
app:cardUseCompatPadding="true">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="155dp"
|
||||
android:layout_height="155dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_alignParentTop="false"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:contentDescription="@string/icon"
|
||||
android:foreground="@drawable/background_gradient"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/default_icon" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:alpha="242.25"
|
||||
android:ellipsize="marquee"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
android:textColor="@android:color/white"
|
||||
android:textStyle="bold"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintBottom_toTopOf="@id/text_subtitle"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:text="Title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:alpha="242.25"
|
||||
android:ellipsize="marquee"
|
||||
android:fadingEdge="horizontal"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
|
||||
android:textColor="@android:color/white"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:text="Subtitle" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
</FrameLayout>
|
@ -1,40 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/app_item_linear"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:orientation="vertical"
|
||||
android:padding="15dp">
|
||||
android:padding="16dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="false"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:contentDescription="@string/icon" />
|
||||
android:contentDescription="@string/icon"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/default_icon" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignTop="@id/icon"
|
||||
android:layout_toEndOf="@id/icon"
|
||||
android:layout_marginStart="10dp"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
tools:ignore="RelativeOverlap" />
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_subtitle"
|
||||
app:layout_constraintStart_toEndOf="@id/icon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/text_title"
|
||||
android:layout_alignStart="@id/text_title"
|
||||
android:layout_marginStart="10dp"
|
||||
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
|
||||
android:textColor="@android:color/tertiary_text_light" />
|
||||
</RelativeLayout>
|
||||
android:textColor="@android:color/tertiary_text_light"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/icon"
|
||||
app:layout_constraintTop_toBottomOf="@+id/text_title"
|
||||
tools:text="SubTitle" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
@ -15,10 +15,12 @@
|
||||
<string-array name="layout_type">
|
||||
<item>List</item>
|
||||
<item>Grid</item>
|
||||
<item>Grid Compact</item>
|
||||
</string-array>
|
||||
<string-array name="layout_type_val">
|
||||
<item>0</item>
|
||||
<item>1</item>
|
||||
<item>2</item>
|
||||
</string-array>
|
||||
<string-array name="app_theme">
|
||||
<item>Light</item>
|
||||
|
5
app/src/main/res/values/dimens.xml
Normal file
5
app/src/main/res/values/dimens.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="app_card_margin">8dp</dimen>
|
||||
<dimen name="app_card_margin_half">4dp</dimen>
|
||||
</resources>
|
Loading…
Reference in New Issue
Block a user