【Android】造轮子:轮播图
一杯倒
8年前
<h2><strong>前言</strong></h2> <p>目前市场上的APP中,轮播图可以说是很常见的。一个好的轮播图,基本上适用于所有的APP。是时候打造一个自己的轮播图了,不要等到用的时候才去Google。</p> <h2><strong>功能</strong></h2> <p>轮播图需要实现一下功能</p> <ul> <li>图片循环轮播</li> <li>可添加文字</li> <li>最后一张到第一张的切换也要有切换效果</li> <li>循环、自动播放可控制</li> </ul> <p>还有我们都比较关注的一点: <strong>这轮子必须易拆、易装,可扩展性强</strong> 。每次换个项目就要拷贝好几个文件,改一大堆代码,这是很烦的。</p> <h2><strong>实现</strong></h2> <p>再多的文字也不如一张图来得直观,先来个福利,回头再说怎实现的。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/dc9e0f40b68424027aba806d1b756dcc.gif"></p> <p style="text-align:center">效果</p> <p><strong>思路</strong></p> <p>这里使用 ViewPager 来实现轮播的效果,但是 ViewPager 是滑动到最后一张时,是不能跳转到第一张的。于是,我们可以这样:</p> <ul> <li>需要显示的轮播图有N张</li> <li>往 ViewPager 中添加 N 个 View ,这时 ViewPager 中有:<br> View(1)、View(2)、View(3) ... View(N)</li> <li>再往 ViewPager 中添加 View(1) ,这时 ViewPager 中有:<br> View(1)、View(2)、View(3) ... View(N)、View(1)</li> </ul> <p>这样就可以实现一种视觉效果:滑动到最后一张 View(N) 的时候,再往后滑动就回到了第一张 View(1) 。</p> <p>这也适用于从第一张条转到最后一张的实现。</p> <p>文字看着费解?那就看图吧(还好会那么一点点PS)</p> <p>例:</p> <p>需要显示三张图:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/0a7c1fc2f56c18ab6322af65a579d4b0.png"></p> <p style="text-align:center">需要轮播的图片</p> <p>经过处理,变成这样</p> <p><img src="https://simg.open-open.com/show/87db194635231b045825721e763eb6c3.png"></p> <p style="text-align:center">处理后的轮播图</p> <p>在界面上看到的是三张图片,而实际在ViewPager中的是这样的5张。</p> <ul> <li>当从View4跳转到View5时,在代码中立刻将视图切换到View2,应为图片是一样的,所有在界面上看不到任何效果。</li> <li>同理,当从View2跳转到View1时,在代码中将视图切换到View4。</li> </ul> <p>自动轮播流程:</p> <p>View2 --> View3 --> View4 --> View5 --> View2 (完成一次循环)--> View3 --> View4 ....</p> <p>当显示 View5 的时候,立刻切换到 View2 ( View5 和 View2 显示的内容是相同的),这样就实现了图片轮播。</p> <p>这里 View5 -> View2 的切换巧妙利用了 ViewPager 中的方法:</p> <pre> <code class="language-java">setCurrentItem(int item, boolean smoothScroll)</code></pre> <p>参数 smoothScroll为false 的时候,实现了“看不见”的跳转。</p> <p>还是不大清楚?那就直接看代码吧</p> <p><strong>代码</strong></p> <p>思路说完,上代码</p> <ul> <li> <p>创建model</p> <p>这里创建一个 Info 类,模拟实际应用中的数据。里面有 title 和 url 字段。</p> <pre> <code class="language-java">public class Info { private String url; private String title; public Info(String title, String url) { this.url = url; this.title = title; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } }</code></pre> </li> <li> <p><strong>布局</strong><br> 为了实现画面重叠的效果,这里用了相对布局,轮播图使用 ViewPager 来实现。后面有两个 LinearLayout ,第一个 LinearLayout 用来放指示器,在 java 代码中动态添加;第二个 LinearLayout 就用来显示 Title 了,当然,如果还需要显示的其他内容,可以在这个布局里面中添加。</p> <pre> <code class="language-java"><?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/white" android:orientation="vertical"> <android.support.v4.view.ViewPager android:id="@+id/cycle_view_pager" android:layout_width="match_parent" android:layout_height="match_parent" /> <LinearLayout android:id="@+id/cycle_indicator" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_marginBottom="10dp" android:gravity="center" android:orientation="horizontal" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_above="@id/cycle_indicator" android:orientation="vertical"> <TextView android:id="@+id/cycle_title" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="10dp" android:gravity="center" android:textColor="@android:color/white" android:textSize="20sp" /> </LinearLayout> </RelativeLayout></code></pre> </li> <li> <p><strong>CycleViewPager</strong></p> <p>重点来了,自定义的轮播图。来个重磅炸弹,别看晕了</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/c789038aa18a8dba8537889b080ecb01.png"></p> <pre> <code class="language-java">public class CycleViewPager extends FrameLayout implements ViewPager.OnPageChangeListener { private static final String TAG = "CycleViewPager"; private Context mContext; private ViewPager mViewPager;//实现轮播图的ViewPager private TextView mTitle;//标题 private LinearLayout mIndicatorLayout; // 指示器 private Handler handler;//每几秒后执行下一张的切换 private int WHEEL = 100; // 转动 private int WHEEL_WAIT = 101; // 等待 private List<View> mViews = new ArrayList<>(); //需要轮播的View,数量为轮播图数量+2 private ImageView[] mIndicators; //指示器小圆点 private boolean isScrolling = false; // 滚动框是否滚动着 private boolean isCycle = true; // 是否循环,默认为true private boolean isWheel = true; // 是否轮播,默认为true private int delay = 4000; // 默认轮播时间 private int mCurrentPosition = 0; // 轮播当前位置 private long releaseTime = 0; // 手指松开、页面不滚动时间,防止手机松开后短时间进行切换 private ViewPagerAdapter mAdapter; private ImageCycleViewListener mImageCycleViewListener; private List<Info> infos;//数据集合 private int mIndicatorSelected;//指示器图片,被选择状态 private int mIndicatorUnselected;//指示器图片,未被选择状态 final Runnable runnable = new Runnable() { @Override public void run() { if (mContext != null && isWheel) { long now = System.currentTimeMillis(); // 检测上一次滑动时间与本次之间是否有触击(手滑动)操作,有的话等待下次轮播 if (now - releaseTime > delay - 500) { handler.sendEmptyMessage(WHEEL); } else { handler.sendEmptyMessage(WHEEL_WAIT); } } } }; public CycleViewPager(Context context) { this(context, null); } public CycleViewPager(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CycleViewPager(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); this.mContext = context; initView(); } /** * 初始化View */ private void initView() { LayoutInflater.from(mContext).inflate(R.layout.layout_cycle_view, this, true); mViewPager = (ViewPager) findViewById(R.id.cycle_view_pager); mTitle = (TextView) findViewById(R.id.cycle_title); mIndicatorLayout = (LinearLayout) findViewById(R.id.cycle_indicator); handler = new Handler() { @Override public void handleMessage(Message msg) { super.handleMessage(msg); if (msg.what == WHEEL && mViews.size() > 0) { if (!isScrolling) { //当前为非滚动状态,切换到下一页 int posttion = (mCurrentPosition + 1) % mViews.size(); mViewPager.setCurrentItem(posttion, true); } releaseTime = System.currentTimeMillis(); handler.removeCallbacks(runnable); handler.postDelayed(runnable, delay); return; } if (msg.what == WHEEL_WAIT && mViews.size() > 0) { handler.removeCallbacks(runnable); handler.postDelayed(runnable, delay); } } }; } /** * 设置指示器图片,在setData之前调用 * * @param select 选中时的图片 * @param unselect 未选中时的图片 */ public void setIndicators(int select, int unselect) { mIndicatorSelected = select; mIndicatorUnselected = unselect; } public void setData(List<Info> list, ImageCycleViewListener listener) { setData(list, listener, 0); } /** * 初始化viewpager * * @param list 要显示的数据 * @param showPosition 默认显示位置 */ public void setData(List<Info> list, ImageCycleViewListener listener, int showPosition) { if (list == null || list.size() == 0) { //没有数据时隐藏整个布局 this.setVisibility(View.GONE); return; } mViews.clear(); infos = list; if (isCycle) { //添加轮播图View,数量为集合数+2 // 将最后一个View添加进来 mViews.add(getImageView(mContext, infos.get(infos.size() - 1).getUrl())); for (int i = 0; i < infos.size(); i++) { mViews.add(getImageView(mContext, infos.get(i).getUrl())); } // 将第一个View添加进来 mViews.add(getImageView(mContext, infos.get(0).getUrl())); } else { //只添加对应数量的View for (int i = 0; i < infos.size(); i++) { mViews.add(getImageView(mContext, infos.get(i).getUrl())); } } if (mViews == null || mViews.size() == 0) { //没有View时隐藏整个布局 this.setVisibility(View.GONE); return; } mImageCycleViewListener = listener; int ivSize = mViews.size(); // 设置指示器 mIndicators = new ImageView[ivSize]; if (isCycle) mIndicators = new ImageView[ivSize - 2]; mIndicatorLayout.removeAllViews(); for (int i = 0; i < mIndicators.length; i++) { mIndicators[i] = new ImageView(mContext); LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); lp.setMargins(10, 0, 10, 0); mIndicators[i].setLayoutParams(lp); mIndicatorLayout.addView(mIndicators[i]); } mAdapter = new ViewPagerAdapter(); // 默认指向第一项,下方viewPager.setCurrentItem将触发重新计算指示器指向 setIndicator(0); mViewPager.setOffscreenPageLimit(3); mViewPager.setOnPageChangeListener(this); mViewPager.setAdapter(mAdapter); if (showPosition < 0 || showPosition >= mViews.size()) showPosition = 0; if (isCycle) { showPosition = showPosition + 1; } mViewPager.setCurrentItem(showPosition); setWheel(true);//设置轮播 } /** * 获取轮播图View * * @param context * @param url */ private View getImageView(Context context, String url) { return MainActivity.getImageView(context, url); } /** * 设置指示器 * * @param selectedPosition 默认指示器位置 */ private void setIndicator(int selectedPosition) { setText(mTitle, infos.get(selectedPosition).getTitle()); try { for (int i = 0; i < mIndicators.length; i++) { mIndicators[i] .setBackgroundResource(mIndicatorUnselected); } if (mIndicators.length > selectedPosition) mIndicators[selectedPosition] .setBackgroundResource(mIndicatorSelected); } catch (Exception e) { Log.i(TAG, "指示器路径不正确"); } } /** * 页面适配器 返回对应的view * * @author Yuedong Li */ private class ViewPagerAdapter extends PagerAdapter { @Override public int getCount() { return mViews.size(); } @Override public boolean isViewFromObject(View arg0, Object arg1) { return arg0 == arg1; } @Override public void destroyItem(ViewGroup container, int position, Object object) { container.removeView((View) object); } @Override public View instantiateItem(ViewGroup container, final int position) { View v = mViews.get(position); if (mImageCycleViewListener != null) { v.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { mImageCycleViewListener.onImageClick( infos.get(mCurrentPosition - 1), mCurrentPosition, v); } }); } container.addView(v); return v; } @Override public int getItemPosition(Object object) { return POSITION_NONE; } } @Override public void onPageScrolled( int position, float positionOffset, int positionOffsetPixels) { } @Override public void onPageSelected(int arg0) { int max = mViews.size() - 1; int position = arg0; mCurrentPosition = arg0; if (isCycle) { if (arg0 == 0) { //滚动到mView的1个(界面上的最后一个),将mCurrentPosition设置为max - 1 mCurrentPosition = max - 1; } else if (arg0 == max) { //滚动到mView的最后一个(界面上的第一个),将mCurrentPosition设置为1 mCurrentPosition = 1; } position = mCurrentPosition - 1; } setIndicator(position); } @Override public void onPageScrollStateChanged(int state) { if (state == 1) { // viewPager在滚动 isScrolling = true; return; } else if (state == 0) { // viewPager滚动结束 releaseTime = System.currentTimeMillis(); //跳转到第mCurrentPosition个页面(没有动画效果,实际效果页面上没变化) mViewPager.setCurrentItem(mCurrentPosition, false); } isScrolling = false; } /** * 为textview设置文字 * @param textView * @param text */ public static void setText(TextView textView, String text) { if (text != null && textView != null) textView.setText(text); } /** * 为textview设置文字 * * @param textView * @param text */ public static void setText(TextView textView, int text) { if (textView != null) setText(textView, text + ""); } /** * 是否循环,默认开启。必须在setData前调用 * * @param isCycle 是否循环 */ public void setCycle(boolean isCycle) { this.isCycle = isCycle; } /** * 是否处于循环状态 * * @return */ public boolean isCycle() { return isCycle; } /** * 设置是否轮播,默认轮播,轮播一定是循环的 * * @param isWheel */ public void setWheel(boolean isWheel) { this.isWheel = isWheel; isCycle = true; if (isWheel) { handler.postDelayed(runnable, delay); } } /** * 刷新数据,当外部视图更新后,通知刷新数据 */ public void refreshData() { if (mAdapter != null) mAdapter.notifyDataSetChanged(); } /** * 是否处于轮播状态 * * @return */ public boolean isWheel() { return isWheel; } /** * 设置轮播暂停时间,单位毫秒(默认4000毫秒) * @param delay */ public void setDelay(int delay) { this.delay = delay; } /** * 轮播控件的监听事件 * * @author minking */ public static interface ImageCycleViewListener { /** * 单击图片事件 * * @param info * @param position * @param imageView */ public void onImageClick(Info info, int position, View imageView); } }</code></pre> <p>从里面挑了几个变量和方法说明一下:</p> <p>变量:</p> <p>handler 、 runnable :实现定时轮播</p> <p>mCurrentPosition :表示当前位置</p> <p>方法:</p> <p>setIndicators() :设置指示器的图片(必须在 setData 前调用)</p> <p>setData() :根据数据,生成对应的轮播图</p> <p>setIndicator() :设置指示器和文字内容</p> <p>onPageSelected() 、 onPageScrollStateChanged() :利用 ViewPager 的滚动监听,实现了上面的思路。 onPageSelected() 中根据 ViewPager 中显示的位置,改变 mCurrentPosition 的值,然后在 onPageScrollStateChanged() 中根据 mCurrentPosition 重新设置页面(这里的 setCurrentItem 没有动画效果)。</p> <p>getImageView() :根据URL生成 Viewpager 中对应的各个 View ( <em>根据实际的图片加载框架来生成,这里使用了Picasso实现了网络图片的加载</em> ),看看 getImageView() 中调用的代码</p> <pre> <code class="language-java">/** * 得到轮播图的View * @param context * @param url * @return */ public static View getImageView(Context context, String url) { RelativeLayout rl = new RelativeLayout(context); //添加一个ImageView,并加载图片 ImageView imageView = new ImageView(context); RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams( RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT); imageView.setScaleType(ImageView.ScaleType.CENTER_CROP); imageView.setLayoutParams(layoutParams); //使用Picasso来加载图片 Picasso.with(context).load(url).into(imageView); //在Imageview前添加一个半透明的黑色背景,防止文字和图片混在一起 ImageView backGround = new ImageView(context); backGround.setLayoutParams(layoutParams); backGround.setBackgroundResource(R.color.cycle_image_bg); rl.addView(imageView); rl.addView(backGround); return rl; }</code></pre> <pre> <code class="language-java"><color name="cycle_image_bg">#44222222</color></code></pre> <p>代码很简单,创建了一个显示图片的布局,先在布局中添加了需要显示的图片,然后加了个半透明的图,防止显示时文字和图片中白色的部分重叠在一起,导致看不清文字。</p> </li> <li> <p>在Acitivty中使用</p> <p>轮子打造好了,不拿出来溜一溜?</p> <pre> <code class="language-java">public class MainActivity extends AppCompatActivity { /** * 模拟请求后得到的数据 */ List<Info> mList = new ArrayList<>(); /** * 轮播图 */ CycleViewPager mCycleViewPager; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initData(); initView(); } /** * 初始化数据 */ private void initData() { mList.add(new Info("标题1", "http://img2.3lian.com/2014/c7/25/d/40.jpg")); mList.add(new Info("标题2", "http://img2.3lian.com/2014/c7/25/d/41.jpg")); mList.add(new Info("标题3", "http://imgsrc.baidu.com/forum/pic/item/b64543a98226cffc8872e00cb9014a90f603ea30.jpg")); mList.add(new Info("标题4", "http://imgsrc.baidu.com/forum/pic/item/261bee0a19d8bc3e6db92913828ba61eaad345d4.jpg")); } /** * 初始化View */ private void initView() { mCycleViewPager = (CycleViewPager) findViewById(R.id.cycle_view); //设置选中和未选中时的图片 mCycleViewPager.setIndicators(R.mipmap.ad_select, R.mipmap.ad_unselect); //设置轮播间隔时间 mCycleViewPager.setDelay(2000); mCycleViewPager.setData(mList, mAdCycleViewListener); } /** * 轮播图点击监听 */ private CycleViewPager.ImageCycleViewListener mAdCycleViewListener = new CycleViewPager.ImageCycleViewListener() { @Override public void onImageClick(Info info, int position, View imageView) { if (mCycleViewPager.isCycle()) { position = position - 1; } Toast.makeText(MainActivity.this, info.getTitle() + "选择了--" + position, Toast.LENGTH_LONG).show(); } }; }</code></pre> </li> </ul> <p>使用起来也是很简单的,只要设置下图片、数据、点击监听就可以了。(之前贴过 MainActivity.getImageView() 方法了,这里就不贴了)</p> <p>放到自己的项目中?</p> <p>只需要调下布局,根据自己的图片加载框架改下 getImageView (或者也可以直接用我的),然后把 CycleViewPager 中的 Info 改成自己的 Model 就可以了。</p> <p> </p> <p> </p> <p> </p>