Android自定義View精品(LimitScrollerView-仿天貓廣告欄上下滾動效果)

版權聲明:本文爲openXu原創文章【openXu的博客】,未經博主允許不得以任何形式轉載


  最近項目中需要在首頁做一個跑馬燈類型的廣告欄,最後上面決定仿照天貓的廣告欄效果做(中間部位),效果圖如下(右邊是我們的效果):

    這裏寫圖片描述 這裏寫圖片描述

  天貓上搶購那一欄的廣告條可以向上滾動,每次展示一條廣告,展示一定時間後,第二條廣告從下往上頂起。但項目經理說我們需要一次展示兩條廣告,廣告每次停留5秒,然後向上滾動,滾動的過程持續1.5秒。要求還真多,想着這麼多要求說不定什麼時候又得改了,每次展示三條廣告,需要停留8秒,滾動持續3秒,那就死球了。所以乾脆自己封裝一個通用的,你愛咋改咋改…

1、分析

  遇到這種展示效果,我們第一反應就會想到兩個控件:ListViewScrollerViewListView可以展示條目,只需要重寫下onMeasure就能達到一次只顯示n條的效果,但是要自動滾動、滾動時間限制貌似有點困難;ScrollerView可以動態的往裏面添加指定數量的條目,可以實現自動滾動,但是滾動持續時間不可控制。想到這裏,頓時絕望、一頭霧水,既然系統自帶的控件實現起來有困難,那就自己造。

  經過一小陣思索,突然靈光一現,如下:
    

  既然要實現滾動的效果,肯定有一個容器容納當前展示的條目,還有一個容器在下面作爲預備展示的容器,需要展示幾條就動態的向容器中添加指定數量的子條目;最外層是一個大的容器,如果將他的高度設置爲小容器的高度,即可實現遮擋預備容器的目的;滾動可使用動畫集合,讓兩個容器同時向上滾動;滾動結束後,馬上讓被頂上去的容器復位到預備位置;這裏需要兩個引用指向當前展示的容器和預備容器,當動畫結束之後,這兩個引用需要互換。經過一段時間停留後重複上述步驟即可。

  思路是有了,要實現起來得考慮細節了。最外層用什麼包裹?繼承ViewGroup?太麻煩(得重寫onLayout計算麻煩),我要實現的效果就是裏面的兩個容器在開始的時候能夠垂直向下排列好即可,所以最簡單的就是LinearLayout,裏面的容器就不用說了,子條目都是垂直向下排列,肯定也是LinearLayout。是直接繼承LinearLayout後動態向裏面添加兩個LinearLayout?還是使用組合控件?考慮到之前博客中自定義控件系列沒有講到組合控件,就這個機會寫個小demo填充空白。那下面就開始了(不要嫌我囉嗦,大神們如果覺得太easy請口下留人,這些實現思路我想對很多人還是有幫助的)

2、定義組合控件佈局

  組合控件,顧名思義就是由很多個控件組合而成,這裏第一步就是定義好這些控件組合:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">
    <LinearLayout
        android:id="@+id/ll_content1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"/>
    <LinearLayout
        android:id="@+id/ll_content2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"/>
</LinearLayout>

3、繼承最外層控件

  上面的控件組合定義好之後,下面就需要用一個類去形容他,那這個類就是組合控件了。用什麼形容他合適呢?那就看控件組合最外層用的是什麼,這裏最外層是LinearLayout,那就定義一個類繼承LinearLayout,然後覆蓋其構造方法,使用LayoutInflater將控件組合掛在自己身上,並完成容器內控件的初始化:

public class LimitScrollerView extends LinearLayout{
    private LinearLayout ll_content1, ll_content2;  //展示容器 和 預備容器
    public LimitScrollerView(Context context) {
        this(context, null);
    }

    public LimitScrollerView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public LimitScrollerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        //將控件組合掛載到自己身上
        LayoutInflater.from(context).inflate(R.layout.limit_scroller, this, true);
        ll_content1 = (LinearLayout) findViewById(R.id.ll_content1);
        ll_content2 = (LinearLayout) findViewById(R.id.ll_content2);
    }
}

4、自定義屬性

  爲了達到通用的效果,自定義屬性是必不可少的(自定義屬性詳解請參見: Android自定義View(二、深入解析自定義屬性))。這裏需要定義的是:一次顯示的條目數量、滾動動畫持續時間、停留時間,如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="LimitScroller">
        <!--顯示的條目數量-->
        <attr name="limit" format="integer" />
        <!--滾動速度,比如3000,滾動時間會持續3秒鐘-->
        <attr name="durationTime" format="integer" />
        <!--滾動間隔,比如5000,滾動完成後停留5秒繼續滾動-->
        <attr name="periodTime" format="integer" />      
    </declare-styleable>
</resources>

  然後就是使用這個自定義的控件了,在使用的時候可以指定屬性值:

<com.openxu.lc.LimitScrollerView
    android:id="@+id/limitScroll"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    openxu:limit="2"
    openxu:durationTime="200"
    openxu:periodTime="5000"/>

  最後需要在控件初始化的時候,獲取到屬性值:

private void init(Context context, AttributeSet attrs){
    LayoutInflater.from(context).inflate(R.layout.limit_scroller, this, true);
    ll_content1 = (LinearLayout) findViewById(R.id.ll_content1);
    ll_content2 = (LinearLayout) findViewById(R.id.ll_content2);
    if(attrs!=null){
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.LimitScroller);
        limit = ta.getInt(R.styleable.LimitScroller_limit, 1);
        durationTime = ta.getInt(R.styleable.LimitScroller_durationTime, 1000);
        periodTime = ta.getInt(R.styleable.LimitScroller_periodTime, 1000);
        ta.recycle();  //注意回收
        Log.v(TAG, "limit="+limit);
        Log.v(TAG, "durationTime="+durationTime);
        Log.v(TAG, "periodTime="+periodTime);
    }
}

5、重寫onMeasure

  由於每次只能顯示需要展示的容器,遮蓋預備容器,所以只能設置整個高度的一半,這裏使用一個小技巧,由於最外層是LinearLayout,並且是豎直向下的,自帶的LinearLayoutonMeasure()方法完成之後組合控件的高度就是兩個子容器的高度了,所以直接調用super.onMeasuer()之後,再設置高度爲getMeasureHeight()/2即可(onMeasure()詳解請移步: Android自定義View(三、深入解析控件測量onMeasure)

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    //設置高度爲整體高度的一般,以達到遮蓋預備容器的效果
    setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight()/2);
    //此處記下控件的高度,此高度就是動畫執行時向上滾動的高度
    scrollHeight = getMeasuredHeight();
}

6、數據適配器

  上面的步驟完成之後,展示的框架已經搭好了,但是運行之後是看不到控件的,因爲容器中還沒有子條目,整個控件的高度是0,下面就開始綁定數據、動態添加子條目。由於大家對ListView的數據填充模式已經很熟練,所以這裏模仿Adapter的方式:

/**數據適配器*/
interface LimitScrllAdapter{
    public int getCount();
    public View getView(int index);
}
private LimitScrllAdapter adapter;

public void setDataAdapter(LimitScrllAdapter adapter){
    this.adapter = adapter;
    handler.sendEmptyMessage(MSG_SETDATA);
}

  在Activity請求數據完畢後,爲適配器添加數據,這裏需要實現LimitScrollAdapter的兩個抽象方法,使用方式和ListView一樣,這裏就不贅述:

class MyLimitScrllAdapter implements LimitScrollerView.LimitScrllAdapter{

    private List<DataBean> datas;
    public void setDatas(List<DataBean> datas){
        this.datas = datas;
        //API:2、開始滾動
        limitScroll.startScroll();
    }
    @Override
    public int getCount() {
        return datas==null?0:datas.size();
    }

    @Override
    public View getView(int index) {
        View itemView = LayoutInflater.from(MainActivity.this).inflate(R.layout.limit_scroller_item, null, false);
        ImageView iv_icon = (ImageView)itemView.findViewById(R.id.iv_icon);
        TextView tv_text = (TextView)itemView.findViewById(R.id.tv_text);

        //綁定數據
        DataBean data = datas.get(index);
        itemView.setTag(data);
        iv_icon.setImageResource(data.getIcon());
        tv_text.setText(data.getText());
        return itemView;
    }
}

7、動態添加子條目

  數據有了,子條目通過adapter.getView()獲取,那什麼時候向容器中添加條目呢?第一次肯定是兩個容器中都得添加,向上滾動之後,有一個容器被定到上面,然後復位到預備位置了,但是他的數據還是之前的數據,所以每次動畫結束之後得爲預備容器更新新的子條目:

private void boundData(boolean first){
    if(adapter==null || adapter.getCount()<=0)
        return;
    if(first){
        //第一次綁定數據,需要爲兩個容器添加子條目
        boundData = true;
        ll_now.removeAllViews();
        for(int i = 0; i<limit; i++){
            if(dataIndex>=adapter.getCount())
                dataIndex = 0;
            View view = adapter.getView(dataIndex);
            ll_now.addView(view);
            dataIndex ++;
        }
    }

    //每次動畫結束之後,爲預備容器添加新條目
    ll_down.removeAllViews();
    for(int i = 0; i<limit; i++){
        if(dataIndex>=adapter.getCount())
            dataIndex = 0;
        View view = adapter.getView(dataIndex);
        ll_down.addView(view);
        dataIndex ++;
    }
}

8、滾動動畫

  什麼時候開始動畫?這是個需要考慮的問題,沒有數據的時候肯定不需要吧?有數據之後,activity不可見了也不需要動畫,所以這裏需要提供接口讓activity中控制,Activity中請求完數據之後調用此接口開始動畫,在onStart()中也需要調用開啓動畫,在onStop()中調用停止動畫的接口。動畫開啓之後會無限循環的執行,每次動畫執行完畢後通過Handler發送一個延遲指定時間的消息,停留指定時間後,handler收到消息後又調用startAnimation()方法:

private final int MSG_SETDATA = 1;
private final int MSG_SCROL = 2;
private Handler handler = new Handler(){
    @Override
    public void handleMessage(Message msg) {
        if(msg.what == MSG_SETDATA){
            boundData(true);
        }else if(msg.what == MSG_SCROL){
            //繼續動畫
            startAnimation();
        }
    }
};
private void startAnimation(){
    if(isCancel)
        return;
    //當前展示的容器,從當前位置(0),向上滾動scrollHeight
    ObjectAnimator anim1 = ObjectAnimator.ofFloat(ll_now, "Y",ll_now.getY(), ll_now.getY()-scrollHeight);
    //預備容器,從當前位置,向上滾動scrollHeight
    ObjectAnimator anim2 = ObjectAnimator.ofFloat(ll_down, "Y",ll_down.getY(), ll_down.getY()-scrollHeight);
    AnimatorSet animSet = new AnimatorSet();
    animSet.setDuration(durationTime);
    animSet.playTogether(anim1, anim2);
    animSet.addListener(new Animator.AnimatorListener() {
        @Override
        public void onAnimationStart(Animator animation) {
        }
        @Override
        public void onAnimationEnd(Animator animation) {
            //滾動結束後,now的位置變成了-scrollHeight,這時將他移動到最底下
            ll_now.setY(scrollHeight);
            //down的位置變爲0,也就是當前看見的
            ll_down.setY(0);
            //引用交換
            LinearLayout temp = ll_now;
            ll_now = ll_down;
            ll_down = temp;
            //給不可見的控件綁定新數據
            boundData(false);
            //停留指定時間後,重複動畫
            handler.sendEmptyMessageDelayed(MSG_SCROL, periodTime);
        }
        @Override
        public void onAnimationCancel(Animator animation) {
        }
        @Override
        public void onAnimationRepeat(Animator animation) {
        }
    });
    animSet.start();
}
/**
 * 2、開始滾動
 * 應該在兩處調用此方法:
 * ①、Activity.onStart()
 * ②、MyLimitScrllAdapter.setDatas()
 */
public void startScroll(){
    if(adapter==null||adapter.getCount()<=0)
        return;
    if(!boundData){
        handler.sendEmptyMessage(MSG_SETDATA);
    }
    isCancel = false;
    handler.sendEmptyMessageDelayed(MSG_SCROL, periodTime);
}
/**
 * 3、停止滾動
 * 當在Activity不可見時,在Activity.onStop()中調用
 */
public void cancel(){
    isCancel = true;
}

9、條目點擊事件

  在組合控件中寫一個條目點擊事件的接口,在動態添加子條目時,爲子條目添加點擊事件,通過view.getTag()(數據適配器綁定數據時,將數據對象設置給子條目view)將當前點擊的子條目對應的數據對象返回即可:

interface OnItemClickListener{
    public void onItemClick(Object obj);
}
private OnItemClickListener clickListener;
/**
 * 向容器中添加子條目
 * @param first
 */
private void boundData(boolean first){
    if(adapter==null || adapter.getCount()<=0)
        return;
    if(first){
        //第一次綁定數據,需要爲兩個容器添加子條目
        boundData = true;
        ll_now.removeAllViews();
        for(int i = 0; i<limit; i++){
            if(dataIndex>=adapter.getCount())
                dataIndex = 0;
            View view = adapter.getView(dataIndex);

            //設置點擊監聽
            view.setClickable(true);
            view.setOnClickListener(this);

            ll_now.addView(view);
            dataIndex ++;
        }
    }

    //每次動畫結束之後,爲預備容器添加新條目
    ll_down.removeAllViews();
    for(int i = 0; i<limit; i++){
        if(dataIndex>=adapter.getCount())
            dataIndex = 0;
        View view = adapter.getView(dataIndex);
        //設置點擊監聽
        view.setClickable(true);
        view.setOnClickListener(this);
        ll_down.addView(view);
        dataIndex ++;
    }
}

@Override
public void onClick(View v) {
    if(clickListener!=null){
        Object obj = v.getTag();
        clickListener.onItemClick(obj);
    }
}

  好了,該考慮的基本上都有了,看看最終的效果:

        這裏寫圖片描述

注意:修復一處bug,生命週期方法可能導致消息反覆發送,所以在發送滾動消息時應該移除handler中滾動的消息,否則會出現滾動動畫錯亂。

handler.sendEmptyMessageDelayed(MSG_SCROL, periodTime);

改爲

handler.removeMessages(MSG_SCROL);   //先清空所有滾動消息,避免滾動錯亂
handler.sendEmptyMessageDelayed(MSG_SCROL, periodTime);

喜歡請點贊,no愛請勿噴~O(∩_∩)O謝謝

##源碼下載:

http://download.csdn.net/detail/u010163442/9690822 CSDN下載平臺太流氓

https://github.com/openXu/LimitScrollerView

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章