Android 高仿Iphone Settings 基于Android M版本

之前在公司有需要做高仿的项目,恰好分配到了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的搜索栏与标题栏的交互效果(下滑搜索栏隐藏,标题栏字体变化的效果)等等。这些内容放到以后专门提出来分享。

希望这些内容对大家有用!




  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值