先看VPGAME客戶端的這個(gè)效果:
接著是我實(shí)現(xiàn)的效果:
轉(zhuǎn)成gif圖質(zhì)量不太好,實(shí)際效果比這個(gè)好很多,可以去運(yùn)行demo看看實(shí)際效果卵惦。鏈接:https://github.com/DarkSherlock/DateViewWithRvDemo
我們可以看到這個(gè)效果,當(dāng)recyclerview滑動(dòng)的時(shí)候玖像,這個(gè)控件里的那個(gè)時(shí)鐘指針
會(huì)跟著轉(zhuǎn)動(dòng)樱溉,后面的文字也會(huì)跟著item的值 有一個(gè)滑進(jìn)滑出動(dòng)畫挣输。
我本以為這是一個(gè)自定義View,然而當(dāng)我用打開DDMS用HierachyView查看它的布局的時(shí)候。
我們可以看到他這個(gè)不是用一個(gè)自定義View來完成的饺窿,而是多個(gè)自定義View
來組合在RelativieLayout里來實(shí)現(xiàn)的歧焦。那么我們就可以借鑒他的這個(gè)思路。
Studio打開HierachyView的步驟:
那么接下來就來分析下實(shí)現(xiàn)的思路:
1.首先要和RecyclerView完成交互肚医,那么就需要添加OnScrollListener來監(jiān)聽
RV的滑動(dòng)绢馍,根據(jù)滑動(dòng)距離來算出滑動(dòng)了幾個(gè)Item,根據(jù)Item的某字段(它這里是時(shí)間月份)來傳給自定義控件肠套,讓其完成UI更新舰涌。
2.那個(gè)滑進(jìn)滑出的控件,覺得不需要再去自定義你稚,只需要用TextView加位移動(dòng)畫就能實(shí)現(xiàn)瓷耙。
3.自定義指針轉(zhuǎn)動(dòng)控件,根據(jù)OnScrollListener監(jiān)聽到的dy滑動(dòng)距離刁赖,來設(shè)置轉(zhuǎn)動(dòng)的角度搁痛。
具體實(shí)現(xiàn):
//為了和dateview 完成聯(lián)動(dòng),添加滑動(dòng)監(jiān)聽
rv.addOnScrollListener(new MyScrollListener());
在onScrollListener()里著重關(guān)注onScrolled()宇弛;
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if ( mRvItemHeight != 0 ) {
y += dy;
//將累計(jì)的滑動(dòng)距離 跟一個(gè)item的高度 比較鸡典,判斷滑動(dòng)了相當(dāng)于幾個(gè)item的距離。
float position = y / mRvItemHeight;
//將每次滑動(dòng)了相當(dāng)于多少個(gè)Item高度的值傳給指針控件枪芒,
//滑動(dòng)一個(gè)item高度指針就轉(zhuǎn)動(dòng)一圈彻况,按比例轉(zhuǎn)動(dòng)角度。
dateview.setProcess(position);
mBean = mList.get((int) position);//拿到對(duì)應(yīng)的item的javabean
//只要有輕微的滑動(dòng)onScrolled就會(huì)調(diào)用舅踪,但是我們不需要這么頻繁的去更新滑進(jìn)滑出的UI
//所以我們這里判斷只有當(dāng)2個(gè)item的月份字段不一樣的時(shí)候纽甘,這時(shí)候需要執(zhí)行滑進(jìn)滑出的
//動(dòng)畫,并且將月份更新顯示抽碌。
if (mBean.getMonth()!= Integer.parseInt(mTvMonth.getText().toString())) {
mCurrentMonth = mBean.getMonth();
if (dy > 0) { //判斷執(zhí)行向上還是向下滑動(dòng)動(dòng)畫
startUpAnim( );
} else {
startDownAnim();
}
}
}
}
接著看看動(dòng)畫:
由于位移動(dòng)畫我們需要拿到執(zhí)行動(dòng)畫的textview的Y軸起始坐標(biāo)和高度悍赢,所以我們post一個(gè)runnable(直接在activity的oncreat()中去拿的話因?yàn)榭丶赡苓€未layout完畢,所以可能取到的值為0)货徙;
動(dòng)畫分為:1.向上滑出動(dòng)畫2.向上滑進(jìn)動(dòng)畫3.向下滑出動(dòng)畫4.向下滑進(jìn)動(dòng)畫泽裳。
textview向上滑出頂部不可見后再?gòu)牡撞肯蛏匣M(jìn)(1執(zhí)行完畢后執(zhí)行2)
textview向下滑出底部不可見后再?gòu)捻敳肯蛳禄M(jìn)(3執(zhí)行完畢后執(zhí)行4)
//post 一個(gè)runnable 待 view layout 完畢后測(cè)量 rcyclerview item的高度 并且初始化動(dòng)畫
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軸方向上的坐標(biāo)
float translationY = mTvMonth.getTranslationY();
float tvMonthHeight = mTvMonth.getHeight();
//向上彈出動(dòng)畫
//第一個(gè)參數(shù)是要執(zhí)行動(dòng)畫的控件,第二個(gè)參數(shù)是更改的屬性字段(需帶有setter方法)破婆,
//第三個(gè)參數(shù)是 動(dòng)畫開始時(shí) 要更改的屬性字段的起始值,第四個(gè)是結(jié)束時(shí)的值(translationY - tvMonthHeight 相當(dāng)于滑出邊界不可見了胸囱。)
//這里指mTvMonth執(zhí)行Y軸上的坐標(biāo) 更改(Y軸位移動(dòng)畫)
mUpAnimOut = ObjectAnimator.ofFloat(mTvMonth, "translationY", translationY, translationY - tvMonthHeight);
//向上彈進(jìn)動(dòng)畫
mUpAnimIn =ObjectAnimator.ofFloat(mTvMonth, "translationY", translationY + tvMonthHeight, translationY);
mUpAnimOut.setDuration(ANIMATION_DURATION);
mUpAnimIn.setDuration(ANIMATION_DURATION);
//添加動(dòng)畫執(zhí)行監(jiān)聽
addUpAnimListener(mUpAnimIn);
//向下彈出動(dòng)畫
mDownAnimOut =ObjectAnimator.ofFloat(mTvMonth, "translationY", translationY, translationY + tvMonthHeight);
//向下彈進(jìn)動(dòng)畫
ObjectAnimator downAnimIn =ObjectAnimator.ofFloat(mTvMonth, "translationY", translationY - tvMonthHeight, translationY);
mDownAnimOut.setDuration(ANIMATION_DURATION);
downAnimIn.setDuration(ANIMATION_DURATION);
//添加動(dòng)畫執(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) {
//當(dāng)recycler滑動(dòng)速度非车灰ǎ快的時(shí)候,當(dāng)前的動(dòng)畫還未執(zhí)行,已經(jīng)滑動(dòng)到下條數(shù)據(jù)要執(zhí)行下一個(gè)動(dòng)畫時(shí)裳扯,
//因?yàn)槲覀兣袛嗔?upAnimIn.isStarted() ,所以下個(gè)動(dòng)畫不會(huì)執(zhí)行抛丽,這時(shí)候就需要以下判斷當(dāng)RecyclerView
//滑動(dòng)停止,當(dāng)前動(dòng)畫結(jié)束時(shí)將正確的(下一條的數(shù)據(jù))設(shè)置給mTvMonth饰豺,避免數(shù)據(jù)錯(cuò)亂.
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) {
}
});
}
動(dòng)畫設(shè)置執(zhí)行時(shí)間為50ms亿鲜,但是由于recyclerview可能會(huì)非常快速地滑動(dòng)冤吨,所以如果動(dòng)畫還在執(zhí)行就跳過蒿柳,在 RecyclerView滑動(dòng)停止時(shí)即狀態(tài)等于SCROLL_STATE_IDLE時(shí)將要更新的值保存下來,在動(dòng)畫執(zhí)行完畢的時(shí)候去判斷 如果數(shù)據(jù)顯示不正確再重新賦值正確的數(shù)據(jù)給textview
/**
* 開始向上滑出的動(dòng)畫
*/
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;
//當(dāng)非充鲶。快速滑動(dòng)的時(shí)候 在滑動(dòng)的最后判斷數(shù)據(jù)是否準(zhǔn)確垒探,將正確的數(shù)據(jù)返回。
if (scrollState == RecyclerView.SCROLL_STATE_IDLE && mCurrentMonth != Integer.parseInt(mTvMonth.getText().toString())) {
mCurrentMonth = mBean.getMonth();
}
}
}
這樣動(dòng)畫的部分就實(shí)現(xiàn)完了怠李,接著看轉(zhuǎn)動(dòng)指針的部分
轉(zhuǎn)動(dòng)指針自定義View分為2部分:1.不動(dòng)的圓形背景類似于時(shí)鐘背景
2.轉(zhuǎn)動(dòng)的指針圾叼,類似于時(shí)鐘指針。
背景直接canvas.drawCircle就行捺癞,沒什么可說的夷蚊。
指針轉(zhuǎn)動(dòng)的角度就需要根據(jù)傳onScrollListener傳進(jìn)來的值進(jìn)行一定的計(jì)算來算出需要轉(zhuǎn)動(dòng)多少角度,直接看代碼就懂了。
@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 四個(gè)刻度 長(zhǎng)度為半徑(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
//畫根據(jù)傳進(jìn)來的process 轉(zhuǎn)動(dòng)的指針
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);
}
/**
* 設(shè)置指針轉(zhuǎn)動(dòng)角度比率
* @param process
*/
public void setProcess(float process) {
this.mProcess = process;
invalidate();
}
這樣就完成了髓介,挺簡(jiǎn)單的代碼惕鼓,完整的代碼可以去githup上的demo中看。