Android 精通自定义视图(2)

本项目Demo: https://github.com/liaozhoubei/CustomViewDemo

轮播图的实现

现在世面上的app非常流行的一个功能,轮播图效果如下:

myviewpager.gif

轮播图实现的原理是借由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控件来实现,但是这个控件所能实现的功能并不多,所以才需要自定义一个下拉选择框,效果如下图:

MyPopupwindow.gif

要实现下拉选择框,其核心使用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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,057评论 25 707
  • 内容抽屉菜单ListViewWebViewSwitchButton按钮点赞按钮进度条TabLayout图标下拉刷新...
    皇小弟阅读 46,658评论 22 664
  • 一提到符法,自然而然想到道家法术,没错,法术是道教千百年来长期赖以助道和济世的奇功秘术。也是最为众生熟知的...
    灵济堂阅读 4,267评论 0 3
  • 漆黑的天空没有半点星光,就像是浓墨渲染过的清池一般。 远远望去整个夜色一片荒芜,只有偶尔几道划破天际的闪电以及紧随...
    幻梦邪魂阅读 503评论 0 2
  • 唤醒49-16 触动 倔强的母亲总是触动我心里最柔软的地方 今天在电梯里遇见了住在我家楼下的...
    我和榕树阅读 237评论 5 1