顶部动态伸缩搜索框的简单实现
项目上需要一个点击输入框,然后输入框拉长的需求,自己参考了别人的两个案例简单地实现了一下,作为一个新人开发来说,思路很简单,但是也是相当花时间。本人开发时间不足一年,所以写的代码可能有点烂,如有何处处理不当或者错误的地方请直说。
太年轻走了太多弯路。之后也有遇到更好的方案,但也没更新这篇文。
直接使用ObjectAnimator
对view进行控制,不需要啰里啰唆这么多代码。
ObjectAnimator.ofInt(view, "width", view.getWidth(), maxValue).setDuration(duration).start();
与之前不同的是少了通过view
当前的宽度在目标宽度的比例乘以默认持续时长duration
得到需要的时长。
不加的原因有两个:1、动画时间较短,这部分的时间差基本很难感受到差别。2、增加时间计算的话又会啰里啰唆加很多代码。所以在投入较多收益不高的情况下选择性价比最高的方案。
已更新至Github
============================================================
以下内容过时,暂时留做记录
日常的基于百度的项目开发与实现 笑 。进入百度搜索能找到类似需求的这两片文章
- 虾皮皮 的 仿简书发现页面头部透明度渐变及搜索框动态效果
- chiyidun 的 仿简书动态searchview的实现,代码就这么多点
第一篇排版有点乱,原谅我没怎么仔细看下去,原理应该是通过ScrollView的滚动监听然后设置搜索框的宽度,稍微仔细阅读过后发现竟然继承了ScrollView自定义了一个,这个处理方法在我觉得不太简单方便。开头的动图发现作者对动画的过度不够平滑,还有就是搜索框展开了之后都突出屏幕了,这些都是细节的问题,虽然是个demo但是看着也有点难受。
第二篇也差不多,不过在动画方面利用了系统提供的Transition提供了一个不错的效果,就算在Android 5.0以下,只要引入api 'com.android.support:design:25.3.1'
就没有问题,但是我就是想自己试试,而且在看到需求的时候就已经有用属性动画的想法了,而Transition刚好是属性动画的封装,所以就尝试着自己写了一下。事实证明,还是调用现有的比较方便,不过自己写一遍也算是有点收获吧。
先上一张自己做的效果图吧,效果也还行,不过写的过程中遇到的问题挺多的。最后卡了一下是git录制原因。说好的点击展开呢,怎么又变滑动展开了??其实有留点击展开的方法啦,只不过滑动看着更有意思!
想法
通过监听ScrollView的滚动触发伸缩动画,通过属性动画产生一个平滑的过度数值,可以说是相当简单了。当然,直接使用Transition,只要设置好开始时的布局、结束的布局以及动画持续的时间就能更快地实现了。
动手
因为本身需求原因,不是特别需要透明度的变化,所以会主要说明动画方面。代码中很多能用lambda表达式的地方都用了,用多了就会了,虽然可读性变差了,但是真的是简洁。
首先是用到了属性动画中的ValueAnimator,它能够提供一个平滑的过度数值。没有使用ObjectAnimator是因为刚接触属性动画,对于ObjectAnimator中的拉伸还没有仔细研究,但是看别人的示例图发现ObjectAnimator的拉伸是对view包括其中的字体等都会有拉伸效果,所以暂时先不考虑使用,后续会进行尝试。
在使用ValueAnimator需要提供开始值和结束值,这就需要对绘制完成的view进行宽度测量,这里采用View.post()
的方式,获得view的宽度。给自己的笔记:margin值不会被计入width
subView.post(() -> {
isSubPost = true;
subViewWidth = subView.getWidth();
});
parentView.post(() -> {
isParentPost = true;
margin = parentView.getPaddingLeft() + parentView.getPaddingRight();
parentViewWidth = parentView.getWidth();
});
在触发动画的时候,需要注意以下几点:
- 动画还未开始,非常简单直接从开始到结束
- 动画进行中,但下一个动画还是当前动画,同上,只要忽略多余的,继续执行当前的动画即可
例如:搜索框正在展开,然后又触发了展开的操作,这个情况会在SrcollView上滑触发展开动画后 - 动画进行中,但下一个动画不是当前动画
例如:展开动画还没有结束时,发生了收缩动画,这时候就要停止展开的动画,从当前位置开始收缩动画 - 动画完成
上面需要重点关注的就是第三点
public void expand() {
//进入此处说明必须展开搜索框
isExpand = true;
boolean c; //确定有没有正在进行搜收缩动画
if (closeAnimator == null) {
c = false;
} else {
c = closeAnimator.isRunning();
}
if (subView.getWidth() == subViewWidth || c) {
//当subView.getWidth() == subViewWidth时说明动画还未开始
if (openAnimator != null && openAnimator.isRunning()) return; //正在进行当前动画,无需进行下去,直接return
if (closeAnimator != null) closeAnimator.cancel();//c为true时存在收缩动画,进行cancel
openAnimator = ValueAnimator.ofFloat(subView.getWidth(), parentViewWidth - margin);
openAnimator.setDuration(getDuration(duration, true));//获得动画需要的时间
openAnimator.addUpdateListener(animator -> updata(animator));//动画数值监听
openAnimator.start();//开始动画
} else if (openAnimator != null || openAnimator.isRunning()) {
//这里是动画正在进行中
//本来可以和上面归并的,但是openAnimator一直要做非空判断,不然可能会出现空指针异常
//具体还要自己再理理
}
}
收缩的部分和展开的基本类似,updata(ValueAnimator animator)
就是更新view的宽度
private void updata(ValueAnimator animator) {
float currentValue = (float) animator.getAnimatedValue();
subView.getLayoutParams().width = (int) currentValue;
subView.requestLayout();
}
getDuration(long duration, boolean isReverse)
计算动画需要的时长,通过view当前的宽度在目标宽度的比例乘以默认持续时长duration
得到需要的时长,isReverse
用于指明是收缩还是展开,主要用在展开进行一半时被停止,然后接着执行收缩。用Transition实现可没有这个特点哦,也就是说view在展开一半的时候进行收缩的话,使用的可是原来全程的时间哦。(总算自己写的还有点微不足道的用处)
private long getDuration(long duration, boolean isReverse) {
if (isReverse)
return duration * (parentViewWidth - margin - subView.getWidth()) / (parentViewWidth - margin - subViewWidth);
else
return duration * (subView.getWidth() - subViewWidth) / (parentViewWidth - margin - subViewWidth);
}
setDuration(long duration)
设置动画持续时间,不设置时默认300毫秒。
public void setDuration(long duration) {
this.duration = duration;
}
doAnimat()
是为点击展开收缩预留的方法
public void doAnimat() {
Log.i(tag, "doAnimat");
if (subView.getWidth() == subViewWidth) {
Log.i(tag, "doAnimat expand");
expand();
} else if (subView.getWidth() == parentViewWidth - margin) {
Log.i(tag, "doAnimat reduce");
reduce();
}
}
最后甩上最后一点东西
方法的调用
telescopicAnimator = new TelescopicAnimator(findViewById(R.id.ll_bg));//实例化,只需要被拉伸的那个view
//在监听中直接食用即可
scrollView.getViewTreeObserver().addOnScrollChangedListener(() -> {
if (scrollView.getScrollY() >= (imageView.getHeight() - relativeLayout.getHeight())) {
telescopicAnimator.expand();
} else if (scrollView.getScrollY() <= 0) {
telescopicAnimator.reduce();
}
});
以及activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:id="@+id/sl_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="none">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="2000dp">
<ImageView
android:id="@+id/iv_"
android:layout_width="match_parent"
android:layout_height="200dp"
android:scaleType="centerCrop"
android:src="@mipmap/img" />
</RelativeLayout>
</ScrollView>
<RelativeLayout
android:id="@+id/rl_bg"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:fitsSystemWindows="true">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@null"
android:paddingBottom="7dp"
android:paddingLeft="14dp"
android:paddingRight="14dp"
android:paddingTop="7dp">
<LinearLayout
android:id="@+id/ll_bg"
android:layout_width="wrap_content"
android:layout_height="30dp"
android:layout_alignParentRight="true"
android:background="@drawable/activity_main_search_bg"
android:gravity="center"
android:paddingLeft="30dp"
android:paddingRight="30dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="搜索"
android:textColor="@color/searchText" />
</LinearLayout>
</RelativeLayout>
</RelativeLayout>
</RelativeLayout>
总结:
如果只是使用的话,单个.class文件拉走用就行了,适合对design:25.3.1
没有其他需求,不想在项目中额外引入的。如果对design:25.3.1
使用较多的话,还是使用封装好的吧,毕竟简单方便。剩下的关于顶部透明度的变化等在开头的两篇文章中有很好地说明了。关于属性动画的话,我是通过 guolin 的 Android属性动画完全解析 了解的,大家如果有兴趣的话可以去了解一下。最后demo的代码放在Github了(点击这里)