1. 程式人生 > >Android自定義通知欄Notification字型適配問題

Android自定義通知欄Notification字型適配問題

前言

專案之前自定義的通知欄是一直有一個問題,就是不同的手機廠商生產的手機狀態列的背景顏色是不同的,所以自定義佈局的字型顏色就會出現衝突,看了幾種適配方案都不是特別完美,把官方文件看了個遍,最後在前輩的總結下新增自己的方案終於得到了一個比較完美的解決方案。文章底部附送github連結。

方案

通過設定style的方式

通過設定style的方式來設定字型顏色可以達到字型顏色隨著下拉選單的背景色變化而變化,字型的顏色會跟隨系統的字型顏色。

這個方案給出了低於5.0的Android版本和高於5.0的Android版本的不同的方案,在低於5.0版本的新增一行程式碼 android:textAppearance="@style/TextAppearance.StatusBar.EventContent" 如下:

<TextView
    android:id="@+id/notification_content_title"
    android:textAppearance="@style/TextAppearance.StatusBar.EventContent"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Custom_content_title" />

在高於5.0的版本中,在res的values-v21目錄下定義styles.xml檔案如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style
        name="NotificationContentTitle"
        parent="@android:style/TextAppearance.Material.Notification.Title" />
    <style
        name="NotificationContentText"
        parent="@android:style/TextAppearance.Material.Notification.Line2"
/> </resources>

自定義通知佈局檔案呼叫:

 <LinearLayout
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:orientation="vertical" >

            <TextView
                android:id="@+id/notification_content_title"
                style="@style/NotificationContentTitle"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Custom_content_title" />

            <TextView
                android:id="@+id/notification_content_text"
                style="@style/NotificationContentText"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Custom_content_text" />
        </LinearLayout>

缺點:要適配5.0以上和5.0以下版本,在測試過程中在部分機型改變手機主題後字型顏色並沒有跟隨系統的字型顏色。

通過設定的contentTitle和contentText的文字來獲取

這種方法通過我們建立預設的Notification設定我們指定的文字後通過遍歷拿到子Tview,在通過 getText()判斷是不是我們設定的文字,如果是就通過.getTextColors().getDefaultColor()拿到系統預設的字型顏色。

首先我們需要建立一個預設的系統通知並且設定我們指定的ContentTitle和ContentText

/**
     * 建立預設的Notification獲取預設通知內RemoteViews
     *
     * @param context
     * @return
     */
    @SuppressWarnings("deprecation")
    private RemoteViews buildFakeRemoteViews(Context context) {
        Notification.Builder builder;
                builder = new Notification.Builder(context);
                builder.setContentTitle(fakeContentTitle)
                       .setContentText(fakeContentText)
                       .setTicker("fackTicker");
                Notification notification = builder.getNotification();
                RemoteViews remoteViews = notification.contentView;
                    return remoteViews;
    }

我們可以看到返回的是一個RemoteView,這個RemoteViews給我們提供了一種可以在其他程序中生成View並進行更新的機制,這裡就不介紹RemoteViews了,這裡我們主要是通過RemoteViews在自定義的通知佈局中設定了TextView並在程式碼中動態地更新了其顯示的內容。少年你以為這樣就行了麼,哼 你還是太年輕了?。在android N之後的版本經測試notification.contentView會返回null,如果返回null那麼我們根本拿不到裡面的佈局。這時候怎麼辦,少年你別慌?,這時候基礎我們的法寶 百度 谷歌 。但是我硬是沒搜到解決方案,這條路走不通可怎麼辦。我尼瑪?,不解決我下面怎麼吹NB啊,我默默的開啟的我的寶典?Android Documentation ,這裡我們可看到文件告訴我們為什麼返回null。 在這裡插入圖片描述 最後在我翻遍了所有關於Notification的文件後找到了這個方法: 在這裡插入圖片描述 可以看到文件說返回的是一個最終的RemoteViews的內容檢視,這樣我們的問題就解決了這個問題,我們修改下上面的方法:

/**
     * 建立預設的Notification獲取預設通知內RemoteViews
     *
     * @param context
     * @return
     */
    @SuppressWarnings("deprecation")
    private RemoteViews buildFakeRemoteViews(Context context) {

        Notification.Builder builder;
        builder = new Notification.Builder(context);
        builder.setContentTitle(fakeContentTitle)
                .setContentText(fakeContentText)
                .setTicker("fackTicker");

        RemoteViews remoteViews = null;

        if (builder != null) {

            //notification.contentView 在android N 被棄用 返回的remoteViews有可能是空的
            //翻遍官方文件找到builder.createContentView() 來獲取remoteViews
            // 官方文件原話:Construct a RemoteViews for the final 1U notification layout.
            //理論上返回為預設RemoteViews 經測試 有效

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {

                remoteViews = builder.createContentView();

            } else {

                Notification notification = builder.getNotification();
                remoteViews = notification.contentView;
            }

        }
        return remoteViews;
    }

通過我們設定的contentTitle和contentText的文字來獲取對應的textView

 /**
     * 通過我們設定的contentTitle和contentText的文字來獲取對應的textView 
     * @param remoteViews
     * @return
     */
    private boolean NotificationTextColorByText(final RemoteViews remoteViews) {
        if (DEBUG) {
            log("fetchNotificationTextColorByText");
        }
        fetchMode = "ByText";
        try {
            if (remoteViews != null) {
                TextView contentTitleTextView = null, contentTextTextView = null;
                View notificationRootView = remoteViews.apply(context, new FrameLayout(context));

                Stack<View> stack = new Stack<View>();
                stack.push(notificationRootView);
                while (!stack.isEmpty()) {
                    View v = stack.pop();
                    if (v instanceof TextView) {
                        final TextView childTextView = ((TextView) v);
                        final CharSequence charSequence = childTextView.getText();
                        if (TextUtils.equals(fakeContentTitle, charSequence)) {
                            contentTitleTextView = childTextView;
                            
                        } else if (TextUtils.equals(fakeContentText, charSequence)) {
                            contentTextTextView = childTextView;
                          
                        }
                if ((contentTitleTextView != null) && (contentTextTextView != null)) {
                            break;
                        }
                    }
                    if (v instanceof ViewGroup) {
                        ViewGroup vg = (ViewGroup) v;
                        final int count = vg.getChildCount();
                        for (int i = 0; i < count; i++) {
                            stack.push(vg.getChildAt(i));
                        }
                    }
                }
                stack.clear();
                return checkAndGuessColor(contentTitleTextView, contentTextTextView);
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return false;
    }

獲取文字預設顏色:

private boolean checkAndGuessColor(TextView contentTitleTextView, TextView contentTextTextView) {

        if (contentTitleTextView != null) {
            contentTitleColor = contentTitleTextView.getTextColors().getDefaultColor();
        }
        if (contentTextTextView != null) {
            contentTextColor = contentTextTextView.getTextColors().getDefaultColor();
        }
       f (contentTitleColor != INVALID_COLOR && contentTextColor != INVALID_COLOR) {
            return true;
        }

        if (contentTitleColor != INVALID_COLOR) {
            if (isLightColor(contentTitleColor)) {
                contentTextColor = DEFAULT_LIGHT_CONTENT_TEXT_COLOR;
            } else {
                contentTextColor = DEFAULT_DARK_CONTENT_TEXT_COLOR;
            }
            return true;
        }
        if (contentTextColor != INVALID_COLOR) {
            if (isLightColor(contentTextColor)) {
                contentTitleColor = DEFAULT_LIGHT_CONTENT_TITLE_COLOR;
            } else {
                contentTitleColor = DEFAULT_DARK_CONTENT_TITLE_COLOR;
            }
            return true;
        }
        return false;
    }

這樣就解決了我們的需求,不過這個方法有可能不相容AppCompatActivity,但是我測試了暫時沒有出現問題。

通過contentTitle/contentText(反射獲取)的id來取得TextView

為了解決上個方法出現的問題我們在上述的基礎上在通過反射獲取的方法獲取TextView,程式碼比較好理解所以不做過多解釋。

核心程式碼:

/**
     * 通過contentTitle/contentText(反射獲取)的id來取得TextView
     *
     * @param remoteViews
     * @return
     */
    private boolean fetchNotificationTextColorById(final RemoteViews remoteViews) {
        if (DEBUG) {
            log("fetchNotificationTextColorById");
        }
        fetchMode = "ById";
        try {
            final int systemNotificationContentTitleId = getAndroidInternalResourceId("title");//android.R.id.title;
            final int systemNotificationContentTextId = getAndroidInternalResourceId("text");//獲取android.R.id.text
            if (DEBUG) {
                log("systemNotificationContentId -> #" + Integer.toHexString(systemNotificationContentTextId));
            }
            if (remoteViews != null && remoteViews.getLayoutId() > 0) {
                TextView contentTitleTextView = null, contentTextTextView = null;
                View notificationRootView = LayoutInflater.from(context).inflate(remoteViews.getLayoutId(), null);
                View titleView = notificationRootView.findViewById(systemNotificationContentTitleId);
                if (titleView instanceof TextView) {
                    contentTitleTextView = (TextView) titleView;
                }
                if (systemNotificationContentTextId > 0) {
                    View contentView = notificationRootView.findViewById(systemNotificationContentTextId);
                    contentTextTextView = (TextView) contentView;
                }
                return checkAndGuessColor(contentTitleTextView, contentTextTextView);
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return false;
    }

獲取id:

public static int getAndroidInternalResourceId(String resourceName) {
        //獲取"android"包名裡的id
        //即com.android.internal.R.id.resourceName
        //實際上如果getIdentifier沒有的話,下面反射的方式也應該是沒有的
        //defType = "id",還可以有"layout","drawable"之類的
        final int id = Resources.getSystem().getIdentifier(resourceName, "id", "android");//defType和defPackage必須指定
        if (id > 0) {
            return id;
        }

        try {
            // 如果上面的方法沒有返回id 通過反射獲取
            // 反射的方法取com.android.internal.R.id.resourceName
            // 通知欄的大圖示imageView的id="icon"
            // 標題是"title" 內容是"text"
            Class<?> clazz = Class.forName("com.android.internal.R$id");
            Field field = clazz.getField(resourceName);
            field.setAccessible(true);
            return field.getInt(null);
        } catch (Exception e) {
        }
        return 0;
    }

這樣通過這幾個方法我們就能獲取手機的背景色進行設定字型顏色。

按照安卓版本猜測

這種方法只是通過原生的版本進行設定,但是國內廠商都對android進行的訂製所以這種方法不是很有效果,但是若果是原生系統返回為準確的顏色。

程式碼:

    /**
     * 按照安卓版本純猜測
     * 我們要知道每個版本顏色進行設定
     * 這是隻是舉個例子
     */
    private void fetchNotificationTextColorBySdkVersion() {
        fetchMode = "BySdkVersion";
        final int SDK_INT = Build.VERSION.SDK_INT;
        final boolean isLightColor = (SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
                && (SDK_INT < Build.VERSION_CODES.LOLLIPOP);// 安卓3.0到4.4之間是黑色通知欄
        if (isLightColor) {
            contentTitleColor = DEFAULT_LIGHT_CONTENT_TITLE_COLOR;
            contentTextColor = DEFAULT_LIGHT_CONTENT_TEXT_COLOR;
        } else {// DRAK
            if (SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                contentTitleColor = ANDROID_5_CONTENT_TITLE_COLOR;
                contentTextColor = ANDROID_5_CONTENT_TEXT_COLOR;
            } else {
                contentTitleColor = DEFAULT_DARK_CONTENT_TITLE_COLOR;
                contentTextColor = DEFAULT_DARK_CONTENT_TEXT_COLOR;
            }
        }
    }

github

地址: 點這

感謝

  • 感謝 Android自定義通知樣式適配 文章,參考 點這.
  • 感謝 設定Android通知欄Notification的字型/圖示顏色隨背景色變化而變化 文章,參考 點這.
  • 感謝 Mixiaoxiao 作者,參考 點這.
  • 感謝 Notification顯示過程詳解 作者,參考 點這.