仿VPGAME客戶端跟RecyclerView聯動指針控件

先看VPGAME客戶端的這個效果:

2017-08-28-10mzvp.gif

接著是我實現的效果:

2017-08-28-10mzdemo.gif

轉成gif圖質量不太好,實際效果比這個好很多,可以去運行demo看看實際效果。鏈接:https://github.com/DarkSherlock/DateViewWithRvDemo

我們可以看到這個效果,當recyclerview滑動的時候,這個控件里的那個時鐘指針
會跟著轉動,后面的文字也會跟著item的值 有一個滑進滑出動畫。

我本以為這是一個自定義View,然而當我用打開DDMS用HierachyView查看它的布局的時候。

VPGAME布局分析

我們可以看到他這個不是用一個自定義View來完成的,而是多個自定義View
來組合在RelativieLayout里來實現的。那么我們就可以借鑒他的這個思路。

Studio打開HierachyView的步驟:

ddms.png
dump.png

那么接下來就來分析下實現的思路:

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中看。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,908評論 2 372

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,618評論 25 708
  • 內容抽屜菜單ListViewWebViewSwitchButton按鈕點贊按鈕進度條TabLayout圖標下拉刷新...
    皇小弟閱讀 46,840評論 22 665
  • 發(fā)現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,151評論 4 61
  • 20170828-0903號 本周計劃 5理想的狀況 a制度改革了, 效益提高了 b學習升級了 幸福指數高 c游戲...
    芮涵琪雪閱讀 139評論 0 0
  • 忘了怎樣的開始 忘了怎樣的結束 沒有你的日子 總是下著漫天的雨 走不出從前 走不出回憶 無盡的情思 總是纏繞在過去...
    小金甲閱讀 155評論 0 0