本项目Demo: https://github.com/liaozhoubei/CustomViewDemo
轮播图的实现
现在世面上的app非常流行的一个功能,轮播图效果如下:
轮播图实现的原理是借由ViewPager这个V4包中的类实现的,要在layout布局中使用它,需要写出它的绝对路径,layout代码如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="160dp" >
<android.support.v4.view.ViewPager
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_alignParentBottom="true"
android:background="#66000000"
android:gravity="center_horizontal"
android:orientation="vertical"
android:padding="5dp" >
<TextView
android:id="@+id/tv_desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:text="天王盖地虎, 天王盖地虎, 天王盖地虎, "
android:textColor="@android:color/white" />
<LinearLayout
android:id="@+id/ll_point_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:orientation="horizontal" >
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</RelativeLayout>
我们的轮播图由三部分组成,第一部分是底层的轮换图片,第二部分是需要轮换的文字标题,最后就是轮换文字下面的小白点。
轮播图的具体实现代码:
public class MyViewpager extends Activity {
private int[] imageResIds;
private String[] contentDescs;
private ViewPager mViewPager;
private TextView mTv_desc;
private LinearLayout mLl_point_container;
private ArrayList<ImageView> mImageViewList;
private int previousSelectedPosition = 0;
private Timer timer;
private MyPagerListener myPagerListener;
private boolean isRunning = true; // 判断Activity是否仍在运行,以便在Activity销毁时结束自动循环
private long delay = 3 * 1000; // 设置轮播图延时时间
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_myviewpager);
initView();
initData();
initAdapter();
// 实现viewpager自动循环的方法
timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
runOnUiThread(new Runnable() {
@Override
public void run() {
mViewPager.setCurrentItem(mViewPager.getCurrentItem() + 1);
}
});
}
}, 2000, delay); // 第一次仔细在两秒之后,然后每个3秒执行一次
}
private void initView() {
// 初始化视图
mViewPager = (ViewPager) findViewById(R.id.viewpager);
mTv_desc = (TextView) findViewById(R.id.tv_desc);
mLl_point_container = (LinearLayout) findViewById(R.id.ll_point_container);
myPagerListener = new MyPagerListener();
mViewPager.addOnPageChangeListener(myPagerListener);
}
private void initData() {
// 初始化要显示的数据
// 图片资源id数组
imageResIds = new int[] { R.drawable.a, R.drawable.b, R.drawable.c, R.drawable.d, R.drawable.e };
// 文本描述
contentDescs = new String[] { "巩俐不低俗,我就不能低俗", "扑树又回来啦!再唱经典老歌引万人大合唱", "揭秘北京电影如何升级", "乐视网TV版大派送", "热血屌丝的反杀" };
mImageViewList = new ArrayList<ImageView>();
ImageView imageView;
View pointView;
for(int i = 0; i < imageResIds.length; i++) {
imageView = new ImageView(getApplicationContext());
// 设置viewpager的图像
imageView.setBackgroundResource(imageResIds[i]);
mImageViewList.add(imageView);
// 创建小圆点,当可用的时候为白色,不可用的时候为灰色
pointView = new View(getApplicationContext());
// 设置小圆点的图像
pointView.setBackgroundResource(R.drawable.selector_bg_point);
// 设置小圆点的大小
LayoutParams params = new LayoutParams(5, 5);
if (i != 0) {
// 当小圆点不是处于第一位的时候每个相隔10像素
params.leftMargin = 10;
}
// 设置小圆点为不可以,当前状态为灰色
pointView.setEnabled(false);
mLl_point_container.addView(pointView, params);
}
}
private void initAdapter() {
// 设置小圆点小白点第一个位置的为白色
mLl_point_container.getChildAt(0).setEnabled(true);
// 设置开始时的文字标题
mTv_desc.setText(contentDescs[0]);
// 初始化上一次记录位置为0
previousSelectedPosition = 0;
mViewPager.setAdapter(new MyPagerAdapter());
// 使用Integer.MAX_VALUE可能会产生BUG,因此可以直接使用500000
int position = Integer.MAX_VALUE / 2 - (Integer.MAX_VALUE / 2 % mImageViewList.size());
// 设置viewpager当前的条目位置,设置大数值充当无限循环的角色
mViewPager.setCurrentItem(5000000);
}
private class MyPagerAdapter extends PagerAdapter{
@Override
public int getCount() {
return Integer.MAX_VALUE;
}
@Override
public boolean isViewFromObject(View view, Object object) {
// 指定复用的判断逻辑, 固定写法
return view == object;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
//返回要显示的条目内容, 创建条目
// 设置但position大于mImageViewList.size()的时候重新从0开始,避免数组越界
int newPosition = position % mImageViewList.size();
// a. 把View对象添加到container中
ImageView imageView = mImageViewList.get(newPosition);
// b. 把View对象返回给框架, 适配器
container.addView(imageView);
return imageView; // 必须重写, 否则报异常
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
// object 要销毁的对象
container.removeView((View) object);
}
}
private class MyPagerListener implements OnPageChangeListener{
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
// 滑动的时候调用
}
@Override
public void onPageSelected(int position) {
// 新的条目被选中时调用
// 设置但position大于mImageViewList.size()的时候重新从0开始
int newPosition = position % mImageViewList.size();
// 设置标题和白色小圆点的位置
mTv_desc.setText(contentDescs[newPosition]);
mLl_point_container.getChildAt(previousSelectedPosition).setEnabled(false);
mLl_point_container.getChildAt(newPosition).setEnabled(true);
previousSelectedPosition = newPosition;
}
@Override
public void onPageScrollStateChanged(int state) {
// 滑动状态发生改变的时候调用
}
}
@Override
protected void onDestroy() {
super.onDestroy();
//界面销毁时结束定时任务
timer.cancel();
// 结束监听页面滑动
mViewPager.removeOnPageChangeListener(myPagerListener);
}
}
代码解析
轮播图实现的代码虽然看上去很多,实质上就只有这几个要点:
-
使用ViewPager,并且给ViewPager设置PagerAdapter,实现轮播图的效果,代码如下:
mViewPager.setAdapter(new MyPagerAdapter());
-
监听ViewPager的滑动事件MyPagerListener,设置ViewPager滑动时的变化,代码如下:
mViewPager.addOnPageChangeListener(myPagerListener);
当ViewPager的图片滑动的时候,同步更新页面中的标题和白色小圆点的举例
-
设置ViewPager图片自动轮换,在这里我们使用Timer定时任务来执行,代码如下:
timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { runOnUiThread(new Runnable() { @Override public void run() { mViewPager.setCurrentItem(mViewPager.getCurrentItem() + 1); } }); } }, 2000, delay);
好了,介绍完大概的方法后,我们来详细的解析一下代码吧。
与listView的使用方法差不多,制作轮播图一样要设置一个Adapter,但是ViewPager要使用的却是PagerAdapter,它必须重写PagerAdapter的四个方法:
getCount()
isViewFromObject()
instantiateItem()
destroyItem()
getCount()方法是为了获取所传入内容的多少,isViewFromObject()则是指定复用的判断逻辑, 一般是固定写return view == object就可以了。instantiateItem()则是初始化要显示的内容,destroyItem()则是销毁移出视线的内容。
在instantiateItem()我们没有处理过多的内容,仅仅是获得了通过轮播图的位置来获得轮播图,而这个轮播图的位置position则是通过前面mViewPager.setCurrentItem(5000000)这行代码获得的。
为什么要将ViewPager的条目设置成5000000,因为我们要使viewpager滑动的时候给用户形成一种这是无限循环的错觉,当我们 设置了一个足够大的数字时,用户滑动很长的时间都不会滑倒Item的尽头,更不会造成数组越界。
同时通过以下代码:
int newPosition = position % mImageViewList.size();
ImageView imageView = mImageViewList.get(newPosition);
来获得之前设置在List集合之中的数据,并且newPosition的大小适中在0 - mImageViewList.size()直接,从而使得list数组不会有数组越界的问题。
事实上,当我们设置完PagerAdapter之后,我们就已经完成了轮播图,实现了轮播图的效果了。剩下的只是轮播图的一些扫尾工作,也就是添加每个轮播图的标题,以及展示轮播图当前位置标识的小白点。
轮播图的标题以及其底下几个小白点实质上与Adapter是分隔开的,它所形成的效果是通过viewpager设置的OnPageChangeListener改变的。OnPageChangeListener中的onPageSelected方法,当图片页面被选择的时候就会调用,每次调用的时候我们就获得了当前视图的条目position,然后通过它来确定当前的文字和白点所显示的位置。
这里顺带说一下小白点是如何出现的并且更换颜色的。
// 创建小圆点,当可用的时候为白色,不可用的时候为灰色
pointView = new View(getApplicationContext());
// 设置小圆点的图像
pointView.setBackgroundResource(R.drawable.selector_bg_point);
// 设置小圆点的大小
LayoutParams params = new LayoutParams(5, 5);
if (i != 0) {
// 当小圆点不是处于第一位的时候每个相隔10像素
params.leftMargin = 10;
}
// 设置小圆点为不可以,当前状态为灰色
pointView.setEnabled(false);
mLl_point_container.addView(pointView, params);
在之前的layout布局中我们设置了一个空的LinearLayout并且设置id为ll_point_container,这个就是包含小白点的线性布局,然后我们通过用代码的方式创建了白色原点,那么我们怎么控制它是白色还是灰色呢?我们是直接通过一个状态选择来设置的,当设置setEnabled(true)的时候是白色,为false时就是灰色,状态选择器代码如下:
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/shape_bg_point_disenable" android:state_enabled="false"></item>
<item android:drawable="@drawable/shape_bg_point_enable" android:state_enabled="true"></item>
</selector>
至此,轮播图的实现便到此为止了,话说回来,轮播图的实现也没有我们想象中的那么难嘛~~~
下拉选择框的实现
对于下拉选择框,其实android也有一个spinner控件来实现,但是这个控件所能实现的功能并不多,所以才需要自定义一个下拉选择框,效果如下图:
要实现下拉选择框,其核心使用PopupWindow和ListView这两个类。下拉选择空中有一系列的条目,其形式都是一样的,这种状况之下,自然是使用ListView了,但是我们想要的是展示选择框,那么就不可能将Listview直接放在一个布局中。而PopupWindow则可以创建一个小的气泡窗口,将ListView直接塞进去。这些便是下拉选择框实现的基础了。
代码如下:
public class MyPopupwindow extends Activity implements OnClickListener, OnItemClickListener{
private EditText et_input;
private ImageView ib_dropdown;
private ListView listView;
private ArrayList<String> data;
private PopupWindow popupWindow;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_mypopupwindow);
et_input = (EditText) findViewById(R.id.et_input);
ib_dropdown = (ImageView) findViewById(R.id.ib_dropdown);
ib_dropdown.setOnClickListener(this);
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.ib_dropdown){
showPopupWindow();
}
}
private void showPopupWindow() {
initListView();
// 显示下拉选择框
popupWindow = new PopupWindow(listView, et_input.getWidth(), 300);
// 设置点击外部区域, 自动隐藏
popupWindow.setOutsideTouchable(true);// 外部可触摸
popupWindow.setBackgroundDrawable(new BitmapDrawable());// 设置空的背景, 响应点击事件
popupWindow.setFocusable(true); //设置可获取焦点
popupWindow.showAsDropDown(et_input, 0, 0);
}
// 初始化要显示的内容
private void initListView() {
listView = new ListView(getApplicationContext());
listView.setDividerHeight(0); // 设置分割线边距
listView.setBackgroundResource(R.drawable.listview_background);
listView.setOnItemClickListener(this);
data = new ArrayList<String>();
for (int i = 0; i < 30; i ++){
// 添加数据
String str = "1000" + i;
data.add(str);
}
listView.setAdapter(new MyAdapter());
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
// listview条目点击事件
String string = data.get(position);
et_input.setText(string);
popupWindow.dismiss();
}
private class MyAdapter extends BaseAdapter{
@Override
public int getCount() {
return data.size();
}
@Override
public Object getItem(int position) {
return data.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
View view;
ViewHolder viewHolder;
if (convertView == null) {
view = View.inflate(getApplicationContext(), R.layout.item_number, null);
viewHolder = new ViewHolder();
viewHolder.iv_number = (ImageView) view.findViewById(R.id.iv_number);
viewHolder.tv_number = (TextView) view.findViewById(R.id.tv_number);
viewHolder.ib_delete = (ImageView) view.findViewById(R.id.ib_delete);
view.setTag(viewHolder);
} else {
view = convertView;
viewHolder = (ViewHolder) view.getTag();
}
viewHolder.tv_number.setText(data.get(position));
viewHolder.ib_delete.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// 在条目中点击删除
data.remove(position);
notifyDataSetChanged();
if (data.size() == 0) {
// 当数据为0的时候隐藏popuwindow
popupWindow.dismiss();
}
}
});
return view;
}
class ViewHolder{
ImageView iv_number;
TextView tv_number;
ImageView ib_delete;
}
}
}
我们在点击EditText右边的下箭头时要显示下拉选择框,所以创建了一个showPopupWindow()方法,显示下拉选择框,而这也是我们的核心代码所在,代码如下:
popupWindow = new PopupWindow(listView, et_input.getWidth(), 300);
// 设置点击外部区域, 自动隐藏
popupWindow.setOutsideTouchable(true);// 外部可触摸
popupWindow.setBackgroundDrawable(new BitmapDrawable());// 设置空的背景, 响应点击事件
popupWindow.setFocusable(true); //设置可获取焦点
// 将PopupWindow显示在et_input输入框之下
popupWindow.showAsDropDown(et_input, 0, 0);
PopupWindow里面放置了一个listView,同时它的宽度为et_input.getWidth()输入框的宽度,高度为300。setOutsideTouchable(true)表示外部可触摸,与setBackgroundDrawable()一起用,那么点击下拉选择框外部时,PopupWindow会被隐藏。我们发现还设置了setFocusable(true)让popupWindow可获取焦点,这是因为popupWindow是默认不可获取焦点的,也就是说它里面的条目是不能够给点击的!
接下来就是设置listView的正常流程,需要读者自己详细了解了。
但是当一切都设置好了之后,我们选择listView的其中一条时,发现仍然不可点击,但是点击右侧删除的图标确实可以的,这又是怎么回事呢?
这是listView被强占了焦点的缘故,因为listView中有ImageButton这种能够获取点击事件的组件存在,所以listView每个条目的点击事件都集中在了删除图标上了。那么怎么样才能设置listView的其他部分具有点击事件呢?只需要来listView所在的item布局中添加以下代码:
android:descendantFocusability="blocksDescendants"
这时listview中的item中的其他组件都能够获取焦点,实现点击事件
本项目Demo: https://github.com/liaozhoubei/CustomViewDemo
扩展阅读:
Android 精通自定义视图(1) http://www.jianshu.com/p/c2195269ce44
Android 精通自定义视图(3) http://www.jianshu.com/p/1660479e76ef
Android 精通自定义视图(4) http://www.jianshu.com/p/850e387fc9d8
Android 精通自定义视图(5) http://www.jianshu.com/p/93feac19c396