需求
最近遇到了一个需求,需求的界面是类似这样的
页面:列表头部有一个搜索框,并且搜索框可跟随RecyclerView 上下滑动,下拉刷新控件在界面最顶部。
交互效果:当列表向下滑动,搜索框浮起,当列表项滑动到界面顶部后,搜索框向上滑动隐藏。搜索框隐藏后再次下拉,搜索框向下滑动出现。
实现的效果如下:
实现分析
界面分析
最简单快速的实现方式, 蓝色区域 作为RecyclerView的一个Item, 红色区域是正常的列表项 ,最外层布局使用FrameLayout或RelativeLayout,在RecyclerView外层浮动一个与列表项相同的搜索框布局初始状态为不可见,当RecyclerView向上滑动时,将外层浮动搜索框布局显示,当RecyclerView再次滑动到顶部时,将外层浮动布局隐藏。同时在滑动距离超出列表中搜索框高度时,向上滑动,搜索框开启向上平移动画隐藏,向下滑动时,搜索框开启向下平移动画隐藏。
实现
1、界面布局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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:layout_height="match_parent"
tools:context="com.art88.scwen.searchsuspendscroll.MainActivity">
<span class="hljs-tag"><<span class="hljs-name">android.support.v4.widget.SwipeRefreshLayout</span>
<span class="hljs-attr">android:id</span>=<span class="hljs-string">"@+id/refresh_layout"</span>
<span class="hljs-attr">android:layout_width</span>=<span class="hljs-string">"match_parent"</span>
<span class="hljs-attr">android:layout_height</span>=<span class="hljs-string">"match_parent"</span>></span>
<span class="hljs-tag"><<span class="hljs-name">android.support.v7.widget.RecyclerView</span>
<span class="hljs-attr">android:id</span>=<span class="hljs-string">"@+id/recycler"</span>
<span class="hljs-attr">android:layout_width</span>=<span class="hljs-string">"match_parent"</span>
<span class="hljs-attr">android:layout_height</span>=<span class="hljs-string">"wrap_content"</span>/></span>
<span class="hljs-tag"></<span class="hljs-name">android.support.v4.widget.SwipeRefreshLayout</span>></span>
<span class="hljs-tag"><<span class="hljs-name">LinearLayout</span>
<span class="hljs-attr">android:id</span>=<span class="hljs-string">"@+id/ll_search"</span>
<span class="hljs-attr">android:layout_width</span>=<span class="hljs-string">"match_parent"</span>
<span class="hljs-attr">android:layout_height</span>=<span class="hljs-string">"50dp"</span>
<span class="hljs-attr">android:background</span>=<span class="hljs-string">"@color/white"</span>
<span class="hljs-attr">android:elevation</span>=<span class="hljs-string">"3dp"</span>
<span class="hljs-attr">android:paddingBottom</span>=<span class="hljs-string">"8dp"</span>
<span class="hljs-attr">android:paddingTop</span>=<span class="hljs-string">"10dp"</span>
<span class="hljs-attr">android:visibility</span>=<span class="hljs-string">"invisible"</span>></span>
<span class="hljs-tag"><<span class="hljs-name">LinearLayout</span>
<span class="hljs-attr">android:layout_width</span>=<span class="hljs-string">"match_parent"</span>
<span class="hljs-attr">android:layout_height</span>=<span class="hljs-string">"32dp"</span>
<span class="hljs-attr">android:layout_marginLeft</span>=<span class="hljs-string">"12dp"</span>
<span class="hljs-attr">android:layout_marginRight</span>=<span class="hljs-string">"12dp"</span>
<span class="hljs-attr">android:background</span>=<span class="hljs-string">"@drawable/home_search_shape"</span>
<span class="hljs-attr">android:gravity</span>=<span class="hljs-string">"center"</span>></span>
<span class="hljs-tag"><<span class="hljs-name">LinearLayout</span>
<span class="hljs-attr">android:layout_width</span>=<span class="hljs-string">"wrap_content"</span>
<span class="hljs-attr">android:layout_height</span>=<span class="hljs-string">"wrap_content"</span>
<span class="hljs-attr">android:gravity</span>=<span class="hljs-string">"center_vertical"</span>></span>
<span class="hljs-tag"><<span class="hljs-name">ImageView</span>
<span class="hljs-attr">android:layout_width</span>=<span class="hljs-string">"wrap_content"</span>
<span class="hljs-attr">android:layout_height</span>=<span class="hljs-string">"wrap_content"</span>
<span class="hljs-attr">android:src</span>=<span class="hljs-string">"@drawable/icon_search"</span> /></span>
<span class="hljs-tag"><<span class="hljs-name">TextView</span>
<span class="hljs-attr">android:layout_width</span>=<span class="hljs-string">"wrap_content"</span>
<span class="hljs-attr">android:layout_height</span>=<span class="hljs-string">"wrap_content"</span>
<span class="hljs-attr">android:layout_marginLeft</span>=<span class="hljs-string">"6dp"</span>
<span class="hljs-attr">android:text</span>=<span class="hljs-string">"搜索"</span>
<span class="hljs-attr">android:textColor</span>=<span class="hljs-string">"@color/cccccc"</span>
<span class="hljs-attr">android:textSize</span>=<span class="hljs-string">"14sp"</span> /></span>
<span class="hljs-tag"></<span class="hljs-name">LinearLayout</span>></span>
<span class="hljs-tag"></<span class="hljs-name">LinearLayout</span>></span>
<span class="hljs-tag"></<span class="hljs-name">LinearLayout</span>></span>
</RelativeLayout>
布局很简单,如果搜索框没要求 高度阴影,可以使用 include 标签 复用搜索框的布局
android:elevation="3dp"
设置视图的高度,并且加上阴影
2、Adapter
public class TestAdapter extends RecyclerView.Adapter<TestAdapter.ViewHolder> {
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-keyword">int</span> TYPE_SEARCH = <span class="hljs-number">0</span>;
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-keyword">int</span> TYPE_NORMAL = <span class="hljs-number">1</span>;
<span class="hljs-keyword">private</span> List<String> mDatas;
<span class="hljs-keyword">private</span> LayoutInflater mLayoutInflater;
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">TestAdapter</span><span class="hljs-params">(Context context, List<String> datas)</span> </span>{
mDatas = datas;
mLayoutInflater = LayoutInflater.from(context);
}
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> <span class="hljs-title">getItemViewType</span><span class="hljs-params">(<span class="hljs-keyword">int</span> position)</span> </span>{
<span class="hljs-keyword">return</span> position == <span class="hljs-number">0</span> ? TYPE_SEARCH : TYPE_NORMAL;
}
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> ViewHolder <span class="hljs-title">onCreateViewHolder</span><span class="hljs-params">(ViewGroup parent, <span class="hljs-keyword">int</span> viewType)</span> </span>{
<span class="hljs-keyword">switch</span> (viewType) {
<span class="hljs-keyword">case</span> TYPE_NORMAL:
<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ViewHolder(mLayoutInflater.inflate(R.layout.item_normal_list, parent, <span class="hljs-keyword">false</span>));
<span class="hljs-keyword">case</span> TYPE_SEARCH:
<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ViewHolder(mLayoutInflater.inflate(R.layout.item_search_list, parent, <span class="hljs-keyword">false</span>));
}
<span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
}
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onBindViewHolder</span><span class="hljs-params">(ViewHolder holder, <span class="hljs-keyword">int</span> position)</span> </span>{
}
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> <span class="hljs-title">getItemCount</span><span class="hljs-params">()</span> </span>{
<span class="hljs-keyword">return</span> mDatas == <span class="hljs-keyword">null</span> ? <span class="hljs-number">0</span> : mDatas.size();
}
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ViewHolder</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">RecyclerView</span>.<span class="hljs-title">ViewHolder</span> </span>{
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">ViewHolder</span><span class="hljs-params">(View itemView)</span> </span>{
<span class="hljs-keyword">super</span>(itemView);
}
}
}
Adapter的代码也很简单,一个测试使用的分类型adapter,搜索框类型和列表项类型。
3、实现滑动
重点来了,需要实现滑动交互,需要监听RecyclerView的滑动,计算滑动距离,判断滑动方向,并根据距离和方向改变外层搜索框的可见性并开启动画。
需要的变量:
private LinearLayout ll_search; //外层的搜索框控件
<span class="hljs-keyword">private</span> <span class="hljs-keyword">int</span> mLlSearchHeight; <span class="hljs-comment">// 搜索框的高度</span>
<span class="hljs-keyword">private</span> <span class="hljs-keyword">int</span> mScrollY; <span class="hljs-comment">//recyclerview 滑动的距离</span>
<span class="hljs-keyword">private</span> <span class="hljs-keyword">boolean</span> isShow = <span class="hljs-keyword">true</span>; <span class="hljs-comment">//搜索框是否显示</span>
<span class="hljs-keyword">private</span> <span class="hljs-keyword">boolean</span> isAnimmating;<span class="hljs-comment">//是否正在进行动画</span>
主要实现代码
recycler.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
//dy是垂直滚动距离,手指上滑动的时候为正,手指下滑的时候为负
<span class="hljs-comment">//需要获取llSearch 的高度作为 判断条件 所以布局文件中 llSearch的 visiable属性 不能设置为 gone</span>
<span class="hljs-comment">// 设置为 gone之后,llSearch 不进行渲染 获取不到高度</span>
<span class="hljs-keyword">if</span> (mLlSearchHeight == <span class="hljs-number">0</span>) {
mLlSearchHeight = ll_search.getHeight();
}
<span class="hljs-comment">//记录滑动的距离</span>
mScrollY += dy;
<span class="hljs-keyword">if</span> (mScrollY <= <span class="hljs-number">0</span>) {
ll_search.setVisibility(View.INVISIBLE);
} <span class="hljs-keyword">else</span> {
ll_search.setVisibility(View.VISIBLE);
}
<span class="hljs-keyword">if</span> (isAnimmating || (mScrollY <= mLlSearchHeight)) {
<span class="hljs-keyword">return</span>;
}
<span class="hljs-keyword">if</span> (dy < <span class="hljs-number">0</span>) {
<span class="hljs-keyword">if</span> (isShow) {
<span class="hljs-keyword">return</span>;
}
ObjectAnimator animator = ObjectAnimator.ofFloat(ll_search, <span class="hljs-string">"translationY"</span>, -mLlSearchHeight, <span class="hljs-number">0</span>);
animator.setDuration(<span class="hljs-number">300</span>);
animator.addListener(<span class="hljs-keyword">new</span> AnimatorListenerAdapter() {
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onAnimationEnd</span><span class="hljs-params">(Animator animation)</span> </span>{
<span class="hljs-keyword">super</span>.onAnimationEnd(animation);
isShow = <span class="hljs-keyword">true</span>;
isAnimmating = <span class="hljs-keyword">false</span>;
animation.removeAllListeners();
}
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onAnimationStart</span><span class="hljs-params">(Animator animation)</span> </span>{
<span class="hljs-keyword">super</span>.onAnimationStart(animation);
isAnimmating = <span class="hljs-keyword">true</span>;
}
});
animator.start();
} <span class="hljs-keyword">else</span> {
<span class="hljs-keyword">if</span> (!isShow) {
<span class="hljs-keyword">return</span>;
}
ObjectAnimator animator = ObjectAnimator.ofFloat(ll_search, <span class="hljs-string">"translationY"</span>, <span class="hljs-number">0</span>, -mLlSearchHeight);
animator.setDuration(<span class="hljs-number">300</span>);
animator.addListener(<span class="hljs-keyword">new</span> AnimatorListenerAdapter() {
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onAnimationEnd</span><span class="hljs-params">(Animator animation)</span> </span>{
<span class="hljs-keyword">super</span>.onAnimationEnd(animation);
isShow = <span class="hljs-keyword">false</span>;
isAnimmating = <span class="hljs-keyword">false</span>;
animation.removeAllListeners();
}
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onAnimationStart</span><span class="hljs-params">(Animator animation)</span> </span>{
<span class="hljs-keyword">super</span>.onAnimationStart(animation);
isAnimmating = <span class="hljs-keyword">true</span>;
}
});
animator.start();
}
}
});
}
1、监听滑动的方向并计算滑动的距离
RecyclerView可以通过onScrolled方法参数中的 dy 来判断滑动的方向,手指向上滑动 dy 为正,手指向下滑动dy 为负数。并且这个方法调用非常频繁,当界面发生滚动,方法就会被调用,dy表示竖直方向上视图滚动的差值,通过mScrollY += dy
记录RecyclerView滑动的距离。
2、根据滑动的距离改变ll_search 的可见性
当RecyclerView 滑动到界面最顶部,设置ll_search 为不可见状态,否则设置为可见状态。
if (mScrollY <= 0) {
ll_search.setVisibility(View.INVISIBLE);
} else {
ll_search.setVisibility(View.VISIBLE);
}
3、根据滑动的距离和方向进行动画
只有当滑动超过 ll_search 的高度之后才进行动画
if (mScrollY <= mLlSearchHeight) {
return;
}
根据方向执行动画
if (dy < 0) {
ObjectAnimator animator = ObjectAnimator.ofFloat(ll_search, <span class="hljs-string">"translationY"</span>, -mLlSearchHeight, 0);
animator.setDuration(300);
animator.start();
} <span class="hljs-keyword">else</span> {
ObjectAnimator animator = ObjectAnimator.ofFloat(ll_search, <span class="hljs-string">"translationY"</span>, 0, -mLlSearchHeight);
animator.setDuration(300);
animator.start();
}
当ll_search 已经处于显示状态,屏蔽下移动画,当ll_search 处于隐藏状态,屏蔽上移动画
if (dy < 0) {
if (isShow) {
return;
}
else{
if (!isShow) {
return;
}
}
当正在执行动画,屏蔽开启动画的操作
if (isAnimmating || (mScrollY <= mLlSearchHeight)) {
return;
}
至此,实现效果代码分析完毕。
</div>
</div>