#サンタさんへ
Xamarinライセンスください('ω')
もしくはぼっちクリスマスを一緒に過ごしてくれる人を下さい
もしくは美味しいディナーを下さい。おでんとか好きです。
おでん食べながら日本酒とか飲みたいです。
もしくはクリスマスの日をお休みにしてください。
というより休み下さい。
この記事は「Xamarin Advent Calendar 2015 - Qiita」の6日目です。
#はじめに
みなさん凄い人だらけなのでRead only memberとしてカレンダー記事を読もうと思ってたのですが、エクセルソフトの田淵さんよりお誘いを受けまして記事を書いてみることにしました。
あまり難しい事は書けなくて凄く申し訳ないのですが、どこかの誰かの役に立てば幸いだと思います。
#さてさて
WPFにはViewBoxがあります。
http://blog.okazuki.jp/entry/20130105/1357400239
便利ですよね。
Stretchにuniformって指定すれば縦横比を維持したままサイズを自在に変えられるなんて。
スマホ向けに画面を作成していたけどタブレットの見た目もスマホ同様にしなければならなくなった。
なんて事があった時、ViewBoxがあれば簡単に簡易な対応を行う事が出来そうです。
がXamarin.Formsにはありません。
リクエストもほんのちょっとだけでてます。
Xamarin.FormsはWpfとはXAMLという点では共通ですがやってることは全然違うので無理な気がしますが(;・∀・)
で、Viewboxっぽいことできないかなって考えてみました。
今回する事は
- Viewbox的なContentViewを作る。
- Labelを縦横サイズに応じてサイズ変更する。
- 試してみる。
をやってみたいと思います。
#1. Viewbox的なContentViewを作る。
まずは本家のViewboxがどんな事してるのかなと思ってみてみます。
http://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Controls/ViewBox.cs
へぇよくわからん(;・∀・)
このあたりでサイズの変更が行われてこの関数がきっとどんな縦横比に変更すればよいかの係数を算出しているのだろう。
ん~~(´・ω・`)
ComputeScaleFactorって関数は使えそうです(; ・`д・´)
ではContentViewを継承したFitboxというViewを作ってみます。
using System;
using Xamarin.Forms;
namespace App3
{
public enum StretchDirection
{
UpOnly,
DownOnly,
Both
}
public enum Stretch
{
None = 0,
Fill = 1,
Uniform = 2,
UniformToFill = 3
}
public class Fitbox : ContentView
{
public static readonly BindableProperty StretchProperty
= BindableProperty.Create(
nameof(Stretch),
typeof (Stretch),
typeof (Fitbox),
Stretch.Uniform,
validateValue: ValidateStretchValue);
public static readonly BindableProperty StretchDirectionProperty
= BindableProperty.Create(
nameof(StretchDirection),
typeof (StretchDirection),
typeof (Fitbox),
StretchDirection.Both,
validateValue: ValidateStretchDirectionValue);
public Stretch Stretch
{
get { return (Stretch) GetValue(StretchProperty); }
set { SetValue(StretchProperty, value); }
}
public StretchDirection StretchDirection
{
get { return (StretchDirection) GetValue(StretchDirectionProperty); }
set { SetValue(StretchDirectionProperty, value); }
}
private static bool ValidateStretchValue(BindableObject bindable, object value)
{
var s = (Stretch) value;
return s == Stretch.Uniform
|| s == Stretch.None
|| s == Stretch.Fill
|| s == Stretch.UniformToFill;
}
private static bool ValidateStretchDirectionValue(BindableObject bindable, object value)
{
var sd = (StretchDirection) value;
return sd == StretchDirection.Both
|| sd == StretchDirection.DownOnly
|| sd == StretchDirection.UpOnly;
}
protected override void LayoutChildren(double x, double y, double width, double height)
{
var scalefact = ComputeScaleFactor(new Size(width, height),
new Size(Content.WidthRequest, Content.HeightRequest), Stretch,
StretchDirection);
base.LayoutChildren(x, y, scalefact.Width*Content.WidthRequest, scalefact.Height*Content.HeightRequest);
}
public static Size ComputeScaleFactor(Size availableSize,
Size contentSize,
Stretch stretch,
StretchDirection stretchDirection)
{
var scaleX = 1.0;
var scaleY = 1.0;
var isConstrainedWidth = !double.IsPositiveInfinity(availableSize.Width);
var isConstrainedHeight = !double.IsPositiveInfinity(availableSize.Height);
if ((stretch == Stretch.Uniform || stretch == Stretch.Fill)
&& (isConstrainedWidth || isConstrainedHeight))
{
scaleX = IsZero(contentSize.Width) ? 0.0 : availableSize.Width/contentSize.Width;
scaleY = IsZero(contentSize.Height) ? 0.0 : availableSize.Height/contentSize.Height;
if (!isConstrainedWidth) scaleX = scaleY;
else if (!isConstrainedHeight) scaleY = scaleX;
else
{
switch (stretch)
{
case Stretch.Uniform:
var minscale = scaleX < scaleY ? scaleX : scaleY;
scaleX = scaleY = minscale;
break;
case Stretch.UniformToFill:
var maxscale = scaleX > scaleY ? scaleX : scaleY;
scaleX = scaleY = maxscale;
break;
case Stretch.Fill:
break;
}
}
switch (stretchDirection)
{
case StretchDirection.UpOnly:
if (scaleX < 1.0) scaleX = 1.0;
if (scaleY < 1.0) scaleY = 1.0;
break;
case StretchDirection.DownOnly:
if (scaleX > 1.0) scaleX = 1.0;
if (scaleY > 1.0) scaleY = 1.0;
break;
case StretchDirection.Both:
break;
default:
break;
}
}
return new Size(scaleX, scaleY);
}
public static bool IsZero(double value)
{
return Math.Abs(value) < 10.0*1e-8;
}
}
}
Microsoftのコードから
enum (Stretch, StretchDirection)
DependencyProperty (Stretch, StretchDirection)
ComputeScaleFactor関数
を持ってきました。
DependencyPropertyはXamarin.FormsではBindablePropertyに置き換えます。
大きくは変わりません。
ComputeScapeFactor関数もほとんど変わりませんがDouleUtilの部分に関してはIsZero関数を追加することで置き換えました。
LayoutChildren関数をoverrideします。
LayoutChildrenが呼ばれたときに、ComputeScapeFactor関数を呼び出して変化させるべき係数を算出します。
で係数をRequestWidth RequestHeightにかけて要素の縦横を決定します。
大体こんな感じで動くかなぁと。
#2. Labelを縦横サイズに応じてサイズ変更する。
ようするにLabelの大きさに応じてフォントサイズを調整して大きくしたり小さくしたりしたいです。
iOSのUILabelにはAdjustsFontSizeToFitWidthがあるのですが、なんかうまい感じに動かないんですよね。
長い文字列をフォントを小さくする方向でFitさせるのは問題ないんですが、大きくする方向がうまく動かなくて。
Androidにはそういうのはないです。
なので自分で作ってしまう事にしました。
PCLでは
public class FitLabel : Label
{
}
以上!(^^)!
まずはiOSから
http://stackoverflow.com/questions/8812192/how-to-set-font-size-to-fill-uilabel-height
を参考にしました。
public class LabelWithAdaptiveTextHeight : UILabel
{
public override void LayoutSubviews()
{
base.LayoutSubviews();
Font = FontToFitHeight();
}
private UIFont FontToFitHeight()
{
nfloat minFontSize = 0.1f;
nfloat maxFontSize = 1000f;
nfloat fontSizeAverage = 0f;
nfloat textAndLabelHeightDiff = 0f;
while (minFontSize <= maxFontSize)
{
fontSizeAverage = minFontSize + (maxFontSize - minFontSize)/2;
if (!string.IsNullOrEmpty(Text))
{
var labelText = new NSString(Text);
var labelHeight = Frame.Size.Height;
var testStringHeight =
labelText.GetSizeUsingAttributes(new UIStringAttributes {Font = Font.WithSize(fontSizeAverage)})
.Height;
textAndLabelHeightDiff = labelHeight - testStringHeight;
if (fontSizeAverage == minFontSize || fontSizeAverage == maxFontSize)
{
if (textAndLabelHeightDiff < 0)
{
return Font.WithSize(fontSizeAverage - 1);
}
return Font.WithSize(fontSizeAverage);
}
if (textAndLabelHeightDiff < 0)
{
maxFontSize = fontSizeAverage - 1;
}
else if (textAndLabelHeightDiff > 0)
{
minFontSize = fontSizeAverage + 1;
}
else
{
return Font.WithSize(fontSizeAverage);
}
}
}
return Font.WithSize(fontSizeAverage);
}
}
SwiftのコードをC#に変換しただけです。
Renderはこんな感じ。特になし。
Textの移動とちょっとプロパティ値を変更。
[assembly: ExportRenderer(typeof(FitLabel), typeof(FitLabelRender))]
namespace App3
{
public class FitLabelRender : ViewRenderer<FitLabel, LabelWithAdaptiveTextHeight>
{
protected override void OnElementChanged(ElementChangedEventArgs<FitLabel> e)
{
base.OnElementChanged(e);
SetNativeControl(new LabelWithAdaptiveTextHeight() {Text = e.NewElement.Text});
this.Control.AdjustsFontSizeToFitWidth = true;
this.Control.Lines = 1;
}
}
}
続いてAndroid
http://stackoverflow.com/questions/5033012/auto-scale-textview-text-to-fit-within-bounds/5280436
を参考にしました。
public class FitTextView : TextView
{
public const float MIN_TEXT_SIZE = 5;
private const string Ellipsis = "...";
public FitTextView(Context context) : base(context)
{
}
public FitTextView(Context context, IAttributeSet attrs) : base(context, attrs)
{
}
public FitTextView(Context context, IAttributeSet attrs, int defStyle) : base(context, attrs, defStyle)
{
}
public bool NeedsResize { get; private set; }
public float MaxTextSize { get; } = 1000;
public float MinTextSize { get; } = MIN_TEXT_SIZE;
public float SpacingMult { get; private set; } = 1.0f;
public float SpacingAdd { get; private set; }
public bool AddEllipsis { get; } = true;
public override void SetLineSpacing(float add, float mult)
{
base.SetLineSpacing(add, mult);
SpacingMult = mult;
SpacingAdd = add;
}
protected override void OnLayout(bool changed, int left, int top, int right, int bottom)
{
if (changed || NeedsResize)
{
var widthLimit = right - left - CompoundPaddingLeft - CompoundPaddingRight;
var heightLimit = bottom - top - CompoundPaddingBottom - CompoundPaddingTop;
ResizeText(widthLimit, heightLimit);
}
base.OnLayout(changed, left, top, right, bottom);
}
public void ResizeText()
{
var heightLimit = Height - PaddingBottom - PaddingTop;
var widthLimit = Width - PaddingLeft - PaddingRight;
ResizeText(widthLimit, heightLimit);
}
public void ResizeText(int width, int height)
{
ICharSequence text = new String(Text);
var linecount = 0;
if (text.Length() == 0 || height <= 0 || width <= 0 || Math.Abs(TextSize) <= 0)
{
return;
}
if (TransformationMethod != null)
{
text = TransformationMethod.GetTransformationFormatted(text, this);
}
var textPaint = Paint;
var targetTextSize = MaxTextSize > 0 ? Java.Lang.Math.Min(TextSize, MaxTextSize) : TextSize;
var textHeight = GetTextHeight(text, textPaint, width, targetTextSize, ref linecount);
var toLow = false;
while (linecount > 1 || (textHeight > height && targetTextSize > MinTextSize))
{
targetTextSize = Java.Lang.Math.Max(targetTextSize - 2, MinTextSize);
textHeight = GetTextHeight(text, textPaint, width, targetTextSize, ref linecount);
toLow = true;
}
if (!toLow)
{
while (linecount == 1 && textHeight < height && targetTextSize < MaxTextSize)
{
targetTextSize = Java.Lang.Math.Max(targetTextSize + 2, MinTextSize);
textHeight = GetTextHeight(text, textPaint, width, targetTextSize, ref linecount);
}
if (linecount > 1 || textHeight >= height)
{
targetTextSize -= 2;
}
}
if (AddEllipsis && targetTextSize.Equals(MinTextSize) && textHeight > height)
{
var paint = new TextPaint(textPaint);
var layout = new StaticLayout(text, paint, width, Layout.Alignment.AlignNormal, SpacingMult, SpacingAdd,
false);
if (layout.LineCount > 0)
{
var lastLine = layout.GetLineForVertical(height) - 1;
if (lastLine < 0)
{
SetText("", BufferType.Normal);
}
else
{
var start = layout.GetLineStart(lastLine);
var end = layout.GetLineEnd(lastLine);
var lineWidth = layout.GetLineWidth(lastLine);
var ellipseWidth = textPaint.MeasureText(Ellipsis);
while (width < lineWidth + ellipseWidth)
{
lineWidth = textPaint.MeasureText(text.SubSequence(start, --end + 1));
}
SetText(text.SubSequence(0, end) + Ellipsis, BufferType.Normal);
}
}
}
SetTextSize(ComplexUnitType.Px, targetTextSize);
SetLineSpacing(SpacingAdd, SpacingMult);
NeedsResize = false;
}
private int GetTextHeight(ICharSequence source, TextPaint paint, int width, float textSize, ref int linetCount)
{
var paintCopy = new TextPaint(paint)
{
TextSize = textSize
};
var layout = new StaticLayout(source, paintCopy, width, Layout.Alignment.AlignNormal, SpacingMult,
SpacingAdd, true);
linetCount = layout.LineCount;
return layout.Height;
}
}
参考にしたソースコードだと改行してしまう感じだったので、改行しないでサイズを変更するように修正しました。
ちょっと、余分なコードが多すぎるので余裕があったら直します。
やってることはiOSと同じで疑似的にフォントサイズを変更していって良い時点で適用する感じです。
Renderはこんな感じ
public class FitLabelRender : ViewRenderer<FitLabel, FitTextView>
{
protected override void OnElementChanged(ElementChangedEventArgs<FitLabel> e)
{
base.OnElementChanged(e);
var label = e.NewElement as FitLabel;
var newfitTextView = new FitTextView(this.Context) {Text = label.Text};
SetNativeControl(newfitTextView);
}
}
Textだけ移動してます。
iOSもそうなんですけどちゃんとやるなら他のプロパティも移動するようにしないとだめです。
#3. 試してみる。
実際使ってみるのでXAML書いてみます。
<?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:App3;assembly=App3"
x:Class="App3.Page2">
<RelativeLayout>
<local:FitLabel
RelativeLayout.WidthConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Width, Factor=1}"
RelativeLayout.HeightConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Height, Factor=0.05}"
RelativeLayout.XConstraint = "{ConstraintExpression Type=Constant, Constant=0}"
RelativeLayout.YConstraint = "{ConstraintExpression Type=Constant, Constant=0}"
>
Fitbox FitLabelのテストです。</local:FitLabel>
<local:Fitbox
RelativeLayout.WidthConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Width, Factor=1}"
RelativeLayout.HeightConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Height, Factor=0.8}"
RelativeLayout.XConstraint = "{ConstraintExpression Type=Constant, Constant=0}"
RelativeLayout.YConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Height, Factor=0.1}"
>
<RelativeLayout WidthRequest="100" HeightRequest="100">
<!--左上-->
<BoxView
RelativeLayout.WidthConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Width, Factor=0.33}"
RelativeLayout.HeightConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Height, Factor=0.33}"
RelativeLayout.XConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Width, Factor=0}"
RelativeLayout.YConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Height, Factor=0}"
Color="Aqua"></BoxView>
<!--上-->
<BoxView
RelativeLayout.WidthConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Width, Factor=0.33}"
RelativeLayout.HeightConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Height, Factor=0.33}"
RelativeLayout.XConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Width, Factor=0.33}"
RelativeLayout.YConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Height, Factor=0}"
Color="Black"></BoxView>
<!--右上-->
<BoxView
RelativeLayout.WidthConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Width, Factor=0.33}"
RelativeLayout.HeightConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Height, Factor=0.33}"
RelativeLayout.XConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Width, Factor=0.66}"
RelativeLayout.YConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Height, Factor=0}"
Color="Blue"></BoxView>
<!--左中-->
<BoxView
RelativeLayout.WidthConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Width, Factor=0.33}"
RelativeLayout.HeightConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Height, Factor=0.33}"
RelativeLayout.XConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Width, Factor=0}"
RelativeLayout.YConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Height, Factor=0.33}"
Color="Fuchsia"></BoxView>
<!--中-->
<BoxView
RelativeLayout.WidthConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Width, Factor=0.33}"
RelativeLayout.HeightConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Height, Factor=0.33}"
RelativeLayout.XConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Width, Factor=0.33}"
RelativeLayout.YConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Height, Factor=0.33}"
Color="Gray"></BoxView>
<!--右中-->
<BoxView
RelativeLayout.WidthConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Width, Factor=0.33}"
RelativeLayout.HeightConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Height, Factor=0.33}"
RelativeLayout.XConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Width, Factor=0.66}"
RelativeLayout.YConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Height, Factor=0.33}"
Color="Green"></BoxView>
<!--左下-->
<BoxView
RelativeLayout.WidthConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Width, Factor=0.33}"
RelativeLayout.HeightConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Height, Factor=0.33}"
RelativeLayout.XConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Width, Factor=0}"
RelativeLayout.YConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Height, Factor=0.66}"
Color="Lime"></BoxView>
<!--下-->
<BoxView
RelativeLayout.WidthConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Width, Factor=0.33}"
RelativeLayout.HeightConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Height, Factor=0.33}"
RelativeLayout.XConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Width, Factor=0.33}"
RelativeLayout.YConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Height, Factor=0.66}"
Color="Maroon"></BoxView>
<!--右下-->
<BoxView
RelativeLayout.WidthConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Width, Factor=0.33}"
RelativeLayout.HeightConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Height, Factor=0.33}"
RelativeLayout.XConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Width, Factor=0.66}"
RelativeLayout.YConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Height, Factor=0.66}"
Color="Olive"></BoxView>
</RelativeLayout>
</local:Fitbox>
</RelativeLayout>
</ContentPage>
画面の上から0.5割にラベルを付けて
上から1割の地点から8割分の高さをにFitboxを置きます。
Fitboxの中には、またRelativeLayoutを置いてWidthRequestとHeightRequestを100にして正方形とします。
中身はBoxviewで9分割します。
結果がこれ。
キャプチャはNexus4のエミュレータですがNexus10でもNexus7でもiPhoneで表示してもiPad Proで表示しても大体同じような見た目になります。
もちろんこのレイアウトだとスマホを横に傾けるとダメですが、縦長環境であれば大体同じ感じで表示できるはずです。
本来はスマホとタブレットは表示方法を変える、レスポンシブデザインで作るなどが正常な開発だと思いますが時と場合によってはそうでない時もあるかと思います。
そんな時に役に立てば良いなぁと思います。