Un piccolo serpente ci conduce attraverso le principali caratteristiche del Windows Phone 7
Introduzione Core UI & Design Conclusione Premi Discussioni
Marcosroom.it Didatticando

Selettore circolare

Circular selector screen shot

E quindi, questo è uno screenshot del nostro controllo; fondamentalmente, è composto da un certo numero di elementi (settori circolari), e ognuno di loro è personalizzabile  cambiando intestazione, colore e visibilità. Comunque, questa è la lista precisa di ogni proprietà interessante del controllo (le proprietà sono tutte dependency property, di modo tale che sia possbile applicare il binding):

  Nome Tipo Descrizione
Property SweepDirection SweepDirection

Uno dei valori dell'enum System.Windows.Media.SweepDirection che indica il senso di rotazione (orario o antiorario)

Property LinesBrush Brush

Brush utilizzato per disegnare il bordo di ogni elemento

Property AlwaysDrawLines bool

Restituisce o imposta un valore che indica se il bordo di ogni elemento deve essere disegnato anche se non è visibile

Property SelectedItemPushOut double

Restituisce o imposta un valore che indica di quanti pixel l'elemento deve allontanarsi dal centro quando selezionato

Property FontFamily FontFamily

Carattere utilizzato per le intestazioni

Property FontSize double

Dimensione del carattere delle intestazioni

Property FontStyle FontStyle

Stile del carattere delle intestazioni

Property FontWeight FontWeight

Spessore del carattere per le intestazioni

Property Foreground Brush

Brush utilizzato per disegnare le intestazioni

Property ItemsSource CircularSelectorItem[]

Array contenente tutti gli elementi

Property SelectedItem CircularSelectorItem

Restituisce o imposta l'elemento selezionato

Ogni elemento è un CircularSelectorItem; questo oggetto ha le seguenti proprietà (anche queste sono dependency property):

  Property name Property type Description
Property Color Color

Restituisce o imposta il colore dell'elemento

Property IsVisible bool

Restituisce o imposta la visibilità dell'elemento

Property Header string

Restituisce o imposta l'intestazione dell'elemento

Property Tag object

Restituisce o imposta un oggetto contenente unteriori informazioni

Bene, ora la nostra piccola panoramica è finita, e possiamo finalmente iniziare a parlare di codice! Il CircularSelectorItem è noiso: è solo una semplica classe con qualche proprietà! Quindi, andremo direttamente al CircularSelector. Da dove iniziare? Forse dal diagramma? Si, penso proprio di si!

Circular selector

Come puoi vedere, ci sono molte proprietà e molti metodi: il loro significato è già stato spiegato, ma ora scaveremo più a fondo nel metodo UpdatePaths. Questo è il metodo che aggiorna tutti i settori circolari del controllo; viene chiamato quando il controllo cambia dimensioni, la ItemsSource e la SweepDirection. Il codice è abbastanza complesso, quindi, lo divideremo in tanti piccoli pezzi:

  1. Prima cosa da fare: pulire tutti gli oggetti già presenti.
    ClearPaths();
    La funzione ClearPaths esegue solo una chiamata a base.Children.Clear() (ricorda che CircularSelector eredita da Panel)
  2. Controlliamo che sia disponibile almeno un elemento:
    if (this.ItemsSource == null || this.ItemsSource.Count() == 0)
        return;
  3. Ora calcoliamo e memorizziamo qualche informazione utile, come la misura dell'angolo di ogni settore, lo spazio disponibile e il raggio del cerchio
    //Calcola l'angolo per ogni elemento
    double angle = Math.PI * 2 / this.ItemsSource.Count();
                
    //Calcola le dimensioni del selettore
    double size = Math.Min(this.ActualWidth, this.ActualHeight);
    if (double.IsNaN(size) || size == 0)
        return;
    double radius = size / 2;
  4. Qui le cose si fanno un po' più complicate:
    //Trova i punti
    double cumulativeAngle = 0;
    List<Point> pointList = new List<Point>();
    angles = new Dictionary<int, double>();
    for (int i = 0; i < this.ItemsSource.Count(); i++) {
        pointList.Add(PolarToCartesian(radius, cumulativeAngle)
                      .Multiply((this.SweepDirection == SweepDirection.Counterclockwise ? 1 : -1), -1)
                      .Offset(radius, radius));
        angles.Add(i, cumulativeAngle);
        cumulativeAngle += angle;
    }
    Qui troviamo i punti della circonferenza che useremo successivamente per disegnare gli archi. Forse un disegno potrebbe aiutare... In poche parole, stiamo cercando i punti rossi:

    Circular selector - fig 0

    Un'altra cosa strana, è la prima riga all'interno del blocco for: ci sono due metodi di estensione che ho creato per facilitare la manipolazione dei punti. Ecco il codice:
    public static Point Offset(this Point p, double x, double y) { p.X += x; p.Y += y; return p; }
    public static Point Multiply(this Point p, double x, double y) { p.X *= x; p.Y *= y; return p; }
    Il codice è molto semplice e non ha bisogno di spiegazioni, ma la cosa interessante è perchè ho fatto così? Iniziamo dalle origni: nel nostro controllo l'origine è il centro del contenitore e l'asse delle Y è diretto verso il basso. Ma ciò che realmente otteniamo utilizzando il sistema di coordinate polari è leggermente differente: innanzitutto, l'origine del sistema è l'angolo in alto a sinistra del contenitore e, poi, l'asse delle Y è rivolto verso l'alto. Quindi, dobbiamo applicare un po' di trasformazioni ai nostri punti, in particolare:

    Circular selector - fig 1

    Quindi, la prima immagine è la situazione iniziale (descritta precedentemente); la seconda ci dice di invertire l'asse delle Y; l'ultima, invece, è lo spostamento dell'origine. Tradtto in codice, ciò significa che:
    1. Dobbiamo convertire la nostre coordinate polari in cartesiane
    2. Dobbiamo moltiplicare il valore delle coordinata Y per -1
    3. Se la direzione è oraria, dobbiamo moltiplicare anche la coordinata X per -1
    4. Infine, dobbiamo spostare il punto per centrarlo nel contenitore
    Un'altra cosa strana, è la seconda riga all'interno del blocco for: qui memorizziamo il nostro angolo in un dizionario utilizzando come chiave l'indice del settore. Potrebbe sembrare inutile ora, ma successivamente vedremo che non è così.
  5. //Crea le path
    for (int i = 0; i < this.ItemsSource.Count(); i++ ) {
    
        //Elemento corrente
        CircularSelectorItem item = this.ItemsSource.ElementAt(i);
    
        //Salta l'elemento se non è visibile
        if (this.AlwaysDrawLines == false && item.IsVisible == false)
            continue;
    
        //Crea una nuova path e una nuova geometry
        Path path = new Path() { 
            Stroke = this.LinesBrush,
            StrokeThickness = 1,
            Fill = 
                (item.IsVisible ? 
                CreateBackgroundBrush(item.Color, i) : 
                new SolidColorBrush(Colors.Transparent)) 
        };
        PathGeometry geom = new PathGeometry();
        PathFigure fig = new PathFigure();
        geom.Figures.Add(fig);
        path.Data = geom;
    
        //Disegna il settore
        fig.StartPoint = new Point(radius, radius);
        Point p1 = pointList[i];
        Point p2 = pointList[(i + 1 == pointList.Count ? 0 : i + 1)];
        fig.Segments.Add(new LineSegment() { Point = p1 });
        fig.Segments.Add(new ArcSegment() { 
            Point = p2, 
            Size = new Size(radius, radius), 
            IsLargeArc = angle > Math.PI, 
            SweepDirection = this.SweepDirection, RotationAngle = 0 
        });
        fig.Segments.Add(new LineSegment() { Point = fig.StartPoint });
        base.Children.Add(path);
        path.Tag = new object[] { item, null };
        if (item.IsVisible) path.MouseLeftButtonUp += Path_MouseLeftButtonUp;
    
        //Crea il contenuto dell'elemento
        if (item.IsVisible) {
            //Crea la textblock
            TextBlock txt = new TextBlock() { 
                Text = item.Header, 
                Foreground = this.Foreground,
                FontFamily = this.FontFamily, 
                FontSize = this.FontSize, 
                FontStyle = this.FontStyle,
                FontWeight = this.FontWeight 
            };
            Point middlePoint = new Point((p1.X + p2.X) / 2, (p1.Y + p2.Y) / 2);
            txt.Margin = new Thickness(
                middlePoint.X - txt.ActualWidth / 2, 
                middlePoint.Y - txt.ActualHeight / 2, 
                0, 
                0
            );
            base.Children.Add(txt);
            txt.MouseLeftButtonUp += Path_MouseLeftButtonUp;
            //Imposta la tag
            ((object[])path.Tag)[1] = txt;
            txt.Tag = path.Tag;
        }
    
    }
    Questa potrebbe essere la parte più complessa del metodo. Le prime righe sono semplici: scorriamo tutti gli elementi della ItemsSource e controlliamo se dobbiamo disegnarle. Da qui in poi, è un po' più complesso. Prima, creiamo un nuovo oggetto Path e ne impostiamo le proprietà (CreateBackgroundBrush è una semplice funzione che trasforma un Color in un Brush); poi, creiamo una nuova path PathGeometry con una nuova PathFigure. Il blocco successivo disegna il settore circolare:

    Circular selector - fig 2

    Il punto iniziale è il centro del controllo. Da quel punto, disegnamo una linea fino al punto di inizio dell'arco (li avevamo trovati tutti prima, nel blocco di codice precedente). Poi, disegnamo un arco dal punto iniziale al punto finale, e, finalmente, possiamo chiudere la nostra path aggiungendo l'ultima linea dal punto finale dell'arco al punto iniziale. Per comprendere a pieno la logica dell'oggeto ArcSegment, ti consiglio di leggere questo eccellente post chiamato "The Mathematics of ArcSegment".

    Se l'elemento è visibile, aggiungiamo un handler per il suo evento MouseLeftButtonUp; nell'handler cambiamo solo la proprietà SelectedItem impostandola all'elemento cliccato. Solo se l'elemento è visibile, creiamo una nuova TextBlock per visualizzare l'intestazione del settore; poi aggiungiamo lo stesso handler per l'evento MouseLeftButtonUp.

    Se hai notato, impostiamo la stessa proprietà Tag dei due oggetti ad un array di oggetti contenente l'istanza del CircularSelectorItem rappresentato dal settore, e la TextBlock utilizzata per l'intestazione. Scoprirai dopo perchè ho fatto così, quando parleremo della proprietà SelectedItem.

Abbiamo finalmente terminato il metodo UpdatePaths, e possiamo immadiatamente iniziare ad analizzare un altro punto focale del controllo:
la proprietà SelectedItem.

public CircularSelectorItem SelectedItem
{
    get { ... }
    set { 
        
        //Ottiene il valore corrente
        CircularSelectorItem current = SelectedItem;

        //Controlla che sia cambiato
        if (value != current && base.Children.Count > 0) {

            //Controlla che l'ItemsSource contenga il nuovo elemento
            if (ItemsSource == null || ItemsSource.Contains(value) == false)
                throw new ArgumentException("Item not in the ItemsSource");

            //Variabili temporanee
            object[] obj = new object[] { };
            Path path = null;
            int i = 0;

            //Anima l'elemento corrente
            if (current != null) {
                path = base.Children.OfType<Path>().First(x => ((object[])x.Tag)[0] == current);
                obj = (object[])path.Tag;
                i = this.ItemsSource.IndexOf((CircularSelectorItem)obj[0]);
                AnimatePath(path, false, i);
                if (obj[1] != null) AnimatePath((FrameworkElement)obj[1], false, i);
            }

            //Anima il nuovo valore
            path = base.Children.OfType<Path>().First(x => ((object[])x.Tag)[0] == value);
            obj = (object[])path.Tag;
            i = this.ItemsSource.IndexOf((CircularSelectorItem)obj[0]);
            AnimatePath(path, true, i);
            if (obj[1] != null) AnimatePath((FrameworkElement)obj[1], true, i);

            //Memorizza il nuovo valore
            SetValue(SelectedItemProperty, value);

            //Genera l'evento
            OnSelectionChanged(new SelectionChangedEventArgs(
                new List<CircularSelectorItem>(new CircularSelectorItem[] { current }),
                new List<CircularSelectorItem>(new CircularSelectorItem[] { value })
            ));

        }

    }

}

Una volta che abbiamo memorizzato in una variabile l'elemento attuale, controlliamo se il valore precedente è diverso da quello nuovo e che ci sia almeno una path già disegnata. Se è così, controlliamo che ItemsSource contenga il nuovo elemento, e solleviamo un'eccezione se non è così. Dopo tutti questi controlli, animiamo la path correntemente selezionata e quella nuova. Per trovare l'oggetto Path che rappresenta il CircularSelectorItem, cerchiamo una Path la cui proprietà Tag contiene quell'elemento. Infine, memorizziamo il nuovo valore e generiamo l'evento.

Una caratteristica carina del CircularSelector è che gli elementi vengono animati quando selezionati o deselezionati. Ciò è dato dal metodo AnimatePath:

private void AnimatePath(FrameworkElement path, bool isSelected, int i) {
            
    //Controlla che gli angoli siano già stati calcolati
    if (angles == null || angles.Count == 0)
        return;

    //Crea una storyboard
    Storyboard story = new Storyboard();
            
    //Crea una TranslateTransform
    TranslateTransform transform = new TranslateTransform();
    path.RenderTransform = transform;
            
    //Calcola il punto di arrivo
    double radius = Math.Min(this.ActualWidth, this.ActualHeight) / 2;
    double middleAngle = angles[i] + Math.PI * 2 / this.ItemsSource.Count() / 2;
    Point endPoint = 
        PolarToCartesian(this.SelectedItemPushOut, middleAngle)
        .Multiply((this.SweepDirection == SweepDirection.Counterclockwise ? 1 : -1), -1)
        .Offset(radius, radius);
    Point difference = endPoint.Offset(-radius, -radius);

    //Crea l'animazione
    DoubleAnimation animX = new DoubleAnimation() { 
        From = (isSelected ? 0 : difference.X), 
        To = (isSelected ? difference.X : 0), 
        Duration = TimeSpan.FromMilliseconds(200) 
    };
    Storyboard.SetTarget(animX, transform);
    Storyboard.SetTargetProperty(animX, new PropertyPath("X"));
    DoubleAnimation animY = new DoubleAnimation() { 
        From = (isSelected ? 0 : difference.Y), 
        To = (isSelected ? difference.Y : 0), 
        Duration = TimeSpan.FromMilliseconds(200) 
    };
    Storyboard.SetTarget(animY, transform);
    Storyboard.SetTargetProperty(animY, new PropertyPath("Y"));

    //Avvia l'animazione
    story.Children.Add(animX);
    story.Children.Add(animY);
    story.Begin();

}

Questo metodo è molto semplice: innanzitutto, creiamo una nuova TranslateTransform e la aggiungiamo alla path, poi calcoliamo il punto finale (punto di destinazione) e, infine, facciamo la differenza con il centro per calcolare la diferenza lungo l'asse X e l'asse Y.
Dopodichè, impostiamo le animazioni e iniziamo la storyboard.

Circular selector - fig 3

 

Condividi
Indietro Tutti i webmaster che volessero segnalare, non copiare,
il contenuto di questa pagina sul proprio sito, possono farlo liberamente.
E' gradito un preavviso tramite mail all'autore e l'iserimento,
nella pagina di citazione, di un link verso la pagina corrente.
© Copyright    Marco's Room
Avanti
Download SnakeMobile

Scaricato 115 volte

Navigazione Transizioni Pulsante rotondo luminoso Trigger Un piccolo ripasso di matematica Selettore circolare Isolated storage: file e impostazioni Impostazioni Build action: Content o Resource? Audio & XNA