美文网首页Android开发AndroidAndroid
Android底部控件随软键盘上移

Android底部控件随软键盘上移

作者: 浩_feng | 来源:发表于2021-02-03 17:08 被阅读0次

    很多时候我们会遇到这个问题,就是点击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

    第一次写文章,写的很简单,主要是为了记录下方便以后用到查看

    相关文章

      网友评论

        本文标题:Android底部控件随软键盘上移

        本文链接:https://www.haomeiwen.com/subject/ciqmtltx.html