You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

求带详解的Xamarin Forms Expandable Tree View与Android Expandable List View实现方案

Hey there! Let's tackle your expandable tree view requirement in Xamarin.Forms, with a native Android ExpandableListView implementation that matches the look and feel you're targeting. I'll break this down into actionable steps with detailed explanations so you understand how each part works together.


1. Create a Reusable Expandable Node Model

First, we need a data model that represents each node in the tree. This model tracks expansion state, child nodes, and display content, with built-in UI update notifications.

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;

public class ExpandableNode : INotifyPropertyChanged
{
    private bool _isExpanded;
    private string _title;

    public string Title
    {
        get => _title;
        set
        {
            _title = value;
            OnPropertyChanged();
        }
    }

    public bool IsExpanded
    {
        get => _isExpanded;
        set
        {
            _isExpanded = value;
            OnPropertyChanged();
        }
    }

    public ObservableCollection<ExpandableNode> Children { get; set; }

    public ExpandableNode()
    {
        Children = new ObservableCollection<ExpandableNode>();
        IsExpanded = false; // Start collapsed by default
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

Explanation:

  • INotifyPropertyChanged ensures the UI reacts instantly when IsExpanded or Title changes.
  • ObservableCollection<ExpandableNode> auto-refreshes the UI when child nodes are added/removed.
  • IsExpanded controls whether the node's children are visible in the interface.

2. Xamarin.Forms Cross-Platform Tree View UI

For the shared UI, we'll build a custom TreeView control using ListView and a reusable ViewCell that handles expand/collapse logic.

2.1 The TreeView Control (Shared Project)

A wrapper around ListView to standardize tree view behavior:

using Xamarin.Forms;

public class TreeView : ListView
{
    public static readonly BindableProperty ItemsSourceProperty =
        BindableProperty.Create(nameof(ItemsSource), typeof(ObservableCollection<ExpandableNode>), typeof(TreeView));

    public ObservableCollection<ExpandableNode> ItemsSource
    {
        get => (ObservableCollection<ExpandableNode>)GetValue(ItemsSourceProperty);
        set => SetValue(ItemsSourceProperty, value);
    }

    public TreeView()
    {
        ItemTemplate = new DataTemplate(() => new TreeViewCell());
        SeparatorVisibility = SeparatorVisibility.None;
        CachingStrategy = ListViewCachingStrategy.RecycleElement; // Optimize for large datasets
    }
}

2.2 The TreeViewCell (Custom ViewCell)

This cell displays the parent node, an expand/collapse icon, and nested child nodes:

<ViewCell xmlns="http://xamarin.com/schemas/2014/forms"
          xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
          x:Class="YourNamespace.TreeViewCell">
    <StackLayout Spacing="0">
        <!-- Parent Node Header -->
        <Grid x:Name="HeaderGrid" Padding="10,8" BackgroundColor="#F5F5F5">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            <!-- Expand/Collapse Icon -->
            <Image Source="{Binding IsExpanded, Converter={StaticResource BoolToIconConverter}}"
                   WidthRequest="20" HeightRequest="20"
                   VerticalOptions="Center" />
            <!-- Node Title -->
            <Label Grid.Column="1" 
                   Text="{Binding Title}"
                   FontSize="16"
                   VerticalOptions="Center" />
        </Grid>
        <!-- Child Nodes (Hidden by default) -->
        <ListView ItemsSource="{Binding Children}"
                  ItemTemplate="{StaticResource TreeViewCellTemplate}"
                  IsVisible="{Binding IsExpanded}"
                  SeparatorVisibility="None"
                  Margin="20,0,0,0"
                  CachingStrategy="RecycleElement" />
    </StackLayout>
</ViewCell>

2.3 BoolToIconConverter

Swaps icons based on the node's expansion state:

using Xamarin.Forms;

public class BoolToIconConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return (bool)value ? "ic_collapse.png" : "ic_expand.png";
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

2.4 Handle Expand/Collapse Taps

Add a tap gesture to the header to toggle the node's state:

public partial class TreeViewCell : ViewCell
{
    public TreeViewCell()
    {
        InitializeComponent();
        var tapGesture = new TapGestureRecognizer();
        tapGesture.Tapped += (s, e) =>
        {
            if (BindingContext is ExpandableNode node)
            {
                node.IsExpanded = !node.IsExpanded;
            }
        };
        HeaderGrid.GestureRecognizers.Add(tapGesture);
    }
}

Explanation:

  • The nested child ListView only appears when IsExpanded is true.
  • The tap gesture triggers a state change, which updates the UI via INotifyPropertyChanged.
  • The converter provides visual feedback for expansion state.

3. Android Native ExpandableListView Implementation

To get the smooth, native Android ExpandableListView experience, we'll build a custom renderer that replaces the cross-platform TreeView with Android's native control.

3.1 Marker Control (Shared Project)

A placeholder control to trigger Android-specific rendering:

public class NativeTreeView : TreeView
{
    // Marker class for Android custom rendering
}

3.2 Android Custom Renderer

Maps the NativeTreeView to Android's ExpandableListView:

using Android.Content;
using Android.Widget;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using YourNamespace;
using YourNamespace.Droid;

[assembly: ExportRenderer(typeof(NativeTreeView), typeof(NativeTreeViewRenderer))]
namespace YourNamespace.Droid
{
    public class NativeTreeViewRenderer : ViewRenderer<NativeTreeView, ExpandableListView>
    {
        private ExpandableListAdapter _adapter;

        public NativeTreeViewRenderer(Context context) : base(context)
        {
        }

        protected override void OnElementChanged(ElementChangedEventArgs<NativeTreeView> e)
        {
            base.OnElementChanged(e);

            if (Control == null)
            {
                var expandableListView = new ExpandableListView(Context);
                SetNativeControl(expandableListView);
            }

            if (e.NewElement != null)
            {
                _adapter = new ExpandableListAdapter(Context, e.NewElement.ItemsSource);
                Control.SetAdapter(_adapter);

                // Sync expansion state between model and native control
                Control.GroupCollapse += (s, args) =>
                {
                    var node = e.NewElement.ItemsSource[args.GroupPosition];
                    node.IsExpanded = false;
                };

                Control.GroupExpand += (s, args) =>
                {
                    var node = e.NewElement.ItemsSource[args.GroupPosition];
                    node.IsExpanded = true;
                };
            }
        }
    }
}

3.3 ExpandableListAdapter (Android)

Converts our cross-platform ExpandableNode model to Android's group/child structure:

using Android.Content;
using Android.Views;
using Android.Widget;
using System.Collections.ObjectModel;

namespace YourNamespace.Droid
{
    public class ExpandableListAdapter : BaseExpandableListAdapter
    {
        private readonly Context _context;
        private readonly ObservableCollection<ExpandableNode> _groupNodes;

        public ExpandableListAdapter(Context context, ObservableCollection<ExpandableNode> groupNodes)
        {
            _context = context;
            _groupNodes = groupNodes;
        }

        public override Java.Lang.Object GetChild(int groupPosition, int childPosition)
        {
            return _groupNodes[groupPosition].Children[childPosition].Title;
        }

        public override long GetChildId(int groupPosition, int childPosition)
        {
            return childPosition;
        }

        public override View GetChildView(int groupPosition, int childPosition, bool isLastChild, View convertView, ViewGroup parent)
        {
            if (convertView == null)
            {
                var inflater = (LayoutInflater)_context.GetSystemService(Context.LayoutInflaterService);
                convertView = inflater.Inflate(Resource.Layout.child_item, parent, false);
            }

            var childTitle = convertView.FindViewById<TextView>(Resource.Id.child_title);
            childTitle.Text = _groupNodes[groupPosition].Children[childPosition].Title;

            return convertView;
        }

        public override int GetChildrenCount(int groupPosition)
        {
            return _groupNodes[groupPosition].Children.Count;
        }

        public override Java.Lang.Object GetGroup(int groupPosition)
        {
            return _groupNodes[groupPosition].Title;
        }

        public override long GetGroupId(int groupPosition)
        {
            return groupPosition;
        }

        public override View GetGroupView(int groupPosition, bool isExpanded, View convertView, ViewGroup parent)
        {
            if (convertView == null)
            {
                var inflater = (LayoutInflater)_context.GetSystemService(Context.LayoutInflaterService);
                convertView = inflater.Inflate(Resource.Layout.group_item, parent, false);
            }

            var groupTitle = convertView.FindViewById<TextView>(Resource.Id.group_title);
            var expandIcon = convertView.FindViewById<ImageView>(Resource.Id.expand_icon);

            groupTitle.Text = _groupNodes[groupPosition].Title;
            expandIcon.SetImageResource(isExpanded ? Resource.Drawable.ic_collapse : Resource.Drawable.ic_expand);

            return convertView;
        }

        public override int GroupCount => _groupNodes.Count;

        public override bool HasStableIds => true;

        public override bool IsChildSelectable(int groupPosition, int childPosition)
        {
            return true; // Allow child node selection
        }
    }
}

3.4 Android Layout Files

Create two layouts in your Android project's Resources/Layout folder:
group_item.axml (parent node layout):

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp"
    android:orientation="horizontal"
    android:gravity="center_vertical">

    <ImageView
        android:id="@+id/expand_icon"
        android:layout_width="24dp"
        android:layout_height="24dp"
        android:layout_marginRight="12dp"/>

    <TextView
        android:id="@+id/group_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="18sp"
        android:textColor="#333333"/>
</LinearLayout>

child_item.axml (child node layout):

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp"
    android:paddingLeft="40dp"
    android:orientation="horizontal">

    <TextView
        android:id="@+id/child_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="16sp"
        android:textColor="#666666"/>
</LinearLayout>

Explanation:

  • The custom renderer replaces the cross-platform TreeView with Android's native control, which provides smooth expand/collapse animations and native touch behavior.
  • The adapter bridges our cross-platform model to Android's group/child view system.
  • The layouts define the native look and feel for parent and child nodes.

4. Best Practices & Optimizations

  • Performance: Use RecycleElement caching strategy in Xamarin.Forms ListViews to reduce memory usage for large datasets.
  • Data Updates: Always use ObservableCollection for child nodes to ensure automatic UI refreshes.
  • Memory Management: In the Android adapter, avoid holding strong references to the context (use WeakReference if needed) to prevent memory leaks.
  • Platform Consistency: For iOS, you can either use the cross-platform TreeView or build a custom renderer using UITableView with expandable sections.

内容的提问来源于stack exchange,提问作者HarshShah

火山引擎 最新活动