2022年12月7日水曜日

ラベルと値が縦に並んでいて、値部分の start が揃っている Composable を作る

ラベルと値が縦に並んでいる表があって、その値の start の位置を合わせたい。
ConstraintLayout で Barrier を使うか、自分で Layout を作ることになる。


Composableの順番で位置を指定する場合

content 内の Composable が「ラベル、値、区切り線、ラベル、値、区切り線、...」のようになっている前提の場合、このようなコードで実現できる。 @Composable fun LabelValueTable( modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { Layout( content = content, modifier = modifier ) { measurables, constraints -> val layoutWidth = constraints.maxWidth val labelMeasurables = mutableListOf<Measurable>() val valueMeasurables = mutableListOf<Measurable>() val dividerMeasurables = mutableListOf<Measurable>() measurables.forEachIndexed { index, measurable -> when (index % 3) { 0 -> labelMeasurables.add(measurable) 1 -> valueMeasurables.add(measurable) 2 -> dividerMeasurables.add(measurable) } } val constraintsForLabel = constraints.copy(minWidth = 0, minHeight = 0) val labelPlaceables = labelMeasurables.map { it.measure(constraintsForLabel) } val widthOfLabel = labelPlaceables.maxOf { it.width } val constraintsForValue = constraintsForLabel.copy(maxWidth = layoutWidth - widthOfLabel) val valuePlaceables = valueMeasurables.map { it.measure(constraintsForValue) } val dividerPlaceables = dividerMeasurables.map { it.measure(constraintsForLabel) } val heights = labelPlaceables.mapIndexed { index, labelPlaceable -> val valuePlaceable = valuePlaceables.getOrNull(index) max(labelPlaceable.height, valuePlaceable?.height ?: 0) } layout( width = constraints.maxWidth, height = max( heights.sum() + dividerPlaceables.sumOf { it.height }, constraints.minHeight ), ) { var top = 0 labelPlaceables.forEachIndexed { index, labelPlaceable -> val rowHeight = heights[index] labelPlaceable.placeRelative( x = 0, y = top + (rowHeight - labelPlaceable.height) / 2 ) val valuePlaceable = valuePlaceables.getOrNull(index) valuePlaceable?.placeRelative( x = widthOfLabel, y = top + (rowHeight - valuePlaceable.height) / 2 ) val dividerPlaceable = dividerPlaceables.getOrNull(index) dividerPlaceable?.placeRelative( x = 0, y = top + rowHeight ) top += rowHeight + (dividerPlaceable?.height ?: 0) } } } } まずラベル部分を measure して、全てのラベルから最大の width(= widthOfLabel) を計算する。

constraints.maxWidth から widthOfLabel を引いた値を maxWidth とした Constrains で値部分を measure する。

あとは、ラベル、値、区切り線を配置する。

値や区切り線に何も表示しないところは Spacer() をおけばいい。 LabelValueTable( modifier = modifier.fillMaxWidth() ) { Text( text = "名前", modifier = Modifier.padding(16.dp) ) Text( text = "山田 太郎", modifier = Modifier.padding(16.dp) ) Divider() Text( text = "bio", modifier = Modifier.padding(16.dp) ) Text( text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam", modifier = Modifier.padding(16.dp) ) Divider() Text( text = "label", modifier = Modifier.padding(16.dp) ) Column( modifier = Modifier.padding(16.dp) ) { Text( text = "headline", style = MaterialTheme.typography.bodyLarge, ) Text( text = "subtitle", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) } Divider( modifier = Modifier.padding(bottom = 24.dp) ) Text( text = "生年月日", modifier = Modifier.padding(16.dp) ) Row( modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically, ) { Text( text = "1990-01-01", modifier = Modifier.weight(1f) ) IconButton(onClick = { /*TODO*/ }) { Icon(imageVector = Icons.Default.Edit, contentDescription = "edit") } } Divider() Text( text = "性別", modifier = Modifier.padding(16.dp) ) Text( text = "男性", modifier = Modifier.padding(16.dp) ) }

ParentDataModifier で位置を指定する場合

ParentDataModifier を使って Cell の位置を指定する場合、このようなコードで実現できる。 @Composable fun LabelValueTable( modifier: Modifier = Modifier, content: @Composable LabelValueTableScope.() -> Unit, ) { Layout( content = { LabelValueTableScopeInstance.content() }, modifier = modifier ) { measurables, constraints -> val layoutWidth = constraints.maxWidth val labelMeasurables = mutableListOf<Measurable>() val valueMeasurables = mutableListOf<Measurable>() val dividerMeasurables = mutableListOf<Measurable>() measurables.forEach { measurable -> when (measurable.parentData) { is LabelIndex -> labelMeasurables.add(measurable) is ValueIndex -> valueMeasurables.add(measurable) is DividerIndex -> dividerMeasurables.add(measurable) } } val map = mutableMapOf<Int, Triple<Placeable?, Placeable?, Placeable?>>() val constraintsForLabel = constraints.copy(minWidth = 0, minHeight = 0) val labelPlaceables = labelMeasurables.map { it.measure(constraintsForLabel) } val widthOfLabel = labelPlaceables.maxOf { it.width } labelMeasurables.forEachIndexed { index, measurable -> val columnIndex = (measurable.parentData as LabelIndex).columnIndex map[columnIndex] = Triple(labelPlaceables[index], null, null) } val constraintsForValue = constraintsForLabel.copy(maxWidth = layoutWidth - widthOfLabel) val valuePlaceables = valueMeasurables.map { it.measure(constraintsForValue) } valueMeasurables.forEachIndexed { index, measurable -> val columnIndex = (measurable.parentData as ValueIndex).columnIndex map[columnIndex] = map.getOrDefault(columnIndex, Triple(null, null, null)) .copy(second = valuePlaceables[index]) } val dividerPlaceables = dividerMeasurables.map { it.measure(constraintsForLabel) } dividerMeasurables.forEachIndexed { index, measurable -> val columnIndex = (measurable.parentData as DividerIndex).columnIndex map[columnIndex] = map.getOrDefault(columnIndex, Triple(null, null, null)) .copy(third = dividerPlaceables[index]) } val list = map.toList() .sortedBy { it.first } .map { it.second } val heights = list.map { max(it.first?.height ?: 0, it.second?.height ?: 0) } layout( width = constraints.maxWidth, height = max( heights.sum() + dividerPlaceables.sumOf { it.height }, constraints.minHeight ), ) { var top = 0 list.forEachIndexed { index, triple -> val (labelPlaceable, valuePlaceable, dividerPlaceable) = triple val rowHeight = heights[index] labelPlaceable?.placeRelative( x = 0, y = top + (rowHeight - labelPlaceable.height) / 2 ) valuePlaceable?.placeRelative( x = widthOfLabel, y = top + (rowHeight - valuePlaceable.height) / 2 ) dividerPlaceable?.placeRelative( x = 0, y = top + rowHeight ) top += rowHeight + (dividerPlaceable?.height ?: 0) } } } } @Immutable interface LabelValueTableScope { @Stable fun Modifier.label(columnIndex: Int): Modifier @Stable fun Modifier.value(columnIndex: Int): Modifier @Stable fun Modifier.divider(columnIndex: Int): Modifier } internal object LabelValueTableScopeInstance : LabelValueTableScope { @Stable override fun Modifier.label(columnIndex: Int): Modifier { return this.then(LabelIndex(columnIndex)) } @Stable override fun Modifier.value(columnIndex: Int): Modifier { return this.then(ValueIndex(columnIndex)) } @Stable override fun Modifier.divider(columnIndex: Int): Modifier { return this.then(DividerIndex(columnIndex)) } } @Immutable private data class LabelIndex(val columnIndex: Int) : ParentDataModifier { override fun Density.modifyParentData(parentData: Any?): Any? { return this@LabelIndex } } @Immutable private data class ValueIndex(val columnIndex: Int) : ParentDataModifier { override fun Density.modifyParentData(parentData: Any?): Any? { return this@ValueIndex } } @Immutable private data class DividerIndex(val columnIndex: Int) : ParentDataModifier { override fun Density.modifyParentData(parentData: Any?): Any? { return this@DividerIndex } } measurable.parentData からラベル、値、区切り線のどれで、column index が何かを取得する。

measure と配置部分でやっていることは同じ。

使う側では Modifier.label(), Modifier.value(), Modifier.divider() を使って位置を指定する。 LabelValueTable( modifier = modifier.fillMaxWidth() ) { Text( text = "名前", modifier = Modifier .padding(16.dp) .label(0) ) Text( text = "山田 太郎", modifier = Modifier .padding(16.dp) .value(0) ) Divider( modifier = Modifier.divider(0) ) Text( text = "bio", modifier = Modifier .padding(16.dp) .label(1) ) Text( text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam", modifier = Modifier .padding(16.dp) .value(1) ) Divider( modifier = Modifier.divider(1) ) Text( text = "label", modifier = Modifier .padding(16.dp) .label(2) ) Column( modifier = Modifier .padding(16.dp) .value(2) ) { Text( text = "headline", style = MaterialTheme.typography.bodyLarge, ) Text( text = "subtitle", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) } Divider( modifier = Modifier .padding(bottom = 24.dp) .divider(2) ) Text( text = "生年月日", modifier = Modifier .padding(16.dp) .label(3) ) Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .padding(16.dp) .value(3), ) { Text( text = "1990-01-01", modifier = Modifier.weight(1f) ) IconButton(onClick = { /*TODO*/ }) { Icon(imageVector = Icons.Default.Edit, contentDescription = "edit") } } Divider( modifier = Modifier.divider(3) ) Text( text = "性別", modifier = Modifier .padding(16.dp) .label(4) ) Text( text = "男性", modifier = Modifier .padding(16.dp) .value(4) ) }

2022年12月6日火曜日

ParentDataModifier

ParentDataModifier は、親の Layout にデータを提供するための Modifier です。
ParentDataModifier の Density.modifyParentData() で返したデータが IntrinsicMeasurable.parentData に格納されます。

Measureable が IntrinsicMeasurable を実装しているので、MeasurePolicy の MeasureScope.measure() でこのデータを利用できます。 Layout( content = content, modifier = modifier ) { measurables, constraints -> measurables.forEach { val parentData = measurable.parentData ... } ... } ParentDataModifier interface を実装している Modifier として
  • LayoutId
  • BoxChildData
  • LayoutWeightImpl
  • HorizontalAlignModifier
  • VerticalAlignModifier
  • SiblingsAlignedModifier
などがあります。

この中で LayoutId は自分の Layout でも使うことができます。 Layout( content = { Icon( imageVector = Icons.Default.Home, contentDescription = "home", modifier = Modifier.layoutId("icon") ) Text( text = "home", modifier = Modifier.layoutId("text") ) }, modifier = modifier ) { measurables, constraints -> val iconMeasurable = measurables.first { it.layoutId == "icon" } val textMeasurable = measurables.first { it.layoutId == "text" } ... } Modifier.layoutId() は LayoutId を適用した Modifier を返すメソッドです。 @Stable fun Modifier.layoutId(layoutId: Any) = this.then( LayoutId( layoutId = layoutId, inspectorInfo = debugInspectorInfo { name = "layoutId" value = layoutId } ) ) LayoutId は ParentDataModifier と LayoutIdParentData を実装した Modifier で、Density.modifyParentData() では LayoutId 自身を返します。 @Immutable private class LayoutId( override val layoutId: Any, inspectorInfo: InspectorInfo.() -> Unit ) : ParentDataModifier, LayoutIdParentData, InspectorValueInfo(inspectorInfo) { override fun Density.modifyParentData(parentData: Any?): Any? { return this@LayoutId } ... } interface LayoutIdParentData { val layoutId: Any } Measurable.layoutId は parentData から Modifier.layoutId() で渡した layoutId を取得する便利メソッドです。 val Measurable.layoutId: Any? get() = (parentData as? LayoutIdParentData)?.layoutId

ParentDataModifier を実装した独自 Modifier を用意することができます。 @Immutable interface MyLayoutScope { @Stable fun Modifier.myLayoutData(index: Int): Modifier } internal object MyLayoutScopeInstance : MyLayoutScope { @Stable override fun Modifier.myLayoutData(index: Int): Modifier { return this.then(MyLayoutData(index = index)) } } @Immutable private data class MyLayoutData(val index: Int) : ParentDataModifier { override fun Density.modifyParentData(parentData: Any?): Any? { return this@MyLayoutData } } val Measurable.index: Int? get() = (parentData as? MyLayoutData)?.index



'},ClipboardSwf:null,Version:'1.5.1'}};dp.SyntaxHighlighter=dp.sh;dp.sh.Toolbar.Commands={ExpandSource:{label:'+ expand source',check:function(highlighter){return highlighter.collapse;},func:function(sender,highlighter) {sender.parentNode.removeChild(sender);highlighter.div.className=highlighter.div.className.replace('collapsed','');}},ViewSource:{label:'view plain',func:function(sender,highlighter) {var code=dp.sh.Utils.FixForBlogger(highlighter.originalCode).replace(/'+code+'');wnd.document.close();}},CopyToClipboard:{label:'copy to clipboard',check:function(){return window.clipboardData!=null||dp.sh.ClipboardSwf!=null;},func:function(sender,highlighter) {var code=dp.sh.Utils.FixForBlogger(highlighter.originalCode).replace(/</g,'<').replace(/>/g,'>').replace(/&/g,'&');if(window.clipboardData) {window.clipboardData.setData('text',code);} else if(dp.sh.ClipboardSwf!=null) {var flashcopier=highlighter.flashCopier;if(flashcopier==null) {flashcopier=document.createElement('div');highlighter.flashCopier=flashcopier;highlighter.div.appendChild(flashcopier);} flashcopier.innerHTML='';} alert('The code is in your clipboard now');}},PrintSource:{label:'print',func:function(sender,highlighter) {var iframe=document.createElement('IFRAME');var doc=null;iframe.style.cssText='position:absolute;width:0px;height:0px;left:-500px;top:-500px;';document.body.appendChild(iframe);doc=iframe.contentWindow.document;dp.sh.Utils.CopyStyles(doc,window.document);doc.write('

'+highlighter.div.innerHTML+'

');doc.close();iframe.contentWindow.focus();iframe.contentWindow.print();alert('Printing...');document.body.removeChild(iframe);}},About:{label:'?',func:function(highlighter) {var wnd=window.open('','_blank','dialog,width=300,height=150,scrollbars=0');var doc=wnd.document;dp.sh.Utils.CopyStyles(doc,window.document);doc.write(dp.sh.Strings.AboutDialog.replace('{V}',dp.sh.Version));doc.close();wnd.focus();}}};dp.sh.Toolbar.Create=function(highlighter) {var div=document.createElement('DIV');div.className='tools';for(var name in dp.sh.Toolbar.Commands) {var cmd=dp.sh.Toolbar.Commands[name];if(cmd.check!=null&&!cmd.check(highlighter)) continue;div.innerHTML+=''+cmd.label+'';} return div;} dp.sh.Toolbar.Command=function(name,sender) {var n=sender;while(n!=null&&n.className.indexOf('dp-highlighter')==-1) n=n.parentNode;if(n!=null) dp.sh.Toolbar.Commands[name].func(sender,n.highlighter);} dp.sh.Utils.CopyStyles=function(destDoc,sourceDoc) {var links=sourceDoc.getElementsByTagName('link');for(var i=0;i');} dp.sh.Utils.FixForBlogger=function(str) {return(dp.sh.isBloggerMode==true)?str.replace(/
|<br\s*\/?>/gi,''):str;} dp.sh.RegexLib={MultiLineCComments:new RegExp('/\\*[\\s\\S]*?\\*/','gm'),SingleLineCComments:new RegExp('//.*$','gm'),SingleLinePerlComments:new RegExp('#.*$','gm'),DoubleQuotedString:new RegExp('"(?:\\.|(\\\\\\")|[^\\""\\n])*"','g'),SingleQuotedString:new RegExp("'(?:\\.|(\\\\\\')|[^\\''\\n])*'",'g')};dp.sh.Match=function(value,index,css) {this.value=value;this.index=index;this.length=value.length;this.css=css;} dp.sh.Highlighter=function() {this.noGutter=false;this.addControls=true;this.collapse=false;this.tabsToSpaces=true;this.wrapColumn=80;this.showColumns=true;} dp.sh.Highlighter.SortCallback=function(m1,m2) {if(m1.indexm2.index) return 1;else {if(m1.lengthm2.length) return 1;} return 0;} dp.sh.Highlighter.prototype.CreateElement=function(name) {var result=document.createElement(name);result.highlighter=this;return result;} dp.sh.Highlighter.prototype.GetMatches=function(regex,css) {var index=0;var match=null;while((match=regex.exec(this.code))!=null) this.matches[this.matches.length]=new dp.sh.Match(match[0],match.index,css);} dp.sh.Highlighter.prototype.AddBit=function(str,css) {if(str==null||str.length==0) return;var span=this.CreateElement('SPAN');str=str.replace(/ /g,' ');str=str.replace(/');if(css!=null) {if((/br/gi).test(str)) {var lines=str.split(' 
');for(var i=0;ic.index)&&(match.index/gi,'\n');var lines=html.split('\n');if(this.addControls==true) this.bar.appendChild(dp.sh.Toolbar.Create(this));if(this.showColumns) {var div=this.CreateElement('div');var columns=this.CreateElement('div');var showEvery=10;var i=1;while(i<=150) {if(i%showEvery==0) {div.innerHTML+=i;i+=(i+'').length;} else {div.innerHTML+='·';i++;}} columns.className='columns';columns.appendChild(div);this.bar.appendChild(columns);} for(var i=0,lineIndex=this.firstLine;i0;i++) {if(Trim(lines[i]).length==0) continue;var matches=regex.exec(lines[i]);if(matches!=null&&matches.length>0) min=Math.min(matches[0].length,min);} if(min>0) for(var i=0;i

Blogger Syntax Highliter

Version: {V}

http://www.dreamprojections.com/syntaxhighlighter

©2004-2007 Alex Gorbatchev.

'},ClipboardSwf:null,Version:'1.5.1'}};dp.SyntaxHighlighter=dp.sh;dp.sh.Toolbar.Commands={ExpandSource:{label:'+ expand source',check:function(highlighter){return highlighter.collapse;},func:function(sender,highlighter) {sender.parentNode.removeChild(sender);highlighter.div.className=highlighter.div.className.replace('collapsed','');}},ViewSource:{label:'view plain',func:function(sender,highlighter) {var code=dp.sh.Utils.FixForBlogger(highlighter.originalCode).replace(/'+code+'');wnd.document.close();}},CopyToClipboard:{label:'copy to clipboard',check:function(){return window.clipboardData!=null||dp.sh.ClipboardSwf!=null;},func:function(sender,highlighter) {var code=dp.sh.Utils.FixForBlogger(highlighter.originalCode).replace(/</g,'<').replace(/>/g,'>').replace(/&/g,'&');if(window.clipboardData) {window.clipboardData.setData('text',code);} else if(dp.sh.ClipboardSwf!=null) {var flashcopier=highlighter.flashCopier;if(flashcopier==null) {flashcopier=document.createElement('div');highlighter.flashCopier=flashcopier;highlighter.div.appendChild(flashcopier);} flashcopier.innerHTML='';} alert('The code is in your clipboard now');}},PrintSource:{label:'print',func:function(sender,highlighter) {var iframe=document.createElement('IFRAME');var doc=null;iframe.style.cssText='position:absolute;width:0px;height:0px;left:-500px;top:-500px;';document.body.appendChild(iframe);doc=iframe.contentWindow.document;dp.sh.Utils.CopyStyles(doc,window.document);doc.write('

'+highlighter.div.innerHTML+'

');doc.close();iframe.contentWindow.focus();iframe.contentWindow.print();alert('Printing...');document.body.removeChild(iframe);}},About:{label:'?',func:function(highlighter) {var wnd=window.open('','_blank','dialog,width=300,height=150,scrollbars=0');var doc=wnd.document;dp.sh.Utils.CopyStyles(doc,window.document);doc.write(dp.sh.Strings.AboutDialog.replace('{V}',dp.sh.Version));doc.close();wnd.focus();}}};dp.sh.Toolbar.Create=function(highlighter) {var div=document.createElement('DIV');div.className='tools';for(var name in dp.sh.Toolbar.Commands) {var cmd=dp.sh.Toolbar.Commands[name];if(cmd.check!=null&&!cmd.check(highlighter)) continue;div.innerHTML+=''+cmd.label+'';} return div;} dp.sh.Toolbar.Command=function(name,sender) {var n=sender;while(n!=null&&n.className.indexOf('dp-highlighter')==-1) n=n.parentNode;if(n!=null) dp.sh.Toolbar.Commands[name].func(sender,n.highlighter);} dp.sh.Utils.CopyStyles=function(destDoc,sourceDoc) {var links=sourceDoc.getElementsByTagName('link');for(var i=0;i');} dp.sh.Utils.FixForBlogger=function(str) {return(dp.sh.isBloggerMode==true)?str.replace(/
|<br\s*\/?>/gi,'\n'):str;} dp.sh.RegexLib={MultiLineCComments:new RegExp('/\\*[\\s\\S]*?\\*/','gm'),SingleLineCComments:new RegExp('//.*$','gm'),SingleLinePerlComments:new RegExp('#.*$','gm'),DoubleQuotedString:new RegExp('"(?:\\.|(\\\\\\")|[^\\""\\n])*"','g'),SingleQuotedString:new RegExp("'(?:\\.|(\\\\\\')|[^\\''\\n])*'",'g')};dp.sh.Match=function(value,index,css) {this.value=value;this.index=index;this.length=value.length;this.css=css;} dp.sh.Highlighter=function() {this.noGutter=false;this.addControls=true;this.collapse=false;this.tabsToSpaces=true;this.wrapColumn=80;this.showColumns=true;} dp.sh.Highlighter.SortCallback=function(m1,m2) {if(m1.indexm2.index) return 1;else {if(m1.lengthm2.length) return 1;} return 0;} dp.sh.Highlighter.prototype.CreateElement=function(name) {var result=document.createElement(name);result.highlighter=this;return result;} dp.sh.Highlighter.prototype.GetMatches=function(regex,css) {var index=0;var match=null;while((match=regex.exec(this.code))!=null) this.matches[this.matches.length]=new dp.sh.Match(match[0],match.index,css);} dp.sh.Highlighter.prototype.AddBit=function(str,css) {if(str==null||str.length==0) return;var span=this.CreateElement('SPAN');str=str.replace(/ /g,' ');str=str.replace(/');if(css!=null) {if((/br/gi).test(str)) {var lines=str.split(' 
');for(var i=0;ic.index)&&(match.index/gi,'\n');var lines=html.split('\n');if(this.addControls==true) this.bar.appendChild(dp.sh.Toolbar.Create(this));if(this.showColumns) {var div=this.CreateElement('div');var columns=this.CreateElement('div');var showEvery=10;var i=1;while(i<=150) {if(i%showEvery==0) {div.innerHTML+=i;i+=(i+'').length;} else {div.innerHTML+='·';i++;}} columns.className='columns';columns.appendChild(div);this.bar.appendChild(columns);} for(var i=0,lineIndex=this.firstLine;i0;i++) {if(Trim(lines[i]).length==0) continue;var matches=regex.exec(lines[i]);if(matches!=null&&matches.length>0) min=Math.min(matches[0].length,min);} if(min>0) for(var i=0;i

ページビューの合計