Exibindo bitmaps do SkiaSharp
O assunto dos bitmaps do SkiaSharp foi introduzido no artigo Noções básicas de bitmap no SkiaSharp. Esse artigo mostrou três maneiras de carregar bitmaps e três maneiras de exibir bitmaps. Este artigo analisa as técnicas para carregar bitmaps e se aprofunda no uso dos DrawBitmap
métodos de SKCanvas
.
Os DrawBitmapLattice
métodos e DrawBitmapNinePatch
são discutidos no artigo Exibição segmentada de bitmaps SkiaSharp.
Os exemplos nesta página são do aplicativo de exemplo. Na página inicial desse aplicativo, escolha SkiaSharp Bitmaps e vá para a seção Exibindo Bitmaps .
Carregando um bitmap
Um bitmap usado por um aplicativo SkiaSharp geralmente vem de uma das três fontes diferentes:
- Pela Internet
- De um recurso inserido no executável
- Da biblioteca de fotos do usuário
Também é possível que um aplicativo SkiaSharp crie um novo bitmap e, em seguida, desenhe nele ou defina os bits de bitmap algoritmicamente. Essas técnicas são discutidas nos artigos Criando e desenhando em bitmaps do SkiaSharp e Acessando pixels de bitmap do SkiaSharp.
Nos três exemplos de código a seguir de carregamento de um bitmap, supõe-se que a classe contenha um campo do tipo SKBitmap
:
SKBitmap bitmap;
Como afirmou o artigo Noções básicas de bitmap no SkiaSharp , a melhor maneira de carregar um bitmap pela Internet é com a HttpClient
classe. Uma única instância da classe pode ser definida como um campo:
HttpClient httpClient = new HttpClient();
Ao usar HttpClient
com aplicativos iOS e Android, você desejará definir as propriedades do projeto conforme descrito nos documentos sobre Transport Layer Security (TLS) 1.2.
O código que usa HttpClient
geralmente envolve o await
operador, portanto, ele deve residir em um async
método:
try
{
using (Stream stream = await httpClient.GetStreamAsync("https:// ··· "))
using (MemoryStream memStream = new MemoryStream())
{
await stream.CopyToAsync(memStream);
memStream.Seek(0, SeekOrigin.Begin);
bitmap = SKBitmap.Decode(memStream);
···
};
}
catch
{
···
}
Observe que o Stream
objeto obtido de é copiado GetStreamAsync
para um MemoryStream
arquivo . O Android não permite que o Stream
from HttpClient
seja processado pelo thread principal, exceto em métodos assíncronos.
O SKBitmap.Decode
faz muito trabalho: o Stream
objeto passado para ele faz referência a um bloco de memória contendo um bitmap inteiro em um dos formatos de arquivo bitmap comuns, geralmente JPEG, PNG ou GIF. O Decode
método deve determinar o formato e, em seguida, decodificar o arquivo de bitmap no próprio formato de bitmap interno do SkiaSharp.
Depois que o código chamar SKBitmap.Decode
, ele provavelmente invalidará o CanvasView
para que o PaintSurface
manipulador possa exibir o bitmap recém-carregado.
A segunda maneira de carregar um bitmap é incluindo o bitmap como um recurso inserido na biblioteca .NET Standard referenciada pelos projetos de plataforma individuais. Uma ID de recurso é passada para o GetManifestResourceStream
método. Essa ID de recurso consiste no nome do assembly, no nome da pasta e no nome do arquivo do recurso separados por pontos:
string resourceID = "assemblyName.folderName.fileName";
Assembly assembly = GetType().GetTypeInfo().Assembly;
using (Stream stream = assembly.GetManifestResourceStream(resourceID))
{
bitmap = SKBitmap.Decode(stream);
···
}
Os arquivos bitmap também podem ser armazenados como recursos no projeto de plataforma individual para iOS, Android e UWP (Plataforma Universal do Windows). No entanto, carregar esses bitmaps requer código localizado no projeto da plataforma.
Uma terceira abordagem para obter um bitmap é da biblioteca de imagens do usuário. O código a seguir usa um serviço de dependência incluído no aplicativo de exemplo. A Biblioteca Padrão do .NET SkiaSharpFormsDemo inclui a IPhotoLibrary
interface, enquanto cada um dos projetos de plataforma contém uma PhotoLibrary
classe que implementa essa interface.
IPhotoicturePicker picturePicker = DependencyService.Get<IPhotoLibrary>();
using (Stream stream = await picturePicker.GetImageStreamAsync())
{
if (stream != null)
{
bitmap = SKBitmap.Decode(stream);
···
}
}
Geralmente, esse código também invalida o CanvasView
para que o PaintSurface
manipulador possa exibir o novo bitmap.
A SKBitmap
classe define várias propriedades úteis, incluindo Width
e Height
, que revelam as dimensões em pixels do bitmap, bem como muitos métodos, incluindo métodos para criar bitmaps, copiá-los e expor os bits de pixel.
Exibição em dimensões de pixel
A classe SkiaSharp Canvas
define quatro DrawBitmap
métodos. Esses métodos permitem que os bitmaps sejam exibidos de duas maneiras fundamentalmente diferentes:
- Especificar um
SKPoint
valor (ou separarx
ey
valores) exibe o bitmap em suas dimensões de pixel. Os pixels do bitmap são mapeados diretamente para os pixels da exibição de vídeo. - Especificar um retângulo faz com que o bitmap seja esticado para o tamanho e a forma do retângulo.
Você exibe um bitmap em suas dimensões de pixel usando DrawBitmap
com um SKPoint
parâmetro ou DrawBitmap
com parâmetros e y
separadosx
:
DrawBitmap(SKBitmap bitmap, SKPoint pt, SKPaint paint = null)
DrawBitmap(SKBitmap bitmap, float x, float y, SKPaint paint = null)
Esses dois métodos são funcionalmente idênticos. O ponto especificado indica o local do canto superior esquerdo do bitmap em relação à tela. Como a resolução de pixels dos dispositivos móveis é tão alta, bitmaps menores geralmente aparecem bem pequenos nesses dispositivos.
O parâmetro opcional SKPaint
permite exibir o bitmap usando transparência. Para fazer isso, crie um SKPaint
objeto e defina a Color
propriedade como qualquer SKColor
valor com um canal alfa menor que 1. Por exemplo:
paint.Color = new SKColor(0, 0, 0, 0x80);
O 0x80 aprovado como o último argumento indica 50% de transparência. Você também pode definir um canal alfa em uma das cores predefinidas:
paint.Color = SKColors.Red.WithAlpha(0x80);
No entanto, a cor em si é irrelevante. Somente o canal alfa é examinado quando você usa o SKPaint
objeto em uma DrawBitmap
chamada.
O SKPaint
objeto também desempenha um papel ao exibir bitmaps usando modos de mesclagem ou efeitos de filtro. Estes são demonstrados nos artigos Modos de composição e mistura SkiaSharp e filtros de imagem SkiaSharp.
A página Dimensões em Pixels no programa de exemplo exibe um recurso de bitmap com 320 pixels de largura por 240 pixels de altura:
public class PixelDimensionsPage : ContentPage
{
SKBitmap bitmap;
public PixelDimensionsPage()
{
Title = "Pixel Dimensions";
// Load the bitmap from a resource
string resourceID = "SkiaSharpFormsDemos.Media.Banana.jpg";
Assembly assembly = GetType().GetTypeInfo().Assembly;
using (Stream stream = assembly.GetManifestResourceStream(resourceID))
{
bitmap = SKBitmap.Decode(stream);
}
// Create the SKCanvasView and set the PaintSurface handler
SKCanvasView canvasView = new SKCanvasView();
canvasView.PaintSurface += OnCanvasViewPaintSurface;
Content = canvasView;
}
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
float x = (info.Width - bitmap.Width) / 2;
float y = (info.Height - bitmap.Height) / 2;
canvas.DrawBitmap(bitmap, x, y);
}
}
O PaintSurface
manipulador centraliza o bitmap calculando x
valores com y
base nas dimensões em pixels da superfície de exibição e nas dimensões em pixels do bitmap:
Se o aplicativo quiser exibir o bitmap em seu canto superior esquerdo, ele simplesmente passará coordenadas de (0, 0).
Um método para carregar bitmaps de recursos
Muitos dos exemplos que estão chegando precisarão carregar recursos de bitmap. A classe estática BitmapExtensions
na solução de exemplo contém um método para ajudar:
static class BitmapExtensions
{
public static SKBitmap LoadBitmapResource(Type type, string resourceID)
{
Assembly assembly = type.GetTypeInfo().Assembly;
using (Stream stream = assembly.GetManifestResourceStream(resourceID))
{
return SKBitmap.Decode(stream);
}
}
···
}
Observe o Type
parâmetro. Esse pode ser o Type
objeto associado a qualquer tipo no assembly que armazena o recurso de bitmap.
Esse LoadBitmapResource
método será usado em todos os exemplos subsequentes que exigem recursos de bitmap.
Esticar para preencher um retângulo
A SKCanvas
classe também define um DrawBitmap
método que renderiza o bitmap em um retângulo e outro DrawBitmap
método que renderiza um subconjunto retangular do bitmap em um retângulo:
DrawBitmap(SKBitmap bitmap, SKRect dest, SKPaint paint = null)
DrawBitmap(SKBitmap bitmap, SKRect source, SKRect dest, SKPaint paint = null)
Em ambos os casos, o bitmap é esticado para preencher o retângulo chamado dest
. No segundo método, o source
retângulo permite selecionar um subconjunto do bitmap. O dest
retângulo é relativo ao dispositivo de saída; o source
retângulo é relativo ao bitmap.
A página Preencher Retângulo demonstra o primeiro desses dois métodos exibindo o mesmo bitmap usado no exemplo anterior em um retângulo do mesmo tamanho da tela:
public class FillRectanglePage : ContentPage
{
SKBitmap bitmap =
BitmapExtensions.LoadBitmapResource(typeof(FillRectanglePage),
"SkiaSharpFormsDemos.Media.Banana.jpg");
public FillRectanglePage ()
{
Title = "Fill Rectangle";
SKCanvasView canvasView = new SKCanvasView();
canvasView.PaintSurface += OnCanvasViewPaintSurface;
Content = canvasView;
}
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
canvas.DrawBitmap(bitmap, info.Rect);
}
}
Observe o uso do novo BitmapExtensions.LoadBitmapResource
método para definir o SKBitmap
campo. O retângulo de destino é obtido a Rect
partir da propriedade de SKImageInfo
, que descreve o tamanho da superfície de exibição:
Isso geralmente não é o que você quer. A imagem é distorcida ao ser esticada de forma diferente nas direções horizontal e vertical. Ao exibir um bitmap em algo diferente do tamanho do pixel, geralmente você deseja preservar a taxa de proporção original do bitmap.
Alongamento preservando a proporção
Esticar um bitmap preservando a taxa de proporção é um processo também conhecido como dimensionamento uniforme. Esse termo sugere uma abordagem algorítmica. Uma solução possível é mostrada na página Dimensionamento uniforme :
public class UniformScalingPage : ContentPage
{
SKBitmap bitmap =
BitmapExtensions.LoadBitmapResource(typeof(UniformScalingPage),
"SkiaSharpFormsDemos.Media.Banana.jpg");
public UniformScalingPage()
{
Title = "Uniform Scaling";
SKCanvasView canvasView = new SKCanvasView();
canvasView.PaintSurface += OnCanvasViewPaintSurface;
Content = canvasView;
}
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
float scale = Math.Min((float)info.Width / bitmap.Width,
(float)info.Height / bitmap.Height);
float x = (info.Width - scale * bitmap.Width) / 2;
float y = (info.Height - scale * bitmap.Height) / 2;
SKRect destRect = new SKRect(x, y, x + scale * bitmap.Width,
y + scale * bitmap.Height);
canvas.DrawBitmap(bitmap, destRect);
}
}
O PaintSurface
manipulador calcula um scale
fator que é o mínimo da proporção entre a largura e a altura da exibição e a largura e a altura do bitmap. Os x
valores and y
podem ser calculados para centralizar o bitmap dimensionado dentro da largura e altura da exibição. O retângulo de destino tem um canto superior esquerdo de x
e y
um canto inferior direito desses valores, além da largura e altura dimensionadas do bitmap:
Vire o telefone de lado para ver o bitmap esticado para essa área:
A vantagem de usar esse scale
fator torna-se óbvia quando você deseja implementar um algoritmo ligeiramente diferente. Suponha que você queira preservar a taxa de proporção do bitmap, mas também preencher o retângulo de destino. A única maneira de isso ser possível é cortando parte da imagem, mas você pode implementar esse algoritmo simplesmente alterando Math.Min
para Math.Max
no código acima. Eis o resultado:
A taxa de proporção do bitmap é preservada, mas as áreas à esquerda e à direita do bitmap são cortadas.
Uma função de exibição de bitmap versátil
Ambientes de programação baseados em XAML (como UWP e Xamarin.Forms) têm a facilidade de expandir ou reduzir o tamanho dos bitmaps, preservando suas taxas de proporção. Embora o SkiaSharp não inclua esse recurso, você mesmo pode implementá-lo.
A BitmapExtensions
classe incluída no aplicativo de exemplo mostra como. A classe define dois novos DrawBitmap
métodos que executam o cálculo da taxa de proporção. Esses novos métodos são métodos de extensão do SKCanvas
.
Os novos DrawBitmap
métodos incluem um parâmetro do tipo BitmapStretch
, uma enumeração definida no arquivo BitmapExtensions.cs :
public enum BitmapStretch
{
None,
Fill,
Uniform,
UniformToFill,
AspectFit = Uniform,
AspectFill = UniformToFill
}
Os None
membros , Fill
, Uniform
e UniformToFill
são os mesmos da enumeração UWP Stretch
. A enumeração semelhante Xamarin.FormsAspect
define os membros Fill
, AspectFit
, e AspectFill
.
A página Dimensionamento Uniforme mostrada acima centraliza o bitmap dentro do retângulo, mas talvez você queira outras opções, como posicionar o bitmap no lado esquerdo ou direito do retângulo ou na parte superior ou inferior. Esse é o objetivo da BitmapAlignment
enumeração:
public enum BitmapAlignment
{
Start,
Center,
End
}
As configurações de alinhamento não têm efeito quando usadas com BitmapStretch.Fill
.
A primeira DrawBitmap
função de extensão contém um retângulo de destino, mas nenhum retângulo de origem. Os padrões são definidos para que, se você quiser que o bitmap seja centralizado, você só precisa especificar um BitmapStretch
membro:
static class BitmapExtensions
{
···
public static void DrawBitmap(this SKCanvas canvas, SKBitmap bitmap, SKRect dest,
BitmapStretch stretch,
BitmapAlignment horizontal = BitmapAlignment.Center,
BitmapAlignment vertical = BitmapAlignment.Center,
SKPaint paint = null)
{
if (stretch == BitmapStretch.Fill)
{
canvas.DrawBitmap(bitmap, dest, paint);
}
else
{
float scale = 1;
switch (stretch)
{
case BitmapStretch.None:
break;
case BitmapStretch.Uniform:
scale = Math.Min(dest.Width / bitmap.Width, dest.Height / bitmap.Height);
break;
case BitmapStretch.UniformToFill:
scale = Math.Max(dest.Width / bitmap.Width, dest.Height / bitmap.Height);
break;
}
SKRect display = CalculateDisplayRect(dest, scale * bitmap.Width, scale * bitmap.Height,
horizontal, vertical);
canvas.DrawBitmap(bitmap, display, paint);
}
}
···
}
A principal finalidade desse método é calcular um fator de dimensionamento chamado scale
que é aplicado à largura e altura do bitmap ao chamar o CalculateDisplayRect
método. Este é o método que calcula um retângulo para exibir o bitmap com base no alinhamento horizontal e vertical:
static class BitmapExtensions
{
···
static SKRect CalculateDisplayRect(SKRect dest, float bmpWidth, float bmpHeight,
BitmapAlignment horizontal, BitmapAlignment vertical)
{
float x = 0;
float y = 0;
switch (horizontal)
{
case BitmapAlignment.Center:
x = (dest.Width - bmpWidth) / 2;
break;
case BitmapAlignment.Start:
break;
case BitmapAlignment.End:
x = dest.Width - bmpWidth;
break;
}
switch (vertical)
{
case BitmapAlignment.Center:
y = (dest.Height - bmpHeight) / 2;
break;
case BitmapAlignment.Start:
break;
case BitmapAlignment.End:
y = dest.Height - bmpHeight;
break;
}
x += dest.Left;
y += dest.Top;
return new SKRect(x, y, x + bmpWidth, y + bmpHeight);
}
}
A BitmapExtensions
classe contém um método adicional DrawBitmap
com um retângulo de origem para especificar um subconjunto do bitmap. Esse método é semelhante ao primeiro, exceto que o fator de escala é calculado com base no source
retângulo e, em seguida, aplicado ao source
retângulo na chamada para CalculateDisplayRect
:
static class BitmapExtensions
{
···
public static void DrawBitmap(this SKCanvas canvas, SKBitmap bitmap, SKRect source, SKRect dest,
BitmapStretch stretch,
BitmapAlignment horizontal = BitmapAlignment.Center,
BitmapAlignment vertical = BitmapAlignment.Center,
SKPaint paint = null)
{
if (stretch == BitmapStretch.Fill)
{
canvas.DrawBitmap(bitmap, source, dest, paint);
}
else
{
float scale = 1;
switch (stretch)
{
case BitmapStretch.None:
break;
case BitmapStretch.Uniform:
scale = Math.Min(dest.Width / source.Width, dest.Height / source.Height);
break;
case BitmapStretch.UniformToFill:
scale = Math.Max(dest.Width / source.Width, dest.Height / source.Height);
break;
}
SKRect display = CalculateDisplayRect(dest, scale * source.Width, scale * source.Height,
horizontal, vertical);
canvas.DrawBitmap(bitmap, source, display, paint);
}
}
···
}
O primeiro desses dois novos DrawBitmap
métodos é demonstrado na página Modos de dimensionamento. O arquivo XAML contém três Picker
elementos que permitem selecionar membros das BitmapStretch
enumerações e BitmapAlignment
:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:SkiaSharpFormsDemos"
xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
x:Class="SkiaSharpFormsDemos.Bitmaps.ScalingModesPage"
Title="Scaling Modes">
<Grid Padding="10">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<skia:SKCanvasView x:Name="canvasView"
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
PaintSurface="OnCanvasViewPaintSurface" />
<Label Text="Stretch:"
Grid.Row="1" Grid.Column="0"
VerticalOptions="Center" />
<Picker x:Name="stretchPicker"
Grid.Row="1" Grid.Column="1"
SelectedIndexChanged="OnPickerSelectedIndexChanged">
<Picker.ItemsSource>
<x:Array Type="{x:Type local:BitmapStretch}">
<x:Static Member="local:BitmapStretch.None" />
<x:Static Member="local:BitmapStretch.Fill" />
<x:Static Member="local:BitmapStretch.Uniform" />
<x:Static Member="local:BitmapStretch.UniformToFill" />
</x:Array>
</Picker.ItemsSource>
<Picker.SelectedIndex>
0
</Picker.SelectedIndex>
</Picker>
<Label Text="Horizontal Alignment:"
Grid.Row="2" Grid.Column="0"
VerticalOptions="Center" />
<Picker x:Name="horizontalPicker"
Grid.Row="2" Grid.Column="1"
SelectedIndexChanged="OnPickerSelectedIndexChanged">
<Picker.ItemsSource>
<x:Array Type="{x:Type local:BitmapAlignment}">
<x:Static Member="local:BitmapAlignment.Start" />
<x:Static Member="local:BitmapAlignment.Center" />
<x:Static Member="local:BitmapAlignment.End" />
</x:Array>
</Picker.ItemsSource>
<Picker.SelectedIndex>
0
</Picker.SelectedIndex>
</Picker>
<Label Text="Vertical Alignment:"
Grid.Row="3" Grid.Column="0"
VerticalOptions="Center" />
<Picker x:Name="verticalPicker"
Grid.Row="3" Grid.Column="1"
SelectedIndexChanged="OnPickerSelectedIndexChanged">
<Picker.ItemsSource>
<x:Array Type="{x:Type local:BitmapAlignment}">
<x:Static Member="local:BitmapAlignment.Start" />
<x:Static Member="local:BitmapAlignment.Center" />
<x:Static Member="local:BitmapAlignment.End" />
</x:Array>
</Picker.ItemsSource>
<Picker.SelectedIndex>
0
</Picker.SelectedIndex>
</Picker>
</Grid>
</ContentPage>
O arquivo code-behind simplesmente invalida o CanvasView
quando qualquer Picker
item é alterado. O PaintSurface
manipulador acessa as três Picker
exibições para chamar o DrawBitmap
método de extensão:
public partial class ScalingModesPage : ContentPage
{
SKBitmap bitmap =
BitmapExtensions.LoadBitmapResource(typeof(ScalingModesPage),
"SkiaSharpFormsDemos.Media.Banana.jpg");
public ScalingModesPage()
{
InitializeComponent();
}
private void OnPickerSelectedIndexChanged(object sender, EventArgs args)
{
canvasView.InvalidateSurface();
}
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
SKRect dest = new SKRect(0, 0, info.Width, info.Height);
BitmapStretch stretch = (BitmapStretch)stretchPicker.SelectedItem;
BitmapAlignment horizontal = (BitmapAlignment)horizontalPicker.SelectedItem;
BitmapAlignment vertical = (BitmapAlignment)verticalPicker.SelectedItem;
canvas.DrawBitmap(bitmap, dest, stretch, horizontal, vertical);
}
}
Aqui estão algumas combinações de opções:
A página Subconjunto Retangular tem praticamente o mesmo arquivo XAML que os Modos de Dimensionamento, mas o arquivo code-behind define um subconjunto retangular do bitmap fornecido pelo SOURCE
campo:
public partial class ScalingModesPage : ContentPage
{
SKBitmap bitmap =
BitmapExtensions.LoadBitmapResource(typeof(ScalingModesPage),
"SkiaSharpFormsDemos.Media.Banana.jpg");
static readonly SKRect SOURCE = new SKRect(94, 12, 212, 118);
public RectangleSubsetPage()
{
InitializeComponent();
}
private void OnPickerSelectedIndexChanged(object sender, EventArgs args)
{
canvasView.InvalidateSurface();
}
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
SKRect dest = new SKRect(0, 0, info.Width, info.Height);
BitmapStretch stretch = (BitmapStretch)stretchPicker.SelectedItem;
BitmapAlignment horizontal = (BitmapAlignment)horizontalPicker.SelectedItem;
BitmapAlignment vertical = (BitmapAlignment)verticalPicker.SelectedItem;
canvas.DrawBitmap(bitmap, SOURCE, dest, stretch, horizontal, vertical);
}
}
Essa origem retangular isola a cabeça do macaco, conforme mostrado nestas capturas de tela: