Android 6.0撥號介面號碼格式化
需求及分析
客戶需求:
使用hierarchyviewer工具可以發現這個介面對應的activity是DialtactsActivity.
通過搜尋撥號盤的source id(dialpad_view)找到dialpad_fragment.xml,從而找到DialpadFragment.java。
最後在Dialpad_view.xml裡面自定義了一個EditText類來容納撥號的內容:
<view class="com.android.phone.common.dialpad.DigitsEditText"
xmlns:ex="http://schemas.android.com/apk/res-auto"
android:id="@+id/digits"
public class DigitsEditText extends ResizingTextEditText
所以,主要的函式邏輯在DialpadFragment.java裡面。
線索
在DialpadFragment裡面全域性搜尋mDigits。在初始化函式onCreateView裡面,對mDigits註冊了對輸入內容變化的監聽器。
mDigits.addTextChangedListener(this);
PhoneNumberFormatter.setPhoneNumberFormattingTextWatcher (getActivity(), mDigits);
在PhoneNumberFormatter中會啟動一個TextWatcherLoadAsyncTask來另起一個執行緒來進行數字的分割。
//PhoneNumberFormatter.java
public static final void setPhoneNumberFormattingTextWatcher(Context context, TextView textView) {
new TextWatcherLoadAsyncTask(GeoUtil.getCurrentCountryIso(context), textView).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null );
}
注意,使用國家ISO碼以及mDigits這個TextView來初始化這個物件。這樣在該物件中就可以直接操作TextView的內容了。而在這個AsyncTask中會例項化一個PhoneNumberFormattingTextWatcher物件來對變化的數字進行格式化。
最後的數字格式處理是在這個TextWatcher的afterTextChanged中進行處理。
//PhoneNumberFormattingTextWatcher.java
public synchronized void afterTextChanged(Editable s) {
String formatted = reformat(s, Selection.getSelectionEnd(s));
if (formatted != null) {
int rememberedPos = mFormatter.getRememberedPosition();
s.replace(0, s.length(), formatted, 0, formatted.length());
}
}
針對通過Intent,啟動撥號介面顯示的號碼,我們在DialpadFragment中可以找到fillDigitsIfNecessary。這個函式中,該函式接收通過intent呼叫傳送過來的號碼,並且呼叫下面的setFormattedDigits來格式化這個號碼,並且顯示在mDigits這個TextView中。
/** DialpadFragment.java
* Sets formatted digits to digits field.
*/
private void setFormattedDigits(String data, String normalizedNumber) {
// strip the non-dialable numbers out of the data string.
String dialString = PhoneNumberUtils.extractNetworkPortion(data);
dialString =
PhoneNumberUtils.formatNumber(dialString, normalizedNumber, mCurrentCountryIso);
if (!TextUtils.isEmpty(dialString)) {
Editable digits = mDigits.getText();
digits.replace(0, digits.length(), dialString);
// for some reason this isn't getting called in the digits.replace call above..
// but in any case, this will make sure the background drawable looks right
afterTextChanged(digits);
}
}
這兩個的共同點是都使用了CountryIso這個引數。所以數字是按照不同的國家,顯示不同的樣式的。
這兩個分支中CountryIso都是通過下面的程式碼獲取的:
mCurrentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
//packages/apps/contactcommon/src/com/android/contacts/common/location/CountryDetector.java
public String getCurrentCountryIso() {
String result = null;
if (isNetworkCountryCodeAvailable()) {
result = getNetworkBasedCountryIso();
Log.w(TAG, "getNetworkBasedCountryIso." + result);
}
if (TextUtils.isEmpty(result)) {
result = getLocationBasedCountryIso();
Log.w(TAG, "getLocationBasedCountryIso." + result);
}
if (TextUtils.isEmpty(result)) {
result = getSimBasedCountryIso();
Log.w(TAG, "getSimBasedCountryIso." + result);
}
if (TextUtils.isEmpty(result)) {
result = getLocaleBasedCountryIso();
Log.w(TAG, "getLocaleBasedCountryIso." + result);
}
if (TextUtils.isEmpty(result)) {
result = DEFAULT_COUNTRY_ISO;
}
Log.w(TAG, "DEFAULT_COUNTRY_ISO." + result);
//for debug
result = "BR";
return result.toUpperCase(Locale.US);
}
這個值是由很多條件判斷來決定的。先判斷當前連線上的網路所屬的國家。如果為空,判斷當前的物理位置所在的國家。如果依然為空,獲取Sim卡所屬的國家。依然為空,判斷當前選擇的語言Locale所屬的國家。否則就預設使用DEFAULT_COUNTRY_ISO = “US”作為國家碼。
注意,上面的Log列印語句都是新增用來驗證國家碼是否會影響分割方法的。
修改之後輸出結果為:
W/CountryDetector( 3948): getLocationBasedCountryIso.null
W/CountryDetector( 3948): getSimBasedCountryIso.
W/CountryDetector( 3948): getLocaleBasedCountryIso.US
W/CountryDetector( 3948): DEFAULT_COUNTRY_ISO.US
說明Location和Sim都是為null,而LocaleBaseCountryIso則為US。
getLocaleBasedCountryIso()這個函式還有點意思,是通過屬性中的值類判定使用者屬於哪個locale的:
//Locale.java
public static Locale getDefaultLocaleFromSystemProperties() {
final String languageTag = System.getProperty("user.locale", "");
final Locale defaultLocale;
if (!languageTag.isEmpty()) {
defaultLocale = Locale.forLanguageTag(languageTag);
} else {
String language = System.getProperty("user.language", "en");
String region = System.getProperty("user.region", "US");
String variant = System.getProperty("user.variant", "");
defaultLocale = new Locale(language, region, variant);
}
return defaultLocale;
}
然後通過defaultLocale.getCountry()來獲取國家。第二個,也就是region實際上就是country的值。
查一下“String region = System.getProperty(“user.region”, “US”);”這個值是怎麼獲取的(其實也就是systemProperties.getProperty(name, defaultValue))。
在liccore/luni/src/main/java/java/lang/System.java裡面有如下語句:
static {
in = new BufferedInputStream(new FileInputStream(FileDescriptor.in));
unchangeableSystemProperties = initUnchangeableSystemProperties();
systemProperties = createSystemProperties();//會將unchangeableSystemProperties的內容新增到systemProperties裡面
addLegacyLocaleSystemProperties();
}
在addLegacyLocaleSystemProperties裡面,會根據user.locale的值來設定一些值,如下:
private static void addLegacyLocaleSystemProperties() {
final String locale = getProperty("user.locale", "");
if (!locale.isEmpty()) {
Locale l = Locale.forLanguageTag(locale);
setUnchangeableSystemProperty("user.language", l.getLanguage());
setUnchangeableSystemProperty("user.region", l.getCountry());
setUnchangeableSystemProperty("user.variant", l.getVariant());
} else {
// If "user.locale" isn't set we fall back to our old defaults of
// language="en" and region="US" (if unset) and don't attempt to set it.
// The Locale class will fall back to using user.language and user.region if unset.
final String language = getProperty("user.language", "");
final String region = getProperty("user.region", "");
if (language.isEmpty()) { setUnchangeableSystemProperty("user.language", "en");
}
if (region.isEmpty()) {
setUnchangeableSystemProperty("user.region", "US");
}}}
其實就是根據user.locale的值來設定user.language和user.region的值。
總結
通過上面的分析,我們知道撥號介面數字的分割是按照當前網路所屬的國家來顯示。如果針對特定國家的專案,可以通過設定user.locale來改變撥號介面數字的分割方式,以適應相關的客戶需求。這個分割方式都是google提供的原生方式,推薦不要修改。當然如果客戶有特殊需求,那麼就需要修改PhoneNumberFormattingTextWatcher裡面afterTextChanged函式中的reformat方法,來滿足客戶需求。