Forum Xamarin Xamarin.Forms

How can I duplicate a Label with Bounds data that is immediately ready to use?

aormsbyaormsby USMember ✭✭

I need to make a copy of a label and immediately perform calculations on it using the VisualElement.Measure() function. The reason I need to make a copy is that I sometimes manipulate the label data (mostly the text) and I don't want to modify the original label at all. Unfortunately, I haven't found a way to properly do this.

Here's what I've tried to so far:

Attempt #1: referencing the object

Label myCopy = origLabel;
myCopy.Text = origLabel.Text;

Doesn't work because: Yes, this was silly. Those of you who've tried this before know this is only a shallow copy, so myCopy only points to the same data in memory. Changing myCopy changes the original. Bad for me.

Attempt #2: Making a deep copy: I initially looked at this post for possible solutions.
Two ways I've seen to make a deep copy:
1. Implement ICloneable. I am using a static class and can't implement ICloneable. :disappointed:
2. Use a JSON converter to serialize and then deserialize the object data. I tried this using a few different JSON packages, but I got an exception about a self-referencing loop related to the label's children. I don't know if there's a way around this.

Attempt #3: Make a new label and copy some values

Label myCopy = new Label() {
                Text = String.Copy(origLabel.Text),
                WidthRequest = origLabel.WidthRequest,
                HeightRequest = origLabel.HeightRequest
            };

Doesn't work because: The width and height of myCopy are -1 until they go through a layout pass (which they will never have a chance to do naturally), so Measure() tries to measure them without the correct bounds values, even if I try to add the size requests like above

So what can I do? Are there any fixes for these issues? Or simply other options?

For example, perhaps there is a fix for the JSON exception that I haven't seen. But would a deep copy even work for the size values? Or maybe I can force the new label to size itself before I do my calculations?

I've been noodling over this for quite some time, and any help is appreciated.

Best Answer

  • aormsbyaormsby USMember ✭✭
    Accepted Answer

    Okay, I figured out a safe, simple workaround for this issue that I call 'ghost sizing'. You can create an invisible 'ghost label' and place it into a page where a label will be sized. It's not visible, so it doesn't change the layout at all, but it is still usable in size calculations. You pass the text of the label you want to size into the ghost label, size the ghost label to the desired space, and apply the returned size value to the target label.

    XAML:

    <!--  for sizing other elements without breaking bindings  -->
    <Label x:Name="ghostLabel" IsVisible="false" />
    

    C# code-behind:

    ghostLabel.Text = String.Copy(myLabel.Text);
    ...
    double labelFontSize = ...// do font size calculations here using the ghostLabel object
    ...
    myLabel.FontSize = labelFontSize;
    

    I've made my own font sizing class for this if you're curious about what I ended up with.

    Github repo
    further ghost sizing notes

Answers

  • ColeXColeX Member, Xamarin Team Xamurai

    Only Attempt#3 should be working ,i test and it seems to work as expected, VisualElement.Measure() gets correct values of size .

    This is my Xaml

        <StackLayout x:Name="stackLayout" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
            <Label x:Name="label1" Text="123" WidthRequest="200" HeightRequest="200" BackgroundColor="Red"/>
        </StackLayout>
    

  • aormsbyaormsby USMember ✭✭
    edited August 4

    Okay, I see part of the problem, at least.

    I never gave my original Label a WidthRequest or HeightRequest value, so those properties return a value of -1. In my function, I can't assume that other users will do that, either. One workaround here might be to copy the Width and Height values over instead.

    Label labelCopy = new Label() {
        Text = String.Copy(label.Text),
        WidthRequest = label.Width,     // using Width
        HeightRequest = label.Height,   // using Height
        VerticalOptions = label.VerticalOptions,
        HorizontalOptions = label.HorizontalOptions
    };
    

    This does seem to work for the Label copy and returns non-negative values in the size request immediately after. So I guess this might be a good fix for the simple copy problem. However, if you see any issues with this, please let me know.

    However, I'm somehow still getting results based on the -1 values after doing more calculations on my end. I'm unclear on what's happening with the rest of my function, so I'll put together a cleaner code sample of what I'm doing to post here for further help.

    As a side question - Why is it that unset view properties often default to -1? I can already think of other situations where trying to get those values for various calculations would be impossible if they weren't set in the initial declaration of the view.

  • ColeXColeX Member, Xamarin Team Xamurai
    edited August 5

    You could get the actual size in event SizeChanged rather than in class constructor.

            Label myCopy = new Label()
            {
                Text = String.Copy(label1.Text),
                BackgroundColor = Color.Green
            };
    
    
            this.SizeChanged += (sener, e) =>
            {
                myCopy.WidthRequest = label1.Width;
                myCopy.HeightRequest = label1.Height;
                SizeRequest s = myCopy.Measure(0, 0, MeasureFlags.None);
            };
    

    Refer to

    https://stackoverflow.com/a/42538883/8187800.

  • aormsbyaormsby USMember ✭✭

    Hmm, I see what you're saying, but I'm not sure that will work in my case since I'm already reacting to a size changed event. I'm also no longer convinced that setting the WidthRequest and HeightRequest properties will fully solve this. To make my issue more clear, I've got a full code sample ready to look at (attached).

    Quick explanation -> I modified the Xamarin book sample for Empirically Sizing Fonts. Two main changes - I separated the sizing calculations into their own function that returns a double font size, and instead of using the container view directly, I pass in the width and height values so I can make any modifications to my space constraints.

    In my sample, I have 2 labels of the same text. They are inside a stack layout with a small margin. I have two nearly identical sizing functions (one with a copied label). On stack size changed, each label should size to fit 1/2 the height of the stack layout, but the version of the function where I copy of the label does not calculate the font size correctly.

    I'm sure there's a way to get these 2 functions to properly calculate an identical font size, but I've been unable to pinpoint where things are going wrong. It's possible I just don't understand what's happening behind the scenes.

  • ColeXColeX Member, Xamarin Team Xamurai
    edited August 5

    Why don't you use label2.FontSize = newFontSize; directly?

    The control size(height/width) would not correct until you add it into layout , so it is incorrect that you create a brand new label and use it to calculate font size in CalculateMaxFontWithCopy method .

  • aormsbyaormsby USMember ✭✭

    @ColeX said:
    Why don't you use label2.FontSize = newFontSize; directly?

    Oh, I just used the same label text data to show the difference in output. If they each had different text, we'd certainly need to calculate the font size of each label to fit its space accordingly. Or even if they were the same text but fitting in different sized spaces. So I can't just use the same results as you suggest.

    The control size(height/width) would not correct until you add it into layout , so it is incorrect that you create a brand new label and use it to calculate font size in CalculateMaxFontWithCopy method .

    Okay, so this is what I'm trying to understand. In order to size the label copy, it has to be in a layout. But then wouldn't this mean that your previous code is incorrect for this case? Since your new label copy is not in a layout, I mean?

    I guess what I'm missing is that I don't fully know how sizing works. For example, the copied label isn't in a layout, but what's preventing it from using the constraints provided through my function? What would I need to do to make this work for me? Do you have any recommendations on how to proceed?

  • ColeXColeX Member, Xamarin Team Xamurai
    edited August 5

    Replace the new label with existing Label2 , it works fine .

        double newFontSizeWithCopy = CalculateMaxFontWithCopy(label2, view.Width, view.Height / 2);     
    
       double CalculateMaxFontWithCopy(Label copyLabel, double outerViewWidth, double outerViewHeight)
        {
            copyLabel.FontSize = label1.FontSize;
    
            FontCalc lowerFontCalc = new FontCalc(copyLabel, 10, outerViewWidth);
            FontCalc upperFontCalc = new FontCalc(copyLabel, 100, outerViewWidth);
    
            //xxxx
    
  • aormsbyaormsby USMember ✭✭
    1. Whoops, I did make a small mistake. Lines 71-72 in my sample code should both use label2 like this:
    double newFontSizeWithCopy = CalculateMaxFontWithCopy(    label2    , view.Width, view.Height / 2);
    label2.FontSize = newFontSizeWithCopy;
    

    While that does not affect the output, not passing the correct label does make it though to test properly.

    1. Your suggestion is clearly not a fix for the issue I've shown. It does not solve the problem of having different labels with different text values (which would result in different maximum font sizes). It also does not prevent changes to the referenced label data, which has been my primary goal in trying to make some sort of copy. Sorry, but it doesn't 'work fine' for my needs.

    2. In fact, all you've done is renamed the function argument and set the font size based on label1 before doing calculations anyway, which accomplishes exactly nothing different than the first function. You're right, it does work - but it's also not what I'm after. I need to ensure the original referenced label data is safe from changes I might need to make during calculations, which is why I need some sort of copy. I feel like I was quite clear about that.

    If you can help, great! But please don't fill this thread with junk solutions that imply you haven't read what I've written.

  • aormsbyaormsby USMember ✭✭
    Accepted Answer

    Okay, I figured out a safe, simple workaround for this issue that I call 'ghost sizing'. You can create an invisible 'ghost label' and place it into a page where a label will be sized. It's not visible, so it doesn't change the layout at all, but it is still usable in size calculations. You pass the text of the label you want to size into the ghost label, size the ghost label to the desired space, and apply the returned size value to the target label.

    XAML:

    <!--  for sizing other elements without breaking bindings  -->
    <Label x:Name="ghostLabel" IsVisible="false" />
    

    C# code-behind:

    ghostLabel.Text = String.Copy(myLabel.Text);
    ...
    double labelFontSize = ...// do font size calculations here using the ghostLabel object
    ...
    myLabel.FontSize = labelFontSize;
    

    I've made my own font sizing class for this if you're curious about what I ended up with.

    Github repo
    further ghost sizing notes

  • aormsbyaormsby USMember ✭✭

    Also, my apologies to @ColeX. I may have been a little rude in my last response, and I certainly meant no harm. I was very frustrated at the time. Sorry about that.

Sign In or Register to comment.