1. 程式人生 > >Android自定義View(LimitScrollerView-仿天貓廣告欄上下滾動效果)

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

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

    這裡寫圖片描述 這裡寫圖片描述

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

1、分析

  遇到這種展示效果,我們第一反應就會想到兩個控制元件:ListViewScrollerView

ListView可以展示條目,只需要重寫下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下載平臺太流氓