From 91031d5fe8d6384ad4aef46f66f02bd97bf232ff Mon Sep 17 00:00:00 2001 From: Surma Date: Sun, 17 Jun 2018 10:46:44 +0100 Subject: [PATCH] Add some metadata for relative layout demo --- layout-worklet/relative/README.md | 3 + layout-worklet/relative/index.html | 190 +---------- layout-worklet/relative/layout.js | 517 ++++++++++++++++------------- layout-worklet/relative/main.js | 210 ++++++++++++ 4 files changed, 503 insertions(+), 417 deletions(-) create mode 100644 layout-worklet/relative/README.md create mode 100644 layout-worklet/relative/main.js diff --git a/layout-worklet/relative/README.md b/layout-worklet/relative/README.md new file mode 100644 index 0000000..5354095 --- /dev/null +++ b/layout-worklet/relative/README.md @@ -0,0 +1,3 @@ +`relative` is constrained-based layout written in Houdini’s Custom Layout. + +Video: https://www.youtube.com/watch?v=ClOk3RXOuDc diff --git a/layout-worklet/relative/index.html b/layout-worklet/relative/index.html index 26b5800..321c5d1 100644 --- a/layout-worklet/relative/index.html +++ b/layout-worklet/relative/index.html @@ -74,195 +74,7 @@

Houdini - Constraint Layout

- + diff --git a/layout-worklet/relative/layout.js b/layout-worklet/relative/layout.js index aa542f4..856bca6 100644 --- a/layout-worklet/relative/layout.js +++ b/layout-worklet/relative/layout.js @@ -14,234 +14,234 @@ // Implements a version of Android's RelativeLayout. // https://android.googlesource.com/platform/frameworks/base.git/+/master/core/java/android/widget/RelativeLayout.java - // We accept "-" in all of the operators. function normalizeOperator(operator) { - switch (operator) { - case 'above': - return 'above'; - case 'below': - return 'below'; - case 'left-of': - case 'leftOf': - return 'leftOf'; - case 'right-of': - case 'rightOf': - return 'rightOf'; - case 'align-top': - case 'alignTop': - return 'alignTop'; - case 'align-bottom': - case 'alignBottom': - return 'alignBottom'; - case 'align-left': - case 'alignLeft': - return 'alignLeft'; - case 'align-right': - case 'alignRight': - return 'alignRight'; - case 'align-parent-top': - case 'alignParentTop': - return 'alignParentTop'; - case 'align-parent-bottom': - case 'alignParentBottom': - return 'alignParentBottom'; - case 'align-parent-left': - case 'alignParentLeft': - return 'alignParentLeft'; - case 'align-parent-right': - case 'alignParentRight': - return 'alignParentRight'; - case 'center-horizontal': - case 'centerHorizontal': - return 'centerHorizontal'; - case 'center-vertical': - case 'centerVertical': - return 'centerVertical'; - default: - return null; - } + switch (operator) { + case "above": + return "above"; + case "below": + return "below"; + case "left-of": + case "leftOf": + return "leftOf"; + case "right-of": + case "rightOf": + return "rightOf"; + case "align-top": + case "alignTop": + return "alignTop"; + case "align-bottom": + case "alignBottom": + return "alignBottom"; + case "align-left": + case "alignLeft": + return "alignLeft"; + case "align-right": + case "alignRight": + return "alignRight"; + case "align-parent-top": + case "alignParentTop": + return "alignParentTop"; + case "align-parent-bottom": + case "alignParentBottom": + return "alignParentBottom"; + case "align-parent-left": + case "alignParentLeft": + return "alignParentLeft"; + case "align-parent-right": + case "alignParentRight": + return "alignParentRight"; + case "center-horizontal": + case "centerHorizontal": + return "centerHorizontal"; + case "center-vertical": + case "centerVertical": + return "centerVertical"; + default: + return null; + } } // Parses the --relative-constraints property, returning a map constraints. function parseRelativeConstraints(str, inlineSize) { - const parts = str.split(',').map(str => str.trim()); - const constraintList = str.split(',').map(str => str.trim().split(' ').map(str2 => str2.trim())); - const relativeConstraints = {}; - - for (let part of parts) { - const [constraintStr, queryStr] = part.split('/').map(str => str.trim()); - const constraint = constraintStr.split(' ').map(str => str.trim()); - const query = queryStr ? queryStr.split(' ').map(str => str.trim()) : ''; - if (constraint.length === 3 || constraint.length === 2) { - const [target, op, dest] = constraint; - - const operator = normalizeOperator(op); - if (!operator) - continue; + const parts = str.split(",").map(str => str.trim()); + const constraintList = str.split(",").map(str => + str + .trim() + .split(" ") + .map(str2 => str2.trim()) + ); + const relativeConstraints = {}; + + for (let part of parts) { + const [constraintStr, queryStr] = part.split("/").map(str => str.trim()); + const constraint = constraintStr.split(" ").map(str => str.trim()); + const query = queryStr ? queryStr.split(" ").map(str => str.trim()) : ""; + if (constraint.length === 3 || constraint.length === 2) { + const [target, op, dest] = constraint; + + const operator = normalizeOperator(op); + if (!operator) continue; if (query.length == 2) { const [queryType, queryLength] = query; - if (queryType === 'min-width' && inlineSize < parseInt(queryLength)) { + if (queryType === "min-width" && inlineSize < parseInt(queryLength)) { continue; } - if (queryType === 'max-width' && inlineSize >= parseInt(queryLength)) { + if (queryType === "max-width" && inlineSize >= parseInt(queryLength)) { continue; } } - if (!relativeConstraints[target]) - relativeConstraints[target] = {}; + if (!relativeConstraints[target]) relativeConstraints[target] = {}; - relativeConstraints[target][normalizeOperator(op)] = dest || true; - } - } + relativeConstraints[target][normalizeOperator(op)] = dest || true; + } + } - return relativeConstraints; + return relativeConstraints; } // Sorts the children based on the constraints. E.g. the constraint: // b below a, c below b // would return [a, b, c]. function sortChildren(relativeConstraints, childNames, mode) { - const predecessorsByChild = {}; - for (let childName of childNames) { - predecessorsByChild[childName] = mode === 'vertical' ? - getPredecessorsVertical(relativeConstraints, childName) : - getPredecessorsHorizontal(relativeConstraints, childName); - } - - let sortedSoFar = 0; - let changed = true; - - while (changed) { - changed = false; - - for (let i = sortedSoFar; i < childNames.length; i++) { - const childName = childNames[i]; - const predecessors = predecessorsByChild[childName]; - if (predecessors.length === 0) { - // Move element into sorted part of the list. - if (i !== sortedSoFar) { - const tmp = childNames[i]; - childNames[i] = childNames[sortedSoFar]; - childNames[sortedSoFar] = tmp; - } - - sortedSoFar++; - changed = true; - - // Remove this as a predecessor. - for (let j = sortedSoFar; j < childNames.length; j++) { - const l = predecessorsByChild[childNames[j]]; - const idx = l.indexOf(childName); - if (idx >= 0) - l.splice(idx, 1); - } - } - } - } - - if (sortedSoFar < childNames.length) { - throw Error("Cycle in dependency graph."); - } + const predecessorsByChild = {}; + for (let childName of childNames) { + predecessorsByChild[childName] = + mode === "vertical" + ? getPredecessorsVertical(relativeConstraints, childName) + : getPredecessorsHorizontal(relativeConstraints, childName); + } + + let sortedSoFar = 0; + let changed = true; + + while (changed) { + changed = false; + + for (let i = sortedSoFar; i < childNames.length; i++) { + const childName = childNames[i]; + const predecessors = predecessorsByChild[childName]; + if (predecessors.length === 0) { + // Move element into sorted part of the list. + if (i !== sortedSoFar) { + const tmp = childNames[i]; + childNames[i] = childNames[sortedSoFar]; + childNames[sortedSoFar] = tmp; + } + + sortedSoFar++; + changed = true; + + // Remove this as a predecessor. + for (let j = sortedSoFar; j < childNames.length; j++) { + const l = predecessorsByChild[childNames[j]]; + const idx = l.indexOf(childName); + if (idx >= 0) l.splice(idx, 1); + } + } + } + } + + if (sortedSoFar < childNames.length) { + throw Error("Cycle in dependency graph."); + } } function getPredecessorsVertical(relativeConstraints, childName) { - let predecessors = []; + let predecessors = []; - const c = relativeConstraints[childName] || {}; - if (c.above) - predecessors.push(c.above); + const c = relativeConstraints[childName] || {}; + if (c.above) predecessors.push(c.above); - if (c.below) - predecessors.push(c.below); + if (c.below) predecessors.push(c.below); - if (c.alignTop) - predecessors.push(c.alignTop); + if (c.alignTop) predecessors.push(c.alignTop); - if (c.alignBottom) - predecessors.push(c.alignBottom); + if (c.alignBottom) predecessors.push(c.alignBottom); - return predecessors; + return predecessors; } function getPredecessorsHorizontal(relativeConstraints, childName) { - let predecessors = []; + let predecessors = []; - const c = relativeConstraints[childName] || {}; - if (c.leftOf) - predecessors.push(c.leftOf); + const c = relativeConstraints[childName] || {}; + if (c.leftOf) predecessors.push(c.leftOf); - if (c.rightOf) - predecessors.push(c.rightOf); + if (c.rightOf) predecessors.push(c.rightOf); - if (c.alignLeft) - predecessors.push(c.alignLeft); + if (c.alignLeft) predecessors.push(c.alignLeft); - if (c.alignRight) - predecessors.push(c.alignRight); + if (c.alignRight) predecessors.push(c.alignRight); - return predecessors; + return predecessors; } -function applyHorizontalRules(relativeConstraints, childPositions, childName, inlineSize) { - const position = childPositions[childName]; - position.left = -1; - position.right = -1; +function applyHorizontalRules( + relativeConstraints, + childPositions, + childName, + inlineSize +) { + const position = childPositions[childName]; + position.left = -1; + position.right = -1; - const c = relativeConstraints[childName] || {}; - if (c.leftOf && childPositions[c.leftOf]) - position.right = childPositions[c.leftOf].left; + const c = relativeConstraints[childName] || {}; + if (c.leftOf && childPositions[c.leftOf]) + position.right = childPositions[c.leftOf].left; - if (c.rightOf && childPositions[c.rightOf]) - position.left = childPositions[c.rightOf].right; + if (c.rightOf && childPositions[c.rightOf]) + position.left = childPositions[c.rightOf].right; - if (c.alignLeft && childPositions[c.alignLeft]) - position.left = childPositions[c.alignLeft].left + if (c.alignLeft && childPositions[c.alignLeft]) + position.left = childPositions[c.alignLeft].left; - if (c.alignRight && childPositions[c.alignRight]) - position.right = childPositions[c.alignRight].right; + if (c.alignRight && childPositions[c.alignRight]) + position.right = childPositions[c.alignRight].right; - if (c.alignParentLeft) - position.left = 0; + if (c.alignParentLeft) position.left = 0; - if (c.alignParentRight) - position.right = inlineSize; + if (c.alignParentRight) position.right = inlineSize; } -function applyVerticalRules(relativeConstraints, childPositions, childName, blockSize) { - const position = childPositions[childName]; - position.top = -1; - position.bottom = -1; +function applyVerticalRules( + relativeConstraints, + childPositions, + childName, + blockSize +) { + const position = childPositions[childName]; + position.top = -1; + position.bottom = -1; - const c = relativeConstraints[childName] || {}; - if (c.above && childPositions[c.above]) - position.bottom = childPositions[c.above].top; + const c = relativeConstraints[childName] || {}; + if (c.above && childPositions[c.above]) + position.bottom = childPositions[c.above].top; - if (c.below && childPositions[c.below]) - position.top = childPositions[c.below].bottom; + if (c.below && childPositions[c.below]) + position.top = childPositions[c.below].bottom; - if (c.alignTop && childPositions[c.alignTop]) - position.top = childPositions[c.alignTop].top + if (c.alignTop && childPositions[c.alignTop]) + position.top = childPositions[c.alignTop].top; - if (c.alignBottom && childPositions[c.alignBottom]) - position.bottom = childPositions[c.alignBottom].bottom; + if (c.alignBottom && childPositions[c.alignBottom]) + position.bottom = childPositions[c.alignBottom].bottom; - if (c.alignParentTop) - position.top = 0; + if (c.alignParentTop) position.top = 0; - if (c.alignParentBottom && blockSize !== null) - position.bottom = blockSize; + if (c.alignParentBottom && blockSize !== null) position.bottom = blockSize; } // Measures the child in the *inline* direction. function* measureChildHorizontal(child, position) { - let childInlineSize = 0; + let childInlineSize = 0; if (position.left >= 0 && position.right >= 0) { childInlineSize = Math.max(0, position.right - position.left); - } else { + } else { childInlineSize = (yield child.layoutNextFragment({})).inlineSize; } @@ -253,16 +253,27 @@ function* measureChild(child, position) { const childConstraints = {}; if (position.top >= 0 && position.bottom >= 0) - childConstraints.fixedBlockSize = Math.max(0, position.bottom - position.top); + childConstraints.fixedBlockSize = Math.max( + 0, + position.bottom - position.top + ); if (position.left >= 0 && position.right >= 0) - childConstraints.fixedInlineSize = Math.max(0, position.right - position.left); + childConstraints.fixedInlineSize = Math.max( + 0, + position.right - position.left + ); return yield child.layoutNextFragment(childConstraints); } // Positions a child in the *inline* direction. -function positionChildHorizontal(position, constraint, inlineSize, childInlineSize) { +function positionChildHorizontal( + position, + constraint, + inlineSize, + childInlineSize +) { if (position.left < 0 && position.right >= 0) { // Right fixed, left unspecified. position.left = position.right - childInlineSize; @@ -274,7 +285,7 @@ function positionChildHorizontal(position, constraint, inlineSize, childInlineSi if (c.centerHorizontal) { position.left = (inlineSize - childInlineSize) / 2; position.right = position.left + childInlineSize; - } else { + } else { position.left = 0; position.right = childInlineSize; } @@ -282,7 +293,12 @@ function positionChildHorizontal(position, constraint, inlineSize, childInlineSi } // Positions a child in the *block* direction. -function positionChildVertical(position, constraint, blockSize, childBlockSize) { +function positionChildVertical( + position, + constraint, + blockSize, + childBlockSize +) { if (position.top < 0 && position.bottom >= 0) { position.top = position.bottom - childBlockSize; } else if (position.top >= 0 && position.bottom < 0) { @@ -300,64 +316,109 @@ function positionChildVertical(position, constraint, blockSize, childBlockSize) } } -registerLayout('relative', class { - static get inputProperties() { return [ '--relative-constraints', ]; } - static get childInputProperties() { return [ '--relative-name', ]; } - - *intrinsicSizes() {} - - *layout(children, edges, constraints, styleMap) { - const relativeConstraints = parseRelativeConstraints(styleMap.get('--relative-constraints').toString(), constraints.fixedInlineSize); - - const childrenMap = children.reduce((map, child) => { - const val = child.styleMap.get('--relative-name'); - if (val && val.toString() !== '') { - child.name = val.toString().trim(); - map[child.name] = child; - } - return map; - }, {}); - - const childPositions = Object.keys(childrenMap).reduce((map, key) => { - map[key] = {}; - return map; - }, {}); - - const sortedChildNamesHorizontal = Object.keys(childrenMap); - const sortedChildNamesVertical = Object.keys(childrenMap); - sortChildren(relativeConstraints, sortedChildNamesHorizontal, 'horizontal'); - sortChildren(relativeConstraints, sortedChildNamesVertical, 'vertical'); - - for (let childName of sortedChildNamesHorizontal) { - applyHorizontalRules(relativeConstraints, childPositions, childName, constraints.fixedInlineSize); - const childInlineSize = yield* measureChildHorizontal(childrenMap[childName], childPositions[childName]); - positionChildHorizontal(childPositions[childName], relativeConstraints[childName], constraints.fixedInlineSize, childInlineSize); - } - - const childFragmentMap = {}; - for (let childName of sortedChildNamesVertical) { - applyVerticalRules(relativeConstraints, childPositions, childName, constraints.fixedBlockSize); - const fragment = yield* measureChild(childrenMap[childName], childPositions[childName]); - childFragmentMap[childName] = fragment; - positionChildVertical(childPositions[childName], relativeConstraints[childName], constraints.fixedBlockSize, fragment.blockSize); - } - - let autoBlockSize = 0; - const childFragments = []; - for (let i = 0; i < children.length; i++) { - const childName = children[i].name; - const fragment = childName ? childFragmentMap[childName] : (yield children[i].layoutNextFragment({})); - childFragments.push(fragment); - - if (childName) { - const position = childPositions[childName]; - fragment.inlineOffset = position.left; - fragment.blockOffset = position.top; +registerLayout( + "relative", + class { + static get inputProperties() { + return ["--relative-constraints"]; + } + static get childInputProperties() { + return ["--relative-name"]; + } + + *intrinsicSizes() {} + + *layout(children, edges, constraints, styleMap) { + const relativeConstraints = parseRelativeConstraints( + styleMap.get("--relative-constraints").toString(), + constraints.fixedInlineSize + ); + + const childrenMap = children.reduce((map, child) => { + const val = child.styleMap.get("--relative-name"); + if (val && val.toString() !== "") { + child.name = val.toString().trim(); + map[child.name] = child; + } + return map; + }, {}); + + const childPositions = Object.keys(childrenMap).reduce((map, key) => { + map[key] = {}; + return map; + }, {}); + + const sortedChildNamesHorizontal = Object.keys(childrenMap); + const sortedChildNamesVertical = Object.keys(childrenMap); + sortChildren( + relativeConstraints, + sortedChildNamesHorizontal, + "horizontal" + ); + sortChildren(relativeConstraints, sortedChildNamesVertical, "vertical"); + + for (let childName of sortedChildNamesHorizontal) { + applyHorizontalRules( + relativeConstraints, + childPositions, + childName, + constraints.fixedInlineSize + ); + const childInlineSize = yield* measureChildHorizontal( + childrenMap[childName], + childPositions[childName] + ); + positionChildHorizontal( + childPositions[childName], + relativeConstraints[childName], + constraints.fixedInlineSize, + childInlineSize + ); } - autoBlockSize = Math.max(autoBlockSize, fragment.blockOffset + fragment.blockSize); - } + const childFragmentMap = {}; + for (let childName of sortedChildNamesVertical) { + applyVerticalRules( + relativeConstraints, + childPositions, + childName, + constraints.fixedBlockSize + ); + const fragment = yield* measureChild( + childrenMap[childName], + childPositions[childName] + ); + childFragmentMap[childName] = fragment; + positionChildVertical( + childPositions[childName], + relativeConstraints[childName], + constraints.fixedBlockSize, + fragment.blockSize + ); + } + + let autoBlockSize = 0; + const childFragments = []; + for (let i = 0; i < children.length; i++) { + const childName = children[i].name; + const fragment = childName + ? childFragmentMap[childName] + : yield children[i].layoutNextFragment({}); + childFragments.push(fragment); + + if (childName) { + const position = childPositions[childName]; + fragment.inlineOffset = position.left; + fragment.blockOffset = position.top; + } - return {autoBlockSize, childFragments}; + autoBlockSize = Math.max( + autoBlockSize, + fragment.blockOffset + fragment.blockSize + ); + } + + return { autoBlockSize, childFragments }; + } } -}); +); diff --git a/layout-worklet/relative/main.js b/layout-worklet/relative/main.js new file mode 100644 index 0000000..71e49bf --- /dev/null +++ b/layout-worklet/relative/main.js @@ -0,0 +1,210 @@ +import { + html, + render +} from "https://unpkg.com/lit-html/lib/lit-extended.js?module"; + +if (location.protocol === "http:" && location.hostname !== "localhost") { + location.protocol = "https:"; +} +if (!("layoutWorklet" in CSS)) { + document.body.innerHTML = + 'You need support for Layout Worklet to view this demo :('; +} + +CSS.layoutWorklet.addModule("layout.js"); + +const constraintOps = [ + "", + "above", + "below", + "left-of", + "right-of", + "align-top", + "align-bottom", + "align-left", + "align-right", + "align-parent-top", + "align-parent-bottom", + "align-parent-left", + "align-parent-right", + "center-horizontal", + "center-vertical" +]; + +const childNames = ["", "A", "B", "C", "D", "E", "F", "G"]; + +const queries = ["", "min-width", "max-width"]; + +function constraintOpHasNoDest(op) { + switch (op) { + case "align-parent-top": + case "align-parent-bottom": + case "align-parent-left": + case "align-parent-right": + case "center-horizontal": + case "center-vertical": + return true; + default: + return false; + } +} + +function updateRelativeStyle(constraints) { + const str = constraints.reduce( + (acc, { target, op, dest, query, queryLength, valid }) => { + if (!valid) return acc; + + let queryStr = ""; + if (query !== "" && queryLength !== null) { + queryStr = `/ ${query} ${queryLength}`; + } + + return acc + `, ${target} ${op} ${dest} ${queryStr}`; + }, + "" + ); + document + .getElementById("actual-layout") + .style.setProperty("--relative-constraints", str); +} + +const selectTmpl = (items, selected, disabled) => html` + +`; + +const rowTmpl = constraint => html` + + ${selectTmpl(childNames, constraint.target, false)} + ${selectTmpl(constraintOps, constraint.op, false)} + ${selectTmpl( + childNames, + constraint.dest, + constraintOpHasNoDest(constraint.op) + )} + / + ${selectTmpl(queries, constraint.query)} + px + + ${constraint.valid ? html`✅` : html`❌`} + +`; + +const tableTmpl = constraints => html` + + + + + + + + + + ${constraints.map(constraint => rowTmpl(constraint))} + + + + + + + + + +
Constraint + /Element QueryValid?
+`; + +let constraints = [ + { target: "", op: "", dest: "", query: "", queryLength: null, valid: false } +]; + +const controlsEl = document.getElementById("controls"); +const sliderEl = document.getElementById("slider"); +const actualLayoutEl = document.getElementById("actual-layout"); + +render(tableTmpl(constraints), controlsEl); + +// This event listener code is amazing - please don't copy it. +controlsEl.addEventListener("change", evt => { + const rows = document.getElementsByClassName("row"); + constraints = []; + let changedConstraint = null; + + for (let row of rows) { + const selects = Array.from(row.getElementsByTagName("select")); + const [target, op, dest, query] = selects.map( + select => select.children[select.selectedIndex].textContent + ); + const valid = row.getElementsByTagName("span")[0].textContent === "\u2705"; + const queryLength = + parseInt(row.getElementsByTagName("input")[0].value) || null; + const newConstraint = { target, op, dest, query, queryLength, valid }; + if (selects.find(select => select === evt.target)) { + changedConstraint = newConstraint; + } + constraints.push(newConstraint); + } + + if (changedConstraint) { + const noDest = constraintOpHasNoDest(changedConstraint.op); + if (noDest) { + changedConstraint.dest = ""; + } + + if ( + changedConstraint.target !== "" && + changedConstraint.op !== "" && + (noDest || changedConstraint.dest !== "") + ) { + changedConstraint.valid = true; + } + } + + updateRelativeStyle(constraints); + render(tableTmpl(constraints), controlsEl); +}); + +sliderEl.addEventListener("change", evt => { + const layoutChildren = Array.from(actualLayoutEl.children); + for (let i = 0; i < layoutChildren.length; i++) { + layoutChildren[i].style.display = i < evt.target.value ? "" : "none"; + } +}); + +controlsEl.addEventListener("click", evt => { + if (evt.target.tagName !== "BUTTON") return; + + if (evt.target.textContent === "Add") { + constraints.push({ + target: "", + op: "", + dest: "", + query: "", + queryLength: null, + valid: false + }); + updateRelativeStyle(constraints); + render(tableTmpl(constraints), controlsEl); + return; + } + + if (evt.target.textContent === "Remove") { + const rows = document.getElementsByClassName("row"); + for (let i = 0; i < rows.length; i++) { + if (rows[i].contains(evt.target)) { + constraints.splice(i, 1); + updateRelativeStyle(constraints); + render(tableTmpl(constraints), controlsEl); + return; + } + } + } +});