先看VPGAME客戶端的這個效果:
接著是我實現的效果:
轉成gif圖質量不太好,實際效果比這個好很多,可以去運行demo看看實際效果。鏈接:https://github.com/DarkSherlock/DateViewWithRvDemo
我們可以看到這個效果,當recyclerview滑動的時候,這個控件里的那個時鐘指針
會跟著轉動,后面的文字也會跟著item的值 有一個滑進滑出動畫。
我本以為這是一個自定義View,然而當我用打開DDMS用HierachyView查看它的布局的時候。
我們可以看到他這個不是用一個自定義View來完成的,而是多個自定義View
來組合在RelativieLayout里來實現的。那么我們就可以借鑒他的這個思路。
Studio打開HierachyView的步驟:
那么接下來就來分析下實現的思路:
1.首先要和RecyclerView完成交互,那么就需要添加OnScrollListener來監(jiān)聽
RV的滑動,根據滑動距離來算出滑動了幾個Item,根據Item的某字段(它這里是時間月份)來傳給自定義控件,讓其完成UI更新。
2.那個滑進滑出的控件,覺得不需要再去自定義,只需要用TextView加位移動畫就能實現。
3.自定義指針轉動控件,根據OnScrollListener監(jiān)聽到的dy滑動距離,來設置轉動的角度。
具體實現:
//為了和dateview 完成聯動,添加滑動監(jiān)聽
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的月份字段不一樣的時候,這時候需要執(zhí)行滑進滑出的
//動畫,并且將月份更新顯示。
if (mBean.getMonth()!= Integer.parseInt(mTvMonth.getText().toString())) {
mCurrentMonth = mBean.getMonth();
if (dy > 0) { //判斷執(zhí)行向上還是向下滑動動畫
startUpAnim( );
} else {
startDownAnim();
}
}
}
}
接著看看動畫:
由于位移動畫我們需要拿到執(zhí)行動畫的textview的Y軸起始坐標和高度,所以我們post一個runnable(直接在activity的oncreat()中去拿的話因為控件可能還未layout完畢,所以可能取到的值為0);
動畫分為:1.向上滑出動畫2.向上滑進動畫3.向下滑出動畫4.向下滑進動畫。
textview向上滑出頂部不可見后再從底部向上滑進(1執(zhí)行完畢后執(zhí)行2)
textview向下滑出底部不可見后再從頂部向下滑進(3執(zhí)行完畢后執(zhí)行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();
//向上彈出動畫
//第一個參數是要執(zhí)行動畫的控件,第二個參數是更改的屬性字段(需帶有setter方法),
//第三個參數是 動畫開始時 要更改的屬性字段的起始值,第四個是結束時的值(translationY - tvMonthHeight 相當于滑出邊界不可見了。)
//這里指mTvMonth執(zhí)行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);
//添加動畫執(zhí)行監(jiān)聽
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);
//添加動畫執(zhí)行監(jiān)聽
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滑動速度非常快的時候,當前的動畫還未執(zhí)行,已經滑動到下條數據要執(zhí)行下一個動畫時,
//因為我們判斷了!upAnimIn.isStarted() ,所以下個動畫不會執(zhí)行,這時候就需要以下判斷當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) {
}
});
}
動畫設置執(zhí)行時間為50ms,但是由于recyclerview可能會非常快速地滑動,所以如果動畫還在執(zhí)行就跳過,在 RecyclerView滑動停止時即狀態(tài)等于SCROLL_STATE_IDLE時將要更新的值保存下來,在動畫執(zhí)行完畢的時候去判斷 如果數據顯示不正確再重新賦值正確的數據給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中看。