如何*动态*改变 iOS/Android 的每种颜色?例如。在 Droid 代码中,覆盖“Styles.xml”中的“colorAccent”

Posted

技术标签:

【中文标题】如何*动态*改变 iOS/Android 的每种颜色?例如。在 Droid 代码中,覆盖“Styles.xml”中的“colorAccent”【英文标题】:How *dynamically* change EVERY color iOS/Android? E.g. in Droid code, override "colorAccent" in "Styles.xml" 【发布时间】:2019-11-29 18:26:39 【问题描述】:

我正在向 Xamarin Forms 应用程序添加颜色主题。 主题被指定为动态变化,具体取决于用户站在/靠近哪个客户的位置。我们有“浅色”和“深色”主题的客户,有多种调色板;因此需要能够更改所有显示项目的所有颜色。动态的。

到目前为止,我的动态主题适用于几乎所有类型的视图。

在为 android 上的 ios 样式(垂直滚轮)Xamarin.Forms.Picker 着色时,我坚持两个细节

问题一:如何改变背景颜色?问题二:如何改变“取消”按钮的文字颜色?

这个基于 Android NumberPicker 的 XF 视图似乎不能开箱即用地完全自定义颜色。截至最新的 XF,4.3.0.991211。


--我试过的:-----


A.在Style.xml 资源文件中静态地设置 Android 主题颜色。成功。但它是静态的。


B.下载Xamarin Forms Light/Dark Theme Sample。

在 Android 上构建和运行。转到其“选择主题”选择器。选择浅色主题。再次转到选择器 - 白色背景,到目前为止,非常好。选择深色主题。再次转到选择器 - 选择器仍然是黑白的。这里没有解决方案。


C.在网上找到了如何动态更改AlertDialogs“确定”/“取消”按钮文本颜色,使用 AlertDialog“构建器”,并获取资源 ID。成功实现了这一点。不确定如何将相同的技术应用于 Picker。

问题 3:如何发现 XF Picker 使用的 Android Control 中使用了哪些资源 ID?


D.克隆 Xamarin Forms 源代码。将 Droid PickerRenderer 复制到我的代码中。大多数代码无法运行,因为它依赖于internal 类/函数。让我的继承自内置的,并注释掉任何不需要/不会编译的代码。

自定义OnElementChanged,因为这是设置TextColor 的位置。

在自定义渲染器源代码中,IPickerRenderer.OnClick() 构建一个包含NumberPickerAlertDialog。我没有看到任何设置背景颜色或“取消”按钮文本颜色的逻辑。但至少我们知道 Android 类。 请参阅上面的问题 3。


E.至少,having a small number of predefined themes 可能是一个解决方案。或Android theme.ApplyStyle - 但这可以是动态 风格吗?我还没有测试过这些中的任何一个是否成功地改变了我坚持的细节。 [我尝试后会报告。]


F。 重新启动应用程序可以更改主题:any-color themes。这将是我最后的手段;首先看看我是否可以在 C# 中完全控制颜色,而无需重新启动。


注意:在 SO 和 Xamarin.Forums 上有很多帖子一般讨论 Xamarin Forms 中的主题。我已经过了这一点,所以我不会在这里列出这些,除了提到Clint St.Laurent's theming post,它(快速浏览一下)看起来与我所做的相似。 我只剩下少量的小细节——在我的测试中,Xamarin Forms 未能将主题颜色应用到这些细节上。


G.其他看似相关的 SO 帖子 - 但不执行问题 1 和 2 中列出的确切任务:

Picker implemented as an AlertDialog。显示自定义 PickerRenderer。但没有显示如何在 Q 1 & 2 中设置颜色。

另一个Picker implemented as an AlertDialog。同样。

How to really change primary and accent color in android lollipop - 我在EF 中提到了这个问题的答案。如果我无法通过自定义 PickerRenderer 中的 C# 代码实现结果,我会调查这些。


Xamarin 论坛帖子:

How to change the colorAccent at runtime?。给出的唯一答案不会改变主题的colorAccent。它只是改变了应用栏的颜色。


澄清:有很多文档、论坛和博客文章描述了 Xamarin Forms 主题的一般机制。不是在寻找答案。我只是想解决少量缺失的细节,从最新的稳定 Xamarin Forms 4.3.0.991211 开始。 希望 Xamarin Forms 成功地为所有类型控件的 100% 详细信息设置主题的那一天会到来 - 届时此问答将过时。

【问题讨论】:

【参考方案1】:

问题中D. 的实现。在代码中以 cmets 形式给出的学分。

自定义 Android PickerRenderer 动态设置 3 种颜色:文本(蓝色)、文本“轮”后面的背景(浅橙色)、按钮(深红色)。

澄清:static Android 主题具有深色背景。目标是动态地从暗色主题变为亮色主题。为了使成功设置的颜色非常明显,使用了强烈着色的颜色。

我尚未着色的文本之间的白色“分隔线”。我已经在 Xamarin 或 Android 文档中看到了可绘制分隔线的内容。掌握它并设置它的颜色应该很简单。 编辑也许 titleDivider 在 Change picker control pop-up background and text colors 或 Vahid's answer 中的 Tiago Flores 代码 sn-p。或selectionDividerField 中的Tapa Save's answer。

注意:我没有添加自定义 XF 属性来控制按钮颜色;硬编码为Color.Accent

using Android.App;
using Android.Util;
using Android.Views;
using Android.Widget;
using System;
using System.ComponentModel;
using System.Linq;
using Orientation = Android.Widget.Orientation;
using Android.Content;
using Android.Text;
using Android.Text.Style;
using Java.Lang;

// PickerEditText
using Xamarin.Forms.Platform.Android;

using AColor = Android.Graphics.Color;
using Picker = Xamarin.Forms.Picker;

[assembly: Xamarin.Forms.ExportRenderer(typeof(Picker), typeof(LCPickerRenderer))]
namespace Xamarin.Forms.Platform.Android

    // Based on Xamarin.Forms source code, modified.
    public class LCPickerRenderer : PickerRenderer, IPickerRenderer   //ViewRenderer<Picker, EditText>, IPickerRenderer
    
        AlertDialog _dialog;
        NumberPicker _picker;

        public LCPickerRenderer(Context context) : base(context)
        
            //AutoPackage = false;
        

        [Obsolete("This constructor is obsolete as of version 2.5. Please use PickerRenderer(Context) instead.")]
        [EditorBrowsable(EditorBrowsableState.Never)]
        public LCPickerRenderer()
        
            //AutoPackage = false;
        

        IElementController ElementController => Element as IElementController;

        protected override void OnElementChanged(ElementChangedEventArgs<Picker> e)
        
            if (e.OldElement != null) 
                //((INotifyCollectionChanged)e.OldElement.Items).CollectionChanged -= RowsCollectionChanged;
            

            base.OnElementChanged(e);

            if (e.NewElement != null) 
                // --- custom work ---
                if (Control != null) 
                    SetTextColor();
                    SetBackgroundColor();
                
            
        

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        
            base.OnElementPropertyChanged(sender, e);

            if (e.PropertyName == Picker.BackgroundColorProperty.PropertyName) 
                SetBackgroundColor();
             else if (e.PropertyName == Picker.TextColorProperty.PropertyName) 
                SetTextColor();
            

            //if (e.PropertyName == Picker.TitleProperty.PropertyName || e.PropertyName == Picker.TitleColorProperty.PropertyName)
            //    UpdatePicker();
            //else if (e.PropertyName == Picker.SelectedIndexProperty.PropertyName)
            //    UpdatePicker();
            //else if (e.PropertyName == Picker.CharacterSpacingProperty.PropertyName)
            //    UpdateCharacterSpacing();
            //else if (e.PropertyName == Picker.TextColorProperty.PropertyName)
            //    UpdateTextColor();
            //else if (e.PropertyName == Picker.FontAttributesProperty.PropertyName || e.PropertyName == Picker.FontFamilyProperty.PropertyName || e.PropertyName == Picker.FontSizeProperty.PropertyName)
            //    UpdateFont();
        


        void IPickerRenderer.OnClick()
        
            Picker model = Element;

            if (_dialog != null)
                return;

            var xfTextColor = Element.TextColor;
            _picker = new NumberPicker(Context);
            if (model.Items != null && model.Items.Any()) 
                _picker.MaxValue = model.Items.Count - 1;
                _picker.MinValue = 0;
                _picker.SetDisplayedValues(model.Items.ToArray());
                _picker.WrapSelectorWheel = false;
                _picker.DescendantFocusability = DescendantFocusability.BlockDescendants;
                _picker.Value = model.SelectedIndex;
            

            var layout = new LinearLayout(Context)  Orientation = Orientation.Vertical ;
            layout.AddView(_picker);

            ElementController.SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, true);

            var builder = new AlertDialog.Builder(Context);
            builder.SetView(layout);

            if (!Element.IsSet(Picker.TitleColorProperty)) 
                builder.SetTitle(model.Title ?? "");
             else 
                var title = new SpannableString(model.Title ?? "");
                title.SetSpan(new ForegroundColorSpan(model.TitleColor.ToAndroid()), 0, title.Length(), SpanTypes.ExclusiveExclusive);

                builder.SetTitle(title);
            

            // TODO: get texts (and whether to show) from XF Element.
            builder.SetNegativeButton(global::Android.Resource.String.Cancel, (s, a) => 
                ElementController.SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, false);
                _dialog = null;
            );
            builder.SetPositiveButton(global::Android.Resource.String.Ok, (s, a) => 
                ElementController.SetValueFromRenderer(Picker.SelectedIndexProperty, _picker.Value);
                // It is possible for the Content of the Page to be changed on SelectedIndexChanged. 
                // In this case, the Element & Control will no longer exist.
                if (Element != null) 
                    if (model.Items.Count > 0 && Element.SelectedIndex >= 0)
                        Control.Text = model.Items[Element.SelectedIndex];
                    ElementController.SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, false);
                
                _dialog = null;
            );

            _dialog = builder.Create();
            _dialog.DismissEvent += (sender, args) => 
                ElementController?.SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, false);
                _dialog?.Dispose();
                _dialog = null;
            ;
            _dialog.Show();

            // TODO: Control this via a custom XF property.
            var buttonColor = Color.Accent.ToAndroid();
            // From https://***.com/a/56555965/199364.
            var btnCancel = _dialog.GetButton((int)DialogInterface.ButtonNegative);
            // "?" in case no such button was attached.
            btnCancel?.SetTextColor(buttonColor);
            var btnOK = _dialog.GetButton((int)DialogInterface.ButtonPositive);
            btnOK?.SetTextColor(buttonColor);

            SetTextColor();
            SetBackgroundColor();
        

        private void SetTextColor()
        
            Control?.SetTextColor(Element.TextColor.ToAndroid());
            if (_picker != null)
                SetTextColor(_picker, Element.TextColor.ToAndroid());
        

        private void SetBackgroundColor()
        
            Control?.SetBackgroundColor(Element.BackgroundColor.ToAndroid());
            _picker?.SetBackgroundColor(Element.BackgroundColor.ToAndroid());
            _dialog?.Window.SetBackgroundDrawable(new global::Android.Graphics.Drawables.ColorDrawable(Element.BackgroundColor.ToAndroid()));
        

        // From https://***.com/a/26657169/199364
        // TODO: When support API 29, this needs to be modified re https://***.com/a/56883356/199364 ?
        public static bool SetTextColor(NumberPicker numberPicker, AColor color)
        
            var sdkInt = global::Android.OS.Build.VERSION.SdkInt;
            //var limitSdk = global::Android.OS.Build.VERSION_CODES.Q;

            int count = numberPicker.ChildCount;
            for (int i = 0; i < count; i++) 
                var child = numberPicker.GetChildAt(i);
                if (child.GetType() == typeof(EditText)) 
                    try 
                        var selectorWheelPaintField = numberPicker.Class
                                                                    .GetDeclaredField("mSelectorWheelPaint");
                        selectorWheelPaintField.Accessible = true;

                        EditText editText = (EditText)child;
                        editText.SetTextColor(color);

                        var paint = (global::Android.Graphics.Paint)selectorWheelPaintField.Get(numberPicker);
                        paint.Color = color;

                        numberPicker.Invalidate();
                        return true;
                    
                    catch (NoSuchFieldException e) 
                        Log.Warn("setNumberPickerTextColor", e);
                    
                    catch (IllegalAccessException e) 
                        Log.Warn("setNumberPickerTextColor", e);
                    
                    catch (IllegalArgumentException e) 
                        Log.Warn("setNumberPickerTextColor", e);
                    
                
            
            return false;
        
    


奖励:为 Android AlertDialog 设置样式:

大多数对话框都可以使用 AlertDialog.Builder 在 Android 上构建(在您的 Android 项目代码中)。这可以在制作自定义渲染器的情况下完成。在这段代码中,我使用了来自我的 XF 应用程序资源的各种命名颜色。 Google 技术在 Xamarin Form 中进行颜色主题化,了解如何将此类资源设置为不同的颜色值:

// Extracted from a larger class. Some of these not used in the code here.
using System;
using System.Collections.Generic;
using System.IO;

using Android.Content;
using Android.Content.PM;
using Android.Graphics;
using Android.Provider;
using Android.Views;
using Android.Widget;

using Android.Support.V4.Content;
using Android.Support.V4.App;
using Android.Support.V7.App;

// For Extension: color.ToAndroid.
using Xamarin.Forms.Platform.Android;

public static partial class UI


    // If cancelHandler is null, no Cancel button will be added.
    // If noHandler is not null, then "OK" is renamed "Yes".
    public static void ShowAlertDialog(string title, string message,
                                        EventHandler okHandler, EventHandler cancelHandler,
                                        EventHandler noHandler = null, bool textIsYes = false)
    
        bool hasCancel = (cancelHandler != null);
        bool hasNo = (noHandler != null);
        string okOrYes = PS.LocalizedString((textIsYes || hasNo ? "Yes" : "OK"));
        // When both cancel and no, is "Cancel".
        // When neither cancel or no, defaults to "Cancel", which does nothing.
        // The only time this is "No", is if there is a No button, but no Cancel button.
        string cancelOrNo = PS.LocalizedString((hasNo && !hasCancel ? "No" : "Cancel"));
        EventHandler cancelOrNoHandler = (hasCancel ? cancelHandler : noHandler);

        AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.it);

        var titleOb = new Android.Text.SpannableString(title);
        // TODO: Add custom XF property to set this.
        var titleColor = (Xamarin.Forms.Color)Xamarin.Forms.Application.Current.Resources["MaxForegroundColor"];
        titleOb.SetSpan(new Android.Text.Style.ForegroundColorSpan(titleColor.ToAndroid()), 0, title.Length, Android.Text.SpanTypes.ExclusiveExclusive);
        builder.SetTitle(titleOb);

        builder.SetMessage(message);

        if (hasCancel || hasNo) 
            builder.SetNegativeButton(cancelOrNo,
                                 (s, e) => 
                                     cancelOrNoHandler?.Invoke(null, EventArgs.Empty);
                                 );
        

        // Only used when BOTH Cancel and No buttons.
        if (hasNo && hasCancel)
            builder.SetNeutralButton(PS.LocalizedString("No"),
                                (s, e) =>  noHandler.Invoke(null, EventArgs.Empty); );

        builder.SetPositiveButton(okOrYes, (s, e) => 
            if (okHandler != null)
                okHandler.Invoke(null, EventArgs.Empty);
        );

        AlertDialog dialog = builder.Show();
        SetAlertStyle(dialog);
    

    /// <summary>
    /// This doesn't set Title Color. Set that earlier, while building the dialog.
    /// </summary>
    /// <param name="dialog"></param>
    private static void SetAlertStyle(AlertDialog dialog)
    
        // Use these if succeed in changing both title and background colors.
        var foreColor = (Xamarin.Forms.Color)Xamarin.Forms.Application.Current.Resources["ForegroundColor"];
        var accentAgainstBack = (Xamarin.Forms.Color)Xamarin.Forms.Application.Current.Resources["AccentSaturated25Fore33"];

        var backColor = (Xamarin.Forms.Color)Xamarin.Forms.Application.Current.Resources["BackgroundColor"];
        SetBackgroundColor(dialog, backColor);

        //// DIDN'T WORK: No effect on title color. Set earlier, while building the dialog.
        //int alertViewId = Android.App.Application.Context.Resources.GetIdentifier("alertTitle", "id", "android");
        //SetItemTextColor(dialog, "alertTitle", accentAgainstBack);

        SetItemTextColor(dialog, "message", foreColor);

        SetItemTextColor(dialog, "button1", (Xamarin.Forms.Color)Xamarin.Forms.Application.Current.Resources["AccentSaturated25Fore33"]);
        SetItemTextColor(dialog, "button2", (Xamarin.Forms.Color)Xamarin.Forms.Application.Current.Resources["ForegroundColor"]);
    

    private static void SetItemTextColor(AlertDialog dialog, string itemResourceName, Xamarin.Forms.Color color)
    
        int textId = Android.App.Application.Context.Resources.GetIdentifier(itemResourceName, "id", "android");
        TextView text = dialog.FindViewById<TextView>(textId);
        text?.SetTextColor(color.ToAndroid());
    

    private static void SetBackgroundColor(AlertDialog dialog, Xamarin.Forms.Color xfColor)
    
        dialog?.Window.SetBackgroundDrawable(new global::Android.Graphics.Drawables.ColorDrawable(xfColor.ToAndroid()));
    


【讨论】:

以上是关于如何*动态*改变 iOS/Android 的每种颜色?例如。在 Droid 代码中,覆盖“Styles.xml”中的“colorAccent”的主要内容,如果未能解决你的问题,请参考以下文章

React Native之code-push的热更新(ios android)

c++如何改变控制台文字颜色

React Native之code-push的热更新(ios android)

如何在 Flutter 上更改颜色索引。你好,我想用值''einmal'改变索引的颜色

书单|可以提高男生颜值的7本书(入门篇)

Flutter:如何避免改变方向?