本項目Demo: https://github.com/liaozhoubei/CustomViewDemo
輪播圖的實現(xiàn)
現(xiàn)在世面上的app非常流行的一個功能,輪播圖效果如下:

輪播圖實現(xiàn)的原理是借由ViewPager這個V4包中的類實現(xiàn)的,要在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>
我們的輪播圖由三部分組成,第一部分是底層的輪換圖片,第二部分是需要輪換的文字標題,最后就是輪換文字下面的小白點。
輪播圖的具體實現(xiàn)代碼:
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銷毀時結束自動循環(huán)
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();
// 實現(xiàn)viewpager自動循環(huán)的方法
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秒執(zhí)行一次
}
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() {
// 初始化要顯示的數(shù)據(jù)
// 圖片資源id數(shù)組
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);
// 創(chuàng)建小圓點,當可用的時候為白色,不可用的時候為灰色
pointView = new View(getApplicationContext());
// 設置小圓點的圖像
pointView.setBackgroundResource(R.drawable.selector_bg_point);
// 設置小圓點的大小
LayoutParams params = new LayoutParams(5, 5);
if (i != 0) {
// 當小圓點不是處于第一位的時候每個相隔10像素
params.leftMargin = 10;
}
// 設置小圓點為不可以,當前狀態(tài)為灰色
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當前的條目位置,設置大數(shù)值充當無限循環(huán)的角色
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) {
//返回要顯示的條目內容, 創(chuàng)建條目
// 設置但position大于mImageViewList.size()的時候重新從0開始,避免數(shù)組越界
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) {
// 滑動狀態(tài)發(fā)生改變的時候調用
}
}
@Override
protected void onDestroy() {
super.onDestroy();
//界面銷毀時結束定時任務
timer.cancel();
// 結束監(jiān)聽頁面滑動
mViewPager.removeOnPageChangeListener(myPagerListener);
}
}
代碼解析
輪播圖實現(xiàn)的代碼雖然看上去很多,實質上就只有這幾個要點:
-
使用ViewPager,并且給ViewPager設置PagerAdapter,實現(xiàn)輪播圖的效果,代碼如下:
mViewPager.setAdapter(new MyPagerAdapter()); -
監(jiān)聽ViewPager的滑動事件MyPagerListener,設置ViewPager滑動時的變化,代碼如下:
mViewPager.addOnPageChangeListener(myPagerListener);
當ViewPager的圖片滑動的時候,同步更新頁面中的標題和白色小圓點的舉例
-
設置ViewPager圖片自動輪換,在這里我們使用Timer定時任務來執(zhí)行,代碼如下:
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滑動的時候給用戶形成一種這是無限循環(huán)的錯覺,當我們 設置了一個足夠大的數(shù)字時,用戶滑動很長的時間都不會滑倒Item的盡頭,更不會造成數(shù)組越界。
同時通過以下代碼:
int newPosition = position % mImageViewList.size();
ImageView imageView = mImageViewList.get(newPosition);
來獲得之前設置在List集合之中的數(shù)據(jù),并且newPosition的大小適中在0 - mImageViewList.size()直接,從而使得list數(shù)組不會有數(shù)組越界的問題。
事實上,當我們設置完PagerAdapter之后,我們就已經完成了輪播圖,實現(xiàn)了輪播圖的效果了。剩下的只是輪播圖的一些掃尾工作,也就是添加每個輪播圖的標題,以及展示輪播圖當前位置標識的小白點。
輪播圖的標題以及其底下幾個小白點實質上與Adapter是分隔開的,它所形成的效果是通過viewpager設置的OnPageChangeListener改變的。OnPageChangeListener中的onPageSelected方法,當圖片頁面被選擇的時候就會調用,每次調用的時候我們就獲得了當前視圖的條目position,然后通過它來確定當前的文字和白點所顯示的位置。
這里順帶說一下小白點是如何出現(xiàn)的并且更換顏色的。
// 創(chuàng)建小圓點,當可用的時候為白色,不可用的時候為灰色
pointView = new View(getApplicationContext());
// 設置小圓點的圖像
pointView.setBackgroundResource(R.drawable.selector_bg_point);
// 設置小圓點的大小
LayoutParams params = new LayoutParams(5, 5);
if (i != 0) {
// 當小圓點不是處于第一位的時候每個相隔10像素
params.leftMargin = 10;
}
// 設置小圓點為不可以,當前狀態(tài)為灰色
pointView.setEnabled(false);
mLl_point_container.addView(pointView, params);
在之前的layout布局中我們設置了一個空的LinearLayout并且設置id為ll_point_container,這個就是包含小白點的線性布局,然后我們通過用代碼的方式創(chuàng)建了白色原點,那么我們怎么控制它是白色還是灰色呢?我們是直接通過一個狀態(tài)選擇來設置的,當設置setEnabled(true)的時候是白色,為false時就是灰色,狀態(tài)選擇器代碼如下:
<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>
至此,輪播圖的實現(xiàn)便到此為止了,話說回來,輪播圖的實現(xiàn)也沒有我們想象中的那么難嘛~~~
下拉選擇框的實現(xiàn)
對于下拉選擇框,其實android也有一個spinner控件來實現(xiàn),但是這個控件所能實現(xiàn)的功能并不多,所以才需要自定義一個下拉選擇框,效果如下圖:

要實現(xiàn)下拉選擇框,其核心使用PopupWindow和ListView這兩個類。下拉選擇空中有一系列的條目,其形式都是一樣的,這種狀況之下,自然是使用ListView了,但是我們想要的是展示選擇框,那么就不可能將Listview直接放在一個布局中。而PopupWindow則可以創(chuàng)建一個小的氣泡窗口,將ListView直接塞進去。這些便是下拉選擇框實現(xiàn)的基礎了。
代碼如下:
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);
// 設置點擊外部區(qū)域, 自動隱藏
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 ++){
// 添加數(shù)據(jù)
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) {
// 當數(shù)據(jù)為0的時候隱藏popuwindow
popupWindow.dismiss();
}
}
});
return view;
}
class ViewHolder{
ImageView iv_number;
TextView tv_number;
ImageView ib_delete;
}
}
}
我們在點擊EditText右邊的下箭頭時要顯示下拉選擇框,所以創(chuàng)建了一個showPopupWindow()方法,顯示下拉選擇框,而這也是我們的核心代碼所在,代碼如下:
popupWindow = new PopupWindow(listView, et_input.getWidth(), 300);
// 設置點擊外部區(qū)域, 自動隱藏
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會被隱藏。我們發(fā)現(xiàn)還設置了setFocusable(true)讓popupWindow可獲取焦點,這是因為popupWindow是默認不可獲取焦點的,也就是說它里面的條目是不能夠給點擊的!
接下來就是設置listView的正常流程,需要讀者自己詳細了解了。
但是當一切都設置好了之后,我們選擇listView的其中一條時,發(fā)現(xiàn)仍然不可點擊,但是點擊右側刪除的圖標確實可以的,這又是怎么回事呢?
這是listView被強占了焦點的緣故,因為listView中有ImageButton這種能夠獲取點擊事件的組件存在,所以listView每個條目的點擊事件都集中在了刪除圖標上了。那么怎么樣才能設置listView的其他部分具有點擊事件呢?只需要來listView所在的item布局中添加以下代碼:
android:descendantFocusability="blocksDescendants"
這時listview中的item中的其他組件都能夠獲取焦點,實現(xiàn)點擊事件
本項目Demo: https://github.com/liaozhoubei/CustomViewDemo
擴展閱讀:
Android 精通自定義視圖(1) http://www.itdecent.cn/p/c2195269ce44
Android 精通自定義視圖(3) http://www.itdecent.cn/p/1660479e76ef
Android 精通自定義視圖(4) http://www.itdecent.cn/p/850e387fc9d8
Android 精通自定義視圖(5) http://www.itdecent.cn/p/93feac19c396