Edited at

AndroidのLayoutのxmlファイルからViewへのアクセスクラスを生成してみる

More than 1 year has passed since last update.

最低限で

    public class Control

{
public string Name { get; set; }
public string Id { get; set; }
}

internal class Program
{
private static void Main(string[] args)
{
var filepath = args[0];
var nameSpaceName = args[1];

var xml = File.ReadAllText(filepath);
var xdoc = XDocument.Parse(xml);

var reader = xdoc.CreateReader();
var controls = new List<Control>();
while (reader.Read())
{
var control = new Control();
control.Name = reader.Name;
if (reader.MoveToAttribute("android:id"))
{
control.Id = reader.Value.Split('/')[1];
controls.Add(control);
}
}

var sb = new StringBuilder();
var classname = Path.GetFileNameWithoutExtension(filepath) + "_holder";

sb.AppendLine(@"using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;"
);

sb.AppendLine($"public class {classname} : IDisposable");
sb.AppendLine("{");

foreach (var control in controls)
{
sb.Append("\t");
sb.Append($"public {control.Name} {control.Id} ");
sb.Append("{ get; }");
sb.AppendLine();
}
sb.AppendLine();

sb.Append("\t");
sb.Append($"public {classname}(View view)");
sb.AppendLine();

sb.AppendLine("\t{");

foreach (var control in controls)
{
sb.Append("\t");
sb.Append("\t");
sb.Append($"{control.Id} = view.FindViewById<{control.Name}>({nameSpaceName}.Resource.Id.{control.Id});");
sb.AppendLine();
}

sb.AppendLine("\t}");

sb.AppendLine($"\tpublic void Dispose()");
sb.AppendLine("\t{");

foreach (var control in controls)
{
sb.Append("\t");
sb.Append("\t");
sb.Append($"{control.Id}.Dispose();");
sb.AppendLine();
}
sb.AppendLine("\t}");

sb.AppendLine("}");

Console.Write(sb);

var file = File.Create($"{classname}.cs");

TextWriter text = new StreamWriter(file);
text.Write(sb.ToString());

text.Flush();
text.Dispose();

Console.Read();
}
}

こんなxmlがあったら


<?xml version="1.0" encoding="utf-8"?>

<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">

<LinearLayout
android:id="@+id/root_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<RelativeLayout
android:id="@+id/language_settings_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/clickable"
android:clickable="true"
android:padding="@dimen/space_16dp">

<TextView
android:id="@+id/txt_title"
style="@style/SettingTitleText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toLeftOf="@+id/txt_language"
android:layout_toStartOf="@+id/txt_language"
android:ellipsize="end"
android:lines="1"
android:text="@string/settings_language" />

<TextView
android:id="@+id/setting_description"
style="@style/SettingDescriptionText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/txt_title"
android:layout_marginTop="@dimen/space_8dp"
android:layout_toLeftOf="@+id/txt_language"
android:layout_toStartOf="@+id/txt_language"
android:text="@string/settings_language_description" />

<TextView
android:id="@+id/txt_language"
style="@style/SessionTag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginLeft="@dimen/space_16dp"
android:layout_marginStart="@dimen/space_16dp"
android:background="@drawable/tag_language"
android:padding="@dimen/space_8dp"
tools:text="@string/lang_en" />

</RelativeLayout>

<View
android:id="@+id/heads_up_border_top"
style="@style/Border" />

<DroidKaigi2017.Droid.Views.CustomViews.SettingSwitchRowView
android:id="@+id/local_time_switch_row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:settingDescription="@string/settings_local_time_description"
app:settingTitle="@string/settings_local_time" />

<View
android:id="@+id/heads_up_border_bottom"
style="@style/Border" />

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/space_16dp">

<TextView
style="@style/TextSubheading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/settings_notification_group"
android:textColor="?attr/colorPrimary" />

</RelativeLayout>

<DroidKaigi2017.Droid.Views.CustomViews.SettingSwitchRowView
android:id="@+id/notification_switch_row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:settingDescription="@string/settings_notification_description"
app:settingTitle="@string/settings_notification" />

<View
style="@style/Border" />

<DroidKaigi2017.Droid.Views.CustomViews.SettingSwitchRowView
android:id="@+id/heads_up_switch_row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:settingDescription="@string/settings_heads_up_notification_description"
app:settingTitle="@string/settings_heads_up_notification" />

<View
android:id="@+id/heads_up_border"
style="@style/Border" />

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/space_16dp">

<TextView
android:id="@+id/developer_menu_title"
style="@style/TextSubheading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/settings_developer_group"
android:textColor="?attr/colorPrimary" />

<TextView
android:id="@+id/developer_menu_tips"
style="@style/SettingDescriptionText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/developer_menu_title"
android:layout_marginTop="@dimen/space_8dp"
android:text="@string/settings_developer_tips" />
</RelativeLayout>

<DroidKaigi2017.Droid.Views.CustomViews.SettingSwitchRowView
android:id="@+id/debug_overlay_view_switch_row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:settingDescription="@string/settings_debug_overlay_view_description"
app:settingTitle="@string/settings_debug_overlay_view" />

<View style="@style/Border" />

<Switch
android:id="@+id/setting_switch"
android:checked="true"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

</LinearLayout>

</ScrollView>

こんな感じ

using System;

using System.Collections.Generic;
using System.Linq;
using System.Text;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
public class fragment_settings_holder : IDisposable
{
public LinearLayout root_view { get; }
public RelativeLayout language_settings_container { get; }
public TextView txt_title { get; }
public TextView setting_description { get; }
public TextView txt_language { get; }
public View heads_up_border_top { get; }
public DroidKaigi2017.Droid.Views.CustomViews.SettingSwitchRowView local_time_switch_row { get; }
public View heads_up_border_bottom { get; }
public DroidKaigi2017.Droid.Views.CustomViews.SettingSwitchRowView notification_switch_row { get; }
public DroidKaigi2017.Droid.Views.CustomViews.SettingSwitchRowView heads_up_switch_row { get; }
public View heads_up_border { get; }
public TextView developer_menu_title { get; }
public TextView developer_menu_tips { get; }
public DroidKaigi2017.Droid.Views.CustomViews.SettingSwitchRowView debug_overlay_view_switch_row { get; }
public Switch setting_switch { get; }

public fragment_settings_holder(View view)
{
root_view = view.FindViewById<LinearLayout>(DroidKaigi2017.Droid.Resource.Id.root_view);
language_settings_container = view.FindViewById<RelativeLayout>(DroidKaigi2017.Droid.Resource.Id.language_settings_container);
txt_title = view.FindViewById<TextView>(DroidKaigi2017.Droid.Resource.Id.txt_title);
setting_description = view.FindViewById<TextView>(DroidKaigi2017.Droid.Resource.Id.setting_description);
txt_language = view.FindViewById<TextView>(DroidKaigi2017.Droid.Resource.Id.txt_language);
heads_up_border_top = view.FindViewById<View>(DroidKaigi2017.Droid.Resource.Id.heads_up_border_top);
local_time_switch_row = view.FindViewById<DroidKaigi2017.Droid.Views.CustomViews.SettingSwitchRowView>(DroidKaigi2017.Droid.Resource.Id.local_time_switch_row);
heads_up_border_bottom = view.FindViewById<View>(DroidKaigi2017.Droid.Resource.Id.heads_up_border_bottom);
notification_switch_row = view.FindViewById<DroidKaigi2017.Droid.Views.CustomViews.SettingSwitchRowView>(DroidKaigi2017.Droid.Resource.Id.notification_switch_row);
heads_up_switch_row = view.FindViewById<DroidKaigi2017.Droid.Views.CustomViews.SettingSwitchRowView>(DroidKaigi2017.Droid.Resource.Id.heads_up_switch_row);
heads_up_border = view.FindViewById<View>(DroidKaigi2017.Droid.Resource.Id.heads_up_border);
developer_menu_title = view.FindViewById<TextView>(DroidKaigi2017.Droid.Resource.Id.developer_menu_title);
developer_menu_tips = view.FindViewById<TextView>(DroidKaigi2017.Droid.Resource.Id.developer_menu_tips);
debug_overlay_view_switch_row = view.FindViewById<DroidKaigi2017.Droid.Views.CustomViews.SettingSwitchRowView>(DroidKaigi2017.Droid.Resource.Id.debug_overlay_view_switch_row);
setting_switch = view.FindViewById<Switch>(DroidKaigi2017.Droid.Resource.Id.setting_switch);
}
public void Dispose()
{
root_view.Dispose();
language_settings_container.Dispose();
txt_title.Dispose();
setting_description.Dispose();
txt_language.Dispose();
heads_up_border_top.Dispose();
local_time_switch_row.Dispose();
heads_up_border_bottom.Dispose();
notification_switch_row.Dispose();
heads_up_switch_row.Dispose();
heads_up_border.Dispose();
developer_menu_title.Dispose();
developer_menu_tips.Dispose();
debug_overlay_view_switch_row.Dispose();
setting_switch.Dispose();
}
}

ソリューションに組み込めば少し幸せになれるかもしれない。

実際に組み込んだらこんな感じ ↓


using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Linq;

namespace Nyanto.ViewSupportTool
{
class Program
{
static Dictionary<string,string> replaceName = new Dictionary<string, string>()
{
{"android.support.constraint.ConstraintLayout", "android.support.constraints.ConstraintLayout" }
};
static void Main(string[] args)
{

var targetProjectPath = args[0];
var rootNamespace = args[1];
var resourcePath = Path.Combine(targetProjectPath, "Resources");
var layoutPath = Path.Combine(resourcePath, "layout");
var outputFilePath = Path.Combine(resourcePath, "Layout.Designer.cs");
var files = Directory.GetFiles(layoutPath);
var sb = new StringBuilder();
sb.AppendLine("using System;");
sb.AppendLine("using System.Collections.Generic;");
sb.AppendLine("using System.Linq;");
sb.AppendLine("using System.Text;");
sb.AppendLine("using Android.App;");
sb.AppendLine("using Android.Content;");
sb.AppendLine("using Android.OS;");
sb.AppendLine("using Android.Runtime;");
sb.AppendLine("using Android.Views;");
sb.AppendLine("using Android.Widget;");
sb.AppendLine();
sb.AppendLine($"namespace {rootNamespace}");
sb.AppendLine("{");
foreach (var file in files)
{
CreateClass(sb, file, rootNamespace);
}
sb.AppendLine("}");

Console.WriteLine(sb);
var writer = File.CreateText(outputFilePath);
writer.Write(sb.ToString());

writer.Flush();
writer.Dispose();

Console.Read();
}

static void CreateClass(StringBuilder sb, string filePath, string nameSpaceName)
{
var classname = Path.GetFileNameWithoutExtension(filePath) + "_holder";
var xml = File.ReadAllText(filePath);
var xdoc = XDocument.Parse(xml);
var reader = xdoc.CreateReader();
var controls = new List<Control>();
while (reader.Read())
{
var control = new Control();
var basename = reader.Name;
if (replaceName.ContainsKey(basename))
{
basename = replaceName[basename];
}
var fileName = string.Join(".", basename.Split('.').Select(x => FirstLetterToUpper(x)));

control.Name = fileName;

if (reader.MoveToAttribute("android:id"))
{
control.Id = reader.Value.Split('/')[1];
controls.Add(control);
}
}

sb.Append("\t");
sb.AppendLine($"public class {classname} : IDisposable");
sb.Append("\t");
sb.AppendLine("{");

foreach (var control in controls)
{
sb.Append("\t");
sb.Append("\t");
sb.Append($"public {control.Name} {control.Id} ");
sb.Append("{ get; }");
sb.AppendLine();
}
sb.AppendLine();

sb.Append("\t");
sb.Append("\t");
sb.Append($"public {classname}(View view)");
sb.AppendLine();

sb.Append("\t");
sb.AppendLine("\t{");

foreach (var control in controls)
{
sb.Append("\t");
sb.Append("\t");
sb.Append("\t");
sb.Append($"{control.Id} = view.FindViewById<{control.Name}>({nameSpaceName}.Resource.Id.{control.Id});");
sb.AppendLine();
}

sb.Append("\t");
sb.AppendLine("\t}");

sb.Append("\t");
sb.AppendLine($"\tpublic void Dispose()");
sb.Append("\t");
sb.AppendLine("\t{");

foreach (var control in controls)
{
sb.Append("\t");
sb.Append("\t");
sb.Append("\t");
sb.Append($"{control.Id}.Dispose();");
sb.AppendLine();
}
sb.Append("\t");
sb.AppendLine("\t}");

sb.Append("\t");
sb.AppendLine("}");
sb.AppendLine();

}

public static string FirstLetterToUpper(string str)
{
if (str == null)
return null;

if (str.Length > 1)
return char.ToUpper(str[0]) + str.Substring(1);

return str.ToUpper();
}

private class Control
{
public string Name { get; set; }
public string Id { get; set; }
}
}
}

こんな感じにしてtargetsスクリプトでごにょごにょすれば自動的に作成されるようになる。


var viewAccesor = new view_session_cell_holder(holder.ItemView);

viewAccesor.root.Clickable = vm.IsSelectable;

viewAccesor.root.SetBackgroundResource(vm.BackgroundResourceId);

viewAccesor.categoryBorder.Visibility = vm.IsNormalSession.ToViewStates();
viewAccesor.categoryBorder.SetBackgroundResource(vm.TopicColorResourceId);
viewAccesor.img_check.Visibility = vm.IsCheckVisible.Value.ToViewStates();
viewAccesor.root.Click += (sender, args) => { };
viewAccesor.root.LongClick += (sender, args) =>
{
var pos = base._recyclerView.GetChildAdapterPosition((View) sender);
var viewmodel = Get(pos);
viewmodel.CheckCommand.Execute(!viewmodel.IsCheckVisible.Value);
};
vm.IsCheckVisible
.Skip(1)
.ObserveOnUIDispatcher()
.Subscribe(x => viewAccesor.img_check.Visibility = x.ToViewStates());

viewAccesor.txt_time.Text = vm.ShortStartTime;
viewAccesor.txt_minutes.Text = vm.Minutes;
viewAccesor.txt_title.Text = vm.Title;
viewAccesor.txt_title.SetMaxLines(vm.TitleMaxLines);
viewAccesor.txt_language.Text = vm.LanguageId;
viewAccesor.txt_language.Visibility = vm.IsLanguageVisible.ToViewStates();
viewAccesor.txt_speaker_name.Text = vm.SpeakerName;
viewAccesor.txt_speaker_name.SetMaxLines(vm.SpeakerNameMaxLines);
viewAccesor.txt_speaker_name.Visibility = vm.IsNormalSession.ToViewStates();

こんな感じでかけて少し幸せ。