自定義ListView實現任意View跑馬燈效果

自定義ListView實現任意View跑馬燈效果

標簽(空格分隔): 開源項目


看圖

話不多說,先來看下大圖效果吧,這里的GIF錄制有點渣,不過真實的跑出來的效果還是挺不錯的。



前言

最近項目中會加入一個新的需求,那就是把圖片和文字都實現那種跑馬燈的效果,之前想的不就是一個TextView的跑馬燈么,這個很好整的啊,并且開源的也是有這個的.這里給出這個TextView跑馬燈的開源地址.MarqueeView,但是這個并不符合我們的產品需求啊(需求如圖,整個View都要進行滾動),找了許久也沒找到自己能用的,看來只有自己去實現了。

目標想法

目標很簡單,就是只要實現這個效果,什么方式并沒有限制啊,但是過程就是比較復雜的,有時候甚至充滿了荊棘坎坷,這里想到的一種就是可不可以使用ListView,顯示幾個item通過方法去設定,然后通過一個線程來讓item進行滾動起來,并且實現循環,這樣不就是相當于實現了這個產品需求了么,想想也是哈,需求不就是這樣的么,當前可見的item是可以滾動的,而且也是循環的.哈哈看來自己的想法是可以的,接下來就看如何去實現了。

代碼實現

既然是對ListView的自定義(谷歌官方的ListView并沒有這個需求的相關函數和方法哈)

第一步:
    AutoScrollListView extends ListView{
        //然后重寫幾個構造方法
        public AutoScrollListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mLoopRunnable = new LoopRunnable();
        mScroller = new Scroller(context, new AccelerateInterpolator());
        mInnerAdapter = new InnerAdapter();
    }

    public AutoScrollListView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }
    }
第二步:

因為需要線程來控制滾動的時間,這里我們使用LoopRunnable(自定義的)

  /**
     * 線程管理類
     */
    class LoopRunnable implements Runnable {

        @Override
        public void run() {
            Log.i("AutoScrollListView", "run");
            mAnimating = true;  //線程啟動的時候設置動畫為ture
            View childAt = getChildAt(0);  //獲取到第一個子view
            //得到滑動的高度  也就是當前可滑動的item的高度
            int scrollHeight = childAt.getMeasuredHeight() + getDividerHeight();
            //然后進行滑動
            mScroller.startScroll(0, 0, 0, mScrollOrientation == SCROLL_UP ? scrollHeight : -scrollHeight);
            invalidate(); //重新繪制
        }

    }
    

//可以看到這里使用了 private Scroller mScroller;
這里就不詳細講解為啥使用Scroller(可以實現想要的效果滑動),這里附上一篇Scroller的講解的文章 Android中滑屏實現----手把手教你如何實現觸摸滑屏以及Scroller類詳解
還有兩點,就是防止泄露內存,這個時候我們需要在View依附Window和接觸Window的時候把線程移除

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        Log.i("AutoScrollListView", "onAttachedToWindow");
        //發送延時消息開始線程,也就是開始View的滾動
        postDelayed(mLoopRunnable, DALY_TIME);
        mAnimating = true;//設置動畫
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        Log.i("AutoScrollListView", "onDetachedFromWindow");
        removeCallbacks(mLoopRunnable);//移除線程防止泄露內存
    }

這個時候我們一個重要的問題就是怎么去測量我們的滾動視圖的高度
首先我們需要獲取到視圖的高度(因為視圖的高度我們上層并不能首先獲取到,因為我們要寫一個方法后者接口,留給使用者去實現然后后去高度),因此這個時候我們寫一個接口

 public interface AutoScroll {
        /**
         * 返回屏幕可見個數
         *
         * @return 可見個數
         */
        public int getVisiableCount();

        /**
         * 獲取條目高度
         *
         * @return 高度
         */
        public int getListItemHeight(Context context);
    }

然后在子類中去獲取到(我們的布局View的高度是可知的,也就是固定的),然后子類中如下后去(根據自己的UI需求制定的高度進行設置)

@Override
    public int getListItemHeight(Context context) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 100, context.getResources().getDisplayMetrics());
    }

然后我們通過獲取到了滾動視圖的高度之后,我們可以重寫onMeasure方法進行測量了。

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mAutoScroll && mOutterAdapter != null) { //如果是自動滾動和當前的adapter不為空
            AutoScroll autoScroll = (AutoScroll) mOutterAdapter; //
            //獲取到高度  也就是滾動的view的高度
            int height = autoScroll.getListItemHeight(getContext()) * autoScroll.getVisiableCount()
                    + (autoScroll.getVisiableCount() - 1) * getDividerHeight();
            //進行測量
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

當然我們還需要重寫computeScroll()方法(由父視圖調用用來請求子視圖根據偏移值 mScrollX,mScrollY重新繪制 ) 為了實現偏移控制,一般自定義View/ViewGroup都需要重載該方法.
其實移動一個view的簡單三部曲

  • 第一、調用Scroller實例去產生一個偏移控制(對應于startScroll()方法)
  • 第二、手動調用invalid()方法去重新繪制,剩下的就是在 computeScroll()里根據當前已經逝去的時間,獲取當前應該偏移的坐標(由Scroller實例對應的computeScrollOffset()計算而得),
  • 第三、當前應該偏移的坐標,調用scrollBy()方法去緩慢移動至該坐標處
  @Override
    public void computeScroll() {
        Log.i("AutoScrollListView", "computeScroll");

        // 如果返回true,表示動畫還沒有結束
        // 因為前面startScroll,所以只有在startScroll完成時 才會為false
        if (!mScroller.computeScrollOffset()) {  //沒有
            Log.i("AutoScrollListView", "compute finish");
            if (mAnimating) {
                Log.i("AutoScrollListView", "compute ignore runnable");
                return;
            }
            Log.i("AutoScrollListView", "compute send runnable");
            removeCallbacks(mLoopRunnable);  //移除
            postDelayed(mLoopRunnable, DALY_TIME); //重新發送
            mAnimating = true;
            preY = 0;
            
            //檢測當前的位置,防止位置錯亂
            checkPosition();
        } else {  //動畫沒有結束
            mAnimating = false;  //動畫標志置為false
            Log.i("AutoScrollListView", "compute not finish");
            int dY = mScroller.getCurrY() - preY;  //獲取到當前的y坐標
            ///**
            //* Scrolls the list items within the view by a specified number of pixels.
            //        *
            //* @param y the amount of pixels to scroll by vertically
            //        * @see #canScrollList(int)
            //*/
            //   public void scrollListBy(int y) {
            //      trackMotionScroll(-y, -y);
            //    } 
            //ListView的item滾動距離y
            ListViewCompat.scrollListBy(this, dY); //
            preY = mScroller.getCurrY();  //獲取到當前y
            invalidate();  //滾動完成之后重新繪制  
        }
    }

這里面有一個檢測位置防止錯亂的方法

   /**
     * 檢測位置信息
     */
    private void checkPosition() {
        if (!mAutoScroll) return;
        int targetPosition = -1; //初始化目標位置
        //第一個可見的view的位置
        int firstVisiblePosition = getFirstVisiblePosition();
        if (firstVisiblePosition == 0) {
            //如果當前的所在的位置是第一個可見的view的位置,也就是第一個item
            AutoScroll autoScroll = (AutoScroll) mInnerAdapter;
            targetPosition = mInnerAdapter.getCount() - autoScroll.getVisiableCount() * 2;
        }
        //最后一個item的位置
        int lastVisiblePosition = getLastVisiblePosition();
        if (lastVisiblePosition == getCount() - 1) {
            AutoScroll autoScroll = (AutoScroll) mOutterAdapter;
            targetPosition = autoScroll.getVisiableCount();
        }
        if (targetPosition >= 0 && firstVisiblePosition != targetPosition) {
            setSelection(targetPosition);
        }
    }

到此差不多就能完成了滾動,接下來就是一些優化了,比如長按點擊,點擊事件,設置自動滑動,停止自動滑動,設置滾動延時時間。
點擊事件(然后在相應的位置進行邏輯處理)

 @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            mMoveDistance = 0;
            mPreX = ev.getX();
            mPreY = ev.getY();
            mIgnoreLongClick = false;
        } else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
            //移動的距離
            mMoveDistance += (Math.abs(ev.getX() - mPreX) + Math.abs(ev.getY() - mPreY));
            mPreX = ev.getX();
            mPreY = ev.getY();
            //移動的距離大于指定值  并且當前的滾動還沒有完成
            if (mMoveDistance > 20 || !mScroller.isFinished()) {
                mIgnoreLongClick = true;
            }
            return true;
        } else if (ev.getAction() == MotionEvent.ACTION_UP
                || ev.getAction() == MotionEvent.ACTION_CANCEL) {
            if (mMoveDistance > 20 || !mScroller.isFinished()) {
                //取消長按時間
                ev.setAction(MotionEvent.ACTION_CANCEL);
            }
            mIgnoreLongClick = false;
        }
        return super.onTouchEvent(ev);
    }
    
    
        class InnerOnItemLongClickListener implements OnItemLongClickListener {

        @Override
        public boolean onItemLongClick(AdapterView<?> parent, View view,
                                       int position, long id) {
            return mOutterOnItemLongClickListener != null && mInnerAdapter != null && !mIgnoreLongClick && mOutterOnItemLongClickListener.onItemLongClick(parent, view, (int) mInnerAdapter.getItemId(position), id);
        }

    }

    //長按事件處理
      @Override
    public void setOnItemLongClickListener(OnItemLongClickListener listener) {
        if (mInnerOnItemLongClickListener == null) {
            mInnerOnItemLongClickListener = new InnerOnItemLongClickListener();
        }
        mOutterOnItemLongClickListener = listener;
        super.setOnItemLongClickListener(mInnerOnItemLongClickListener);
    }

自動和停止滾動

 /**
     * 開始自動滾動
     */
    public void startAutoScroll() {
        if (!mScroller.isFinished()) {
            mScroller.abortAnimation();
        }
        removeCallbacks(mLoopRunnable);
        mAnimating = false;
        post(mLoopRunnable);
    }

    /**
     * 停止自動滾動
     */
    public void stopAutoScroll() {
        if (!mScroller.isFinished()) {
            mScroller.abortAnimation();
        }
        removeCallbacks(mLoopRunnable);
        mAnimating = false;
    }

滾動延時時間

 /**
     * 設置延時事件
     *
     * @param dalyTime 延時事件   單位: ms
     */
    public static void setDalyTime(int dalyTime) {
        DALY_TIME = dalyTime;
    }

好了,代碼也就差不多這么多了,注釋也是比較容易理解的,因為是對于ListView的自定義,那么用法和ListViwe的使用時大致類似,只要注意兩個方法,手動實現AutoScrollListView.AutoScroll這個接口


    //獲取到當前滾動視圖的高度
    @Override
    public int getListItemHeight(Context context) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 100, context.getResources().getDisplayMetrics());
    }


    @Override
    public int getVisiableCount() {
        return 2;  //顯示滾動的item 的個數
    }

其他的都是類似ListView的用法了,這個地方代碼就不進行貼附了,這里直接附上github地址,有需要的和想要學習的可以直接到git上獲取,在此說明,小弟才疏學淺,并不能面面俱到,希望有問題互相交流,共同進步.對于效果圖可以見開頭的兩個gif圖片。

重要的事情說三遍
https://github.com/wuyinlei/MarqueeView

https://github.com/wuyinlei/MarqueeView

https://github.com/wuyinlei/MarqueeView

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

推薦閱讀更多精彩內容