Android实现SearchView获焦从屏幕中央移动到Toolbar效果
Android SearchView 居中-顶部位移动效实现方案
核心问题说明
你当前代码的核心问题是把SearchView固定嵌套在AppBarLayout内部,AppBar的滚动/展开范围最多只能覆盖自身高度,完全无法将SearchView下移到屏幕中央的初始位置,仅靠setExpanded方法不可能实现预期效果。
实现的核心思路是:将SearchView作为CoordinatorLayout的顶层直接子View(不嵌套在AppBar或内容布局内),初始位置放在屏幕中央,监听焦点变化通过属性动画在初始位置、顶部AppBar位置之间平滑平移,全程使用同一个SearchView实例,不会出现输入状态丢失、页面切换的问题。
第一步:调整布局文件
将SearchView从AppBar内部移出,作为CoordinatorLayout的顶层子View,AppBar仅作为顶部位置锚点存在:
<?xml version="1.0" encoding="utf-8"?> <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/coordinatorLayout" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true"> <!-- 顶部AppBar,可根据需求调整高度,代码会自动适配位置 --> <com.google.android.material.appbar.AppBarLayout android:id="@+id/appBar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:backgroundTint="@color/orange_lighter" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> <!-- 内部无需放置SearchView,仅作为顶部位置锚点 --> </com.google.android.material.appbar.AppBarLayout> <!-- 页面内容区 --> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/constraintLayout" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior"> <!-- 此处放置RecyclerView、分类入口等其他页面控件 --> </androidx.constraintlayout.widget.ConstraintLayout> <!-- SearchView作为顶层子View,初始位置居中,方便做位移动画 --> <androidx.appcompat.widget.SearchView android:id="@+id/consultationSearchView" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" android:theme="@style/SearchViewTheme" app:closeIcon="@drawable/ic_cancel" app:searchIcon="@drawable/ic_search" android:layout_gravity="center"/> </androidx.coordinatorlayout.widget.CoordinatorLayout>
第二步:实现焦点监听和平移动画
在Activity中提前计算好两个位置的坐标,焦点变化时启动平移动画,同步处理软键盘、AppBar状态:
class ExploreConsultationsActivity : BaseActivity() { companion object { const val CONSULTATION_COVER_LIST_KEY = "consultation_cover_list" private const val ANIM_DURATION = 300L } private val binding by lazy { ExploreConsultationsLayoutBinding.inflate(layoutInflater) } val viewModel by viewModel<ExploreConsultationViewModel>() // 记录SearchView初始居中位置的Y轴坐标 private var searchViewInitY = 0f // 记录SearchView移动到顶部的目标Y轴坐标 private var searchViewTargetY = 0f override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setupViewModel() setContentView(binding.root) setupView() } private fun setupViewModel() { viewModel.categoriesList = intent?.getParcelableArrayListExtra(CONSULTATION_COVER_LIST_KEY) } private fun setupView() { hideActionBar() // 等布局绘制完成后再计算坐标,避免拿到0值 binding.consultationSearchView.post { searchViewInitY = binding.consultationSearchView.y // 计算顶部位置:AppBar底部坐标 - SearchView自身高度 - 16dp顶部边距 searchViewTargetY = binding.appBar.bottom - binding.consultationSearchView.height - 16.dpToPx() } setupSearchView() } private fun setupSearchView() { binding.consultationSearchView.apply { setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String?) = false override fun onQueryTextChange(newText: String?): Boolean { if (newText != null) { viewModel.removeSearchViewFocus.value = false viewModel.queryText = newText } return true } }) setOnQueryTextFocusChangeListener { _, hasFocus -> isSelected = hasFocus if (hasFocus) { // 获焦:平移到顶部,展开AppBar、弹出软键盘 animate() .y(searchViewTargetY) .setDuration(ANIM_DURATION) .start() binding.appBar.setExpanded(true, true) val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) } else { // 失焦:回到初始居中位置,收起AppBar、隐藏软键盘、清空输入 animate() .y(searchViewInitY) .setDuration(ANIM_DURATION) .start() binding.appBar.setExpanded(false, true) val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager imm.hideSoftInputFromWindow(windowToken, 0) setQuery("", false) } } } } // dp转px扩展方法 private fun Int.dpToPx(): Float = this * resources.displayMetrics.density override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { if (ev?.action == MotionEvent.ACTION_DOWN) { val view: View? = currentFocus if (view is SearchView) { val outRect = Rect() view.getGlobalVisibleRect(outRect) if (!outRect.contains(ev.rawX.toInt(), ev.rawY.toInt())) { view.clearFocus() } } } return super.dispatchTouchEvent(ev) } }
可选优化点
- 如果需要实现更复杂的形变效果(比如SearchView尺寸变化、图标透明度切换、背景圆角变化),可以把根布局替换为
MotionLayout,通过ConstraintSet定义居中、顶部两个状态的约束规则,焦点变化时调用transitionToState切换状态,动画可定制性更强 - 动画执行过程中可以给下层内容区设置渐变透明度,强化搜索态的聚焦视觉效果
- 如果需要适配横竖屏切换,重写
onConfigurationChanged方法重新计算SearchView两个位置的Y轴坐标,避免屏幕旋转后位置错位
内容的提问来源于stack exchange,提问作者Compose Learner
相关产品推荐
相关产品推荐

