public static InternalAsyncEmojiRect New <T, U>(T parentTextElement, Char[] chars) where T : LTAsyncElement, ILayoutableText where U : IMissingSpriteCache, new() { var emojiOrMarkStr = new string(chars); var go = parentTextElement.GenerateGO(emojiOrMarkStr); // TMProのレイアウトをするためには、ここでCanvasに乗っている親要素の上に載せるしかない。 go.transform.SetParent(parentTextElement.transform, false); var emojiRect = go.AddComponent <InternalAsyncEmojiRect>(); // 文字をセットする場所としてRectTransformを取得、レイアウトのために高さに無限値をセット var rectTrans = go.GetComponent <RectTransform>(); // フォント情報を取得するためにT型をセットし、そこからTMProのcomponentを取り出す。そこに絵文字をセットし、絵文字画像を得る。 var textComponent = go.GetComponent <TextMeshProUGUI>(); textComponent.enableWordWrapping = true; textComponent.text = emojiOrMarkStr; rectTrans.sizeDelta = Vector2.positiveInfinity; var textInfos = textComponent.GetTextInfo(emojiOrMarkStr); textComponent.enableWordWrapping = false; var lines = textInfos.lineCount; if (lines != 1) { throw new Exception("unsupported emoji/mark pattern."); } var lineInfo = textInfos.lineInfo[0]; var lineWidth = lineInfo.length; var lineHeight = lineInfo.lineHeight; // サイズの更新 var size = new Vector2(lineWidth, lineHeight); // 絵文字/記号部分を左上アンカーにすると、高さがNanにならずに絵文字/記号スプライトが表示される。 if (0 < rectTrans.childCount) { var emojiRectTrans = rectTrans.GetChild(0).GetComponent <RectTransform>(); emojiRectTrans.pivot = new Vector2(0, 1); emojiRectTrans.anchorMin = new Vector2(0, 1); emojiRectTrans.anchorMax = new Vector2(0, 1); emojiRectTrans.anchoredPosition = Vector2.zero; } // この文字オブジェクト自体の位置、サイズを規定する rectTrans.anchoredPosition = Vector2.zero; rectTrans.sizeDelta = size; // サイズを一旦TMProの情報をもとに決定する emojiRect.Size = size; var(isExist, codePoint) = TextLayoutDefinitions.TMPro_ChechIfEmojiOrMarkExist(emojiOrMarkStr); if (isExist) { // 最低一つ要素が作られているはずなので、そのSptiteの位置情報をレイアウト後に合致するように調整する。 if (rectTrans.childCount == 1) { var emojiRectTrans = rectTrans.GetChild(0).GetComponent <RectTransform>(); emojiRectTrans.pivot = new Vector2(0, 1); emojiRectTrans.anchorMin = new Vector2(0, 1); emojiRectTrans.anchorMax = new Vector2(0, 1); emojiRectTrans.anchoredPosition = Vector2.zero; } else { Debug.LogWarning("絵文字かマークがある状態だが、このcodePointの文字を示すspriteがロードされない codePoint:" + codePoint); } } else { // ローディングフラグを立てる emojiRect.IsLoading = true; /* * ポイント数 * フォント名 * 表示幅 * 表示高さ * コードポイント */ var fontName = textComponent.font.name; var fontSize = textComponent.fontSize; var requestWidth = size.x; var requestHeight = size.y; var cacheInstance = InternalCachePool.Get <U>(); cacheInstance.LoadMissingEmoji( fontName, fontSize, requestWidth, requestHeight, codePoint, cor => { emojiRect.StartCoroutine(cor); }, data => { emojiRect.IsLoading = false; var spr = Sprite.Create(data, new Rect(0, 0, data.width, data.height), Vector2.zero); // サイズを更新 rectTrans.sizeDelta = new Vector2(data.width, data.height); // tmProのコンポーネントを排除する(子供があるとそれを描画しようとしてエラーが出る) GameObject.Destroy(textComponent); if (0 < rectTrans.childCount) { // TMProの文字(カラ)が置いてあるコンポーネントを削除する var emojiChild = rectTrans.GetChild(0); GameObject.Destroy(emojiChild.gameObject); } // スプライトを作って入れる var spriteObject = new GameObject("sprite"); var spriteComponent = spriteObject.AddComponent <Image>(); var childRectTrans = spriteComponent.GetComponent <RectTransform>(); childRectTrans.pivot = new Vector2(0, 1); childRectTrans.anchorMin = new Vector2(0, 1); childRectTrans.anchorMax = new Vector2(0, 1); spriteComponent.transform.SetParent(rectTrans, false); spriteComponent.sprite = spr; spriteComponent.SetNativeSize(); // 決定後のサイズを入力する emojiRect.Size = rectTrans.sizeDelta; }, () => { emojiRect.IsLoading = false; } ); } return(emojiRect); }
public static void TextLayout <T>(T textElement, string contentText, RectTransform rectTrans, float viewWidth, ref float originX, ref float originY, ref float restWidth, ref float currentLineMaxHeight, ref List <RectTransform> lineContents, ref Vector2 wrappedSize) where T : LTElement, ILayoutableText { // 文字列が空な場合、何もせずに返す。 if (string.IsNullOrEmpty(contentText)) { return; } Debug.Assert(rectTrans.pivot.x == 0 && rectTrans.pivot.y == 1 && rectTrans.anchorMin.x == 0 && rectTrans.anchorMin.y == 1 && rectTrans.anchorMax.x == 0 && rectTrans.anchorMax.y == 1, "rectTransform for BasicLayoutFunctions.TextLayout should set pivot to 0,1 and anchorMin 0,1 anchorMax 0,1."); Debug.Assert(textElement.transform.childCount == 0, "BasicLayoutFunctions.TextLayout not allows text element which has child."); var continueContent = false; NextLine: var textComponent = textElement.GetComponent <TextMeshProUGUI>(); TMPro.TMP_TextInfo textInfos = null; { // wordWrappingを可能にすると、表示はともかく実際にこの行にどれだけの文字が入っているか判断できる。 textComponent.enableWordWrapping = true; textComponent.text = contentText; if (0 < textComponent.transform.childCount) { for (var i = 0; i < textComponent.transform.childCount; i++) { var childRectTrans = textComponent.transform.GetChild(i).GetComponent <RectTransform>(); childRectTrans.pivot = new Vector2(0, 1); childRectTrans.anchorMin = new Vector2(0, 1); childRectTrans.anchorMax = new Vector2(0, 1); childRectTrans.anchoredPosition = Vector2.zero; } } // 文字が入る箱のサイズを縦に無限にして、どの程度入るかのレイアウト情報を取得する。 textComponent.rectTransform.sizeDelta = new Vector2(restWidth, Screen.height); textInfos = textComponent.GetTextInfo(contentText); // 文字を中央揃えではなく適正な位置にセットするためにラッピングを解除する。 textComponent.enableWordWrapping = false; } // TODO: 直す // 絵文字が含まれている場合、画像と文字に分けてレイアウトを行う。 if (IsContainsSurrogatePairOrSprite(contentText)) { textComponent.text = string.Empty; // TMProが子オブジェクトを作っている場合があり、それらがあれば消す必要がある。 // 同じフレーム内で同じ絵文字を作るようなことをする場合、作成されないケースがあるため、子供の有無を条件として絵文字の有無を判断することはできなかった。 for (var i = 0; i < textComponent.transform.childCount; i++) { GameObject.Destroy(textComponent.transform.GetChild(i).gameObject); } // 絵文字が含まれている。 // 今後のレイアウトに自分自身を巻き込まないように、レイアウトから自分自身を取り外す lineContents.RemoveAt(lineContents.Count - 1); textComponent.rectTransform.sizeDelta = new Vector2(restWidth, 0);// 高さが0で問題ない。 // この内部で全てのレイアウトを終わらせる。 LayoutContentWithEmoji(textElement, contentText, viewWidth, ref originX, ref originY, ref restWidth, ref currentLineMaxHeight, ref lineContents, ref wrappedSize); return; } var tmGeneratorLines = textInfos.lineInfo; var lineSpacing = textComponent.lineSpacing; var tmLineCount = textInfos.lineCount; var firstLine = tmGeneratorLines[0]; var currentFirstLineWidth = firstLine.length; var currentFirstLineHeight = firstLine.lineHeight; var isHeadOfLine = originX == 0; var isMultiLined = 1 < tmLineCount; /* * 予定している1行目の文字の幅が予定幅を超えている = オーバーフローしている場合、次のケースがある * ・文字列の1行目の末尾がたまたま幅予算を超えてレイアウトされた * * この場合、溢れたケースとして文字列の長さを調整してレイアウトを行う。 */ var isTextOverflow = (viewWidth < originX + currentFirstLineWidth); // TMProで末尾がwhitespaceで終わっている場合、正しい幅を出せていなく、そのくせ改行設定などは正しい。 // 幅がわかる文字を用意し、スペース文字 + 文字をつくり、コンテンツの幅から文字の幅を引けば、スペースの幅が出せる。 // それを1単位として、末尾にあるスペースの幅 x 個数をやれば、このコンテンツの正しい幅が出せる。 if (Char.IsWhiteSpace(contentText[firstLine.lastCharacterIndex])) { // 行末の、whitespaceかそれに該当する非表示な要素の個数 var numEndWhiteSpaceCount = firstLine.lastCharacterIndex - firstLine.lastVisibleCharacterIndex; // サンプリングする対象を今回の文字列の末尾から取得する。 // このため、幅が異なるwhitespace扱いのものが混じっていても、正しい長さを返せない。 var samplingWhiteSpace = contentText[firstLine.lastCharacterIndex]; // 一時退避する var sourceText = textComponent.text; // 0の文字の幅を取得するためにtextComponentにセット、現在のフォントでの0の幅を計測する。 textComponent.text = "0"; var widthOf0 = textComponent.preferredWidth; // whitespace + 0の文字の幅を取得するためにtextComponentにセット、現在のフォントでのws + 0の幅を計測する。 textComponent.text = samplingWhiteSpace + "0"; // 差 = whitespaceの幅を取得する。 var singleWidthOfWhiteSpace = textComponent.preferredWidth - widthOf0; // 退避したテキストを戻す textComponent.text = sourceText; // 現在のコンテンツのX起点 + whitespaceが含まれない幅 + whitespace幅 * 個数 を足し、この行の正しい幅を出す。 var totalWidthWithSpaces = originX + currentFirstLineWidth + (singleWidthOfWhiteSpace * numEndWhiteSpaceCount); // 画面の幅と比較し、小さい方をとる。 // これは、whitespaceを使ってレイアウトした場合、TMProがリクエスト幅を超えたぶんのwhitespaceの計算をサボって、whitespaceではない文字が来た時点でその文字を頭とする改行を行うため。 // 最大でも画面幅になるようにする。 var maxWidth = Mathf.Min(viewWidth, totalWidthWithSpaces); // 行送りが発生しているため、この行の値の幅の更新はもう起きない。そのため、ここでwrappedSize.xを更新する。 wrappedSize.x = Mathf.Max(wrappedSize.x, maxWidth); } var status = TextLayoutDefinitions.GetTextLayoutStatus(isHeadOfLine, isMultiLined, isTextOverflow); switch (status) { case TextLayoutStatus.NotHeadAndSingle: case TextLayoutStatus.HeadAndSingle: { // 全文を表示して終了 textComponent.text = contentText; if (0 < textComponent.transform.childCount) { for (var i = 0; i < textComponent.transform.childCount; i++) { var childRectTrans = textComponent.transform.GetChild(i).GetComponent <RectTransform>(); childRectTrans.pivot = new Vector2(0, 1); childRectTrans.anchorMin = new Vector2(0, 1); childRectTrans.anchorMax = new Vector2(0, 1); childRectTrans.anchoredPosition = Vector2.zero; } } if (continueContent) { continueContent = false; restWidth = viewWidth - currentFirstLineWidth; currentLineMaxHeight = currentFirstLineHeight; } else { textComponent.rectTransform.anchoredPosition = new Vector2(originX, originY); } textComponent.rectTransform.sizeDelta = new Vector2(currentFirstLineWidth, currentFirstLineHeight); ContinueLine(ref originX, originY, currentFirstLineWidth, currentFirstLineHeight, ref currentLineMaxHeight, ref wrappedSize); break; } case TextLayoutStatus.NotHeadAndMulti: { // textComponent.text = "<indent=" + originX + "pixels>" でindentを付けられるが、これの恩恵を素直に受けられるケースが少ない。 // この行をレイアウトした際、行頭の文字のレイアウトが変わるようであれば、利用できない。 // 最適化としてはケースが複雑なので後回しにする。 // これは、この行 + 追加のHead ~ 系の複数のコンテンツに分割する。 var nextLineTextIndex = tmGeneratorLines[1].firstCharacterIndex; var nextLineText = contentText.Substring(nextLineTextIndex); // 現在の行のセット textComponent.text = contentText.Substring(0, nextLineTextIndex); if (0 < textComponent.transform.childCount) { for (var i = 0; i < textComponent.transform.childCount; i++) { var childRectTrans = textComponent.transform.GetChild(i).GetComponent <RectTransform>(); childRectTrans.pivot = new Vector2(0, 1); childRectTrans.anchorMin = new Vector2(0, 1); childRectTrans.anchorMax = new Vector2(0, 1); childRectTrans.anchoredPosition = Vector2.zero; } } textComponent.rectTransform.anchoredPosition = new Vector2(originX, originY); textComponent.rectTransform.sizeDelta = new Vector2(currentFirstLineWidth, currentFirstLineHeight); var childOriginX = originX; var currentTotalLineHeight = LineFeed <LTRootElement>(ref originX, ref originY, currentFirstLineHeight, ref currentLineMaxHeight, ref lineContents, ref wrappedSize); // 文字コンテンツの高さ分改行する // 次の行のコンテンツをこのコンテンツの子として生成するが、レイアウトまでを行わず次の行の起点の計算を行う。 // ここで全てを計算しない理由は、この処理の結果、複数種類のレイアウトが発生するため、ここで全てを書かない方が変えやすい。 { // 末尾でgotoを使って次の行頭からのコンテンツの設置に行くので、計算に使う残り幅をビュー幅へとセットする。 restWidth = viewWidth; // 次の行のコンテンツを入れる var nextLineTextElement = textElement.GenerateGO(nextLineText).GetComponent <T>(); nextLineTextElement.transform.SetParent(textElement.transform, false); // 消しやすくするため、この新規コンテンツを子にする // xは-に、yは親の直下に置く。yは特に、「親が親の行上でどのように配置されたのか」を加味する必要がある。 // 例えば親行の中で親が最大の背の高さのコンテンツでない場合、改行すべき値は 親の背 + (行の背 - 親の背)/2 になる。 var yPosFromLinedParentY = -( // 下向きがマイナスな座標系なのでマイナス currentFirstLineHeight // 現在の文字の高さと行の高さを比較し、文字の高さ + 上側の差の高さ(差/2)を足して返す。 + ( currentTotalLineHeight - currentFirstLineHeight ) / 2 ); // X表示位置を原点にずらす、Yは次のコンテンツの開始Y位置 = LineFeedで変更された親の位置に依存し、親の位置からoriginYを引いた値になる。 var newTailTextElementRectTrans = nextLineTextElement.GetComponent <RectTransform>(); newTailTextElementRectTrans.anchoredPosition = new Vector2(-childOriginX, yPosFromLinedParentY); // テキスト特有の継続したコンテンツ扱いする。 continueContent = true; // 生成したコンテンツを次の行の要素へと追加する lineContents.Add(newTailTextElementRectTrans); // 上書きを行う textElement = nextLineTextElement; contentText = nextLineText; goto NextLine; } } case TextLayoutStatus.HeadAndMulti: { // このコンテンツは矩形で、行揃えの影響を受けないため、明示的に行から取り除く。 lineContents.RemoveAt(lineContents.Count - 1); var lineCount = textInfos.lineCount; var lineStrs = new string[lineCount]; var totalHeight = 0f; for (var j = 0; j < lineCount; j++) { var lInfo = textInfos.lineInfo[j]; totalHeight += lInfo.lineHeight; // ここで、含まれている絵文字の数がわかれば、そのぶんを後ろに足すことで文字切れを回避できそう、、ではある、、 lineStrs[j] = contentText.Substring(lInfo.firstCharacterIndex, lInfo.lastCharacterIndex - lInfo.firstCharacterIndex + 1); } // 矩形になるテキスト = 最終行を取り除いたテキストを得る var rectTexts = new string[lineStrs.Length - 1]; for (var i = 0; i < rectTexts.Length; i++) { rectTexts[i] = lineStrs[i]; } // 最終行のテキストを得る var lastLineText = lineStrs[lineStrs.Length - 1]; // rectの高さの取得 var lastLineHeight = textInfos.lineInfo[lineCount - 1].lineHeight; var rectHeight = totalHeight - lastLineHeight; // 改行コードを入れ、複数行での表示をいい感じにする。 textComponent.text = string.Join("\n", rectTexts); if (0 < textComponent.transform.childCount) { for (var i = 0; i < textComponent.transform.childCount; i++) { var childRectTrans = textComponent.transform.GetChild(i).GetComponent <RectTransform>(); childRectTrans.pivot = new Vector2(0, 1); childRectTrans.anchorMin = new Vector2(0, 1); childRectTrans.anchorMax = new Vector2(0, 1); childRectTrans.anchoredPosition = Vector2.zero; } } textComponent.rectTransform.sizeDelta = new Vector2(restWidth, rectHeight); // 幅の最大値を取得 var max = Mathf.Max(textComponent.rectTransform.sizeDelta.x, textComponent.preferredWidth); // wrappedな幅の更新 wrappedSize.x = Mathf.Max(wrappedSize.x, max); // なんらかの続きの文字コンテンツである場合、そのコンテンツの子になっているので位置情報を調整しない。最終行を分割する。 if (continueContent) { // 別のコンテンツから継続している行はじめの処理なので、子をセットする前にここまでの分の改行を行う。 LineFeed <LTRootElement>(ref originX, ref originY, rectHeight, ref currentLineMaxHeight, ref lineContents, ref wrappedSize); // 末尾でgotoを使って次の行頭からのコンテンツの設置に行くので、計算に使う残り幅をビュー幅へとセットする。 restWidth = viewWidth; // 最終行のコンテンツを入れる var nextLineTextElement = textElement.GenerateGO(lastLineText).GetComponent <T>(); nextLineTextElement.transform.SetParent(textElement.transform.parent, false); // 消しやすくするため、この新規コンテンツを現在の要素の親の子にする // 次の行の行頭になる = 続いている要素と同じxを持つ var childX = textComponent.rectTransform.anchoredPosition.x; // yは親の分移動する var childY = textComponent.rectTransform.anchoredPosition.y - rectHeight; var newBrotherTailTextElementRectTrans = nextLineTextElement.GetComponent <RectTransform>(); newBrotherTailTextElementRectTrans.anchoredPosition = new Vector2(childX, childY); // 継続させる continueContent = true; // 生成したコンテンツを次の行の要素へと追加する lineContents.Add(newBrotherTailTextElementRectTrans); // 上書きを行う textElement = nextLineTextElement; contentText = lastLineText; goto NextLine; } // 誰かの子ではないので、独自に自分の位置をセットする textComponent.rectTransform.anchoredPosition = new Vector2(originX, originY); // 最終行のコンテンツを入れる var newTextElement = textElement.GenerateGO(lastLineText).GetComponent <T>(); newTextElement.transform.SetParent(rectTrans, false); // 消しやすくするため、この新規コンテンツを現在の要素の子にする // 残りの行のサイズは最大化する restWidth = viewWidth; // 次の行の行頭になる originX = 0; // yは親の分移動する originY -= rectHeight; { // 新規オブジェクトはそのy位置を親コンテンツの高さを加えた値にセットする。 var newTailTextElementRectTrans = newTextElement.GetComponent <RectTransform>(); newTailTextElementRectTrans.anchoredPosition = new Vector2(originX, -rectHeight); // 残りのデータをテキスト特有の継続したコンテンツ扱いする。 continueContent = true; // 生成したコンテンツを次の行の要素へと追加する lineContents.Add(newTailTextElementRectTrans); textElement = newTextElement; contentText = lastLineText; goto NextLine; } } case TextLayoutStatus.OutOfViewAndSingle: { // 1行ぶんのみのコンテンツで、行末が溢れている。入るように要素を切り、残りを継続してHeadAndSingleとして処理する。 // 超過している幅を収めるために、1文字ぶんだけ引いた文字を使ってレイアウトを行う。 var index = contentText.Length - 1; var firstLineText = contentText.Substring(0, index); var restText = contentText.Substring(index); // 現在の行のセット textComponent.text = firstLineText; if (0 < textComponent.transform.childCount) { for (var i = 0; i < textComponent.transform.childCount; i++) { var childRectTrans = textComponent.transform.GetChild(i).GetComponent <RectTransform>(); childRectTrans.pivot = new Vector2(0, 1); childRectTrans.anchorMin = new Vector2(0, 1); childRectTrans.anchorMax = new Vector2(0, 1); childRectTrans.anchoredPosition = Vector2.zero; } } textComponent.rectTransform.anchoredPosition = new Vector2(originX, originY); textComponent.rectTransform.sizeDelta = new Vector2(currentFirstLineWidth, currentFirstLineHeight); var childOriginX = originX; var currentTotalLineHeight = LineFeed <LTRootElement>(ref originX, ref originY, currentFirstLineHeight, ref currentLineMaxHeight, ref lineContents, ref wrappedSize); // 文字コンテンツの高さ分改行する // 次の行のコンテンツをこのコンテンツの子として生成するが、レイアウトまでを行わず次の行の起点の計算を行う。 // ここで全てを計算しない理由は、この処理の結果、複数種類のレイアウトが発生するため、ここで全てを書かない方が変えやすい。 { // 末尾でgotoを使って次の行頭からのコンテンツの設置に行くので、計算に使う残り幅をビュー幅へとセットする。 restWidth = viewWidth; // 次の行のコンテンツを入れる var nextLineTextElement = textElement.GenerateGO(restText).GetComponent <T>(); nextLineTextElement.transform.SetParent(textElement.transform, false); // 消しやすくするため、この新規コンテンツを子にする // xは-に、yは親の直下に置く。yは特に、「親が親の行上でどのように配置されたのか」を加味する必要がある。 // 例えば親行の中で親が最大の背の高さのコンテンツでない場合、改行すべき値は 親の背 + (行の背 - 親の背)/2 になる。 var yPosFromLinedParentY = -( // 下向きがマイナスな座標系なのでマイナス currentFirstLineHeight // 現在の文字の高さと行の高さを比較し、文字の高さ + 上側の差の高さ(差/2)を足して返す。 + ( currentTotalLineHeight - currentFirstLineHeight ) / 2 ); // X表示位置を原点にずらす、Yは次のコンテンツの開始Y位置 = LineFeed<LTAsyncRootElement>で変更された親の位置に依存し、親の位置からoriginYを引いた値になる。 var newTailTextElementRectTrans = nextLineTextElement.GetComponent <RectTransform>(); newTailTextElementRectTrans.anchoredPosition = new Vector2(-childOriginX, yPosFromLinedParentY); // テキスト特有の継続したコンテンツ扱いする。 continueContent = true; // 生成したコンテンツを次の行の要素へと追加する lineContents.Add(newTailTextElementRectTrans); // 上書きを行う textElement = nextLineTextElement; contentText = restText; goto NextLine; } } case TextLayoutStatus.OutOfViewAndMulti: { // 複数行があるのが確定していて、最初の行の内容が溢れている。入るように要素を切り、残りを継続してHeadAndMultiとして処理する。 // 複数行が既に存在するのが確定しているので、次の行のコンテンツをそのまま取得、分割する。 var nextLineTextIndex = tmGeneratorLines[1].firstCharacterIndex; // 超過している幅を収めるために、1文字ぶんだけ引いた文字を使ってレイアウトを行う。 var index = nextLineTextIndex - 1; var firstLineText = contentText.Substring(0, index); var restText = contentText.Substring(index); // 現在の行のセット textComponent.text = firstLineText; if (0 < textComponent.transform.childCount) { for (var i = 0; i < textComponent.transform.childCount; i++) { var childRectTrans = textComponent.transform.GetChild(i).GetComponent <RectTransform>(); childRectTrans.pivot = new Vector2(0, 1); childRectTrans.anchorMin = new Vector2(0, 1); childRectTrans.anchorMax = new Vector2(0, 1); childRectTrans.anchoredPosition = Vector2.zero; } } textComponent.rectTransform.anchoredPosition = new Vector2(originX, originY); textComponent.rectTransform.sizeDelta = new Vector2(currentFirstLineWidth, currentFirstLineHeight); var childOriginX = originX; var currentTotalLineHeight = LineFeed <LTRootElement>(ref originX, ref originY, currentFirstLineHeight, ref currentLineMaxHeight, ref lineContents, ref wrappedSize); // 文字コンテンツの高さ分改行する // 次の行のコンテンツをこのコンテンツの子として生成するが、レイアウトまでを行わず次の行の起点の計算を行う。 // ここで全てを計算しない理由は、この処理の結果、複数種類のレイアウトが発生するため、ここで全てを書かない方が変えやすい。 { // 末尾でgotoを使って次の行頭からのコンテンツの設置に行くので、計算に使う残り幅をビュー幅へとセットする。 restWidth = viewWidth; // 次の行のコンテンツを入れる var nextLineTextElement = textElement.GenerateGO(restText).GetComponent <T>(); nextLineTextElement.transform.SetParent(textElement.transform, false); // 消しやすくするため、この新規コンテンツを子にする // xは-に、yは親の直下に置く。yは特に、「親が親の行上でどのように配置されたのか」を加味する必要がある。 // 例えば親行の中で親が最大の背の高さのコンテンツでない場合、改行すべき値は 親の背 + (行の背 - 親の背)/2 になる。 var yPosFromLinedParentY = -( // 下向きがマイナスな座標系なのでマイナス currentFirstLineHeight // 現在の文字の高さと行の高さを比較し、文字の高さ + 上側の差の高さ(差/2)を足して返す。 + ( currentTotalLineHeight - currentFirstLineHeight ) / 2 ); // X表示位置を原点にずらす、Yは次のコンテンツの開始Y位置 = LineFeed<LTAsyncRootElement>で変更された親の位置に依存し、親の位置からoriginYを引いた値になる。 var newTailTextElementRectTrans = nextLineTextElement.GetComponent <RectTransform>(); newTailTextElementRectTrans.anchoredPosition = new Vector2(-childOriginX, yPosFromLinedParentY); // テキスト特有の継続したコンテンツ扱いする。 continueContent = true; // 生成したコンテンツを次の行の要素へと追加する lineContents.Add(newTailTextElementRectTrans); // 上書きを行う textElement = nextLineTextElement; contentText = restText; goto NextLine; } } } }
public static void TextLayout(TextElement newTailTextElement, string contentText, RectTransform transform, float viewWidth, ref float originX, ref float originY, ref float restWidth, ref float currentLineMaxHeight, ref List <RectTransform> lineContents) { Debug.Assert(transform.pivot.x == 0 && transform.pivot.y == 1 && transform.anchorMin.x == 0 && transform.anchorMin.y == 1 && transform.anchorMax.x == 0 && transform.anchorMax.y == 1, "rectTransform for LayouTaro should set pivot to 0,1 and anchorMin 0,1 anchorMax 0,1."); var continueContent = false; NextLine: var textComponent = newTailTextElement.GetComponent <TMPro.TextMeshProUGUI>(); // wordWrappingを可能にすると、表示はともかく実際にこの行にどれだけの文字が入っているか判断できる。 textComponent.enableWordWrapping = true; textComponent.text = contentText; textComponent.rectTransform.sizeDelta = new Vector2(restWidth, float.PositiveInfinity); var textInfos = textComponent.GetTextInfo(contentText); var tmGeneratorLines = textInfos.lineInfo; var lineSpacing = textComponent.lineSpacing; var tmLineCount = textInfos.lineCount; var currentFirstLineWidth = tmGeneratorLines[0].length; // 文字を中央揃えではなく適正な位置にセットするためにラッピングを解除する。 textComponent.enableWordWrapping = false; var currentFirstLineHeight = tmGeneratorLines[0].lineHeight; var isHeadOfLine = originX == 0; var isMultiLined = 1 < tmLineCount; var isLayoutedOutOfView = viewWidth < originX + currentFirstLineWidth; var status = TextLayoutDefinitions.GetTextLayoutStatus(isHeadOfLine, isMultiLined, isLayoutedOutOfView); switch (status) { case TextLayoutStatus.NotHeadAndSingle: case TextLayoutStatus.HeadAndSingle: { // 全文を表示して終了 textComponent.text = contentText; if (continueContent) { continueContent = false; restWidth = viewWidth - currentFirstLineWidth; currentLineMaxHeight = currentFirstLineHeight; } else { textComponent.rectTransform.anchoredPosition = new Vector2(originX, originY); } textComponent.rectTransform.sizeDelta = new Vector2(currentFirstLineWidth, currentFirstLineHeight); ElementLayoutFunctions.ContinueLine(ref originX, currentFirstLineWidth, currentFirstLineHeight, ref currentLineMaxHeight); break; } case TextLayoutStatus.NotHeadAndMulti: { // これは、この行 + 追加のHead ~ 系の最大2コンテンツにできる。 var nextLineTextIndex = tmGeneratorLines[1].firstCharacterIndex; var nextLineText = contentText.Substring(nextLineTextIndex); // 現在の行のセット textComponent.text = contentText.Substring(0, nextLineTextIndex); textComponent.rectTransform.anchoredPosition = new Vector2(originX, originY); textComponent.rectTransform.sizeDelta = new Vector2(currentFirstLineWidth, currentFirstLineHeight); var childOriginX = originX; var currentTotalLineHeight = ElementLayoutFunctions.LineFeed(ref originX, ref originY, currentFirstLineHeight, ref currentLineMaxHeight, ref lineContents); // 文字コンテンツの高さ分改行する // 次の行のコンテンツをこのコンテンツの子として生成するが、レイアウトまでを行わず次の行の起点の計算を行う。 // ここで全てを計算しない理由は、この処理の結果、複数種類のレイアウトが発生するため、ここで全てを書かない方が変えやすい。 { // 末尾でgotoを使って次の行頭からのコンテンツの設置に行くので、計算に使う残り幅をビュー幅へとセットする。 restWidth = viewWidth; // 次の行のコンテンツを入れる contentText = nextLineText; newTailTextElement = TextElement.GO(contentText); newTailTextElement.transform.SetParent(transform); // 消しやすくするため、この新規コンテンツを子にする // xは-に、yは親の直下に置く。yは特に、「親が親の行上でどのように配置されたのか」を加味する必要がある。 // 例えば親行の中で親が最大の背の高さのコンテンツでない場合、改行すべき値は 親の背 + (行の背 - 親の背)/2 になる。 var yPosFromLinedParentY = -( // 下向きがマイナスな座標系なのでマイナス currentFirstLineHeight // 現在の文字の高さと行の高さを比較し、文字の高さ + 上側の差の高さ(差/2)を足して返す。 + ( currentTotalLineHeight - currentFirstLineHeight ) / 2 ); // X表示位置を原点にずらす、Yは次のコンテンツの開始Y位置 = LineFeedで変更された親の位置に依存し、親の位置からoriginYを引いた値になる。 var newTailTextElementRectTrans = newTailTextElement.GetComponent <RectTransform>(); newTailTextElementRectTrans.anchoredPosition = new Vector2(-childOriginX, yPosFromLinedParentY); // ここかー // テキスト特有の継続したコンテンツ扱いする。 continueContent = true; // 追加する(次の処理で確実に消されるが、足しておかないと次の行頭複数行で-されるケースがあり詰む) lineContents.Add(newTailTextElementRectTrans); goto NextLine; } } case TextLayoutStatus.HeadAndMulti: { // この形式のコンテンツは明示的に行に追加しない。末行が行中で終わるケースがあるため、追加するとコンテンツ全体が上下してしまう。 lineContents.RemoveAt(lineContents.Count - 1); var lineCount = textInfos.lineCount; var lineStrs = new string[lineCount]; var totalHeight = 0f; // totalHeightを出す + 改行を入れる for (var j = 0; j < lineCount; j++) { var lInfo = textInfos.lineInfo[j]; totalHeight += lInfo.lineHeight; lineStrs[j] = contentText.Substring(lInfo.firstCharacterIndex, lInfo.lastCharacterIndex - lInfo.firstCharacterIndex + 1); } // 改行コードを入れ、複数行での表示をいい感じにする。 textComponent.text = string.Join("\n", lineStrs); textComponent.rectTransform.sizeDelta = new Vector2(restWidth, totalHeight); // 最終行関連のデータを揃える var lastLineWidth = textInfos.lineInfo[lineCount - 1].length; var lastLineHeight = textInfos.lineInfo[lineCount - 1].lineHeight; var contentHeightWithoutLastLine = totalHeight - lastLineHeight; // なんらかの続きの文字コンテンツである場合、そのコンテンツの子になっているので位置情報を調整しない。 if (continueContent) { continueContent = false; } else { textComponent.rectTransform.anchoredPosition = new Vector2(originX, originY); } originX = lastLineWidth; // 最終ラインのx位置を次のコンテンツに使う originY -= contentHeightWithoutLastLine; // Y位置の継続ポイントとして、最終行の高さを引いたコンテンツの高さを足す restWidth = viewWidth - lastLineWidth; // この行に入る残りのコンテンツの幅を初期化する currentLineMaxHeight = lastLineHeight; // 最終行の高さを使ってコンテンツの高さを初期化する break; } case TextLayoutStatus.NotHeadAndOutOfView: { // 次の行にコンテンツを置き、継続する // 現在最後の追加要素である自分自身を取り出し、ここまでの行の要素を整列させる。 lineContents.RemoveAt(lineContents.Count - 1); ElementLayoutFunctions.LineFeed(ref originX, ref originY, currentLineMaxHeight, ref currentLineMaxHeight, ref lineContents); // レイアウト対象のビューサイズを新しい行のものとして更新する restWidth = viewWidth; lineContents.Add(textComponent.rectTransform); goto NextLine; } } }