首页 > 代码库 > Android7.0 拨号盘应用源码分析(一) 界面浅析

Android7.0 拨号盘应用源码分析(一) 界面浅析

前言

android拨号盘的源码目录在package/app/Dialer

自7.0以后Incallui的源码直接放到了Dialer目录下,虽然在7.0以前incallui有自己独立的目录,但实际编译过程中只是作为链接库最后还是被编译到Dialer的apk里

博主这里只取Dialer相关的源码并导入AS中,并稍作调整兼容至L

源码目录结构如下:

技术分享

先理一理各个工程的依赖关系

com.android.dialer是主工程依赖于

com.android.contacts.common工程和com.android.phone.common工程

com.android.contacts.common又依赖于

com.android.phone.common工程和com.android.common工程

另外一些support包也作为链接工程被引入,以上代码均取自google源码

github下载链接:https://github.com/geniusgithub/AndroidDialer

1.1拨号盘概览

先来看看几张原图

技术分享

技术分享

1.2 DialtactsActivity

技术分享

主activity为DialtactsActivity

com.android.dialer.DialtactsActivity
public class DialtactsActivity extends TransactionSafeActivity 。。。{

    // Fragment containing the dialpad that slides into view
    protected DialpadFragment mDialpadFragment;
   
    // Fragment for searching phone numbers using the alphanumeric keyboard.
    private RegularSearchFragment mRegularSearchFragment;

    // Fragment for searching phone numbers using the dialpad.
    private SmartDialSearchFragment mSmartDialSearchFragment;

    // Fragment containing the speed dial list, call history list, and all contacts list.    
    private ListsFragment mListsFragment;

    private DialerDatabaseHelper mDialerDatabaseHelper;

    private FloatingActionButtonController mFloatingActionButtonController;

    ...... ......
    ...... .....
 }

如类图关系所示,主要有以下几个关键的成员变量

com.android.dialer.dialpad .DialpadFragment // 拨号盘fragment

com.android.dialer.list.RegularSearchFragment // 联系人搜索fragment

com.android.dialer.list.SmartDialSearchFragment // 拨号搜索fragment

com.android.dialer.list.ListsFragment // TAB页fragment,包含快速联系人,最近通话记录,联系人列表三个子fragment

com.android.dialer.database.DialerDatabaseHelper // 拨号搜索数据库SQLiteOpenHelper对象

com.android.contacts.common.widget.FloatingActionButtonController // 悬浮按钮控制器

再看看onCreate里的主要实现(部分内容省略)

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setContentView(R.layout.dialtacts_activity);

    final ActionBar actionBar = getSupportActionBar();
    actionBar.setCustomView(R.layout.search_edittext);
    // 给actionbar设置自定义view (SearchEditTextLayout)
    SearchEditTextLayout searchEditTextLayout = (SearchEditTextLayout) actionBar
    .getCustomView().findViewById(R.id.search_view_container);

    // 给SearchEditTextLayout添加管理器ActionBarController
    mActionBarController = new ActionBarController(this, searchEditTextLayout);
   
  final View floatingActionButtonContainer = findViewById(
            R.id.floating_action_button_container);
  ImageButton floatingActionButton = (ImageButton) findViewById(R.id.floating_action_button);
  floatingActionButton.setOnClickListener(this);
   // 用FloatingActionButtonController管理悬浮按钮
  mFloatingActionButtonController = new FloatingActionButtonController(this,
     floatingActionButtonContainer, floatingActionButton);

   // 添加ListsFragment
    getFragmentManager().beginTransaction()
        .add(R.id.dialtacts_frame, new ListsFragment(), TAG_FAVORITES_FRAGMENT)
        .commit();

  // 初始化单例对象DialerDatabaseHelper
   mDialerDatabaseHelper = DatabaseHelperManager.getDatabaseHelper(this);
  SmartDialPrefix.initializeNanpSettings(this);

}

1.3 ListsFragment

ListsFragment是主fragment,结构如下

public class ListsFragment extends Fragment{
 
    private ViewPager mViewPager;
    private ViewPagerTabs mViewPagerTabs;  
    // 自定义TAB标签,继承自HorizontalScrollView
    private ViewPagerAdapter mViewPagerAdapter;

    // 拖拽常用联系人时悬浮视图
    private RemoveView mRemoveView;
    private View mRemoveViewContent;

    // 常用联系人fragment
    private SpeedDialFragment mSpeedDialFragment;

    // 最近通话记录fragment
    private CallLogFragment mHistoryFragment;

    // 联系人列表fragment
    private AllContactsFragment mAllContactsFragment;

    // Voicemail列表fragment
    private CallLogFragment mVoicemailFragment;

   @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
   
        final View parentView = inflater.inflate(R.layout.lists_fragment, container, false);

        mViewPager = (ViewPager) parentView.findViewById(R.id.lists_pager);
        mViewPagerAdapter = new ViewPagerAdapter(getChildFragmentManager());
        mViewPager.setAdapter(mViewPagerAdapter);
        mViewPager.setOffscreenPageLimit(TAB_COUNT_WITH_VOICEMAIL - 1);
        mViewPager.setOnPageChangeListener(this);
        showTab(TAB_INDEX_SPEED_DIAL);

        ...... ......  ...... ......

        mViewPagerTabs = (ViewPagerTabs) parentView.findViewById(R.id.lists_pager_header);
        mViewPagerTabs.configureTabIcons(mTabIcons);
        mViewPagerTabs.setViewPager(mViewPager);
        addOnPageChangeListener(mViewPagerTabs);

        mRemoveView = (RemoveView) parentView.findViewById(R.id.remove_view);
        mRemoveViewContent = parentView.findViewById(R.id.remove_view_content);

        return parentView;
    }
}

ListsFragment最多可以显示四个fragment,有个VisualVoicemailCallLogFragment显示一种特定的通话记录(提供视频语音邮件服务)

类型为Calls.VOICEMAIL_TYPE,需要运营商支持,只有存在该类通话记录才会显示该TAB页,国内运营商暂不支持

技术分享

SpeedDialFragment显示常用联系人列表

public class SpeedDialFragment extends Fragment ...{

    // 显示数据的GridView列表
    private PhoneFavoriteListView mListView;    

   // 源数据BaseAdapter
    private PhoneFavoritesTileAdapter mContactTileAdapter;

  // 查询源数据的LoaderCallbacks
  private class ContactTileLoaderListener implements LoaderManager.LoaderCallbacks<Cursor> {
        @Override
        public CursorLoader onCreateLoader(int id, Bundle args) {
            if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onCreateLoader.");
            return ContactTileLoaderFactory.createStrequentPhoneOnlyLoader(getActivity());
        }

        @Override
        public void onl oadFinished(Loader<Cursor> loader, Cursor data) {
            if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onLoadFinished");
            mContactTileAdapter.setContactCursor(data);
            setEmptyViewVisibility(mContactTileAdapter.getCount() == 0);
        }

        @Override
        public void onl oaderReset(Loader<Cursor> loader) {
            if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onLoaderReset. ");
        }
    }
}

使用LoadManager方式获取cursor数据,查询ContactsProvider数据库的data表

com.android.contacts.common.ContactTileLoaderFactory
public static CursorLoader createStrequentPhoneOnlyLoader(Context context) {
     Uri uri = Contacts.CONTENT_STREQUENT_URI.buildUpon()
          .appendQueryParameter(ContactsContract.STREQUENT_PHONE_ONLY, "true").build();

     return new CursorLoader(context, uri, COLUMNS_PHONE_ONLY, null, null, null);
}

数据来源包括收藏的联系人以及有通话记录的联系人

1.4 DialpadFragment

DialpadFragment显示拨号盘fragment

在DialtactsActivity中添加如下

private void showDialpadFragment(boolean animate) {

  if (mDialpadFragment == null) {
            mDialpadFragment = new DialpadFragment();
            ft.add(R.id.dialtacts_container, mDialpadFragment, TAG_DIALPAD_FRAGMENT);
        } else {
            ft.show(mDialpadFragment);
        }
 }

第一次显示时动态添加进去,后续动态控制显示隐藏

public class DialpadFragment extends Fragment{
 
  private DialpadView mDialpadView; // 拨号数字面板(包括输入号码框)
  private EditText mDigits;          // 输入号码框

  private ToneGenerator mToneGenerator; // DTMF音播放器
  private ListView mDialpadChooser;     // 通话状态时显示的视图
  private DialpadChooserAdapter mDialpadChooserAdapter;
  // 通话状态时显示的视图adapter

  @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {

        // 横竖屏加载不同的布局
        final View fragmentView = inflater.inflate(R.layout.dialpad_fragment, container,
                false);
        fragmentView.buildLayer();

        mDialpadView = (DialpadView) fragmentView.findViewById(R.id.dialpad_view);
        mDialpadView.setCanDigitsBeEdited(true);
        mDigits = mDialpadView.getDigits();
        ...... ........... ......
        PhoneNumberFormatter.setPhoneNumberFormattingTextWatcher(getActivity(), mDigits);  // 格式化输入框中的号码
        // Check for the presence of the keypad
        View oneButton = fragmentView.findViewById(R.id.one);
        if (oneButton != null) {  // 绑定各个数字按键onPress事件
            configureKeypadListeners(fragmentView);
        }
       ...... ............ ......
        mDialpadChooser = (ListView) fragmentView.findViewById(R.id.dialpadChooser);
        mDialpadChooser.setOnItemClickListener(this);
        ...... ..... ...... ......
        return fragmentView;
    }

}

横屏和竖屏所加载的拨号面板布局是不一样的

技术分享技术分享

DialpadView是个自定义视图,主要用于显示数字按键和输入号码框

public class DialpadView extends LinearLayout {

    private EditText mDigits;     // 输入号码框
    private ImageButton mDelete; // 删除按钮

    private void setupKeypad() {
        ...... ............ ......
        DialpadKeyButton dialpadKey;
        TextView numberView;
        TextView lettersView;
         ...... ............ ......
        for (int i = 0; i < mButtonIds.length; i++) {
            dialpadKey = (DialpadKeyButton) findViewById(mButtonIds[i]);
            numberView = (TextView) dialpadKey.findViewById(R.id.dialpad_key_number);
            lettersView = (TextView) dialpadKey.findViewById(R.id.dialpad_key_letters);
          ...... ............ ......
            final RippleDrawable rippleBackground = (RippleDrawable)
                    getDrawableCompat(getContext(), R.drawable.btn_dialpad_key);
            if (mRippleColor != null) {
                rippleBackground.setColor(mRippleColor);            }

            numberView.setText(numberString);
            numberView.setElegantTextHeight(false);
            dialpadKey.setContentDescription(numberContentDescription);
            dialpadKey.setBackground(rippleBackground); // 设置数字按键水波纹背景色


            if (lettersView != null) {
                lettersView.setText(resources.getString(letterIds[i]));
            }
        }
         ...... ............ ......
    }
public void animateShow() {  // 显示拨号面板时各个数字按键的动画效果
            ...... ............ ......
        for (int i = 0; i < mButtonIds.length; i++) {
             ...... ............ ......
            ViewPropertyAnimator animator = dialpadKey.animate();
            if (mIsLandscape) {
                // Landscape orientation requires translation along the X axis.
                // For RTL locales, ensure we translate negative on the X axis.
                dialpadKey.setTranslationX((mIsRtl ? -1 : 1) * mTranslateDistance);
                animator.translationX(0);
            } else {
                // Portrait orientation requires translation along the Y axis.
                dialpadKey.setTranslationY(mTranslateDistance);
                animator.translationY(0);
            }
            animator.setInterpolator(AnimUtils.EASE_OUT_EASE_IN)
                    .setStartDelay(delay)
                    .setDuration(duration)
                    .setListener(showListener)
                    .start();
        }
    }
}

当处于通话状态时显示如下

技术分享

1.5SmartDialSearchFragment RegularSearchFragment

SmartDialSearchFragment显示拨号搜索结果fragment(在拨号面板输入数字时显示)

RegularSearchFragment显示联系人搜索结果fragment(在actionbar输入框输入字符时显示)

在DialtactsActivity中进入或退出搜索模式时动态添加移除

private void enterSearchUi(boolean smartDialSearch, String query, boolean animate) {
         ...... ............ ......
        if (fragment == null) {
            if (smartDialSearch) {
                fragment = new SmartDialSearchFragment();
            } else {
                fragment = ObjectFactory.newRegularSearchFragment();
                  ...... ............ ......
            }
            transaction.add(R.id.dialtacts_frame, fragment, tag);
        } else {
            transaction.show(fragment);
        }
         ...... ............ ......
    }
    private void exitSearchUi() {
         ...... ............ ......
        final FragmentTransaction transaction = getFragmentManager().beginTransaction();
        if (mSmartDialSearchFragment != null) {
            transaction.remove(mSmartDialSearchFragment);
        }
        if (mRegularSearchFragment != null) {
            transaction.remove(mRegularSearchFragment);
        }
        transaction.commit();

        mListsFragment.getView().animate().alpha(1).withLayer();
         ...... ............ ......
        mActionBarController.onSearchUiExited();
    }

技术分享

拨号搜素只能通过拨号面板的输入数字,支持T9搜索,但是原生不支持拼音检索

public class SmartDialSearchFragment extends SearchFragment{

    @Override
    protected ContactEntryListAdapter createListAdapter() {
        SmartDialNumberListAdapter adapter = 
            new SmartDialNumberListAdapter(getActivity());
        adapter.setUseCallableUri(super.usesCallableUri());
        adapter.setQuickContactEnabled(true);
        // Set adapter‘s query string to restore previous instance state.
        adapter.setQueryString(getQueryString());
        adapter.setListener(this);
        return adapter;
    }

    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        // Smart dialing does not support Directory Load, falls back to normal search instead.
        if (id == getDirectoryLoaderId()) {
            return super.onCreateLoader(id, args);
        } else {
            final SmartDialNumberListAdapter adapter = 
                 (SmartDialNumberListAdapter) getAdapter();
            SmartDialCursorLoader loader = new SmartDialCursorLoader(super.getContext());
            adapter.configureLoader(loader);
            return loader;
        }
    }
}

技术分享

联系人搜索则通过软键盘输入,不过不支持T9搜索

public class RegularSearchFragment extends SearchFragment{

  @Override
    protected ContactEntryListAdapter createListAdapter() {
        RegularSearchListAdapter adapter = new RegularSearchListAdapter(getActivity());
        adapter.setDisplayPhotos(true);
        adapter.setUseCallableUri(usesCallableUri());
        adapter.setListener(this);
        return adapter;
    }
}

从类关系图上可以得知两个fragment和对应的adapter都继承于同一个父类,最终都派生自ContactsCommon工程里的模板类ContactEntryListFragment

public abstract class ContactEntryListFragment<T extends ContactEntryListAdapter>
        extends Fragment{

    private T mAdapter;          // 模板adapter
    private View mView;
    private ListView mListView;

    private ContactPhotoManager mPhotoManager;  // 头像管理


    protected abstract View inflateView(LayoutInflater inflater, ViewGroup container);
    protected abstract T createListAdapter();        // 子类中实现具体adapter

    @Override   // 子类可重写获取数据的Loader
    public Loader<Cursor> onCreateLoader(int id, Bundle args) { 
        if (id == DIRECTORY_LOADER_ID) {
            DirectoryListLoader loader = new DirectoryListLoader(mContext);
            loader.setDirectorySearchMode(mAdapter.getDirectorySearchMode());
            loader.setLocalInvisibleDirectoryEnabled(
                    ContactEntryListAdapter.LOCAL_INVISIBLE_DIRECTORY_ENABLED);
            return loader;
        } else {
            CursorLoader loader = createCursorLoader(mContext);
            long directoryId = args != null && args.containsKey(DIRECTORY_ID_ARG_KEY)
                    ? args.getLong(DIRECTORY_ID_ARG_KEY)
                    : Directory.DEFAULT;
            mAdapter.configureLoader(loader, directoryId);
            return loader;
        }
    }

}

ContactEntryListFragment内部封装了很多操作,绑定了ContactEntryListAdapter,具体细节就不在这里详述了

1.6小结

最后附上Dialer里主要类图

技术分享

Android7.0 拨号盘应用源码分析(一) 界面浅析