Search code examples
iosxamarin.formscustom-renderer

How do I get text to wrap and add another line below in a Xamarin Forms iOS custom renderer?


I am creating a custom renderer to display a title and description in a translucent white space on an image. The title should wrap to the second line if wider than the image, and an optional description may appear below if there is room. In Android I am able to do this using a StaticLayout:

            // Create text rectangle
            var height = Height / 3;
            canvas.Save();
            canvas.ClipRect(0, Height - height, Width, Height);
            canvas.DrawARGB(191, 255, 255, 255);

            canvas.Restore();
            var item = ((ImageTile) Element).Item;
            var textSize = (height - 15) / 2;

            var textPaint = new TextPaint
            {
                StrokeWidth = 5,
                TextSize = textSize,
                FakeBoldText = true,
            };
            if (Build.VERSION.SdkInt >= BuildVersionCodes.Honeycomb)
                SetLayerType(LayerType.Software, textPaint);
            textPaint.SetStyle(Paint.Style.Fill);
            textPaint.Color = global::Android.Graphics.Color.Black;
            // init StaticLayout for text
            var titleLayout = new StaticLayout(
                item.Title, textPaint, Width - 10, Android.Text.Layout.Alignment.AlignNormal, 1.0f, 0.0f, false);
            canvas.Translate(5, Height - height + 5);
            titleLayout.Draw(canvas);
            canvas.Restore();

            textPaint = new TextPaint
            {
                StrokeWidth = 4,
                TextSize = textSize - 10,
            };
            if (Build.VERSION.SdkInt >= BuildVersionCodes.Honeycomb)
                SetLayerType(LayerType.Software, textPaint);
            var descLayout = new StaticLayout(
                item.Description, textPaint, Width - 10, Android.Text.Layout.Alignment.AlignNormal, 1.0f, 0.0f, false);
            canvas.Translate(5, Height - height + titleLayout.Height + 5);
            descLayout.Draw(canvas);
            canvas.Restore();

in iOS I am using CATextLayers, but I am unable to get the text to wrap even thought I define the frame and set both Wrapped to true and TextTruncationMode to None. I also don't know how to get the actual height of the tilteLayer so I can position the descLayer below it. This is what I have so far, which draws the title and description on top of each other without wraping.

                var textLayer = new CALayer();
                var textRec = new CGRect(0, element.HeightRequest - textheight, element.WidthRequest,
                    textheight);
                textLayer.Frame = textRec;
                var backgroundcolor = Color.FromRgba(255, 255, 255, .25).ToCGColor();
                textLayer.BackgroundColor = backgroundcolor;
                Control.Layer.AddSublayer(textLayer);
                var titleLayer = new CATextLayer
                {
                    String = element.Item.Title,
                    ForegroundColor = Color.Black.ToCGColor(),
                    FontSize = 14,
                    Wrapped = true,
                    TextTruncationMode = CATextLayerTruncationMode.None,
                    TextAlignmentMode = CATextLayerAlignmentMode.Left,
                    //Bounds = new CGRect(2, element.HeightRequest - textheight + 2, element.WidthRequest - 4,
                    //    textheight - 4),
                };
                var titleRec = new CGRect(2, element.HeightRequest - textheight + 2, element.WidthRequest - 4,
                    textheight - 4);
                titleLayer.Frame = titleRec;
                Control.Layer.AddSublayer(titleLayer);
                var descLayer = new CATextLayer
                {
                    String = element.Item.Description,
                    ForegroundColor = Color.Black.ToCGColor(),
                    FontSize = 12,
                    Wrapped = true,
                    TextTruncationMode = CATextLayerTruncationMode.None,
                };
                var descRec = new CGRect(2, element.HeightRequest - textheight + 2, element.WidthRequest - 4,
                    textheight - 4);
                descLayer.ContentsRect = descRec;
                Control.Layer.AddSublayer(descLayer);

Solution

  • Why not try autoLayout? You want to add a background view and two types of text on the original image. CALayer may achieve your effect, but it can't use autoLayout so that you need to use hard code(calculate the text height and the layer's position) to construct that. Also you said

    I am unable to get the text to wrap. And I also don't know how to get the actual height of the tilteLayer so I can position the descLayer below it.

    In the image's custom renderer, since this control has not been rendered, its Frame and HeightRequest are also unknown. Then you won't get the correct frame neither the layer, so the text will not be shown. I think the best way to do that is using AutoLayout:

    // Create a view to hold content just like your textLayer 
    UIView bgView = new UIView();
    bgView.BackgroundColor = UIColor.FromRGBA(1, 1, 1, 0.25f);
    bgView.TranslatesAutoresizingMaskIntoConstraints = false;
    Control.AddSubview(bgView);
    
    bgView.LeadingAnchor.ConstraintEqualTo(Control.LeadingAnchor).Active = true;
    bgView.TopAnchor.ConstraintGreaterThanOrEqualTo(Control.TopAnchor).Active = true;
    bgView.TrailingAnchor.ConstraintEqualTo(Control.TrailingAnchor).Active = true;
    bgView.BottomAnchor.ConstraintEqualTo(Control.BottomAnchor).Active = true;
    
    UILabel titleLabel = new UILabel();
    bgView.AddSubview(titleLabel);
    titleLabel.TranslatesAutoresizingMaskIntoConstraints = false;
    // Set this property to 0, then your label will move to several lines if your text is too large.
    titleLabel.Lines = 0;
    titleLabel.Font = UIFont.SystemFontOfSize(14);
    titleLabel.Text = Element.Item.Title;
    
    titleLabel.LeadingAnchor.ConstraintEqualTo(bgView.LeadingAnchor).Active = true;
    titleLabel.TopAnchor.ConstraintEqualTo(bgView.TopAnchor).Active = true;
    titleLabel.TrailingAnchor.ConstraintEqualTo(bgView.TrailingAnchor).Active = true;
    // This constraint will show the titleLabel's content at high priority. It means show the descLabel if the image has enough place.
    titleLabel.SetContentHuggingPriority(249, UILayoutConstraintAxis.Vertical);
    
    UILabel descLabel = new UILabel();
    bgView.AddSubview(descLabel);
    descLabel.TranslatesAutoresizingMaskIntoConstraints = false;
    descLabel.Lines = 0;
    descLabel.Text = Element.Item.Description;
    descLabel.Font = UIFont.SystemFontOfSize(12);
    
    descLabel.LeadingAnchor.ConstraintEqualTo(bgView.LeadingAnchor).Active = true;
    descLabel.TopAnchor.ConstraintEqualTo(titleLabel.BottomAnchor).Active = true;
    descLabel.TrailingAnchor.ConstraintEqualTo(bgView.TrailingAnchor).Active = true;
    descLabel.BottomAnchor.ConstraintEqualTo(bgView.BottomAnchor).Active = true;
    

    In this way, bgView will expand its height depending on the titleLabel and descLabel. The largest height will be the original image's height. Moreover titleLabel will auto calculate its size depending on its content. Also the descLabel will always lies below the titleLabel if the room allows.

    You can adjust these constraints to fit your own requirement.