Merge pull request #97 from lucasnlm/implement-new-2d-grid-layout
Implement new 2d grid layout
|
@ -43,7 +43,8 @@ class HistoryAdapter(
|
|||
SaveStatus.VICTORY -> ContextCompat.getColor(context, R.color.victory)
|
||||
SaveStatus.ON_GOING -> ContextCompat.getColor(context, R.color.ongoing)
|
||||
SaveStatus.DEFEAT -> ContextCompat.getColor(context, R.color.lose)
|
||||
}, PorterDuff.Mode.SRC_IN
|
||||
},
|
||||
PorterDuff.Mode.SRC_IN
|
||||
)
|
||||
|
||||
holder.minefieldSize.text = String.format("%d x %d", minefield.width, minefield.height)
|
||||
|
|
|
@ -79,10 +79,12 @@ open class LevelFragment : DaggerFragment() {
|
|||
|
||||
withContext(Dispatchers.Main) {
|
||||
recyclerGrid.apply {
|
||||
val horizontalPadding = calcHorizontalPadding(levelSetup.width)
|
||||
val verticalPadding = calcVerticalPadding(levelSetup.height)
|
||||
setHasFixedSize(true)
|
||||
addItemDecoration(SpaceItemDecoration(R.dimen.field_padding))
|
||||
isNestedScrollingEnabled = false
|
||||
layoutManager = makeNewLayoutManager(levelSetup.width, levelSetup.height)
|
||||
setPadding(horizontalPadding, verticalPadding, 0, 0)
|
||||
layoutManager = makeNewLayoutManager(levelSetup.width)
|
||||
adapter = areaAdapter
|
||||
alpha = 0.0f
|
||||
|
||||
|
@ -105,7 +107,12 @@ open class LevelFragment : DaggerFragment() {
|
|||
levelSetup.observe(
|
||||
viewLifecycleOwner,
|
||||
Observer {
|
||||
recyclerGrid.layoutManager = makeNewLayoutManager(it.width, it.height)
|
||||
recyclerGrid.apply {
|
||||
val horizontalPadding = calcHorizontalPadding(it.width)
|
||||
val verticalPadding = calcVerticalPadding(it.height)
|
||||
layoutManager = makeNewLayoutManager(it.width)
|
||||
setPadding(horizontalPadding, verticalPadding, 0, 0)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -165,16 +172,22 @@ open class LevelFragment : DaggerFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun makeNewLayoutManager(boardWidth: Int, boardHeight: Int) =
|
||||
FixedGridLayoutManager(boardWidth, calcHorizontalPadding(boardWidth), calcVerticalPadding(boardHeight))
|
||||
private fun makeNewLayoutManager(boardWidth: Int) =
|
||||
FixedGridLayoutManager().apply {
|
||||
setTotalColumnCount(boardWidth)
|
||||
}
|
||||
|
||||
private fun calcHorizontalPadding(boardWidth: Int): Int =
|
||||
((recyclerGrid.measuredWidth - dimensionRepository.areaSizeWithPadding() * boardWidth) / 2)
|
||||
.coerceAtLeast(0.0f)
|
||||
.toInt()
|
||||
private fun calcHorizontalPadding(boardWidth: Int): Int {
|
||||
val width = recyclerGrid.measuredWidth
|
||||
val recyclerViewWidth = (dimensionRepository.areaSize() * boardWidth)
|
||||
val separatorsWidth = (dimensionRepository.areaSeparator() * (boardWidth - 1))
|
||||
return ((width - recyclerViewWidth - separatorsWidth) / 2).coerceAtLeast(0.0f).toInt()
|
||||
}
|
||||
|
||||
private fun calcVerticalPadding(boardHeight: Int): Int =
|
||||
((recyclerGrid.measuredHeight - dimensionRepository.areaSizeWithPadding() * boardHeight) / 2)
|
||||
.coerceAtLeast(0.0f)
|
||||
.toInt()
|
||||
private fun calcVerticalPadding(boardHeight: Int): Int {
|
||||
val height = recyclerGrid.measuredHeight
|
||||
val recyclerViewHeight = (dimensionRepository.areaSize() * boardHeight)
|
||||
val separatorsHeight = (dimensionRepository.areaSeparator() * (boardHeight - 1))
|
||||
return ((height - recyclerViewHeight - separatorsHeight) / 2).coerceAtLeast(0.0f).toInt()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,16 +11,16 @@ import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository
|
|||
import dev.lucasnlm.antimine.common.level.repository.IMinefieldRepository
|
||||
import dev.lucasnlm.antimine.common.level.repository.ISavesRepository
|
||||
import dev.lucasnlm.antimine.common.level.repository.IStatsRepository
|
||||
import dev.lucasnlm.antimine.common.level.repository.MemorySavesRepository
|
||||
import dev.lucasnlm.antimine.common.level.repository.MemoryStatsRepository
|
||||
import dev.lucasnlm.antimine.common.level.utils.Clock
|
||||
import dev.lucasnlm.antimine.common.level.utils.IHapticFeedbackInteractor
|
||||
import dev.lucasnlm.antimine.common.level.viewmodel.GameViewModelFactory
|
||||
import dev.lucasnlm.antimine.core.analytics.AnalyticsManager
|
||||
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
|
||||
import dev.lucasnlm.antimine.mocks.MockDimensionRepository
|
||||
import dev.lucasnlm.antimine.mocks.MockHapticFeedbackInteractor
|
||||
import dev.lucasnlm.antimine.mocks.MockMinefieldRepository
|
||||
import dev.lucasnlm.antimine.mocks.MockSavesRepository
|
||||
import dev.lucasnlm.antimine.mocks.MockStatsRepository
|
||||
import dev.lucasnlm.antimine.mocks.DisabledHapticFeedbackInteractor
|
||||
import dev.lucasnlm.antimine.mocks.FixedDimensionRepository
|
||||
import dev.lucasnlm.antimine.mocks.FixedMinefieldRepository
|
||||
|
||||
@Module
|
||||
class TestLevelModule(
|
||||
|
@ -61,20 +61,20 @@ class TestLevelModule(
|
|||
override fun provideDimensionRepository(
|
||||
context: Context,
|
||||
preferencesRepository: IPreferencesRepository
|
||||
): IDimensionRepository = MockDimensionRepository()
|
||||
): IDimensionRepository = FixedDimensionRepository()
|
||||
|
||||
@Provides
|
||||
override fun provideSavesRepository(): ISavesRepository = MockSavesRepository()
|
||||
override fun provideSavesRepository(): ISavesRepository = MemorySavesRepository()
|
||||
|
||||
@Provides
|
||||
override fun provideStatsRepository(): IStatsRepository = MockStatsRepository(listOf())
|
||||
override fun provideStatsRepository(): IStatsRepository = MemoryStatsRepository()
|
||||
|
||||
@Provides
|
||||
override fun provideMinefieldRepository(): IMinefieldRepository = MockMinefieldRepository()
|
||||
override fun provideMinefieldRepository(): IMinefieldRepository = FixedMinefieldRepository()
|
||||
|
||||
@Provides
|
||||
override fun provideHapticFeedbackInteractor(
|
||||
application: Application,
|
||||
preferencesRepository: IPreferencesRepository
|
||||
): IHapticFeedbackInteractor = MockHapticFeedbackInteractor()
|
||||
): IHapticFeedbackInteractor = DisabledHapticFeedbackInteractor()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package dev.lucasnlm.antimine.mocks
|
||||
|
||||
import dev.lucasnlm.antimine.common.level.utils.IHapticFeedbackInteractor
|
||||
|
||||
class DisabledHapticFeedbackInteractor : IHapticFeedbackInteractor {
|
||||
override fun toggleFlagFeedback() { }
|
||||
|
||||
override fun explosionFeedback() { }
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package dev.lucasnlm.antimine.mocks
|
||||
|
||||
import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository
|
||||
import dev.lucasnlm.antimine.common.level.repository.Size
|
||||
|
||||
class FixedDimensionRepository : IDimensionRepository {
|
||||
override fun areaSize(): Float = 50.0f
|
||||
|
||||
override fun areaSizeWithPadding(): Float {
|
||||
return areaSize() + 2 * areaSeparator()
|
||||
}
|
||||
|
||||
override fun areaSeparator(): Float = 1.0f
|
||||
|
||||
override fun displaySize(): Size = Size(50 * 15, 50 * 30)
|
||||
|
||||
override fun actionBarSize(): Int = 50
|
||||
}
|
|
@ -6,7 +6,7 @@ import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository
|
|||
import dev.lucasnlm.antimine.common.level.repository.IMinefieldRepository
|
||||
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
|
||||
|
||||
class MockMinefieldRepository : IMinefieldRepository {
|
||||
class FixedMinefieldRepository : IMinefieldRepository {
|
||||
override fun fromDifficulty(
|
||||
difficulty: Difficulty,
|
||||
dimensionRepository: IDimensionRepository,
|
|
@ -1,14 +0,0 @@
|
|||
package dev.lucasnlm.antimine.mocks
|
||||
|
||||
import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository
|
||||
import dev.lucasnlm.antimine.common.level.repository.Size
|
||||
|
||||
class MockDimensionRepository : IDimensionRepository {
|
||||
override fun areaSize(): Float = 50.0f
|
||||
|
||||
override fun areaSizeWithPadding(): Float = 52.0f
|
||||
|
||||
override fun displaySize(): Size = Size(50 * 20, 50 * 30)
|
||||
|
||||
override fun actionBarSize(): Int = 50
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
package dev.lucasnlm.antimine.mocks
|
||||
|
||||
import dev.lucasnlm.antimine.common.level.utils.IHapticFeedbackInteractor
|
||||
|
||||
class MockHapticFeedbackInteractor : IHapticFeedbackInteractor {
|
||||
override fun toggleFlagFeedback() {
|
||||
// Empty
|
||||
}
|
||||
|
||||
override fun explosionFeedback() {
|
||||
// Empty
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
package dev.lucasnlm.antimine.mocks
|
||||
|
||||
import dev.lucasnlm.antimine.common.level.database.models.Save
|
||||
import dev.lucasnlm.antimine.common.level.repository.ISavesRepository
|
||||
|
||||
class MockSavesRepository : ISavesRepository {
|
||||
override suspend fun getAllSaves(): List<Save> {
|
||||
return listOf()
|
||||
}
|
||||
|
||||
override suspend fun fetchCurrentSave(): Save? {
|
||||
return null
|
||||
}
|
||||
|
||||
override suspend fun loadFromId(id: Int): Save? {
|
||||
return null
|
||||
}
|
||||
|
||||
override suspend fun saveGame(save: Save): Long? {
|
||||
return 1
|
||||
}
|
||||
|
||||
override fun setLimit(maxSavesStorage: Int) {
|
||||
// Empty
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
package dev.lucasnlm.antimine.mocks
|
||||
|
||||
import dev.lucasnlm.antimine.common.level.database.models.Stats
|
||||
import dev.lucasnlm.antimine.common.level.repository.IStatsRepository
|
||||
|
||||
class MockStatsRepository(
|
||||
private val list: List<Stats>
|
||||
) : IStatsRepository {
|
||||
override suspend fun getAllStats(): List<Stats> = list
|
||||
|
||||
override suspend fun addStats(stats: Stats): Long? {
|
||||
return null
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
package dev.lucasnlm.antimine.stats.viewmodel
|
||||
|
||||
import dev.lucasnlm.antimine.common.level.database.models.Stats
|
||||
import dev.lucasnlm.antimine.mocks.MockStatsRepository
|
||||
import dev.lucasnlm.antimine.common.level.repository.MemoryStatsRepository
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.Assert.assertEquals
|
||||
|
@ -17,60 +17,60 @@ class StatsViewModelTest {
|
|||
@Test
|
||||
fun testStatsTotalGames() = runBlockingTest {
|
||||
val viewModel = StatsViewModel()
|
||||
val statsModel = viewModel.getStatsModel(MockStatsRepository(listOfStats))
|
||||
val statsModel = viewModel.getStatsModel(MemoryStatsRepository(listOfStats.toMutableList()))
|
||||
assertEquals(2, statsModel?.totalGames)
|
||||
|
||||
val emptyStatsModel = viewModel.getStatsModel(MockStatsRepository(listOf()))
|
||||
val emptyStatsModel = viewModel.getStatsModel(MemoryStatsRepository())
|
||||
assertEquals(0, emptyStatsModel?.totalGames)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testStatsDuration() = runBlockingTest {
|
||||
val viewModel = StatsViewModel()
|
||||
val statsModel = viewModel.getStatsModel(MockStatsRepository(listOfStats))
|
||||
val statsModel = viewModel.getStatsModel(MemoryStatsRepository(listOfStats.toMutableList()))
|
||||
assertEquals(2200L, statsModel?.duration)
|
||||
|
||||
val emptyStatsModel = viewModel.getStatsModel(MockStatsRepository(listOf()))
|
||||
val emptyStatsModel = viewModel.getStatsModel(MemoryStatsRepository())
|
||||
assertEquals(0L, emptyStatsModel?.duration)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testStatsAverageDuration() = runBlockingTest {
|
||||
val viewModel = StatsViewModel()
|
||||
val statsModel = viewModel.getStatsModel(MockStatsRepository(listOfStats))
|
||||
val statsModel = viewModel.getStatsModel(MemoryStatsRepository(listOfStats.toMutableList()))
|
||||
assertEquals(1100L, statsModel?.averageDuration)
|
||||
|
||||
val emptyStatsModel = viewModel.getStatsModel(MockStatsRepository(listOf()))
|
||||
val emptyStatsModel = viewModel.getStatsModel(MemoryStatsRepository(mutableListOf()))
|
||||
assertEquals(0L, emptyStatsModel?.averageDuration)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testStatsMines() = runBlockingTest {
|
||||
val viewModel = StatsViewModel()
|
||||
val statsModel = viewModel.getStatsModel(MockStatsRepository(listOfStats))
|
||||
val statsModel = viewModel.getStatsModel(MemoryStatsRepository(listOfStats.toMutableList()))
|
||||
assertEquals(34, statsModel?.mines)
|
||||
|
||||
val emptyStatsModel = viewModel.getStatsModel(MockStatsRepository(listOf()))
|
||||
val emptyStatsModel = viewModel.getStatsModel(MemoryStatsRepository(mutableListOf()))
|
||||
assertEquals(0, emptyStatsModel?.mines)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testVictory() = runBlockingTest {
|
||||
val viewModel = StatsViewModel()
|
||||
val statsModel = viewModel.getStatsModel(MockStatsRepository(listOfStats))
|
||||
val statsModel = viewModel.getStatsModel(MemoryStatsRepository(listOfStats.toMutableList()))
|
||||
assertEquals(1, statsModel?.victory)
|
||||
|
||||
val emptyStatsModel = viewModel.getStatsModel(MockStatsRepository(listOf()))
|
||||
val emptyStatsModel = viewModel.getStatsModel(MemoryStatsRepository(mutableListOf()))
|
||||
assertEquals(0, emptyStatsModel?.victory)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOpenArea() = runBlockingTest {
|
||||
val viewModel = StatsViewModel()
|
||||
val statsModel = viewModel.getStatsModel(MockStatsRepository(listOfStats))
|
||||
val statsModel = viewModel.getStatsModel(MemoryStatsRepository(listOfStats.toMutableList()))
|
||||
assertEquals(110, statsModel?.openArea)
|
||||
|
||||
val emptyStatsModel = viewModel.getStatsModel(MockStatsRepository(listOf()))
|
||||
val emptyStatsModel = viewModel.getStatsModel(MemoryStatsRepository(mutableListOf()))
|
||||
assertEquals(0, emptyStatsModel?.openArea)
|
||||
}
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 627 B After Width: | Height: | Size: 664 B |
Before Width: | Height: | Size: 639 B After Width: | Height: | Size: 799 B |
Before Width: | Height: | Size: 867 B After Width: | Height: | Size: 864 B |
Before Width: | Height: | Size: 907 B After Width: | Height: | Size: 969 B |
Before Width: | Height: | Size: 605 B After Width: | Height: | Size: 647 B |
Before Width: | Height: | Size: 639 B After Width: | Height: | Size: 799 B |
Before Width: | Height: | Size: 627 B After Width: | Height: | Size: 664 B |
Before Width: | Height: | Size: 639 B After Width: | Height: | Size: 799 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 825 B After Width: | Height: | Size: 865 B |
Before Width: | Height: | Size: 990 B After Width: | Height: | Size: 1,001 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 592 B After Width: | Height: | Size: 635 B |
Before Width: | Height: | Size: 730 B After Width: | Height: | Size: 726 B |
Before Width: | Height: | Size: 986 B After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2 KiB |
|
@ -9,6 +9,7 @@ import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
|
|||
interface IDimensionRepository {
|
||||
fun areaSize(): Float
|
||||
fun areaSizeWithPadding(): Float
|
||||
fun areaSeparator(): Float
|
||||
fun displaySize(): Size
|
||||
fun actionBarSize(): Int
|
||||
}
|
||||
|
@ -29,8 +30,12 @@ class DimensionRepository(
|
|||
context.resources.getDimension(R.dimen.field_size)
|
||||
}
|
||||
|
||||
override fun areaSeparator(): Float {
|
||||
return context.resources.getDimension(R.dimen.field_padding)
|
||||
}
|
||||
|
||||
override fun areaSizeWithPadding(): Float {
|
||||
return areaSize() + 2 * context.resources.getDimension(R.dimen.field_padding)
|
||||
return areaSize() + 2 * areaSeparator()
|
||||
}
|
||||
|
||||
override fun displaySize(): Size = with(Resources.getSystem().displayMetrics) {
|
||||
|
|
|
@ -37,3 +37,27 @@ class SavesRepository @Inject constructor(
|
|||
private const val MAX_STORAGE = 15
|
||||
}
|
||||
}
|
||||
|
||||
class MemorySavesRepository : ISavesRepository {
|
||||
private var memoryList = mutableListOf<Save>()
|
||||
private var maxSavesStorage = -1
|
||||
|
||||
override suspend fun getAllSaves(): List<Save> = memoryList.toList()
|
||||
|
||||
override suspend fun fetchCurrentSave(): Save? = memoryList.lastOrNull()
|
||||
|
||||
override suspend fun loadFromId(id: Int): Save? = memoryList.find { it.uid == id }
|
||||
|
||||
override suspend fun saveGame(save: Save): Long? {
|
||||
if (maxSavesStorage - 1 > 0) {
|
||||
memoryList = memoryList.subList(0, maxSavesStorage - 1)
|
||||
}
|
||||
memoryList.add(save)
|
||||
return memoryList.count().toLong()
|
||||
}
|
||||
|
||||
override fun setLimit(maxSavesStorage: Int) {
|
||||
this.maxSavesStorage = maxSavesStorage
|
||||
memoryList = memoryList.subList(0, maxSavesStorage)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,3 +19,14 @@ class StatsRepository(
|
|||
return statsDao.insertAll(stats).firstOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
class MemoryStatsRepository(
|
||||
private val memoryStats: MutableList<Stats> = mutableListOf()
|
||||
) : IStatsRepository {
|
||||
override suspend fun getAllStats(): List<Stats> = memoryStats.toList()
|
||||
|
||||
override suspend fun addStats(stats: Stats): Long? {
|
||||
memoryStats.add(stats)
|
||||
return memoryStats.count().toLong()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,3 +42,9 @@ class HapticFeedbackInteractor(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DisabledIHapticFeedbackInteractor : IHapticFeedbackInteractor {
|
||||
override fun toggleFlagFeedback() { }
|
||||
|
||||
override fun explosionFeedback() { }
|
||||
}
|
||||
|
|
|
@ -0,0 +1,937 @@
|
|||
package dev.lucasnlm.antimine.common.level.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.PointF;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.util.SparseIntArray;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.LinearSmoothScroller;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A {@link RecyclerView.LayoutManager} implementation
|
||||
* that places children in a two-dimensional grid, sized to a fixed column count
|
||||
* value. User scrolling is possible in both horizontal and vertical directions
|
||||
* to view the data set.
|
||||
*
|
||||
* <p>The column count is controllable via {@link #setTotalColumnCount(int)}. The layout manager
|
||||
* will generate the number of rows necessary to accommodate the data set based on
|
||||
* the fixed column count.
|
||||
*
|
||||
* <p>This manager does make some assumptions to simplify the implementation:
|
||||
* <ul>
|
||||
* <li>All child views are assumed to be the same size</li>
|
||||
* <li>The window of visible views is a constant</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class FixedGridLayoutManager extends RecyclerView.LayoutManager {
|
||||
|
||||
private static final String TAG = FixedGridLayoutManager.class.getSimpleName();
|
||||
|
||||
private static final int DEFAULT_COUNT = 1;
|
||||
|
||||
/* View Removal Constants */
|
||||
private static final int REMOVE_VISIBLE = 0;
|
||||
private static final int REMOVE_INVISIBLE = 1;
|
||||
|
||||
/* Fill Direction Constants */
|
||||
private static final int DIRECTION_NONE = -1;
|
||||
private static final int DIRECTION_START = 0;
|
||||
private static final int DIRECTION_END = 1;
|
||||
private static final int DIRECTION_UP = 2;
|
||||
private static final int DIRECTION_DOWN = 3;
|
||||
|
||||
/* First (top-left) position visible at any point */
|
||||
private int mFirstVisiblePosition;
|
||||
/* Consistent size applied to all child views */
|
||||
private int mDecoratedChildWidth;
|
||||
private int mDecoratedChildHeight;
|
||||
/* Number of columns that exist in the grid */
|
||||
private int mTotalColumnCount = DEFAULT_COUNT;
|
||||
/* Metrics for the visible window of our data */
|
||||
private int mVisibleColumnCount;
|
||||
private int mVisibleRowCount;
|
||||
|
||||
/* Used for tracking off-screen change events */
|
||||
private int mFirstChangedPosition;
|
||||
private int mChangedPositionCount;
|
||||
|
||||
/**
|
||||
* Set the number of columns the layout manager will use. This will
|
||||
* trigger a layout update.
|
||||
* @param count Number of columns.
|
||||
*/
|
||||
public void setTotalColumnCount(int count) {
|
||||
mTotalColumnCount = count;
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
/*
|
||||
* You must return true from this method if you want your
|
||||
* LayoutManager to support anything beyond "simple" item
|
||||
* animations. Enabling this causes onLayoutChildren() to
|
||||
* be called twice on each animated change; once for a
|
||||
* pre-layout, and again for the real layout.
|
||||
*/
|
||||
@Override
|
||||
public boolean supportsPredictiveItemAnimations() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Called by RecyclerView when a view removal is triggered. This is called
|
||||
* before onLayoutChildren() in pre-layout if the views removed are not visible. We
|
||||
* use it in this case to inform pre-layout that a removal took place.
|
||||
*
|
||||
* This method is still called if the views removed were visible, but it will
|
||||
* happen AFTER pre-layout.
|
||||
*/
|
||||
@Override
|
||||
public void onItemsRemoved(@NonNull RecyclerView recyclerView, int positionStart, int itemCount) {
|
||||
mFirstChangedPosition = positionStart;
|
||||
mChangedPositionCount = itemCount;
|
||||
}
|
||||
|
||||
/*
|
||||
* This method is your initial call from the framework. You will receive it when you
|
||||
* need to start laying out the initial set of views. This method will not be called
|
||||
* repeatedly, so don't rely on it to continually process changes during user
|
||||
* interaction.
|
||||
*
|
||||
* This method will be called when the data set in the adapter changes, so it can be
|
||||
* used to update a layout based on a new item count.
|
||||
*
|
||||
* If predictive animations are enabled, you will see this called twice. First, with
|
||||
* state.isPreLayout() returning true to lay out children in their initial conditions.
|
||||
* Then again to lay out children in their final locations.
|
||||
*/
|
||||
@Override
|
||||
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
|
||||
// We have nothing to show for an empty data set but clear any existing views
|
||||
if (getItemCount() == 0) {
|
||||
detachAndScrapAttachedViews(recycler);
|
||||
return;
|
||||
}
|
||||
if (getChildCount() == 0 && state.isPreLayout()) {
|
||||
// Nothing to do during prelayout when empty
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear change tracking state when a real layout occurs
|
||||
if (!state.isPreLayout()) {
|
||||
mFirstChangedPosition = mChangedPositionCount = 0;
|
||||
}
|
||||
|
||||
if (getChildCount() == 0) { // First or empty layout
|
||||
// Scrap measure one child
|
||||
View scrap = recycler.getViewForPosition(0);
|
||||
addView(scrap);
|
||||
measureChildWithMargins(scrap, 0, 0);
|
||||
|
||||
/*
|
||||
* We make some assumptions in this code based on every child
|
||||
* view being the same size (i.e. a uniform grid). This allows
|
||||
* us to compute the following values up front because they
|
||||
* won't change.
|
||||
*/
|
||||
mDecoratedChildWidth = getDecoratedMeasuredWidth(scrap);
|
||||
mDecoratedChildHeight = getDecoratedMeasuredHeight(scrap);
|
||||
|
||||
detachAndScrapView(scrap, recycler);
|
||||
}
|
||||
|
||||
// Always update the visible row/column counts
|
||||
updateWindowSizing();
|
||||
|
||||
SparseIntArray removedCache = null;
|
||||
/*
|
||||
* During pre-layout, we need to take note of any views that are
|
||||
* being removed in order to handle predictive animations
|
||||
*/
|
||||
if (state.isPreLayout()) {
|
||||
removedCache = new SparseIntArray(getChildCount());
|
||||
for (int i=0; i < getChildCount(); i++) {
|
||||
final View view = getChildAt(i);
|
||||
if (view != null) {
|
||||
LayoutParams lp = (LayoutParams) view.getLayoutParams();
|
||||
|
||||
if (lp.isItemRemoved()) {
|
||||
// Track these view removals as visible
|
||||
removedCache.put(lp.getViewLayoutPosition(), REMOVE_VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Track view removals that happened out of bounds (i.e. off-screen)
|
||||
if (removedCache.size() == 0 && mChangedPositionCount > 0) {
|
||||
for (int i = mFirstChangedPosition; i < (mFirstChangedPosition + mChangedPositionCount); i++) {
|
||||
removedCache.put(i, REMOVE_INVISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
int childLeft;
|
||||
int childTop;
|
||||
if (getChildCount() == 0) { // First or empty layout
|
||||
// Reset the visible and scroll positions
|
||||
mFirstVisiblePosition = 0;
|
||||
childLeft = getPaddingLeft();
|
||||
childTop = getPaddingTop();
|
||||
} else if (!state.isPreLayout()
|
||||
&& getVisibleChildCount() >= state.getItemCount()) {
|
||||
// Data set is too small to scroll fully, just reset position
|
||||
mFirstVisiblePosition = 0;
|
||||
childLeft = getPaddingLeft();
|
||||
childTop = getPaddingTop();
|
||||
} else { // Adapter data set changes
|
||||
/*
|
||||
* Keep the existing initial position, and save off
|
||||
* the current scrolled offset.
|
||||
*/
|
||||
final View topChild = getChildAt(0);
|
||||
childLeft = topChild != null ? getDecoratedLeft(topChild) : 0;
|
||||
childTop = topChild != null ? getDecoratedTop(topChild) : 0;
|
||||
|
||||
/*
|
||||
* When data set is too small to scroll vertically, adjust vertical offset
|
||||
* and shift position to the first row, preserving current column
|
||||
*/
|
||||
if (!state.isPreLayout() && getVerticalSpace() > (getTotalRowCount() * mDecoratedChildHeight)) {
|
||||
mFirstVisiblePosition = mFirstVisiblePosition % getTotalColumnCount();
|
||||
childTop = getPaddingTop();
|
||||
|
||||
// If the shift overscrolls the column max, back it off
|
||||
if ((mFirstVisiblePosition + mVisibleColumnCount) > state.getItemCount()) {
|
||||
mFirstVisiblePosition = Math.max(state.getItemCount() - mVisibleColumnCount, 0);
|
||||
childLeft = getPaddingLeft();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Adjust the visible position if out of bounds in the
|
||||
* new layout. This occurs when the new item count in an adapter
|
||||
* is much smaller than it was before, and you are scrolled to
|
||||
* a location where no items would exist.
|
||||
*/
|
||||
int maxFirstRow = getTotalRowCount() - (mVisibleRowCount-1);
|
||||
int maxFirstCol = getTotalColumnCount() - (mVisibleColumnCount-1);
|
||||
boolean isOutOfRowBounds = getFirstVisibleRow() > maxFirstRow;
|
||||
boolean isOutOfColBounds = getFirstVisibleColumn() > maxFirstCol;
|
||||
if (isOutOfRowBounds || isOutOfColBounds) {
|
||||
int firstRow;
|
||||
if (isOutOfRowBounds) {
|
||||
firstRow = maxFirstRow;
|
||||
} else {
|
||||
firstRow = getFirstVisibleRow();
|
||||
}
|
||||
int firstCol;
|
||||
if (isOutOfColBounds) {
|
||||
firstCol = maxFirstCol;
|
||||
} else {
|
||||
firstCol = getFirstVisibleColumn();
|
||||
}
|
||||
mFirstVisiblePosition = firstRow * getTotalColumnCount() + firstCol;
|
||||
|
||||
childLeft = getHorizontalSpace() - (mDecoratedChildWidth * mVisibleColumnCount);
|
||||
childTop = getVerticalSpace() - (mDecoratedChildHeight * mVisibleRowCount);
|
||||
|
||||
// Correct cases where shifting to the bottom-right overscrolls the top-left
|
||||
// This happens on data sets too small to scroll in a direction.
|
||||
if (getFirstVisibleRow() == 0) {
|
||||
childTop = Math.min(childTop, getPaddingTop());
|
||||
}
|
||||
if (getFirstVisibleColumn() == 0) {
|
||||
childLeft = Math.min(childLeft, getPaddingLeft());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear all attached views into the recycle bin
|
||||
detachAndScrapAttachedViews(recycler);
|
||||
|
||||
// Fill the grid for the initial layout of views
|
||||
fillGrid(DIRECTION_NONE, childLeft, childTop, recycler, state, removedCache);
|
||||
|
||||
// Evaluate any disappearing views that may exist
|
||||
if (!state.isPreLayout() && !recycler.getScrapList().isEmpty()) {
|
||||
final List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList();
|
||||
final HashSet<View> disappearingViews = new HashSet(scrapList.size());
|
||||
|
||||
for (RecyclerView.ViewHolder holder : scrapList) {
|
||||
final View child = holder.itemView;
|
||||
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
|
||||
if (!lp.isItemRemoved()) {
|
||||
disappearingViews.add(child);
|
||||
}
|
||||
}
|
||||
|
||||
for (View child : disappearingViews) {
|
||||
layoutDisappearingView(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) {
|
||||
// Completely scrap the existing layout
|
||||
removeAllViews();
|
||||
}
|
||||
|
||||
/*
|
||||
* Rather than continuously checking how many views we can fit
|
||||
* based on scroll offsets, we simplify the math by computing the
|
||||
* visible grid as what will initially fit on screen, plus one.
|
||||
*/
|
||||
private void updateWindowSizing() {
|
||||
mVisibleColumnCount = (getHorizontalSpace() / mDecoratedChildWidth) + 1;
|
||||
if (getHorizontalSpace() % mDecoratedChildWidth > 0) {
|
||||
mVisibleColumnCount++;
|
||||
}
|
||||
|
||||
// Allow minimum value for small data sets
|
||||
if (mVisibleColumnCount > getTotalColumnCount()) {
|
||||
mVisibleColumnCount = getTotalColumnCount();
|
||||
}
|
||||
|
||||
|
||||
mVisibleRowCount = (getVerticalSpace()/ mDecoratedChildHeight) + 1;
|
||||
if (getVerticalSpace() % mDecoratedChildHeight > 0) {
|
||||
mVisibleRowCount++;
|
||||
}
|
||||
|
||||
if (mVisibleRowCount > getTotalRowCount()) {
|
||||
mVisibleRowCount = getTotalRowCount();
|
||||
}
|
||||
}
|
||||
|
||||
private void fillGrid(int direction, RecyclerView.Recycler recycler, RecyclerView.State state) {
|
||||
fillGrid(direction, 0, 0, recycler, state, null);
|
||||
}
|
||||
|
||||
private void fillGrid(int direction, int emptyLeft, int emptyTop,
|
||||
RecyclerView.Recycler recycler,
|
||||
RecyclerView.State state,
|
||||
SparseIntArray removedPositions) {
|
||||
if (mFirstVisiblePosition < 0) mFirstVisiblePosition = 0;
|
||||
if (mFirstVisiblePosition >= getItemCount()) mFirstVisiblePosition = (getItemCount() - 1);
|
||||
|
||||
/*
|
||||
* First, we will detach all existing views from the layout.
|
||||
* detachView() is a lightweight operation that we can use to
|
||||
* quickly reorder views without a full add/remove.
|
||||
*/
|
||||
SparseArray<View> viewCache = new SparseArray<View>(getChildCount());
|
||||
int startLeftOffset = emptyLeft;
|
||||
int startTopOffset = emptyTop;
|
||||
if (getChildCount() != 0) {
|
||||
final View topView = getChildAt(0);
|
||||
startLeftOffset = topView != null ? getDecoratedLeft(topView) : 0;
|
||||
startTopOffset = topView != null ? getDecoratedTop(topView) : 0;
|
||||
switch (direction) {
|
||||
case DIRECTION_START:
|
||||
startLeftOffset -= mDecoratedChildWidth;
|
||||
break;
|
||||
case DIRECTION_END:
|
||||
startLeftOffset += mDecoratedChildWidth;
|
||||
break;
|
||||
case DIRECTION_UP:
|
||||
startTopOffset -= mDecoratedChildHeight;
|
||||
break;
|
||||
case DIRECTION_DOWN:
|
||||
startTopOffset += mDecoratedChildHeight;
|
||||
break;
|
||||
}
|
||||
|
||||
// Cache all views by their existing position, before updating counts
|
||||
for (int i=0; i < getChildCount(); i++) {
|
||||
int position = positionOfIndex(i);
|
||||
final View child = getChildAt(i);
|
||||
viewCache.put(position, child);
|
||||
}
|
||||
|
||||
// Temporarily detach all views.
|
||||
// Views we still need will be added back at the proper index.
|
||||
for (int i=0; i < viewCache.size(); i++) {
|
||||
detachView(viewCache.valueAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Next, we advance the visible position based on the fill direction.
|
||||
* DIRECTION_NONE doesn't advance the position in any direction.
|
||||
*/
|
||||
switch (direction) {
|
||||
case DIRECTION_START:
|
||||
mFirstVisiblePosition--;
|
||||
break;
|
||||
case DIRECTION_END:
|
||||
mFirstVisiblePosition++;
|
||||
break;
|
||||
case DIRECTION_UP:
|
||||
mFirstVisiblePosition -= getTotalColumnCount();
|
||||
break;
|
||||
case DIRECTION_DOWN:
|
||||
mFirstVisiblePosition += getTotalColumnCount();
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
* Next, we supply the grid of items that are deemed visible.
|
||||
* If these items were previously there, they will simply be
|
||||
* re-attached. New views that must be created are obtained
|
||||
* from the Recycler and added.
|
||||
*/
|
||||
int leftOffset = startLeftOffset;
|
||||
int topOffset = startTopOffset;
|
||||
|
||||
for (int i = 0; i < getVisibleChildCount(); i++) {
|
||||
int nextPosition = positionOfIndex(i);
|
||||
|
||||
/*
|
||||
* When a removal happens out of bounds, the pre-layout positions of items
|
||||
* after the removal are shifted to their final positions ahead of schedule.
|
||||
* We have to track off-screen removals and shift those positions back
|
||||
* so we can properly lay out all current (and appearing) views in their
|
||||
* initial locations.
|
||||
*/
|
||||
int offsetPositionDelta = 0;
|
||||
if (state.isPreLayout()) {
|
||||
int offsetPosition = nextPosition;
|
||||
|
||||
for (int offset = 0; offset < removedPositions.size(); offset++) {
|
||||
// Look for off-screen removals that are less-than this
|
||||
if (removedPositions.valueAt(offset) == REMOVE_INVISIBLE
|
||||
&& removedPositions.keyAt(offset) < nextPosition) {
|
||||
// Offset position to match
|
||||
offsetPosition--;
|
||||
}
|
||||
}
|
||||
offsetPositionDelta = nextPosition - offsetPosition;
|
||||
nextPosition = offsetPosition;
|
||||
}
|
||||
|
||||
if (nextPosition < 0 || nextPosition >= state.getItemCount()) {
|
||||
// Item space beyond the data set, don't attempt to add a view
|
||||
continue;
|
||||
}
|
||||
|
||||
// Layout this position
|
||||
View view = viewCache.get(nextPosition);
|
||||
if (view == null) {
|
||||
/*
|
||||
* The Recycler will give us either a newly constructed view,
|
||||
* or a recycled view it has on-hand. In either case, the
|
||||
* view will already be fully bound to the data by the
|
||||
* adapter for us.
|
||||
*/
|
||||
view = recycler.getViewForPosition(nextPosition);
|
||||
addView(view);
|
||||
|
||||
/*
|
||||
* Update the new view's metadata, but only when this is a real
|
||||
* layout pass.
|
||||
*/
|
||||
if (!state.isPreLayout()) {
|
||||
LayoutParams lp = (LayoutParams) view.getLayoutParams();
|
||||
lp.row = getGlobalRowOfPosition(nextPosition);
|
||||
lp.column = getGlobalColumnOfPosition(nextPosition);
|
||||
}
|
||||
|
||||
/*
|
||||
* It is prudent to measure/layout each new view we
|
||||
* receive from the Recycler. We don't have to do
|
||||
* this for views we are just re-arranging.
|
||||
*/
|
||||
measureChildWithMargins(view, 0, 0);
|
||||
layoutDecorated(view, leftOffset, topOffset,
|
||||
leftOffset + mDecoratedChildWidth,
|
||||
topOffset + mDecoratedChildHeight);
|
||||
|
||||
} else {
|
||||
// Re-attach the cached view at its new index
|
||||
attachView(view);
|
||||
viewCache.remove(nextPosition);
|
||||
}
|
||||
|
||||
if (i % mVisibleColumnCount == (mVisibleColumnCount - 1)) {
|
||||
leftOffset = startLeftOffset;
|
||||
topOffset += mDecoratedChildHeight;
|
||||
|
||||
// During pre-layout, on each column end, apply any additional appearing views
|
||||
if (state.isPreLayout()) {
|
||||
layoutAppearingViews(recycler, view, nextPosition, removedPositions.size(), offsetPositionDelta);
|
||||
}
|
||||
} else {
|
||||
leftOffset += mDecoratedChildWidth;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Finally, we ask the Recycler to scrap and store any views
|
||||
* that we did not re-attach. These are views that are not currently
|
||||
* necessary because they are no longer visible.
|
||||
*/
|
||||
for (int i=0; i < viewCache.size(); i++) {
|
||||
final View removingView = viewCache.valueAt(i);
|
||||
recycler.recycleView(removingView);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* You must override this method if you would like to support external calls
|
||||
* to shift the view to a given adapter position. In our implementation, this
|
||||
* is the same as doing a fresh layout with the given position as the top-left
|
||||
* (or first visible), so we simply set that value and trigger onLayoutChildren()
|
||||
*/
|
||||
@Override
|
||||
public void scrollToPosition(int position) {
|
||||
if (position >= getItemCount()) {
|
||||
Log.e(TAG, "Cannot scroll to "+position+", item count is "+getItemCount());
|
||||
return;
|
||||
}
|
||||
|
||||
// Set requested position as first visible
|
||||
mFirstVisiblePosition = position;
|
||||
// Toss all existing views away
|
||||
removeAllViews();
|
||||
// Trigger a new view layout
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
/*
|
||||
* You must override this method if you would like to support external calls
|
||||
* to animate a change to a new adapter position. The framework provides a
|
||||
* helper scroller implementation (LinearSmoothScroller), which we leverage
|
||||
* to do the animation calculations.
|
||||
*/
|
||||
@Override
|
||||
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, final int position) {
|
||||
if (position >= getItemCount()) {
|
||||
Log.e(TAG, "Cannot scroll to "+position+", item count is "+getItemCount());
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* LinearSmoothScroller's default behavior is to scroll the contents until
|
||||
* the child is fully visible. It will snap to the top-left or bottom-right
|
||||
* of the parent depending on whether the direction of travel was positive
|
||||
* or negative.
|
||||
*/
|
||||
LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext()) {
|
||||
/*
|
||||
* LinearSmoothScroller, at a minimum, just need to know the vector
|
||||
* (x/y distance) to travel in order to get from the current positioning
|
||||
* to the target.
|
||||
*/
|
||||
@Override
|
||||
public PointF computeScrollVectorForPosition(int targetPosition) {
|
||||
final int rowOffset = getGlobalRowOfPosition(targetPosition)
|
||||
- getGlobalRowOfPosition(mFirstVisiblePosition);
|
||||
final int columnOffset = getGlobalColumnOfPosition(targetPosition)
|
||||
- getGlobalColumnOfPosition(mFirstVisiblePosition);
|
||||
|
||||
return new PointF(columnOffset * mDecoratedChildWidth, rowOffset * mDecoratedChildHeight);
|
||||
}
|
||||
};
|
||||
scroller.setTargetPosition(position);
|
||||
startSmoothScroll(scroller);
|
||||
}
|
||||
|
||||
/*
|
||||
* Use this method to tell the RecyclerView if scrolling is even possible
|
||||
* in the horizontal direction.
|
||||
*/
|
||||
@Override
|
||||
public boolean canScrollHorizontally() {
|
||||
// We do allow scrolling
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* This method describes how far RecyclerView thinks the contents should scroll horizontally.
|
||||
* You are responsible for verifying edge boundaries, and determining if this scroll
|
||||
* event somehow requires that new views be added or old views get recycled.
|
||||
*/
|
||||
@Override
|
||||
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
|
||||
if (getChildCount() == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Take leftmost measurements from the top-left child
|
||||
final View topView = getChildAt(0);
|
||||
// Take rightmost measurements from the top-right child
|
||||
final View bottomView = getChildAt(mVisibleColumnCount-1);
|
||||
|
||||
// Optimize the case where the entire data set is too small to scroll
|
||||
int viewSpan = getDecoratedRight(bottomView) - getDecoratedLeft(topView);
|
||||
if (viewSpan < getHorizontalSpace()) {
|
||||
// We cannot scroll in either direction
|
||||
return 0;
|
||||
}
|
||||
|
||||
int delta;
|
||||
boolean leftBoundReached = getFirstVisibleColumn() == 0;
|
||||
boolean rightBoundReached = getLastVisibleColumn() >= getTotalColumnCount();
|
||||
if (dx > 0) { // Contents are scrolling left
|
||||
// Check right bound
|
||||
if (rightBoundReached) {
|
||||
// If we've reached the last column, enforce limits
|
||||
int rightOffset = getHorizontalSpace() - getDecoratedRight(bottomView) + getPaddingRight();
|
||||
delta = Math.max(-dx, rightOffset);
|
||||
} else {
|
||||
// No limits while the last column isn't visible
|
||||
delta = -dx;
|
||||
}
|
||||
} else { // Contents are scrolling right
|
||||
// Check left bound
|
||||
if (leftBoundReached) {
|
||||
int leftOffset = -getDecoratedLeft(topView) + getPaddingLeft();
|
||||
delta = Math.min(-dx, leftOffset);
|
||||
} else {
|
||||
delta = -dx;
|
||||
}
|
||||
}
|
||||
|
||||
offsetChildrenHorizontal(delta);
|
||||
|
||||
if (dx > 0) {
|
||||
if (getDecoratedRight(topView) < 0 && !rightBoundReached) {
|
||||
fillGrid(DIRECTION_END, recycler, state);
|
||||
} else if (!rightBoundReached) {
|
||||
fillGrid(DIRECTION_NONE, recycler, state);
|
||||
}
|
||||
} else {
|
||||
if (getDecoratedLeft(topView) > 0 && !leftBoundReached) {
|
||||
fillGrid(DIRECTION_START, recycler, state);
|
||||
} else if (!leftBoundReached) {
|
||||
fillGrid(DIRECTION_NONE, recycler, state);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Return value determines if a boundary has been reached
|
||||
* (for edge effects and flings). If returned value does not
|
||||
* match original delta (passed in), RecyclerView will draw
|
||||
* an edge effect.
|
||||
*/
|
||||
return -delta;
|
||||
}
|
||||
|
||||
/*
|
||||
* Use this method to tell the RecyclerView if scrolling is even possible
|
||||
* in the vertical direction.
|
||||
*/
|
||||
@Override
|
||||
public boolean canScrollVertically() {
|
||||
// We do allow scrolling
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* This method describes how far RecyclerView thinks the contents should scroll vertically.
|
||||
* You are responsible for verifying edge boundaries, and determining if this scroll
|
||||
* event somehow requires that new views be added or old views get recycled.
|
||||
*/
|
||||
@Override
|
||||
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
|
||||
if (getChildCount() == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Take top measurements from the top-left child
|
||||
final View topView = getChildAt(0);
|
||||
// Take bottom measurements from the bottom-right child.
|
||||
final View bottomView = getChildAt(getChildCount()-1);
|
||||
|
||||
// Optimize the case where the entire data set is too small to scroll
|
||||
int viewSpan = getDecoratedBottom(bottomView) - getDecoratedTop(topView);
|
||||
if (viewSpan < getVerticalSpace()) {
|
||||
// We cannot scroll in either direction
|
||||
return 0;
|
||||
}
|
||||
|
||||
int delta;
|
||||
int maxRowCount = getTotalRowCount();
|
||||
boolean topBoundReached = getFirstVisibleRow() == 0;
|
||||
boolean bottomBoundReached = getLastVisibleRow() >= maxRowCount;
|
||||
if (dy > 0) { // Contents are scrolling up
|
||||
// Check against bottom bound
|
||||
if (bottomBoundReached) {
|
||||
// If we've reached the last row, enforce limits
|
||||
int bottomOffset;
|
||||
if (rowOfIndex(getChildCount() - 1) >= (maxRowCount - 1)) {
|
||||
// We are truly at the bottom, determine how far
|
||||
bottomOffset = getVerticalSpace() - getDecoratedBottom(bottomView)
|
||||
+ getPaddingBottom();
|
||||
} else {
|
||||
/*
|
||||
* Extra space added to account for allowing bottom space in the grid.
|
||||
* This occurs when the overlap in the last row is not large enough to
|
||||
* ensure that at least one element in that row isn't fully recycled.
|
||||
*/
|
||||
bottomOffset = getVerticalSpace() - (getDecoratedBottom(bottomView)
|
||||
+ mDecoratedChildHeight) + getPaddingBottom();
|
||||
}
|
||||
|
||||
delta = Math.max(-dy, bottomOffset);
|
||||
} else {
|
||||
// No limits while the last row isn't visible
|
||||
delta = -dy;
|
||||
}
|
||||
} else { // Contents are scrolling down
|
||||
// Check against top bound
|
||||
if (topBoundReached) {
|
||||
int topOffset = -getDecoratedTop(topView) + getPaddingTop();
|
||||
|
||||
delta = Math.min(-dy, topOffset);
|
||||
} else {
|
||||
delta = -dy;
|
||||
}
|
||||
}
|
||||
|
||||
offsetChildrenVertical(delta);
|
||||
|
||||
if (dy > 0) {
|
||||
if (getDecoratedBottom(topView) < 0 && !bottomBoundReached) {
|
||||
fillGrid(DIRECTION_DOWN, recycler, state);
|
||||
} else if (!bottomBoundReached) {
|
||||
fillGrid(DIRECTION_NONE, recycler, state);
|
||||
}
|
||||
} else {
|
||||
if (getDecoratedTop(topView) > 0 && !topBoundReached) {
|
||||
fillGrid(DIRECTION_UP, recycler, state);
|
||||
} else if (!topBoundReached) {
|
||||
fillGrid(DIRECTION_NONE, recycler, state);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Return value determines if a boundary has been reached
|
||||
* (for edge effects and flings). If returned value does not
|
||||
* match original delta (passed in), RecyclerView will draw
|
||||
* an edge effect.
|
||||
*/
|
||||
return -delta;
|
||||
}
|
||||
|
||||
/*
|
||||
* This is a helper method used by RecyclerView to determine
|
||||
* if a specific child view can be returned.
|
||||
*/
|
||||
@Override
|
||||
public View findViewByPosition(int position) {
|
||||
for (int i=0; i < getChildCount(); i++) {
|
||||
if (positionOfIndex(i) == position) {
|
||||
return getChildAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Boilerplate to extend LayoutParams for tracking row/column of attached views */
|
||||
|
||||
/*
|
||||
* Even without extending LayoutParams, we must override this method
|
||||
* to provide the default layout parameters that each child view
|
||||
* will receive when added.
|
||||
*/
|
||||
@Override
|
||||
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
|
||||
return new LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
}
|
||||
@Override
|
||||
public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) {
|
||||
return new LayoutParams(c, attrs);
|
||||
}
|
||||
@Override
|
||||
public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
|
||||
if (lp instanceof ViewGroup.MarginLayoutParams) {
|
||||
return new LayoutParams((ViewGroup.MarginLayoutParams) lp);
|
||||
} else {
|
||||
return new LayoutParams(lp);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public boolean checkLayoutParams(RecyclerView.LayoutParams lp) {
|
||||
return lp instanceof LayoutParams;
|
||||
}
|
||||
|
||||
public static class LayoutParams extends RecyclerView.LayoutParams {
|
||||
|
||||
// Current row in the grid
|
||||
public int row;
|
||||
// Current column in the grid
|
||||
public int column;
|
||||
|
||||
public LayoutParams(Context c, AttributeSet attrs) {
|
||||
super(c, attrs);
|
||||
}
|
||||
public LayoutParams(int width, int height) {
|
||||
super(width, height);
|
||||
}
|
||||
public LayoutParams(ViewGroup.MarginLayoutParams source) {
|
||||
super(source);
|
||||
}
|
||||
public LayoutParams(ViewGroup.LayoutParams source) {
|
||||
super(source);
|
||||
}
|
||||
public LayoutParams(RecyclerView.LayoutParams source) {
|
||||
super(source);
|
||||
}
|
||||
}
|
||||
|
||||
/** Animation Layout Helpers */
|
||||
|
||||
/* Helper to obtain and place extra appearing views */
|
||||
private void layoutAppearingViews(RecyclerView.Recycler recycler, View referenceView, int referencePosition, int extraCount, int offset) {
|
||||
// Nothing to do...
|
||||
if (extraCount < 1) return;
|
||||
|
||||
// FIXME: This code currently causes double layout of views that are still visible…
|
||||
for (int extra = 1; extra <= extraCount; extra++) {
|
||||
// Grab the next position after the reference
|
||||
final int extraPosition = referencePosition + extra;
|
||||
if (extraPosition < 0 || extraPosition >= getItemCount()) {
|
||||
// Can't do anything with this
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* Obtain additional position views that we expect to appear
|
||||
* as part of the animation.
|
||||
*/
|
||||
View appearing = recycler.getViewForPosition(extraPosition);
|
||||
addView(appearing);
|
||||
|
||||
// Find layout delta from reference position
|
||||
final int newRow = getGlobalRowOfPosition(extraPosition + offset);
|
||||
final int rowDelta = newRow - getGlobalRowOfPosition(referencePosition + offset);
|
||||
final int newCol = getGlobalColumnOfPosition(extraPosition + offset);
|
||||
final int colDelta = newCol - getGlobalColumnOfPosition(referencePosition + offset);
|
||||
|
||||
layoutTempChildView(appearing, rowDelta, colDelta, referenceView);
|
||||
}
|
||||
}
|
||||
|
||||
/* Helper to place a disappearing view */
|
||||
private void layoutDisappearingView(View disappearingChild) {
|
||||
/*
|
||||
* LayoutManager has a special method for attaching views that
|
||||
* will only be around long enough to animate.
|
||||
*/
|
||||
addDisappearingView(disappearingChild);
|
||||
|
||||
// Adjust each disappearing view to its proper place
|
||||
final LayoutParams lp = (LayoutParams) disappearingChild.getLayoutParams();
|
||||
|
||||
final int newRow = getGlobalRowOfPosition(lp.getViewAdapterPosition());
|
||||
final int rowDelta = newRow - lp.row;
|
||||
final int newCol = getGlobalColumnOfPosition(lp.getViewAdapterPosition());
|
||||
final int colDelta = newCol - lp.column;
|
||||
|
||||
layoutTempChildView(disappearingChild, rowDelta, colDelta, disappearingChild);
|
||||
}
|
||||
|
||||
|
||||
/* Helper to lay out appearing/disappearing children */
|
||||
private void layoutTempChildView(View child, int rowDelta, int colDelta, View referenceView) {
|
||||
// Set the layout position to the global row/column difference from the reference view
|
||||
int layoutTop = getDecoratedTop(referenceView) + rowDelta * mDecoratedChildHeight;
|
||||
int layoutLeft = getDecoratedLeft(referenceView) + colDelta * mDecoratedChildWidth;
|
||||
|
||||
measureChildWithMargins(child, 0, 0);
|
||||
layoutDecorated(child, layoutLeft, layoutTop,
|
||||
layoutLeft + mDecoratedChildWidth,
|
||||
layoutTop + mDecoratedChildHeight);
|
||||
}
|
||||
|
||||
/** Private Helpers and Metrics Accessors */
|
||||
|
||||
/* Return the overall column index of this position in the global layout */
|
||||
private int getGlobalColumnOfPosition(int position) {
|
||||
return position % mTotalColumnCount;
|
||||
}
|
||||
/* Return the overall row index of this position in the global layout */
|
||||
private int getGlobalRowOfPosition(int position) {
|
||||
return position / mTotalColumnCount;
|
||||
}
|
||||
|
||||
/*
|
||||
* Mapping between child view indices and adapter data
|
||||
* positions helps fill the proper views during scrolling.
|
||||
*/
|
||||
private int positionOfIndex(int childIndex) {
|
||||
int row = childIndex / mVisibleColumnCount;
|
||||
int column = childIndex % mVisibleColumnCount;
|
||||
|
||||
return mFirstVisiblePosition + (row * getTotalColumnCount()) + column;
|
||||
}
|
||||
|
||||
private int rowOfIndex(int childIndex) {
|
||||
int position = positionOfIndex(childIndex);
|
||||
|
||||
return position / getTotalColumnCount();
|
||||
}
|
||||
|
||||
private int getFirstVisibleColumn() {
|
||||
return (mFirstVisiblePosition % getTotalColumnCount());
|
||||
}
|
||||
|
||||
private int getLastVisibleColumn() {
|
||||
return getFirstVisibleColumn() + mVisibleColumnCount;
|
||||
}
|
||||
|
||||
private int getFirstVisibleRow() {
|
||||
return (mFirstVisiblePosition / getTotalColumnCount());
|
||||
}
|
||||
|
||||
private int getLastVisibleRow() {
|
||||
return getFirstVisibleRow() + mVisibleRowCount;
|
||||
}
|
||||
|
||||
private int getVisibleChildCount() {
|
||||
return mVisibleColumnCount * mVisibleRowCount;
|
||||
}
|
||||
|
||||
private int getTotalColumnCount() {
|
||||
if (getItemCount() < mTotalColumnCount) {
|
||||
return getItemCount();
|
||||
}
|
||||
|
||||
return mTotalColumnCount;
|
||||
}
|
||||
|
||||
private int getTotalRowCount() {
|
||||
if (getItemCount() == 0 || mTotalColumnCount == 0) {
|
||||
return 0;
|
||||
}
|
||||
int maxRow = getItemCount() / mTotalColumnCount;
|
||||
// Bump the row count if it's not exactly even
|
||||
if (getItemCount() % mTotalColumnCount != 0) {
|
||||
maxRow++;
|
||||
}
|
||||
|
||||
return maxRow;
|
||||
}
|
||||
|
||||
private int getHorizontalSpace() {
|
||||
return getWidth();
|
||||
}
|
||||
|
||||
private int getVerticalSpace() {
|
||||
return getHeight();
|
||||
}
|
||||
}
|
|
@ -1,640 +0,0 @@
|
|||
package dev.lucasnlm.antimine.common.level.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.PointF
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.util.SparseArray
|
||||
import android.util.SparseIntArray
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
import androidx.recyclerview.widget.LinearSmoothScroller
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.Recycler
|
||||
|
||||
// Based on `FixedGridLayoutManager` from https://github.com/devunwired/recyclerview-playground
|
||||
|
||||
class FixedGridLayoutManager(
|
||||
private var maxColumnCount: Int,
|
||||
private val horizontalPadding: Int,
|
||||
private val verticalPadding: Int
|
||||
) : RecyclerView.LayoutManager() {
|
||||
|
||||
private var firstVisiblePosition = 0
|
||||
|
||||
private var decoratedChildWidth = 0
|
||||
private var decoratedChildHeight = 0
|
||||
|
||||
private var visibleColumnCount = 0
|
||||
private var visibleRowCount = 0
|
||||
|
||||
private var firstChangedPosition = 0
|
||||
private var changedPositionCount = 0
|
||||
|
||||
override fun supportsPredictiveItemAnimations(): Boolean = false
|
||||
|
||||
override fun onItemsRemoved(recyclerView: RecyclerView, positionStart: Int, itemCount: Int) {
|
||||
firstChangedPosition = positionStart
|
||||
changedPositionCount = itemCount
|
||||
}
|
||||
|
||||
override fun onLayoutChildren(recycler: Recycler, state: RecyclerView.State) {
|
||||
if (itemCount == 0) {
|
||||
detachAndScrapAttachedViews(recycler)
|
||||
return
|
||||
}
|
||||
|
||||
if (childCount == 0 && state.isPreLayout) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!state.isPreLayout) {
|
||||
changedPositionCount = 0
|
||||
firstChangedPosition = 0
|
||||
}
|
||||
|
||||
if (childCount == 0) {
|
||||
recycler.getViewForPosition(0).run {
|
||||
addView(this)
|
||||
measureChildWithMargins(this, 0, 0)
|
||||
|
||||
decoratedChildWidth = getDecoratedMeasuredWidth(this)
|
||||
decoratedChildHeight = getDecoratedMeasuredHeight(this)
|
||||
detachAndScrapView(this, recycler)
|
||||
}
|
||||
}
|
||||
|
||||
updateWindowSizing()
|
||||
var removedCache: SparseIntArray? = null
|
||||
|
||||
if (state.isPreLayout) {
|
||||
removedCache = SparseIntArray(childCount)
|
||||
|
||||
(0 until childCount)
|
||||
.mapNotNull { getChildAt(it) }
|
||||
.map { it.layoutParams as LayoutParams }
|
||||
.filter { it.isItemRemoved }
|
||||
.forEach { removedCache.put(it.viewLayoutPosition, REMOVE_VISIBLE) }
|
||||
|
||||
if (removedCache.size() == 0 && changedPositionCount > 0) {
|
||||
(firstChangedPosition until firstChangedPosition + changedPositionCount)
|
||||
.forEach { index ->
|
||||
removedCache.put(index, REMOVE_INVISIBLE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var childLeft: Int? = null
|
||||
var childTop: Int? = null
|
||||
|
||||
if (childCount == 0) {
|
||||
firstVisiblePosition = 0
|
||||
childLeft = horizontalPadding
|
||||
childTop = verticalPadding
|
||||
} else if (!state.isPreLayout && visibleChildCount >= state.itemCount) {
|
||||
// Data set is too small to scroll fully, just reset position
|
||||
firstVisiblePosition = 0
|
||||
childLeft = horizontalPadding
|
||||
childTop = verticalPadding
|
||||
} else {
|
||||
getChildAt(0)?.let { topChild ->
|
||||
childLeft = getDecoratedLeft(topChild)
|
||||
childTop = getDecoratedTop(topChild)
|
||||
}
|
||||
|
||||
if (!state.isPreLayout && verticalSpace > totalRowCount * decoratedChildHeight) {
|
||||
firstVisiblePosition %= totalColumnCount
|
||||
childTop = verticalPadding
|
||||
|
||||
if (firstVisiblePosition + visibleColumnCount > state.itemCount) {
|
||||
firstVisiblePosition = (state.itemCount - visibleColumnCount).coerceAtLeast(0)
|
||||
childLeft = horizontalPadding
|
||||
}
|
||||
}
|
||||
|
||||
val maxFirstRow = totalRowCount - (visibleRowCount - 1)
|
||||
val maxFirstCol = totalColumnCount - (visibleColumnCount - 1)
|
||||
val isOutOfRowBounds = firstVisibleRow > maxFirstRow
|
||||
val isOutOfColBounds = firstVisibleColumn > maxFirstCol
|
||||
|
||||
if (isOutOfRowBounds || isOutOfColBounds) {
|
||||
val firstRow: Int = if (isOutOfRowBounds) {
|
||||
maxFirstRow
|
||||
} else {
|
||||
firstVisibleRow
|
||||
}
|
||||
val firstCol: Int = if (isOutOfColBounds) {
|
||||
maxFirstCol
|
||||
} else {
|
||||
firstVisibleColumn
|
||||
}
|
||||
|
||||
firstVisiblePosition = firstRow * totalColumnCount + firstCol
|
||||
childLeft = horizontalSpace - decoratedChildWidth * visibleColumnCount
|
||||
childTop = verticalSpace - decoratedChildHeight * visibleRowCount
|
||||
|
||||
if (firstVisibleRow == 0) {
|
||||
childTop = childTop?.coerceAtMost(verticalPadding) ?: verticalSpace
|
||||
}
|
||||
if (firstVisibleColumn == 0) {
|
||||
childLeft = childLeft?.coerceAtMost(horizontalPadding) ?: horizontalSpace
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear all attached views into the recycle bin
|
||||
detachAndScrapAttachedViews(recycler)
|
||||
|
||||
// Fill the grid for the initial layout of views
|
||||
val finalChildLeft = childLeft
|
||||
val finalChildTop = childTop
|
||||
if (finalChildLeft != null && finalChildTop != null) {
|
||||
fillGrid(DIRECTION_NONE, finalChildLeft, finalChildTop, recycler, state, removedCache)
|
||||
}
|
||||
|
||||
// Evaluate any disappearing views that may exist
|
||||
if (!state.isPreLayout && recycler.scrapList.isNotEmpty()) {
|
||||
recycler.scrapList
|
||||
.map { it.itemView }
|
||||
.filterNot { (it.layoutParams as LayoutParams).isItemRemoved }
|
||||
.forEach { layoutDisappearingView(it) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAdapterChanged(oldAdapter: RecyclerView.Adapter<*>?, newAdapter: RecyclerView.Adapter<*>?) {
|
||||
// Completely scrap the existing layout
|
||||
removeAllViews()
|
||||
}
|
||||
|
||||
private fun updateWindowSizing() {
|
||||
visibleColumnCount = horizontalSpace / decoratedChildWidth + 1
|
||||
if (horizontalSpace % decoratedChildWidth > 0) {
|
||||
visibleColumnCount++
|
||||
}
|
||||
|
||||
// Allow minimum value for small data sets
|
||||
visibleColumnCount = visibleColumnCount.coerceAtMost(totalColumnCount)
|
||||
|
||||
visibleRowCount = verticalSpace / decoratedChildHeight + 1
|
||||
if (verticalSpace % decoratedChildHeight > 0) {
|
||||
visibleRowCount++
|
||||
}
|
||||
|
||||
visibleRowCount.coerceAtMost(totalRowCount)
|
||||
}
|
||||
|
||||
private fun fillGrid(direction: Int, recycler: Recycler, state: RecyclerView.State) {
|
||||
fillGrid(direction, 0, 0, recycler, state, null)
|
||||
}
|
||||
|
||||
private fun fillGrid(
|
||||
direction: Int,
|
||||
emptyLeft: Int,
|
||||
emptyTop: Int,
|
||||
recycler: Recycler,
|
||||
state: RecyclerView.State,
|
||||
removedPositions: SparseIntArray?
|
||||
) {
|
||||
firstVisiblePosition = firstVisiblePosition.coerceIn(0, itemCount - 1)
|
||||
|
||||
var startLeftOffset = emptyLeft
|
||||
var startTopOffset = emptyTop
|
||||
val viewCache = SparseArray<View?>(childCount)
|
||||
|
||||
if (childCount != 0) {
|
||||
getChildAt(0)?.let { topView ->
|
||||
startLeftOffset = getDecoratedLeft(topView)
|
||||
startTopOffset = getDecoratedTop(topView)
|
||||
}
|
||||
|
||||
when (direction) {
|
||||
DIRECTION_START -> startLeftOffset -= decoratedChildWidth
|
||||
DIRECTION_END -> startLeftOffset += decoratedChildWidth
|
||||
DIRECTION_UP -> startTopOffset -= decoratedChildHeight
|
||||
DIRECTION_DOWN -> startTopOffset += decoratedChildHeight
|
||||
}
|
||||
|
||||
// Cache all views by their existing position, before updating counts
|
||||
(0 until childCount).forEach { index ->
|
||||
val position = positionOfIndex(index)
|
||||
val child = getChildAt(index)
|
||||
viewCache.put(position, child)
|
||||
}
|
||||
|
||||
// Temporarily detach all views.
|
||||
// Views we still need will be added back at the proper index.
|
||||
(0 until viewCache.size())
|
||||
.mapNotNull { index -> viewCache.valueAt(index) }
|
||||
.forEach(::detachView)
|
||||
}
|
||||
|
||||
when (direction) {
|
||||
DIRECTION_START -> firstVisiblePosition--
|
||||
DIRECTION_END -> firstVisiblePosition++
|
||||
DIRECTION_UP -> firstVisiblePosition -= totalColumnCount
|
||||
DIRECTION_DOWN -> firstVisiblePosition += totalColumnCount
|
||||
}
|
||||
|
||||
// Next, we supply the grid of items that are deemed visible.
|
||||
// If these items were previously there, they will simply be
|
||||
// re-attached. New views that must be created are obtained
|
||||
// from the Recycler and added.
|
||||
var leftOffset = startLeftOffset
|
||||
var topOffset = startTopOffset
|
||||
for (index in 0 until visibleChildCount) {
|
||||
var nextPosition = positionOfIndex(index)
|
||||
|
||||
var offsetPositionDelta = 0
|
||||
if (state.isPreLayout) {
|
||||
var offsetPosition = nextPosition
|
||||
|
||||
removedPositions?.let {
|
||||
for (offset in 0 until it.size()) {
|
||||
// Look for off-screen removals that are less-than this
|
||||
if (removedPositions.valueAt(offset) ==
|
||||
REMOVE_INVISIBLE && removedPositions.keyAt(offset) < nextPosition
|
||||
) {
|
||||
// Offset position to match
|
||||
offsetPosition--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
offsetPositionDelta = nextPosition - offsetPosition
|
||||
nextPosition = offsetPosition
|
||||
}
|
||||
|
||||
if (nextPosition < 0 || nextPosition >= state.itemCount) {
|
||||
// Item space beyond the data set, don't attempt to add a view
|
||||
continue
|
||||
}
|
||||
|
||||
// Layout this position
|
||||
var view = viewCache[nextPosition]
|
||||
if (view == null) {
|
||||
view = recycler.getViewForPosition(nextPosition)
|
||||
addView(view)
|
||||
|
||||
if (!state.isPreLayout) {
|
||||
val layoutParams = view.layoutParams as LayoutParams
|
||||
layoutParams.apply {
|
||||
row = getGlobalRowOfPosition(nextPosition)
|
||||
column = getGlobalColumnOfPosition(nextPosition)
|
||||
}
|
||||
}
|
||||
|
||||
measureChildWithMargins(view, 0, 0)
|
||||
layoutDecorated(
|
||||
view, leftOffset, topOffset,
|
||||
leftOffset + decoratedChildWidth,
|
||||
topOffset + decoratedChildHeight
|
||||
)
|
||||
} else {
|
||||
// Re-attach the cached view at its new index
|
||||
attachView(view)
|
||||
viewCache.remove(nextPosition)
|
||||
}
|
||||
|
||||
if (index % visibleColumnCount == visibleColumnCount - 1) {
|
||||
leftOffset = startLeftOffset
|
||||
topOffset += decoratedChildHeight
|
||||
|
||||
if (state.isPreLayout) {
|
||||
layoutAppearingViews(
|
||||
recycler,
|
||||
view,
|
||||
nextPosition,
|
||||
removedPositions?.size() ?: 0,
|
||||
offsetPositionDelta
|
||||
)
|
||||
}
|
||||
} else {
|
||||
leftOffset += decoratedChildWidth
|
||||
}
|
||||
}
|
||||
|
||||
for (i in 0 until viewCache.size()) {
|
||||
viewCache.valueAt(i)?.let { removingView ->
|
||||
recycler.recycleView(removingView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun scrollToPosition(position: Int) {
|
||||
if (position >= itemCount) {
|
||||
Log.e(TAG, "Cannot scroll to $position, item count is $itemCount")
|
||||
return
|
||||
}
|
||||
|
||||
firstVisiblePosition = position
|
||||
removeAllViews()
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
override fun smoothScrollToPosition(
|
||||
recyclerView: RecyclerView,
|
||||
state: RecyclerView.State,
|
||||
position: Int
|
||||
) {
|
||||
if (position >= itemCount) {
|
||||
Log.e(TAG, "Cannot scroll to $position, item count is $itemCount")
|
||||
return
|
||||
}
|
||||
|
||||
val scroller: LinearSmoothScroller = object : LinearSmoothScroller(recyclerView.context) {
|
||||
override fun computeScrollVectorForPosition(targetPosition: Int): PointF? {
|
||||
val rowOffset =
|
||||
(getGlobalRowOfPosition(targetPosition) - getGlobalRowOfPosition(firstVisiblePosition))
|
||||
val columnOffset =
|
||||
(getGlobalColumnOfPosition(targetPosition) - getGlobalColumnOfPosition(firstVisiblePosition))
|
||||
return PointF(
|
||||
(columnOffset * decoratedChildWidth).toFloat(),
|
||||
(rowOffset * decoratedChildHeight).toFloat()
|
||||
)
|
||||
}
|
||||
}
|
||||
scroller.targetPosition = position
|
||||
startSmoothScroll(scroller)
|
||||
}
|
||||
|
||||
override fun scrollHorizontallyBy(dx: Int, recycler: Recycler, state: RecyclerView.State): Int {
|
||||
if (childCount == 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
val leftView = getChildAt(0)
|
||||
val rightView = getChildAt(visibleColumnCount - 1)
|
||||
|
||||
if (leftView == null || rightView == null) {
|
||||
return 0
|
||||
}
|
||||
|
||||
val viewSpan = getDecoratedRight(rightView) - getDecoratedLeft(leftView)
|
||||
if (viewSpan < horizontalSpace) {
|
||||
return 0
|
||||
}
|
||||
|
||||
val leftBoundReached = firstVisibleColumn == 0
|
||||
val rightBoundReached = lastVisibleColumn >= totalColumnCount
|
||||
val delta: Int = if (dx > 0) {
|
||||
if (rightBoundReached) {
|
||||
val rightOffset = horizontalSpace - getDecoratedRight(rightView) + horizontalPadding
|
||||
(-dx).coerceAtLeast(rightOffset)
|
||||
} else {
|
||||
-dx
|
||||
}
|
||||
} else {
|
||||
if (leftBoundReached) {
|
||||
val leftOffset = -getDecoratedLeft(leftView) + horizontalPadding
|
||||
(-dx).coerceAtMost(leftOffset)
|
||||
} else {
|
||||
-dx
|
||||
}
|
||||
}
|
||||
|
||||
offsetChildrenHorizontal(delta)
|
||||
if (dx > 0) {
|
||||
if (getDecoratedRight(leftView) < 0 && !rightBoundReached) {
|
||||
fillGrid(DIRECTION_END, recycler, state)
|
||||
} else if (!rightBoundReached) {
|
||||
fillGrid(DIRECTION_NONE, recycler, state)
|
||||
}
|
||||
} else {
|
||||
if (getDecoratedLeft(leftView) > 0 && !leftBoundReached) {
|
||||
fillGrid(DIRECTION_START, recycler, state)
|
||||
} else if (!leftBoundReached) {
|
||||
fillGrid(DIRECTION_NONE, recycler, state)
|
||||
}
|
||||
}
|
||||
|
||||
return -delta
|
||||
}
|
||||
|
||||
override fun canScrollHorizontally(): Boolean = true
|
||||
|
||||
override fun canScrollVertically(): Boolean = true
|
||||
|
||||
override fun scrollVerticallyBy(dy: Int, recycler: Recycler, state: RecyclerView.State): Int {
|
||||
if (childCount == 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
val topView = getChildAt(0)
|
||||
val bottomView = getChildAt(childCount - 1)
|
||||
|
||||
if (topView == null || bottomView == null) {
|
||||
return 0
|
||||
}
|
||||
|
||||
val viewSpan = getDecoratedBottom(bottomView) - getDecoratedTop(topView)
|
||||
if (viewSpan < verticalSpace) {
|
||||
return 0
|
||||
}
|
||||
|
||||
val maxRowCount = totalRowCount
|
||||
val topBoundReached = firstVisibleRow == 0
|
||||
val bottomBoundReached = lastVisibleRow >= maxRowCount
|
||||
val delta: Int = if (dy > 0) {
|
||||
if (bottomBoundReached) {
|
||||
val bottomOffset: Int = if (rowOfIndex(childCount - 1) >= maxRowCount - 1) {
|
||||
(verticalSpace - getDecoratedBottom(bottomView) + verticalPadding)
|
||||
} else {
|
||||
verticalSpace - (getDecoratedBottom(bottomView) + decoratedChildHeight) + verticalPadding
|
||||
}
|
||||
(-dy).coerceAtLeast(bottomOffset)
|
||||
} else {
|
||||
-dy
|
||||
}
|
||||
} else {
|
||||
if (topBoundReached) {
|
||||
val topOffset = -getDecoratedTop(topView) + verticalPadding
|
||||
(-dy).coerceAtMost(topOffset)
|
||||
} else {
|
||||
-dy
|
||||
}
|
||||
}
|
||||
|
||||
offsetChildrenVertical(delta)
|
||||
|
||||
if (dy > 0) {
|
||||
if (getDecoratedBottom(topView) < 0 && !bottomBoundReached) {
|
||||
fillGrid(DIRECTION_DOWN, recycler, state)
|
||||
} else if (!bottomBoundReached) {
|
||||
fillGrid(DIRECTION_NONE, recycler, state)
|
||||
}
|
||||
} else {
|
||||
if (getDecoratedTop(topView) > 0 && !topBoundReached) {
|
||||
fillGrid(DIRECTION_UP, recycler, state)
|
||||
} else if (!topBoundReached) {
|
||||
fillGrid(DIRECTION_NONE, recycler, state)
|
||||
}
|
||||
}
|
||||
|
||||
return -delta
|
||||
}
|
||||
|
||||
override fun findViewByPosition(position: Int): View? =
|
||||
(0 until childCount).firstOrNull { positionOfIndex(it) == position }?.let { getChildAt(it) }
|
||||
|
||||
override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
|
||||
return LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
|
||||
override fun generateLayoutParams(context: Context, attrs: AttributeSet): RecyclerView.LayoutParams {
|
||||
return LayoutParams(context, attrs)
|
||||
}
|
||||
|
||||
override fun generateLayoutParams(layoutParams: ViewGroup.LayoutParams): RecyclerView.LayoutParams {
|
||||
return if (layoutParams is MarginLayoutParams) {
|
||||
LayoutParams(layoutParams)
|
||||
} else {
|
||||
LayoutParams(layoutParams)
|
||||
}
|
||||
}
|
||||
|
||||
override fun checkLayoutParams(layoutParams: RecyclerView.LayoutParams): Boolean {
|
||||
return layoutParams is LayoutParams
|
||||
}
|
||||
|
||||
private fun layoutAppearingViews(
|
||||
recycler: Recycler,
|
||||
referenceView: View,
|
||||
referencePosition: Int,
|
||||
extraCount: Int,
|
||||
offset: Int
|
||||
) {
|
||||
if (extraCount > 1) {
|
||||
// FIXME: This code currently causes double layout of views that are still visible…
|
||||
(1..extraCount)
|
||||
.map { extra -> referencePosition + extra }
|
||||
.filterNot { extraPosition ->
|
||||
extraPosition < 0 || extraPosition >= itemCount
|
||||
}
|
||||
.forEach { extraPosition ->
|
||||
val appearing = recycler.getViewForPosition(extraPosition)
|
||||
addView(appearing)
|
||||
|
||||
val newRow = getGlobalRowOfPosition(extraPosition + offset)
|
||||
val rowDelta = newRow - getGlobalRowOfPosition(referencePosition + offset)
|
||||
val newCol = getGlobalColumnOfPosition(extraPosition + offset)
|
||||
val colDelta = newCol - getGlobalColumnOfPosition(referencePosition + offset)
|
||||
layoutTempChildView(appearing, rowDelta, colDelta, referenceView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun layoutDisappearingView(disappearingChild: View) {
|
||||
addDisappearingView(disappearingChild)
|
||||
|
||||
val layoutParams = disappearingChild.layoutParams as LayoutParams
|
||||
val newRow = getGlobalRowOfPosition(layoutParams.viewAdapterPosition)
|
||||
val rowDelta = newRow - layoutParams.row
|
||||
val newCol = getGlobalColumnOfPosition(layoutParams.viewAdapterPosition)
|
||||
val colDelta = newCol - layoutParams.column
|
||||
layoutTempChildView(disappearingChild, rowDelta, colDelta, disappearingChild)
|
||||
}
|
||||
|
||||
private fun layoutTempChildView(
|
||||
child: View,
|
||||
rowDelta: Int,
|
||||
colDelta: Int,
|
||||
referenceView: View
|
||||
) {
|
||||
val layoutTop = getDecoratedTop(referenceView) + rowDelta * decoratedChildHeight
|
||||
val layoutLeft = getDecoratedLeft(referenceView) + colDelta * decoratedChildWidth
|
||||
measureChildWithMargins(child, 0, 0)
|
||||
layoutDecorated(
|
||||
child, layoutLeft, layoutTop,
|
||||
layoutLeft + decoratedChildWidth,
|
||||
layoutTop + decoratedChildHeight
|
||||
)
|
||||
}
|
||||
|
||||
private fun getGlobalColumnOfPosition(position: Int): Int =
|
||||
position % maxColumnCount
|
||||
|
||||
private fun getGlobalRowOfPosition(position: Int): Int =
|
||||
position / maxColumnCount
|
||||
|
||||
private fun positionOfIndex(childIndex: Int): Int {
|
||||
val row = childIndex / visibleColumnCount
|
||||
val column = childIndex % visibleColumnCount
|
||||
return firstVisiblePosition + row * totalColumnCount + column
|
||||
}
|
||||
|
||||
private fun rowOfIndex(childIndex: Int): Int =
|
||||
positionOfIndex(childIndex) / totalColumnCount
|
||||
|
||||
private val firstVisibleColumn: Int
|
||||
get() = firstVisiblePosition % totalColumnCount
|
||||
|
||||
private val lastVisibleColumn: Int
|
||||
get() = firstVisibleColumn + visibleColumnCount
|
||||
|
||||
private val firstVisibleRow: Int
|
||||
get() = firstVisiblePosition / totalColumnCount
|
||||
|
||||
private val lastVisibleRow: Int
|
||||
get() = firstVisibleRow + visibleRowCount
|
||||
|
||||
private val visibleChildCount: Int
|
||||
get() = visibleColumnCount * visibleRowCount
|
||||
|
||||
private val totalColumnCount: Int
|
||||
get() = if (itemCount < maxColumnCount) {
|
||||
itemCount
|
||||
} else {
|
||||
maxColumnCount
|
||||
}
|
||||
|
||||
private val totalRowCount: Int
|
||||
get() {
|
||||
return if (itemCount == 0 || maxColumnCount == 0) {
|
||||
0
|
||||
} else {
|
||||
val maxRow = itemCount / maxColumnCount
|
||||
|
||||
// Bump the row count if it's not exactly even
|
||||
if (itemCount % maxColumnCount != 0) {
|
||||
maxRow + 1
|
||||
} else {
|
||||
maxRow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val horizontalSpace: Int
|
||||
get() = width
|
||||
|
||||
private val verticalSpace: Int
|
||||
get() = height
|
||||
|
||||
companion object {
|
||||
private val TAG = FixedGridLayoutManager::class.simpleName
|
||||
|
||||
// View Removal Constants
|
||||
private const val REMOVE_VISIBLE = 0
|
||||
private const val REMOVE_INVISIBLE = 1
|
||||
|
||||
// Fill Direction Constants
|
||||
private const val DIRECTION_NONE = -1
|
||||
private const val DIRECTION_START = 0
|
||||
private const val DIRECTION_END = 1
|
||||
private const val DIRECTION_UP = 2
|
||||
private const val DIRECTION_DOWN = 3
|
||||
}
|
||||
|
||||
class LayoutParams : RecyclerView.LayoutParams {
|
||||
// Current row in the grid
|
||||
var row = 0
|
||||
|
||||
// Current column in the grid
|
||||
var column = 0
|
||||
|
||||
constructor(c: Context?, attrs: AttributeSet?) : super(c, attrs)
|
||||
constructor(width: Int, height: Int) : super(width, height)
|
||||
constructor(source: MarginLayoutParams?) : super(source)
|
||||
constructor(source: ViewGroup.LayoutParams?) : super(source)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package dev.lucasnlm.antimine.common.level.di
|
||||
|
||||
import dev.lucasnlm.antimine.common.level.models.Difficulty
|
||||
import dev.lucasnlm.antimine.common.level.models.Minefield
|
||||
import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository
|
||||
import dev.lucasnlm.antimine.common.level.repository.IMinefieldRepository
|
||||
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
|
||||
|
||||
class FixedMinefieldRepository : IMinefieldRepository {
|
||||
override fun fromDifficulty(
|
||||
difficulty: Difficulty,
|
||||
dimensionRepository: IDimensionRepository,
|
||||
preferencesRepository: IPreferencesRepository
|
||||
) = Minefield(9, 9, 9)
|
||||
|
||||
override fun randomSeed(): Long = 200
|
||||
}
|
|
@ -5,18 +5,17 @@ import android.content.Context
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dev.lucasnlm.antimine.common.level.database.models.Save
|
||||
import dev.lucasnlm.antimine.common.level.database.models.Stats
|
||||
import dev.lucasnlm.antimine.common.level.models.Difficulty
|
||||
import dev.lucasnlm.antimine.common.level.mocks.FixedDimensionRepository
|
||||
import dev.lucasnlm.antimine.common.level.models.Event
|
||||
import dev.lucasnlm.antimine.common.level.models.Minefield
|
||||
import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository
|
||||
import dev.lucasnlm.antimine.common.level.repository.IMinefieldRepository
|
||||
import dev.lucasnlm.antimine.common.level.repository.ISavesRepository
|
||||
import dev.lucasnlm.antimine.common.level.repository.IStatsRepository
|
||||
import dev.lucasnlm.antimine.common.level.repository.MemorySavesRepository
|
||||
import dev.lucasnlm.antimine.common.level.repository.MemoryStatsRepository
|
||||
import dev.lucasnlm.antimine.common.level.repository.MinefieldRepository
|
||||
import dev.lucasnlm.antimine.common.level.repository.Size
|
||||
import dev.lucasnlm.antimine.common.level.utils.Clock
|
||||
import dev.lucasnlm.antimine.common.level.utils.DisabledIHapticFeedbackInteractor
|
||||
import dev.lucasnlm.antimine.common.level.utils.IHapticFeedbackInteractor
|
||||
import dev.lucasnlm.antimine.common.level.viewmodel.GameViewModelFactory
|
||||
import dev.lucasnlm.antimine.core.analytics.AnalyticsManager
|
||||
|
@ -61,54 +60,20 @@ class TestLevelModule(
|
|||
fun provideDimensionRepository(
|
||||
context: Context,
|
||||
preferencesRepository: IPreferencesRepository
|
||||
): IDimensionRepository = object : IDimensionRepository {
|
||||
override fun areaSize(): Float = 50.0f
|
||||
|
||||
override fun areaSizeWithPadding(): Float = 52.0f
|
||||
|
||||
override fun displaySize(): Size = Size(50 * 15, 50 * 30)
|
||||
|
||||
override fun actionBarSize(): Int = 50
|
||||
}
|
||||
): IDimensionRepository = FixedDimensionRepository()
|
||||
|
||||
@Provides
|
||||
fun provideSavesRepository(): ISavesRepository = object : ISavesRepository {
|
||||
override suspend fun getAllSaves(): List<Save> = listOf()
|
||||
|
||||
override suspend fun fetchCurrentSave(): Save? = null
|
||||
|
||||
override suspend fun loadFromId(id: Int): Save? = null
|
||||
|
||||
override suspend fun saveGame(save: Save): Long? = null
|
||||
|
||||
override fun setLimit(maxSavesStorage: Int) { }
|
||||
}
|
||||
fun provideSavesRepository(): ISavesRepository = MemorySavesRepository()
|
||||
|
||||
@Provides
|
||||
fun provideMinefieldRepository(): IMinefieldRepository = object : IMinefieldRepository {
|
||||
override fun fromDifficulty(
|
||||
difficulty: Difficulty,
|
||||
dimensionRepository: IDimensionRepository,
|
||||
preferencesRepository: IPreferencesRepository
|
||||
) = Minefield(9, 9, 9)
|
||||
|
||||
override fun randomSeed(): Long = 200
|
||||
}
|
||||
fun provideMinefieldRepository(): IMinefieldRepository = FixedMinefieldRepository()
|
||||
|
||||
@Provides
|
||||
fun provideStatsRepository(): IStatsRepository = object : IStatsRepository {
|
||||
override suspend fun getAllStats(): List<Stats> = listOf()
|
||||
|
||||
override suspend fun addStats(stats: Stats): Long? = null
|
||||
}
|
||||
fun provideStatsRepository(): IStatsRepository = MemoryStatsRepository()
|
||||
|
||||
@Provides
|
||||
fun provideHapticFeedbackInteractor(
|
||||
application: Application,
|
||||
preferencesRepository: IPreferencesRepository
|
||||
): IHapticFeedbackInteractor = object : IHapticFeedbackInteractor {
|
||||
override fun toggleFlagFeedback() { }
|
||||
|
||||
override fun explosionFeedback() { }
|
||||
}
|
||||
): IHapticFeedbackInteractor = DisabledIHapticFeedbackInteractor()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package dev.lucasnlm.antimine.common.level.mocks
|
||||
|
||||
import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository
|
||||
import dev.lucasnlm.antimine.common.level.repository.Size
|
||||
|
||||
class FixedDimensionRepository() : IDimensionRepository {
|
||||
override fun areaSize(): Float = 50.0f
|
||||
|
||||
override fun areaSizeWithPadding(): Float {
|
||||
return areaSize() + 2 * areaSeparator()
|
||||
}
|
||||
|
||||
override fun areaSeparator(): Float = 1.0f
|
||||
|
||||
override fun displaySize(): Size = Size(50 * 15, 50 * 30)
|
||||
|
||||
override fun actionBarSize(): Int = 50
|
||||
}
|
|
@ -166,9 +166,11 @@ class WatchGameActivity : DaggerAppCompatActivity(), AmbientModeSupport.AmbientC
|
|||
when (event) {
|
||||
Event.StartNewGame -> {
|
||||
status = Status.PreGame
|
||||
newGame.visibility = View.GONE
|
||||
}
|
||||
Event.Resume, Event.Running -> {
|
||||
status = Status.Running
|
||||
newGame.visibility = View.GONE
|
||||
}
|
||||
Event.Victory -> {
|
||||
status = Status.Over()
|
||||
|
|
|
@ -7,7 +7,6 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import dev.lucasnlm.antimine.common.R
|
||||
import dagger.android.support.DaggerFragment
|
||||
|
@ -62,10 +61,12 @@ class WatchLevelFragment : DaggerFragment() {
|
|||
|
||||
withContext(Dispatchers.Main) {
|
||||
recyclerGrid.apply {
|
||||
val horizontalPadding = calcHorizontalPadding(levelSetup.width)
|
||||
val verticalPadding = calcVerticalPadding(levelSetup.height)
|
||||
setHasFixedSize(true)
|
||||
addItemDecoration(SpaceItemDecoration(R.dimen.field_padding))
|
||||
isNestedScrollingEnabled = false
|
||||
layoutManager = makeNewLayoutManager(levelSetup.width, levelSetup.height)
|
||||
setPadding(horizontalPadding, verticalPadding, 0, 0)
|
||||
layoutManager = makeNewLayoutManager(levelSetup.width)
|
||||
adapter = areaAdapter
|
||||
alpha = 0.0f
|
||||
|
||||
|
@ -87,8 +88,12 @@ class WatchLevelFragment : DaggerFragment() {
|
|||
levelSetup.observe(
|
||||
viewLifecycleOwner,
|
||||
Observer {
|
||||
recyclerGrid.layoutManager =
|
||||
GridLayoutManager(activity, it.width, RecyclerView.VERTICAL, false)
|
||||
recyclerGrid.apply {
|
||||
val horizontalPadding = calcHorizontalPadding(it.width)
|
||||
val verticalPadding = calcVerticalPadding(it.height)
|
||||
layoutManager = makeNewLayoutManager(it.width)
|
||||
setPadding(horizontalPadding, verticalPadding, 0, 0)
|
||||
}
|
||||
}
|
||||
)
|
||||
fieldRefresh.observe(
|
||||
|
@ -114,18 +119,24 @@ class WatchLevelFragment : DaggerFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun makeNewLayoutManager(boardWidth: Int, boardHeight: Int) =
|
||||
FixedGridLayoutManager(boardWidth, calcHorizontalPadding(boardWidth), calcVerticalPadding(boardHeight))
|
||||
private fun makeNewLayoutManager(boardWidth: Int) =
|
||||
FixedGridLayoutManager().apply {
|
||||
setTotalColumnCount(boardWidth)
|
||||
}
|
||||
|
||||
private fun calcHorizontalPadding(boardWidth: Int): Int =
|
||||
((recyclerGrid.measuredWidth - dimensionRepository.areaSizeWithPadding() * boardWidth) / 2)
|
||||
.coerceAtLeast(0.0f)
|
||||
.toInt()
|
||||
private fun calcHorizontalPadding(boardWidth: Int): Int {
|
||||
val width = recyclerGrid.measuredWidth
|
||||
val recyclerViewWidth = (dimensionRepository.areaSize() * boardWidth)
|
||||
val separatorsWidth = (dimensionRepository.areaSeparator() * (boardWidth - 1))
|
||||
return ((width - recyclerViewWidth - separatorsWidth) / 2).coerceAtLeast(0.0f).toInt()
|
||||
}
|
||||
|
||||
private fun calcVerticalPadding(boardHeight: Int): Int =
|
||||
((recyclerGrid.measuredHeight - dimensionRepository.areaSizeWithPadding() * boardHeight) / 2)
|
||||
.coerceAtLeast(0.0f)
|
||||
.toInt()
|
||||
private fun calcVerticalPadding(boardHeight: Int): Int {
|
||||
val height = recyclerGrid.measuredHeight
|
||||
val recyclerViewHeight = (dimensionRepository.areaSize() * boardHeight)
|
||||
val separatorsHeight = (dimensionRepository.areaSeparator() * (boardHeight - 1))
|
||||
return ((height - recyclerViewHeight - separatorsHeight) / 2).coerceAtLeast(0.0f).toInt()
|
||||
}
|
||||
|
||||
fun setAmbientMode(ambientSettings: AmbientSettings) {
|
||||
areaAdapter.apply {
|
||||
|
|