- Screenshots are Expanded by Default

- Reformatted Code
- Added background to Show More Button
- Shortened Tab Indicator...
This commit is contained in:
Mohit 2021-06-08 21:35:46 +05:30
parent 8c0c48236e
commit baf9944dc9
5 changed files with 2719 additions and 2318 deletions

File diff suppressed because it is too large Load Diff

View File

@ -18,26 +18,21 @@ import androidx.fragment.app.DialogFragment
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.schedulers.Schedulers
import com.looker.droidify.R import com.looker.droidify.R
import com.looker.droidify.content.ProductPreferences import com.looker.droidify.content.ProductPreferences
import com.looker.droidify.database.Database import com.looker.droidify.database.Database
import com.looker.droidify.entity.InstalledItem import com.looker.droidify.entity.*
import com.looker.droidify.entity.Product
import com.looker.droidify.entity.ProductPreference
import com.looker.droidify.entity.Release
import com.looker.droidify.entity.Repository
import com.looker.droidify.service.Connection import com.looker.droidify.service.Connection
import com.looker.droidify.service.DownloadService import com.looker.droidify.service.DownloadService
import com.looker.droidify.utility.RxUtils import com.looker.droidify.utility.RxUtils
import com.looker.droidify.utility.Utils import com.looker.droidify.utility.Utils
import com.looker.droidify.utility.extension.android.* import com.looker.droidify.utility.extension.android.*
import com.looker.droidify.widget.DividerItemDecoration import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.schedulers.Schedulers
class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks { class ProductFragment() : ScreenFragment(), ProductAdapter.Callbacks {
companion object { companion object {
private const val EXTRA_PACKAGE_NAME = "packageName" private const val EXTRA_PACKAGE_NAME = "packageName"
@ -45,7 +40,7 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
private const val STATE_ADAPTER = "adapter" private const val STATE_ADAPTER = "adapter"
} }
constructor(packageName: String): this() { constructor(packageName: String) : this() {
arguments = Bundle().apply { arguments = Bundle().apply {
putString(EXTRA_PACKAGE_NAME, packageName) putString(EXTRA_PACKAGE_NAME, packageName)
} }
@ -53,7 +48,11 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
private class Nullable<T>(val value: T?) private class Nullable<T>(val value: T?)
private enum class Action(val id: Int, val adapterAction: ProductAdapter.Action, val iconResId: Int) { private enum class Action(
val id: Int,
val adapterAction: ProductAdapter.Action,
val iconResId: Int
) {
INSTALL(1, ProductAdapter.Action.INSTALL, R.drawable.ic_archive), INSTALL(1, ProductAdapter.Action.INSTALL, R.drawable.ic_archive),
UPDATE(2, ProductAdapter.Action.UPDATE, R.drawable.ic_archive), UPDATE(2, ProductAdapter.Action.UPDATE, R.drawable.ic_archive),
LAUNCH(3, ProductAdapter.Action.LAUNCH, R.drawable.ic_launch), LAUNCH(3, ProductAdapter.Action.LAUNCH, R.drawable.ic_launch),
@ -61,8 +60,10 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
UNINSTALL(5, ProductAdapter.Action.UNINSTALL, R.drawable.ic_delete) UNINSTALL(5, ProductAdapter.Action.UNINSTALL, R.drawable.ic_delete)
} }
private class Installed(val installedItem: InstalledItem, val isSystem: Boolean, private class Installed(
val launcherActivities: List<Pair<String, String>>) val installedItem: InstalledItem, val isSystem: Boolean,
val launcherActivities: List<Pair<String, String>>
)
val packageName: String val packageName: String
get() = requireArguments().getString(EXTRA_PACKAGE_NAME)!! get() = requireArguments().getString(EXTRA_PACKAGE_NAME)!!
@ -87,7 +88,11 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
downloadDisposable = null downloadDisposable = null
}) })
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment, container, false) return inflater.inflate(R.layout.fragment, container, false)
} }
@ -122,15 +127,16 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
isVerticalScrollBarEnabled = false isVerticalScrollBarEnabled = false
val adapter = ProductAdapter(this@ProductFragment, columns) val adapter = ProductAdapter(this@ProductFragment, columns)
this.adapter = adapter this.adapter = adapter
layoutManager.spanSizeLookup = object: GridLayoutManager.SpanSizeLookup() { layoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int { override fun getSpanSize(position: Int): Int {
return if (adapter.requiresGrid(position)) 1 else layoutManager.spanCount return if (adapter.requiresGrid(position)) 1 else layoutManager.spanCount
} }
} }
addOnScrollListener(scrollListener) addOnScrollListener(scrollListener)
addItemDecoration(adapter.gridItemDecoration) addItemDecoration(adapter.gridItemDecoration)
addItemDecoration(DividerItemDecoration(context, adapter::configureDivider)) // addItemDecoration(DividerItemDecoration(context, adapter::configureDivider))
savedInstanceState?.getParcelable<ProductAdapter.SavedState>(STATE_ADAPTER)?.let(adapter::restoreState) savedInstanceState?.getParcelable<ProductAdapter.SavedState>(STATE_ADAPTER)
?.let(adapter::restoreState)
layoutManagerState = savedInstanceState?.getParcelable(STATE_LAYOUT_MANAGER) layoutManagerState = savedInstanceState?.getParcelable(STATE_LAYOUT_MANAGER)
recyclerView = this recyclerView = this
}, FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT) }, FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
@ -140,22 +146,41 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
.concatWith(Database.observable(Database.Subject.Products)) .concatWith(Database.observable(Database.Subject.Products))
.observeOn(Schedulers.io()) .observeOn(Schedulers.io())
.flatMapSingle { RxUtils.querySingle { Database.ProductAdapter.get(packageName, it) } } .flatMapSingle { RxUtils.querySingle { Database.ProductAdapter.get(packageName, it) } }
.flatMapSingle { products -> RxUtils .flatMapSingle { products ->
RxUtils
.querySingle { Database.RepositoryAdapter.getAll(it) } .querySingle { Database.RepositoryAdapter.getAll(it) }
.map { it.asSequence().map { Pair(it.id, it) }.toMap() .map { it ->
.let { products.mapNotNull { product -> it[product.repositoryId]?.let { Pair(product, it) } } } } } it.asSequence().map { Pair(it.id, it) }.toMap()
.flatMapSingle { products -> RxUtils .let {
products.mapNotNull { product ->
it[product.repositoryId]?.let {
Pair(
product,
it
)
}
}
}
}
}
.flatMapSingle { products ->
RxUtils
.querySingle { Nullable(Database.InstalledAdapter.get(packageName, it)) } .querySingle { Nullable(Database.InstalledAdapter.get(packageName, it)) }
.map { Pair(products, it) } } .map { Pair(products, it) }
}
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { .subscribe { it ->
val (products, installedItem) = it val (products, installedItem) = it
val firstChanged = first val firstChanged = first
first = false first = false
val productChanged = this.products != products val productChanged = this.products != products
val installedItemChanged = this.installed?.installedItem != installedItem.value val installedItemChanged = this.installed?.installedItem != installedItem.value
if (firstChanged || productChanged || installedItemChanged) { if (firstChanged || productChanged || installedItemChanged) {
layoutManagerState?.let { recyclerView?.layoutManager!!.onRestoreInstanceState(it) } layoutManagerState?.let {
recyclerView?.layoutManager!!.onRestoreInstanceState(
it
)
}
layoutManagerState = null layoutManagerState = null
if (firstChanged || productChanged) { if (firstChanged || productChanged) {
this.products = products this.products = products
@ -163,19 +188,28 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
if (firstChanged || installedItemChanged) { if (firstChanged || installedItemChanged) {
installed = installedItem.value?.let { installed = installedItem.value?.let {
val isSystem = try { val isSystem = try {
((requireContext().packageManager.getApplicationInfo(packageName, 0).flags) ((requireContext().packageManager.getApplicationInfo(
packageName,
0
).flags)
and ApplicationInfo.FLAG_SYSTEM) != 0 and ApplicationInfo.FLAG_SYSTEM) != 0
} catch (e: Exception) { } catch (e: Exception) {
false false
} }
val launcherActivities = if (packageName == requireContext().packageName) { val launcherActivities =
if (packageName == requireContext().packageName) {
// Don't allow to launch self // Don't allow to launch self
emptyList() emptyList()
} else { } else {
val packageManager = requireContext().packageManager val packageManager = requireContext().packageManager
packageManager packageManager
.queryIntentActivities(Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER), 0) .queryIntentActivities(
.asSequence().mapNotNull { it.activityInfo }.filter { it.packageName == packageName } Intent(Intent.ACTION_MAIN).addCategory(
Intent.CATEGORY_LAUNCHER
), 0
)
.asSequence().mapNotNull { it.activityInfo }
.filter { it.packageName == packageName }
.mapNotNull { activityInfo -> .mapNotNull { activityInfo ->
val label = try { val label = try {
activityInfo.loadLabel(packageManager).toString() activityInfo.loadLabel(packageManager).toString()
@ -193,7 +227,12 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
val recyclerView = recyclerView!! val recyclerView = recyclerView!!
val adapter = recyclerView.adapter as ProductAdapter val adapter = recyclerView.adapter as ProductAdapter
if (firstChanged || productChanged || installedItemChanged) { if (firstChanged || productChanged || installedItemChanged) {
adapter.setProducts(recyclerView.context, packageName, products, installedItem.value) adapter.setProducts(
recyclerView.context,
packageName,
products,
installedItem.value
)
} }
updateButtons() updateButtons()
} }
@ -218,7 +257,8 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
val layoutManagerState = layoutManagerState ?: recyclerView?.layoutManager?.onSaveInstanceState() val layoutManagerState =
layoutManagerState ?: recyclerView?.layoutManager?.onSaveInstanceState()
layoutManagerState?.let { outState.putParcelable(STATE_LAYOUT_MANAGER, it) } layoutManagerState?.let { outState.putParcelable(STATE_LAYOUT_MANAGER, it) }
val adapterState = (recyclerView?.adapter as? ProductAdapter)?.saveState() val adapterState = (recyclerView?.adapter as? ProductAdapter)?.saveState()
adapterState?.let { outState.putParcelable(STATE_ADAPTER, it) } adapterState?.let { outState.putParcelable(STATE_ADAPTER, it) }
@ -234,10 +274,12 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
val compatible = product != null && product.selectedReleases.firstOrNull() val compatible = product != null && product.selectedReleases.firstOrNull()
.let { it != null && it.incompatibilities.isEmpty() } .let { it != null && it.incompatibilities.isEmpty() }
val canInstall = product != null && installed == null && compatible val canInstall = product != null && installed == null && compatible
val canUpdate = product != null && compatible && product.canUpdate(installed?.installedItem) && val canUpdate =
product != null && compatible && product.canUpdate(installed?.installedItem) &&
!preference.shouldIgnoreUpdate(product.versionCode) !preference.shouldIgnoreUpdate(product.versionCode)
val canUninstall = product != null && installed != null && !installed.isSystem val canUninstall = product != null && installed != null && !installed.isSystem
val canLaunch = product != null && installed != null && installed.launcherActivities.isNotEmpty() val canLaunch =
product != null && installed != null && installed.launcherActivities.isNotEmpty()
val actions = mutableSetOf<Action>() val actions = mutableSetOf<Action>()
if (canInstall) { if (canInstall) {
@ -263,7 +305,8 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
else -> null else -> null
} }
val adapterAction = if (downloading) ProductAdapter.Action.CANCEL else primaryAction?.adapterAction val adapterAction =
if (downloading) ProductAdapter.Action.CANCEL else primaryAction?.adapterAction
(recyclerView?.adapter as? ProductAdapter)?.setAction(adapterAction) (recyclerView?.adapter as? ProductAdapter)?.setAction(adapterAction)
val toolbar = toolbar val toolbar = toolbar
@ -299,7 +342,10 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
val status = when (state) { val status = when (state) {
is DownloadService.State.Pending -> ProductAdapter.Status.Pending is DownloadService.State.Pending -> ProductAdapter.Status.Pending
is DownloadService.State.Connecting -> ProductAdapter.Status.Connecting is DownloadService.State.Connecting -> ProductAdapter.Status.Connecting
is DownloadService.State.Downloading -> ProductAdapter.Status.Downloading(state.read, state.total) is DownloadService.State.Downloading -> ProductAdapter.Status.Downloading(
state.read,
state.total
)
is DownloadService.State.Success, is DownloadService.State.Error, is DownloadService.State.Cancel, null -> null is DownloadService.State.Success, is DownloadService.State.Error, is DownloadService.State.Cancel, null -> null
} }
val downloading = status != null val downloading = status != null
@ -314,11 +360,12 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
} }
} }
private val scrollListener = object: RecyclerView.OnScrollListener() { private val scrollListener = object : RecyclerView.OnScrollListener() {
private var lastPosition = -1 private var lastPosition = -1
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
val position = (recyclerView.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() val position =
(recyclerView.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
val lastPosition = lastPosition val lastPosition = lastPosition
this.lastPosition = position this.lastPosition = position
if ((lastPosition == 0) != (position == 0)) { if ((lastPosition == 0) != (position == 0)) {
@ -338,36 +385,48 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
val release = if (compatibleReleases.size >= 2) { val release = if (compatibleReleases.size >= 2) {
compatibleReleases compatibleReleases
.filter { it.platforms.contains(Android.primaryPlatform) } .filter { it.platforms.contains(Android.primaryPlatform) }
.minBy { it.platforms.size } .minByOrNull { it.platforms.size }
?: compatibleReleases.minBy { it.platforms.size } ?: compatibleReleases.minByOrNull { it.platforms.size }
?: compatibleReleases.firstOrNull() ?: compatibleReleases.firstOrNull()
} else { } else {
compatibleReleases.firstOrNull() compatibleReleases.firstOrNull()
} }
val binder = downloadConnection.binder val binder = downloadConnection.binder
if (productRepository != null && release != null && binder != null) { if (productRepository != null && release != null && binder != null) {
binder.enqueue(packageName, productRepository.first.name, productRepository.second, release) binder.enqueue(
packageName,
productRepository.first.name,
productRepository.second,
release
)
} }
Unit Unit
} }
ProductAdapter.Action.LAUNCH -> { ProductAdapter.Action.LAUNCH -> {
val launcherActivities = installed?.launcherActivities.orEmpty() val launcherActivities = installed?.launcherActivities.orEmpty()
if (launcherActivities.size >= 2) { if (launcherActivities.size >= 2) {
LaunchDialog(launcherActivities).show(childFragmentManager, LaunchDialog::class.java.name) LaunchDialog(launcherActivities).show(
childFragmentManager,
LaunchDialog::class.java.name
)
} else { } else {
launcherActivities.firstOrNull()?.let { startLauncherActivity(it.first) } launcherActivities.firstOrNull()?.let { startLauncherActivity(it.first) }
} }
Unit Unit
} }
ProductAdapter.Action.DETAILS -> { ProductAdapter.Action.DETAILS -> {
startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) startActivity(
.setData(Uri.parse("package:$packageName"))) Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
.setData(Uri.parse("package:$packageName"))
)
} }
ProductAdapter.Action.UNINSTALL -> { ProductAdapter.Action.UNINSTALL -> {
// TODO Handle deprecation // TODO Handle deprecation
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
startActivity(Intent(Intent.ACTION_UNINSTALL_PACKAGE) startActivity(
.setData(Uri.parse("package:$packageName"))) Intent(Intent.ACTION_UNINSTALL_PACKAGE)
.setData(Uri.parse("package:$packageName"))
)
} }
ProductAdapter.Action.CANCEL -> { ProductAdapter.Action.CANCEL -> {
val binder = downloadConnection.binder val binder = downloadConnection.binder
@ -381,10 +440,12 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
private fun startLauncherActivity(name: String) { private fun startLauncherActivity(name: String) {
try { try {
startActivity(Intent(Intent.ACTION_MAIN) startActivity(
Intent(Intent.ACTION_MAIN)
.addCategory(Intent.CATEGORY_LAUNCHER) .addCategory(Intent.CATEGORY_LAUNCHER)
.setComponent(ComponentName(packageName, name)) .setComponent(ComponentName(packageName, name))
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
@ -395,17 +456,26 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
} }
override fun onPermissionsClick(group: String?, permissions: List<String>) { override fun onPermissionsClick(group: String?, permissions: List<String>) {
MessageDialog(MessageDialog.Message.Permissions(group, permissions)).show(childFragmentManager) MessageDialog(MessageDialog.Message.Permissions(group, permissions)).show(
childFragmentManager
)
} }
override fun onScreenshotClick(screenshot: Product.Screenshot) { override fun onScreenshotClick(screenshot: Product.Screenshot) {
val pair = products.asSequence() val pair = products.asSequence()
.map { Pair(it.second, it.first.screenshots.find { it === screenshot }?.identifier) } .map { it ->
Pair(
it.second,
it.first.screenshots.find { it === screenshot }?.identifier
)
}
.filter { it.second != null }.firstOrNull() .filter { it.second != null }.firstOrNull()
if (pair != null) { if (pair != null) {
val (repository, identifier) = pair val (repository, identifier) = pair
if (identifier != null) { if (identifier != null) {
ScreenshotsFragment(packageName, repository.id, identifier).show(childFragmentManager) ScreenshotsFragment(packageName, repository.id, identifier).show(
childFragmentManager
)
} }
} }
} }
@ -414,20 +484,30 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
val installedItem = installed?.installedItem val installedItem = installed?.installedItem
when { when {
release.incompatibilities.isNotEmpty() -> { release.incompatibilities.isNotEmpty() -> {
MessageDialog(MessageDialog.Message.ReleaseIncompatible(release.incompatibilities, MessageDialog(
release.platforms, release.minSdkVersion, release.maxSdkVersion)).show(childFragmentManager) MessageDialog.Message.ReleaseIncompatible(
release.incompatibilities,
release.platforms, release.minSdkVersion, release.maxSdkVersion
)
).show(childFragmentManager)
} }
installedItem != null && installedItem.versionCode > release.versionCode -> { installedItem != null && installedItem.versionCode > release.versionCode -> {
MessageDialog(MessageDialog.Message.ReleaseOlder).show(childFragmentManager) MessageDialog(MessageDialog.Message.ReleaseOlder).show(childFragmentManager)
} }
installedItem != null && installedItem.signature != release.signature -> { installedItem != null && installedItem.signature != release.signature -> {
MessageDialog(MessageDialog.Message.ReleaseSignatureMismatch).show(childFragmentManager) MessageDialog(MessageDialog.Message.ReleaseSignatureMismatch).show(
childFragmentManager
)
} }
else -> { else -> {
val productRepository = products.asSequence().filter { it.first.releases.any { it === release } }.firstOrNull() val productRepository =
products.asSequence().filter { it -> it.first.releases.any { it === release } }
.firstOrNull()
if (productRepository != null) { if (productRepository != null) {
downloadConnection.binder?.enqueue(packageName, productRepository.first.name, downloadConnection.binder?.enqueue(
productRepository.second, release) packageName, productRepository.first.name,
productRepository.second, release
)
} }
} }
} }
@ -448,13 +528,13 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
} }
} }
class LaunchDialog(): DialogFragment() { class LaunchDialog() : DialogFragment() {
companion object { companion object {
private const val EXTRA_NAMES = "names" private const val EXTRA_NAMES = "names"
private const val EXTRA_LABELS = "labels" private const val EXTRA_LABELS = "labels"
} }
constructor(launcherActivities: List<Pair<String, String>>): this() { constructor(launcherActivities: List<Pair<String, String>>) : this() {
arguments = Bundle().apply { arguments = Bundle().apply {
putStringArrayList(EXTRA_NAMES, ArrayList(launcherActivities.map { it.first })) putStringArrayList(EXTRA_NAMES, ArrayList(launcherActivities.map { it.first }))
putStringArrayList(EXTRA_LABELS, ArrayList(launcherActivities.map { it.second })) putStringArrayList(EXTRA_LABELS, ArrayList(launcherActivities.map { it.second }))
@ -466,8 +546,10 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
val labels = requireArguments().getStringArrayList(EXTRA_LABELS)!! val labels = requireArguments().getStringArrayList(EXTRA_LABELS)!!
return AlertDialog.Builder(requireContext()) return AlertDialog.Builder(requireContext())
.setTitle(R.string.launch) .setTitle(R.string.launch)
.setItems(labels.toTypedArray()) { _, position -> (parentFragment as ProductFragment) .setItems(labels.toTypedArray()) { _, position ->
.startLauncherActivity(names[position]) } (parentFragment as ProductFragment)
.startLauncherActivity(names[position])
}
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.create() .create()
} }

View File

@ -18,15 +18,14 @@ import com.looker.droidify.entity.Repository
import com.looker.droidify.network.PicassoDownloader import com.looker.droidify.network.PicassoDownloader
import com.looker.droidify.utility.Utils import com.looker.droidify.utility.Utils
import com.looker.droidify.utility.extension.resources.* import com.looker.droidify.utility.extension.resources.*
import com.looker.droidify.utility.extension.text.* import com.looker.droidify.utility.extension.text.nullIfEmpty
import com.looker.droidify.widget.CursorRecyclerAdapter import com.looker.droidify.widget.CursorRecyclerAdapter
import com.looker.droidify.widget.DividerItemDecoration
class ProductsAdapter(private val onClick: (ProductItem) -> Unit): class ProductsAdapter(private val onClick: (ProductItem) -> Unit) :
CursorRecyclerAdapter<ProductsAdapter.ViewType, RecyclerView.ViewHolder>() { CursorRecyclerAdapter<ProductsAdapter.ViewType, RecyclerView.ViewHolder>() {
enum class ViewType { PRODUCT, LOADING, EMPTY } enum class ViewType { PRODUCT, LOADING, EMPTY }
private class ProductViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { private class ProductViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val name = itemView.findViewById<TextView>(R.id.name)!! val name = itemView.findViewById<TextView>(R.id.name)!!
val status = itemView.findViewById<TextView>(R.id.status)!! val status = itemView.findViewById<TextView>(R.id.status)!!
val summary = itemView.findViewById<TextView>(R.id.summary)!! val summary = itemView.findViewById<TextView>(R.id.summary)!!
@ -42,18 +41,23 @@ class ProductsAdapter(private val onClick: (ProductItem) -> Unit):
} }
} }
private class LoadingViewHolder(context: Context): RecyclerView.ViewHolder(FrameLayout(context)) { private class LoadingViewHolder(context: Context) :
RecyclerView.ViewHolder(FrameLayout(context)) {
init { init {
itemView as FrameLayout itemView as FrameLayout
val progressBar = ProgressBar(itemView.context) val progressBar = ProgressBar(itemView.context)
itemView.addView(progressBar, FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, itemView.addView(progressBar, FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT).apply { gravity = Gravity.CENTER }) FrameLayout.LayoutParams.WRAP_CONTENT,
itemView.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT
RecyclerView.LayoutParams.MATCH_PARENT) ).apply { gravity = Gravity.CENTER })
itemView.layoutParams = RecyclerView.LayoutParams(
RecyclerView.LayoutParams.MATCH_PARENT,
RecyclerView.LayoutParams.MATCH_PARENT
)
} }
} }
private class EmptyViewHolder(context: Context): RecyclerView.ViewHolder(TextView(context)) { private class EmptyViewHolder(context: Context) : RecyclerView.ViewHolder(TextView(context)) {
val text: TextView val text: TextView
get() = itemView as TextView get() = itemView as TextView
@ -64,22 +68,10 @@ class ProductsAdapter(private val onClick: (ProductItem) -> Unit):
itemView.typeface = TypefaceExtra.light itemView.typeface = TypefaceExtra.light
itemView.setTextColor(context.getColorFromAttr(android.R.attr.textColorPrimary)) itemView.setTextColor(context.getColorFromAttr(android.R.attr.textColorPrimary))
itemView.setTextSizeScaled(20) itemView.setTextSizeScaled(20)
itemView.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, itemView.layoutParams = RecyclerView.LayoutParams(
RecyclerView.LayoutParams.MATCH_PARENT) RecyclerView.LayoutParams.MATCH_PARENT,
} RecyclerView.LayoutParams.MATCH_PARENT
} )
fun configureDivider(context: Context, position: Int, configuration: DividerItemDecoration.Configuration) {
val currentItem = if (getItemEnumViewType(position) == ViewType.PRODUCT) getProductItem(position) else null
val nextItem = if (position + 1 < itemCount && getItemEnumViewType(position + 1) == ViewType.PRODUCT)
getProductItem(position + 1) else null
when {
currentItem != null && nextItem != null && currentItem.matchRank != nextItem.matchRank -> {
configuration.set(true, false, 0, 0)
}
else -> {
configuration.set(true, false, context.resources.sizeScaled(72), 0)
}
} }
} }
@ -120,7 +112,10 @@ class ProductsAdapter(private val onClick: (ProductItem) -> Unit):
return Database.ProductAdapter.transformItem(moveTo(position)) return Database.ProductAdapter.transformItem(moveTo(position))
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: ViewType): RecyclerView.ViewHolder { override fun onCreateViewHolder(
parent: ViewGroup,
viewType: ViewType
): RecyclerView.ViewHolder {
return when (viewType) { return when (viewType) {
ViewType.PRODUCT -> ProductViewHolder(parent.inflate(R.layout.product_item)).apply { ViewType.PRODUCT -> ProductViewHolder(parent.inflate(R.layout.product_item)).apply {
itemView.setOnClickListener { onClick(getProductItem(adapterPosition)) } itemView.setOnClickListener { onClick(getProductItem(adapterPosition)) }
@ -136,12 +131,18 @@ class ProductsAdapter(private val onClick: (ProductItem) -> Unit):
holder as ProductViewHolder holder as ProductViewHolder
val productItem = getProductItem(position) val productItem = getProductItem(position)
holder.name.text = productItem.name holder.name.text = productItem.name
holder.summary.text = if (productItem.name == productItem.summary) "" else productItem.summary holder.summary.text =
holder.summary.visibility = if (holder.summary.text.isNotEmpty()) View.VISIBLE else View.GONE if (productItem.name == productItem.summary) "" else productItem.summary
holder.summary.visibility =
if (holder.summary.text.isNotEmpty()) View.VISIBLE else View.GONE
val repository: Repository? = repositories[productItem.repositoryId] val repository: Repository? = repositories[productItem.repositoryId]
if ((productItem.icon.isNotEmpty() || productItem.metadataIcon.isNotEmpty()) && repository != null) { if ((productItem.icon.isNotEmpty() || productItem.metadataIcon.isNotEmpty()) && repository != null) {
holder.icon.load(PicassoDownloader.createIconUri(holder.icon, productItem.packageName, holder.icon.load(
productItem.icon, productItem.metadataIcon, repository)) { PicassoDownloader.createIconUri(
holder.icon, productItem.packageName,
productItem.icon, productItem.metadataIcon, repository
)
) {
placeholder(holder.progressIcon) placeholder(holder.progressIcon)
error(holder.defaultIcon) error(holder.defaultIcon)
} }
@ -155,8 +156,12 @@ class ProductsAdapter(private val onClick: (ProductItem) -> Unit):
if (background == null) { if (background == null) {
resources.sizeScaled(4).let { setPadding(it, 0, it, 0) } resources.sizeScaled(4).let { setPadding(it, 0, it, 0) }
setTextColor(holder.status.context.getColorFromAttr(android.R.attr.colorBackground)) setTextColor(holder.status.context.getColorFromAttr(android.R.attr.colorBackground))
background = GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, null).apply { background = GradientDrawable(
color = holder.status.context.getColorFromAttr(android.R.attr.colorAccent) GradientDrawable.Orientation.TOP_BOTTOM,
null
).apply {
color =
holder.status.context.getColorFromAttr(android.R.attr.colorAccent)
cornerRadius = holder.status.resources.sizeScaled(2).toFloat() cornerRadius = holder.status.resources.sizeScaled(2).toFloat()
} }
} }
@ -170,7 +175,9 @@ class ProductsAdapter(private val onClick: (ProductItem) -> Unit):
} }
} }
val enabled = productItem.compatible || productItem.installedVersion.isNotEmpty() val enabled = productItem.compatible || productItem.installedVersion.isNotEmpty()
sequenceOf(holder.name, holder.status, holder.summary).forEach { it.isEnabled = enabled } sequenceOf(holder.name, holder.status, holder.summary).forEach {
it.isEnabled = enabled
}
} }
ViewType.LOADING -> { ViewType.LOADING -> {
// Do nothing // Do nothing

View File

@ -17,7 +17,6 @@ import com.looker.droidify.database.CursorOwner
import com.looker.droidify.database.Database import com.looker.droidify.database.Database
import com.looker.droidify.entity.ProductItem import com.looker.droidify.entity.ProductItem
import com.looker.droidify.utility.RxUtils import com.looker.droidify.utility.RxUtils
import com.looker.droidify.widget.DividerItemDecoration
import com.looker.droidify.widget.RecyclerFastScroller import com.looker.droidify.widget.RecyclerFastScroller
class ProductsFragment(): ScreenFragment(), CursorOwner.Callback { class ProductsFragment(): ScreenFragment(), CursorOwner.Callback {
@ -80,7 +79,6 @@ class ProductsFragment(): ScreenFragment(), CursorOwner.Callback {
recycledViewPool.setMaxRecycledViews(ProductsAdapter.ViewType.PRODUCT.ordinal, 30) recycledViewPool.setMaxRecycledViews(ProductsAdapter.ViewType.PRODUCT.ordinal, 30)
val adapter = ProductsAdapter { screenActivity.navigateProduct(it.packageName) } val adapter = ProductsAdapter { screenActivity.navigateProduct(it.packageName) }
this.adapter = adapter this.adapter = adapter
addItemDecoration(DividerItemDecoration(context, adapter::configureDivider))
RecyclerFastScroller(this) RecyclerFastScroller(this)
recyclerView = this recyclerView = this
} }

View File

@ -9,28 +9,15 @@ import android.graphics.Paint
import android.graphics.PixelFormat import android.graphics.PixelFormat
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Bundle import android.os.Bundle
import android.view.Gravity import android.view.*
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateInterpolator import android.view.animation.AccelerateInterpolator
import android.view.animation.DecelerateInterpolator import android.view.animation.DecelerateInterpolator
import android.widget.FrameLayout import android.widget.*
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.SearchView
import android.widget.TextView
import android.widget.Toolbar
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.schedulers.Schedulers
import com.looker.droidify.R import com.looker.droidify.R
import com.looker.droidify.content.Preferences import com.looker.droidify.content.Preferences
import com.looker.droidify.database.Database import com.looker.droidify.database.Database
@ -44,9 +31,13 @@ import com.looker.droidify.utility.extension.resources.*
import com.looker.droidify.widget.DividerItemDecoration import com.looker.droidify.widget.DividerItemDecoration
import com.looker.droidify.widget.FocusSearchView import com.looker.droidify.widget.FocusSearchView
import com.looker.droidify.widget.StableRecyclerAdapter import com.looker.droidify.widget.StableRecyclerAdapter
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.schedulers.Schedulers
import kotlin.math.* import kotlin.math.*
class TabsFragment: ScreenFragment() { class TabsFragment : ScreenFragment() {
companion object { companion object {
private const val STATE_SEARCH_FOCUSED = "searchFocused" private const val STATE_SEARCH_FOCUSED = "searchFocused"
private const val STATE_SEARCH_QUERY = "searchQuery" private const val STATE_SEARCH_QUERY = "searchQuery"
@ -75,8 +66,10 @@ class TabsFragment: ScreenFragment() {
if (field != value) { if (field != value) {
field = value field = value
val layout = layout val layout = layout
layout?.tabs?.let { (0 until it.childCount) layout?.tabs?.let {
.forEach { index -> it.getChildAt(index)!!.isEnabled = !value } } (0 until it.childCount)
.forEach { index -> it.getChildAt(index)!!.isEnabled = !value }
}
layout?.sectionIcon?.scaleY = if (value) -1f else 1f layout?.sectionIcon?.scaleY = if (value) -1f else 1f
if ((sectionsList?.parent as? View)?.height ?: 0 > 0) { if ((sectionsList?.parent as? View)?.height ?: 0 > 0) {
animateSectionsList() animateSectionsList()
@ -106,7 +99,11 @@ class TabsFragment: ScreenFragment() {
get() = if (host == null) emptySequence() else get() = if (host == null) emptySequence() else
childFragmentManager.fragments.asSequence().mapNotNull { it as? ProductsFragment } childFragmentManager.fragments.asSequence().mapNotNull { it as? ProductsFragment }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.fragment, container, false) return inflater.inflate(R.layout.fragment, container, false)
} }
@ -125,7 +122,7 @@ class TabsFragment: ScreenFragment() {
searchView.allowFocus = savedInstanceState?.getBoolean(STATE_SEARCH_FOCUSED) == true searchView.allowFocus = savedInstanceState?.getBoolean(STATE_SEARCH_FOCUSED) == true
searchView.maxWidth = Int.MAX_VALUE searchView.maxWidth = Int.MAX_VALUE
searchView.queryHint = getString(R.string.search) searchView.queryHint = getString(R.string.search)
searchView.setOnQueryTextListener(object: SearchView.OnQueryTextListener { searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean { override fun onQueryTextSubmit(query: String?): Boolean {
searchView.clearFocus() searchView.clearFocus()
return true return true
@ -155,12 +152,14 @@ class TabsFragment: ScreenFragment() {
.let { menu -> .let { menu ->
menu.item.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS) menu.item.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS)
val items = Preferences.Key.SortOrder.default.value.values val items = Preferences.Key.SortOrder.default.value.values
.map { sortOrder -> menu .map { sortOrder ->
menu
.add(sortOrder.order.titleResId) .add(sortOrder.order.titleResId)
.setOnMenuItemClickListener { .setOnMenuItemClickListener {
Preferences[Preferences.Key.SortOrder] = sortOrder Preferences[Preferences.Key.SortOrder] = sortOrder
true true
} } }
}
menu.setGroupCheckable(0, true, true) menu.setGroupCheckable(0, true, true)
Pair(menu.item, items) Pair(menu.item, items)
} }
@ -193,20 +192,29 @@ class TabsFragment: ScreenFragment() {
val layout = Layout(view) val layout = Layout(view)
this.layout = layout this.layout = layout
layout.tabs.background = TabsBackgroundDrawable(layout.tabs.context, layout.tabs.background = TabsBackgroundDrawable(
layout.tabs.layoutDirection == View.LAYOUT_DIRECTION_RTL) layout.tabs.context,
layout.tabs.layoutDirection == View.LAYOUT_DIRECTION_RTL
)
ProductsFragment.Source.values().forEach { ProductsFragment.Source.values().forEach {
val tab = TextView(layout.tabs.context) val tab = TextView(layout.tabs.context)
val selectedColor = tab.context.getColorFromAttr(android.R.attr.textColorPrimary).defaultColor val selectedColor =
val normalColor = tab.context.getColorFromAttr(android.R.attr.textColorSecondary).defaultColor tab.context.getColorFromAttr(android.R.attr.textColorPrimary).defaultColor
val normalColor =
tab.context.getColorFromAttr(android.R.attr.textColorSecondary).defaultColor
tab.gravity = Gravity.CENTER tab.gravity = Gravity.CENTER
tab.typeface = TypefaceExtra.medium tab.typeface = TypefaceExtra.medium
tab.setTextColor(ColorStateList(arrayOf(intArrayOf(android.R.attr.state_selected), intArrayOf()), tab.setTextColor(
intArrayOf(selectedColor, normalColor))) ColorStateList(
arrayOf(intArrayOf(android.R.attr.state_selected), intArrayOf()),
intArrayOf(selectedColor, normalColor)
)
)
tab.setTextSizeScaled(14) tab.setTextSizeScaled(14)
tab.isAllCaps = true tab.isAllCaps = true
tab.text = getString(it.titleResId) tab.text = getString(it.titleResId)
tab.background = tab.context.getDrawableFromAttr(android.R.attr.selectableItemBackground) tab.background =
tab.context.getDrawableFromAttr(android.R.attr.selectableItemBackground)
tab.setOnClickListener { _ -> tab.setOnClickListener { _ ->
setSelectedTab(it) setSelectedTab(it)
viewPager!!.setCurrentItem(it.ordinal, Utils.areAnimationsEnabled(tab.context)) viewPager!!.setCurrentItem(it.ordinal, Utils.areAnimationsEnabled(tab.context))
@ -216,10 +224,13 @@ class TabsFragment: ScreenFragment() {
} }
showSections = savedInstanceState?.getByte(STATE_SHOW_SECTIONS)?.toInt() ?: 0 != 0 showSections = savedInstanceState?.getByte(STATE_SHOW_SECTIONS)?.toInt() ?: 0 != 0
sections = savedInstanceState?.getParcelableArrayList<ProductItem.Section>(STATE_SECTIONS).orEmpty() sections = savedInstanceState?.getParcelableArrayList<ProductItem.Section>(STATE_SECTIONS)
.orEmpty()
section = savedInstanceState?.getParcelable(STATE_SECTION) ?: ProductItem.Section.All section = savedInstanceState?.getParcelable(STATE_SECTION) ?: ProductItem.Section.All
layout.sectionChange.setOnClickListener { showSections = sections layout.sectionChange.setOnClickListener {
.any { it !is ProductItem.Section.All } && !showSections } showSections = sections
.any { it !is ProductItem.Section.All } && !showSections
}
updateOrder() updateOrder()
sortOrderDisposable = Preferences.observable.subscribe { sortOrderDisposable = Preferences.observable.subscribe {
@ -232,12 +243,18 @@ class TabsFragment: ScreenFragment() {
viewPager = ViewPager2(content.context).apply { viewPager = ViewPager2(content.context).apply {
id = R.id.fragment_pager id = R.id.fragment_pager
adapter = object: FragmentStateAdapter(this@TabsFragment) { adapter = object : FragmentStateAdapter(this@TabsFragment) {
override fun getItemCount(): Int = ProductsFragment.Source.values().size override fun getItemCount(): Int = ProductsFragment.Source.values().size
override fun createFragment(position: Int): Fragment = ProductsFragment(ProductsFragment override fun createFragment(position: Int): Fragment = ProductsFragment(
.Source.values()[position]) ProductsFragment
.Source.values()[position]
)
} }
content.addView(this, FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT) content.addView(
this,
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
registerOnPageChangeCallback(pageChangeCallback) registerOnPageChangeCallback(pageChangeCallback)
offscreenPageLimit = 1 offscreenPageLimit = 1
} }
@ -247,15 +264,21 @@ class TabsFragment: ScreenFragment() {
.observeOn(Schedulers.io()) .observeOn(Schedulers.io())
.flatMapSingle { RxUtils.querySingle { Database.CategoryAdapter.getAll(it) } } .flatMapSingle { RxUtils.querySingle { Database.CategoryAdapter.getAll(it) } }
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { setSectionsAndUpdate(it.asSequence().sorted() .subscribe {
.map(ProductItem.Section::Category).toList(), null) } setSectionsAndUpdate(
it.asSequence().sorted()
.map(ProductItem.Section::Category).toList(), null
)
}
repositoriesDisposable = Observable.just(Unit) repositoriesDisposable = Observable.just(Unit)
.concatWith(Database.observable(Database.Subject.Repositories)) .concatWith(Database.observable(Database.Subject.Repositories))
.observeOn(Schedulers.io()) .observeOn(Schedulers.io())
.flatMapSingle { RxUtils.querySingle { Database.RepositoryAdapter.getAll(it) } } .flatMapSingle { RxUtils.querySingle { Database.RepositoryAdapter.getAll(it) } }
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { setSectionsAndUpdate(null, it.asSequence().filter { it.enabled } .subscribe { it ->
.map { ProductItem.Section.Repository(it.id, it.name) }.toList()) } setSectionsAndUpdate(null, it.asSequence().filter { it.enabled }
.map { ProductItem.Section.Repository(it.id, it.name) }.toList())
}
updateSection() updateSection()
val sectionsList = RecyclerView(toolbar.context).apply { val sectionsList = RecyclerView(toolbar.context).apply {
@ -273,7 +296,7 @@ class TabsFragment: ScreenFragment() {
} }
this.adapter = adapter this.adapter = adapter
addItemDecoration(DividerItemDecoration(context, adapter::configureDivider)) addItemDecoration(DividerItemDecoration(context, adapter::configureDivider))
setBackgroundColor(context.getColorFromAttr(android.R.attr.colorPrimaryDark).defaultColor) setBackgroundResource(R.drawable.background_border)
elevation = resources.sizeScaled(4).toFloat() elevation = resources.sizeScaled(4).toFloat()
content.addView(this, FrameLayout.LayoutParams.MATCH_PARENT, 0) content.addView(this, FrameLayout.LayoutParams.MATCH_PARENT, 0)
visibility = View.GONE visibility = View.GONE
@ -368,7 +391,9 @@ class TabsFragment: ScreenFragment() {
private fun setSelectedTab(source: ProductsFragment.Source) { private fun setSelectedTab(source: ProductsFragment.Source) {
val layout = layout!! val layout = layout!!
(0 until layout.tabs.childCount).forEach { layout.tabs.getChildAt(it).isSelected = it == source.ordinal } (0 until layout.tabs.childCount).forEach {
layout.tabs.getChildAt(it).isSelected = it == source.ordinal
}
} }
internal fun selectUpdates() = selectUpdatesInternal(true) internal fun selectUpdates() = selectUpdatesInternal(true)
@ -376,7 +401,10 @@ class TabsFragment: ScreenFragment() {
private fun selectUpdatesInternal(allowSmooth: Boolean) { private fun selectUpdatesInternal(allowSmooth: Boolean) {
if (view != null) { if (view != null) {
val viewPager = viewPager val viewPager = viewPager
viewPager?.setCurrentItem(ProductsFragment.Source.UPDATES.ordinal, allowSmooth && viewPager.isLaidOut) viewPager?.setCurrentItem(
ProductsFragment.Source.UPDATES.ordinal,
allowSmooth && viewPager.isLaidOut
)
} else { } else {
needSelectUpdates = true needSelectUpdates = true
} }
@ -397,13 +425,15 @@ class TabsFragment: ScreenFragment() {
productFragments.forEach { it.setOrder(order) } productFragments.forEach { it.setOrder(order) }
} }
private inline fun <reified T: ProductItem.Section> collectOldSections(list: List<T>?): List<T>? { private inline fun <reified T : ProductItem.Section> collectOldSections(list: List<T>?): List<T>? {
val oldList = sections.mapNotNull { it as? T } val oldList = sections.mapNotNull { it as? T }
return if (list == null || oldList == list) oldList else null return if (list == null || oldList == list) oldList else null
} }
private fun setSectionsAndUpdate(categories: List<ProductItem.Section.Category>?, private fun setSectionsAndUpdate(
repositories: List<ProductItem.Section.Repository>?) { categories: List<ProductItem.Section.Category>?,
repositories: List<ProductItem.Section.Repository>?
) {
val oldCategories = collectOldSections(categories) val oldCategories = collectOldSections(categories)
val oldRepositories = collectOldSections(repositories) val oldRepositories = collectOldSections(repositories)
if (oldCategories == null || oldRepositories == null) { if (oldCategories == null || oldRepositories == null) {
@ -423,7 +453,8 @@ class TabsFragment: ScreenFragment() {
is ProductItem.Section.Category -> section.name is ProductItem.Section.Category -> section.name
is ProductItem.Section.Repository -> section.name is ProductItem.Section.Repository -> section.name
} }
layout?.sectionIcon?.visibility = if (sections.any { it !is ProductItem.Section.All }) View.VISIBLE else View.GONE layout?.sectionIcon?.visibility =
if (sections.any { it !is ProductItem.Section.All }) View.VISIBLE else View.GONE
productFragments.forEach { it.setSection(section) } productFragments.forEach { it.setSection(section) }
sectionsList?.adapter?.notifyDataSetChanged() sectionsList?.adapter?.notifyDataSetChanged()
} }
@ -439,7 +470,8 @@ class TabsFragment: ScreenFragment() {
if (value != target) { if (value != target) {
sectionsAnimator = ValueAnimator.ofFloat(value, target).apply { sectionsAnimator = ValueAnimator.ofFloat(value, target).apply {
duration = (250 * abs(target - value)).toLong() duration = (250 * abs(target - value)).toLong()
interpolator = if (target >= 1f) AccelerateInterpolator(2f) else DecelerateInterpolator(2f) interpolator =
if (target >= 1f) AccelerateInterpolator(2f) else DecelerateInterpolator(2f)
addUpdateListener { addUpdateListener {
val newValue = animatedValue as Float val newValue = animatedValue as Float
sectionsList.apply { sectionsList.apply {
@ -462,8 +494,12 @@ class TabsFragment: ScreenFragment() {
} }
} }
private val pageChangeCallback = object: ViewPager2.OnPageChangeCallback() { private val pageChangeCallback = object : ViewPager2.OnPageChangeCallback() {
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
val layout = layout!! val layout = layout!!
val fromSections = ProductsFragment.Source.values()[position].sections val fromSections = ProductsFragment.Source.values()[position].sections
val toSections = if (positionOffset <= 0f) fromSections else val toSections = if (positionOffset <= 0f) fromSections else
@ -490,8 +526,11 @@ class TabsFragment: ScreenFragment() {
val source = ProductsFragment.Source.values()[position] val source = ProductsFragment.Source.values()[position]
updateUpdateNotificationBlocker(source) updateUpdateNotificationBlocker(source)
sortOrderMenu!!.first.isVisible = source.order sortOrderMenu!!.first.isVisible = source.order
syncRepositoriesMenuItem!!.setShowAsActionFlags(if (!source.order || syncRepositoriesMenuItem!!.setShowAsActionFlags(
resources.configuration.screenWidthDp >= 400) MenuItem.SHOW_AS_ACTION_ALWAYS else 0) if (!source.order ||
resources.configuration.screenWidthDp >= 400
) MenuItem.SHOW_AS_ACTION_ALWAYS else 0
)
setSelectedTab(source) setSelectedTab(source)
if (showSections && !source.sections) { if (showSections && !source.sections) {
showSections = false showSections = false
@ -500,7 +539,8 @@ class TabsFragment: ScreenFragment() {
override fun onPageScrollStateChanged(state: Int) { override fun onPageScrollStateChanged(state: Int) {
val source = ProductsFragment.Source.values()[viewPager!!.currentItem] val source = ProductsFragment.Source.values()[viewPager!!.currentItem]
layout!!.sectionChange.isEnabled = state != ViewPager2.SCROLL_STATE_DRAGGING && source.sections layout!!.sectionChange.isEnabled =
state != ViewPager2.SCROLL_STATE_DRAGGING && source.sections
if (state == ViewPager2.SCROLL_STATE_IDLE) { if (state == ViewPager2.SCROLL_STATE_IDLE) {
// onPageSelected can be called earlier than fragments created // onPageSelected can be called earlier than fragments created
updateUpdateNotificationBlocker(source) updateUpdateNotificationBlocker(source)
@ -508,10 +548,10 @@ class TabsFragment: ScreenFragment() {
} }
} }
private class TabsBackgroundDrawable(context: Context, private val rtl: Boolean): Drawable() { private class TabsBackgroundDrawable(context: Context, private val rtl: Boolean) : Drawable() {
private val height = context.resources.sizeScaled(2) private val height = context.resources.sizeScaled(2)
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = context.getColorFromAttr(android.R.attr.colorPrimary).defaultColor color = context.getColorFromAttr(android.R.attr.textColor).defaultColor
} }
private var position = 0f private var position = 0f
@ -529,11 +569,18 @@ class TabsFragment: ScreenFragment() {
val width = bounds.width() / total.toFloat() val width = bounds.width() / total.toFloat()
val x = width * position val x = width * position
if (rtl) { if (rtl) {
canvas.drawRect(bounds.right - width - x, (bounds.bottom - height).toFloat(), canvas.drawRect(
bounds.right - x, bounds.bottom.toFloat(), paint) bounds.right - width - x, (bounds.bottom - height).toFloat(),
bounds.right - x, bounds.bottom.toFloat(), paint
)
} else { } else {
canvas.drawRect(bounds.left + x, (bounds.bottom - height).toFloat(), canvas.drawRect(
bounds.left + x + width, bounds.bottom.toFloat(), paint) bounds.left + x + width / 4, // + width/4 from start
(bounds.bottom - height).toFloat(),
bounds.left + x + width - width / 4, // - width/4 from end
bounds.bottom.toFloat(),
paint
)
} }
} }
} }
@ -543,12 +590,15 @@ class TabsFragment: ScreenFragment() {
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
} }
private class SectionsAdapter(private val sections: () -> List<ProductItem.Section>, private class SectionsAdapter(
private val onClick: (ProductItem.Section) -> Unit): StableRecyclerAdapter<SectionsAdapter.ViewType, private val sections: () -> List<ProductItem.Section>,
private val onClick: (ProductItem.Section) -> Unit
) : StableRecyclerAdapter<SectionsAdapter.ViewType,
RecyclerView.ViewHolder>() { RecyclerView.ViewHolder>() {
enum class ViewType { SECTION } enum class ViewType { SECTION }
private class SectionViewHolder(context: Context): RecyclerView.ViewHolder(TextView(context)) { private class SectionViewHolder(context: Context) :
RecyclerView.ViewHolder(TextView(context)) {
val title: TextView val title: TextView
get() = itemView as TextView get() = itemView as TextView
@ -556,24 +606,41 @@ class TabsFragment: ScreenFragment() {
itemView as TextView itemView as TextView
itemView.gravity = Gravity.CENTER_VERTICAL itemView.gravity = Gravity.CENTER_VERTICAL
itemView.resources.sizeScaled(16).let { itemView.setPadding(it, 0, it, 0) } itemView.resources.sizeScaled(16).let { itemView.setPadding(it, 0, it, 0) }
itemView.setTextColor(context.getColorFromAttr(android.R.attr.textColorPrimary)) itemView.setTextColor(context.getColorFromAttr(android.R.attr.textColor))
itemView.setTextSizeScaled(16) itemView.setTextSizeScaled(16)
itemView.background = context.getDrawableFromAttr(android.R.attr.selectableItemBackground) itemView.background =
itemView.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, context.getDrawableFromAttr(android.R.attr.selectableItemBackground)
itemView.resources.sizeScaled(48)) itemView.layoutParams = RecyclerView.LayoutParams(
RecyclerView.LayoutParams.MATCH_PARENT,
itemView.resources.sizeScaled(48)
)
} }
} }
fun configureDivider(context: Context, position: Int, configuration: DividerItemDecoration.Configuration) { fun configureDivider(
context: Context,
position: Int,
configuration: DividerItemDecoration.Configuration
) {
val currentSection = sections()[position] val currentSection = sections()[position]
val nextSection = sections().getOrNull(position + 1) val nextSection = sections().getOrNull(position + 1)
when { when {
nextSection != null && currentSection.javaClass != nextSection.javaClass -> { nextSection != null && currentSection.javaClass != nextSection.javaClass -> {
val padding = context.resources.sizeScaled(16) val padding = context.resources.sizeScaled(16)
configuration.set(true, false, padding, padding) configuration.set(
needDivider = true,
toTop = false,
paddingStart = padding,
paddingEnd = padding
)
} }
else -> { else -> {
configuration.set(false, false, 0, 0) configuration.set(
needDivider = false,
toTop = false,
paddingStart = 0,
paddingEnd = 0
)
} }
} }
} }
@ -585,7 +652,10 @@ class TabsFragment: ScreenFragment() {
override fun getItemDescriptor(position: Int): String = sections()[position].toString() override fun getItemDescriptor(position: Int): String = sections()[position].toString()
override fun getItemEnumViewType(position: Int): ViewType = ViewType.SECTION override fun getItemEnumViewType(position: Int): ViewType = ViewType.SECTION
override fun onCreateViewHolder(parent: ViewGroup, viewType: ViewType): RecyclerView.ViewHolder { override fun onCreateViewHolder(
parent: ViewGroup,
viewType: ViewType
): RecyclerView.ViewHolder {
return SectionViewHolder(parent.context).apply { return SectionViewHolder(parent.context).apply {
itemView.setOnClickListener { onClick(sections()[adapterPosition]) } itemView.setOnClickListener { onClick(sections()[adapterPosition]) }
} }
@ -599,9 +669,11 @@ class TabsFragment: ScreenFragment() {
val margin = holder.itemView.resources.sizeScaled(8) val margin = holder.itemView.resources.sizeScaled(8)
val layoutParams = holder.itemView.layoutParams as RecyclerView.LayoutParams val layoutParams = holder.itemView.layoutParams as RecyclerView.LayoutParams
layoutParams.topMargin = if (previousSection == null || layoutParams.topMargin = if (previousSection == null ||
section.javaClass != previousSection.javaClass) margin else 0 section.javaClass != previousSection.javaClass
) margin else 0
layoutParams.bottomMargin = if (nextSection == null || layoutParams.bottomMargin = if (nextSection == null ||
section.javaClass != nextSection.javaClass) margin else 0 section.javaClass != nextSection.javaClass
) margin else 0
holder.title.text = when (section) { holder.title.text = when (section) {
is ProductItem.Section.All -> holder.itemView.resources.getString(R.string.all_applications) is ProductItem.Section.All -> holder.itemView.resources.getString(R.string.all_applications)
is ProductItem.Section.Category -> section.name is ProductItem.Section.Category -> section.name