committed by
bunnei
6 changed files with 255 additions and 279 deletions
-
14src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
-
264src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/SoftwareKeyboard.java
-
117src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard.kt
-
100src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/keyboard/ui/KeyboardDialogFragment.kt
-
16src/android/app/src/main/jni/applets/software_keyboard.cpp
-
23src/android/app/src/main/res/layout/dialog_edit_text.xml
@ -1,264 +0,0 @@ |
|||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project |
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later |
|
||||
|
|
||||
package org.yuzu.yuzu_emu.applets; |
|
||||
|
|
||||
import android.app.Activity; |
|
||||
import android.app.Dialog; |
|
||||
import android.content.Context; |
|
||||
import android.content.DialogInterface; |
|
||||
import android.graphics.Rect; |
|
||||
import android.os.Bundle; |
|
||||
import android.os.Handler; |
|
||||
import android.os.ResultReceiver; |
|
||||
import android.text.InputFilter; |
|
||||
import android.text.InputType; |
|
||||
import android.view.ViewGroup; |
|
||||
import android.view.ViewTreeObserver; |
|
||||
import android.view.WindowInsets; |
|
||||
import android.view.inputmethod.InputMethodManager; |
|
||||
import android.widget.EditText; |
|
||||
import android.widget.FrameLayout; |
|
||||
|
|
||||
import androidx.annotation.NonNull; |
|
||||
import androidx.appcompat.app.AlertDialog; |
|
||||
import androidx.core.view.ViewCompat; |
|
||||
import androidx.fragment.app.DialogFragment; |
|
||||
|
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder; |
|
||||
|
|
||||
import org.yuzu.yuzu_emu.YuzuApplication; |
|
||||
import org.yuzu.yuzu_emu.NativeLibrary; |
|
||||
import org.yuzu.yuzu_emu.R; |
|
||||
import org.yuzu.yuzu_emu.activities.EmulationActivity; |
|
||||
|
|
||||
import java.util.Objects; |
|
||||
|
|
||||
public final class SoftwareKeyboard { |
|
||||
/// Corresponds to Service::AM::Applets::SwkbdType |
|
||||
private interface SwkbdType { |
|
||||
int Normal = 0; |
|
||||
int NumberPad = 1; |
|
||||
int Qwerty = 2; |
|
||||
int Unknown3 = 3; |
|
||||
int Latin = 4; |
|
||||
int SimplifiedChinese = 5; |
|
||||
int TraditionalChinese = 6; |
|
||||
int Korean = 7; |
|
||||
}; |
|
||||
|
|
||||
/// Corresponds to Service::AM::Applets::SwkbdPasswordMode |
|
||||
private interface SwkbdPasswordMode { |
|
||||
int Disabled = 0; |
|
||||
int Enabled = 1; |
|
||||
}; |
|
||||
|
|
||||
/// Corresponds to Service::AM::Applets::SwkbdResult |
|
||||
private interface SwkbdResult { |
|
||||
int Ok = 0; |
|
||||
int Cancel = 1; |
|
||||
}; |
|
||||
|
|
||||
public static class KeyboardConfig implements java.io.Serializable { |
|
||||
public String ok_text; |
|
||||
public String header_text; |
|
||||
public String sub_text; |
|
||||
public String guide_text; |
|
||||
public String initial_text; |
|
||||
public short left_optional_symbol_key; |
|
||||
public short right_optional_symbol_key; |
|
||||
public int max_text_length; |
|
||||
public int min_text_length; |
|
||||
public int initial_cursor_position; |
|
||||
public int type; |
|
||||
public int password_mode; |
|
||||
public int text_draw_type; |
|
||||
public int key_disable_flags; |
|
||||
public boolean use_blur_background; |
|
||||
public boolean enable_backspace_button; |
|
||||
public boolean enable_return_button; |
|
||||
public boolean disable_cancel_button; |
|
||||
} |
|
||||
|
|
||||
/// Corresponds to Frontend::KeyboardData |
|
||||
public static class KeyboardData { |
|
||||
public int result; |
|
||||
public String text; |
|
||||
|
|
||||
private KeyboardData(int result, String text) { |
|
||||
this.result = result; |
|
||||
this.text = text; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public static class KeyboardDialogFragment extends DialogFragment { |
|
||||
static KeyboardDialogFragment newInstance(KeyboardConfig config) { |
|
||||
KeyboardDialogFragment frag = new KeyboardDialogFragment(); |
|
||||
Bundle args = new Bundle(); |
|
||||
args.putSerializable("config", config); |
|
||||
frag.setArguments(args); |
|
||||
return frag; |
|
||||
} |
|
||||
|
|
||||
@NonNull |
|
||||
@Override |
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) { |
|
||||
final Activity emulationActivity = getActivity(); |
|
||||
assert emulationActivity != null; |
|
||||
|
|
||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( |
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); |
|
||||
params.leftMargin = params.rightMargin = |
|
||||
YuzuApplication.getAppContext().getResources().getDimensionPixelSize( |
|
||||
R.dimen.dialog_margin); |
|
||||
|
|
||||
KeyboardConfig config = Objects.requireNonNull( |
|
||||
(KeyboardConfig) requireArguments().getSerializable("config")); |
|
||||
|
|
||||
// Set up the input |
|
||||
EditText editText = new EditText(YuzuApplication.getAppContext()); |
|
||||
editText.setHint(config.initial_text); |
|
||||
editText.setSingleLine(!config.enable_return_button); |
|
||||
editText.setLayoutParams(params); |
|
||||
editText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(config.max_text_length)}); |
|
||||
|
|
||||
// Handle input type |
|
||||
int input_type = 0; |
|
||||
switch (config.type) |
|
||||
{ |
|
||||
case SwkbdType.Normal: |
|
||||
case SwkbdType.Qwerty: |
|
||||
case SwkbdType.Unknown3: |
|
||||
case SwkbdType.Latin: |
|
||||
case SwkbdType.SimplifiedChinese: |
|
||||
case SwkbdType.TraditionalChinese: |
|
||||
case SwkbdType.Korean: |
|
||||
default: |
|
||||
input_type = InputType.TYPE_CLASS_TEXT; |
|
||||
if (config.password_mode == SwkbdPasswordMode.Enabled) |
|
||||
{ |
|
||||
input_type |= InputType.TYPE_TEXT_VARIATION_PASSWORD; |
|
||||
} |
|
||||
break; |
|
||||
case SwkbdType.NumberPad: |
|
||||
input_type = InputType.TYPE_CLASS_NUMBER; |
|
||||
if (config.password_mode == SwkbdPasswordMode.Enabled) |
|
||||
{ |
|
||||
input_type |= InputType.TYPE_NUMBER_VARIATION_PASSWORD; |
|
||||
} |
|
||||
break; |
|
||||
} |
|
||||
|
|
||||
// Apply input type |
|
||||
editText.setInputType(input_type); |
|
||||
|
|
||||
FrameLayout container = new FrameLayout(emulationActivity); |
|
||||
container.addView(editText); |
|
||||
|
|
||||
String headerText = config.header_text.isEmpty() ? emulationActivity.getString(R.string.software_keyboard) : config.header_text; |
|
||||
String okText = config.header_text.isEmpty() ? emulationActivity.getString(android.R.string.ok) : config.ok_text; |
|
||||
|
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(emulationActivity) |
|
||||
.setTitle(headerText) |
|
||||
.setView(container); |
|
||||
setCancelable(false); |
|
||||
|
|
||||
builder.setPositiveButton(okText, null); |
|
||||
builder.setNegativeButton(emulationActivity.getString(android.R.string.cancel), null); |
|
||||
|
|
||||
final AlertDialog dialog = builder.create(); |
|
||||
dialog.create(); |
|
||||
if (dialog.getButton(DialogInterface.BUTTON_POSITIVE) != null) { |
|
||||
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener((view) -> { |
|
||||
data.result = SwkbdResult.Ok; |
|
||||
data.text = editText.getText().toString(); |
|
||||
dialog.dismiss(); |
|
||||
|
|
||||
synchronized (finishLock) { |
|
||||
finishLock.notifyAll(); |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
if (dialog.getButton(DialogInterface.BUTTON_NEUTRAL) != null) { |
|
||||
dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener((view) -> { |
|
||||
data.result = SwkbdResult.Ok; |
|
||||
dialog.dismiss(); |
|
||||
synchronized (finishLock) { |
|
||||
finishLock.notifyAll(); |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
if (dialog.getButton(DialogInterface.BUTTON_NEGATIVE) != null) { |
|
||||
dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((view) -> { |
|
||||
data.result = SwkbdResult.Cancel; |
|
||||
dialog.dismiss(); |
|
||||
synchronized (finishLock) { |
|
||||
finishLock.notifyAll(); |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
return dialog; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private static KeyboardData data; |
|
||||
private static final Object finishLock = new Object(); |
|
||||
|
|
||||
private static void ExecuteNormalImpl(KeyboardConfig config) { |
|
||||
final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); |
|
||||
|
|
||||
data = new KeyboardData(SwkbdResult.Cancel, ""); |
|
||||
|
|
||||
KeyboardDialogFragment fragment = KeyboardDialogFragment.newInstance(config); |
|
||||
fragment.show(emulationActivity.getSupportFragmentManager(), "keyboard"); |
|
||||
} |
|
||||
|
|
||||
private static void ExecuteInlineImpl(KeyboardConfig config) { |
|
||||
final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); |
|
||||
|
|
||||
var overlayView = emulationActivity.findViewById(R.id.surface_input_overlay); |
|
||||
InputMethodManager im = (InputMethodManager)overlayView.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); |
|
||||
im.showSoftInput(overlayView, InputMethodManager.SHOW_FORCED); |
|
||||
|
|
||||
// There isn't a good way to know that the IMM is dismissed, so poll every 500ms to submit inline keyboard result. |
|
||||
final Handler handler = new Handler(); |
|
||||
final int delayMs = 500; |
|
||||
handler.postDelayed(new Runnable() { |
|
||||
public void run() { |
|
||||
var insets = ViewCompat.getRootWindowInsets(overlayView); |
|
||||
var isKeyboardVisible = insets.isVisible(WindowInsets.Type.ime()); |
|
||||
if (isKeyboardVisible) { |
|
||||
handler.postDelayed(this, delayMs); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
// No longer visible, submit the result. |
|
||||
NativeLibrary.SubmitInlineKeyboardInput(android.view.KeyEvent.KEYCODE_ENTER); |
|
||||
} |
|
||||
}, delayMs); |
|
||||
} |
|
||||
|
|
||||
public static KeyboardData ExecuteNormal(KeyboardConfig config) { |
|
||||
NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteNormalImpl(config)); |
|
||||
|
|
||||
synchronized (finishLock) { |
|
||||
try { |
|
||||
finishLock.wait(); |
|
||||
} catch (Exception ignored) { |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return data; |
|
||||
} |
|
||||
|
|
||||
public static void ExecuteInline(KeyboardConfig config) { |
|
||||
NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteInlineImpl(config)); |
|
||||
} |
|
||||
|
|
||||
public static void ShowError(String error) { |
|
||||
NativeLibrary.displayAlertMsg( |
|
||||
YuzuApplication.getAppContext().getResources().getString(R.string.software_keyboard), |
|
||||
error, false); |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,117 @@ |
|||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project |
||||
|
// SPDX-License-Identifier: GPL-2.0-or-later |
||||
|
|
||||
|
package org.yuzu.yuzu_emu.applets.keyboard |
||||
|
|
||||
|
import android.content.Context |
||||
|
import android.os.Handler |
||||
|
import android.os.Looper |
||||
|
import android.view.KeyEvent |
||||
|
import android.view.View |
||||
|
import android.view.WindowInsets |
||||
|
import android.view.inputmethod.InputMethodManager |
||||
|
import androidx.core.view.ViewCompat |
||||
|
import org.yuzu.yuzu_emu.NativeLibrary |
||||
|
import org.yuzu.yuzu_emu.R |
||||
|
import org.yuzu.yuzu_emu.applets.keyboard.ui.KeyboardDialogFragment |
||||
|
import java.io.Serializable |
||||
|
|
||||
|
object SoftwareKeyboard { |
||||
|
lateinit var data: KeyboardData |
||||
|
val dataLock = Object() |
||||
|
|
||||
|
private fun executeNormalImpl(config: KeyboardConfig) { |
||||
|
val emulationActivity = NativeLibrary.sEmulationActivity.get() |
||||
|
data = KeyboardData(SwkbdResult.Cancel.ordinal, "") |
||||
|
val fragment = KeyboardDialogFragment.newInstance(config) |
||||
|
fragment.show(emulationActivity!!.supportFragmentManager, KeyboardDialogFragment.TAG) |
||||
|
} |
||||
|
|
||||
|
private fun executeInlineImpl(config: KeyboardConfig) { |
||||
|
val emulationActivity = NativeLibrary.sEmulationActivity.get() |
||||
|
|
||||
|
val overlayView = emulationActivity!!.findViewById<View>(R.id.surface_input_overlay) |
||||
|
val im = |
||||
|
overlayView.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager |
||||
|
im.showSoftInput(overlayView, InputMethodManager.SHOW_FORCED) |
||||
|
|
||||
|
// There isn't a good way to know that the IMM is dismissed, so poll every 500ms to submit inline keyboard result. |
||||
|
val handler = Handler(Looper.myLooper()!!) |
||||
|
val delayMs = 500 |
||||
|
handler.postDelayed(object : Runnable { |
||||
|
override fun run() { |
||||
|
val insets = ViewCompat.getRootWindowInsets(overlayView) |
||||
|
val isKeyboardVisible = insets!!.isVisible(WindowInsets.Type.ime()) |
||||
|
if (isKeyboardVisible) { |
||||
|
handler.postDelayed(this, delayMs.toLong()) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// No longer visible, submit the result. |
||||
|
NativeLibrary.SubmitInlineKeyboardInput(KeyEvent.KEYCODE_ENTER) |
||||
|
} |
||||
|
}, delayMs.toLong()) |
||||
|
} |
||||
|
|
||||
|
@JvmStatic |
||||
|
fun executeNormal(config: KeyboardConfig): KeyboardData { |
||||
|
NativeLibrary.sEmulationActivity.get()!!.runOnUiThread { executeNormalImpl(config) } |
||||
|
synchronized(dataLock) { |
||||
|
dataLock.wait() |
||||
|
} |
||||
|
return data |
||||
|
} |
||||
|
|
||||
|
@JvmStatic |
||||
|
fun executeInline(config: KeyboardConfig) { |
||||
|
NativeLibrary.sEmulationActivity.get()!!.runOnUiThread { executeInlineImpl(config) } |
||||
|
} |
||||
|
|
||||
|
// Corresponds to Service::AM::Applets::SwkbdType |
||||
|
enum class SwkbdType { |
||||
|
Normal, |
||||
|
NumberPad, |
||||
|
Qwerty, |
||||
|
Unknown3, |
||||
|
Latin, |
||||
|
SimplifiedChinese, |
||||
|
TraditionalChinese, |
||||
|
Korean |
||||
|
} |
||||
|
|
||||
|
// Corresponds to Service::AM::Applets::SwkbdPasswordMode |
||||
|
enum class SwkbdPasswordMode { |
||||
|
Disabled, |
||||
|
Enabled |
||||
|
} |
||||
|
|
||||
|
// Corresponds to Service::AM::Applets::SwkbdResult |
||||
|
enum class SwkbdResult { |
||||
|
Ok, |
||||
|
Cancel |
||||
|
} |
||||
|
|
||||
|
data class KeyboardConfig( |
||||
|
var ok_text: String? = null, |
||||
|
var header_text: String? = null, |
||||
|
var sub_text: String? = null, |
||||
|
var guide_text: String? = null, |
||||
|
var initial_text: String? = null, |
||||
|
var left_optional_symbol_key: Short = 0, |
||||
|
var right_optional_symbol_key: Short = 0, |
||||
|
var max_text_length: Int = 0, |
||||
|
var min_text_length: Int = 0, |
||||
|
var initial_cursor_position: Int = 0, |
||||
|
var type: Int = 0, |
||||
|
var password_mode: Int = 0, |
||||
|
var text_draw_type: Int = 0, |
||||
|
var key_disable_flags: Int = 0, |
||||
|
var use_blur_background: Boolean = false, |
||||
|
var enable_backspace_button: Boolean = false, |
||||
|
var enable_return_button: Boolean = false, |
||||
|
var disable_cancel_button: Boolean = false |
||||
|
) : Serializable |
||||
|
|
||||
|
// Corresponds to Frontend::KeyboardData |
||||
|
data class KeyboardData(var result: Int, var text: String) |
||||
|
} |
||||
@ -0,0 +1,100 @@ |
|||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project |
||||
|
// SPDX-License-Identifier: GPL-2.0-or-later |
||||
|
|
||||
|
package org.yuzu.yuzu_emu.applets.keyboard.ui |
||||
|
|
||||
|
import android.app.Dialog |
||||
|
import android.content.DialogInterface |
||||
|
import android.os.Bundle |
||||
|
import android.text.InputFilter |
||||
|
import android.text.InputType |
||||
|
import androidx.fragment.app.DialogFragment |
||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder |
||||
|
import org.yuzu.yuzu_emu.R |
||||
|
import org.yuzu.yuzu_emu.applets.keyboard.SoftwareKeyboard |
||||
|
import org.yuzu.yuzu_emu.applets.keyboard.SoftwareKeyboard.KeyboardConfig |
||||
|
import org.yuzu.yuzu_emu.databinding.DialogEditTextBinding |
||||
|
import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable |
||||
|
|
||||
|
class KeyboardDialogFragment : DialogFragment() { |
||||
|
private lateinit var binding: DialogEditTextBinding |
||||
|
private lateinit var config: KeyboardConfig |
||||
|
|
||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { |
||||
|
binding = DialogEditTextBinding.inflate(layoutInflater) |
||||
|
config = requireArguments().serializable(CONFIG)!! |
||||
|
|
||||
|
// Set up the input |
||||
|
binding.editText.hint = config.initial_text |
||||
|
binding.editText.isSingleLine = !config.enable_return_button |
||||
|
binding.editText.filters = |
||||
|
arrayOf<InputFilter>(InputFilter.LengthFilter(config.max_text_length)) |
||||
|
|
||||
|
// Handle input type |
||||
|
var inputType: Int |
||||
|
when (config.type) { |
||||
|
SoftwareKeyboard.SwkbdType.Normal.ordinal, |
||||
|
SoftwareKeyboard.SwkbdType.Qwerty.ordinal, |
||||
|
SoftwareKeyboard.SwkbdType.Unknown3.ordinal, |
||||
|
SoftwareKeyboard.SwkbdType.Latin.ordinal, |
||||
|
SoftwareKeyboard.SwkbdType.SimplifiedChinese.ordinal, |
||||
|
SoftwareKeyboard.SwkbdType.TraditionalChinese.ordinal, |
||||
|
SoftwareKeyboard.SwkbdType.Korean.ordinal -> { |
||||
|
inputType = InputType.TYPE_CLASS_TEXT |
||||
|
if (config.password_mode == SoftwareKeyboard.SwkbdPasswordMode.Enabled.ordinal) { |
||||
|
inputType = inputType or InputType.TYPE_TEXT_VARIATION_PASSWORD |
||||
|
} |
||||
|
} |
||||
|
SoftwareKeyboard.SwkbdType.NumberPad.ordinal -> { |
||||
|
inputType = InputType.TYPE_CLASS_NUMBER |
||||
|
if (config.password_mode == SoftwareKeyboard.SwkbdPasswordMode.Enabled.ordinal) { |
||||
|
inputType = inputType or InputType.TYPE_NUMBER_VARIATION_PASSWORD |
||||
|
} |
||||
|
} |
||||
|
else -> { |
||||
|
inputType = InputType.TYPE_CLASS_TEXT |
||||
|
if (config.password_mode == SoftwareKeyboard.SwkbdPasswordMode.Enabled.ordinal) { |
||||
|
inputType = inputType or InputType.TYPE_TEXT_VARIATION_PASSWORD |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
binding.editText.inputType = inputType |
||||
|
|
||||
|
val headerText = |
||||
|
config.header_text!!.ifEmpty { resources.getString(R.string.software_keyboard) } |
||||
|
val okText = |
||||
|
if (config.header_text!!.isEmpty()) resources.getString(android.R.string.ok) else config.ok_text!! |
||||
|
|
||||
|
return MaterialAlertDialogBuilder(requireContext()) |
||||
|
.setTitle(headerText) |
||||
|
.setView(binding.root) |
||||
|
.setPositiveButton(okText) { _, _ -> |
||||
|
SoftwareKeyboard.data.result = SoftwareKeyboard.SwkbdResult.Ok.ordinal |
||||
|
SoftwareKeyboard.data.text = binding.editText.text.toString() |
||||
|
} |
||||
|
.setNegativeButton(resources.getString(android.R.string.cancel)) { _, _ -> |
||||
|
SoftwareKeyboard.data.result = SoftwareKeyboard.SwkbdResult.Cancel.ordinal |
||||
|
} |
||||
|
.create() |
||||
|
} |
||||
|
|
||||
|
override fun onDismiss(dialog: DialogInterface) { |
||||
|
super.onDismiss(dialog) |
||||
|
synchronized(SoftwareKeyboard.dataLock) { |
||||
|
SoftwareKeyboard.dataLock.notifyAll() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
companion object { |
||||
|
const val TAG = "KeyboardDialogFragment" |
||||
|
const val CONFIG = "keyboard_config" |
||||
|
|
||||
|
fun newInstance(config: KeyboardConfig?): KeyboardDialogFragment { |
||||
|
val frag = KeyboardDialogFragment() |
||||
|
val args = Bundle() |
||||
|
args.putSerializable(CONFIG, config) |
||||
|
frag.arguments = args |
||||
|
return frag |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
<?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="match_parent" |
||||
|
android:layout_height="match_parent"> |
||||
|
|
||||
|
<com.google.android.material.textfield.TextInputLayout |
||||
|
android:id="@+id/edit_text_layout" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:layout_margin="24dp" |
||||
|
app:layout_constraintTop_toTopOf="parent"> |
||||
|
|
||||
|
<com.google.android.material.textfield.TextInputEditText |
||||
|
android:id="@+id/edit_text" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:inputType="none" /> |
||||
|
|
||||
|
</com.google.android.material.textfield.TextInputLayout> |
||||
|
|
||||
|
</androidx.constraintlayout.widget.ConstraintLayout> |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue