很多時(shí)候我們會(huì)遇到這個(gè)問(wèn)題翎迁,就是點(diǎn)擊EditText彈出軟鍵盤(pán)的時(shí)候會(huì)遮擋底部的Button,網(wǎng)上的解決方法一般都是設(shè)置WindowSoftInputMode和scrollview嵌套净薛,這個(gè)局限性很多汪榔,而且有時(shí)候達(dá)不到我們想要的效果,下面我自定義了一個(gè)AutoKeyboardConstraintLayout肃拜,可以很小代碼改動(dòng)輕松解決這一問(wèn)題痴腌。
閑話不多說(shuō),先看效果圖:
用法很簡(jiǎn)單
第一種模式的用法(圖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上移后最高不能越過(guò)哪個(gè)view id-->
app:aboveViewId="@+id/tv_register"
<!--移動(dòng)模式. 不需要滾動(dòng)的就用這個(gè)模式-->
app:autoType="auto_translation"
<!-- 底部需要上移的view id集燃领,中間用,隔開(kāi)-->
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上移后最高不能越過(guò)哪個(gè)view id-->
app:aboveViewId="@+id/scroll_view"
<!--移動(dòng)模式. 需要滾動(dòng)的就用這個(gè)模式-->
app:autoType="auto_scroll"
<!-- 底部需要上移的view id集士聪,中間用,隔開(kāi)-->
app:bottomViewIds="btn_continue,btn_cancel">
<!--需要滾動(dòng)的內(nèi)容用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" />
用法簡(jiǎn)單吧^?_?^
主要就是封裝自定義了AutoKeyboardConstraintLayout,用的時(shí)候只需要替換原布局的ConstraintLayout猛蔽,xml中設(shè)置幾個(gè)參數(shù)即可剥悟,代碼不需要任何改動(dòng)灵寺,不習(xí)慣用ConstraintLayout的童鞋,可以參照封裝改成LinearLayout
下面是實(shí)現(xiàn)
定義下幾個(gè)屬性
<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>
主要是這個(gè)AutoKeyboardConstraintLayout這個(gè)類
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
}
}
注釋是英文的区岗,因?yàn)槭窃谧龉卷?xiàng)目的時(shí)候?qū)懙倪@個(gè)控件略板,公司項(xiàng)目注釋要求是英文的,也懶得改中文了( ̄? ̄)
附上demo地址:https://github.com/fengpeihao/AutoKeyboardDemo
第一次寫(xiě)文章慈缔,寫(xiě)的很簡(jiǎn)單叮称,主要是為了記錄下方便以后用到查看