先看VPGAME客户端的这个效果:
接着是我实现的效果:
转成gif图质量不太好,实际效果比这个好很多,可以去运行demo看看实际效果。链接:https://github.com/DarkSherlock/DateViewWithRvDemo
我们可以看到这个效果,当recyclerview滑动的时候,这个控件里的那个时钟指针
会跟着转动,后面的文字也会跟着item的值 有一个滑进滑出动画。
我本以为这是一个自定义View,然而当我用打开DDMS用HierachyView查看它的布局的时候。
我们可以看到他这个不是用一个自定义View来完成的,而是多个自定义View
来组合在RelativieLayout里来实现的。那么我们就可以借鉴他的这个思路。
Studio打开HierachyView的步骤:
那么接下来就来分析下实现的思路:
1.首先要和RecyclerView完成交互,那么就需要添加OnScrollListener来监听
RV的滑动,根据滑动距离来算出滑动了几个Item,根据Item的某字段(它这里是时间月份)来传给自定义控件,让其完成UI更新。
2.那个滑进滑出的控件,觉得不需要再去自定义,只需要用TextView加位移动画就能实现。
3.自定义指针转动控件,根据OnScrollListener监听到的dy滑动距离,来设置转动的角度。
具体实现:
//为了和dateview 完成联动,添加滑动监听
rv.addOnScrollListener(new MyScrollListener());
在onScrollListener()里着重关注onScrolled();
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if ( mRvItemHeight != 0 ) {
y += dy;
//将累计的滑动距离 跟一个item的高度 比较,判断滑动了相当于几个item的距离。
float position = y / mRvItemHeight;
//将每次滑动了相当于多少个Item高度的值传给指针控件,
//滑动一个item高度指针就转动一圈,按比例转动角度。
dateview.setProcess(position);
mBean = mList.get((int) position);//拿到对应的item的javabean
//只要有轻微的滑动onScrolled就会调用,但是我们不需要这么频繁的去更新滑进滑出的UI
//所以我们这里判断只有当2个item的月份字段不一样的时候,这时候需要执行滑进滑出的
//动画,并且将月份更新显示。
if (mBean.getMonth()!= Integer.parseInt(mTvMonth.getText().toString())) {
mCurrentMonth = mBean.getMonth();
if (dy > 0) { //判断执行向上还是向下滑动动画
startUpAnim( );
} else {
startDownAnim();
}
}
}
}
接着看看动画:
由于位移动画我们需要拿到执行动画的textview的Y轴起始坐标和高度,所以我们post一个runnable(直接在activity的oncreat()中去拿的话因为控件可能还未layout完毕,所以可能取到的值为0);
动画分为:1.向上滑出动画2.向上滑进动画3.向下滑出动画4.向下滑进动画。
textview向上滑出顶部不可见后再从底部向上滑进(1执行完毕后执行2)
textview向下滑出底部不可见后再从顶部向下滑进(3执行完毕后执行4)
//post 一个runnable 待 view layout 完毕后测量 rcyclerview item的高度 并且初始化动画
rv.post(new Runnable() {
@Override
public void run() {
View childAt = rv.getLayoutManager().findViewByPosition(0);
if (childAt != null) {
mRvItemHeight = (float) childAt.getHeight();
initAnimation();
}
}
});
private void initAnimation() {
// Y轴方向上的坐标
float translationY = mTvMonth.getTranslationY();
float tvMonthHeight = mTvMonth.getHeight();
//向上弹出动画
//第一个参数是要执行动画的控件,第二个参数是更改的属性字段(需带有setter方法),
//第三个参数是 动画开始时 要更改的属性字段的起始值,第四个是结束时的值(translationY - tvMonthHeight 相当于滑出边界不可见了。)
//这里指mTvMonth执行Y轴上的坐标 更改(Y轴位移动画)
mUpAnimOut = ObjectAnimator.ofFloat(mTvMonth, "translationY", translationY, translationY - tvMonthHeight);
//向上弹进动画
mUpAnimIn =ObjectAnimator.ofFloat(mTvMonth, "translationY", translationY + tvMonthHeight, translationY);
mUpAnimOut.setDuration(ANIMATION_DURATION);
mUpAnimIn.setDuration(ANIMATION_DURATION);
//添加动画执行监听
addUpAnimListener(mUpAnimIn);
//向下弹出动画
mDownAnimOut =ObjectAnimator.ofFloat(mTvMonth, "translationY", translationY, translationY + tvMonthHeight);
//向下弹进动画
ObjectAnimator downAnimIn =ObjectAnimator.ofFloat(mTvMonth, "translationY", translationY - tvMonthHeight, translationY);
mDownAnimOut.setDuration(ANIMATION_DURATION);
downAnimIn.setDuration(ANIMATION_DURATION);
//添加动画执行监听
addDownAnimListener(downAnimIn);
}
private void addUpAnimListener(final ObjectAnimator upAnimIn) {
mUpAnimOut.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
if (!upAnimIn.isStarted()) {
upAnimIn.start();
}
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
upAnimIn.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
mTvMonth.setText(String.valueOf(mCurrentMonth));
}
@Override
public void onAnimationEnd(Animator animation) {
//当recycler滑动速度非常快的时候,当前的动画还未执行,已经滑动到下条数据要执行下一个动画时,
//因为我们判断了!upAnimIn.isStarted() ,所以下个动画不会执行,这时候就需要以下判断当RecyclerView
//滑动停止,当前动画结束时将正确的(下一条的数据)设置给mTvMonth,避免数据错乱.
if (scrollState == RecyclerView.SCROLL_STATE_IDLE && mCurrentMonth != Integer.parseInt(mTvMonth.getText().toString())) {
mTvMonth.setText(String.valueOf(mCurrentMonth));
}
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
动画设置执行时间为50ms,但是由于recyclerview可能会非常快速地滑动,所以如果动画还在执行就跳过,在 RecyclerView滑动停止时即状态等于SCROLL_STATE_IDLE时将要更新的值保存下来,在动画执行完毕的时候去判断 如果数据显示不正确再重新赋值正确的数据给textview
/**
* 开始向上滑出的动画
*/
private void startUpAnim( ) {
if (!mUpAnimOut.isStarted()) {
mUpAnimOut.start();
}
}
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (mBean != null) {
scrollState = newState;
//当非常快速滑动的时候 在滑动的最后判断数据是否准确,将正确的数据返回。
if (scrollState == RecyclerView.SCROLL_STATE_IDLE && mCurrentMonth != Integer.parseInt(mTvMonth.getText().toString())) {
mCurrentMonth = mBean.getMonth();
}
}
}
这样动画的部分就实现完了,接着看转动指针的部分
转动指针自定义View分为2部分:1.不动的圆形背景类似于时钟背景
2.转动的指针,类似于时钟指针。
背景直接canvas.drawCircle就行,没什么可说的。
指针转动的角度就需要根据传onScrollListener传进来的值进行一定的计算来算出需要转动多少角度,直接看代码就懂了。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int height = getHeight();
int width = getWidth();
int radius = width / 2;//圆形背景半径
canvas.translate(width / 2, height / 2);
canvas.save();
//画灰色圆形背景
canvas.drawCircle(0, 0, width / 2, mCirclePaint);
//画12 3 6 9 四个刻度 长度为半径(width/2)的0.25
mCursor.setColor(Color.parseColor("#FFAAAAAA"));
canvas.drawLine(0, -height / 2, 0, ((radius * R_QUARTER) - height / 2), mCursor);//12
canvas.drawLine(width / 2, 0, (width / 2 - (radius * R_QUARTER)), 0, mCursor);//3
canvas.drawLine(0, height / 2, 0, (height / 2 - (radius * R_QUARTER)), mCursor);//6
canvas.drawLine(-width / 2, 0, (-width / 2 + (radius * R_QUARTER)), 0, mCursor);//9
//画根据传进来的process 转动的指针
int stopX = (int) (0.6 * (width / 2) * Math.sin(mProcess * 2 * Math.PI));
int stopY = (int) (0.6 * (width / 2) * Math.cos(mProcess * 2 * Math.PI));
mCursor.setColor(Color.WHITE);
canvas.drawLine(0, 0, stopX, -stopY, mCursor);
}
/**
* 设置指针转动角度比率
* @param process
*/
public void setProcess(float process) {
this.mProcess = process;
invalidate();
}
这样就完成了,挺简单的代码,完整的代码可以去githup上的demo中看。