Fix issue where soft keyboard overlaps extra keys or terminal in some cases

Check TermuxActivityRootView javadocs for details.
This commit is contained in:
agnostic-apollo
2021-06-10 03:06:10 +05:00
parent 7ef9255437
commit e7dd0eeebe
6 changed files with 449 additions and 68 deletions

View File

@@ -30,6 +30,7 @@ import android.widget.ListView;
import android.widget.Toast; import android.widget.Toast;
import com.termux.R; import com.termux.R;
import com.termux.app.terminal.TermuxActivityRootView;
import com.termux.shared.termux.TermuxConstants; import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY; import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY;
import com.termux.app.activities.HelpActivity; import com.termux.app.activities.HelpActivity;
@@ -68,7 +69,6 @@ import androidx.viewpager.widget.ViewPager;
*/ */
public final class TermuxActivity extends Activity implements ServiceConnection { public final class TermuxActivity extends Activity implements ServiceConnection {
/** /**
* The connection to the {@link TermuxService}. Requested in {@link #onCreate(Bundle)} with a call to * The connection to the {@link TermuxService}. Requested in {@link #onCreate(Bundle)} with a call to
* {@link #bindService(Intent, ServiceConnection, int)}, and obtained and stored in * {@link #bindService(Intent, ServiceConnection, int)}, and obtained and stored in
@@ -77,7 +77,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
TermuxService mTermuxService; TermuxService mTermuxService;
/** /**
* The main view of the activity showing the terminal. Initialized in onCreate(). * The {@link TerminalView} shown in {@link TermuxActivity} that displays the terminal.
*/ */
TerminalView mTerminalView; TerminalView mTerminalView;
@@ -103,6 +103,16 @@ public final class TermuxActivity extends Activity implements ServiceConnection
*/ */
private TermuxAppSharedProperties mProperties; private TermuxAppSharedProperties mProperties;
/**
* The root view of the {@link TermuxActivity}.
*/
TermuxActivityRootView mTermuxActivityRootView;
/**
* The space at the bottom of {@link @mTermuxActivityRootView} of the {@link TermuxActivity}.
*/
View mTermuxActivityBottomSpaceView;
/** /**
* The terminal extra keys view. * The terminal extra keys view.
*/ */
@@ -189,6 +199,10 @@ public final class TermuxActivity extends Activity implements ServiceConnection
return; return;
} }
mTermuxActivityRootView = findViewById(R.id.activity_termux_root_view);
mTermuxActivityRootView.setActivity(this);
mTermuxActivityBottomSpaceView = findViewById(R.id.activity_termux_bottom_space_view);
View content = findViewById(android.R.id.content); View content = findViewById(android.R.id.content);
content.setOnApplyWindowInsetsListener((v, insets) -> { content.setOnApplyWindowInsetsListener((v, insets) -> {
mNavBarHeight = insets.getSystemWindowInsetBottom(); mNavBarHeight = insets.getSystemWindowInsetBottom();
@@ -241,6 +255,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
if (mTermuxTerminalViewClient != null) if (mTermuxTerminalViewClient != null)
mTermuxTerminalViewClient.onStart(); mTermuxTerminalViewClient.onStart();
addTermuxActivityRootViewGlobalLayoutListener();
registerTermuxActivityBroadcastReceiver(); registerTermuxActivityBroadcastReceiver();
} }
@@ -277,6 +293,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
if (mTermuxTerminalViewClient != null) if (mTermuxTerminalViewClient != null)
mTermuxTerminalViewClient.onStop(); mTermuxTerminalViewClient.onStop();
removeTermuxActivityRootViewGlobalLayoutListener();
unregisterTermuxActivityBroadcastReceiever(); unregisterTermuxActivityBroadcastReceiever();
getDrawer().closeDrawers(); getDrawer().closeDrawers();
} }
@@ -390,6 +408,17 @@ public final class TermuxActivity extends Activity implements ServiceConnection
public void addTermuxActivityRootViewGlobalLayoutListener() {
getTermuxActivityRootView().getViewTreeObserver().addOnGlobalLayoutListener(getTermuxActivityRootView());
}
public void removeTermuxActivityRootViewGlobalLayoutListener() {
if (getTermuxActivityRootView() != null)
getTermuxActivityRootView().getViewTreeObserver().removeOnGlobalLayoutListener(getTermuxActivityRootView());
}
private void setTermuxTerminalViewAndClients() { private void setTermuxTerminalViewAndClients() {
// Set termux terminal view and session clients // Set termux terminal view and session clients
mTermuxTerminalSessionClient = new TermuxTerminalSessionClient(this); mTermuxTerminalSessionClient = new TermuxTerminalSessionClient(this);
@@ -438,8 +467,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
if (terminalToolbarViewPager == null) return; if (terminalToolbarViewPager == null) return;
ViewGroup.LayoutParams layoutParams = terminalToolbarViewPager.getLayoutParams(); ViewGroup.LayoutParams layoutParams = terminalToolbarViewPager.getLayoutParams();
layoutParams.height = (int) Math.round(mTerminalToolbarDefaultHeight * layoutParams.height = (int) Math.round(mTerminalToolbarDefaultHeight *
(mProperties.getExtraKeysInfo() == null ? 0 : mProperties.getExtraKeysInfo().getMatrix().length) * (mProperties.getExtraKeysInfo() == null ? 0 : mProperties.getExtraKeysInfo().getMatrix().length) *
mProperties.getTerminalToolbarHeightScaleFactor()); mProperties.getTerminalToolbarHeightScaleFactor());
terminalToolbarViewPager.setLayoutParams(layoutParams); terminalToolbarViewPager.setLayoutParams(layoutParams);
} }
@@ -679,6 +708,14 @@ public final class TermuxActivity extends Activity implements ServiceConnection
return mNavBarHeight; return mNavBarHeight;
} }
public TermuxActivityRootView getTermuxActivityRootView() {
return mTermuxActivityRootView;
}
public View getTermuxActivityBottomSpaceView() {
return mTermuxActivityBottomSpaceView;
}
public ExtraKeysView getExtraKeysView() { public ExtraKeysView getExtraKeysView() {
return mExtraKeysView; return mExtraKeysView;
} }

View File

@@ -0,0 +1,227 @@
package com.termux.app.terminal;
import android.content.Context;
import android.graphics.Rect;
import android.inputmethodservice.InputMethodService;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.inputmethod.EditorInfo;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import com.termux.app.TermuxActivity;
import com.termux.shared.logger.Logger;
import com.termux.shared.view.ViewUtils;
/**
* The {@link TermuxActivity} relies on {@link android.view.WindowManager.LayoutParams#SOFT_INPUT_ADJUST_RESIZE)}
* set by {@link TermuxTerminalViewClient#setSoftKeyboardState(boolean, boolean)} to automatically
* resize the view and push the terminal up when soft keyboard is opened. However, this does not
* always work properly. When `enforce-char-based-input=true` is set in `termux.properties`
* and {@link com.termux.view.TerminalView#onCreateInputConnection(EditorInfo)} sets the inputType
* to `InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS`
* instead of the default `InputType.TYPE_NULL` for termux, some keyboards may still show suggestions.
* Gboard does too, but only when text is copied and clipboard suggestions **and** number keys row
* toggles are enabled in its settings. When number keys row toggle is not enabled, Gboard will still
* show the row but will switch it with suggestions if needed. If its enabled, then number keys row
* is always shown and suggestions are shown in an additional row on top of it. This additional row is likely
* part of the candidates view returned by the keyboard app in {@link InputMethodService#onCreateCandidatesView()}.
*
* With the above configuration, the additional clipboard suggestions row partially covers the
* extra keys/terminal. Reopening the keyboard/activity does not fix the issue. This is either a bug
* in the Android OS where it does not consider the candidate's view height in its calculation to push
* up the view or because Gboard does not include the candidate's view height in the height reported
* to android that should be used, hence causing an overlap.
*
* Gboard logs the following entry to `logcat` when its opened with or without the suggestions bar showing:
* I/KeyboardViewUtil: KeyboardViewUtil.calculateMaxKeyboardBodyHeight():62 leave 500 height for app when screen height:2392, header height:176 and isFullscreenMode:false, so the max keyboard body height is:1716
* where `keyboard_height = screen_height - height_for_app - header_height` (62 is a hardcoded value in Gboard source code and may be a version number)
* So this may in fact be due to Gboard but https://stackoverflow.com/questions/57567272 suggests
* otherwise. Another similar report https://stackoverflow.com/questions/66761661.
* Also check https://github.com/termux/termux-app/issues/1539.
*
* This overlap may happen even without `enforce-char-based-input=true` for keyboards with extended layouts
* like number row, etc.
*
* To fix these issues, `activity_termux.xml` has the constant 1sp transparent
* `activity_termux_bottom_space_view` View at the bottom. This will appear as a line matching the
* activity theme. When {@link TermuxActivity} {@link ViewTreeObserver.OnGlobalLayoutListener} is
* called when any of the sub view layouts change, like keyboard opening/closing keyboard,
* extra keys/input view switched, etc, we check if the bottom space view is visible or not.
* If its not, then we add a margin to the bottom of the root view, so that the keyboard does not
* overlap the extra keys/terminal, since the margin will push up the view. By default the margin
* added is equal to the height of the hidden part of extra keys/terminal. For Gboard's case, the
* hidden part equals the `header_height`. The updates to margins may cause a jitter in some cases
* when the view is redrawn if the margin is incorrect, but logic has been implemented to avoid that.
*/
public class TermuxActivityRootView extends LinearLayout implements ViewTreeObserver.OnGlobalLayoutListener {
public TermuxActivity mActivity;
public Integer marginBottom;
public Integer lastMarginBottom;
/** Log root view events. */
private boolean ROOT_VIEW_LOGGING_ENABLED = false;
private static final String LOG_TAG = "TermuxActivityRootView";
public TermuxActivityRootView(Context context) {
super(context);
}
public TermuxActivityRootView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public TermuxActivityRootView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setActivity(TermuxActivity activity) {
mActivity = activity;
}
/**
* Sets whether root view logging is enabled or not.
*
* @param value The boolean value that defines the state.
*/
public void setIsRootViewLoggingEnabled(boolean value) {
ROOT_VIEW_LOGGING_ENABLED = value;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (marginBottom != null) {
if (ROOT_VIEW_LOGGING_ENABLED)
Logger.logVerbose(LOG_TAG, "onMeasure: Setting bottom margin to " + marginBottom);
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) getLayoutParams();
params.setMargins(0, 0, 0, marginBottom);
setLayoutParams(params);
marginBottom = null;
requestLayout();
}
}
@Override
public void onGlobalLayout() {
if (mActivity == null || !mActivity.isVisible()) return;
View bottomSpaceView = mActivity.getTermuxActivityBottomSpaceView();
if (bottomSpaceView == null) return;
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) getLayoutParams();
// Get the position Rects of the bottom space view and the main window holding it
Rect[] windowAndViewRects = ViewUtils.getWindowAndViewRects(bottomSpaceView);
if (windowAndViewRects == null)
return;
Rect windowAvailableRect = windowAndViewRects[0];
Rect bottomSpaceViewRect = windowAndViewRects[1];
// If the bottomSpaceViewRect is inside the windowAvailableRect, then it must be completely visible
boolean isVisible = windowAvailableRect.contains(bottomSpaceViewRect);
boolean isVisibleBecauseMargin = (windowAvailableRect.bottom == bottomSpaceViewRect.bottom) && params.bottomMargin > 0;
boolean isVisibleBecauseExtraMargin = (bottomSpaceViewRect.bottom - windowAvailableRect.bottom) < 0;
if (ROOT_VIEW_LOGGING_ENABLED)
Logger.logVerbose(LOG_TAG, "onGlobalLayout: windowAvailableRect " + windowAvailableRect.bottom + ", bottomSpaceViewRect " + bottomSpaceViewRect.bottom + ", diff " + (bottomSpaceViewRect.bottom - windowAvailableRect.bottom) + ", bottom " + params.bottomMargin + ", isVisible " + isVisible + ", isVisibleBecauseMargin " + isVisibleBecauseMargin + ", isVisibleBecauseExtraMargin " + isVisibleBecauseExtraMargin);
// If the bottomSpaceViewRect is visible, then remove the margin if needed
if (isVisible) {
// If visible because of margin, i.e the bottom of bottomSpaceViewRect equals that of windowAvailableRect
// and a margin has been added
// Necessary so that we don't get stuck in an infinite loop since setting margin
// will call OnGlobalLayoutListener again and next time bottom space view
// will be visible and margin will be set to 0, which again will call
// OnGlobalLayoutListener...
// Calling addTermuxActivityRootViewGlobalLayoutListener with a delay fails to
// set appropriate margins when views are changed quickly since some changes
// may be missed.
if (isVisibleBecauseMargin) {
if (ROOT_VIEW_LOGGING_ENABLED)
Logger.logVerbose(LOG_TAG, "onGlobalLayout: Visible due to margin");
// Once the view has been redrawn with new margin, we set margin back to 0 so that
// when next time onMeasure() is called, margin 0 is used. This is necessary for
// cases when view has been redrawn with new margin because bottom space view was
// hidden by keyboard and then view was redrawn again due to layout change (like
// keyboard symbol view is switched to), android will add margin below its new position
// if its greater than 0, which was already above the keyboard creating x2x margin.
marginBottom = 0;
return;
}
boolean setMargin = params.bottomMargin != 0;
// If visible because of extra margin, i.e the bottom of bottomSpaceViewRect is above that of windowAvailableRect
// onGlobalLayout: windowAvailableRect 1408, bottomSpaceViewRect 1232, diff -176, bottom 0, isVisible true, isVisibleBecauseMargin false, isVisibleBecauseExtraMargin false
// onGlobalLayout: Bottom margin already equals 0
if (isVisibleBecauseExtraMargin) {
if (ROOT_VIEW_LOGGING_ENABLED)
Logger.logVerbose(LOG_TAG, "onGlobalLayout: Resetting margin since visible due to extra margin");
setMargin = true;
// lastMarginBottom must be invalid. May also happen when keyboards are changed.
lastMarginBottom = null;
}
if (setMargin) {
if (ROOT_VIEW_LOGGING_ENABLED)
Logger.logVerbose(LOG_TAG, "onGlobalLayout: Setting bottom margin to 0");
params.setMargins(0, 0, 0, 0);
setLayoutParams(params);
} else {
if (ROOT_VIEW_LOGGING_ENABLED)
Logger.logVerbose(LOG_TAG, "onGlobalLayout: Bottom margin already equals 0");
// This is done so that when next time onMeasure() is called, lastMarginBottom is used.
// This is done since we **expect** the keyboard to have same dimensions next time layout
// changes, so best set margin while view is drawn the first time, otherwise it will
// cause a jitter when OnGlobalLayoutListener is called with margin 0 and it sets the
// likely same lastMarginBottom again and requesting a redraw. Hopefully, this logic
// works fine for all cases.
marginBottom = lastMarginBottom;
}
}
// ELse find the part of the extra keys/terminal that is hidden and add a margin accordingly
else {
int pxHidden = bottomSpaceViewRect.bottom - windowAvailableRect.bottom;
if (ROOT_VIEW_LOGGING_ENABLED)
Logger.logVerbose(LOG_TAG, "onGlobalLayout: pxHidden " + pxHidden + ", bottom " + params.bottomMargin);
boolean setMargin = params.bottomMargin != pxHidden;
// If invisible despite margin, i.e a margin was added, but the bottom of bottomSpaceViewRect
// is still below that of windowAvailableRect, this will trigger OnGlobalLayoutListener
// again, so that margins are set properly. May happen when toolbar/extra keys is disabled
// and enabled from left drawer, just like case for isVisibleBecauseExtraMargin.
// onMeasure: Setting bottom margin to 176
// onGlobalLayout: windowAvailableRect 1232, bottomSpaceViewRect 1408, diff 176, bottom 176, isVisible false, isVisibleBecauseMargin false, isVisibleBecauseExtraMargin false
// onGlobalLayout: Bottom margin already equals 176
if ((bottomSpaceViewRect.bottom - windowAvailableRect.bottom) > 0) {
if (ROOT_VIEW_LOGGING_ENABLED)
Logger.logVerbose(LOG_TAG, "onGlobalLayout: Force setting margin since not visible despite margin");
setMargin = true;
}
if (setMargin) {
if (ROOT_VIEW_LOGGING_ENABLED)
Logger.logVerbose(LOG_TAG, "onGlobalLayout: Setting bottom margin to " + pxHidden);
params.setMargins(0, 0, 0, pxHidden);
setLayoutParams(params);
lastMarginBottom = pxHidden;
} else {
if (ROOT_VIEW_LOGGING_ENABLED)
Logger.logVerbose(LOG_TAG, "onGlobalLayout: Bottom margin already equals " + pxHidden);
}
}
}
}

View File

@@ -79,10 +79,13 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
* Should be called when mActivity.onStart() is called * Should be called when mActivity.onStart() is called
*/ */
public void onStart() { public void onStart() {
// Set {@link TerminalView#TERMINAL_VIEW_KEY_LOGGING_ENABLED} value // Set {@link TerminalView#TERMINAL_VIEW_KEY_LOGGING_ENABLED} value
// Also required if user changed the preference from {@link TermuxSettings} activity and returns // Also required if user changed the preference from {@link TermuxSettings} activity and returns
mActivity.getTerminalView().setIsTerminalViewKeyLoggingEnabled(mActivity.getPreferences().isTerminalViewKeyLoggingEnabled()); boolean isTerminalViewKeyLoggingEnabled = mActivity.getPreferences().isTerminalViewKeyLoggingEnabled();
mActivity.getTerminalView().setIsTerminalViewKeyLoggingEnabled(isTerminalViewKeyLoggingEnabled);
// Piggyback on the terminal view key logging toggle for now, should add a separate toggle in future
mActivity.getTermuxActivityRootView().setIsRootViewLoggingEnabled(isTerminalViewKeyLoggingEnabled);
} }
/** /**
@@ -459,7 +462,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
mShowSoftKeyboardWithDelayOnce = true; mShowSoftKeyboardWithDelayOnce = true;
} else { } else {
// Set flag to automatically push up TerminalView when keyboard is opened instead of showing over it // Set flag to automatically push up TerminalView when keyboard is opened instead of showing over it
KeyboardUtils.setResizeTerminalViewForSoftKeyboardFlags(mActivity); KeyboardUtils.setSoftInputModeAdjustResize(mActivity);
// Clear any previous flags to disable soft keyboard in case setting updated // Clear any previous flags to disable soft keyboard in case setting updated
KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity); KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity);

View File

@@ -1,80 +1,96 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <com.termux.app.terminal.TermuxActivityRootView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_termux_root_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
android:fitsSystemWindows="true"> android:fitsSystemWindows="true">
<androidx.drawerlayout.widget.DrawerLayout <RelativeLayout
android:id="@+id/drawer_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_alignParentTop="true" android:layout_height="0dp"
android:layout_above="@+id/terminal_toolbar_view_pager" android:layout_weight="1"
android:layout_height="match_parent"> android:orientation="vertical">
<com.termux.view.TerminalView <androidx.drawerlayout.widget.DrawerLayout
android:id="@+id/terminal_view" android:id="@+id/drawer_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_alignParentTop="true"
android:layout_marginRight="3dp" android:layout_above="@+id/terminal_toolbar_view_pager"
android:layout_marginLeft="3dp" android:layout_height="match_parent">
android:focusableInTouchMode="true"
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
android:scrollbars="vertical"
android:importantForAutofill="no"
android:autofillHints="password" />
<LinearLayout <com.termux.view.TerminalView
android:id="@+id/left_drawer" android:id="@+id/terminal_view"
android:layout_width="240dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="@android:color/white"
android:choiceMode="singleChoice"
android:divider="@android:color/transparent"
android:dividerHeight="0dp"
android:descendantFocusability="blocksDescendants"
android:orientation="vertical">
<ListView
android:id="@+id/terminal_sessions_list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="match_parent"
android:layout_gravity="top" android:layout_marginRight="3dp"
android:layout_weight="1" android:layout_marginLeft="3dp"
android:choiceMode="singleChoice" android:focusableInTouchMode="true"
android:longClickable="true" /> android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
android:scrollbars="vertical"
android:importantForAutofill="no"
android:autofillHints="password" />
<LinearLayout <LinearLayout
style="?android:attr/buttonBarStyle" android:id="@+id/left_drawer"
android:layout_width="match_parent" android:layout_width="240dp"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:orientation="horizontal"> android:layout_gravity="start"
android:background="@android:color/white"
android:choiceMode="singleChoice"
android:divider="@android:color/transparent"
android:dividerHeight="0dp"
android:descendantFocusability="blocksDescendants"
android:orientation="vertical">
<Button <ListView
android:id="@+id/toggle_keyboard_button" android:id="@+id/terminal_sessions_list"
style="?android:attr/buttonBarButtonStyle" android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_gravity="top"
android:layout_weight="1"
android:choiceMode="singleChoice"
android:longClickable="true" />
<LinearLayout
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:orientation="horizontal">
android:text="@string/action_toggle_soft_keyboard" />
<Button <Button
android:id="@+id/new_session_button" android:id="@+id/toggle_keyboard_button"
style="?android:attr/buttonBarButtonStyle" style="?android:attr/buttonBarButtonStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:text="@string/action_new_session" /> android:text="@string/action_toggle_soft_keyboard" />
<Button
android:id="@+id/new_session_button"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/action_new_session" />
</LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout>
</androidx.drawerlayout.widget.DrawerLayout> </androidx.drawerlayout.widget.DrawerLayout>
<androidx.viewpager.widget.ViewPager <androidx.viewpager.widget.ViewPager
android:id="@+id/terminal_toolbar_view_pager" android:id="@+id/terminal_toolbar_view_pager"
android:visibility="gone" android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="37.5dp"
android:background="@android:drawable/screen_background_dark_transparent"
android:layout_alignParentBottom="true" />
</RelativeLayout>
<View
android:id="@+id/activity_termux_bottom_space_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="37.5dp" android:layout_height="1dp"
android:background="@android:drawable/screen_background_dark_transparent" android:background="@android:color/transparent" />
android:layout_alignParentBottom="true" />
</RelativeLayout> </com.termux.app.terminal.TermuxActivityRootView>

View File

@@ -102,7 +102,7 @@ public class KeyboardUtils {
activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
} }
public static void setResizeTerminalViewForSoftKeyboardFlags(final Activity activity) { public static void setSoftInputModeAdjustResize(final Activity activity) {
// TODO: The flag is deprecated for API 30 and WindowInset API should be used // TODO: The flag is deprecated for API 30 and WindowInset API should be used
// https://developer.android.com/reference/android/view/WindowManager.LayoutParams#SOFT_INPUT_ADJUST_RESIZE // https://developer.android.com/reference/android/view/WindowManager.LayoutParams#SOFT_INPUT_ADJUST_RESIZE
// https://medium.com/androiddevelopers/animating-your-keyboard-fb776a8fb66d // https://medium.com/androiddevelopers/animating-your-keyboard-fb776a8fb66d

View File

@@ -0,0 +1,98 @@
package com.termux.shared.view;
import android.app.Activity;
import android.content.Context;
import android.content.ContextWrapper;
import android.graphics.Rect;
import android.util.TypedValue;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
public class ViewUtils {
/**
* Check if a {@link View} is fully visible and not hidden or partially covered by another view.
*
* https://stackoverflow.com/a/51078418/14686958
*
* @param view The {@link View} to check.
* @return Returns {@code true} if view is fully visible.
*/
public static boolean isViewFullyVisible(View view) {
Rect[] windowAndViewRects = getWindowAndViewRects(view);
if (windowAndViewRects == null)
return false;
return windowAndViewRects[0].contains(windowAndViewRects[1]);
}
/**
* Get the {@link Rect} of a {@link View} and the {@link Rect} of the window inside which it
* exists.
*
* https://stackoverflow.com/a/51078418/14686958
*
* @param view The {@link View} inside the window whose {@link Rect} to get.
* @return Returns {@link Rect[]} if view is visible where Rect[0] will contain window
* {@link Rect} and Rect[1] will contain view {@link Rect}. This will be {@code null}
* if view is not visible.
*/
@Nullable
public static Rect[] getWindowAndViewRects(View view) {
if (view == null || !view.isShown())
return null;
// windowRect - will hold available area where content remain visible to users
// Takes into account screen decorations (e.g. statusbar)
Rect windowRect = new Rect();
view.getWindowVisibleDisplayFrame(windowRect);
// If there is actionbar, get his height
int actionBarHeight = 0;
Context context = view.getContext();
if (context instanceof AppCompatActivity) {
androidx.appcompat.app.ActionBar actionBar = ((AppCompatActivity) context).getSupportActionBar();
if (actionBar != null) actionBarHeight = actionBar.getHeight();
} else if (context instanceof Activity) {
android.app.ActionBar actionBar = ((Activity) context).getActionBar();
if (actionBar != null) actionBarHeight = actionBar.getHeight();
}
// windowAvailableRect - takes into account actionbar and statusbar height
Rect windowAvailableRect;
windowAvailableRect = new Rect(windowRect.left, windowRect.top + actionBarHeight, windowRect.right, windowRect.bottom);
// viewRect - holds position of the view in window
// (methods as getGlobalVisibleRect, getHitRect, getDrawingRect can return different result,
// when partialy visible)
Rect viewRect;
final int[] viewsLocationInWindow = new int[2];
view.getLocationInWindow(viewsLocationInWindow);
int viewLeft = viewsLocationInWindow[0];
int viewTop = viewsLocationInWindow[1];
int viewRight = viewLeft + view.getWidth();
int viewBottom = viewTop + view.getHeight();
viewRect = new Rect(viewLeft, viewTop, viewRight, viewBottom);
return new Rect[]{windowAvailableRect, viewRect};
}
/** Get the {@link Activity} associated with the {@link Context} if available. */
@Nullable
public static Activity getActivity(Context context) {
while (context instanceof ContextWrapper) {
if (context instanceof Activity) {
return (Activity)context;
}
context = ((ContextWrapper)context).getBaseContext();
}
return null;
}
/** Convert value in device independent pixels (dp) to pixels (px) units. */
public static int dpToPx(Context context, int dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics());
}
}