利用OpacityMask製作打洞效果
起因
專案上存在一個連線功能,在設計的原型中,在連線中間文字上下各有15畫素的空白。接手的同事覺得沒思路,問我能不能在不影響連線後面的背景情況下解決該問題。我就抽了點時間給他寫了個Demo。回家後趁熱打鐵,重新寫了個Demo,新增和完善了些功能。下面是效果圖:
程式碼實現
OpacityMask
在最開始看到效果圖的時候,我就想到利用OpacityMask來解決問題。可能這個屬性平時很多朋友都沒注意到,因為一般情況下用Opacity就足夠了。
OpacityMask定義在UIElement中,型別為Brush。僅使用提供的 Brush 的任意 Alpha 通道值。 Brush 呈現內容的其他通道(紅色、綠色或藍色)被忽略。具體來說,在Brush中Alpha通道值為0的地方將為透明,不為0的將顯示在UIElement中定義的背景。下面就以Demo中的三個例子簡單分享下怎麼利用OpacityMask。
矩形空洞
OpacityMask是一個VisualBrush,VisualBrush中有一個三行三列的Grid,Grid中除第二行第二列的單元格外,其餘的單元格均用黑色的矩形填充。這樣就會在第二行第二列的單元格處形成一個空洞。
主要程式碼在RectangleHoleConverter中,程式碼如下:
namespace HoleWithOpacityMask { using System; using System.Globalization; using System.Windows; using System.Windows.Controls;using System.Windows.Data; using System.Windows.Media; using System.Windows.Shapes; /// <summary> /// 矩形空洞的轉換器 /// </summary> public class RectangleHoleConverter : IMultiValueConverter { /// <summary> /// 轉換成矩形空洞 /// </summary> /// <param name="values"> ///轉換值列表,第一個表示起始寬度,第二個表示起始高度, /// 第三個表示總寬度,第四個表示總高度 /// 第五個表示宿主寬度,第六個表示宿主寬度 /// </param> /// <param name="targetType"></param> /// <param name="parameter"></param> /// <param name="culture"></param> /// <returns></returns> public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { if (values == null || values.Length != 6 || values.HaveNullItem() || !values.IsAllInstanceOfType(typeof(double))) { return DependencyProperty.UnsetValue; } var maskStartWidth = (double)values[0]; var maskStartHeight = (double)values[1]; var maskTotalWidth = (double)values[2]; var maskTotalHeight = (double)values[3]; var hostWidth = (double)values[4]; var hostHeight = (double)values[5]; if (hostWidth == 0.0 || hostHeight == 0.0) { return null; } var maskGrid = new Grid { Width = hostWidth, Height = hostHeight }; var opacityStartColumnDefinition = new ColumnDefinition { Width = new GridLength(maskStartWidth) }; var transparentColumnDefinition = new ColumnDefinition { Width = new GridLength(maskTotalWidth) }; ColumnDefinition opacityEndColumnDefinition = new ColumnDefinition(); opacityEndColumnDefinition.Width = new GridLength(1.0, GridUnitType.Star); maskGrid.ColumnDefinitions.Add(opacityStartColumnDefinition); maskGrid.ColumnDefinitions.Add(transparentColumnDefinition); maskGrid.ColumnDefinitions.Add(opacityEndColumnDefinition); var opacityStartRowDefinition = new RowDefinition { Height = new GridLength(maskStartHeight) }; var transparentRowDefinition = new RowDefinition { Height = new GridLength(maskTotalHeight) }; RowDefinition opacityEndRowDefinition = new RowDefinition(); opacityEndRowDefinition.Height = new GridLength(1.0, GridUnitType.Star); maskGrid.RowDefinitions.Add(opacityStartRowDefinition); maskGrid.RowDefinitions.Add(transparentRowDefinition); maskGrid.RowDefinitions.Add(opacityEndRowDefinition); for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { if ((i != 1) || (j != 1)) { Rectangle opacityRectangle = new Rectangle { Fill = Brushes.Black }; Grid.SetRow(opacityRectangle, i); Grid.SetColumn(opacityRectangle, j); maskGrid.Children.Add(opacityRectangle); } } } return new VisualBrush(maskGrid); } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { return new[] { Binding.DoNothing }; } } }
其中用到了兩個擴充套件方法如下:
namespace HoleTest { using System; using System.Collections.Generic; using System.Linq; public static class EnumerableExtension { /// <summary> /// 列舉器中是否存在null條目 /// </summary> /// <typeparam name="T">元素型別</typeparam> /// <param name="enumerable">元素列舉</param> /// <returns>存在null條目返回true,否則返回false</returns> public static bool HaveNullItem<T>(this IEnumerable<T> enumerable) { return enumerable.Any(item => item == null); } /// <summary> /// 列舉器中是否全為指定型別的例項 /// </summary> /// <typeparam name="T">元素型別</typeparam> /// <param name="enumerable">元素列舉</param> /// <returns>全為指定型別的例項返回true,否則返回false</returns> public static bool IsAllInstanceOfType<T>(this IEnumerable<T> enumerable, Type type) { return enumerable.All(item => type.IsInstanceOfType(item)); } } }
橢圓形空洞
OpacityMask是一個DrawingBrush,DrawingBrush是利用GeometryDrawing繪製。而GeometryDrawing中是一個由黑色填充的形狀,該形狀是在矩形中除去一個橢圓。這樣就會在矩形中形成一個空洞。
主要程式碼在EllipseHoleConverter中,程式碼如下:
namespace HoleWithOpacityMask { using System; using System.Globalization; using System.Windows; using System.Windows.Data; using System.Windows.Media; /// <summary> /// 橢圓形空洞的轉換器 /// </summary> public class EllipseHoleConverter : IMultiValueConverter { /// <summary> /// 轉換成矩形空洞 /// </summary> /// <param name="values"> /// 轉換值列表,第一個表示起始寬度,第二個表示起始高度, /// 第三個表示總寬度,第四個表示總高度 /// 第五個表示宿主寬度,第六個表示宿主寬度 /// </param> /// <param name="targetType"></param> /// <param name="parameter"></param> /// <param name="culture"></param> /// <returns></returns> public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { if (values == null || values.Length != 6 || values.HaveNullItem() || !values.IsAllInstanceOfType(typeof(double))) { return DependencyProperty.UnsetValue; } var maskEllipseCenterX = (double)values[0]; var maskEllipseCenterY = (double)values[1]; var maskRadiusX = (double)values[2]; var maskRadiusY = (double)values[3]; var hostWidth = (double)values[4]; var hostHeight = (double)values[5]; if (hostWidth == 0.0 || hostHeight == 0.0) { return null; } var maskRectangle = new RectangleGeometry(new Rect(new Size(hostWidth, hostHeight))); var maskEllipse = new EllipseGeometry( new Point(maskEllipseCenterX, maskEllipseCenterY), maskRadiusX, maskRadiusY); var combinedGeometry = Geometry.Combine(maskRectangle, maskEllipse, GeometryCombineMode.Exclude, null); var drawingBrush = new DrawingBrush(new GeometryDrawing(Brushes.Black, null, combinedGeometry)); return drawingBrush; } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { return new[] { Binding.DoNothing }; } } }
圖形空洞
其實最常用的還是ImageBrush,因為很多複雜的效果用VisualBrush或者DrawingBrush來實現效果很複雜且不清晰。在上面的Demo中我使用的是一張PNG圖片,當然一般也用PNG圖示,誰叫它支援透明呢。圖片是
XAML程式碼如下:
<Border Width="180" Height="180" Margin="3" Background="Aquamarine"> <Border Background="LightPink"> <Border.OpacityMask> <ImageBrush ImageSource="pig.png" /> </Border.OpacityMask> </Border> </Border>
容易看出,PNG圖片中透明的就真的透明瞭(直接看到外層Border上級的背景BurlyWood了),不透明的地方就使用了當前Border設定的背景色LightPink。
更多
還可使用線性漸變(LinearGradientBrush)、徑向漸變(RadialGradientBrush)實現更多有趣的效果,下圖是MSDN中的一個例子
純色畫刷(SolidColorBrush)基本用不到,要麼透明,要麼不透明,還不如直接設定Opacity。
下載連結
部落格園:HoleWithOpacityMask