Avalonia:开发Android应用

我把成功开发Android应用的经过记录下来,在开发过程中,模拟器经常出问题,将Java Development Kit的位置和Android SDK的位置改动一下,就解决了模拟器报错的问题,这是在Github上看到的解决办法。
先建Models文件夹,创建模型ColorItem.cs文件。

using Avalonia.Media;

namespace AvaloniaMobileApp.Models
{
    public class ColorItem
    {
        public Color? Color { get; set; }
        public string? ColorName {  get; set; }
    }
}

再创建ColorItemMessage记录,用于在viewmodel间传递参数。

namespace AvaloniaMobileApp.Models
{
    public  record ColorItemMessage(string Sender,ColorItem Item);    
    
}

先前用ReactiveUI写移动应用,结果闪退了,换用Communitytoolkit.mvvm社区工具,所以将ViewModelBase.cs继承 ObservableRecipient。

using CommunityToolkit.Mvvm.ComponentModel;

namespace AvaloniaMobileApp.ViewModels;

public abstract class ViewModelBase : ObservableRecipient
{
}

在ViewModels文件夹下先创建要展示的页面ViewModel,ColorsViewModel.cs,AboutViewModel.cs,PalletteViewModel.cs。
ColorsViewModel.cs

using Avalonia.Data.Converters;
using Avalonia.Media;
using AvaloniaMobileApp.Models;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using System;
using System.Collections.ObjectModel;
using System.Reflection;
using System.Threading.Tasks;

namespace AvaloniaMobileApp.ViewModels
{
    public partial class ColorsViewModel : ViewModelBase
    {
        public ObservableCollection<ColorItem> Colors { get; }
        public ColorsViewModel()
        {
            Colors = [];
        }

        [ObservableProperty]
        private ColorItem _selectedColorItem = new();

        partial void OnSelectedColorItemChanged(ColorItem value)
        {
            IsActive = true;

            WeakReferenceMessenger.Default.Send(new ColorItemMessage("colors", value));
        }

        [RelayCommand]
        private Task Init()
        {
            if (Colors.Count > 0)
            {
                return Task.CompletedTask;
            }
            var properties = typeof(Colors).GetProperties(BindingFlags.Public | BindingFlags.Static);

            foreach (var property in properties)
            {
                if (property.GetValue(null) is Color color)
                {
                    Colors.Add(new ColorItem
                    {
                        Color = color,
                        ColorName = property.Name
                    });
                }
            }
            return Task.CompletedTask;
        }

        public static FuncValueConverter<Color, string> ColorToHex => new(color =>
        {
            return $"#{color.R:X2}{color.G:X2}{color.B:X2}";
        });

        public static FuncValueConverter<Color, string> ColorToCMYK => new(value =>
        {
            if (value is Color color)
            {
                double r = color.R / 255.0;
                double g = color.G / 255.0;
                double b = color.B / 255.0;

                double k = 1 - Math.Max(Math.Max(r, g), b);

                double c = k < 1 ? (1 - r - k) / (1 - k) : 0;
                double m = k < 1 ? (1 - g - k) / (1 - k) : 0;
                double y = k < 1 ? (1 - b - k) / (1 - k) : 0;

                return $"CMYK = ({Math.Round(c*100,1)}% {Math.Round(m*100,1)}% {Math.Round(y*100,1)}% {Math.Round(k*100,1)}%)";
            }
            else
            {
                return "";
            }
        });
    }
}

AboutViewModel.cs

using System.Reflection;

namespace AvaloniaMobileApp.ViewModels
{
    public class AboutViewModel : ViewModelBase
    {
        public string? AppName => Assembly.GetExecutingAssembly().GetName().Name;

        public string? Version => Assembly.GetExecutingAssembly().GetName().Version!.ToString();

        public string? Message => $"该应用使用 Avalonia框架,Avalonia 是跨平台的优秀框架,这是 Android App,使用 Avalonia 基础导航功能,应用 CommunityToolKit.Mvvm 工具开发";
    }
}

PalletteViewModel.cs

using Avalonia.Media;
using AvaloniaMobileApp.Models;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;

namespace AvaloniaMobileApp.ViewModels
{
    public partial class PalletteViewModel : ViewModelBase, IRecipient<ColorItemMessage>
    {
        [ObservableProperty]
        private Color? _colorType;

        [ObservableProperty]
        private ColorItem? _colorItem;
        public void Receive(ColorItemMessage message)
        {            
            if(message.Sender == "main")
            {
                ColorType = message.Item.Color;
                ColorItem = message.Item;
                Red = message.Item.Color!.Value.R;
                Green = message.Item.Color.Value.G;
                Blue = message.Item.Color.Value.B;
            }
        }

        public PalletteViewModel()
        {
            IsActive = true;
        }

        [ObservableProperty]
        private byte red;

        [ObservableProperty]
        private byte green;

        [ObservableProperty]
        private byte blue;

        private void UpdateColorType()
        {
            ColorType = Color.FromRgb((byte)Red, (byte)Green, (byte)Blue);
        }

        partial void OnRedChanged(byte value) => UpdateColorType(); 
        partial void OnGreenChanged(byte value) => UpdateColorType();
        partial void OnBlueChanged(byte value) => UpdateColorType();
    }
}

更改MainViewModel.cs

using AvaloniaMobileApp.Models;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.DependencyInjection;
using System.Threading.Tasks;

namespace AvaloniaMobileApp.ViewModels;

public partial class MainViewModel : ViewModelBase, IRecipient<ColorItemMessage>
{
    [ObservableProperty]   
    private ViewModelBase? _currentPage;

    [ObservableProperty]
    private ColorItem _selectedColorItem = new();
   
    public MainViewModel()
    {
        CurrentPage = App.Current.Services?.GetService<ColorsViewModel>();
        IsActive = true;
    }

    public void Receive(ColorItemMessage message)
    {
        if(message.Sender == "colors")
        {
            SelectedColorItem = message.Item;

            GotoPalletteCommand.Execute(null);
        }      
        
    }

    [RelayCommand]
    private Task GotoAbout()
    {
        if(CurrentPage is not AboutViewModel)
        {
            CurrentPage = App.Current.Services?.GetService<AboutViewModel>();
        }
        return Task.CompletedTask;
    }

    [RelayCommand]
    private Task GotoHome()
    {
        if(CurrentPage is not ColorsViewModel)
        {
            CurrentPage = App.Current.Services?.GetService<ColorsViewModel>();
        }
        return Task.CompletedTask;
    }

    [RelayCommand]
    private Task GotoPallette()
    {
        if (CurrentPage is not PalletteViewModel)
        {
           
            CurrentPage = App.Current.Services!.GetService<PalletteViewModel>();

            WeakReferenceMessenger.Default.Send(new ColorItemMessage("main", SelectedColorItem));
        }

        return Task.CompletedTask;
    }
}

创建要展示的Views,ColorsView.axaml。

<UserControl xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
			 xmlns:vm="using:AvaloniaMobileApp.ViewModels"
			 xmlns:models="using:AvaloniaMobileApp.Models"
			 xmlns:b="using:AvaloniaMobileApp.Behaviors"
			 x:DataType="vm:ColorsViewModel"
			 b:LoadedBehavior.ExecuteCommand="{Binding InitCommand}"
             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
             x:Class="AvaloniaMobileApp.Views.ColorsView">	
  <Grid RowDefinitions="Auto,*,auto">
	  <TextBlock Text="{Binding Colors.Count,StringFormat='Avalonia.Media Colors = {0}'}" Grid.Row="0"/>
	  <ListBox x:Name="ColorsListBox" Grid.Row="1" ItemsSource="{Binding Colors}" SelectedItem="{Binding SelectedColorItem}">
		  <ListBox.ItemTemplate>
			 <DataTemplate x:DataType="models:ColorItem">
				  <StackPanel Orientation="Horizontal">
				  <Rectangle Height="80" Width="80">
					  <Rectangle.Fill>
						  <SolidColorBrush Color="{Binding Color}"/>
					  </Rectangle.Fill>
				  </Rectangle>
				  <StackPanel>
					  <TextBlock Text="{Binding ColorName}"/>
					  <TextBlock Text="{Binding Color,Converter={x:Static vm:ColorsViewModel.ColorToHex}}"/>
					  <TextBlock Text="{Binding Color,Converter={x:Static vm:ColorsViewModel.ColorToCMYK}}"/>
				  </StackPanel>
			  </StackPanel>
			 </DataTemplate>
		  </ListBox.ItemTemplate>
	  </ListBox>	  
  </Grid>
</UserControl>

由于在代码中应用了这二行代码,导致xaml设计器报错。

<TextBlock Text="{Binding Color,Converter={x:Static vm:ColorsViewModel.ColorToHex}}"/>
<TextBlock Text="{Binding Color,Converter={x:Static vm:ColorsViewModel.ColorToCMYK}}"/>

如果不介意的话,可以忽略,不影响运行,介意就改成IValueConverter。
AboutView.axaml

<UserControl xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
			 xmlns:vm="using:AvaloniaMobileApp.ViewModels"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
			 x:DataType="vm:AboutViewModel"
             x:Class="AvaloniaMobileApp.Views.AboutView">
  <Grid RowDefinitions="Auto,Auto">
	  <StackPanel Orientation="Horizontal" Spacing="5" Grid.Row="0">
			<TextBlock x:Name="AppNameTextBlock" Text="{Binding AppName}" FontSize="18" FontWeight="Bold"/>
			<TextBlock x:Name="VersionTextBlock" FontSize="16" Text="{Binding Version}"/>
		</StackPanel>
		<TextBlock x:Name="MessageTextBlock" Grid.Row="1" TextWrapping="Wrap" Text="{Binding Message}"/>
  </Grid>
</UserControl>

Pallette.axaml

<UserControl xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
			 xmlns:vm="using:AvaloniaMobileApp.ViewModels"
			 x:DataType="vm:PalletteViewModel"
			 xmlns:conv="using:AvaloniaMobileApp.Converter"
             x:Class="AvaloniaMobileApp.Views.PalletteView">
	<Grid RowDefinitions="Auto,Auto,Auto">
		<TextBlock Text="{Binding ColorItem.ColorName,StringFormat='传过来的颜色名: {0}'}" Grid.Row="0"/>
		<Border Grid.Row="1" Background="White" Margin="5">
			<Rectangle HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Height="200">
				<Rectangle.Fill>
					<SolidColorBrush Color="{Binding ColorType}"/>
				</Rectangle.Fill>
			</Rectangle>
		</Border>
		<UniformGrid Columns="6" Grid.Row="2">
			<TextBlock Text="Red:" Grid.Column="0" HorizontalAlignment="Right"/>
			<TextBox Watermark="16进制值" Text="{Binding Red,Converter={x:Static conv:ByteToString.Instance},UpdateSourceTrigger=LostFocus}"  Grid.Column="1"/>
			<TextBlock Text="Green:" Grid.Column="2" HorizontalAlignment="Right"/>
			<TextBox Watermark="16进制值" Text="{Binding Green,Converter={x:Static conv:ByteToString.Instance},UpdateSourceTrigger=LostFocus}" Grid.Column="3"/>
			<TextBlock Text="Blue:" Grid.Column="4" HorizontalAlignment="Right"/>
			<TextBox Watermark="16进制值" Text="{Binding Blue,Converter={x:Static conv:ByteToString.Instance},UpdateSourceTrigger=LostFocus}" Grid.Column="5"/>
		</UniformGrid>
	</Grid>
</UserControl>

MainView.axaml

<UserControl xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:vm="clr-namespace:AvaloniaMobileApp.ViewModels"
             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
             x:Class="AvaloniaMobileApp.Views.MainView"
             x:DataType="vm:MainViewModel">
  <Design.DataContext>
    <!-- This only sets the DataContext for the previewer in an IDE,
         to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
    <vm:MainViewModel />
  </Design.DataContext>

	<Border x:Name="MainBorder">
		<Grid RowDefinitions="Auto,*,Auto">
			<Grid ColumnDefinitions="*,Auto" Grid.Row="0">
				<TextBlock Text="Colors View" x:Name="TitleTextBlock" Grid.Column="0"/>
				<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10">
					<Button Command="{Binding GotoHomeCommand}" Content="&lt;&#8211;" FontSize="18"/>
					<Button Command="{Binding GotoAboutCommand}" Content="About"/>
				</StackPanel>
			</Grid>			
			<TransitioningContentControl Grid.Row="1" Content="{Binding CurrentPage}">
				<TransitioningContentControl.PageTransition>
					<CrossFade Duration="0:0:0.500"/>
				</TransitioningContentControl.PageTransition>
			</TransitioningContentControl>			
		</Grid>
	</Border>
</UserControl>

MainWindow.axaml文件不用动。
由于不能用ReactiveUI,就得写附加属性,将命令绑定到事件。

using Avalonia;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Interactivity;
using System.Windows.Input;

namespace AvaloniaMobileApp.Behaviors
{
    public class LoadedBehavior : AvaloniaObject
    {
        static LoadedBehavior()
        {
            ExecuteCommandProperty.Changed.AddClassHandler<Interactive>(OnExecuteCommandChanged);
        }

        private static void OnExecuteCommandChanged(Interactive interactive, AvaloniaPropertyChangedEventArgs args)
        {
            if (args.NewValue is ICommand command)
            {
                interactive.AddHandler(Control.LoadedEvent, Handler);
            }
            else
            {
                interactive.RemoveHandler(Control.LoadedEvent, Handler);
            }
        }

        private static void Handler(object? sender, RoutedEventArgs e)
        {
            if (sender is Interactive interactive)
            {
                var command = interactive.GetValue(ExecuteCommandProperty);
                if (command?.CanExecute(null) == true)
                {
                    command.Execute(null);
                }
            }
        }

        public static readonly AttachedProperty<ICommand> ExecuteCommandProperty = AvaloniaProperty
            .RegisterAttached<LoadedBehavior, Interactive, ICommand>("ExecuteCommand", default, false, BindingMode.OneWay);

        public static ICommand GetExecuteCommand(AvaloniaObject obj)
        {
            return obj.GetValue(ExecuteCommandProperty);
        }

        public static void SetExecuteCommand(AvaloniaObject obj, ICommand value)
        {
            obj.SetValue(ExecuteCommandProperty, value);
        }
    }
}

在Converter文件夹下创建ByteToString.cs,将byte转换成string。

using Avalonia.Data;
using Avalonia.Data.Converters;
using System;
using System.Globalization;

namespace AvaloniaMobileApp.Converter
{
    public class ByteToString : IValueConverter
    {
        public static readonly ByteToString Instance = new();
        public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
        {
            if (value is byte b && targetType.IsAssignableTo(typeof(string)))
            {
                return b.ToString("X2");
            }

            return new BindingNotification(new InvalidCastException(), BindingErrorType.Error);
        }

        public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
        {
            if(value is string str && targetType.IsAssignableTo(typeof(byte)))
            {
                if(byte.TryParse(str,NumberStyles.HexNumber,CultureInfo.InvariantCulture,out var b))
                {
                    return b;
                }
                return byte.MinValue;
            }

            return new BindingNotification(new InvalidCastException(), BindingErrorType.Error);
        }
    }
}

在App.axaml中编写样式。

<Application xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="using:AvaloniaMobileApp"
             x:Class="AvaloniaMobileApp.App"
             RequestedThemeVariant="Default">
             <!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->

    <Application.DataTemplates>
        <local:ViewLocator/>
    </Application.DataTemplates>
	
	<Application.Resources>
		<SolidColorBrush x:Key="PrimaryBackground">#4A598F</SolidColorBrush>
		<SolidColorBrush x:Key="PrimaryForeground">#E8EFF7</SolidColorBrush>		
	</Application.Resources>

    <Application.Styles>
        <FluentTheme />
		<!--style-->
		<Style Selector="Border#MainBorder">
			<Setter Property="Background" Value="{StaticResource PrimaryBackground}"/>
		</Style>
		
		<Style Selector="Border">
			<Setter Property="Padding" Value="10"/>
		</Style>

		<Style Selector="TextBlock">
			<Setter Property="Foreground" Value="{StaticResource PrimaryForeground}"/>
			<Setter Property="Margin" Value="5"/>
			<Setter Property="VerticalAlignment" Value="Center"/>
		</Style>

		<Style Selector="ListBox">
			<Setter Property="Margin" Value="5"/>
		</Style>

		<Style Selector="ListBox TextBlock">
			<Setter Property="Foreground" Value="Black"/>
		</Style>

		<Style Selector="TextBlock#TitleTextBlock">
			<Setter Property="FontSize" Value="18"/>
			<Setter Property="FontWeight" Value="Bold"/>
		</Style>

		<Style Selector="Button">
			<Setter Property="Background" Value="Transparent"/>
			<Setter Property="VerticalAlignment" Value="Center"/>
			<Setter Property="Padding" Value="5"/>
		</Style>		

		<Style Selector="Button /template/ ContentPresenter">
			<Setter Property="Foreground" Value="{StaticResource PrimaryForeground}"/>
		</Style>

		<Style Selector="Rectangle">
			<Setter Property="Margin" Value="5"/>
		</Style>
    </Application.Styles>
</Application>

在App.axaml.cs文件中使用ioc。

using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core.Plugins;
using Avalonia.Markup.Xaml;
using AvaloniaMobileApp.ViewModels;
using AvaloniaMobileApp.Views;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;

namespace AvaloniaMobileApp;

public partial class App : Application
{
    public override void Initialize()
    {
        AvaloniaXamlLoader.Load(this);
    }

    public IServiceProvider? Services { get; private set; }

    public new static App Current => (App)Application.Current!;

    private IServiceProvider ConfigureServices()
    {
        var services = new ServiceCollection();
        services.AddTransient<AboutViewModel>();
        services.AddTransient<ColorsViewModel>();
        services.AddTransient<PalletteViewModel>();

        return services.BuildServiceProvider();
    }

    public override void OnFrameworkInitializationCompleted()
    {
        BindingPlugins.DataValidators.RemoveAt(0);

        Services = ConfigureServices();

        if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
        {
            // Avoid duplicate validations from both Avalonia and the CommunityToolkit. 
            // More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
            DisableAvaloniaDataAnnotationValidation();
            desktop.MainWindow = new MainWindow
            {
                DataContext = new MainViewModel()
            };
        }
        else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
        {
            singleViewPlatform.MainView = new MainView
            {
                DataContext = new MainViewModel()
            };
        }

        base.OnFrameworkInitializationCompleted();
    }

    private void DisableAvaloniaDataAnnotationValidation()
    {
        // Get an array of plugins to remove
        var dataValidationPluginsToRemove =
            BindingPlugins.DataValidators.OfType<DataAnnotationsValidationPlugin>().ToArray();

        // remove each entry found
        foreach (var plugin in dataValidationPluginsToRemove)
        {
            BindingPlugins.DataValidators.Remove(plugin);
        }
    }
}

将项目设为Android启动,存档,分发。
在我的荣耀手机Magic5 pro上成功运行。
效果展示。

Screenshot_20250925_211533_com_CompanyName_AvaloniaMobileApp_MainActivity

Screenshot_20250925_211547_com_CompanyName_AvaloniaMobileApp_MainActivity

Screenshot_20250925_211556_com_CompanyName_AvaloniaMobileApp_MainActivity

posted @ 2025-09-25 21:23  孤独的小苗  阅读(48)  评论(0)    收藏  举报