之前在公司有需要做高仿的项目,恰好分配到了Settings的修改
一级菜单效果图如下:
Settings的修改简单来说,主要是资源的一些替换,各种Preference的定制。当然,还有一些零星的其他问题。这里我会把自己碰到的最后解决了的一些问题贴出来。
1.资源的替换
首先修改下Android.mk
LOCAL_RESOURCE_DIR:= $(LOCAL_PATH)/res
LOCAL_RESOURCE_DIR+= $(LOCAL_PATH)/res_ext
修改为
LOCAL_RESOURCE_DIR:=$(LOCAL_PATH)/res_iphone$(LOCAL_PATH)/res
LOCAL_RESOURCE_DIR+= $(LOCAL_PATH)/res_ext
新增一个资源目录,并优先读取,这样在保留settings原资源的同时又一目了然。
2.一级菜单的item的差异化设计
我们知道AndroidM上Settings一级菜单加载的依然是Settings \res\layout\dashboard.xml
dashboard.xml中只有一个id为dashboard_container的容器,然后加载Settings \res\xml\dashboard_categories.xml去生成布局。
dashboard_categories.xml中标签有两种:dashboard-category和dashboard-tile
我们参考Iphone的Settings发现还有其他类型的item,比如账户这个item的高度比较大,飞行模式这个item需要加入一个switch,wifi和蓝牙的item还有一个textview来反应实时状态。
那么如何来实现这种差异化呢?
我当时的第一种做法是dashboard.xml中去添加,写一些布局控制的和dashboard-tile
的外观一致,然后去代码里面拿到控件后去写事件。
这样做有两个问题:一是代码的布局结构混乱,难以阅读;二是无法在dashboard_categories中去穿插差异化的item,这么写只是在dashboard_categories的所有item之上加差异化的item,有局限性。
后来采用的是第二种方式,为dashboard-tile新增类型的属性。
实现方式如下:
1.在\frameworks\base\core\res\res\values\attrs.xml为PreferenceHeader新增属性
<declare-styleablename="PreferenceHeader">
<!-- Identifier value for theheader. -->
<attr name="id" />
<!-- The title of the item that isshown to the user. -->
<attr name="title" />
<!-- The summary for the item.-->
<attr name="summary"format="string" />
<!-- The title for the bread crumbof this item. -->
<attrname="breadCrumbTitle" format="string" />
<!-- The short title for the breadcrumb of this item. -->
<attrname="breadCrumbShortTitle" format="string" />
<!-- An icon for the item. -->
<attr name="icon" />
<!-- The fragment that is displayedwhen the user selects this item. -->
<attr name="fragment"format="string" />
<!--liuqipeng add20170815-->
<attr name="qsItemType"/>
<!--liuqipeng end 20170815-->
</declare-styleable>
<attr name="qsItemType">
<flag name="withArrow"value="1" />
<flag name="withSwitch"value="2" />
<flag name="withArrowAndText"value="3" />
<flagname="withArrowForAccount" value="4" />
</attr>
2. \frameworks\base\core\res\res\values\public.xml中把qsItemType这个属性公开
<publictype="attr" name="qsItemType" id="0x01010500"/>
后面的id自己根据public.xml中的值依次递增的写就ok
3. Settings\res\xml\dashboard_categories.xml中引入这个属性
例如:
<dashboard-tile
android:id="@+id/iphone_flight_mode_settings"
android:title="@string/airplane_mode"
android:icon="@drawable/iphone_icon_settings_airplane"
android:qsItemType="withSwitch"
/>
<dashboard-tile
android:id="@+id/wifi_settings"
android:title="@string/wifi_settings_title"
android:fragment="com.android.settings.wifi.WifiSettings"
android:icon="@drawable/iphone_icon_settings_wifi"
android:qsItemType="withArrowAndText"
/>
4. Settings\src\com\android\settings\dashboard\DashboardTile.java照葫芦画瓢添加qsItemType
//liuqipeng add
public int qsItemType;
//liuqipeng end
public DashboardTile() {
// Empty
}
//liuqipeng add
public int getQsItemType() {
return qsItemType;
}
//liuqipeng end
@Override
public int describeContents() {
return 0;
}
//liuqipeng add
dest.writeInt(qsItemType);
//liuqipeng end
TextUtils.writeToParcel(summary, dest, flags);
//liuqipeng add
qsItemType = in.readInt();
//liuqipeng end
summary = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
5. Settings\src\com\android\settings\dashboard\DashboardTileView.java添加qsItemType
这个类修改先是根据qsItemType的不同值来加载不同的布局
然后对每个item的点击做了下优化,避免了item点击后的intent不可用导致settings报错。
注意:DashboardTileView.java这个类中我们对构造方法做了修改,新增了一个形参,用于区分不同的item布局。那么DashboardTileView实例化的位置也需要同步修改,对吧。
DashboardTileView实例化的位置在Settings\src\com\android\settings\dashboard\DashboardSummary.java,这一处的修改放到最后讲。
package com.android.settings.dashboard;
import android.app.Activity;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import com.android.settings.ProfileSelectDialog;
import com.android.settings.R;
import com.android.settings.Utils;
//liuqipeng add
import android.content.pm.PackageManager;
import java.util.List;
import android.content.Intent;
//liuqipeng end
public class DashboardTileView extends FrameLayout implements View.OnClickListener {
private static final int DEFAULT_COL_SPAN = 1;
private ImageView mImageView;
private TextView mTitleTextView;
private TextView mStatusTextView;
private View mDivider;
//liuqipeng add
private int mLayoutType;
private Context mContext;
//liuqipeng end
private int mColSpan = DEFAULT_COL_SPAN;
private DashboardTile mTile;
public DashboardTileView(Context context) {
//liuqipeng change
//this(context, null);
this(context, null,1);
//liuqipeng end
}
//liuqipeng can expand xingcan,pass some params from DashboardSummary,then inflate diff layout
public DashboardTileView(Context context, AttributeSet attrs,int layoutType) {
super(context, attrs);
//liuqipeng add
mContext=context;
mLayoutType=layoutType;
View view;
switch (mLayoutType) {
case 1:
view = LayoutInflater.from(context).inflate(R.layout.dashboard_tile, this);
break;
case 2:
view = LayoutInflater.from(context).inflate(R.layout.dashboard_tile_with_switch, this);
break;
case 3:
view = LayoutInflater.from(context).inflate(R.layout.dashboard_tile_with_arrow_and_text, this);
break;
case 4:
view = LayoutInflater.from(context).inflate(R.layout.dashboard_tile_with_arrow_for_account, this);
break;
default:
view = LayoutInflater.from(context).inflate(R.layout.dashboard_tile, this);
break;
}
//final View view = LayoutInflater.from(context).inflate(R.layout.dashboard_tile, this);
//liuqipeng end
mImageView = (ImageView) view.findViewById(R.id.icon);
mTitleTextView = (TextView) view.findViewById(R.id.title);
mStatusTextView = (TextView) view.findViewById(R.id.status);
mDivider = view.findViewById(R.id.tile_divider);
setOnClickListener(this);
setBackgroundResource(R.drawable.dashboard_tile_background);
setFocusable(true);
}
public TextView getTitleTextView() {
return mTitleTextView;
}
public TextView getStatusTextView() {
return mStatusTextView;
}
public ImageView getImageView() {
return mImageView;
}
public void setTile(DashboardTile tile) {
mTile = tile;
}
public void setDividerVisibility(boolean visible) {
mDivider.setVisibility(visible ? View.VISIBLE : View.GONE);
}
void setColumnSpan(int span) {
mColSpan = span;
}
int getColumnSpan() {
return mColSpan;
}
@Override
public void onClick(View v) {
if (mTile.fragment != null) {
Utils.startWithFragment(getContext(), mTile.fragment, mTile.fragmentArguments, null, 0,
mTile.titleRes, mTile.getTitle(getResources()));
//liuqipeng add isIntentAvailable
} else if (mTile.intent != null&&isIntentAvailable(mTile.intent)) {
//liuqipeng end isIntentAvailable avoid exception
int numUserHandles = mTile.userHandle.size();
if (numUserHandles > 1) {
ProfileSelectDialog.show(((Activity) getContext()).getFragmentManager(), mTile);
} else if (numUserHandles == 1) {
getContext().startActivityAsUser(mTile.intent, mTile.userHandle.get(0));
} else {
getContext().startActivity(mTile.intent);
}
}
}
//liuqipeng add
public boolean isIntentAvailable(Intent intent) {
PackageManager packageManager = mContext.getPackageManager();
List list = packageManager.queryIntentActivities(intent,PackageManager.MATCH_DEFAULT_ONLY);
return list.size() > 0;
}
//liuqipeng end
}
说一下layout资源新增的例如dashboard_tile_with_switch怎么写
复制\Settings\res\layout\dashboard_tile.xml,然后改名添加新的控件即可。
6. Settings\src\com\android\settings\SettingsActivity.java引入qsItemType
public static void loadCategoriesFromResource(int resid, List<DashboardCategory> target,
Context context) {
...
//liuqipeng add
int qsItemType=sa.getInt(com.android.internal.R.styleable.PreferenceHeader_qsItemType, 1);
tile.qsItemType=qsItemType;
//liuqipeng end
tile.iconRes = sa.getResourceId(
com.android.internal.R.styleable.PreferenceHeader_icon, 0);
tile.fragment = sa.getString(
com.android.internal.R.styleable.PreferenceHeader_fragment);
sa.recycle();
...
}
7. Settings\src\com\android\settings\dashboard\DashboardSummary.java不同的item中的控件的逻辑处理
做到第5步,这时不同的ui已经可以显示了,剩余要解决的就是不同的item的ui对应的比如textview的text的动态变化,还有switch的一些事件处理
思路如下:
如何区别不同的item,通过Settings\res\xml\dashboard_categories.xm的每个item都有不同的id。
如何获取单个item中的控件,比如switch,or textview,拿到item的实例后 findviewbyid即可,然后再去写代码做相关的逻辑处理。
这样修改的位置就呼之欲出了
第5步中提到的DashboardTileView的构造方法扩展了形参,这里同步修改
Settings\src\com\android\settings\dashboard\DashboardSummary.java
private void rebuildUI(Context context) {
…
for (int i = 0; i < tilesCount; i++) {
DashboardTile tile = category.getTile(i);
//liuqipeng change
Log.e("liuqipeng","tile.qsItemType" + tile.qsItemType);
DashboardTileView tileView = new DashboardTileView(context,null,tile.qsItemType);
//liuqipeng end
updateTileView(context, res, tile, tileView.getImageView(),
tileView.getTitleTextView(), tileView.getStatusTextView(),tileView);
tileView.setTile(tile);
categoryContent.addView(tileView);
}
…
}
private void updateTileView(Context context, Resources res, DashboardTile tile,
ImageView tileIcon, TextView tileTextView, TextView statusTextView,DashboardTileView tileView) {
…
//liuqipeng add
int id=(int)tile.id;
if(id==R.id.iphone_flight_mode_settings){
mAirSwitch=(Switch)tileView.findViewById(R.id.switch_on_off);
flightModeListen();
}else if(id==R.id.wifi_settings){
mWlanText=(TextView)tileView.findViewById(R.id.on_off_text);
wifiListen();
}else if(id==R.id.bluetooth_settings){
mBluetoothText=(TextView)tileView.findViewById(R.id.on_off_text);
bluetoothListen();
}else if(id==R.id.operator_settings){
mOperatorText=(TextView)tileView.findViewById(R.id.on_off_text);
operatorListen();
}else if(id==R.id.iphone_accout_settings){
mAccountTitleText=(TextView)tileView.findViewById(R.id.title);
mAccountStatusText=(TextView)tileView.findViewById(R.id.status);
//mAccountShortNameText=(TextView)tileView.findViewById(R.id.iphone_account_short_name);
accountListen();
//make it visible
mAccountStatusText.setVisibility(View.VISIBLE);
}
//liuqipeng end
…
}
//liuqipeng add
private void flightModeListen(){
if(mAirSwitch!=null){
int type = android.provider.Settings.Global.getInt(mContext.getContentResolver(),android.provider.Settings.Global.AIRPLANE_MODE_ON,0);
mAirSwitch.setChecked(type!=0);
mAirSwitch.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
android.provider.Settings.Global.putInt(mContext.getContentResolver(), android.provider.Settings.Global.AIRPLANE_MODE_ON,isChecked?1:0);
Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
intent.putExtra("state", isChecked);
mContext.sendBroadcast(intent);
}
});
//sync change when setting in statusbar
mAirMode = new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
String isNull=getActivity()!=null?"true":"false";
Log.e("liuqipeng","getActivity()!=null" + isNull);
String isContextNull=mContext!=null?"true":"false";
Log.e("liuqipeng","isContextNull!=null" + isContextNull);
int type = android.provider.Settings.Global.getInt(mContext.getContentResolver(),android.provider.Settings.Global.AIRPLANE_MODE_ON,0);
mAirSwitch.setChecked(type!=0);
}
};
mContext.getContentResolver().registerContentObserver(android.provider.Settings.Global.getUriFor(android.provider.Settings.Global.AIRPLANE_MODE_ON),true, mAirMode);
}
}
private void wifiListen() {
if(mWlanText!=null){
WifiManager wifiManager = (WifiManager)getActivity().getSystemService(Context.WIFI_SERVICE);
Log.e("liuqipeng","wifiManager.isWifiEnabled() = "+ wifiManager.isWifiEnabled());
mWlanText.setText(wifiManager.isWifiEnabled() ? R.string.kst_open_text : R.string.kst_close_text);
}
}
private void bluetoothListen() {
if(mBluetoothText!=null){
BluetoothAdapter blueadapter = BluetoothAdapter.getDefaultAdapter();
mBluetoothText.setText(blueadapter.isEnabled() ? R.string.kst_open_text : R.string.kst_close_text);
}
}//liuqipeng end
3.常见的themes.xml属性修改
添加的位置在
\Settings\res\values\themes.xml
<stylename="Theme.Settings" parent="Theme.SettingsBase">
…
</style>
1.Switch的风格仿iphone
<itemname="android:switchStyle">@style/IPhone.Custom.Switch</item>
<stylename="IPhone.Custom.Switch"parent="@android:style/Widget.Material.CompoundButton.Switch">
<!--<itemname="android:track">@drawable/iphone_switch_track</item>
<itemname="android:thumb">@drawable/iphone_switch_thumb</item>
<itemname="android:background">@null</item>-->
<itemname="android:track">@null</item>
<item name="android:thumb">@null</item>
<itemname="android:background">@drawable/zzzz_iphone_btn_check_holo_dark</item>
</style>
2.ActionBar的风格修改
<itemname="android:actionBarStyle">@style/Iphone.Theme.ActionBar.SettingsActivity</item>
<itemname="@*android:actionBarSize">@dimen/actionbar_size</item>
<itemname="android:actionBarTheme">@style/IPhone.ActionBar</item>
这里说一下android:与@*android的区别
在public.xml公开的属性通过Android:xxx来引用
而没有在public.xml公开的属性是不是就调用不到呢,其实不是,用@*android:xxx就可以引用到了
3.各种Preference的风格修改
<!--forpreference-->
<itemname="@*android:preferenceScreenStyle">@style/PreferenceScreen</item>
<itemname="@*android:preferenceCategoryStyle">@style/PreferenceCategory</item>
<itemname="@*android:checkBoxPreferenceStyle">@style/CheckBoxPreference</item>
<itemname="@*android:switchPreferenceStyle">@style/SwitchPreference</item>
<itemname="@*android:seekBarPreferenceStyle">@style/SeekBarPreferenceStyle</item>
<itemname="@*android:yesNoPreferenceStyle">@style/YesNoPreferenceStyle</item>
<itemname="@*android:dialogPreferenceStyle">@style/DialogPreferenceStyle</item>
<itemname="@*android:seekBarDialogPreferenceStyle">@style/SeekBarDialogPreferenceStyle</item>
<itemname="@*android:editTextPreferenceStyle">@style/EditTextPreferenceStyle</item>
<itemname="@*android:ringtonePreferenceStyle">@style/RingtonePreferenceStyle</item>
<itemname="@*android:listPreferredItemHeightSmall">@dimen/dashboard_tile_minimum_height</item>
<itemname="@*android:listPreferredItemPaddingStart">@dimen/dashboard_tile_image_margin_start</item>
<itemname="@*android:listPreferredItemPaddingEnd">@dimen/iphone_disclosure_end_margin</item>
引用的style定义在\Settings\res\values\styles.xml
例如
<stylename="CheckBoxPreference"parent="@*android:style/Preference.Material.CheckBoxPreference">
<item name="android:layout">@layout/preference_material_settings_right_summary_no_arrow</item>
</style>
主要是修改了layout,也就是ui上有区别
那么layout文件preference_material_settings_right_summary_no_arrow.xml是怎么修改得来的呢?
frameworks\base\core\res\res\layout\preference_material.xml
为什么是preference_material.xml而不是preference.xml,这个需要去看Settings的themes.xml用的什么theme,然后找到frameworks\base\core\res\res\里面的themes,去找对应的preference使用的对应的layout文件。
需要注意的是preference_material_settings_right_summary_no_arrow.xml中新加的控件(比如上下分割线),它们的android:id="@+android:id/preference_down_divider",为什么不是@+id而是@+android:id,因为我们要把这个id添加到framework的R.java里面提供(而不是Settings里面的R.java,注意区分),然后由frameworks\base\core\java\android\preference\Preference.java去调用并做相关处理。
4.状态栏风格修改
<!--statusbarcolor-->
状态栏的字,图标什么的使用浅色的风格(也就是黑色调)
<itemname="android:windowLightStatusBar">true</item>
状态栏背景色修改(改成淡色调)
<itemname="android:colorPrimaryDark">@color/iphone_statusbar_background_color</item>
5.Listview的风格修改
<!--forlistview-->
<!--<itemname="android:listDivider">@drawable/iphone_custom_divider_drawable</item>-->
<itemname="android:dividerHeight">@dimen/iphone_custom_seperator_diver_size</item>
<itemname="android:dividerVertical">@color/iphone_divider_line_color</item>
<!--<itemname="android:colorEdgeEffect">@color/iphone_window_background_color</item>--><!--hideoverscroll shade-->
<itemname="android:colorEdgeEffectEnabled">false</item>
<itemname="android:textAppearanceListItem">@style/IPhone.TextAppearance.ListItem</item>
<!--<itemname="android:textAppearanceListItemSmall">@style/TextAppearance.Material.Subhead</item>-->
<itemname="android:textAppearanceListItemSecondary">@style/IPhone.TextAppearance.ListItem.Secondary</item>
<itemname="android:listPreferredItemHeightSmall">@dimen/iphone_preference_item_min_height</item>
6. colorAccent修改
也就是checkbox的checked color一类的,保持一致性
<!--widgetchecked color ,checkbox etc-->
<itemname="android:colorAccent">@color/iphone_colorAccent</item>
7.动画修改
Window的进入和退出的动画
<itemname="android:windowAnimationStyle">@style/IPhone.AnimationActivity</item>
<stylename="IPhone.AnimationActivity"parent="@android:style/Animation.Activity">
<itemname="android:activityOpenEnterAnimation">@anim/enter_right_to_left</item>
<itemname="android:activityOpenExitAnimation">@anim/exit_right_to_left</item>
<itemname="android:activityCloseEnterAnimation">@anim/enter_left_to_right</item>
<itemname="android:activityCloseExitAnimation">@anim/exit_left_to_right</item>
</style>
实际的修改内容不止上面提到的这些,还有PreferencCategory.java,Preference.java的定制啊,密码界面的逻辑变更啊(iphone进入密码界面就会弹出输入密码的窗口,而android是在变更密码的时候才会弹出输入密码的窗口),ScrollView的setOnScrollChangeListener来实现iphone的搜索栏与标题栏的交互效果(下滑搜索栏隐藏,标题栏字体变化的效果)等等。这些内容放到以后专门提出来分享。
希望这些内容对大家有用!