先引入個小廣告:
最近買了台小米盒子折騰下,發覺 UI 還是挺漂亮的,特別是主頁那個倒影效果。
(圖隨便找的,就是上面圖片底部的那個倒影效果。)
好了,廣告結束,回歸正題,這個倒影效果我個人覺得是挺不錯的,那么有沒有辦法在 Win10 中實現呢?
稍微分析了一下,大概層次是這樣的:
簡單點來說,就是倒影顯示跟控件顯示一樣,然后往下翻轉,再平移一下就好了。最后再對倒影加個漸變透明就 perfect 了。
翻轉、平移都很容易,使用 RenderTransform 就可以了。麻煩就麻煩在如何讓倒影的顯示跟控件的顯示相同。
在 WinRT 里,是沒有 VisualBrush 這種東西的,因此我們得另尋他徑。俗語說:上帝關閉一扇門的同時也為你打開一扇窗。微軟雖然去掉 VisualBrush,但是給了我們 RenderTargetBitmap 這種獲取絕大部分控件當前樣貌的神器。(MediaElement 獲取不了,WebView 則需另外使用 WebViewBrush 來獲取,這里我們忽略掉這兩個不是常見需求的家伙。)
那么我們就可以將倒影設置為 Image 控件,然后賦值上 RenderTargetBitmap 就可以了。但問題又來了,我們應該什么時候去抓一次控件的外貌?查閱 MSDN 得知,LayoutUpdated 事件可以幫到我們。
還等什么,立馬開始編寫我們的代碼。
創建我們的項目,新建一個名字叫做 ReflectionPanel 的模板化控件。
然后定義我們的控件模板如下:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:ReflectionPanelDemo" xmlns:controls="using:ReflectionPanelDemo.Controls"> <Style TargetType="controls:ReflectionPanel"> <Setter Property="HorizontalAlignment" Value="Center" /> <Setter Property="VerticalAlignment" Value="Center" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="controls:ReflectionPanel"> <Grid x:Name="RootLayout" Background="{TemplateBinding Background}"> <!--#region 倒影--> <!--以控件底部中心作為變換點--> <Image x:Name="ReflectionImage" Stretch="None" RenderTransformOrigin="0.5,1"> <Image.RenderTransform> <TransformGroup> <!--以控件底部反轉控件--> <ScaleTransform ScaleY="-1" /> <!--倒影與實際內容的間距--> <TranslateTransform x:Name="SpacingTransform" Y="0" /> </TransformGroup> </Image.RenderTransform> </Image> <!--#endregion--> <!--#region 實際內容--> <ContentControl x:Name="ContentBorder" Content="{TemplateBinding Content}" /> <!--#endregion--> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>
這樣對應上之前的分析了。
接下來編寫 cs 代碼。
protected override void OnApplyTemplate() { FrameworkElement rootLayout = (FrameworkElement)this.GetTemplateChild("RootLayout"); // 實際內容容器。 this._contentBorder = (ContentControl)this.GetTemplateChild("ContentBorder"); // 倒影圖片。 this._reflectionImage = (Image)this.GetTemplateChild("ReflectionImage"); // 倒影位移。 this._spacingTransform = (TranslateTransform)this.GetTemplateChild("SpacingTransform"); this._spacingTransform.Y = this.ReflectionSpacing; if (DesignMode.DesignModeEnabled == false) { rootLayout.LayoutUpdated += this.RootLayoutChanged; } } private async void RootLayoutChanged(object sender, object e) { try { // 呈現控件到圖像源。 RenderTargetBitmap contentRender = new RenderTargetBitmap(); await contentRender.RenderAsync(this._contentBorder); // 設置倒影圖片。 this._reflectionImage.Source = contentRender; } catch { } }
這里是最關鍵的代碼。詳細可以看文章末尾提供的 demo。
接下來嘗試一下吧。
感覺還不錯的說。
最后,我們來做漸變的半透明效果。
在 WinRT 里,由於沒了 OpacityMask 屬性,因此我們還是從圖片入手吧。
RenderTargetBitmap 有一個叫 GetPixelsAsync 的方法,可以獲取到圖片的數據,格式是以 BGRA8 的格式,這里聯動一下老周的 blog 好了(http://www.cnblogs.com/tcjiaan/p/4231886.html)。
簡單點說,就是每 4 個 byte 代表一個像素。我們再簡單分析下需求,那么可以得出,圖片最頂部是最透明的,最底部是最不透明的。
經過簡單的數學計算,我們可以寫出以下代碼:
// 獲取圖像數據。 byte[] bgra8 = (await contentRender.GetPixelsAsync()).ToArray(); // 獲取圖像高度和寬度。 int width = contentRender.PixelWidth; int height = contentRender.PixelHeight; for (int i = 0; i < bgra8.Length; i += 4) { // 獲取該像素原來的 A 通道。 byte a = bgra8[i + 3]; // 計算該像素的 Y 軸坐標。 int y = (i / 4) / width; // 計算新的 A 通道值。 bgra8[i + 3] = (byte)(a * y / height); }
最后我們將修改后的 data 弄到 Image 控件上去就 ok 了。這里我們使用 WriteableBitmap。
WriteableBitmap outputBitmap = new WriteableBitmap(width, height); bgra8.CopyTo(outputBitmap.PixelBuffer); // 設置倒影圖片。 this._reflectionImage.Source = outputBitmap;
大功告成,看一下 Demo 的效果。
最后附帶完整 Demo 下載:ReflectionPanelDemo.zip