在Android4.0中Contacts拨号盘界面剖析(源码)

转自http://blog.sina.com.cn/s/blog_9f233c070101b2u1.html


上文已经说过拨号界面的分页是怎么实现的,下面我们分三次来分享拨号页面内的每一个页面的具体实现,我们先从拨号盘开始说起。

上文通过在ViewPager的适配器对象中,发现过一下三行代码

private DialpadFragment mDialpadFragment;

private CallLogFragment mCallLogFragment;

private PhoneFavoriteFragment mPhoneFavoriteFragment;

第一行的DialpadFragment就是拨号盘界面

第二行的CallLogFragment就是通话记录页面

第三行的PhoneFavoriteFragment就是收藏界面

本文主要分享“拨号盘界面”,即DialpadFragment.java

1         拨号盘的显示

首先我们先来看看拨号盘的显示样式,4.0中拨号盘的默认显示效果如下图   

     在Android4.0中Contacts拨号盘界面剖析(源码)

                       
 1 拨号盘

给我的感觉是整个盘面分成四个部分,

第一部分: titletab相应的图标

第二部分: editText输入部分

第三部分:拨号盘主界面

第四部分:拨号盘底部控制栏

2. 每一部分的具体实现

 2.1 title图标:

 如果看了上文,我们就会发现,title实际上是ActionBar中添加的一个tab

 而在添加tab时,就已经对该tab进行了setIcon()操作,再次就不再过多的描述了,如果有不太明白的,请查看DialtactsActivity.java文件的setupDialer()方法。

2.2 editText输入部分

此时我们应该注意一下,DialpadFragment类的到底是什么东西?

class DialpadFragment extends Fragment

原来是Fragment,下面我转载了一个经典的Fragment生命周期图片,下图2

                         在Android4.0中Contacts拨号盘界面剖析(源码)
                           2   Fragment生命周期

 通过上图可以发现,加载布局主要是在onCreateView方法中实现的,下面我们进入onCreateView方法,看看这个布局界面是怎么出现的。

  方法第一句

   View fragmentView = inflater.inflate(R.layout.dialpad_fragment, container, false);

来了,原来是dialpad_fragment.xml文件

   在该文件中,下面我讲editText的部分截取出来,共大家分析:

<LinearLayout

        android:id="@+id/digits_container"

        android:layout_width="match_parent"

        android:layout_height="0px"

        android:layout_weight="0.200"

        android:layout_marginTop="@dimen/dialpad_vertical_margin"

        android:gravity="center"

        android:background="@drawable/dialpad_background" >

 

        <com.android.contacts.dialpad.DigitsEditText

            android:id="@+id/digits"

            android:layout_width="0dip"

            android:layout_weight="1"

            android:layout_height="match_parent"

            android:layout_alignParentLeft="true"

            android:gravity="center"

            android:textAppearance="@style/DialtactsDigitsTextAppearance"

            android:textColor="?android:attr/textColorPrimary"

            android:nextFocusRight="@+id/overflow_menu"

            android:background="@android:color/transparent" />

 

        <ImageButton

            android:id="@+id/overflow_menu"

            android:layout_width="48dip"

            android:layout_height="match_parent"

            android:layout_alignParentRight="true"

            android:src="@drawable/ic_menu_overflow"

           android:contentDescription="@*android:string/action_menu_overflow_description"

            android:nextFocusLeft="@id/digits"

            android:background="?android:attr/selectableItemBackground"/>

 

    </LinearLayout>

我将上面的文件,表示的意义用图的形式画出

      在Android4.0中Contacts拨号盘界面剖析(源码)                            

                                  图3 输入框示意图                                            

图中的青色区域代表的就是digits_container,黄色区域代表的是digits,即输入框,红色区域代表的是overflow_menu按钮。

我想您看过上面的布局文件,就会问输入框应该是EditText,而上文的为什么是DigitsEditText

很明显它们之间是继承关系。下面我们来看啊可能这个DigitsEditText有什么特点?、

代码中对其进行了虚拟键盘的设置,焦点的变换,触摸等几个操作。

对此需要说明的一点是,布局文件中明明有overflow_menu,但是为什么没有显示出来呢?

原来是在onCreateView中对其可见性进行了设置,设置代码如下:

final View overflowMenuButton = fragmentView.findViewById(R.id.overflow_menu);

  if (overflowMenuButton != null) {

     if (ViewConfiguration.get(getActivity()).hasPermanentMenuKey()) {

       overflowMenuButton.setVisibility(View.GONE);

     } else {

       overflowMenuButton.setOnClickListener(this);

    }

 }

在这句话上有这样一句注释

Soft menu button should appear only when there's no hardware menu button.

说出了该overflow按钮的显示时间,再次不多废话。

 

2.3 拨号键盘

拨号键盘对应于dialpad.xml的布局文件

该布局采用了典型的table布局,有兴趣的朋友可以看一下

下面紧取一个按键,进行一下细节的描述,就拿1这个按键为例

<ImageButton android:id="@+id/one" style="@style/DialtactsDialpadButtonStyle"

            android:src="@drawable/dial_num_1"

            android:contentDescription="@string/description_image_button_one" />

首先为该ImageButton设置了一个id ,然后为其设置了style

我们先进入style看看都为ImageButton做了些什么,

<style name="DialtactsDialpadButtonStyle">

        <item name="android:layout_width">0dip</item>

        <item name="android:layout_height">match_parent</item>

        <item name="android:layout_weight">1</item>

        <item name="android:background">?android:attr/selectableItemBackground</item>

        <item name="android:soundEffectsEnabled">false</item>

</style>

最引人注意的是android:background android:soundEffectsEnabled

android:background代表着该ImageButton的背景色

android:soundEffectsEnabled设置点击或触摸时是否有声音效果

回过来,我们再来看看ImageButton的前景

android:src="@drawable/dial_num_1"

很显然,这个一个图片嘛,有啥好看的?您不仅会这样想。

实际上这个是一个图片吗?不是,也是,更确切的说是一个图片组,下面我们来看看这个图片组到底有什么神秘的

<selector xmlns:android="http://schemas.android.com/apk/res/android">

    <!-- <item android:state_pressed="true"

        android:drawable="@drawable/dial_num_1" />

    <item android:state_focused="true"

        android:drawable="@drawable/dial_num_1" /> -->

    <item

        android:drawable="@drawable/dial_num_1_wht" />

</selector>

哈哈,原来是个selector,通过其中的条件可以添加点击,触摸等等效果,这个在我们平时开发的过程中还是比较常用的,朋友们一定要记住哦!

2.4拨号盘底部控制栏

通过dialpad_additional_buttons.xml文件查看到,

三个按钮2个分割线的横向布局排列,这个页面没有什么神秘的地方,就不在浪费笔墨了

有兴趣的朋友一看就明白。





最后希望朋友们按照我的思路自己跟一遍代码,这样您将会得到更大的收获,小弟才疏学浅,有肯定地方不到位,希望大家指正!

如果我的一点点总结,对您的有一丁点帮助,那么我将会感到莫大的欣慰!

Android4.0中,Contacts 拨号界面的分页方式是怎么样的呢?是通过传统的TabHost+tab的方式吗?

不是的,而是通过ActionBar + tab + view Pager的方式实现的。

具体的实现详情,请见下面的详细剖析。

DialtactsActivity类中,通过向ActionBar中添加Tab的方式实现分页,每页显示的内容,则通过ViewPager对象设置的Adapter对象来设置。

onCreate()方法中

顺序调用

setupDialer();

setupCallLog();

setupFavorites();

方法加载将三哥tab加载到ActionBar

下面以setupDialer 为例,逐行描述一下其加载过程

private void setupDialer() {

        final Tab tab = getActionBar().newTab();

        tab.setContentDescription(R.string.dialerIconLabel);

        tab.setTabListener(mTabListener);      

        tab.setIcon(R.drawable.ic_tab_dialer);

        getActionBar().addTab(tab);

 }

final Tab tab = getActionBar().newTab();

新建一个Tab

tab.setContentDescription(R.string.dialerIconLabel);

为该tab也设置描述,基本上没意义,也不显示,只是存储一些临时的数据

tab.setTabListener(mTabListener);

设置监听器,当点击该tab页时触发该事件

tab.setIcon(R.drawable.ic_tab_dialer);

给该tab也添加显示的图标

getActionBar().addTab(tab);

将该tab页添加到ActionBar

其它几个方法的具体实现也类似。

那么,您想想, Activity 多次调用getActionBar().addTab(tab) 添加tab都是添加到一个ActionBar中了吗?有此疑问的朋友请点击  

那么创建三个tab就可以了吗?

不是的, DialtactsActivity中又对ActionBar进行了下面的处理

getActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);

getActionBar().setDisplayShowTitleEnabled(false);

getActionBar().setDisplayShowHomeEnabled(false);

第一句将ActionBar设置为“Tab导航”模式

第二句将ActionBar设置为标题不可见

第三句将ActionBar设置为显示主界面按钮不可用

 

是不是感觉缺了点什么?tab 页内显示的内容呢?

先别急,我们继续往下走

上文提到tab.setTabListener(mTabListener),现在我们来看看这个监听器到底做了什么

 

private final TabListener mTabListener = new TabListener() {

   @Override

   public void onTabUnselected(Tab tab, FragmentTransaction ft) {

   }

 @Override

   public void onTabSelected(Tab tab, FragmentTransaction ft) {

       if (mViewPager.getCurrentItem() != tab.getPosition()) {

             mViewPager.setCurrentItem(tab.getPosition(), true);        

        }

….

    }

 

        @Override

    public void onTabReselected(Tab tab, FragmentTransaction ft) {

        }

    };

看到了吗?

  if (mViewPager.getCurrentItem() != tab.getPosition()) {

             mViewPager.setCurrentItem(tab.getPosition(), true);        

  }

tabViewPager联系到一起了。

tab.getPosition()获得当前tab的位置返回int

mViewPager.getCurrentItem()返回滑动页当前的item编号,返回int

mViewPager.setCurrentItem(tab.getPosition(), true); 设置viewPager的平滑滚动到tab设置的位置

tab的位置和ViewPager的当前页不等时,重新设置viewpager的当前页

看样子就是这了,tab页的内容肯定就是通过mViewPager来设置的。

 

下面我们继续跟进mViewPager

private ViewPager mViewPager;

首先声明mViewPager 为类的私有成员

onCreate()方法中被初始化

mViewPager = (ViewPager) findViewById(R.id.pager);

mViewPager.setAdapter(new ViewPagerAdapter(getFragmentManager()));

mViewPager.setOnPageChangeListener(mPageChangeListener);

第一句

mViewPager = (ViewPager) findViewById(R.id.pager);

我们来看类的布局文件dialtacts_activity.xml文件

在该文件中有一处

<com.android.contacts.activities.DialtactsViewPager

        android:id="@+id/pager"

        android:layout_width="match_parent"

        android:layout_height="match_parent" />

原来mViewPager 就是布局中的pager

com.android.contacts.activities.DialtactsViewPager

好像不是传统的ViewPager,难道DialtactsViewPager继承了DialtactsViewPager

答案是正确的

public class DialtactsViewPager extends ViewPager

 

好,我们接下来继续看看viewPager对象往下都做了些什么

第二句mViewPager.setAdapter(new ViewPagerAdapter(getFragmentManager()));

ViewPager对象设置了一个Adapter

我们来看看Adapter的具体内容

public class ViewPagerAdapter extends FragmentPagerAdapter {

        private DialpadFragment mDialpadFragment;

        private CallLogFragment mCallLogFragment;

        private PhoneFavoriteFragment mPhoneFavoriteFragment;

 

        public ViewPagerAdapter(FragmentManager fm) {

            super(fm);

        }

 

        @Override

        public Fragment getItem(int position) {

            switch (position) {

                case TAB_INDEX_DIALER:

                    if (mDialpadFragment == null) {

                        mDialpadFragment = new DialpadFragment();

                    }

                    return mDialpadFragment;

                         

            }

            throw new IllegalStateException("No fragment at position " + position);

        }

 

        @Override

        public int getCount() {

            return TAB_INDEX_COUNT;

        }

    }

哦,我明白了,原来在Adapter里面建立了三种Fragment对象

然后通过当前的选中位置来返回当前页是哪个fragment对象,在fragment对象里面设置页面的具体内容。

 

既然Adapter设置好了,已经可以滑动了,也就是已经可以实现切换效果了,google接下来还要干什么了呢?

mViewPager.setOnPageChangeListener(mPageChangeListener);

  private class PageChangeListener implements OnPageChangeListener {

     private int mCurrentPosition = -1;

     private int mNextPosition = -1;

 

     @Override

     public void onPageScrolled(

            int position, float positionOffset, int positionOffsetPixels) {

     }

      @Override

      public void onPageSelected(int position) {

         final ActionBar actionBar = getActionBar();

         if (mCurrentPosition == position) {

           Log.w(TAG, "Previous position and next position became same (" + position + ")");

         }

         actionBar.selectTab(actionBar.getTabAt(position));

         mNextPosition = position;

      }

      public void setCurrentPosition(int position) {

            mCurrentPosition = position;

      }

      @Override

      public void onPageScrollStateChanged(int state) {

            switch (state) {

                case ViewPager.SCROLL_STATE_IDLE: {

                    if (mCurrentPosition >= 0) {

                        sendFragmentVisibilityChange(mCurrentPosition, false);

                    }

                    if (mNextPosition >= 0) {

                        sendFragmentVisibilityChange(mNextPosition, true);

                    }

                    invalidateOptionsMenu();

 

                    mCurrentPosition = mNextPosition;

                    break;

                }

                case ViewPager.SCROLL_STATE_DRAGGING:

                case ViewPager.SCROLL_STATE_SETTLING:

                default:

                    break;

            }

        }

    }

private Fragment getFragmentAt(int position) {

        switch (position) {

            case TAB_INDEX_DIALER:

                return mDialpadFragment;

            case TAB_INDEX_CALL_LOG:

                return mCallLogFragment;

            case TAB_INDEX_FAVORITES:

                return mPhoneFavoriteFragment;

            default:

                throw new IllegalStateException("Unknown fragment index: " + position);

        }

    }

 

    private void sendFragmentVisibilityChange(int position, boolean visibility) {

        final Fragment fragment = getFragmentAt(position);

        if (fragment instanceof ViewPagerVisibilityListener) {

            ((ViewPagerVisibilityListener) fragment).onVisibilityChanged(visibility);

        }

}

通过上面的操作,了解到PageChangeListener 的主要作用是在页面滑动过程中,menu菜单键有可见到不可见的。

最后希望朋友们按照我的思路自己跟一遍代码,这样您将会得到更大的收获,小弟才疏学浅,有肯定地方不到位,希望大家指正!

如果我的一点点总结,对您的有一丁点帮助,那么我将会感到莫大的欣慰!




看完了拨号盘界面的实现后,大家可能会感觉到,挺简单的,没什么复杂的,只是在onCreateView方法中加载了一个layout,然后就全都看到了,so easy!

那么,我们接下来就分享一下稍微复杂一点的CallLog界面,希望大家看完后,依然会如此说!

   DialtactsActivity中通话记录对应的FragmentCallLogFragment

下面我们先来看看通话记录的默认显示,下图1

在Android4.0中Contacts通话记录界面剖析(源码)

 

                 1  通话记录页面

   咋一看,该页面分为两部分,tab titlelist列表页面

   Tabtitle 就不再复说了,与上文中的拨号界面的实现方式完全相同,现主要描述通话记录的list页面,实际上该页面还会出现通话记录为空的界面等。

通话记录页面的实现

现在我们直接来看onCreateView方法

在该方法第一句

   View view = inflater.inflate(R.layout.call_log_fragment, container, false);

跟进call_log_fragment.xml

发现

<ListView android:id="@android:id/list"

            android:layout_width="match_parent"

            android:layout_height="match_parent"

            android:fadingEdge="none"

            android:scrollbarStyle="outsideOverlay"

            android:divider="@null"

  />

估计该listView就是要显示的列表

跟进代码

奇怪了,CallLogFragment代码中怎么就没有调用list的呢?

如果这条线断了,就不知道往下该咋弄了啊! ~

原来是这样子的,CallLogFragment继承自ListFragment而在ListFragment中的ensureList方法中进行了初始化,具体的语句如下:

View rawListView = root.findViewById(android.R.id.list); 

mList = (ListView)rawListView;

rawListView获得的list的名字是android.R.id.list,而我们的布局文件中list的名字是@android:id/list,没错了吧。

而在我们的CallLogFragment中只要调用getListView即可获得listView

为了避免您的怀疑,我们在看一下ListFragmentgetListView方法:

public ListView getListView() {

        ensureList();

        return mList;

 }

既然找到了list,接下来我们要看看数据的绑定了,

一提到数据的绑定,立即就会想到Adapter,下面我们看看Android源码是怎么写的

onViewCreated方法中,我发现了如下的几句话

String currentCountryIso = ContactsUtils.getCurrentCountryIso(getActivity());

mAdapter = new CallLogAdapter(getActivity(), this, new ContactInfoHelper(getActivity()

, currentCountryIso), mVoiceMailNumber);

setListAdapter(mAdapter);

第二句创建了一个CallLogAdapter对象,然后调用setListAdapter方法,将CallLogAdapter对象设置过去,

不用说setListAdapter方法肯定是ListFragment的方法,看代码

public void setListAdapter(ListAdapter adapter) {

        boolean hadAdapter = mAdapter != null;

        mAdapter = adapter;

        if (mList != null) {

            mList.setAdapter(adapter);

            if (!mListShown && !hadAdapter) {

                // The list was hidden, and previously didn't have an

                // adapter.  It is now time to show it.

                setListShown(true, getView().getWindowToken() != null);

            }

        }

}

代码中对我们来说最重要的一句 

 mList.setAdapter(adapter);

mList对应于我们布局文件中的list,没错了吧,看样一切的秘密都在这个adapter中了,继续跟进adapter

Adapter怎么会没有getView呢?

我们看CallLogAdapter的父类,GroupingListAdapter,在该方法中有getView方法,

public View getView(int position, View convertView, ViewGroup parent) {

                          

        if (view == null) {

            switch (mPositionMetadata.itemType) {

                case ITEM_TYPE_STANDALONE:

                    view = newStandAloneView(mContext, parent);

                    break;

                case ITEM_TYPE_GROUP_HEADER:

                    view = newGroupView(mContext, parent);

                    break;

                case ITEM_TYPE_IN_GROUP:

                    view = newChildView(mContext, parent);

                    break;

            }

        }

      

        return view;

    }

原来是调用了newStandAloneViewnewGroupViewnewChildView方法,来创建view

那么我们接下来继续回到CallLogAdapter中查看上述三个方法所加载的布局文件

call_log_list_item.xml

呵呵,一看该布局,不会有错了,就是该list的分项

 

Ok,简单吧


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值