Add basic attachment keyboard support to CFv2.

This commit is contained in:
Cody Henthorne
2023-05-17 10:28:50 -04:00
committed by Greyson Parrelli
parent 0c57113d8e
commit 4b09f4a654
13 changed files with 555 additions and 115 deletions

View File

@@ -0,0 +1,97 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components
import android.content.Context
import android.util.AttributeSet
import android.widget.EditText
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.util.ViewUtil
/**
* A flavor of [InsetAwareConstraintLayout] that allows "replacing" the keyboard with our
* own input fragment.
*/
class InputAwareConstraintLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : InsetAwareConstraintLayout(context, attrs, defStyleAttr) {
private var inputId: Int? = null
private var input: Fragment? = null
lateinit var fragmentManager: FragmentManager
var listener: Listener? = null
fun showSoftkey(editText: EditText) {
ViewUtil.focusAndShowKeyboard(editText)
hideInput(resetKeyboardGuideline = false)
}
fun toggleInput(fragmentCreator: FragmentCreator, imeTarget: EditText, toggled: (Boolean) -> Unit = { }) {
if (fragmentCreator.id == inputId) {
hideInput(resetKeyboardGuideline = true)
toggled(false)
} else {
hideInput(resetKeyboardGuideline = false)
showInput(fragmentCreator, imeTarget)
}
}
fun hideInput() {
hideInput(resetKeyboardGuideline = true)
}
private fun showInput(fragmentCreator: FragmentCreator, imeTarget: EditText) {
inputId = fragmentCreator.id
input = fragmentCreator.create()
fragmentManager
.beginTransaction()
.replace(R.id.input_container, input!!)
.commit()
overrideKeyboardGuidelineWithPreviousHeight()
ViewUtil.hideKeyboard(context, imeTarget)
listener?.onInputShown()
}
private fun hideInput(resetKeyboardGuideline: Boolean) {
val inputHidden = input != null
input?.let {
fragmentManager
.beginTransaction()
.remove(it)
.commit()
}
input = null
inputId = null
if (resetKeyboardGuideline) {
resetKeyboardGuideline()
} else {
clearKeyboardGuidelineOverride()
}
if (inputHidden) {
listener?.onInputHidden()
}
}
interface FragmentCreator {
val id: Int
fun create(): Fragment
}
interface Listener {
fun onInputShown()
fun onInputHidden()
}
}

View File

@@ -1,94 +0,0 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.util.AttributeSet;
import android.view.WindowInsets;
import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.Guideline;
import androidx.core.graphics.Insets;
import androidx.core.view.WindowInsetsCompat;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ViewUtil;
public class InsetAwareConstraintLayout extends ConstraintLayout {
private WindowInsetsTypeProvider windowInsetsTypeProvider = WindowInsetsTypeProvider.ALL;
private Insets insets;
public InsetAwareConstraintLayout(@NonNull Context context) {
super(context);
}
public InsetAwareConstraintLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public InsetAwareConstraintLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setWindowInsetsTypeProvider(@NonNull WindowInsetsTypeProvider windowInsetsTypeProvider) {
this.windowInsetsTypeProvider = windowInsetsTypeProvider;
requestLayout();
}
@Override
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
WindowInsetsCompat windowInsetsCompat = WindowInsetsCompat.toWindowInsetsCompat(insets);
Insets newInsets = windowInsetsCompat.getInsets(windowInsetsTypeProvider.getInsetsType());
applyInsets(newInsets);
return super.onApplyWindowInsets(insets);
}
public void applyInsets(@NonNull Insets insets) {
Guideline statusBarGuideline = findViewById(R.id.status_bar_guideline);
Guideline navigationBarGuideline = findViewById(R.id.navigation_bar_guideline);
Guideline parentStartGuideline = findViewById(R.id.parent_start_guideline);
Guideline parentEndGuideline = findViewById(R.id.parent_end_guideline);
if (statusBarGuideline != null) {
statusBarGuideline.setGuidelineBegin(insets.top);
}
if (navigationBarGuideline != null) {
navigationBarGuideline.setGuidelineEnd(insets.bottom);
}
if (parentStartGuideline != null) {
if (ViewUtil.isLtr(this)) {
parentStartGuideline.setGuidelineBegin(insets.left);
} else {
parentStartGuideline.setGuidelineBegin(insets.right);
}
}
if (parentEndGuideline != null) {
if (ViewUtil.isLtr(this)) {
parentEndGuideline.setGuidelineEnd(insets.right);
} else {
parentEndGuideline.setGuidelineEnd(insets.left);
}
}
}
public void showSoftkey(@NonNull EditText editText) {
ViewUtil.focusAndShowKeyboard(editText);
}
public interface WindowInsetsTypeProvider {
WindowInsetsTypeProvider ALL = () ->
WindowInsetsCompat.Type.ime() |
WindowInsetsCompat.Type.systemBars() |
WindowInsetsCompat.Type.displayCutout();
@WindowInsetsCompat.Type.InsetsType
int getInsetsType();
}
}

View File

@@ -0,0 +1,142 @@
package org.thoughtcrime.securesms.components
import android.content.Context
import android.os.Build
import android.util.AttributeSet
import android.util.DisplayMetrics
import android.view.Surface
import android.view.WindowInsets
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.Guideline
import androidx.core.graphics.Insets
import androidx.core.view.WindowInsetsCompat
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.ServiceUtil
import org.thoughtcrime.securesms.util.ViewUtil
/**
* A specialized [ConstraintLayout] that sets guidelines based on the window insets provided
* by the system.
*
* In portrait mode these are how the guidelines will be configured:
*
* - [R.id.status_bar_guideline] is set to the bottom of the status bar
* - [R.id.navigation_bar_guideline] is set to the top of the navigation bar
* - [R.id.parent_start_guideline] is set to the start of the parent
* - [R.id.parent_end_guideline] is set to the end of the parent
* - [R.id.keyboard_guideline] will be set to the top of the keyboard and will
* change as the keyboard is shown or hidden
*
* In landscape, the spirit of the guidelines are maintained but their names may not
* correlated exactly to the inset they are providing.
*
* These guidelines will only be updated if present in your layout, you can use
* `<include layout="@layout/system_ui_guidelines" />` to quickly include them.
*/
open class InsetAwareConstraintLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
companion object {
private val keyboardType = WindowInsetsCompat.Type.ime()
private val windowTypes = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()
}
private val statusBarGuideline: Guideline? by lazy { findViewById(R.id.status_bar_guideline) }
private val navigationBarGuideline: Guideline? by lazy { findViewById(R.id.navigation_bar_guideline) }
private val parentStartGuideline: Guideline? by lazy { findViewById(R.id.parent_start_guideline) }
private val parentEndGuideline: Guideline? by lazy { findViewById(R.id.parent_end_guideline) }
private val keyboardGuideline: Guideline? by lazy { findViewById(R.id.keyboard_guideline) }
private val displayMetrics = DisplayMetrics()
private var overridingKeyboard: Boolean = false
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
val windowInsetsCompat = WindowInsetsCompat.toWindowInsetsCompat(insets)
val windowInsets = windowInsetsCompat.getInsets(windowTypes)
val keyboardInset = windowInsetsCompat.getInsets(keyboardType)
applyInsets(windowInsets, keyboardInset)
return super.onApplyWindowInsets(insets)
}
private fun applyInsets(windowInsets: Insets, keyboardInset: Insets) {
val isLtr = ViewUtil.isLtr(this)
statusBarGuideline?.setGuidelineBegin(windowInsets.top)
navigationBarGuideline?.setGuidelineEnd(windowInsets.bottom)
parentStartGuideline?.setGuidelineBegin(if (isLtr) windowInsets.left else windowInsets.right)
parentEndGuideline?.setGuidelineEnd(if (isLtr) windowInsets.right else windowInsets.left)
if (keyboardInset.bottom > 0) {
setKeyboardHeight(keyboardInset.bottom)
keyboardGuideline?.setGuidelineEnd(keyboardInset.bottom)
} else if (!overridingKeyboard) {
keyboardGuideline?.setGuidelineEnd(windowInsets.bottom)
}
}
protected fun overrideKeyboardGuidelineWithPreviousHeight() {
overridingKeyboard = true
keyboardGuideline?.setGuidelineEnd(getKeyboardHeight())
}
protected fun clearKeyboardGuidelineOverride() {
overridingKeyboard = false
}
protected fun resetKeyboardGuideline() {
clearKeyboardGuidelineOverride()
keyboardGuideline?.setGuidelineEnd(navigationBarGuideline.guidelineEnd)
}
private fun getKeyboardHeight(): Int {
val height = if (isLandscape()) {
SignalStore.misc().keyboardLandscapeHeight
} else {
SignalStore.misc().keyboardPortraitHeight
}
return if (height <= 0) {
resources.getDimensionPixelSize(R.dimen.default_custom_keyboard_size)
} else {
height
}
}
private fun setKeyboardHeight(height: Int) {
if (isLandscape()) {
SignalStore.misc().keyboardLandscapeHeight = height
} else {
SignalStore.misc().keyboardPortraitHeight = height
}
}
private fun isLandscape(): Boolean {
val rotation = getDeviceRotation()
return rotation == Surface.ROTATION_90
}
@Suppress("DEPRECATION")
private fun getDeviceRotation(): Int {
if (isInEditMode) {
return Surface.ROTATION_0
}
if (Build.VERSION.SDK_INT >= 30) {
context.display?.getRealMetrics(displayMetrics)
} else {
ServiceUtil.getWindowManager(context).defaultDisplay.getRealMetrics(displayMetrics)
}
return if (displayMetrics.widthPixels > displayMetrics.heightPixels) Surface.ROTATION_90 else Surface.ROTATION_0
}
private val Guideline?.guidelineEnd: Int
get() = if (this == null) 0 else (layoutParams as LayoutParams).guideEnd
}