從慕課網上學了一門叫做“不一樣的自定義實現輪播圖效果”的課程,感覺實用性較強,而且循序漸進,很適合初學者。在此對該課程做一個小小的筆記。
實現輪播思路:
1、一般輪播圖是由一組圖片和底部輪播圓點組成,要想組成這種圓點在圖片之上的效果,首先我們應當想到FrameLayout布局。最外層應該是一個FrameLayout布局,將輪播圖片和圓點添加到這個布局中,並且需要設置圓點的位置在下部正中間(當然視需求而定)。
2、輪播圖片組應該是一個ViewGroup,我們需要對該ViewGroup進行測量、布局等過程。
3、圓點集合應該是一個LinearLayout。
4、要想實現自動輪播效果,可以結合Timer、TimerTask以及Handler的配合使用。
5、要想實現手動滑動輪播圖,可以利用Scroller。
6、實現底部圓點隨圖片切換變化的過程,實現每張圖片的點擊事件。
首先我們來實現圖片組ViewGroup的繪制過程。實現思路很簡單,對於整個ViewGroup,其高度應該是子View的高度(假設我們的輪播圖效果類似於淘寶首頁的頭部效果),寬度應該是所有子View的寬度之和。抓住這個方向,不難實現測量過程。
#ImaBannerViewGroup @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); child = getChildCount(); if (0 == child) { setMeasuredDimension(0, 0); } else { measureChildren(widthMeasureSpec, heightMeasureSpec); View view = getChildAt(0); childHeight = view.getMeasuredHeight(); childWidth = view.getMeasuredWidth(); int height = childHeight; int width = childWidth * child; setMeasuredDimension(width, height); } }
從上面代碼可以看出,我們假定每個子View的寬度都和第一個子View的寬度相等,當后面的圖片寬度小於第一張圖時就會出現留白現象。一般輪播圖每個子View的寬度都是整個屏幕的寬度,我們可以寫一個全局的靜態變量,然后給該變量賦值為屏幕寬度,在需要的時候進行調用就好。
然后重寫onLayout()方法。該方法遍歷子視圖,設置每個子視圖的位置,即在ViewGroup中水平依次平鋪。該例子實際上是不用考慮y方向的,x方向后子視圖應該是在前一個子視圖的位置加上前一個子視圖的寬度。
protected void onLayout(boolean change, int left, int top, int right, int bottom) { if (change) {//當Viewgroup發生改變時為true int leftMargin = 0; for (int i = 0; i < child; i++) { View view = getChildAt(i); view.layout(leftMargin, 0, leftMargin + childWidth, childHeight); leftMargin += childWidth; } } }
接下來就是實現手動輪播了。可以使用ScrollTo,ScrollBy,也可以使用Scroller。既然需要實現手動輪播,自然那涉及到分發過程,也就是需要重寫onInterceptTouchEvent()返回true,並重寫onTouchEvent()方法。
/** * 1)我們在滑動屏幕圖片的過程中,其實就是我們自定義ViewGroup的子視圖的移動過程,那么,我們只需要知道滑動之前橫坐標和滑動之后的橫坐標,此時,我們就可以 * 求出此次過程中我們滑動的距離,我們利用scrollBy方法實現圖片的滑動,所以,此時我們需要兩個值,移動之前和移動之后的橫坐標。 * 2)在我們第一次按下的那一瞬,此時的移動之前和移動之后的距離是相等的,也就是我們此時按下那一瞬的那個點的橫坐標的值 * 3)不斷滑動過程會不斷調用ACTION_MOVE,所以需要保存移動 值。 * 4)當我們抬起那一瞬,需要知道具體滑到哪一頁 * * @param event * @return 返回true的目的是告訴我們該ViewGroup容器的父View我們已經處理好了該事件 */ @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: isClick = true; if (!scroller.isFinished()) { scroller.abortAnimation(); } x = (int) event.getX(); stopAuto(); break; case MotionEvent.ACTION_MOVE: int moveX = (int) event.getX(); int distance = moveX - x; if (Math.abs(distance) > filter){ isClick = false; } scrollBy(-distance, 0); x = moveX; break; case MotionEvent.ACTION_UP: startAuto(); int scrollX = getScrollX(); index = (scrollX + childWidth / 2) / childWidth; if (index < 0) { index = 0; } else if (index > child - 1) { index = child - 1; } int dx = index * childWidth - scrollX; if (isClick) { listener.clickImageIndex(index); } else { /** * public void startScroll(int startX, int startY, int dx, int dy) * startX:水平方向滾動的偏移值,以像素為單位,。正值標明向左滾動 * startY:垂直方向滾動的偏移值,正值標明向上滾動 * dx:水平方向滑動的距離,正值向左滾動 * dy:垂直方向滑動的距離,正值向上滾動。 * * startX 表示起點在水平方向到原點的距離(可以理解為X軸坐標,但與X軸相反),正值表示在原點左邊,負值表示在原點右邊。 dx 表示滑動的距離,正值向左滑,負值向右滑。 */ scroller.startScroll(scrollX, 0, dx, 0); /** * postInvalidate() 方法在非 UI 線程中調用,通知 UI 線程重繪,(當然也可以在UI線程中調用)。 * invalidate() 方法在 UI 線程中調用,重繪當前 UI。 */ postInvalidate(); changeLisener.selectImage(index); } break; default: break; } return true; }
利用Timer、TimerTask、Handler實現自動輪播。
1. 需要兩個方法來控制自動輪播的啟動和關閉,我們稱之為自動輪播的開關,分別為
startAuto(),stopAuto();還需要一個標志來表明當前自動輪播的狀態時開啟還是關閉,設為布爾類
型isAuto,true表示自動輪播啟動,false表示自動輪播關閉。
2. 在ImaBannerViewGroup 的構造方法中,設置一個定時任務,如果自動輪播處於開啟狀態,則利用
Handler每間隔一段時間發送一個空的消息,而在Handler接受消息后利用scrollTo()方法實現圖片的
輪播,相關代碼如下:
private void intiObj() { scroller = new Scroller(getContext()); task = new TimerTask() { @Override public void run() { if (isAuto) { autoHander.sendEmptyMessage(0); } } }; timer.schedule(task, 100, 3000); } private Handler autoHander = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case 0: if (++index >= child) { index = 0; } scrollTo(childWidth * index, 0); changeLisener.selectImage(index); break; default: break; } } };
在點擊發生時,關閉自動輪播,抬起手后需要開啟自動輪播,實現過程只需要在onTouchEvent()方法中,
當MotionEvent.ACTION_DOWN時,調用stopAuto();當MotionEvent.ACTION_UP時,調用
startAuto()即可。同時,利用一個布爾型變量isClick判斷點擊事件,當用戶離開屏幕的一瞬間,來判斷是點擊事件還是移動事件。當按下即觸發ACTION_DOWN時,設置為true,ACTION_MOVE時,若移動距離大於最小移動距離,則設置為false,當ACTION_UP的時候,根據isClick值判斷是移動還是點擊,進行相應的操作即可。
最后實現底部輪播圓點的布局以及切換的過程,首先我們需要自定義一個FrameLayout布局,代碼如下:
public class ImageBannerFrameLayout extends FrameLayout implements DotChangeLisener, ImageBannerListener { private ImaBannerViewGroup imaBannerViewGroup; private LinearLayout linearLayout; private FrameLayoutListenenr layoutListenenr; public FrameLayoutListenenr getLayoutListenenr() { return layoutListenenr; } public void setLayoutListenenr(FrameLayoutListenenr layoutListenenr) { this.layoutListenenr = layoutListenenr; } public ImageBannerFrameLayout(Context context) { super(context); initBannerViewGroup(); initDotLayout(); } public ImageBannerFrameLayout(Context context, AttributeSet attrs) { super(context, attrs); initBannerViewGroup(); initDotLayout(); } public ImageBannerFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initBannerViewGroup(); initDotLayout(); } private void initBannerViewGroup() { imaBannerViewGroup = new ImaBannerViewGroup(getContext()); LayoutParams lp = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); imaBannerViewGroup.setLayoutParams(lp); imaBannerViewGroup.setChangeLisener(this); imaBannerViewGroup.setListener(this); addView(imaBannerViewGroup); } private void initDotLayout() { linearLayout = new LinearLayout(getContext()); LayoutParams lp = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 40); linearLayout.setLayoutParams(lp); linearLayout.setOrientation(LinearLayout.HORIZONTAL); linearLayout.setGravity(Gravity.CENTER); linearLayout.setBackgroundColor(Color.RED); addView(linearLayout); LayoutParams layoutParams = (LayoutParams) linearLayout.getLayoutParams(); layoutParams.gravity = Gravity.BOTTOM; linearLayout.setLayoutParams(layoutParams); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { linearLayout.setAlpha(0.5f); } else { linearLayout.getBackground().setAlpha(100); } } public void addBitmap(List<Bitmap> list) { for (int i = 0; i < list.size(); i++) { Bitmap bitmap = list.get(i); addBitmapToBannerViewGroup(bitmap); addDotToLayout(); } } private void addDotToLayout() { ImageView imageView = new ImageView(getContext()); LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); lp.setMargins(5, 5, 5, 5); imageView.setLayoutParams(lp); imageView.setImageResource(R.drawable.doc_normal); linearLayout.addView(imageView); } private void addBitmapToBannerViewGroup(Bitmap bitmap) { ImageView imageView = new ImageView(getContext()); imageView.setScaleType(ImageView.ScaleType.FIT_XY); imageView.setLayoutParams(new ViewGroup.LayoutParams(Constant.WIDTH, ViewGroup.LayoutParams.WRAP_CONTENT)); imageView.setImageBitmap(bitmap); imaBannerViewGroup.addView(imageView); } @Override public void selectImage(int index) { int count = linearLayout.getChildCount(); for (int i = 0; i < count; i++) { ImageView imageView = (ImageView) linearLayout.getChildAt(i); if (i == index) { imageView.setImageResource(R.drawable.doc_normal); } else { imageView.setImageResource(R.drawable.doc_unnormal); } } } @Override public void clickImageIndex(int pos) { layoutListenenr.clickImageByIndex(pos); } }
關於自定的FrameLayout布局,我們需要先加載圖片的ViewGroup,然后加載點集的LinearLayout;需要對圖片進行監聽,添加接口進行相應的處理;提供addBitmap方法,以便調用者設置輪播圖片,並每張圖片和每個圓點一一映射。
MainActivity中的調用方法如下:
public class MainActivity extends AppCompatActivity implements ImageBannerListener, FrameLayoutListenenr, DotChangeLisener { private ImageBannerViewGroup group; private ImaBannerViewGroup viewGroup; private ImageBannerFrameLayout frameLayout; private int[] ids = new int[]{R.mipmap.top, R.mipmap.second, R.mipmap.qq}; private int[] idss = new int[]{R.mipmap.top, R.mipmap.second, R.mipmap.qq}; private int[] idds = new int[]{R.mipmap.top, R.mipmap.second, R.mipmap.qq}; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); DisplayMetrics dm = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(dm); Constant.WIDTH = dm.widthPixels; group = findViewById(R.id.group); viewGroup = findViewById(R.id.scrollViewGroup); frameLayout = findViewById(R.id.contentGroup); List<Bitmap> list = new ArrayList<>(); for (int i = 0; i < idds.length; i++){ Bitmap bitmap = BitmapFactory.decodeResource(getResources(), idds[i]); list.add(bitmap); } frameLayout.addBitmap(list); for (int i = 0; i < ids.length; i++) { ImageView imageView = new ImageView(this); /** * 解決空白的bug */ imageView.setScaleType(ImageView.ScaleType.FIT_XY); imageView.setLayoutParams(new ViewGroup.LayoutParams(Constant.WIDTH, ViewGroup.LayoutParams.WRAP_CONTENT)); imageView.setImageResource(ids[i]); group.addView(imageView); } for (int i = 0; i < idss.length; i++) { ImageView imageView = new ImageView(this); imageView.setScaleType(ImageView.ScaleType.CENTER_CROP); imageView.setLayoutParams(new ViewGroup.LayoutParams(Constant.WIDTH, ViewGroup.LayoutParams.WRAP_CONTENT)); imageView.setImageResource(idss[i]); viewGroup.addView(imageView); } viewGroup.setListener(this); viewGroup.setChangeLisener(this); frameLayout.setLayoutListenenr(this); } @Override public void clickImageIndex(int pos) { Toast.makeText(this, "pos = " + pos, Toast.LENGTH_LONG).show(); } @Override public void clickImageByIndex(int pos) { Toast.makeText(this, "pos = " + pos, Toast.LENGTH_LONG).show(); } @Override public void selectImage(int index) { } }
因為我實現的是個循序漸進的過程,所有有多余代碼,大家可以僅參考FrameLayout部分。
代碼補充:
//分頁符的監聽 public interface DotChangeLisener { void selectImage(int index); } //FrameLayout上面點擊每張圖片的監聽 public interface FrameLayoutListenenr { void clickImageByIndex(int pos); } //點擊每張圖片的監聽 public interface ImageBannerListener { void clickImageIndex(int pos); } //定義一個全局變量,即每張圖片的寬度 public class Constant { public static int WIDTH = 0; }
上述代碼基本就可以實現輪播效果了。