继续创作,加速成长!这是我参加「日新计划 10 月更文应战」的第7天,点击检查活动概况
前言
在之前软键盘的高度的文章中,咱们定义了一些兼容性相比照较好的东西类。
咱们是把布局单独提取出来,然后放在软键盘上面跟随动画,那么假如是布局中的EditText,该怎么适配软键盘呢?
之前的文章咱们提到过咱们能够经过增加Flag,和替换翻滚布局等办法来适配软键盘,可是这几种办法都不是那么完美,假如想要一些定制的作用,咱们又该怎么适配布局中的 EditText或View 的软键盘高度适配呢?
接下来咱们看看各种不同的布局与不同处理办法。
软键盘的Flag计划
其实我们或多或少的都知道,一个Activity中当软键盘弹起之后,咱们的内容布局要做怎样的改变,是根据咱们增加 windowSoftInputMode 特点来决定的。
而咱们常用的几个 windowSoftInputMode 便是 adjustUnspecified adjustResize adjustPan adjustNoting 四种,其间更高频的其实便是两种 adjustResize 和 adjustPan。
两者的差异:
在一个默许的布局示例中,咱们能够看看他们的差异:
布局是为一般的固定布局,顶部一个TextView,下面一个ImageView,一个EditText。
当清单文件中设置为 android:windowSoftInputMode="adjustPan"
时:
当咱们把清单文件设置为 android:windowSoftInputMode="adjustResize"
时:
可是当咱们把布局调整为翻滚布局ScrollView之后,设置清单文件配置为 android:windowSoftInputMode="adjustPan"
时:
这个作用便是刚刚好,所以咱们通常得出一个定论:
想要EditText适配软键盘,不能翻滚的布局中咱们运用 adjustPan, 而在能翻滚的布局中,咱们运用 adjustResize。
adjustPan 特点为了空出软键盘的方位,自动平移窗口的内容。而 adjustResize 会从头制作布局,假如能翻滚则会翻滚到对应的方位,相当的智能。
所以老师也是教咱们这么运用的,固定搭配!那咱们就只能这么固定用了吗?又有没有其他别的办法呢?
束缚在底部的办法
根据上面的作用图咱们知道 adjustSpan 是平移布局,adjustResize 是从头制作一个新的显现区域。
那么咱们能够根据 adjustResize 这一个特性,咱们把布局固定在底部即可。 LinearLayout RelativeLayout FrameLayout ConstraintLayout 都能够做到这个作用。RelativeLayout FrameLayout ConstraintLayout 三者只需求把布局束缚在底部即可,而 LinearLayout 咱们能够经过权重来结束这个作用。
如下的布局,能够运用多种办法结束
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:orientation="vertical"
android:background="@color/white"
android:layout_height="match_parent">
<View
android:layout_weight="1"
android:layout_width="1dp"
android:layout_height="0dp"/>
<!-- <RelativeLayout-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_alignParentBottom="true"-->
<!-- android:layout_centerHorizontal="true"-->
<!-- android:layout_gravity="center_horizontal|bottom">-->
<EditText
android:id="@+id/editText"
android:layout_gravity="center_horizontal|bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_marginTop="0dp"
android:layout_marginBottom="0dp"
android:hint="输入内容" />
<!-- </RelativeLayout>-->
</LinearLayout>
默许的布局如下:
当弹出软键盘之后:
能够看到 adjustResize 之后咱们的布局为黑色框的部分了,假如是顺序排列的,那么就会呈现上面那种没有改变的作用,而咱们把EditText永远束缚在布局底部,就算Resize了,咱们仍是在底部,就能直接的结束软键盘在键盘上面的作用。
那我的布局不方便束缚在底部,或者我的布局便是顺序排列的,那我就想结束顶部的图片不动,让EditText在软键盘上面,能不能做?
手动偏移的办法
能够的,咱们一起也需求设置 adjustResize 形式,在从头制作的时分,咱们动态的核算当时父容器的高度,父容器的当时方位,指定View的方位等信息,咱们就能核算当时View需求偏移的方位,手动的位移指定的布局。
首先咱们需求拿到需求适配的View和它的父布局,为了适配,咱们需求拿到底部导航栏的高度
public void adjustETWithSoftInput(final View anyView, final ISoftInputChanged listener) {
if (anyView == null || listener == null)
return;
//根View
final View rootView = anyView.getRootView();
if (rootView == null) return;
getNavigationBarHeight(anyView, new NavigationBarCallback() {
@Override
public void onHeight(int height, boolean hasNav) {
SoftInputUtil.this.navigationHeight = height;
//anyView为需求调整高度的View,理论上来说能够是恣意的View
SoftInputUtil.this.anyView = anyView;
SoftInputUtil.this.rootView = rootView;
SoftInputUtil.this.listener = listener;
SoftInputUtil.this.isNavigationBarShow = hasNav;
SoftInputUtil.this.myListener = new myListener();
rootView.addOnLayoutChangeListener(myListener);
}
});
}
getNavigationBarHeight 办法详细的结束在咱们之前的文章中有讲到过。不熟悉的能够看这:Android导航栏的处理。
拿到导航栏高度之后,咱们经过监听父布局的 LayoutChangeListener 监听,因为咱们运用 adjustResize 形式,咱们的布局会从头布局,所以每次软键盘弹起和回收的时分都会回调到这个办法,咱们的逻辑判别则写到对应的监听中。
//RootView的监听回调
class myListener implements View.OnLayoutChangeListener {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
int rootHeight = rootView.getHeight();
Rect rect = new Rect();
//获取当时可见部分,默许可见部分是除了状态栏和导航栏剩余的部分
rootView.getWindowVisibleDisplayFrame(rect);
//仍是要每次回调的时分判别是否有导航栏
if (rootHeight - rect.bottom == navigationHeight) {
//假如可见部分底部与屏幕底部刚好相差导航栏的高度,则认为有导航栏
isNavigationBarShow = true;
} else if (rootHeight - rect.bottom == 0) {
//假如可见部分底部与屏幕底部平齐,阐明没有导航栏
isNavigationBarShow = false;
}
//判别软键盘是否展现并核算软键盘的高度
boolean isSoftInputShow = false;
int softInputHeight = 0;
//假如有导航栏,则要去除导航栏的高度
int mutableHeight = isNavigationBarShow ? navigationHeight : 0;
if (rootHeight - mutableHeight > rect.bottom) {
//除去导航栏高度后,可见区域仍然小于屏幕高度,则阐明键盘弹起了
isSoftInputShow = true;
//键盘高度
softInputHeight = rootHeight - mutableHeight - rect.bottom;
if (SoftInputUtil.this.softInputHeight != softInputHeight) {
softInputHeightChanged = true;
SoftInputUtil.this.softInputHeight = softInputHeight;
} else {
softInputHeightChanged = false;
}
}
//获取方针View的方位坐标
int[] location = new int[2];
anyView.getLocationOnScreen(location);
if (isSoftInputShowing != isSoftInputShow || (isSoftInputShow && softInputHeightChanged)) {
if (listener != null) {
//第三个参数为该View需求调整的偏移量
//此处的坐标都是相对屏幕左上角(0,0)为基准的
listener.onChanged(isSoftInputShow, softInputHeight, location[1]- rect.bottom + anyView.getHeight() );
}
isSoftInputShowing = isSoftInputShow;
}
}
}
获取到父布局的高度和可见矩阵,咱们就能够核算是否有导航栏和软键盘的高度。获取到当时Viewd的坐标之后,拿到Y坐标加上当时View的高度,减去当时可见矩阵的高度,便是咱们需求的偏移量。
运用起来也很简略。
override fun init() {
val etInput = findViewById<EditText>(R.id.et_input)
etInput.bringToFront()
softInputUtil.adjustETWithSoftInput(etInput) { isSoftInputShow, softInputHeight, viewOffset ->
if (isSoftInputShow) {
etInput.translationY = etInput.translationY - viewOffset
} else {
etInput.translationY = 0f;
}
}
}
override fun onDestroy() {
super.onDestroy()
softInputUtil.releaseETWithSoftInput()
}
咱们手动的设置偏移 translationY 即可结束
现在咱们设置 windowSoftInputMode 为 adjustResize 然后咱们能够比照一下 adjustPan 的作用:
adjustPan作用:
adjustResize + 自定义偏移作用:
能够看到不同点便是顶部的图片和标题栏不会往上翻滚,缺陷是运用起来相对费事一点,需求自己核算和位移。
列表中自动定位逻辑
在之前的演示中,咱们设置默许的 windowSoftInputMode 或者指定为 adjustResize 的时分,在咱们翻滚布局中是能够很好的支撑的。那么在列表中运用软键盘会怎么样?
当然了咱们一般不会在列表的Item中直接运用EditText,有复用问题,就算咱们解决了性能也没有那么好,咱们通常的做法是像微信朋友圈一样,运用一个按钮触发一个输入弹窗,在弹窗中运用软键盘。
那么这种输入框布局下面是软键盘的做法,其实有几种做法,咱们之前的文章讲软键盘的高度的一文中咱们介绍了一种布局附着在软键盘的做法,而另一种做法便是运用翻滚布局来结束了。
因为翻滚布局天然就能很好的支撑软键盘和EditText的联动,所以咱们直接在Dialog的布局中运用翻滚布局即可结束指定的作用。
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<View
android:layout_width="1dp"
android:layout_height="0dp"
android:layout_weight="1" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#d2d2d2">
</View>
<LinearLayout
android:id="@+id/dialog_layout_comment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="承认" />
</LinearLayout>
</LinearLayout>
</ScrollView>
Dialog中咱们就能运用布局了
@SuppressLint("ClickableViewAccessibility")
public class ReviewDialog extends Dialog {
public ReviewDialog(Context context) {
this(context, R.style.quick_option_dialog);
}
//两个参数的构造办法结束详细的逻辑
public ReviewDialog(Context context, int themeResId) {
super(context, themeResId);
View view = LayoutInflater.from(context).inflate(R.layout.dialog_review, null);
requestWindowFeature(Window.FEATURE_NO_TITLE); //设置没有标题
//设置接触一下整个View.让其可取消。接触(不是点击)对话框恣意当地取消对话框
view.setOnTouchListener((View view1, MotionEvent motionEvent) -> {
dismiss();
return true; //消费掉此次接触事件
});
//View设置结束,赋值给dialog对话框
super.setContentView(view);
}
/**
* 对话框被创立调用的办法
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//设置对话框显现的方位.在底部显现
getWindow().setGravity(Gravity.BOTTOM);
//设置对话框的宽度,填充屏幕
WindowManager wm = getWindow().getWindowManager();
Display display = wm.getDefaultDisplay();
int width = display.getWidth();
//获取对话框的特点
WindowManager.LayoutParams params = getWindow().getAttributes();
params.width = width; //和屏幕一样宽
getWindow().setAttributes(params);
}
}
咱们定义RV和Item的布局,然后填充一些假数据,看看作用试试:
override fun init() {
val datas = mutableListOf<String>()
for (i in 1..20) {
datas.add(i.toString())
}
findViewById<RecyclerView>(R.id.rv_list)
.vertical()
.bindData(datas, R.layout.item_soft_input_demo) { holder, t, position ->
holder.getView<TextView>(R.id.tv_review).click {
showReviewDialog(it,position)
}
}
}
private fun showReviewDialog(view: View, position: Int) {
ReviewDialog(mActivity)
.show()
}
作用:
咱们点击谈论,运用翻滚布局的弹窗来控制软键盘与EditText,弹出弹窗之后软键盘和输入框完美的契合。当然假如你想Dialog弹出的时分自动呈现软键盘,那么直接在Dialog的onCreate中给EidtText开启软键盘即可。
虽然能结束作用了,可是现在还有问题!什么问题? 我点击谈论按钮,弹框和软键盘的高度加起来把我的谈论
按钮挡住了,微信朋友圈的做法是会自动翻滚列表,让谈论按钮在输入框的上面。
咱们结合之前手动偏移的办法略微修改一下,让RV翻滚一下即可。
咱们让软键盘自动弹起,并延时350毫秒获取软键盘弹出之后的高度,为什么是350毫秒,因为我的Dialog动画theme是300毫秒,咱们让软键盘展现出来再处理就相对简略一点。不然还需求做布局改变的监听相对费事一点。
private fun showReviewDialog(view: View, position: Int) {
val rvReviewY = getY(view)
val rvReviewHeight = view.height
val dialog = ReviewDialog(mActivity)
dialog.show()
view.postDelayed({
//等候弹窗弹起自后再获取到Y的高度,便是加上了软键盘之后的高度了
val etReviewY = getY(dialog.findViewById<LinearLayout>(R.id.dialog_layout_comment))
val offsetY = rvReviewY - etReviewY + rvReviewHeight
rvList.smoothScrollBy(0, offsetY)
}, 350)
}
private fun getY(view: View): Int {
val rect = IntArray(2)
view.getLocationOnScreen(rect)
return rect[1]
}
这样的作用就和微信朋友圈的作用比较相似了:
总结
许多计划都是网上现有的计划,这里我也是做了一些归纳与收拾。
本文也仅仅记录了应用层的设置,假如对源码感爱好能够去搜索检查 ViewRootImpl 类,在其间的 public void handleMessage(Message msg)
办法中有对应的Flag处理,其间一些要点的办法 dispatchApplyInsets
performTraversals
dispatchOnPreDraw
scrollToRectOrFocus
等,假如我们有爱好能够自行查阅。
常用的几种作用大致便是这些了,一般的作用咱们运用 固定布局+ adjustPan 或 翻滚布局 + adjustResize 即可结束默许的作用了。
假如想要一些特别的作用,咱们就能设置 adjustResize 之后自行结束一些一些位移和定制的作用,核算位移的一些公式都是相对固定和简略的一些用法。
而在列表中咱们能够经过View依附软键盘的办法,也能够运用 SrollView+EditText 的办法来结束作用。都能够满足需求作用是一致的。
Ok,本文的全部代码已经开源,想检查作用能够点击源码运转测验哦。
本文的环境与设备都是根据API30结束,因为阻隔在家了没有那么多的设备参加测验,假如有兼容性问题欢迎我们反馈呀。
惯例,如有错漏还请指出,假如有更好的计划也欢迎留言区沟通。
假如感觉本文对你有一点点的启示,还望你能点赞
支撑一下,你的支撑是我最大的动力。
Ok,这一期就此结束。