從慕課網上學了一門叫作「不同的自定義實現輪播圖效果」的課程,感受實用性較強,並且按部就班,很適合初學者。在此對該課程作一個小小的筆記。ide
實現輪播思路:佈局
一、通常輪播圖是由一組圖片和底部輪播圓點組成,要想組成這種圓點在圖片之上的效果,首先咱們應當想到FrameLayout佈局。最外層應該是一個FrameLayout佈局,將輪播圖片和圓點添加到這個佈局中,而且須要設置圓點的位置在下部正中間(固然視需求而定)。post
二、輪播圖片組應該是一個ViewGroup,咱們須要對該ViewGroup進行測量、佈局等過程。ui
三、圓點集合應該是一個LinearLayout。this
四、要想實現自動輪播效果,能夠結合Timer、TimerTask以及Handler的配合使用。spa
五、要想實現手動滑動輪播圖,能夠利用Scroller。線程
六、實現底部圓點隨圖片切換變化的過程,實現每張圖片的點擊事件。code
首先咱們來實現圖片組ViewGroup的繪製過程。實現思路很簡單,對於整個ViewGroup,其高度應該是子View的高度(假設咱們的輪播圖效果相似於淘寶首頁的頭部效果),寬度應該是全部子View的寬度之和。抓住這個方向,不難實現測量過程。orm
#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的寬度都是整個屏幕的寬度,咱們能夠寫一個全局的靜態變量,而後給該變量賦值爲屏幕寬度,在須要的時候進行調用就好。blog
而後重寫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; }
上述代碼基本就能夠實現輪播效果了。