Fluent Designを䜿甚しおWindows 10甚のアプリケヌションを䜜成した方法UWP / C

iviでは、Windows 10PCおよびタブレット甚向けにアプリケヌションを曎新するこずを長い間蚈画しおいたす。 リラクれヌションのための䞀皮の「居心地の良い」コヌナヌにしたかったのです。 そのため、最近発衚されたMicrosoftの流fluentなデザむンのコンセプトが圹に立ちたした。



ただし、ここでは暙準コンポヌネントに぀いおは説明したせん。Microsoftには流なデザむン Acrylic 、 Reveal 、 Connectedアニメヌションなどを提䟛しおいたすが、もちろんそれらも䜿甚しおいたす。 すべおがシンプルで明快です-ドキュメントを受け取っお䜿甚しおください。



しかし、冒険は通垞、人里離れたずころから始たりたす。 したがっお、1぀のカスタムコントロヌルをどのように実行したかに぀いおお話しした方がよいでしょう。 以䞋がその1぀です。



画像



流Theな蚭蚈システムの深さず動きを䜿甚するずいう考え方です。 䞭心的な芁玠は、いわば、他のすべおの芁玠よりもわずかに高くなっおいたす。 これは、スクロヌル䞭にサむズず圱をアニメヌション化するこずで実珟されたす。



FlipViewコントロヌルはすぐには来たせんでした。なぜなら、 圌は、次の芁玠ず前の芁玠の断片を衚瀺する方法を知りたせん「耳」ず呌びたす。 そしお、解決策を探し始めたした。



方法1. GridViewを䜿甚しよう



論理的な解決策は、 GridViewを䜿甚しおみるこずでした。 氎平線にアむテムを䞊べるには、ItemsPanelセットずしお蚭定したす。



<ItemsStackPanel Orientation="Horizontal" />
      
      





珟圚の芁玠を䞭倮に配眮するには、GridViewテンプレヌトのScrollViewerプロパティを䜿甚したす。



 <ScrollViewer HorizontalSnapPointsType="MandatorySingle" HorizontalSnapPointsAlignment="Center" />
      
      





このような実装の䟋は、たずえばここで芋るこずができたす 。



すべおは倧䞈倫のようですが、問題がありたす。



グリッドビュヌ 問題1.画面の幅党䜓にわたる制埡のスケヌリング



デザむナヌが蚭蚈したずおり、コントロヌルはりィンドりの幅党䜓に広がる必芁がありたす。 これ自䜓は問題ではありたせん。 ただし、コントロヌルのサむズを倉曎する堎合、そのすべおの子アむテムのサむズも同期的に倉曎する必芁がありたす。





画像



すぐに䜿甚できるGridViewはありたせん。 UWPToolkitのAdaptiveGridViewコントロヌルの実装で解決策を芋぀けたした 。





 protected override void PrepareContainerForItemOverride(DependencyObject obj, object item) { base.PrepareContainerForItemOverride(obj, item); if (obj is FrameworkElement element) { var heightBinding = new Binding() { Source = this, Path = new PropertyPath("ItemHeight"), Mode = BindingMode.TwoWay }; var widthBinding = new Binding() { Source = this, Path = new PropertyPath("ItemWidth"), Mode = BindingMode.TwoWay }; element.SetBinding(HeightProperty, heightBinding); element.SetBinding(WidthProperty, widthBinding); } }
      
      





詳现に぀いおは、 UWPToolkitの゜ヌスコヌドを参照しおください。



すべおが問題ないように芋えたす しかし...



グリッドビュヌ 問題2.アむテムのサむズを倉曎するず、珟圚のアむテムが範囲倖になる



ただし、GridView内の芁玠の幅を動的に倉曎し始めるずすぐに、次の問題に盎面したす。 この時点で、たったく異なる芁玠が可芖領域に萜ち始めたす。 これは、GridView内のScrollViewerのHorizo​​ntalOffsetが倉曎されないずいう事実によるものです。 GridViewは、このようなキャッチを私たちに提案しおいたせん。

画像






この効果は、りィンドりを最倧化するずきに特に顕著ですサむズが急激に倉化するため。 たた、単にHorizo​​ntalOffsetの倀が倧きい堎合でも。



この問題は、GridViewに目的の芁玠たでスクロヌルするように䟝頌するこずで解決できるようです。



 private async void OnSizeChanged(object sender, SizeChangedEventArgs e) { ... await Task.Yield(); this.ScrollIntoView(getCurrentItem(), ScrollIntoViewAlignment.Default); }
      
      





しかし、いいえ





これは、GridViewのサむズを倉曎するたびにScrollViewerの新しいHorizo​​ntalOffset倀を手動で蚈算しお蚭定するこずでも解決できたす。



 private void OnSizeChanged(object sender, SizeChangedEventArgs e) { ... var scrollViewer = this.FindDescendant<ScrollViewer>(); scrollViewer.ChangeView(calculateNewHorizontalOffset(...), 0, 1, true); }
      
      





ただし、これはりィンドりの段階的なサむズ倉曎でのみ機胜したす。 りィンドりを最倧化するずき、これはしばしば間違った結果をもたらしたす。 おそらく、その理由は、蚈算した新しいHorizo​​ntalOffsetの倀が倧きすぎおExtentWidthScrollViewer内のコンテンツの幅を超えおいるためです。 そしお以来 GridViewはUI-virtualizationを䜿甚し、ExtentWidth Item-sの幅を倉曎した埌に自動的に再カりントされない堎合がありたす。



䞀般に、この問題に察する適切な解決策は芋぀かりたせんでした。



ここで停止しお、次の解決策の怜玢を開始できたす。 しかし、このアプロヌチの別の問題に぀いお説明したす。



グリッドビュヌ 問題3.ネストされたScrollViewersは、マりスでの氎平スクロヌルを䞭断したす



マりスホむヌルが垞に垂盎にスクロヌルするようにしたす。 垞に。 垂盎に。



ただし、GridViewペヌゞを氎平スクロヌルで配眮するず、その深さにあるScrollViewerはマりスホむヌルむベントをキャプチャし、それ以䞊スキップしたせん。 その結果、マりスカヌ゜ルがコントロヌルリストの䞊にある堎合、マりスホむヌルはその䞭を氎平スクロヌルしたす。 これは䞍䟿であり、ナヌザヌを混乱させたす。



画像



この問題には2぀の解決策がありたす。





タッチスクリヌンを倱いたくはありたせんでした。より良い解決策を探し続けたした。



パス2. UWPToolkitからのカルヌセル



次の゜リュヌションは、 UWPToolkitからのカルヌセルの制埡でした 。 すべおの偎面から非垞に興味深く、有益なコントロヌル。 誰でもその実装を勉匷するこずをお勧めしたす。



圌は私たちのニヌズをかなりよくカバヌしおくれたした。 しかし、最終的には適合したせんでした





すなわち 必芁に応じおこのコントロヌルを完成させるのにかなりの時間を費やす必芁があるこずがわかりたす1぀のUI仮想化の䟡倀がありたす。 同時に、出力では、アニメヌションが遅くなる可胜性のある䜜品を取埗したす。



䞀般的に、このアプロヌチを攟棄するこずも決定したした。 時間を無駄にするなら、賢明にそれをするでしょう。



方法3.実装



「タッチのみ」ScrollViewerの䜜成



マりスホむヌルからすべおのむベントをキャプチャするため、暙準のScrollViewerは䜿甚したくないこずを思い出させおください䞊蚘の「GridView。問題3」のセクションを参照。



カルヌセルの実装は奜きではありたせん。 UIストリヌムでアニメヌションを䜿甚し、UWPアプリケヌション甚のアニメヌションを䜜成するための奜たしい方法は、 Compositionアニメヌションです。 より銎染みのあるストヌリヌボヌドずの違いは、別のコンポゞションストリヌムで動䜜するため、UIストリヌムが䜕かでビゞヌな堎合でも60フレヌム/秒を提䟛するこずです。



タスクを実行するには、アニメヌションの゜ヌスずしおタッチ入力を䜿甚できるようにするコンポヌネントであるInteractionTrackerが必芁です。 実際、最初にやらなければならないこずは、画面䞊の指の動きに応じおUI芁玠を氎平に移動するこずです。 実際、カスタムScrollViewerを実装するこずから始めなければなりたせん。 それをTouchOnlyScrollViewerず呌びたしょう



 public class TouchOnlyScrollerViewer : ContentControl { private Visual _thisVisual; private Compositor _compositor; private InteractionTracker _tracker; private VisualInteractionSource _interactionSource; private ExpressionAnimation _positionExpression; private InteractionTrackerOwner _interactionTrackerOwner; public double HorizontalOffset { get; private set; } public event Action<double> ViewChanging; public event Action<double> ViewChanged; public TouchOnlyScrollerViewer() { initInteractionTracker(); Loaded += onLoaded; PointerPressed += onPointerPressed; } private void initInteractionTracker() { //  InteractionTracker  VisualInteractionSource _thisVisual = ElementCompositionPreview.GetElementVisual(this); _compositor = _thisVisual.Compositor; _tracker = InteractionTracker.Create(_compositor); _interactionSource = VisualInteractionSource.Create(_thisVisual); _interactionSource.PositionXSourceMode = InteractionSourceMode.EnabledWithInertia; _tracker.InteractionSources.Add(_interactionSource); //   Expression-,     //  touch-  InteractionTracker _positionExpression = _compositor.CreateExpressionAnimation("-tracker.Position"); _positionExpression.SetReferenceParameter("tracker", _tracker); } private void onLoaded(object sender, RoutedEventArgs e) { //      Offset  UIElement- var visual = ElementCompositionPreview.GetElementVisual((UIElement)Content); visual.StartAnimation("Offset", _positionExpression); } private void onPointerPressed(object sender, PointerRoutedEventArgs e) { //  touch-  composition- if (e.Pointer.PointerDeviceType == PointerDeviceType.Touch) { try { _interactionSource.TryRedirectForManipulation(e.GetCurrentPoint(this)); } catch (Exception ex) { Debug.WriteLine("TryRedirectForManipulation: " + ex.ToString()); } } } }
      
      





ここでは、すべおがMircosoftのドックに厳密に埓っおいたす。 TryRedirectForManipulation呌び出しは、ずきどき突然の䟋倖をスロヌするため、try-catchでラップする必芁がある堎合を陀きたす。 これは非垞にたれに発生しケヌス、ケヌスの玄2-5、理由を芋぀けるこずができたせんでした。 マむクロ゜フトのドキュメントず公匏の䟋でこれに぀いお䜕も蚀われおいないのはなぜですか-私たちは知りたせん;



タッチのみScrollViewer。 Horizo​​ntalOffsetずむベントViewChangingおよびViewChangedを圢成したす



ScrollViewerに䌌おいるため、Horizo​​ntalOffsetプロパティずViewChangingおよびViewChangedむベントが必芁です。 InteractionTrackerのコヌルバックを凊理しお実装したす。 これらを取埗するには、InteractionTrackerを䜜成するずきに、これらのコヌルバックを受け取るIInteractionTrackerOwnerを実装するオブゞェクトを指定する必芁がありたす。



 _interactionTrackerOwner = new InteractionTrackerOwner(this); _tracker = InteractionTracker.CreateWithOwner(_compositor, _interactionTrackerOwner);
      
      





完党を期すために、InteractionTrackerの状態ずむベントを含むドキュメントから写真をコピヌしたす。

画像






ViewChangedむベントは、アむドル状態に入るずスロヌされたす。



IInteractionTrackerOwner.ValuesChangedが発生するず、ViewChangingむベントがスロヌされたす。

InteractionTrackerがアむドル状態のずきにValuesChangedが発生する可胜性があるこずをすぐに蚀わなければなりたせん。 これは、InteractionTracker TryUpdatePositionを呌び出した埌に発生したす。 そしお、それはUWPプラットフォヌムのバグのように芋えたす。



たあ、あなたはそれに我慢しなければなりたせん。 幞いなこずに、それは難しくありたせん。ValuesChangedに応じお、珟圚の状態に応じおViewChangingたたはValuesChangedのいずれかを砎棄したす。



 private class InteractionTrackerOwner : IInteractionTrackerOwner { private readonly TouchOnlyScrollerViewer _scrollViewer; public void ValuesChanged(InteractionTracker sender, InteractionTrackerValuesChangedArgs args) { //   .    . _scrollViewer.HorizontalOffset = args.Position.X; if (_interactionTrackerState != InteractionTrackerState.Idle) { _scrollViewer.ViewChanging?.Invoke(args.Position.X); } else { _scrollViewer.ViewChanged?.Invoke(args.Position.X); } } public void IdleStateEntered(InteractionTracker sender, InteractionTrackerIdleStateEnteredArgs args) { //    _scrollViewer._tracker.Position. //  Windows 14393 (Anniversary Update)  -  0 _scrollViewer.ViewChanged?.Invoke(_scrollViewer.HorizontalOffset, requestType); } }
      
      





タッチのみScrollViewer。 ポむントをスナップしお正確に1぀の芁玠をスクロヌル



1぀の芁玠で正確にスクロヌルするために、すばらしい解決策がありたす-「 慣性修正子を䜿甚したスナップポむント 」。



ポむントは、タッチスクリヌンをスワむプした埌にスクロヌルを停止するポむントを蚭定するこずです。 そしお、残りのロゞックはInteractionTrackerによっお取埗されたす。 実際、スワむプ埌の停止がスムヌズに、同時に必芁な堎所で正確に発生するように、枛速率を倉曎したす。



私たちの実装は、 ドキュメントの䟋で説明されおいる実装ずわずかに異なりたす 。 ナヌザヌがリヌフレットをあたりにも速く「ねじった」堎合でも、䞀床に耇数の芁玠をスクロヌルさせたくないためです。



したがっお、「巊ぞ1ステップ」、「右ぞ1ステップ」、「珟圚の䜍眮にずどたる」ずいう3぀のスナップポむントのみを远加したす。 そしお、各スクロヌルの埌にそれらを曎新したす。



たた、スクロヌル埌に毎回スナップポむントを再䜜成しないように、それらをパラメヌタヌ化可胜にしたす。 これを行うには、3぀のプロパティでPropertySetを開始したす。



  _snapPointProps = _compositor.CreatePropertySet(); _snapPointProps.InsertScalar("offsetLeft", 0); _snapPointProps.InsertScalar("offsetCurrent", 0); _snapPointProps.InsertScalar("offsetRight", 0);
      
      





たた、ConditionおよびRestingValueの匏では、このPropertySetのプロパティを䜿甚したす。



  //    «   » var leftSnap = InteractionTrackerInertiaRestingValue.Create(_compositor); leftSnap.Condition = _compositor.CreateExpressionAnimation( "this.Target.NaturalRestingPosition.x < " + "props.offsetLeft * 0.25 + props.offsetCurrent * 0.75"); leftSnap.Condition.SetReferenceParameter("props", _snapPointProps); leftSnap.RestingValue = _compositor.CreateExpressionAnimation("props.offsetLeft"); leftSnap.RestingValue.SetReferenceParameter("props", _snapPointProps); //    «   » var currentSnap = InteractionTrackerInertiaRestingValue.Create(_compositor); currentSnap.Condition = _compositor.CreateExpressionAnimation( "this.Target.NaturalRestingPosition.x >= " + "props.offsetLeft * 0.25 + props.offsetCurrent * 0.75 && " + "this.Target.NaturalRestingPosition.x < " + "props.offsetCurrent * 0.75 + props.offsetRight * 0.25"); currentSnap.Condition.SetReferenceParameter("props", _snapPointProps); currentSnap.RestingValue = _compositor.CreateExpressionAnimation("props.offsetCurrent"); currentSnap.RestingValue.SetReferenceParameter("props", _snapPointProps); //   «   » var rightSnap = InteractionTrackerInertiaRestingValue.Create(_compositor); rightSnap.Condition = _compositor.CreateExpressionAnimation( "this.Target.NaturalRestingPosition.x >= " + "props.offsetCurrent * 0.75 + props.offsetRight * 0.25"); rightSnap.Condition.SetReferenceParameter("props", _snapPointProps); rightSnap.RestingValue = _compositor.CreateExpressionAnimation("props.offsetRight"); rightSnap.RestingValue.SetReferenceParameter("props", _snapPointProps); _tracker.ConfigurePositionXInertiaModifiers( new InteractionTrackerInertiaModifier[] { leftSnap, currentSnap, rightSnap }); }
      
      





ここに





最初に、スナップポむント間の真ん䞭の条件に境界線を蚭定しようずしたしたが、ナヌザヌは䜕らかの理由ですべおのスワむプが次の芁玠にスクロヌルするわけではないこずに気付きたした。 䞀郚のスワむプは十分に速くなく、ロヌルバックがありたした。



したがっお、Contitionの匏では、0.25および0.75の係数を䜿甚しおいるため、「遅い」スワむプでも次の芁玠にスクロヌルしたす。



次の芁玠にスクロヌルするたびに、このメ゜ッドを呌び出しおスナップポむントパラメヌタヌを曎新したす。



 public void SetSnapPoints(double left, double current, double right) { _snapPointProps.InsertScalar("offsetLeft", (float)Math.Max(left, 0)); _snapPointProps.InsertScalar("offsetCurrent", (float)current); _snapPointProps.InsertScalar("offsetRight", (float)Math.Min(right, _tracker.MaxPosition.X)); }
      
      





UI仮想化パネル



次のステップは、TouchOnlyScrollerViewerに基づいお本栌的なItemsControlを構築するこずでした。



参考のため。 UI仮想化ずは、たずえば1000人の子䟛ではなく、コントロヌルリストが画面に衚瀺されるものだけを䜜成する堎合です。 スクロヌルするずきにそれらを再利甚し、新しいデヌタオブゞェクトにバむンドしたす。 これにより、リストに倚数のアむテムがある堎合、ペヌゞの読み蟌み時間を短瞮できたす。



なぜなら 結局、UI仮想化を実装したくなかったのです。もちろん、最初にやろうずしたこずは、暙準のItemsStackPanelを䜿甚するこずでした 。



私たちのTouchOnlyScrollerViewerで圌女の友達を䜜りたかった。 残念ながら、その内郚構造たたは゜ヌスコヌドに関するドキュメントを芋぀けるこずはできたせんでした。 しかし、䞀連の実隓により、ItemsStackPanelは芪芁玠のリスト内のビゞュアルツリヌでScrollViewerを探すこずが瀺唆されたした。 そしお、どういうわけかこれをオヌバヌラむドしお、暙準のScrollViewerの代わりにそれが私たちのものになるように、芋぀けられたせん。



じゃあ したがっお、UI仮想化を䜿甚したパネルは、独立しお実行する必芁がありたす。 このトピックで芋぀かった最高のものは、11幎前ずいう早くもこのシリヌズの蚘事でした。1、2、3、4です。 確かに、それはWPFに぀いおであり、UWPに぀いおではありたせんが、アむデアを非垞によく䌝えおいたす。 私たちはそれを利甚したした。



実際には、アむデアは簡単です。





実装を瀺したせん。なぜなら かなり耇雑でした。 むしろ、別の蚘事のトピックです。



タッチ入力をコンポゞションストリヌムにリダむレクトした埌に倱われたタップむベントを探しおいたす



たずめるず、別の興味深い問題が明らかになりたした。 タッチ入力がInteractionTrackerにリダむレクトされおいる間に、ナヌザヌがコントロヌル内の芁玠をタップする堎合がありたす。 これは、慣性スクロヌルが発生したずきに発生したす。 この堎合、PointerPressed、PointerReleased、およびTappedむベントは発生したせん。 そしお、これは倧げさな問題ではありたせん。 InteractionTrackerの慣性はかなり長いです。 たた、芖芚的なスクロヌルがほが終了した堎合でも、実際には、最埌の数ピクセルの遅いスクロヌルが発生する堎合がありたす。



その結果、ナヌザヌは動揺しおいたす-圌は、遞択した映画のペヌゞがタップで開くこずを期埅しおいたす。 しかし、これは起こりたせん。



したがっお、InteractionTrackerからのむベントのペアによっおタップを識別したす。





 public void InertiaStateEntered(InteractionTracker sender, InteractionTrackerInertiaStateEnteredArgs args) { if (_interactionTrackerState == InteractionTrackerState.Interacting && (DateTime.Now - _lastStateTime) < TimeSpan.FromMilliseconds(150) && Math.Abs(args.PositionVelocityInPixelsPerSecond.X) < 1 /* 1px/sec */) { _scrollViewer.TappedCustom?.Invoke(_scrollViewer.HorizontalOffset); } _interactionTrackerState = InteractionTrackerState.Inertia; _lastStateTime = DateTime.Now; }
      
      





動䜜したす。 ただし、タップが実装された芁玠を認識するこずはできたせん。 私たちの堎合、これは重芁ではありたせん。 芁玠は、TouchOnlyScrollViewerの衚瀺幅のほが党䜓を占めたす。 したがっお、単玔に䞭心に近いものを遞択したす。 ほずんどの堎合、これはたさにあなたが必芁ずするものです。 これたでのずころ、スクロヌル䞭に時々タップするず間違った堎所に぀ながるこずに気づいおさえいたせん。 あなたがそれを知っおいおも、キャッチするのはそれほど簡単ではありたせん;



䞀般的なケヌスではありたすが、これは完党な゜リュヌションではありたせん。 完党に実装するには、ヒットテストをヒットする必芁がありたす。 しかし、それを行う方法は明確ではありたせん。 タップの座暙は䞍明です...



ボヌナス 䞍透明床、スケヌル、圱の衚珟アニメヌション。 最終的に矎しくなるために



そしお最埌に、ケヌキのチェリヌはそれがすべおであったものです。 スクロヌルしながら、芁玠のサむズ、圱、透明床を倉曎したす。 䞭心にあるものがわずかに盛り䞊がっおいるような感芚を䜜り出すため。



このために、 Expression-animationsを䜿甚したす。 これらはコンポゞションサブシステムの䞀郚でもあり、別のスレッドで動䜜するため、UIスレッドがビゞヌのずきに速床が䜎䞋するこずはありたせん。



これらはこのように䜜成されたす。 プロパティをアニメヌション化するために、このプロパティが他のプロパティに䟝存するこずを定矩する匏を定矩したす。 匏はテキスト文字列ずしお䞎えられたす。



圌らの魅力は、チェヌンで配眮できるこずです。 これを䜿甚したす。

画像



すべおのアニメヌションの゜ヌスは、InteractionTrackerからのピクセル単䜍のオフセットです。 UI- progress, 0 1. progress- .



, _progressExpression , :





 _progressExpression = _compositor.CreateExpressionAnimation( "1 - " + "Clamp(Abs(tracker.Position.X - props.offsetWhenSelected), 0, props.maxDistance)" + " / props.maxDistance");
      
      





:





Expression-:



 _progressExpression.SetReferenceParameter("tracker", tracker); _props = _compositor.CreatePropertySet(); _props.InsertScalar("offsetWhenSelected", (float)offsetWhenSelected); _props.InsertScalar("maxDistance", getMaxDistanceParam()); _progressExpression.SetReferenceParameter("props", _props);
      
      





PropertySet progress, _progressExpression. , :



 _progressProps = _compositor.CreatePropertySet(); _progressProps.InsertScalar("progress", 0f); _progressProps.StartAnimation("progress", _progressExpression);
      
      





progress «» ( Lerp ColorLerp). , Expression- .



:



 _scaleExpression = _compositor.CreateExpressionAnimation( "Vector3(Lerp(earUnfocusScale, 1, props.progress), " + "Lerp(earUnfocusScale, 1, props.progress), 1)"); _scaleExpression.SetScalarParameter("earUnfocusScale", (float)_earUnfocusScale); _scaleExpression.SetReferenceParameter("props", _progressProps); _thisVisual.StartAnimation("Scale", _scaleExpression);
      
      





:



 _shadowBlurRadiusExpression = _compositor.CreateExpressionAnimation( "Lerp(blur1, blur2, props.progress)"); _shadowBlurRadiusExpression.SetScalarParameter("blur1", ShadowBlurRadius1); _shadowBlurRadiusExpression.SetScalarParameter("blur2", ShadowBlurRadius2); _shadowBlurRadiusExpression.SetReferenceParameter("props", _progressProps); _dropShadow.StartAnimation("BlurRadius", _shadowBlurRadiusExpression);
      
      





:



 _shadowColorExpression = _compositor.CreateExpressionAnimation( "ColorLerp(color1, color2, props.progress)")) _shadowColorExpression.SetColorParameter("color1", ShadowColor1); _shadowColorExpression.SetColorParameter("color2", ShadowColor2); _shadowColorExpression.SetReferenceParameter("props", _progressProps); _dropShadow.StartAnimation("Color", _shadowColorExpression);
      
      





.





それだけです , , , , . fluent design :)



→ , , .



All Articles