Preference組件探究之自定義Preference

上一篇文章中我們從源碼入手講解了Preference畫面展示的原理。這篇文章講述下官方提供的Preference組件是怎麼實現的,以及我們自己如何自定義Preference組件。

Preference UI分析

包括兩部分。首先是組件本身的UI,然後是點擊後展示的UI。
比如:

我們知道系統提供了不少Preference組件供我們使用,大體如下幾種。

有些組件是針對組件本身的UI進行的定製,有些是針對點擊後展示的UI進行的定製。
按照這種區別針對這些組件分爲如下兩類。

■定製Preference本身的UI
TwoStatePreference AbstractClass,不能直接使用,需要自定義子類才能使用
CheckBoxPreference 繼承自TwoStatePreference,展示插件爲複選框的設置項目
SwitchPreference 繼承自TwoStatePreference,展示插件爲開關的設置項目
SeekBarPreference 展示拖動條的設置項目

 

 

 

 

 

■定製點擊後展示的UI
DialogPreference AbstractClass,不能直接使用,需要自定義子類才能使用
EditTextPreference 繼承自DialogPreference,點擊後展示內嵌輸入框的對話框的設置項目
ListPreference 繼承自DialogPreference,點擊後展示內嵌ListView的對話框的設置項目
MultiSelectListPreference 繼承自DialogPreference,點擊後展示內嵌複選框的對話框的設置項目
SeekBarDialogPreference 繼承自DialogPreference,點擊後展示內嵌拖動條的對話框的設置項目
VolumePreference 繼承自SeekBarDialogPreference,點擊後展示音量大小拖動條的對話框的設置項目
RingtonePreference 覆寫了點擊處理,點擊後跳轉到系統鈴聲設置頁面的設置項目

 

 

 

 

 

 

 

 

我們以相對複雜一點的VolumePreference爲例,介紹下系統如何實現了自定義的音量調節設置組件。

VolumePreference分析

 extends android.preference.SeekBarDialogPreference
  extends android.preference.DialogPreference
   extends android.preference.Preference

示例效果:

我們先來看下Dialog彈出怎麼實現的。

DialogPreference 

public abstract class DialogPreference extends Preference…{
    public DialogPreference(
            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);

        final TypedArray a = context.obtainStyledAttributes(attrs,
                com.android.internal.R.styleable.DialogPreference, defStyleAttr, defStyleRes);
        …
        mDialogLayoutResId = a.getResourceId(com.android.internal.R.styleable.DialogPreference_dialogLayout,
                mDialogLayoutResId); // 從attr中讀取佈局ID。
        a.recycle();
    }
    …
    // 覆寫onClick邏輯調用展示Dialog
    protected void onClick() {
        if (mDialog != null && mDialog.isShowing()) return;
        showDialog(null);
    }

    protected void showDialog(Bundle state) {
        // 創建Dialog並顯示
        mBuilder = new AlertDialog.Builder(context)
            .setTitle(mDialogTitle)
            .setIcon(mDialogIcon)
            .setPositiveButton(mPositiveButtonText, this)
            .setNegativeButton(mNegativeButtonText, this);

        // 創建Dialog的內容View
        View contentView = onCreateDialogView();
        if (contentView != null) {
            onBindDialogView(contentView); // 內容View的初始化
            mBuilder.setView(contentView);
        } else {
            mBuilder.setMessage(mDialogMessage);
        }    
        …
    }

    // 加載配置的dialog佈局
    // 可由dialogLayout標籤或setDialogLayoutResource()指定
    protected View onCreateDialogView() {
        LayoutInflater inflater = LayoutInflater.from(mBuilder.getContext());
        return inflater.inflate(mDialogLayoutResId, null);
    }

    // 用以準備Dialog的View視圖,進行一些配置,子類可覆寫更改UI
    protected void onBindDialogView(View view) {
        View dialogMessageView = view.findViewById(com.android.internal.R.id.message);
        …
    } }

那麼SeekBar又是如何配置進去的呢。

SeekBarDialogPreference

public class SeekBarDialogPreference extends DialogPreference {
    …
    public SeekBarDialogPreference(Context context, AttributeSet attrs) {
        // 指定了名爲seekBarDialogPreferenceStyle的默認attr給父類的構造函數
        this(context, attrs, R.attr.seekBarDialogPreferenceStyle);★
    }
    …
}

 ★處指定的默認attr如下。

<!-- frameworks/base/core/res/res/values/themes.xml-->
<style name="Theme">
    <item name="seekBarDialogPreferenceStyle">@style/Preference.DialogPreference.SeekBarPreference</item>
    …
</style>

該默認的attr中dialogLayout標籤指定的layout如下。

<!--frameworks/base/core/res/res/values/styles.xml-->
<style name="Preference.DialogPreference.SeekBarPreference">
    <item name="dialogLayout">@layout/preference_dialog_seekbar</item>
</style>

<!--frameworks/base/core/res/res/layout/preference_dialog_seekbar.xml-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:gravity="center_horizontal"
              android:orientation="vertical">

    <ImageView
        android:id="@+id/icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingTop="20dp" />

    <!--此處指定了包含SeekBar控件的佈局-->
    <SeekBar
        android:id="@+id/seekbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="20dp" />

</LinearLayout>

如果APP沒有在style中,佈局中以及setDialogLayoutResource()中複寫dialog的layout ID的話,那麼DialogPreference構造函數將從默認的attr裏將上述包含SeekBar的佈局加載進去。

最後又是怎麼和音量產生關聯的呢?

VolumePreference

public class VolumePreference extends SeekBarDialogPreference… {
    public VolumePreference(Context context, AttributeSet attrs) {
        // 指定的默認attr和父類一致,因爲UI上它和父類完全相同
        this(context, attrs, R.attr.seekBarDialogPreferenceStyle);
    }
    
    protected void onBindDialogView(View view) {
        // 將SeekBar控件和SeekBarVolumizer組件產生關聯
        // 並啓動SeekBarVolumizer
        final SeekBar seekBar = (SeekBar) view.findViewById(R.id.seekbar);
        mSeekBarVolumizer = new SeekBarVolumizer(getContext(), mStreamType, null, this);
        mSeekBarVolumizer.start();
        mSeekBarVolumizer.setSeekBar(seekBar);
        …
        // 設置KEY操作監聽器並將SeekBar獲取的焦點便於快速支持KEY處理
        view.setOnKeyListener(this);
        view.setFocusableInTouchMode(true);
        view.requestFocus();
    }

    public boolean onKey(View v, int keyCode, KeyEvent event) {
        // 監聽硬件的音量+,-和靜音鍵並向SeekBarVolumizer反映
        boolean isdown = (event.getAction() == KeyEvent.ACTION_DOWN);
        switch (keyCode) {
            case KeyEvent.KEYCODE_VOLUME_DOWN:
                if (isdown) {
                    mSeekBarVolumizer.changeVolumeBy(-1);
                }
                return true;
            case KeyEvent.KEYCODE_VOLUME_UP:
                if (isdown) {
                    mSeekBarVolumizer.changeVolumeBy(1);
                }
                return true;
            case KeyEvent.KEYCODE_VOLUME_MUTE:
                if (isdown) {
                    mSeekBarVolumizer.muteVolume();
                }
                return true;
            …
        }
    }

    // Dialog取消或者意外關閉(非OK BTN)的場合
    protected void onDialogClosed(boolean positiveResult) {
        super.onDialogClosed(positiveResult);

        if (!positiveResult && mSeekBarVolumizer != null) {
            mSeekBarVolumizer.revertVolume(); // 將已設置回滾
        }

        cleanup();
    }

    // Activity或者Fragment的onStop回調進入後臺的時候執行
    public void onActivityStop() {
        if (mSeekBarVolumizer != null) {
            mSeekBarVolumizer.stopSample(); // 將預覽的鈴聲播發停止
        }
    }

    // 處理一些意外狀況,將SeekBarVolumizer重置,線程結束等
    private void cleanup() {
       getPreferenceManager().unregisterOnActivityStopListener(this);

       if (mSeekBarVolumizer != null) {
           final Dialog dialog = getDialog();
           if (dialog != null && dialog.isShowing()) {
               final View view = dialog.getWindow().getDecorView().findViewById(R.id.seekbar);
               if (view != null) {
                   view.setOnKeyListener(null);
               }

               // Stopped while dialog was showing, revert changes
               mSeekBarVolumizer.revertVolume();
           }

           mSeekBarVolumizer.stop();
           mSeekBarVolumizer = null;
       }
    }

    // SeekBarVolumizer中鈴聲預覽播放時候的回調,供APP處理
    public void onSampleStarting(SeekBarVolumizer volumizer) {
        if (mSeekBarVolumizer != null && volumizer != mSeekBarVolumizer) {
            mSeekBarVolumizer.stopSample();
        }
    }

    // SeekBar上的拖動條數值發生變化時候的回調,供APP知曉程度
    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch) {
        // noop
    }

    // 外部導致系統音量發生變化的回調
    public void onMuted(boolean muted, boolean zenMuted) {
        // noop
    }
    …
}

至此,VolumePreference就是繼承自SeekBarDialogPreference實現展示帶SeekBar的dialog的組件。內部通過SeekBarVolumizer類去控制音量的設置,預覽,回滾,保存和恢復等處理。

有必要提及下SeekBarVolumizer的處理細節。

SeekBarVolumizer

public class SeekBarVolumizer implements OnSeekBarChangeListener, Handler.Callback {
    // 持有SeekBar實例並監聽拖動條進度
    public void setSeekBar(SeekBar seekBar) {
        if (mSeekBar != null) {
            mSeekBar.setOnSeekBarChangeListener(null);
        }
        mSeekBar = seekBar;
        mSeekBar.setOnSeekBarChangeListener(null);
        mSeekBar.setMax(mMaxStreamVolume);
        updateSeekBar();
        mSeekBar.setOnSeekBarChangeListener(this);
    }
    
    // 更新SeekBar進度
    protected void updateSeekBar() {
        final boolean zenMuted = isZenMuted();
        mSeekBar.setEnabled(!zenMuted);
        if (zenMuted) {
            mSeekBar.setProgress(mLastAudibleStreamVolume, true);
        } else if (mNotificationOrRing && mRingerMode == AudioManager.RINGER_MODE_VIBRATE) {
            mSeekBar.setProgress(0, true);
        } else if (mMuted) {
            mSeekBar.setProgress(0, true);
        } else {
            mSeekBar.setProgress(mLastProgress > -1 ? mLastProgress : mOriginalStreamVolume, true);
        }
    }

    // 音量調節邏輯開始,由Preference調用
    public void start() {
        if (mHandler != null) return;  // already started

        // 啓動工作Thread
        HandlerThread thread = new HandlerThread(TAG + ".CallbackHandler");
        thread.start();

        // 創建該Thread的Handler並在該線程裏初始化鈴聲播放器實例
        mHandler = new Handler(thread.getLooper(), this);
        mHandler.sendEmptyMessage(MSG_INIT_SAMPLE);

        // 監聽系統音量的變化,變化交由上述線程的Handler處理
        mVolumeObserver = new Observer(mHandler);
        mContext.getContentResolver().registerContentObserver(
                System.getUriFor(System.VOLUME_SETTINGS[mStreamType]),
                false, mVolumeObserver);

        // 監聽系統音量,鈴聲模式變化的廣播
        mReceiver.setListening(true);
    }

    //音量調節邏輯結束,由Preference調用
    public void stop() {
        if (mHandler == null) return;  // already stopped
        postStopSample(); // 關閉鈴聲播放
        
        // 註銷內容監聽,廣播監聽,Thread內Looper停止輪詢消息等重置處理
        mContext.getContentResolver().unregisterContentObserver(mVolumeObserver);
        mReceiver.setListening(false);
        mSeekBar.setOnSeekBarChangeListener(null);
        mHandler.getLooper().quitSafely();
        mHandler = null;
        mVolumeObserver = null;
    }

    // 運行在工作線程的Handler回調
    public boolean handleMessage(Message msg) {
        switch (msg.what) {
            case MSG_SET_STREAM_VOLUME:
                …
                break;
            case MSG_START_SAMPLE:
                onStartSample();
                break;
            case MSG_STOP_SAMPLE:
                onStopSample();
                break;
            case MSG_INIT_SAMPLE:
                onInitSample();
                break;
            default:
                Log.e(TAG, "invalid SeekBarVolumizer message: "+msg.what);
        }
        return true;
    }

    // 初始化鈴聲播放,運行在工作Thread中
    private void onInitSample() {
        synchronized (this) {
            mRingtone = RingtoneManager.getRingtone(mContext, mDefaultUri);
            if (mRingtone != null) {
                mRingtone.setStreamType(mStreamType);
            }
        }
    }

    // 通知工作Thread需要開始播放
    private void postStartSample() {
        if (mHandler == null) return;
        mHandler.removeMessages(MSG_START_SAMPLE);
        mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_START_SAMPLE),
                isSamplePlaying() ? CHECK_RINGTONE_PLAYBACK_DELAY_MS : 0);
    }

    // 工作Thread響應開始播放
    private void onStartSample() {
        if (!isSamplePlaying()) {
            // 執行Preference的回調
            if (mCallback != null) {
                mCallback.onSampleStarting(this);
            }

            synchronized (this) {
                if (mRingtone != null) {
                    try {
                        mRingtone.setAudioAttributes(new AudioAttributes.Builder(mRingtone
                                .getAudioAttributes())
                                .setFlags(AudioAttributes.FLAG_BYPASS_MUTE)
                                .build());
                        mRingtone.play();
                    } catch (Throwable e) {
                        Log.w(TAG, "Error playing ringtone, stream " + mStreamType, e);
                    }
                }
            }
        }
    }

    // 通知工作Thread停止播放
    private void postStopSample() {
        if (mHandler == null) return;
        // remove pending delayed start messages
        mHandler.removeMessages(MSG_START_SAMPLE);
        mHandler.removeMessages(MSG_STOP_SAMPLE);
        mHandler.sendMessage(mHandler.obtainMessage(MSG_STOP_SAMPLE));
    }

    // 工作Thread相應停止播放
    private void onStopSample() {
        synchronized (this) {
            if (mRingtone != null) {
                mRingtone.stop();
            }
        }
    }

    // UI線程的進度變化後處理,通知工作線程音量發生變化
    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch) {
        if (fromTouch) {
            postSetVolume(progress);
        }

        // 回調Preference的處理
        if (mCallback != null) {
            mCallback.onProgressChanged(seekBar, progress, fromTouch);
        }
    }

    // 向工作線程發出通知
    private void postSetVolume(int progress) {
        if (mHandler == null) return;
        // Do the volume changing separately to give responsive UI
        mLastProgress = progress;
        mHandler.removeMessages(MSG_SET_STREAM_VOLUME);
        mHandler.sendMessage(mHandler.obtainMessage(MSG_SET_STREAM_VOLUME));
    }

    // 開始拖動不作處理(音量變化由onProgressChanged通工作知線程去更新音量值)
    public void onStartTrackingTouch(SeekBar seekBar) {
    }

    // 拖動停止時開始處理通知工作線程播放,因爲需要預覽暫時設置好的音量效果
    public void onStopTrackingTouch(SeekBar seekBar) {
        postStartSample();
    }

    // 預留了供APP調用用於手動預覽音量效果和停止預覽的接口
    public void startSample() {
        postStartSample();
    }
    public void stopSample() {
        postStopSample();
    }

    // 供APP調用用於逐格調節音量的接口,比如系統的Volume+-按鈕觸發
    // 將通知工作線程設置音量和播放效果
    public void changeVolumeBy(int amount) {
        mSeekBar.incrementProgressBy(amount);
        postSetVolume(mSeekBar.getProgress());
        postStartSample();
        mVolumeBeforeMute = -1;
    }

    // 供APP調用用於設置是否靜音的接口,比如系統的靜音按鈕觸發
    // 將通知工作線程設置音量和播放效果
    public void muteVolume() {
        if (mVolumeBeforeMute != -1) {
            mSeekBar.setProgress(mVolumeBeforeMute, true);
            postSetVolume(mVolumeBeforeMute);
            postStartSample();
            mVolumeBeforeMute = -1;
        } else {
            mVolumeBeforeMute = mSeekBar.getProgress();
            mSeekBar.setProgress(0, true);
            postStopSample();
            postSetVolume(0);
        }
    }

    // 定義在UI線程的Handler,用於更新SeekBar進度
    private final class H extends Handler {
        private static final int UPDATE_SLIDER = 1;

        @Override
        public void handleMessage(Message msg) {
            if (msg.what == UPDATE_SLIDER) {
                if (mSeekBar != null) {
                    mLastProgress = msg.arg1;
                    mLastAudibleStreamVolume = msg.arg2;
                    final boolean muted = ((Boolean)msg.obj).booleanValue();
                    if (muted != mMuted) {
                        mMuted = muted;
                        if (mCallback != null) {
                            mCallback.onMuted(mMuted, isZenMuted());
                        }
                    }
                    updateSeekBar();
                }
            }
        }

        public void postUpdateSlider(int volume, int lastAudibleVolume, boolean mute) {
            obtainMessage(UPDATE_SLIDER, volume, lastAudibleVolume, new Boolean(mute)).sendToTarget();
        }
    }

    // 通知UI線程更新SeekBar
    private void updateSlider() {
        if (mSeekBar != null && mAudioManager != null) {
            final int volume = mAudioManager.getStreamVolume(mStreamType);
            final int lastAudibleVolume = mAudioManager.getLastAudibleStreamVolume(mStreamType);
            final boolean mute = mAudioManager.isStreamMute(mStreamType);
            mUiHandler.postUpdateSlider(volume, lastAudibleVolume, mute);
        }
    }

    // 監聽到系統音量變化通知UI線程刷新
    private final class Observer extends ContentObserver {
        public Observer(Handler handler) {
            super(handler);
        }

        @Override
        public void onChange(boolean selfChange) {
            super.onChange(selfChange);
            updateSlider();
        }
    }

    // 監聽音量變化廣播,必要時向UI線程發送刷新請求
    private final class Receiver extends BroadcastReceiver {
        …
        public void onReceive(Context context, Intent intent) {
            …else if (AudioManager.STREAM_DEVICES_CHANGED_ACTION.equals(action)) {
                int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
                int streamVolume = mAudioManager.getStreamVolume(streamType);
                updateVolumeSlider(streamType, streamVolume);
            } else if (NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED.equals(action)) {
                mZenMode = mNotificationManager.getZenMode();
                updateSlider();
            }
        }
    }
}

總結上述過程。
SeekBarVolumizer的作用是將SeekBar和音量設置產生關聯,讓UI上的展示和設置的數值保持一致。
SeekBar上拖動條拖動或按鍵觸發的音量調節由SeekBarVolumizer經Handler向工作線程發出數值更新,播放和停止的請求。
SeekBarVolumizer監聽系統音量,鈴聲的設置經Handler向UI線程發出UI刷新的請求。

 

除了系統公開的Preference組件外,系統Settings APP也自定了不少組件。

Settings自定義Preference分析

比如:
移動/Wi-Fi使用量畫面展示數據使用圖表的ChartDataUsagePreference。

比如點擊設置項目後彈出下拉列表的DropdownPreference。

比如開發者選項畫面裏用來展示收集日誌的BugreportPreference。 

簡單看下上述Preference是如何自定義的。

ChartDataUsagePreference

public class ChartDataUsagePreference extends Preference {
    public ChartDataUsagePreference(Context context, AttributeSet attrs) {
        …
        // 指定包含圖表UsageView的自定義佈局
        setLayoutResource(R.layout.data_usage_graph); 
    }

    // 採用的是support包的Preference
    // 覆寫了類似onBindView()的onBindViewHolder()
    // 針對自定義佈局內的UsageView做些初始化處理
    public void onBindViewHolder(PreferenceViewHolder holder) {
        super.onBindViewHolder(holder);
        UsageView chart = (UsageView) holder.findViewById(R.id.data_usage);
        if (mNetwork == null) return;

        int top = getTop();
        chart.clearPaths();
        chart.configureGraph(toInt(mEnd - mStart), top);
        calcPoints(chart);
        chart.setBottomLabels(new CharSequence[] {
                Utils.formatDateRange(getContext(), mStart, mStart),
                Utils.formatDateRange(getContext(), mEnd, mEnd),
        });

        bindNetworkPolicy(chart, mPolicy, top);
    }

    // 根據系統的NetworkPolicy接口設置圖表的屬性
    private void bindNetworkPolicy(UsageView chart, NetworkPolicy policy, int top) {
        …
        if (policy.limitBytes != NetworkPolicy.LIMIT_DISABLED) {
            topVisibility = mLimitColor;
            labels[2] = getLabel(policy.limitBytes, R.string.data_usage_sweep_limit, mLimitColor);
        }

        if (policy.warningBytes != NetworkPolicy.WARNING_DISABLED) {
            chart.setDividerLoc((int) (policy.warningBytes / RESOLUTION));
            float weight = policy.warningBytes / RESOLUTION / (float) top;
            float above = 1 - weight;
            chart.setSideLabelWeights(above, weight);
            middleVisibility = mWarningColor;
            labels[1] = getLabel(policy.warningBytes, R.string.data_usage_sweep_warning,
                    mWarningColor);
        }

        chart.setSideLabels(labels);
        chart.setDividerColors(middleVisibility, topVisibility);
    }
    …
}

總結:ChartDataUsagePreference指定包含圖表UsageView的自定義佈局替換系統默認的Preference佈局,並通過業務相關的NetworkPolicy接口獲取數據去填充圖表達到展示獨特Ui的設置組件的目的。

DropdownPreference

public class DropDownPreference extends ListPreference {
    private Spinner mSpinner; // 內部持有Spinner實例
    
    public DropDownPreference(Context context, AttributeSet attrs, int defStyleAttr,
            int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        mContext = context;
        mAdapter = createAdapter(); // 創建Spinner用的Adapter

        updateEntries();
    }

    // 複寫父類方法指定更改了佈局的Adapter實例
    protected ArrayAdapter createAdapter() {
        return new ArrayAdapter<>(mContext, android.R.layout.simple_spinner_dropdown_item);
    }

    
    protected void onClick() {
        mSpinner.performClick(); // Spinner處理點擊事件
    }

    // 複寫父類的數據源往Adapter裏填充
    public void setEntries(@NonNull CharSequence[] entries) {
        super.setEntries(entries);
        updateEntries();
    }

    private void updateEntries() {
        mAdapter.clear();
        if (getEntries() != null) {
            for (CharSequence c : getEntries()) {
                mAdapter.add(c.toString());
            }
        }
    }

    // 複寫數據更新回調,通知Spinner刷新
    protected void notifyChanged() {
        super.notifyChanged();
        mAdapter.notifyDataSetChanged();
    }

    // 複寫綁定邏輯,將Spinner和數據綁定
    public void onBindViewHolder(PreferenceViewHolder view) {
        mSpinner = (Spinner) view.itemView.findViewById(R.id.spinner);
        mSpinner.setAdapter(mAdapter);
        mSpinner.setOnItemSelectedListener(mItemSelectedListener);
        // 設置Spinner初始選中項目
mSpinner.setSelection(findSpinnerIndexOfValue(getValue()));
        super.onBindViewHolder(view);
    }

    // 監聽Spinner點擊事件,將設置保存
    private final OnItemSelectedListener mItemSelectedListener = new OnItemSelectedListener() {
        @Override
        public void onItemSelected(AdapterView<?> parent, View v, int position, long id) {
            if (position >= 0) {
                String value = getEntryValues()[position].toString();
                if (!value.equals(getValue()) && callChangeListener(value)) {
                    setValue(value);
                }
            }
        }
        …
    };
}

總結
DropdownPreference定製的是點擊後UI變爲Spinner,本身的UI和一般的Preference並沒有什麼區別。
而ListPreference是系統提供的點擊後彈出帶ListView的對話框的Preference,和上述的定製需求類似。
所以AOSP選擇繼承自ListPreference並複寫click事件將處理由dialog彈出變爲Spinner的彈出。同時將複寫了其他函數將數據的綁定切換爲針對Spinner的數據處理。

注意:事實上這個Preference還是更改了本身的佈局的。構造函數裏指定了dropdownPreferenceStyle的默認attr,該attr將會指定一個包含Spinner控件的佈局。只不過在佈局裏將Spinner設置爲隱藏,導致該Preference和普通Preference並無明顯區別。

BugreportPreference

public class BugreportPreference extends CustomDialogPreference {
    …
    protected void onPrepareDialogBuilder(Builder builder, DialogInterface.OnClickListener listener) {
        // 指定自定義Dialog的佈局
        final View dialogView = View.inflate(getContext(), R.layout.bugreport_options_dialog, null);
        …
        // 監聽採集LOG選項的點擊事件
        final View.OnClickListener l = new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                if (v == mFullTitle || v == mFullSummary) {
                    mInteractiveTitle.setChecked(false);
                    mFullTitle.setChecked(true);
                }
                if (v == mInteractiveTitle || v == mInteractiveSummary) {
                    mInteractiveTitle.setChecked(true);
                    mFullTitle.setChecked(false);
                }
            }
        };
        mInteractiveTitle.setOnClickListener(l);
        mFullTitle.setOnClickListener(l);
        mInteractiveSummary.setOnClickListener(l);
        mFullSummary.setOnClickListener(l);

        builder.setPositiveButton(com.android.internal.R.string.report, listener);
        builder.setView(dialogView);
    }

    // 複寫Dialog點擊事件,OK的情況下按需調用採集LOG處理
    protected void onClick(DialogInterface dialog, int which) {
        if (which == DialogInterface.BUTTON_POSITIVE) {

            final Context context = getContext();
            if (mFullTitle.isChecked()) {
                Log.v(TAG, "Taking full bugreport right away");
                FeatureFactory.getFactory(context).getMetricsFeatureProvider().action(context,
                        MetricsEvent.ACTION_BUGREPORT_FROM_SETTINGS_FULL);
                takeBugreport(ActivityManager.BUGREPORT_OPTION_FULL);
            }…
        }
    }

    // 封裝的調用系統採集LOG函數
    private void takeBugreport(int bugreportType) {
        try {
            ActivityManager.getService().requestBugReport(bugreportType);
        } catch (RemoteException e) {
            Log.e(TAG, "error taking bugreport (bugreportType=" + bugreportType + ")", e);
        }
    }
}

總結
BugreportPreference通過繼承自CustomDialogPreference複寫佈局和監聽邏輯達到展示採集LOG設置條目的目的。

 

上述分類,分析並總結了典型的系統及Settings APP提供的自定義Preference組件,使我們對於自定義原理有了清晰的瞭解。

我們整理歸納下自定義Preference的方法。

自定義Preference方法

■指定style法

定義一個指定了佈局的style給Activity。

比如:

<style name="MyTheme">
    <item name="preferenceStyle">@style/MyPreferenceStyle</item>
</style>

<style name=" MyPreferenceStyle">
    <item name="android:layout">@layout/my_preference_layout</item>
</style>

備註:
其實不止Preference,像PreferenceFragment,PreferenceScreen,EditTextPreference等都有屬於自己的sytle用的attr。通過官網或者源碼找到對應的attr名稱,APP可以靈活指定自己的style。

■佈局或者JAVA調用法

在Preference佈局裏利用layout標籤或者調用setLayoutResource()去指定自己的佈局。

比如:

<PreferenceScreen>
    <Preference
        android:layout=”@layout/my_preference_layout”
        …
/>
    …
</PreferenceScreen>
或
myPreferenceInstance.setLayoutResource(R.layout. my_preference_layout);

以上兩種方法只適用於簡單的UI定製,無法適用於複雜場景或者UI改動較大的需求。

■複寫系統Preference組件靈活定製

public class MyPreference extends Preference {

// 複寫必要的構造函數。
    // 用於佈局裏使用該Preference時使用
    public MyPreference(Context context, AttributeSet attrs) {
        // 可以參考父類指定默認的attr名
        // 也可以指定自定義的attr,爲方便APP在xml的靈活配置
        this(context, attrs, xxx); 
    }

    // 用於Java裏手動創建Preference時使用
    public DialogPreference(Context context) {
        this(context, null);
    }


// 複寫必要的View綁定邏輯
    // 繼承自base包下Preference時使用
    protected void onBindView(View view) {
        …
    }
    // 繼承自support包下Preference時使用
    public void onBindViewHolder(PreferenceViewHolder view) {
        …
    }


// 複寫點擊事件(如果需要定製點擊處理的話)
    protected void onClick() {
        …
    }


// 複寫一些特定的父類的處理(如果由需要的話)
    // 比如SeekbarDialogPreference需要將dialog佈局內icon隱藏
    protected void onBindDialogView(View view) {
        …
    }
    ...
}

在實際的開發過程中,我們可以根據業務需求去尋找現成的Preference組件,避免重複造輪子。

如果沒有現成的,考慮通過style或者java簡單定製是否可以達到目的。

最後只能通過繼承複寫的方法精準達到我們的目的,當然選擇類似要求的已有Preference組件來複寫將達到事半功倍的效果。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章