Android底部控件随软键盘上移
2021-02-03 本文已影响0人
浩_feng
很多时候我们会遇到这个问题,就是点击EditText弹出软键盘的时候会遮挡底部的Button,网上的解决方法一般都是设置WindowSoftInputMode和scrollview嵌套,这个局限性很多,而且有时候达不到我们想要的效果,下面我自定义了一个AutoKeyboardConstraintLayout,可以很小代码改动轻松解决这一问题。
闲话不多说,先看效果图:
图1.gif图2.gif
用法很简单
第一种模式的用法(图1)
<!--用自定义的ConstraintLayout-->
<com.cfxc.autokeyboarddemo.autokeyboard.AutoKeyboardConstraintLayout 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"
<!--底部view上移后最高不能越过哪个view id-->
app:aboveViewId="@+id/tv_register"
<!--移动模式. 不需要滚动的就用这个模式-->
app:autoType="auto_translation"
<!-- 底部需要上移的view id集,中间用,隔开-->
app:bottomViewIds="btn_login">
<content
....
/>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_login"
style="@style/largeButtonStyle"
android:text="login"
android:layout_marginBottom="30dp"
android:background="@drawable/shape_white_radius_10"
app:layout_constraintBottom_toBottomOf="parent" />
第二种模式的用法(图2)
<com.cfxc.autokeyboarddemo.autokeyboard.AutoKeyboardConstraintLayout 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"
<!--底部view上移后最高不能越过哪个view id-->
app:aboveViewId="@+id/scroll_view"
<!--移动模式. 需要滚动的就用这个模式-->
app:autoType="auto_scroll"
<!-- 底部需要上移的view id集,中间用,隔开-->
app:bottomViewIds="btn_continue,btn_cancel">
<!--需要滚动的内容用scroll view-->
<androidx.core.widget.NestedScrollView
android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginBottom="15dp"
app:layout_constraintBottom_toTopOf="@+id/btn_continue"
app:layout_constraintTop_toTopOf="parent">
<content
....
/>
</androidx.core.widget.NestedScrollView>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_cancel"
style="@style/largeButtonStyle"
android:layout_marginBottom="20dp"
android:background="@drawable/shape_white_radius_10"
android:text="Cancel"
app:layout_constraintBottom_toBottomOf="parent" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_continue"
style="@style/largeButtonStyle"
android:layout_marginBottom="10dp"
android:background="@drawable/shape_white_radius_10"
android:text="Continue"
app:layout_constraintBottom_toTopOf="@id/btn_cancel" />
用法简单吧^ _ ^
主要就是封装自定义了AutoKeyboardConstraintLayout,用的时候只需要替换原布局的ConstraintLayout,xml中设置几个参数即可,代码不需要任何改动,不习惯用ConstraintLayout的童鞋,可以参照封装改成LinearLayout
下面是实现
定义下几个属性
<declare-styleable name="AutoKeyboardConstraintLayout">
<attr name="bottomViewIds"/>
<attr name="aboveViewId"/>
<attr name="autoMargin"/>
<attr name="autoType"/>
</declare-styleable>
<attr name="bottomViewIds" format="string" />
<attr name="aboveViewId" format="reference" />
<attr name="autoMargin" format="dimension" />
<attr name="autoType" format="flags">
<flag name="normal" value="0" />
<flag name="auto_translation" value="1" />
<flag name="auto_scroll" value="2" />
</attr>
主要是这个AutoKeyboardConstraintLayout这个类
class AutoKeyboardConstraintLayout : ConstraintLayout {
// not need move up
private val NORMAL = 0
// bottom view move up, the above view move up as need
private val AUTO_TRANSLATION = 1
/** bottom view move up, and change the scrollView height
* Note: When used this type, that need to have a ScrollView or NestedScrollView or RecyclerView in the layout, and set the above view's id is it's id
**/
private val AUTO_SCROLL = 2
// the id of the views that need move up
private var bottomViewIds = arrayListOf<Int>()
//the id of the view above of the bottom view
private var aboveViewId: Int = NO_ID
private var onGlobalLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
private var rootViewVisibleHeight: Int = 0
private var rootViewHeight: Int = 0
// This margin is the height from the keyboard when the bottom view moves up
private var viewMargin: Float = 10f
//the type of that need move up
private var autoType = NORMAL
private val animatorDuration = 100L
private var toolbarHeight: Int = 0
constructor(context: Context) : super(context) {
initViews(null)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
initViews(attrs)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
initViews(attrs)
}
@SuppressLint("CustomViewStyleable")
private fun initViews(attrs: AttributeSet?) {
attrs?.let {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.AutoKeyboardConstraintLayout)
val idStrings = typedArray.getString(R.styleable.AutoKeyboardConstraintLayout_bottomViewIds)
setBottomViewID(idStrings)
aboveViewId = typedArray.getResourceId(R.styleable.AutoKeyboardConstraintLayout_aboveViewId, NO_ID)
viewMargin = typedArray.getDimension(R.styleable.AutoKeyboardConstraintLayout_autoMargin, 10f)
autoType = typedArray.getInt(R.styleable.AutoKeyboardConstraintLayout_autoType, NORMAL)
typedArray.recycle()
}
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (autoType != NORMAL) {
onGlobalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener {
//Gets the size of the current root view displayed on the screen
val r = Rect()
getWindowVisibleDisplayFrame(r)
val visibleHeight = r.height()
rootViewHeight = rootViewHeight.coerceAtLeast(visibleHeight)
if (rootViewVisibleHeight == 0) {
rootViewVisibleHeight = visibleHeight
return@OnGlobalLayoutListener
}
//The display height of the root view doesn't change
if (rootViewVisibleHeight == visibleHeight) {
return@OnGlobalLayoutListener
}
//The display height of the root view has been reduced to over 200 and can be viewed as a soft keyboard display
if (rootViewHeight - visibleHeight > 200) {
val keyboardHeight = rootViewHeight - visibleHeight
changeLayout(keyboardHeight)
rootViewVisibleHeight = visibleHeight
return@OnGlobalLayoutListener
}
//The root view shows a larger height than 200, which can be seen as a soft keyboard hidden
if (visibleHeight - rootViewVisibleHeight > 200) {
//keyboard hide
restoreView()
rootViewVisibleHeight = visibleHeight
return@OnGlobalLayoutListener
}
}
viewTreeObserver?.addOnGlobalLayoutListener(onGlobalLayoutListener)
}
}
private fun changeLayout(keyboardHeight: Int) {
when (autoType) {
AUTO_TRANSLATION, AUTO_SCROLL -> translationBottomView(keyboardHeight)
}
}
private fun translationBottomView(keyboardHeight: Int) {
//this is to support toolbar
val parentView = getRootView(parent)
// parentView is MainActivity root view
parentView?.let {drawerLayout->
// the height of the toolbar is the view in the MainActivity layout
if (drawerLayout is ViewGroup){
val toolbarParent = drawerLayout[0]
if(toolbarParent is ViewGroup && toolbarParent[0] is Toolbar&& toolbarParent[0].isShown){
toolbarHeight = toolbarParent[0].height
}
}
}
var translationHeight = 0f
var isNeedBottomTranslation = false
// the lowest view of the bottom views
var lowestView: View? = null
// the highest view of the bottom views
var highestView: View? = null
var needHeight = 0f
for (item in bottomViewIds) {
val bottomView = getViewById(item)
bottomView?.let {
if (bottomView.visibility == View.VISIBLE) {
lowestView = if (bottomView.bottom > lowestView?.bottom ?: 0) bottomView else lowestView
highestView = if (bottomView.top < highestView?.top ?: rootViewHeight) bottomView else highestView
}
}
}
val aboveView = if (aboveViewId == NO_ID) null else getViewById(aboveViewId)
var aboveViewBottom = 0
aboveView?.apply {
aboveViewBottom = bottom
}
lowestView?.let { lowest ->
if (this.height - lowest.bottom >= keyboardHeight) return
highestView?.let { highest ->
val bottomViewsHeight = lowest.bottom - highest.top
if (aboveViewBottom == 0
|| keyboardHeight + bottomViewsHeight + dip2px(viewMargin * 2) < this.height - aboveViewBottom
|| autoType == AUTO_SCROLL) {
needHeight = keyboardHeight - (this.height - lowest.bottom) + dip2px(viewMargin) - getNavigationBarHeight()
translationHeight = 0f
isNeedBottomTranslation = true
} else {
needHeight = keyboardHeight + aboveViewBottom + bottomViewsHeight + dip2px(viewMargin * 2) - getNavigationBarHeight()
translationHeight = this.height - needHeight
isNeedBottomTranslation = lowest.bottom + translationHeight > this.height - keyboardHeight - dip2px(viewMargin)
}
}
}
if (isNeedBottomTranslation)
for (item in bottomViewIds) {
val bottomView = getViewById(item)
if (bottomView.visibility == View.VISIBLE)
bottomView.animate().setDuration(animatorDuration).translationY(this.height - (lowestView?.bottom
?: 0) - keyboardHeight - dip2px(viewMargin) - translationHeight + getNavigationBarHeight()).start()
}
if (autoType == AUTO_TRANSLATION && aboveViewBottom > 0 && needHeight > this.height) {
this.animate().setDuration(animatorDuration).translationY(translationHeight).start()
}
if (autoType == AUTO_SCROLL) {
aboveView?.let {
if (aboveView is ScrollView || aboveView is NestedScrollView || aboveView is RecyclerView) {
val layoutParams = aboveView.layoutParams
(layoutParams as LayoutParams).bottomMargin = (needHeight + dip2px(viewMargin)).toInt()
aboveView.layoutParams = layoutParams
}
}
}
}
private fun getRootView(viewParent: ViewParent?): ViewParent? {
if (viewParent != null) {
if (viewParent is DrawerLayout) {
return viewParent
} else {
return getRootView(viewParent.parent)
}
} else {
return null
}
}
private fun restoreView() {
when (autoType) {
AUTO_TRANSLATION, AUTO_SCROLL -> restoreTranslation()
}
}
private fun restoreTranslation() {
for (item in bottomViewIds) {
val bottomView = getViewById(item)
bottomView?.let {
this.animate().setDuration(animatorDuration).translationY(0f)
it.animate().setDuration(animatorDuration).translationY(0f)
}
}
if (autoType == AUTO_SCROLL) {
val aboveView = if (aboveViewId == NO_ID) null else getViewById(aboveViewId)
aboveView?.let {
if (aboveView is ScrollView || aboveView is NestedScrollView || aboveView is RecyclerView) {
val layoutParams = aboveView.layoutParams
(layoutParams as LayoutParams).bottomMargin = 0
aboveView.layoutParams = layoutParams
}
}
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
toolbarHeight = 0
if (autoType != NORMAL)
viewTreeObserver?.removeOnGlobalLayoutListener(onGlobalLayoutListener)
}
private fun setBottomViewID(idStrings: String?) {
idStrings?.let {
val idLists = it.split(",")
for (item in idLists) {
addID(item)
}
}
}
private fun addID(idString: String) {
val idStr = idString.trim()
var tag = 0
try {
val res: Class<*> = id::class.java
val field = res.getField(idStr)
tag = field.getInt(null as Any?)
} catch (var5: Exception) {
}
if (tag == 0) {
tag = context.resources.getIdentifier(idStr, "id", context.packageName)
}
if (tag == 0) {
val constraintLayout = this.parent as ConstraintLayout
val value = constraintLayout.getDesignInformation(0, idStr)
if (value != null && value is Int) {
tag = value
}
}
if (tag != 0) {
bottomViewIds.add(tag)
} else {
Log.w("AutoKeyboardLayout", "Could not find id of \"$idStr\"")
}
}
private fun dip2px(dpValue: Float): Float {
val scale = context.resources.displayMetrics.density
return dpValue * scale + 0.5f
}
// This is to support the navigation bar show hide
private fun getNavigationBarHeight(): Int {
return rootViewHeight - this.height - toolbarHeight
}
}
注释是英文的,因为是在做公司项目的时候写的这个控件,公司项目注释要求是英文的,也懒得改中文了( ̄∇ ̄)
附上demo地址:https://github.com/fengpeihao/AutoKeyboardDemo
第一次写文章,写的很简单,主要是为了记录下方便以后用到查看