Implement rewind screen and logic

Currently, rewind is always enabled and with hardcoded values. Adding settings for these configurations is still required.
This commit is contained in:
Rafael Caetano 2021-11-07 01:54:27 +00:00
parent 72371baa00
commit 39d2fa43a1
20 changed files with 518 additions and 49 deletions

View File

@ -74,6 +74,7 @@ android {
}
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
}
configurations.all {
resolutionStrategy.eachDependency {
@ -90,6 +91,10 @@ android {
dependencies {
val gitHubImplementation by configurations
with(Dependencies.Tools) {
coreLibraryDesugaring(desugarJdkLibs)
}
with(Dependencies.Kotlin) {
implementation(kotlinStdlib)
}

View File

@ -31,6 +31,8 @@
-keep class me.magnum.melonds.domain.model.ConsoleType { *; }
-keep class me.magnum.melonds.domain.model.MicSource { *; }
-keep class me.magnum.melonds.domain.model.Cheat { *; }
-keep class me.magnum.melonds.ui.emulator.rewind.model.RewindSaveState { *; }
-keep class me.magnum.melonds.ui.emulator.rewind.model.RewindWindow { *; }
-keep class me.magnum.melonds.ui.settings.fragments.**
-keep class me.magnum.melonds.common.UriFileHandler {
public int open(java.lang.String, java.lang.String);

View File

@ -267,6 +267,79 @@ Java_me_magnum_melonds_MelonEmulator_loadStateInternal(JNIEnv* env, jobject thiz
return MelonDSAndroid::loadState(saveStatePath);
}
JNIEXPORT jboolean JNICALL
Java_me_magnum_melonds_MelonEmulator_loadRewindState(JNIEnv* env, jobject thiz, jobject rewindSaveState) {
bool result = true;
pthread_mutex_lock(&emuThreadMutex);
if (!stop) {
bool wasPaused = paused;
if (paused) {
pthread_mutex_unlock(&emuThreadMutex);
} else {
pthread_mutex_unlock(&emuThreadMutex);
Java_me_magnum_melonds_MelonEmulator_pauseEmulation(env, thiz);
}
jclass rewindSaveStateClass = env->FindClass("me/magnum/melonds/ui/emulator/rewind/model/RewindSaveState");
jfieldID bufferField = env->GetFieldID(rewindSaveStateClass, "buffer", "Ljava/nio/ByteBuffer;");
jfieldID screenshotBufferField = env->GetFieldID(rewindSaveStateClass, "screenshotBuffer", "Ljava/nio/ByteBuffer;");
jfieldID frameField = env->GetFieldID(rewindSaveStateClass, "frame", "I");
jobject buffer = env->GetObjectField(rewindSaveState, bufferField);
jobject screenshotBuffer = env->GetObjectField(rewindSaveState, screenshotBufferField);
jint frame = (int) env->GetIntField(rewindSaveState, frameField);
// Make sure that the thread is really paused to avoid data corruption
while (!isThreadReallyPaused);
RewindManager::RewindSaveState state = RewindManager::RewindSaveState {
.buffer = (u8*) env->GetDirectBufferAddress(buffer),
.bufferSize = (u32) env->GetDirectBufferCapacity(buffer),
.screenshot = (u8*) env->GetDirectBufferAddress(screenshotBuffer),
.screenshotSize = (u32) env->GetDirectBufferCapacity(screenshotBuffer),
.frame = frame
};
result = MelonDSAndroid::loadRewindState(state);
// Resume emulation if it was running
if (!wasPaused) {
Java_me_magnum_melonds_MelonEmulator_resumeEmulation(env, thiz);
}
} else {
// If the emulation is stopping, just ignore it
pthread_mutex_unlock(&emuThreadMutex);
}
return result;
}
JNIEXPORT jobject JNICALL
Java_me_magnum_melonds_MelonEmulator_getRewindWindow(JNIEnv* env, jobject thiz) {
auto currentRewindWindow = MelonDSAndroid::getRewindWindow();
jclass rewindSaveStateClass = env->FindClass("me/magnum/melonds/ui/emulator/rewind/model/RewindSaveState");
jmethodID rewindSaveStateConstructor = env->GetMethodID(rewindSaveStateClass, "<init>", "(Ljava/nio/ByteBuffer;Ljava/nio/ByteBuffer;I)V");
jclass listClass = env->FindClass("java/util/ArrayList");
jmethodID listConstructor = env->GetMethodID(listClass, "<init>", "()V");
jmethodID listAddMethod = env->GetMethodID(listClass, "add", "(ILjava/lang/Object;)V");
jobject rewindStateList = env->NewObject(listClass, listConstructor);
int index = 0;
for (auto state : currentRewindWindow.rewindStates) {
jobject stateBuffer = env->NewDirectByteBuffer(state.buffer, state.bufferSize);
jobject stateScreenshot = env->NewDirectByteBuffer(state.screenshot, state.screenshotSize);
jobject rewindSaveState = env->NewObject(rewindSaveStateClass, rewindSaveStateConstructor, stateBuffer, stateScreenshot, state.frame);
env->CallVoidMethod(rewindStateList, listAddMethod, index++, rewindSaveState);
}
jclass rewindWindowClass = env->FindClass("me/magnum/melonds/ui/emulator/rewind/model/RewindWindow");
jmethodID rewindWindowConstructor = env->GetMethodID(rewindWindowClass, "<init>", "(ILjava/util/ArrayList;)V");
jobject rewindWindow = env->NewObject(rewindWindowClass, rewindWindowConstructor, currentRewindWindow.currentFrame, rewindStateList);
return rewindWindow;
}
JNIEXPORT void JNICALL
Java_me_magnum_melonds_MelonEmulator_stopEmulation(JNIEnv* env, jobject thiz)
{
@ -414,6 +487,9 @@ MelonDSAndroid::EmulatorConfiguration buildEmulatorConfiguration(JNIEnv* env, jo
finalEmulatorConfiguration.micSource = micSource;
finalEmulatorConfiguration.firmwareConfiguration = buildFirmwareConfiguration(env, firmwareConfigurationObject);
finalEmulatorConfiguration.renderSettings = buildRenderSettings(env, rendererConfigurationObject);
finalEmulatorConfiguration.rewindEnabled = 1;
finalEmulatorConfiguration.rewindCaptureSpacingSeconds = 2;
finalEmulatorConfiguration.rewindLengthSeconds = 30;
return finalEmulatorConfiguration;
}

View File

@ -6,6 +6,8 @@ import me.magnum.melonds.domain.model.Cheat
import me.magnum.melonds.domain.model.EmulatorConfiguration
import me.magnum.melonds.domain.model.Input
import me.magnum.melonds.common.UriFileHandler
import me.magnum.melonds.ui.emulator.rewind.model.RewindSaveState
import me.magnum.melonds.ui.emulator.rewind.model.RewindWindow
import java.nio.ByteBuffer
object MelonEmulator {
@ -81,6 +83,10 @@ object MelonEmulator {
private external fun loadStateInternal(path: String): Boolean
external fun loadRewindState(rewindSaveState: RewindSaveState): Boolean
external fun getRewindWindow(): RewindWindow
external fun onScreenTouch(x: Int, y: Int)
external fun onScreenRelease()

View File

@ -17,8 +17,10 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.net.toUri
import androidx.core.view.*
import androidx.recyclerview.widget.LinearLayoutManager
import com.squareup.picasso.Picasso
import dagger.hilt.android.AndroidEntryPoint
import io.reactivex.Single
@ -42,6 +44,9 @@ import me.magnum.melonds.ui.cheats.CheatsActivity
import me.magnum.melonds.ui.emulator.DSRenderer.RendererListener
import me.magnum.melonds.ui.emulator.firmware.FirmwareEmulatorDelegate
import me.magnum.melonds.ui.emulator.input.*
import me.magnum.melonds.ui.emulator.rewind.EdgeSpacingDecorator
import me.magnum.melonds.ui.emulator.rewind.model.RewindSaveState
import me.magnum.melonds.ui.emulator.rewind.RewindSaveStateAdapter
import me.magnum.melonds.ui.emulator.rom.RomEmulatorDelegate
import me.magnum.melonds.ui.settings.SettingsActivity
import java.net.URLEncoder
@ -160,6 +165,10 @@ class EmulatorActivity : AppCompatActivity(), RendererListener {
microphonePermissionSubject.onNext(it)
}
private val rewindSaveStateAdapter = RewindSaveStateAdapter {
MelonEmulator.loadRewindState(it)
closeRewindWindow()
}
private val microphonePermissionSubject = PublishSubject.create<Boolean>()
private var emulatorSetupDisposable: Disposable? = null
private var cheatsClosedListener: (() -> Unit)? = null
@ -189,6 +198,15 @@ class EmulatorActivity : AppCompatActivity(), RendererListener {
binding.textFps.visibility = View.INVISIBLE
binding.viewLayoutControls.setLayoutComponentViewBuilderFactory(RuntimeLayoutComponentViewBuilderFactory())
binding.layoutRewind.setOnClickListener {
closeRewindWindow()
}
binding.listRewind.apply {
val listLayoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, true)
layoutManager = listLayoutManager
addItemDecoration(EdgeSpacingDecorator())
adapter = rewindSaveStateAdapter
}
viewModel.getBackground().observe(this) {
dsRenderer.setBackground(it)
@ -242,8 +260,9 @@ class EmulatorActivity : AppCompatActivity(), RendererListener {
super.onResume()
binding.surfaceMain.onResume()
if (emulatorReady && !emulatorPaused)
if (emulatorReady && !emulatorPaused) {
MelonEmulator.resumeEmulation()
}
}
private fun launchEmulator() {
@ -307,31 +326,33 @@ class EmulatorActivity : AppCompatActivity(), RendererListener {
binding.textFps.isGone = true
} else {
binding.textFps.isVisible = true
val newParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT)
val newParams = ConstraintLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT)
when (fpsCounterPosition) {
FpsCounterPosition.TOP_LEFT -> {
newParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT)
newParams.addRule(RelativeLayout.ALIGN_PARENT_TOP)
newParams.topToTop = ConstraintLayout.LayoutParams.PARENT_ID
newParams.leftToLeft = ConstraintLayout.LayoutParams.PARENT_ID
}
FpsCounterPosition.TOP_CENTER -> {
newParams.addRule(RelativeLayout.CENTER_HORIZONTAL)
newParams.addRule(RelativeLayout.ALIGN_PARENT_TOP)
newParams.topToTop = ConstraintLayout.LayoutParams.PARENT_ID
newParams.leftToLeft = ConstraintLayout.LayoutParams.PARENT_ID
newParams.rightToRight = ConstraintLayout.LayoutParams.PARENT_ID
}
FpsCounterPosition.TOP_RIGHT -> {
newParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT)
newParams.addRule(RelativeLayout.ALIGN_PARENT_TOP)
newParams.topToTop = ConstraintLayout.LayoutParams.PARENT_ID
newParams.rightToRight = ConstraintLayout.LayoutParams.PARENT_ID
}
FpsCounterPosition.BOTTOM_LEFT -> {
newParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT)
newParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
newParams.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID
newParams.leftToLeft = ConstraintLayout.LayoutParams.PARENT_ID
}
FpsCounterPosition.BOTTOM_CENTER -> {
newParams.addRule(RelativeLayout.CENTER_HORIZONTAL)
newParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
newParams.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID
newParams.leftToLeft = ConstraintLayout.LayoutParams.PARENT_ID
newParams.rightToRight = ConstraintLayout.LayoutParams.PARENT_ID
}
FpsCounterPosition.BOTTOM_RIGHT -> {
newParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT)
newParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
newParams.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID
newParams.rightToRight = ConstraintLayout.LayoutParams.PARENT_ID
}
}
binding.textFps.layoutParams = newParams
@ -443,7 +464,11 @@ class EmulatorActivity : AppCompatActivity(), RendererListener {
override fun onBackPressed() {
if (emulatorReady) {
this.pauseEmulation()
if (isRewindWindowOpen()) {
closeRewindWindow()
} else {
this.pauseEmulation()
}
} else {
super.onBackPressed()
}
@ -490,7 +515,7 @@ class EmulatorActivity : AppCompatActivity(), RendererListener {
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (nativeInputListener.onKeyEvent(event))
if (!isRewindWindowOpen() && nativeInputListener.onKeyEvent(event))
return true
return super.dispatchKeyEvent(event)
@ -520,6 +545,27 @@ class EmulatorActivity : AppCompatActivity(), RendererListener {
cheatsLauncher.launch(intent)
}
private fun isRewindWindowOpen(): Boolean {
return binding.root.currentState == R.id.rewind_visible
}
fun openRewindWindow() {
binding.root.transitionToState(R.id.rewind_visible)
val rewindWindow = MelonEmulator.getRewindWindow()
rewindSaveStateAdapter.setRewindWindow(rewindWindow)
}
fun closeRewindWindow() {
binding.root.transitionToState(R.id.rewind_hidden)
MelonEmulator.resumeEmulation()
}
fun rewindToState(state: RewindSaveState) {
if (!MelonEmulator.loadRewindState(state)) {
Toast.makeText(this@EmulatorActivity, "Failed to rewind", Toast.LENGTH_SHORT).show()
}
}
/**
* Returns a [Single] that emits the emulator's configuration taking into account permissions that have not been granted. If the provided base configuration requires the
* use of certain permissions and [requestPermissions] is true, they will be requested to the user before returning the final configuration.
@ -602,8 +648,9 @@ class EmulatorActivity : AppCompatActivity(), RendererListener {
super.onPause()
binding.surfaceMain.onPause()
if (emulatorReady && !emulatorPaused)
if (emulatorReady && !emulatorPaused) {
MelonEmulator.pauseEmulation()
}
}
override fun onDestroy() {

View File

@ -0,0 +1,38 @@
package me.magnum.melonds.ui.emulator.rewind
import android.graphics.Rect
import android.view.View
import androidx.core.view.marginLeft
import androidx.core.view.marginRight
import androidx.recyclerview.widget.RecyclerView
/**
* [RecyclerView.ItemDecoration] that adds spacing to the edge views so that these items always appear centered in the view. Only works for horizontal lists.
*/
class EdgeSpacingDecorator : RecyclerView.ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
super.getItemOffsets(outRect, view, parent, state)
val position = parent.getChildAdapterPosition(view)
if (position == 0) {
val offset = getEdgeViewOffset(view, parent)
outRect.right = offset
} else if (position == state.itemCount - 1) {
val offset = getEdgeViewOffset(view, parent)
outRect.left = offset
}
}
private fun getEdgeViewOffset(view: View, parent: RecyclerView): Int {
val viewWidth = if (view.width == 0) {
// View size still unknown. Measure it
view.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
view.measuredWidth
} else {
view.width
}
return (parent.width - viewWidth - view.marginLeft - view.marginRight) / 2
}
}

View File

@ -0,0 +1,97 @@
package me.magnum.melonds.ui.emulator.rewind
import android.content.Context
import android.graphics.drawable.BitmapDrawable
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.marginLeft
import androidx.core.view.marginRight
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import me.magnum.melonds.R
import me.magnum.melonds.databinding.ItemRewindSaveStateBinding
import me.magnum.melonds.ui.emulator.rewind.model.RewindSaveState
import me.magnum.melonds.ui.emulator.rewind.model.RewindWindow
import java.text.DecimalFormat
import java.time.Duration
class RewindSaveStateAdapter(private val onRewindSaveStateSelected: (RewindSaveState) -> Unit) : RecyclerView.Adapter<RewindSaveStateAdapter.RewindSaveStateViewHolder>() {
class RewindSaveStateViewHolder(private val context: Context, private val binding: ItemRewindSaveStateBinding) : RecyclerView.ViewHolder(binding.root) {
companion object {
private val SECONDS_FORMATTER = DecimalFormat("#0.##")
}
private lateinit var state: RewindSaveState
fun setRewindSaveState(state: RewindSaveState, window: RewindWindow) {
val screenshotDrawable = BitmapDrawable(context.resources, state.screenshot)
val durationToState = window.getDeltaFromEmulationTimeToRewindState(state)
binding.imageScreenshot.setImageDrawable(screenshotDrawable)
binding.textTimestamp.text = getDurationString(context, durationToState)
this.state = state
}
fun getRewindSaveState(): RewindSaveState {
return state
}
private fun getDurationString(context: Context, duration: Duration): String {
val minutes = duration.toMinutes()
return if (minutes >= 1) {
val seconds = duration.minusMinutes(minutes).toMillis() / 1000f
context.getString(R.string.rewind_time_minutes_seconds, minutes, SECONDS_FORMATTER.format(seconds))
} else {
val seconds = duration.toMillis() / 1000f
context.getString(R.string.rewind_time_seconds, SECONDS_FORMATTER.format(seconds))
}
}
}
private var currentRewindWindow: RewindWindow? = null
private var recyclerView: RecyclerView? = null
fun setRewindWindow(rewindWindow: RewindWindow) {
currentRewindWindow = rewindWindow
notifyDataSetChanged()
// Reset scroll
recyclerView?.scrollToPosition(0)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RewindSaveStateViewHolder {
val binding = ItemRewindSaveStateBinding.inflate(LayoutInflater.from(parent.context), parent, false)
val holder = RewindSaveStateViewHolder(parent.context, binding)
binding.root.setOnClickListener {
onRewindSaveStateSelected(holder.getRewindSaveState())
}
binding.root.setOnFocusChangeListener { v, hasFocus ->
if (hasFocus) {
recyclerView?.let {
// Scroll to new focused item (allows controller users to properly navigate the list)
val linearLayoutManager = it.layoutManager as? LinearLayoutManager ?: return@let
val position = linearLayoutManager.getPosition(v)
val offset = (it.width - v.width - v.marginRight - v.marginLeft) / 2
linearLayoutManager.scrollToPositionWithOffset(position, offset)
}
}
}
return holder
}
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
this.recyclerView = recyclerView
}
override fun onBindViewHolder(holder: RewindSaveStateViewHolder, position: Int) {
currentRewindWindow?.let {
val state = it.rewindStates[position]
holder.setRewindSaveState(state, it)
}
}
override fun getItemCount(): Int {
return currentRewindWindow?.rewindStates?.size ?: 0
}
}

View File

@ -0,0 +1,13 @@
package me.magnum.melonds.ui.emulator.rewind.model
import android.graphics.Bitmap
import me.magnum.melonds.utils.DsScreenshotConverter
import java.nio.ByteBuffer
class RewindSaveState(
val buffer: ByteBuffer,
val screenshotBuffer: ByteBuffer,
val frame: Int,
) {
val screenshot: Bitmap get() = DsScreenshotConverter.fromByteBufferToBitmap(screenshotBuffer)
}

View File

@ -0,0 +1,20 @@
package me.magnum.melonds.ui.emulator.rewind.model
import java.time.Duration
import java.util.*
class RewindWindow(
val currentEmulationFrame: Int,
val rewindStates: ArrayList<RewindSaveState>
) {
companion object {
private const val FRAMES_PER_SECOND = 60
}
fun getDeltaFromEmulationTimeToRewindState(state: RewindSaveState): Duration {
val elapsedFrames = currentEmulationFrame - state.frame
val elapsedMillis = elapsedFrames.toFloat() / FRAMES_PER_SECOND * 1000
return Duration.ofMillis(elapsedMillis.toLong())
}
}

View File

@ -27,6 +27,7 @@ class RomEmulatorDelegate(activity: EmulatorActivity, private val picasso: Picas
SETTINGS(R.string.settings),
SAVE_STATE(R.string.save_state),
LOAD_STATE(R.string.load_state),
REWIND(R.string.rewind),
CHEATS(R.string.cheats),
RESET(R.string.reset),
EXIT(R.string.exit)
@ -142,6 +143,7 @@ class RomEmulatorDelegate(activity: EmulatorActivity, private val picasso: Picas
loadState(it)
activity.resumeEmulation()
}
RomPauseMenuOptions.REWIND -> activity.openRewindWindow()
RomPauseMenuOptions.CHEATS -> openCheatsActivity()
RomPauseMenuOptions.RESET -> activity.resetEmulation()
RomPauseMenuOptions.EXIT -> activity.finish()

View File

@ -0,0 +1,24 @@
package me.magnum.melonds.utils
import android.graphics.Bitmap
import java.nio.ByteBuffer
object DsScreenshotConverter {
private const val SCREEN_WIDTH = 256
private const val SCREEN_HEIGHT = 384
fun fromByteBufferToBitmap(buffer: ByteBuffer): Bitmap {
return Bitmap.createBitmap(SCREEN_WIDTH, SCREEN_HEIGHT, Bitmap.Config.ARGB_8888).apply {
// Texture buffer is in BGR format. Convert to RGB
for (x in 0 until SCREEN_WIDTH) {
for (y in 0 until SCREEN_HEIGHT) {
val b = buffer[(y * SCREEN_WIDTH + x) * 4 + 0].toInt() and 0xFF
val g = buffer[(y * SCREEN_WIDTH + x) * 4 + 1].toInt() and 0xFF
val r = buffer[(y * SCREEN_WIDTH + x) * 4 + 2].toInt() and 0xFF
val argbPixel = 0xFF000000.toInt() or r.shl(16) or g.shl(8) or b
setPixel(x, y, argbPixel)
}
}
}
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="#FFF" />
<corners android:radius="8dp" />
</shape>
</item>
</layer-list>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="true" android:drawable="@drawable/background_rewind_save_state_focused" />
<item android:drawable="@android:color/transparent" />
</selector>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="true" android:color="@android:color/black" />
<item android:color="@android:color/white" />
</selector>

View File

@ -1,40 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/layout_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
tools:context=".ui.emulator.EmulatorActivity">
<androidx.constraintlayout.motion.widget.MotionLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/layout_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
app:layoutDescription="@xml/scene_activity_emulator"
tools:context=".ui.emulator.EmulatorActivity">
<android.opengl.GLSurfaceView
android:id="@+id/surfaceMain"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
android:id="@+id/surfaceMain"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<me.magnum.melonds.ui.emulator.RuntimeLayoutView
android:id="@+id/view_layout_controls"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:id="@+id/view_layout_controls"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:id="@+id/textFps"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="12dp"
android:textColor="@android:color/white"
tools:layout_alignParentBottom="true"
tools:layout_centerHorizontal="true"
tools:text="FPS: 60"/>
android:id="@+id/textFps"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="12dp"
android:textColor="@android:color/white"
tools:text="FPS: 60"/>
<TextView
android:id="@+id/textLoading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textSize="32sp"
android:textStyle="bold"
android:textColor="@android:color/white"
android:text="@string/info_loading"/>
</RelativeLayout>
android:id="@+id/textLoading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:textSize="32sp"
android:textStyle="bold"
android:textColor="@android:color/white"
android:text="@string/info_loading"/>
<RelativeLayout
android:id="@+id/layout_rewind"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#A000"
android:focusable="false">
</RelativeLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list_rewind"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.motion.widget.MotionLayout>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:padding="8dp"
android:background="@drawable/selector_rewind_save_state"
android:clickable="true"
android:focusable="true">
<ImageView
android:id="@+id/image_screenshot"
android:layout_width="95dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:contentDescription="@null"
android:scaleType="fitCenter"
android:adjustViewBounds="true" />
<TextView
android:id="@+id/text_timestamp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@drawable/selector_rewind_save_state_time_text"
app:layout_constraintTop_toBottomOf="@id/image_screenshot"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:duplicateParentState="true" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -49,6 +49,8 @@
<string name="cant_load_empty_slot">Can\'t load an empty save state slot</string>
<string name="failed_reset_emulation">Failed to reset emulation</string>
<string name="save_states_not_supported">Save states are not supported while running the firmware</string>
<string name="rewind_time_seconds">%1$ss</string>
<string name="rewind_time_minutes_seconds">%1$dm%2$ss</string>
<string name="action_sort_alphabetically">Alphabetically</string>
<string name="action_sort_recently_played">Recently played</string>
@ -80,6 +82,7 @@
<string name="settings">Settings</string>
<string name="save_state">Save state</string>
<string name="load_state">Load state</string>
<string name="rewind">Rewind</string>
<string name="save_slot">Save slot</string>
<string name="empty_slot">%1$s. &lt;Empty&gt;</string>
<string name="quick_slot">Quick Slot</string>

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<MotionScene
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<ConstraintSet android:id="@+id/rewind_hidden">
<Constraint
android:id="@+id/layout_rewind"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:alpha="0"
android:visibility="invisible" />
<Constraint
android:id="@+id/list_rewind"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="parent"
android:visibility="invisible" />
<Constraint android:id="@+id/textFps">
<PropertySet
app:applyMotionScene="false"
app:visibilityMode="ignore" />
</Constraint>
<Constraint android:id="@+id/textLoading">
<PropertySet
app:applyMotionScene="false"
app:visibilityMode="ignore" />
</Constraint>
<Constraint android:id="@+id/view_layout_controls">
<PropertySet
app:applyMotionScene="false"
app:visibilityMode="ignore" />
</Constraint>
</ConstraintSet>
<ConstraintSet
android:id="@+id/rewind_visible"
app:deriveConstraintsFrom="@+id/rewind_hidden">
<Constraint
android:id="@id/layout_rewind"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:alpha="1"
android:visibility="visible" />
<Constraint
android:id="@+id/list_rewind"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
android:visibility="visible" />
</ConstraintSet>
<Transition
app:constraintSetStart="@+id/rewind_hidden"
app:constraintSetEnd="@id/rewind_visible"
app:duration="250" />
</MotionScene>

View File

@ -6,6 +6,7 @@ object Dependencies {
const val CommonsCompress = "1.21"
const val ConstraintLayout = "2.0.4"
const val Core = "1.6.0"
const val Desugar = "1.1.5"
const val DocumentFile = "1.0.1"
const val Flexbox = "2.0.1"
const val Fragment = "1.3.5"
@ -38,6 +39,10 @@ object Dependencies {
const val kotlin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.Kotlin}"
}
object Tools {
const val desugarJdkLibs = "com.android.tools:desugar_jdk_libs:${Versions.Desugar}"
}
object Kotlin {
const val kotlinStdlib = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${Versions.Kotlin}"
}

@ -1 +1 @@
Subproject commit bb35140d03af0034f7f759bcb497289d7e7603c5
Subproject commit 159c113545484fc38c0d74c7f8d066a626093911