自定義Android圖片輪播控件

說到輪播圖,想必大家都不陌生。常見的APP都會有一個圖片輪播的區域。之前使用過輪播圖,最近項目又一次用到了,就把原來的代碼照搬過來,結果由於數據結構的差異和照搬使有些代碼的疏忽,調試了很久才讓原本已經OK的輪播圖再次運轉起來。所以決定將這個輪播圖模塊化,做成一個可以通用的組件,方便以後使用。

通過總結網絡上各位大神的思路,這裏本着學習的態度自定義一個可以無限循環輪播,並且支持手勢滑動的輪播圖控件。

輪播效果

自定義控件###

自定義View的實現方式大概可以分爲三種,自繪控件、組合控件、以及繼承控件。這裏的實現方式是用第二種方式,組合控件。

組合控件,顧名思義,就是利用Android原生控件通過xml文件佈局重定義爲自己所需要的UI,然後就此佈局文件的控件實現自身需要的功能。

  • 定義佈局文件

carousel_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <android.support.v4.view.ViewPager
        android:id="@+id/gallery"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:unselectedAlpha="1"></android.support.v4.view.ViewPager>
    <LinearLayout
        android:id="@+id/CarouselLayoutPage"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center|bottom"
        android:gravity="center"
        android:orientation="horizontal"
        android:padding="10dip"></LinearLayout>
</FrameLayout>

這裏定義一個Viewpager(用於放置圖片),並在下方定義一個橫向的LinearLayout(用於放置隋圖滾動的小圓點)

  • 加載佈局文件到View

接下來的步驟,就是將這個xml佈局文件結合到需要實現的自定義View當中。

一般,我們在實現自定義控件時,都會繼承某一個View(比如LinearLayout,Button或者直接就是View及ViewGroup)。
然後,就需要實現其相應的構造方法,構造方法一般會有3個

    public BannerView(Context context) {
        super(context);
        this.context = context;
    }
    public BannerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
    }
    public BannerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
    }

建議是這三個構造方法都實現一下。原因可以看看這篇文章
爲什麼要實現全部三個構造方法

加載佈局文件到當前自定義view中

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        View view = LayoutInflater.from(context).inflate(R.layout.carousel_layout, null);
        this.viewPager = (ViewPager) view.findViewById(R.id.gallery);
        this.carouselLayout = (LinearLayout) view.findViewById(R.id.CarouselLayoutPage);
        IndicatorDotWidth = ConvertUtils.dip2px(context, IndicatorDotWidth);
        this.viewPager.addOnPageChangeListener(this);
        addView(view);
    }

可以在onFinishInflate這個方法中,加載上述佈局文件,並添加到當前view當中.這裏這個關於加載view的邏輯,放到構造函數中實現也是可以的。至於放在兩個地方的區別我們可以從API文檔看出

protected void onFinishInflate ()
Added in API level 1
Finalize inflating a view from XML. This is called as the last phase of inflation, after all child views have been added.
Even if the subclass overrides onFinishInflate, they should always be sure to call the super method, so that we get called.

大概意思就是這個方法會在xml文件所有內容“填充”完成後觸發。說白了就是,如果在這個方法裏實現了xml的加載,那麼在Activity中用java代碼new出一個當前自定義View對象時,將沒有內容(因爲new對象的時候,執行了構造方法,而構造方法中沒有加載內容)。其實,大部分情況下,自定義的控件,都會按照完全路徑放到xml佈局文件中中使用(如本文使用的情況),不會說在代碼中new一個,所以,這個addview(view)的邏輯在哪裏實現,可以根據實際情況決定(當然,這只是我一時的理解)。

  • 初始化

接下來,就需要做一些初始化的工作。

首先可以根據,內容可以繪製出輪播圖指示器(即隨圖滑動的小圓點)

 carouselLayout.removeAllViews();
        if (adapter.isEmpty()) {
            return;
        }
        int count = adapter.getCount();
        showCount = adapter.getCount();
        //繪製切換小圓點
        for (int i = 0; i < count; i++) {
            View view = new View(context);
            if (currentPosition == i) {
                view.setPressed(true);
                LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
                        IndicatorDotWidth + ConvertUtils.dip2px(context, 3),
                        IndicatorDotWidth + ConvertUtils.dip2px(context, 3));
                params.setMargins(IndicatorDotWidth, 0, 0, 0);
                view.setLayoutParams(params);
            } else {
                LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(IndicatorDotWidth, IndicatorDotWidth);
                params.setMargins(IndicatorDotWidth, 0, 0, 0);
                view.setLayoutParams(params);
            }
            view.setBackgroundResource(R.drawable.carousel_layout_dot);
            carouselLayout.addView(view);
        }

這裏看一下這個carousel_layout_dot.xml 佈局文件

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_selected="true">
        <shape  android:shape="oval">
            <solid android:color="#eb6100"></solid>
        </shape>
    </item>
    <item>
        <shape  android:shape="oval">
            <solid android:color="@android:color/transparent"></solid>
            <stroke android:width="1dp" android:color="#FFF"> </stroke>
        </shape>
    </item>
</selector>

做過Button點擊效果的同學,對這種模式一定很熟悉。通過view的當前狀態,設置不同的色值,可以呈現豐富的視覺效果。這裏對小圓點也是一樣,選中項設置了高亮的顏色。
通過修改這個文件,可以實現自定義小圓點的效果。列如可以將圓點修改爲橫線,或者將小圓點切換爲圖片等,這完全可以根據實際需求決定。

這裏使用到了ViewPager,那麼Adapter是必不可少了了。這裏主要需要實現其selected方法

  @Override
    public void onPageSelected(int position) {
        currentPosition = position;
        int count = carouselLayout.getChildCount();
        for (int i = 0; i < count; i++) {
            View view = carouselLayout.getChildAt(i);
            if (position % showCount == i) {
                view.setSelected(true);
                //當前位置的點要繪製的較大一點
                LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
                        IndicatorDotWidth + ConvertUtils.dip2px(context, 3),
                        IndicatorDotWidth + ConvertUtils.dip2px(context, 3));
                params.setMargins(IndicatorDotWidth, 0, 0, 0);
                view.setLayoutParams(params);
            } else {
                view.setSelected(false);
                LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(IndicatorDotWidth, IndicatorDotWidth);
                params.setMargins(IndicatorDotWidth, 0, 0, 0);
                view.setLayoutParams(params);
            }
        }
    }

  • 輪播實現

其實,輪播的實現,思路很簡單,通過一個獨立的線程,不斷更改當前位置position,然後使用handler在UI線程中通過ViewPager的SetCurrentItem(position)方法即可實現圖片輪播效果。

這裏有三點需要注意:
1.選擇合適的定時器在適當的位置開始定時任務
2.當用戶手指滑動時,如何處理獨立線程中對當前位置的更改
3.若要實現無限循環滑動時,滑到第一頁和最後一頁時如何處理

帶着這三個問題,可以看一下完整的代碼(這部分代碼拆開之後敘述起來會有點亂,所以就給出全部代碼)

public class BannerView extends FrameLayout implements ViewPager.OnPageChangeListener {
    private Context context;
    private static final int MSG = 0X100;
    /**
     * 輪播圖最大數
     */
    private int totalCount = Integer.MAX_VALUE;
    /**
     * 當前banner需要顯式的數量
     */
    private int showCount;
    private int currentPosition = 0;
    private ViewPager viewPager;
    private LinearLayout carouselLayout;
    private Adapter adapter;
    /**
     * 輪播切換小圓點寬度默認寬度
     */
    private static final int DOT_DEFAULT_W = 5;
    /**
     * 輪播切換小圓點寬度
     */
    private int IndicatorDotWidth = DOT_DEFAULT_W;
    /**
     * 用戶是否干預
     */
    private boolean isUserTouched = false;
    /**
     * 默認的輪播時間
     */
    private static final int DEFAULT_TIME = 3000;
    /**
     * 設置輪播時間
     */
    private int switchTime = DEFAULT_TIME;
    /**
     * 輪播圖定時器
     */
    private Timer mTimer = new Timer();
    
    public BannerView(Context context) {
        super(context);
        this.context = context;
    }
    public BannerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
    }
    public BannerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
    }
    private void init() {
        viewPager.setAdapter(null);
        carouselLayout.removeAllViews();
        if (adapter.isEmpty()) {
            return;
        }
        int count = adapter.getCount();
        showCount = adapter.getCount();
        //繪製切換小圓點
        for (int i = 0; i < count; i++) {
            View view = new View(context);
            if (currentPosition == i) {
                view.setPressed(true);
                LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
                        IndicatorDotWidth + ConvertUtils.dip2px(context, 3),
                        IndicatorDotWidth + ConvertUtils.dip2px(context, 3));
                params.setMargins(IndicatorDotWidth, 0, 0, 0);
                view.setLayoutParams(params);
            } else {
                LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(IndicatorDotWidth, IndicatorDotWidth);
                params.setMargins(IndicatorDotWidth, 0, 0, 0);
                view.setLayoutParams(params);
            }
            view.setBackgroundResource(R.drawable.carousel_layout_dot);
            carouselLayout.addView(view);
        }
        viewPager.setAdapter(new ViewPagerAdapter());
        viewPager.setCurrentItem(0);
        this.viewPager.setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                    case MotionEvent.ACTION_MOVE:
                    //有用戶滑動事件發生
                        isUserTouched = true;
                        break;
                    case MotionEvent.ACTION_UP:
                        isUserTouched = false;
                        break;
                }
                return false;
            }
        });
        //以指定週期和岩石開啓一個定時任務
        mTimer.schedule(mTimerTask, switchTime, switchTime);
    }

    //設置adapter,這個方法需要再使用時設置
    public void setAdapter(Adapter adapter) {
        this.adapter = adapter;
        if (adapter != null) {
            init();
        }
    }
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        View view = LayoutInflater.from(context).inflate(R.layout.carousel_layout, null);
        this.viewPager = (ViewPager) view.findViewById(R.id.gallery);
        this.carouselLayout = (LinearLayout) view.findViewById(R.id.CarouselLayoutPage);
        IndicatorDotWidth = ConvertUtils.dip2px(context, IndicatorDotWidth);
        this.viewPager.addOnPageChangeListener(this);
        addView(view);
    }
    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
       
    }
    @Override
    public void onPageSelected(int position) {
        currentPosition = position;
        int count = carouselLayout.getChildCount();
        for (int i = 0; i < count; i++) {
            View view = carouselLayout.getChildAt(i);
            if (position % showCount == i) {
                view.setSelected(true);
                //當前位置的點要繪製的較大一點,高亮顯示
                LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
                        IndicatorDotWidth + ConvertUtils.dip2px(context, 3),
                        IndicatorDotWidth + ConvertUtils.dip2px(context, 3));
                params.setMargins(IndicatorDotWidth, 0, 0, 0);
                view.setLayoutParams(params);
            } else {
                view.setSelected(false);
                LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(IndicatorDotWidth, IndicatorDotWidth);
                params.setMargins(IndicatorDotWidth, 0, 0, 0);
                view.setLayoutParams(params);
            }
        }
    }
    @Override
    public void onPageScrollStateChanged(int state) {
        
    }
    class ViewPagerAdapter extends PagerAdapter {
        @Override
        public int getCount() {
            return totalCount;
        }
        @Override
        public boolean isViewFromObject(View view, Object object) {
            return view == object;
        }
        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            position %= showCount;
            View view = adapter.getView(position);
            container.addView(view);
            return view;
        }
        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            container.removeView((View) object);
        }
        @Override
        public int getItemPosition(Object object) {
            return super.getItemPosition(object);
        }
        @Override
        public void finishUpdate(ViewGroup container) {
            super.finishUpdate(container);
            int position = viewPager.getCurrentItem();
            if (position == 0) {
                position = showCount;
                viewPager.setCurrentItem(position, false);
            } else if (position == totalCount - 1) {
                position = showCount - 1;
                viewPager.setCurrentItem(position, false);
            }
        }
    }
    private TimerTask mTimerTask = new TimerTask() {
        @Override
        public void run() {
        //用戶滑動時,定時任務不響應
            if (!isUserTouched) {
                currentPosition = (currentPosition + 1) % totalCount;
                handler.sendEmptyMessage(MSG);
            }
        }
    };
    public void cancelTimer() {
        if (this.mTimer != null) {
            this.mTimer.cancel();
        }
    }
    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (msg.what == MSG) {
                Log.e("Pos", "the position is " + currentPosition);
                if (currentPosition == totalCount - 1) {
                    viewPager.setCurrentItem(showCount - 1, false);
                } else {
                    viewPager.setCurrentItem(currentPosition);
                }
            }
        }
    };
    /**
    *可自定義設置輪播圖切換時間,單位毫秒
     * @param switchTime millseconds
     */
    public void setSwitchTime(int switchTime) {
        this.switchTime = switchTime;
    }
    /**
     * @param indicatorDotWidth
     */
    public void setIndicatorDotWidth(int indicatorDotWidth) {
        IndicatorDotWidth = indicatorDotWidth;
    }
    public interface Adapter {
        boolean isEmpty();
        View getView(int position);
        int getCount();
    }
}

這裏將totalCount的值設置爲一個很大的值(這個貌似是實現無限輪播的一個取巧的方法,網上大部分實現都是這樣),並將這個值作爲ViewPager的個數。每次位置更改時,通過取餘數,避免了數組越界,同時巧妙的實現了無限循環輪播效果。

  • 測試效果
public class BannerViewActivity extends Activity {
    private ListView listview;
    private List<String> datas;
    private List<String> banners;
    private View headView;
    private BannerView carouselView;
    private Context mContext;
    private LayoutInflater mInflater;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mContext = this;
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.carouse_layout);
        InitView();

    }

    private void InitView() {
        InitDatas();
        mInflater = LayoutInflater.from(this);
        headView = mInflater.inflate(R.layout.carouse_layout_header, null);
        carouselView = (BannerView) headView.findViewById(R.id.CarouselView);
        //這裏考慮到不同手機分辨率下的情況
        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT, ConvertUtils.dip2px(this, 200));
        carouselView.setLayoutParams(params);
        carouselView.setSwitchTime(2000);
        carouselView.setAdapter(new MyAdapter());
        listview = V.f(this, R.id.list);
        listview.addHeaderView(headView);
        ArrayAdapter<String> myAdapter = new ArrayAdapter<String>(mContext, android.R.layout.simple_expandable_list_item_1, datas);
        listview.setAdapter(myAdapter);

    }

    /**
     * 設定虛擬數據
     */
    private void InitDatas() {
        datas = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            datas.add("the Item is " + i);
        }
        //圖片來自百度
        banners = Arrays.asList("http://img1.imgtn.bdimg.com/it/u=2826772326,2794642991&fm=15&gp=0.jpg",
                "http://img15.3lian.com/2015/f2/147/d/39.jpg",
                "http://img1.3lian.com/2015/a1/107/d/65.jpg",
                "http://img1.3lian.com/2015/a1/93/d/225.jpg",
                "http://img1.3lian.com/img013/v4/96/d/44.jpg");
    }

//這裏可以按實際需求做調整,在適當的位置可停止輪播,節省資源
    @Override
    protected void onPause() {
        super.onPause();
        if (carouselView != null) {
            carouselView.cancelTimer();
        }
    }

    private class MyAdapter implements BannerView.Adapter {

        @Override
        public boolean isEmpty() {
            return banners.size() > 0 ? false : true;
        }

        @Override
        public View getView(final int position) {
            View view = mInflater.inflate(R.layout.item, null);
            ImageView imageView = (ImageView) view.findViewById(R.id.image);           Picasso.with(mContext).load(banners.get(position)).into(imageView);
            view.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    T.showShort(mContext,"Now is "+position);
                }
            });
            return view;
        }

        @Override
        public int getCount() {
            return banners.size();
        }
    }
}

carouse_layout_header.xml

<?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="match_parent"
    android:orientation="vertical">
    <com.example.dreamwork.activity.CarouselView.BannerView
        android:id="@+id/CarouselView"
        android:layout_width="match_parent"
        android:layout_height="match_parent">   </com.example.dreamwork.activity.CarouselView.BannerView>
</LinearLayout>

這裏BannerViewActivity的佈局文件就是一個ListView,這裏代碼就不在貼出,將BannView自定義控件作爲其頭部添加到ListView上即可。還可以很靈活的設置輪播圖的切換時間,最後設置其Adapter即可。當然,這裏 很簡單的自定義了一個List存放圖片地址,作爲測試。實際開發中,可選取接口返回的後臺配置的圖片地址。

這裏在說一下關於這個輪播圖高度的設置,Android手機的碎片化,導致現在市場上各種分辨率手機都存在,適配起來就顯得特別糾結,這裏的處理方法很值得借鑑

//這裏考慮到不同手機分辨率下的情況
        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT, ConvertUtils.dip2px(this, 200));
        carouselView.setLayoutParams(params);

dip2px,顧名思義就是根據當前手機分辨率將dp轉換爲px,這類通用的方法,想必大家都很熟悉,這裏就是使用這個方法,設定高度爲200dp,然後按照不同手機的分辨率再去分配,這中思路不但在這裏,很多地方都可以使用。


好了,這樣定義一個輪播圖控件後,以後使用時只需要在xml文件中定義BannerView,然後根據業務數據設置其Adapter即可,不必在重新複製粘貼一大堆代碼;關於這個圖片輪播控件的學習就到這裏。

鏈接:https://www.jianshu.com/p/9996a079a3fc
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章