【通过ValueAnimator实现点击展开和关闭的效果】效果图如下:
文章图片
由于这个界面实现的代码比较多,这里就不一一贴出了,这里主要把涉及到"展开和收起动画"相关的代码和实现思路介绍下.
思路:
通过ValueAnimator属性动画的ofInt(int... values) 方法,接收的是一个int类型的可变参数,我们只需要传入一个起始高度和一个终止时的高度即可构建一个ValueAnimator对象,有了这个对象,我们就可以做很多操作了,例如:
1.给动画设置动画的持续时间
public ValueAnimator setDuration(long duration)
2.给动画设置持续变化的监听
public void addUpdateListener(AnimatorUpdateListener listener)
3.还可以设置动画的开始,结束,取消,重复的监听,这个是其父类Animator的方法
public void addListener(AnimatorListener listener)
4.开始动画
public void start()
5.获取动画此时的变化值,返回的是Object类型,需要类型转换为对应的类型来使用
public Object getAnimatedValue()
6.获取当前动画的变化百分比,它是从0.0~1.0之间的变化
public float getAnimatedFraction()
这里改变View的高度变化的逻辑主要是写在AnimatorUpdateListener 的回调方法
void onAnimationUpdate(ValueAnimator animation);
该回调方法会一直执行,伴随着整个动画的开始和结束,我们只需要在此方法中不断的调用目标View的setLayoutParams(ViewGroup.LayoutParams params)方法来改变目标View的高度属性即可实现展开和关闭的操作了.
下面将分别贴出上面效果图的头部安全信息和底部详情信息的展开和关闭的代码
1.关于头部安全信息的实现
1)在界面初始化的时候获取头部安全信息(左侧icon,右侧文字)整体根布局的高度
// mLlDesRoot.getHeight();
必须等界面展示出来后才可以获取
// 测量当前安全信息布局的高度,可以手动测量
mLlDesRoot.measure(0, 0);
//0,0表示不指定宽高,由系统去测量
mLlDesRootHeight = mLlDesRoot.getMeasuredHeight();
2)设置头部安全图标的根布局点击监听,点击后将下面的安全信息展开或者关闭
//设置安全信息的点击监听
rootView.findViewById(R.id.rl_des_root).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
toggle();
}
});
3)实现toggle方法的逻辑,即展开和关闭安全信息的布局mLlDesRoot
/**
* 点击安全信息布局的打开和关闭方法
*/
private void toggle() {
ValueAnimator animator;
//动画器
if (isOpen) {
//点击关闭,布局高度从最大值变化到0
animator = ValueAnimator.ofInt(mLlDesRootHeight, 0);
} else {
//点击打开,布局高度从0变化到完整高度
animator = ValueAnimator.ofInt(0, mLlDesRootHeight);
}
//重置标记
isOpen = !isOpen;
//设置动画时间
animator.setDuration(200);
//开启动画
animator.start();
//设置动画的变化监听
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//获取变化中的值
Integer height = (Integer) animation.getAnimatedValue();
//根据最新高度,更新安全信息的布局高度
mDesRootLp.height = height;
mLlDesRoot.setLayoutParams(mDesRootLp);
}
});
//设置动画监听器,在动画结束后,改变箭头的方向
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
//改变箭头的方向
if (isOpen) {
mIvArrow.setImageResource(R.drawable.arrow_up);
} else {
mIvArrow.setImageResource(R.drawable.arrow_down);
}
}
});
}
2.关于底部详情信息的实现
分下下图可知,详情信息展开时的开始高度和收起时的开始高度都是不固定的,是需要计算的.开始动画时的TextView行数倒是确定的,由图可知最大行数是7行
文章图片
1)布局文件如下:
代码实现,关于BaseHolder的扩展使用,可以参考这篇文章
通过BaseHolder的方式展示数据
/**
* 详情页-应用描述
* Created by mChenys on 2015/11/24.
*/
public class DetailDesInfoHolder extends BaseHolder {
private TextView tvDetailDes;
//详情信息TextView
private TextView tvAuthor;
//应用的名称
private ImageView ivArrow;
//箭头
private ViewGroup.LayoutParams mDesLayoutParams;
//描述信息的布局参数
private boolean isOpen;
//标记是否展开显示更多描述信息@Override
public void refreshView(AppInfo info) {
tvAuthor.setText(info.author);
tvDetailDes.setText(info.des);
//默认进来的时候让tvDetailDes显示的最多7行的数据
// 将更新高度的逻辑放在handler消息队列中处理, 避免当描述很短时也是7行高度的bug出现
UIUtils.getHandler().post(new Runnable() {
@Override
public void run() {
int shortHeight = getShotHeight();
mDesLayoutParams = tvDetailDes.getLayoutParams();
mDesLayoutParams.height = shortHeight;
tvDetailDes.setLayoutParams(mDesLayoutParams);
}
});
}@Override
public View initView() {
View rootView = UIUtils.inflate(R.layout.layout_detail_desinfo);
tvDetailDes = (TextView) rootView.findViewById(R.id.tv_detail_des);
tvAuthor = (TextView) rootView.findViewById(R.id.tv_detail_author);
ivArrow = (ImageView) rootView.findViewById(R.id.iv_arrow);
//对作者的整体布局设置点击监听,已确定是否要展开或关闭显示完整的描述信息
rootView.findViewById(R.id.rl_detail_toggle).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
toggle();
}
});
return rootView;
}/**
* 控制更多描述信息显示与否的开关
*/
private void toggle() {
//创建属性动画
ValueAnimator animator = null;
//描述信息的最大7行的高度和完整的高度
int shortHeight = getShotHeight();
int fullHeight = getFullHeight();
if (isOpen) {
//此时展开,点击后关闭
animator = ValueAnimator.ofInt(fullHeight, shortHeight);
} else {
//此时关闭,点击后展开
animator = ValueAnimator.ofInt(shortHeight, fullHeight);
}
//重置标记
isOpen = !isOpen;
//设置动画的时间
animator.setDuration(200);
//设置动画的更新监听
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//获取变化的高度
int height = (int) animation.getAnimatedValue();
mDesLayoutParams.height = height;
//应用变化的高度
tvDetailDes.setLayoutParams(mDesLayoutParams);
}
});
//如果tvDetailDes的完整高度大于最大7行的高度,则开启动画
if (null != animator && fullHeight > shortHeight) {
animator.start();
}
//更新箭头的方向
animator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}@Override
public void onAnimationEnd(Animator animation) {
if (isOpen) {
ivArrow.setImageResource(R.drawable.arrow_up);
} else {
ivArrow.setImageResource(R.drawable.arrow_down);
}
// 动画播放完后让ScrollView滑动到底部
final ScrollView scrollView = getScrollView();
// 需要注意的是,该方法不能直接被调用
// 因为Android很多函数都是基于消息队列来同步,所以需要一部操作,
// addView完之后,不等于马上就会显示,而是在队列中等待处理,虽然很快,但是如果立即调用fullScroll,
// view可能还没有显示出来,所以会失败
// 应该通过handler在新线程中更新
UIUtils.getHandler().post(new Runnable() {
@Override
public void run() {
scrollView.fullScroll(ScrollView.FOCUS_DOWN);
}
});
}@Override
public void onAnimationCancel(Animator animation) {
}@Override
public void onAnimationRepeat(Animator animation) {
}
});
}/**
* 获取当前rootView所在的父控件ScrollView
*
* @return
*/
private ScrollView getScrollView() {
ViewParent parent = getConvertView().getParent();
while (!(parent instanceof ScrollView)) {
parent = parent.getParent();
}
return (ScrollView) parent;
}/**
* 获取显示描述信息0-7行的高度,即最大的高度是7行数据
*
* @return
*/
private int getShotHeight() {
//创建一个模拟的和布局文件中tvDetailDes布局参数一样的TextView,目的是为了之后的高度测量
TextView copyTextView = new TextView(UIUtils.getContext());
copyTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
copyTextView.setText(getData().des);
copyTextView.setMaxLines(7);
//获取原始tvDetailDes在布局中的宽度
int originalWidth = tvDetailDes.getMeasuredWidth();
//计算出widthMeasureSpec,由于是宽度是match_parent,所以mode是EXACTLY
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(originalWidth, View.MeasureSpec.EXACTLY);
//计算出heightMeasureSpec,由于文字的高度是最大7行,所以高度是不确定的,可以写一个最大的高度.例如2000px,模式为AT_MOST
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(2000, View.MeasureSpec.AT_MOST);
//根据widthMeasureSpec,heightMeasureSpec去测量模拟的TextView的宽高
copyTextView.measure(widthMeasureSpec, heightMeasureSpec);
//最后将测量后的高度返回.
return copyTextView.getMeasuredHeight();
}/**
* 获取显示描述信息显示完整内容的高度,即最大的高度是不固定的
* 和上面的方法类似,只是去掉了MaxLines的限制
*
* @return
*/
private int getFullHeight() {
//创建一个模拟的和布局文件中tvDetailDes布局参数一样的TextView,目的是为了之后的高度测量
TextView copyTextView = new TextView(UIUtils.getContext());
copyTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
copyTextView.setText(getData().des);
//获取原始tvDetailDes在布局中的宽度
int originalWidth = tvDetailDes.getMeasuredWidth();
//计算出widthMeasureSpec,由于是宽度是match_parent,所以mode是EXACTLY
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(originalWidth, View.MeasureSpec.EXACTLY);
//计算出heightMeasureSpec,由于文字的高度是最大7行,所以高度是不确定的,可以写一个最大的高度.例如2000px,模式为AT_MOST
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(2000, View.MeasureSpec.AT_MOST);
//根据widthMeasureSpec,heightMeasureSpec去测量模拟的TextView的宽高
copyTextView.measure(widthMeasureSpec, heightMeasureSpec);
//最后将测量后的高度返回.
return copyTextView.getMeasuredHeight();
}}