65.9K
CodeProject 正在变化。 阅读更多。
Home

动画自动更正

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (10投票s)

2014年12月27日

CPOL

9分钟阅读

viewsIcon

24126

downloadIcon

68

自动纠错正在毁掉我们的拼写,解决方案是动画化修改。

Animated Auto-correct

问题

自动纠错正在毁掉我们的拼写

解决方案

动画化修改

引言

英语不是我的母语,所以每次我试图将我写错的单词与正确的单词进行逐个字符的匹配,找出哪里错了时,我都会在拼写上挣扎。

通过动画化修改,只需观看动画即可知道添加了什么字符,删除了什么字符。仅仅通过观看动画的运行,你就能学会你的错误。

想象一下,任何设备上的任何文本插入控件都运行这个动画,你将再也不会错过任何拼写错误。

我希望微软Office团队中的某个人能读到我的这个技巧,并在Word中添加这个功能,如果那样的话,我将非常高兴。 :)

在我们看看我是如何实现之前

让我们做一个演示

在这个演示中,我将为你提供自己完成这个项目所需的所有技术知识,它非常简单,所以让我们做一个概念验证,让一个字符上下移动,然后我们来玩一下。  

1. 首先,让我们创建我们的 TextBlock

<TextBlock x:Name="MyTextBlock" Text="Some Text" Margin="20" FontSize="25"></TextBlock>

2. 然后,我们需要在 MyTextBlock.TextEffects 中设置一个 TextEffectCollection 类型的新对象。这个集合将包含每个字符的 TextEffect 对象,这样我们就可以单独操作每个字符。  

MyTextBlock.TextEffects = new TextEffectCollection();

然后,我们需要遍历字符并为它们每个设置 TextEffect 对象,但是等等,你将如何构造这个对象以及如何使用它?首先,这个类(TextEffect)有一个名为 Transform 的属性,我们将在此上添加一个新的 TransformGroup。但是这个 TransformGroup 应该包含我们打算应用于每个字符的所有变换对象。对我来说,这里有 TranslateTransform 和 ScaleTransform。我们稍后将需要这些对象来在动画中使用它们。

TranslateTransform 用于将字符从其原始位置移动到其他地方,我们将使用 X 和 Y 这两个属性来完成此操作。

ScaleTransform 用于改变我们字符的大小。1 是其正常大小,你可以通过 ScaleX 和 ScaleY 属性来拉伸或缩小它。

            for (var i = 0; i < MyTextBlock.Text.Count(); i++)
            {
                var transGrp = new TransformGroup();
                transGrp.Children.Add(new TranslateTransform());
                transGrp.Children.Add(new ScaleTransform());
                MyTextBlock.TextEffects.Add(new TextEffect
                {
                    PositionStart = i,
                    PositionCount = 1,
                    Transform = transGrp,
                });
            }

现在,我们构造 TextBlock 控件。我们可以开始玩弄每个字符并对其进行动画处理。

我们将从 Storyboard 开始,我们使用这个类将我们所有的动画添加到其中,就像真实故事中的动作一样,它会负责为我们播放动画。例如,你可以说将这个矩形移动到那里,然后等待 3 秒,然后让这个圆变大 2 倍。当你调用 Storyboard 中的 Begin 函数时,它将开始播放你的动画。

            var storyboard = new Storyboard();

然后,让我们创建我们的动画,我们不应忘记为我们的动画指定目标控件才能使其正常工作。

            var animation = new DoubleAnimation();

            animation.SetValue(Storyboard.TargetNameProperty, MyTextBlock.Name);

然后,让我们对我们的动画做些事情。我们之前创建了 TranslateTransform 和 ScaleTransform,我们将使用它们将我们的字符从其原始位置偏移 40 个点。我们将为此使用 To 属性,这将花费 1 秒钟完成。我们将为此使用 Duration 属性。此外,动画应立即开始。我们将使用 BeginTime 属性。最后,它不应该反转。如果我们将其设置为 True,字符将从其原始位置移动然后返回,这样动画将花费两倍的时间。让我们指定我们刚刚进行的动画将影响目标控件中的哪个属性。   

            animation.To = 40;
            animation.Duration = TimeSpan.FromSeconds(1);
            animation.BeginTime = TimeSpan.FromSeconds(1);
            animation.AutoReverse = false;
            var charIndex = 0;
            Storyboard.SetTargetProperty(animation, new PropertyPath(
            String.Format("TextEffects[{0}].Transform.Children[0].Y", charIndex)));

最后,让我们将此动画添加到 storyboard 并运行它。

            storyboard.Children.Add(animation);
            storyboard.Begin(this);

通过这样做,我们将开始看到我们的字符向下移动并停在那里,这是因为 (0, 0) 在左上角,我们告诉动画将其移动到 (0, 40),也就是 Y 轴的下方。

现在,让我们更改一些内容,看看它对我们的字符有什么影响。如果我们将其缩放到 2,但仅在移动结束后。

创建动画

            var doubleAnimation2 = new DoubleAnimation();
            doubleAnimation2.SetValue(Storyboard.TargetNameProperty, MyTextBlock.Name);

更改属性以适应我们的需求。我们需要缩放发生在移动之后,所以我们必须将 begin time 设置为 2 秒,一个用于移动动画的开始,一个用于第一个动画的持续时间,然后这个将开始。

            var animation2 = doubleAnimation2;
            animation2.To = 2;
            animation2.Duration = TimeSpan.FromSeconds(1);
            animation2.BeginTime = TimeSpan.FromSeconds(2);
            animation2.AutoReverse = false;
            Storyboard.SetTargetProperty(animation2, new PropertyPath(
            String.Format("TextEffects[{0}].Transform.Children[1].ScaleX", charIndex)));

最后,将其添加到 storyboard 并观看魔法开始。

            storyboard.Children.Add(animation);
            storyboard.Begin(this);

但是,如果我们想玩弄另一个字符,我们只需要更改 charIndex 变量,动画将应用于该索引处的字符。

让我们尝试更改字符的颜色。首先,我们将从动画开始,一如既往地设置目标控件。但这次,我们不会将动画放入 storyboard,而是在 TextEffect 中有一个单独的属性可以放入我们的颜色动画。所以我们需要从正确的索引获取正确的 TextEffect,然后将其放入颜色动画中。

            var colorAnimation = new ColorAnimation
            {
                To = Colors.Green,
                Duration = TimeSpan.FromSeconds(0),
            };
            colorAnimation.SetValue(Storyboard.TargetNameProperty, MyTextBlock.Name);
            var solidColorBrush = new SolidColorBrush();
            solidColorBrush.BeginAnimation(SolidColorBrush.ColorProperty,colorAnimation);
            MyTextBlock.TextEffects[charIndex].Foreground = solidColorBrush;

Voilà!字符颜色已更改。  

我们了解了如何移动字符,如何控制其缩放比例以及如何更改其颜色。

通过动画(storyboards)进行纠正

我们需要统一一种描述单词中发生的变化的方式。换句话说,如果我们有一个拼写错误的单词“requir”,我们该如何纠正它?在末尾插入“e”,对吧?这就是我们应该通过动画来做的事情。但是除了忘记一个字符之外,我们还可以犯哪些其他的拼写错误?我们可能会添加一个不必要的字符,我们可能会交换两个字符,或者我们可能会用一个字符代替另一个字符。从这四类错误中,我们创建了我们的四个动画方法,它们通过动画来纠正它们。

每个动画方法都包含影响特定字符的步骤(DoubleAnimation)。所以,如果我们需要在多个字符上执行此操作,我们就需要多次调用它,但使用不同的参数。   

Insert

在我们开始方法之前,我们需要查看辅助方法 GetDoubleAnimation、MoveVertically、Scale、MoveHorizontally、SetColor。

这将创建一个我们的动画实例并指定目标控件。

public DoubleAnimation GetDoubleAnimation()
        {
            var doubleAnimation = new DoubleAnimation();
            doubleAnimation.SetValue(Storyboard.TargetNameProperty,mohamedAhmed.Name);
            return doubleAnimation;
        }

所有辅助方法只是为了隐藏动画的创建和动画参数的内部细节。这将使故事板更易读。

public DoubleAnimation MoveVertically(double to, Duration duration, TimeSpan? timeSpan, int index, bool autoReverse =false)
        {
            var animation = GetDoubleAnimation();
            animation.To = to;
            animation.Duration = duration;
            if (timeSpan != null)
                animation.BeginTime = timeSpan;
            animation.AutoReverse = autoReverse;
            Storyboard.SetTargetProperty(animation, new PropertyPath(
            String.Format("TextEffects[{0}].Transform.Children[0].Y", index)));
            return animation;
        }

每个故事函数都以正确的顺序创建动画来描述错误纠正。换句话说,insert 函数会在用户忘记时插入一个新字符。新字符从单词上方凭空出现,然后向下移动直到到达中间。首先将字符移到上方,通过将其缩放到 0 使其消失。这两步应在瞬间完成。然后颜色应为绿色,并且移动应开始,直到到达原始位置 0。

private TimeSpan Insert(int index, Storyboard storyboard,TimeSpan timeLine,TimeSpan duration)
        {
            timeLine += duration;
            storyboard.Children.Add(MoveVertically(-40, _noTime, null, index));
            storyboard.Children.Add(Scale(0, _noTime, null, index));
            SetColor(index);
            storyboard.Children.Add(Scale(1, _noTime, timeLine, index));
            storyboard.Children.Add(MoveVertically(0, duration, timeLine, index));
            return timeLine;
        }

移除

你会在所有故事函数中找到 takes 和 returns timeline。每个故事都会纠正一个字符。当你需要纠正多个字符时,你应该知道何时运行你的动画,才能获得最终效果,即第一个字符被纠正,然后第二个字符被纠正。如果我们没有这个 timeLine,所有字符将同时被纠正,用户会感到困惑,他会说“刚才发生了什么!?”

private TimeSpan Remove(int index, Storyboard storyboard, TimeSpan timeLine, TimeSpan duration)
        {
            SetColor(index,false);
            storyboard.Children.Add(MoveVertically(40,duration,timeLine,index));
            timeLine += duration;
            storyboard.Children.Add(Scale(0,_noTime,timeLine,index));
            return timeLine;
        }

交换

这里有改进的空间。我们使用“FormattedText”获取整个字符串的宽度,并假设我们的字符宽度是平均值,但事实并非如此。字符宽度可能不同,这就是为什么交换有点粗糙。所以,如果你找到了更好的方法来测量字符宽度,它将更准确。  

private TimeSpan Swap(int index1, int index2, Storyboard storyboard, TimeSpan timeLine, TimeSpan duration)
        {
            timeLine += duration;

            storyboard.Children.Add(MoveVertically(-AverageCharWidth(),duration,timeLine,index1,true));
            storyboard.Children.Add(MoveVertically(-AverageCharWidth(),duration,timeLine,index2,true));
            var distance = Math.Abs(index1 - index2);
            var to = AverageCharWidth() * distance;
            storyboard.Children.Add(MoveHorizontally(to, duration, timeLine, index1));
            storyboard.Children.Add(MoveHorizontally(-to, duration, timeLine, index2));

            return timeLine;

        }

替换

private TimeSpan Replace(int newIndex, int oldIndex, Storyboard storyboard, TimeSpan timeLine, TimeSpan duration)
        {
            SetColor(newIndex);
            SetColor(oldIndex,false);

            storyboard.Children.Add(MoveVertically(-AverageCharWidth(), _noTime, _noTime, newIndex));
            storyboard.Children.Add(Scale(0, _noTime, _noTime, newIndex));
            storyboard.Children.Add(MoveHorizontally(-AverageCharWidth(), _noTime, _noTime, newIndex));

            timeLine += duration;

            storyboard.Children.Add(Scale(1, _noTime, timeLine, newIndex));
            storyboard.Children.Add(MoveVertically(0, duration, timeLine, newIndex));
            storyboard.Children.Add(MoveVertically(AverageCharWidth(), duration, timeLine, oldIndex));
            return timeLine;
        }

   

我是如何做的

步骤

  1. 当用户从自动纠错弹出菜单中选择一个单词时,错误的单词和正确的单词就会被知晓。

    popup

  2. 然后我们将这两个单词(错误的单词和正确的单词)输入“查找更改算法”以查找更改。它将返回一个更改列表(步骤列表)。如果我们对错误的单词进行这些更改的动画处理,最终将得到正确的单词。
    public enum ChangeType
    {
           Insert = 1,
           Remove = 2,
           Swap = 3,
           Replace = 4,
    }
  3. 然后,我们使用这些更改步骤来动画化错误的单词,使其拼写正确。

UI

TextBlock 中有一个名为TextEffects 的对象,我们使用它将动画应用于字符。

算法带来的任何更改都将转换为DoubleAnimation ,然后输入到TextEffect中。通过这样做,每个字符都会获取其动画并播放它。

public DoubleAnimation MoveVertically
(double to, Duration duration, TimeSpan? timeSpan, int index, bool autoReverse =false)
public DoubleAnimation MoveHorizontally
(double to, Duration duration, TimeSpan? timeSpan, int index, bool autoReverse = false)
public DoubleAnimation Scale
(double to, Duration duration, TimeSpan? beginTime, int index, bool autoReverse = false)
public void SetColor(int index,bool isGreen=true)

这些是核心动画函数,然后是那些将ChangeTypes 转换为动画的函数。

private TimeSpan Remove
(int index, Storyboard storyboard, TimeSpan timeLine, TimeSpan duration)
private TimeSpan Insert
(int index, Storyboard storyboard,TimeSpan timeLine,TimeSpan duration)
private TimeSpan Replace
(int newIndex, int oldIndex, Storyboard storyboard, TimeSpan timeLine, TimeSpan duration)
private TimeSpan Swap
(int index1, int index2, Storyboard storyboard, TimeSpan timeLine, TimeSpan duration)

正如你所见,任何函数都返回TimeSpan。我们使用这个时间间隔来获取总的 timeline 周期。

public async Task AnimateIt(List<Change> changes,Action done)
        {
            PrepareTextEffect(changes);

            var storyboard = new Storyboard();
            var timeLine = TimeSpan.FromSeconds(0);
            var duration = TimeSpan.FromSeconds(0.5);

            foreach (var change in changes)
            {
                switch (change.ChangeType)
                {
                    case ChangeType.Insert:
                    {
                        timeLine += Insert
                        (_wrongWordStarts + change.Index, storyboard, timeLine, duration);
                    }
                        break;
                    case ChangeType.Remove:
                    {
                        timeLine += Remove
                        (_wrongWordStarts + change.Index, storyboard, timeLine, duration);
                    }
                    break;
                    case ChangeType.Swap:
                    {
                        timeLine += Swap
                        (_wrongWordStarts + change.Index, _wrongWordStarts + change.Index2.Value, 
                        storyboard, timeLine, duration);
                    }
                    break;
                    case ChangeType.Replace:
                    {
                        var newIndex = _wrongWordStarts + change.Index + 1;
                        var oldIndex = _wrongWordStarts + change.Index;
                        timeLine += Replace
                        (newIndex, oldIndex, storyboard, timeLine, duration);
                    }
                    break;
                }
                timeLine += duration;
            }

            storyboard.Begin(this);
            await Task.Delay( duration );
            done();
        }

InsertRemoveReplaceSwap 中的任何一个函数都会将其自己的DoubleAnimation 添加到StoryBoard中。当你运行StoryBoard时,每个动画都会播放。所有这些动画的总和就是动作,无论是插入还是删除还是……

动画

有一个小问题。我们之前说过,我们使用TextEffect来运行我们的动画。问题是TextBox没有这个属性,只有TextBlock才有。所以我们不得不让用户在TextBox中输入文本,但当他纠正任何单词时,我们隐藏TextBox并显示TextBlock来运行动画,然后再隐藏它。这样做有些粗糙,但为了简单起见,我们这样做了。

检测更改
 public List<Change> Find(string wrong, string right)
        {
            var submatches = SubMatches(wrong, right);

            var insert = GetInsertions(right, submatches);
            var remove = GetRemoves(wrong, submatches);
            var swaps = GetSwaps(wrong, right, submatches);
     
            var all = insert.Union(remove).Union(swaps).Union(replace).ToList();
            return all;
        }

首先,我们查找错误的单词和正确的单词之间的任何匹配项。

Matches

然后,我们查找需要添加到错误单词中的新字符,使其与正确的单词相似。我们可以在insert 对象中找到它。

任何不存在于正确单词中但存在于错误单词中的字符都应该被删除,我们可以在remove 对象中找到它。

如果有一个以上的交换,用户会感到困惑,所以我们检查是否只有一个交换。

所有这些更改的总和将错误的单词转换为正确的单词。

参考文献

© . All rights reserved.