天天看點

Android 自定義Preference 講解1. 前言2. 原機效果圖3. 實作步驟 4. 設定界面代碼5. 自定義Preference效果圖6. 總結

1. 前言

        近期看見XX款平闆上, 設定中手勢導航和虛拟三鍵導航的切換選項,覺得效果做的非常好,然後想在源碼中倒騰一下,仿照寫一個效果圖出來,本篇文章在android11 Settings源碼中做的功能,主要是做的UI效果圖,具體邏輯可以根據項目需要去實作,也是對自定義Preference的一個總結。

2. 原機效果圖

Android 自定義Preference 講解1. 前言2. 原機效果圖3. 實作步驟 4. 設定界面代碼5. 自定義Preference效果圖6. 總結

3. 實作步驟

3.1 UI效果圖,見5實作效果圖

        從上到下第一個Preference有點類似于設定子產品中的RadioButtonPreference, 左邊是一個标題,右側是一個RadioButton, 在android11上面沒有這種Preference直接拿來用,是以需要稍微改動下。

        第二個Preference 的UI布局:上面是一個導航樣式圖檔,下面是一個文本示範,點選會跳轉到另一個示範動畫效果的界面。在設定中也沒有現成的Preference,是以這個也需要自定義

3.2 UI布局代碼

        第一個Preference的 layout 布局圖, 裡面的顔色,字型大小屬性值都可以自己去适配

#1. layout布局檔案: navigation_title_preference.xml


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="58dp"
    android:background="@drawable/navigation_preference_bg">

    <TextView
        android:id="@+id/navigation_type_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_marginLeft="16dp"
        android:layout_centerInParent="true"
        android:textSize="16sp"
        android:textColor="#FF000000"/>

    <RadioButton
        android:id="@android:id/checkbox"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_marginRight="30dp"
        android:layout_centerInParent="true"
        android:focusable="false"
        android:clickable="false"/>

</RelativeLayout>


#2.drawable navigation_preference_bg


<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="false"      android:drawable="@drawable/navi_pressed_false"/>
    <item android:state_pressed="true"  
android:drawable="@drawable/navi_pressed_true"/>
</selector>


#3. drawable  navi_pressed_false

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid android:color="@color/search_bar_background" />
    <corners android:radius="9dp" />
</shape>


#4. drawable  navi_pressed_true

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid android:color="#1a000000" />
    <corners android:radius="9dp" />
</shape>
           

        第二個Preference的 layout 布局圖:

#layout 布局檔案:navigation_preference.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="155dp"
    android:background="@drawable/navigation_preference_bg">
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <ImageView
            android:id="@+id/navigation_type_icon"
            android:layout_marginTop="26dp"
            android:layout_width="240dp"
            android:layout_height="70dp"
            android:layout_centerHorizontal="true"/>

        <TextView
            android:id="@+id/demonstration_effect"
            android:layout_width="90dp"
            android:layout_height="34dp"
            android:layout_below="@id/navigation_type_icon"
            android:layout_marginTop="16dp"
            android:layout_marginBottom="9dp"
            android:background="@drawable/navigation_preference_bg"
            android:gravity="center"
            android:layout_centerHorizontal="true"
            android:textColor="#268AFF"
            android:textSize="16sp"/>
    </RelativeLayout>

</RelativeLayout>
           

           第三,兩個Preference需要用到的屬性:自定義标題,自定義圖檔,自定義文本的屬性,需要前期聲明, 這樣子可以在xml檔案直接配置 preference:navigationTitle    preference:naviDrawable  preference:naviDetail 的值。

#第一個Preference的title屬性
  <declare-styleable name="NavigationTitlePreference">
        <attr name="navigationTitle" format="string" />
    </declare-styleable>

  #第二個Preference的圖檔和文本的屬性
    <declare-styleable name="NavigationPreference">
        <attr name="naviDrawable" format="reference" />
        <attr name="naviDetail" format="string" />
    </declare-styleable>
           

3.3 自定義Preference 代碼

        第一個Preference類似于源生的RadioButtonPreference控件,隻是布局不一樣,通過繼承相同的父類CheckBoxPreference,可以實作自定義的Preference,代碼如下:

public class NavigationTitlePreference extends CheckBoxPreference {

    //Preference的點選事件自定義接口
    public interface OnClickListener {
        void onNaviPrefernceClicked(NavigationTitlePreference emiter);
    }

    private OnClickListener mNaviTitleOnClickListener;

    public void setNaviTitleOnClickListener(OnClickListener onClickListener) {
        mNaviTitleOnClickListener = onClickListener;
    }

    private TextView mTextView;
    private String mNavigationTitle;

    public NavigationTitlePreference(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        TypedArray attributes = context.obtainStyledAttributes(attrs, R.styleable.NavigationTitlePreference, 0, 0);
        // if these are already set that means they were set dynamically and don't need
        // to be loaded from xml
        mNavigationTitle = (mNavigationTitle == null) ?
                attributes.getString(R.styleable.NavigationTitlePreference_navigationTitle) : mNavigationTitle;
        setLayoutResource(R.layout.navigation_title_preference);
        attributes.recycle();
    }

    public NavigationTitlePreference(Context context, AttributeSet attrs) {
        this(context, attrs, TypedArrayUtils.getAttr(context,
                androidx.preference.R.attr.preferenceStyle,
                android.R.attr.preferenceStyle));
    }

    public NavigationTitlePreference(Context context) {
        this(context, null);
    }

    @Override
    public void onBindViewHolder(PreferenceViewHolder view) {
        super.onBindViewHolder(view);
        mTextView = (TextView)view.findViewById(R.id.navigation_type_title);
        mTextView.setText(mNavigationTitle);
    }

    @Override
    protected void onClick() {
        if (null != mNaviTitleOnClickListener) {
            mNaviTitleOnClickListener.onNaviPrefernceClicked(this);
        }
    }
}
           

,         代碼解讀:

         1. 自定義類是繼承于CheckBoxPreference,它裡面已經對按鈕(比如 RadioButton,  SwitchButton)已經做了事件處理,我們隻需要設定按鈕的id為:android:id="@android:id/checkbox" 即可, 父類代碼如下:

# CheckBoxPreference.java 
 
@Override
    protected void onBindView(View view) {
        super.onBindView(view);

        View checkboxView = view.findViewById(com.android.internal.R.id.checkbox);
        if (checkboxView != null && checkboxView instanceof Checkable) {
            ((Checkable) checkboxView).setChecked(mChecked);
        }
        .......
    }
           

        2.  點選Preference的事件自定義接口

                    public interface OnClickListener {

                        void onNaviPrefernceClicked(NavigationTitlePreference emiter);

                    }

           第二個Preference的代碼,繼承Preference,  組成元素為 一張圖檔和一個文本,點選文本會跳轉到相應的動畫示範界面, 不需要圖檔點選事件, 不需要整個Preference的點選事件

public class NavigationPreference extends Preference {

    private TextView mTextView;
    private ImageView mImageView;
    private int mNavigationDrawable;
    private String mNavigationDetail;


    public NavigationPreference(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    public NavigationPreference(Context context) {
        super(context);
        init(context, null);
    }

    private void init(Context context, AttributeSet attrs) {

        TypedArray attributes = context.obtainStyledAttributes(attrs, R.styleable.NavigationPreference, 0, 0);

        // if these are already set that means they were set dynamically and don't need
        // to be loaded from xml
        mNavigationDrawable = (mNavigationDrawable == 0) ?
                attributes.getResourceId(R.styleable.NavigationPreference_naviDrawable, 0) : mNavigationDrawable;

        mNavigationDetail = (mNavigationDetail == null) ?
                attributes.getString(R.styleable.NavigationPreference_naviDetail) : mNavigationDetail;

        setLayoutResource(R.layout.navigation_preference);

        attributes.recycle();
    }



    @Override
    public void onBindViewHolder(PreferenceViewHolder view) {
        super.onBindViewHolder(view);
        mTextView = (TextView)view.findViewById(R.id.demonstration_effect);
        mTextView.setText(mNavigationDetail);
        mTextView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (getIntent() != null) {
                    Context context = getContext();
                    context.startActivity(getIntent());
                }
            }
        });

        mImageView = (ImageView)view.findViewById(R.id.navigation_type_icon);
        mImageView.setImageResource(mNavigationDrawable);
        //點選整個Preference時,無響應事件的關鍵代碼
        view.itemView.setClickable(false);
    }
}
           

        代碼解讀:

        1.  點選整個自定義Preference時,無響應事件的關鍵代碼

            view.itemView.setClickable(false);   //設定為不可點選

            為什麼呢?原理如下:

             在 super.onBindViewHolder(view)中

public void onBindViewHolder(PreferenceViewHolder holder) {
        View itemView = holder.itemView;
        Integer summaryTextColor = null;
        
        //設定 itemView 的點選事件
        itemView.setOnClickListener(mClickListener);
        
        .............
}
           

         當點選後,會執行父類Preference.java 中的 performClick方法時,直接return了。

protected void performClick(View view) {
        performClick();
}

 public void performClick() {
        //當設定不可點選,或者不可選擇的時候,就直接return了。        

        if (!isEnabled() || !isSelectable()) {
            return;
        }

        onClick();

        if (mOnClickListener != null && mOnClickListener.onPreferenceClick(this)) {
            return;
        }

        PreferenceManager preferenceManager = getPreferenceManager();
        if (preferenceManager != null) {
            PreferenceManager.OnPreferenceTreeClickListener listener = preferenceManager
                    .getOnPreferenceTreeClickListener();
            if (listener != null && listener.onPreferenceTreeClick(this)) {
                return;
            }
        }

        if (mIntent != null) {
            Context context = getContext();
            context.startActivity(mIntent);
        }
    }
           

        2.  點選文本的響應代碼:

            @Override

            public void onClick(View v) {

                if (getIntent() != null) {

                    Context context = getContext();

                    context.startActivity(getIntent());

                }

           } //這裡的intent是通過 xml檔案配置的intent參數,進而跳轉到目标界面, 如下:

 <Preference
        android:key="xxxx"
        android:title="xxxxx">
        // 舉個例子 通過配置intent參數,跳轉到指定的界面
        <intent android:action="android.credentials.INSTALL_AS_USER"
                android:targetPackage="com.android.certinstaller"
                android:targetClass="com.android.certinstaller.CertInstallerMain">
               <extra android:name="install_as_uid" android:value="1010" />
        </intent>
 </Preference>
           

4. 設定界面代碼

        自定義的Preference代碼完成後,接下來就開始寫界面加載的xml檔案了

         布局檔案:navigation_type_settings.xml

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:preference="http://schemas.android.com/apk/res-auto"
    android:key="gesture_system_navigation_type"
    android:title="@string/system_navigation_title">

    <com.android.settings.gestures.NavigationTitlePreference
        android:key="gesture_navi"
        //自定義的title
        preference:navigationTitle="@string/system_gesture_navigation_title"/>

    <com.android.settings.gestures.NavigationPreference
        android:key="gesture_navi_drawable"
       //自定義的圖檔
        preference:naviDrawable="@drawable/ic_gesture_navigation"
       //自定義文本
        preference:naviDetail="@string/demonstration_effect">
        <intent
            android:action="android.intent.action.MAIN"
            android:targetPackage="com.android.settings"
            android:targetClass="com.android.settings.Settings$GestureNaviSettingsActivity"/>
    </com.android.settings.gestures.NavigationPreference>


    <com.android.settings.gestures.NavigationTitlePreference
        android:key="virtual_navi"
        preference:navigationTitle="@string/system_virtual_navigation_title"/>

    <com.android.settings.gestures.NavigationPreference
        android:key="virtual_navi_drawable"
        preference:naviDrawable="@drawable/ic_virtual_navigation"
        preference:naviDetail="@string/demonstration_effect">
        <intent
            android:action="android.intent.action.MAIN"
            android:targetPackage="com.android.settings"
            android:targetClass="com.android.settings.Settings$VirtualNaviSettingsActivity"/>
    </com.android.settings.gestures.NavigationPreference>


</PreferenceScreen>

           

   界面加載菜單代碼:

public class NavigationTypeSettings extends SettingsPreferenceFragment implements
        NavigationTitlePreference.OnClickListener{

    private static final String KEY_GESTURE = "gesture_navi";
    private static final String KEY_VIRTUAL = "virtual_navi";

    private NavigationTitlePreference mGestureNaviPref;
    private NavigationTitlePreference mVirtualNaviPref;


    @Override
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        //加載界面
        addPreferencesFromResource(R.xml.navigation_type_settings);

     
        PreferenceScreen root = getPreferenceScreen();
        mGestureNaviPref = (NavigationTitlePreference)root.findPreference(KEY_GESTURE);
        mVirtualNaviPref = (NavigationTitlePreference)root.findPreference(KEY_VIRTUAL);
        //自定義Preference的監聽事件
        mGestureNaviPref.setNaviTitleOnClickListener(this);
        mVirtualNaviPref.setNaviTitleOnClickListener(this);
    }

   
   //點選Preference的回調方法處理
   @Override
   public void onNaviPrefernceClicked(NavigationTitlePreference emiter) {
        ..........
   }
  
           

5. 自定義Preference效果圖

Android 自定義Preference 講解1. 前言2. 原機效果圖3. 實作步驟 4. 設定界面代碼5. 自定義Preference效果圖6. 總結

6. 總結

        本篇文章是對自定義Preference的一個小結,隻寫了UI界面和點選響應事件,具體的邏輯看項目需求,隻是一個抛磚引玉的Demo,設定子產品已經有比較成熟的Preference控件,當有自定義的Preference的需求,根據情況繼承類似的Preference去造輪子。對于UI事件的處理,本文中有響應整個Preference的事件處理,也有隻需要響應其中一個子控件的事件處理,關鍵還是要多看和了解源碼,面向對象程式設計繼承和多态的靈活應用, 後續在工作項目中再多多總結吧。