// // Copyright (c) 2013-2015 Frank A. Krueger // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; using UIKit; namespace Praeclarum.UI { public static class Layout { /// /// Constrains the layout of subviews according to equations and /// inequalities specified in . Issue /// multiple constraints per call using the && operator. /// e.g. button.Frame.Left >= text.Frame.Right + 22 && /// button.Frame.Width == View.Frame.Width * 0.42f /// /// The superview laying out the referenced subviews. /// Constraint equations and inequalities. public static NSLayoutConstraint[] ConstrainLayout (this UIView view, Expression> constraints) { return ConstrainLayout (view, constraints, UILayoutPriority.Required); } /// /// Constrains the layout of subviews according to equations and /// inequalities specified in . Issue /// multiple constraints per call using the && operator. /// e.g. button.Frame.Left >= text.Frame.Right + 22 && /// button.Frame.Width == View.Frame.Width * 0.42f /// /// The superview laying out the referenced subviews. /// Constraint equations and inequalities. /// The priority of the constraints public static NSLayoutConstraint[] ConstrainLayout (this UIView view, Expression> constraints, UILayoutPriority priority) { var body = constraints.Body; var exprs = new List (); FindConstraints (body, exprs); var layoutConstraints = exprs.Select (e => CompileConstraint (e, view)).ToArray (); if (layoutConstraints.Length > 0) { foreach (var c in layoutConstraints) { c.Priority = (float)priority; } view.AddConstraints (layoutConstraints); } return layoutConstraints; } static NSLayoutConstraint CompileConstraint (BinaryExpression expr, UIView constrainedView) { var rel = NSLayoutRelation.Equal; switch (expr.NodeType) { case ExpressionType.Equal: rel = NSLayoutRelation.Equal; break; case ExpressionType.LessThanOrEqual: rel = NSLayoutRelation.LessThanOrEqual; break; case ExpressionType.GreaterThanOrEqual: rel = NSLayoutRelation.GreaterThanOrEqual; break; default: throw new NotSupportedException ("Not a valid relationship for a constrain."); } var left = GetViewAndAttribute (expr.Left); if (left.Item1 != constrainedView) { left.Item1.TranslatesAutoresizingMaskIntoConstraints = false; } var right = GetRight (expr.Right); return NSLayoutConstraint.Create ( left.Item1, left.Item2, rel, right.Item1, right.Item2, right.Item3, right.Item4); } static Tuple GetRight (Expression expr) { var r = expr; UIView view = null; NSLayoutAttribute attr = NSLayoutAttribute.NoAttribute; var mul = 1.0f; var add = 0.0f; var pos = true; if (r.NodeType == ExpressionType.Add || r.NodeType == ExpressionType.Subtract) { var rb = (BinaryExpression)r; if (IsConstant (rb.Left)) { add = ConstantValue (rb.Left); if (r.NodeType == ExpressionType.Subtract) { pos = false; } r = rb.Right; } else if (IsConstant (rb.Right)) { add = ConstantValue (rb.Right); if (r.NodeType == ExpressionType.Subtract) { add = -add; } r = rb.Left; } else { throw new NotSupportedException ("Addition only supports constants: " + rb.Right.NodeType); } } if (r.NodeType == ExpressionType.Multiply) { var rb = (BinaryExpression)r; if (IsConstant (rb.Left)) { mul = ConstantValue (rb.Left); r = rb.Right; } else if (IsConstant (rb.Right)) { mul = ConstantValue (rb.Right); r = rb.Left; } else { throw new NotSupportedException ("Multiplication only supports constants."); } } if (IsConstant (r)) { add = Convert.ToSingle (Eval (r)); } else if (r.NodeType == ExpressionType.MemberAccess || r.NodeType == ExpressionType.Call) { var t = GetViewAndAttribute (r); view = t.Item1; attr = t.Item2; } else { throw new NotSupportedException ("Unsupported layout expression node type " + r.NodeType); } if (!pos) mul = -mul; return Tuple.Create (view, attr, mul, add); } static bool IsConstant (Expression expr) { if (expr.NodeType == ExpressionType.Constant) return true; if (expr.NodeType == ExpressionType.MemberAccess) { var mexpr = (MemberExpression)expr; var m = mexpr.Member; if (m.MemberType == MemberTypes.Field) { return true; } return false; } if (expr.NodeType == ExpressionType.Convert) { var cexpr = (UnaryExpression)expr; return IsConstant (cexpr.Operand); } return false; } static float ConstantValue (Expression expr) { return Convert.ToSingle (Eval (expr)); } static Tuple GetViewAndAttribute (Expression expr) { var attr = NSLayoutAttribute.NoAttribute; MemberExpression frameExpr = null; var fExpr = expr as MethodCallExpression; if (fExpr != null) { switch (fExpr.Method.Name) { case "GetMidX": case "GetCenterX": attr = NSLayoutAttribute.CenterX; break; case "GetMidY": case "GetCenterY": attr = NSLayoutAttribute.CenterY; break; case "GetBaseline": attr = NSLayoutAttribute.Baseline; break; default: throw new NotSupportedException ("Method " + fExpr.Method.Name + " is not recognized."); } frameExpr = fExpr.Arguments.FirstOrDefault () as MemberExpression; } if (attr == NSLayoutAttribute.NoAttribute) { var memExpr = expr as MemberExpression; if (memExpr == null) throw new NotSupportedException ("Left hand side of a relation must be a member expression, instead it is " + expr); switch (memExpr.Member.Name) { case "Width": attr = NSLayoutAttribute.Width; break; case "Height": attr = NSLayoutAttribute.Height; break; case "Left": case "X": attr = NSLayoutAttribute.Left; break; case "Top": case "Y": attr = NSLayoutAttribute.Top; break; case "Right": attr = NSLayoutAttribute.Right; break; case "Bottom": attr = NSLayoutAttribute.Bottom; break; default: throw new NotSupportedException ("Property " + memExpr.Member.Name + " is not recognized."); } frameExpr = memExpr.Expression as MemberExpression; } if (frameExpr == null) throw new NotSupportedException ("Constraints should use the Frame or Bounds property of views."); var viewExpr = frameExpr.Expression; var view = Eval (viewExpr) as UIView; if (view == null) throw new NotSupportedException ("Constraints only apply to views."); return Tuple.Create (view, attr); } static object Eval (Expression expr) { if (expr.NodeType == ExpressionType.Constant) { return ((ConstantExpression)expr).Value; } if (expr.NodeType == ExpressionType.MemberAccess) { var mexpr = (MemberExpression)expr; var m = mexpr.Member; if (m.MemberType == MemberTypes.Field) { var f = (FieldInfo)m; var v = f.GetValue (Eval (mexpr.Expression)); return v; } } if (expr.NodeType == ExpressionType.Convert) { var cexpr = (UnaryExpression)expr; var op = Eval (cexpr.Operand); if (cexpr.Method != null) { return cexpr.Method.Invoke (null, new[]{ op }); } else { return Convert.ChangeType (op, cexpr.Type); } } return Expression.Lambda (expr).Compile ().DynamicInvoke (); } static void FindConstraints (Expression expr, List constraintExprs) { var b = expr as BinaryExpression; if (b == null) return; if (b.NodeType == ExpressionType.AndAlso) { FindConstraints (b.Left, constraintExprs); FindConstraints (b.Right, constraintExprs); } else { constraintExprs.Add (b); } } /// /// The baseline of the view whose frame is viewFrame. Use only when defining constraints. /// public static nfloat GetBaseline (this CoreGraphics.CGRect viewFrame) { return 0; } /// /// The x coordinate of the center of the frame. /// public static nfloat GetCenterX (this CoreGraphics.CGRect viewFrame) { return viewFrame.X + viewFrame.Width / 2; } /// /// The y coordinate of the center of the frame. /// public static nfloat GetCenterY (this CoreGraphics.CGRect viewFrame) { return viewFrame.Y + viewFrame.Height / 2; } } }