質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.31%
Android

Androidは、Google社が開発したスマートフォンやタブレットなど携帯端末向けのプラットフォームです。 カーネル・ミドルウェア・ユーザーインターフェイス・ウェブブラウザ・電話帳などのアプリケーションやソフトウェアをひとつにまとめて構成。 カーネル・ライブラリ・ランタイムはほとんどがC言語/C++、アプリケーションなどはJavaSEのサブセットとAndroid環境で書かれています。

正規表現

正規表現とは特定の文字列によるパターンマッチングを行う際に用いられる宣言型プログラミングです。

Kotlin

Kotlinは、ジェットブレインズ社のアンドリー・ブレスラフ、ドミトリー・ジェメロフが開発した、 静的型付けのオブジェクト指向プログラミング言語です。

Q&A

解決済

1回答

777閲覧

AndroidでHtml.toHtml/fromHtmlを使用時に改行が削除されてしまう

bluvenz

総合スコア22

Android

Androidは、Google社が開発したスマートフォンやタブレットなど携帯端末向けのプラットフォームです。 カーネル・ミドルウェア・ユーザーインターフェイス・ウェブブラウザ・電話帳などのアプリケーションやソフトウェアをひとつにまとめて構成。 カーネル・ライブラリ・ランタイムはほとんどがC言語/C++、アプリケーションなどはJavaSEのサブセットとAndroid環境で書かれています。

正規表現

正規表現とは特定の文字列によるパターンマッチングを行う際に用いられる宣言型プログラミングです。

Kotlin

Kotlinは、ジェットブレインズ社のアンドリー・ブレスラフ、ドミトリー・ジェメロフが開発した、 静的型付けのオブジェクト指向プログラミング言語です。

0グッド

0クリップ

投稿2023/03/01 06:36

編集2023/03/01 09:18

実現したいこと

・EditTextに入力した文字を任意で装飾しRealmに保存
・その後、保存したデータを読み込み、EditTextに正しく表示を行いたいと考えています

前提

入力した文字を範囲選択し、ボタンを押下することで文字の装飾(文字色、ボールド 等)を行うことができるメモ帳のようなアプリを作成しています。

発生している問題・エラーメッセージ

前提に記載したアプリを実現するため、入力された文字(装飾含む)をHTML化してRealmに保存。
次回起動時にRealmからデータを読み込み再表示を行ったのですが、一部の改行が削除されてしまいました。

具体的には、

1 2 3 4 5 6

1 2 3 4 5 6

となってしまいます。

該当のソースコード

kotlin

1val button = findViewById<Button>(R.id.button) 2button.setOnClickListener { 3 val editTest1 = findViewById<EditText>(R.id.editText1) 4 val editTest2 = findViewById<EditText>(R.id.editText2) 5 6 val htmlString = Html.toHtml(editTest1.text, Html.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE) 7 8 val htmlSpanned = Html.fromHtml(htmlString, Html.FROM_HTML_MODE_COMPACT) 9 editTest2.setText(htmlSpanned) 10}
※6行目:editTest1.textの値 1\n2\n\n3\n\n\n4\n\n\n\n5\n\n\n\n\n6 ※6行目:htmlStringの値 <p dir="ltr">&#65297;<br> &#65298;</p> <p dir="ltr">&#65299;<br></p> <p dir="ltr">&#65300;<br><br></p> <p dir="ltr">&#65301;<br><br><br></p> <p dir="ltr">&#65302;</p> ※8行目:htmlSpannedの値 1\n2\n3\n4\n\n5\n\n\n6\n

試したこと

「改行が2個以上なら改行コードを2個追加すれば良い」という記載をしているサイトがあったので正規表現で置換しようとしましたが上手くいきませんでした。

補足情報

そもそもの書き方が悪いのか、正規表現で2個追加するという力業が正しいのか、何も分からない状況で恐縮なのですが、解決方法がわかる方がいればご教示お願いいたします。

正しい書き方、正規表現での対応方法、どちらでも構いません。
どうぞよろしくお願いいたします。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

jimbe

2023/03/01 08:17

どのようなテキストがどのような html になったのでしょうか。 改行やタグが見えるようにご提示ください。
bluvenz

2023/03/01 09:26

hoshi-takanori様 やはりそうなんですね…。 記載いただいたURL内にあるように自力で書くしかないのかもしれませんが、使用する全ての装飾(タグ)を全て書くのは辛く、他に何か良い方法は思いつかないでしょうか…。 jimbe様 いつもありがとうございます。情報が不足しており申し訳ありません。 ご指摘の通り追記いたしましたのでご確認いただけますと幸いです。
hoshi-takanori

2023/03/01 10:12

あれ? br タグがちゃんとある。ってことは fromHtml の問題か…。
jimbe

2023/03/01 20:52 編集

(回答に移しました)
guest

回答1

0

ベストアンサー

まず、 toHtml と fromHtml はお互いを実行することで完全に元に戻ることが保証されたモノでは無いことは理解しておく必要があるでしょう。
その上で、公式のドキュメント https://developer.android.com/reference/android/text/Html#FROM_HTML_MODE_COMPACT
の記述が参考になります。

This inverts the Spanned to HTML string conversion done with the option TO_HTML_PARAGRAPH_LINES_INDIVIDUAL.
これは、 Spannedオプションで行われた HTML 文字列への変換を 逆にしますTO_HTML_PARAGRAPH_LINES_INDIVIDUAL。(Google 翻訳)

つまり、追加頂いたデータで改行が ほぼ 再現されるフラグの組み合わせは、
Html.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL
Html.FROM_HTML_MODE_COMPACT
と思われます。

ただ、 realm で保存・再生する為に HTMLを介するのが正しいのかという点は再考する余地はあるように感じます。


操作できず表示の確認だけですが、 json を用いるようにしてみました。
Span 毎に処理を行う必要があるため、 サンプルとして AbsoluteSizeSpan と ForegroundColorSpan だけに対応です。

java

1import androidx.appcompat.app.AppCompatActivity; 2 3import android.graphics.Color; 4import android.os.*; 5import android.text.*; 6import android.text.style.*; 7import android.util.Log; 8import android.widget.*; 9 10import com.google.gson.Gson; 11import com.google.gson.annotations.SerializedName; 12 13import java.lang.reflect.*; 14import java.util.regex.*; 15 16class EditedText { 17 private static final class SpanObject { 18 private static class What { 19 private static final class Param { 20 final String type, value; 21 Param(int value) { 22 type = int.class.getCanonicalName(); 23 this.value = "" + value; 24 } 25 Param(boolean value) { 26 type = boolean.class.getCanonicalName(); 27 this.value = "" + value; 28 } 29 Class<?> getType() { 30 switch(type) { 31 case "int": return int.class; 32 case "boolean": return boolean.class; 33 default: return String.class; 34 } 35 } 36 Object getValue() { 37 switch(type) { 38 case "int": return Integer.parseInt(value); 39 case "boolean": return Boolean.parseBoolean(value); 40 default: return value; 41 } 42 } 43 @Override 44 public String toString() { return "[type="+type+",value="+value+"]"; } 45 } 46 @SerializedName("class") 47 String className; 48 Param[] params; 49 What(Class<?> clazz, int size) { 50 className = clazz.getCanonicalName(); 51 params = new Param[size]; 52 } 53 What(AbsoluteSizeSpan span) { 54 this(span.getClass(), 2); 55 params[0] = new Param(span.getSize()); 56 params[1] = new Param(span.getDip()); 57 } 58 What(ForegroundColorSpan span) { 59 this(span.getClass(), 1); 60 params[0] = new Param(span.getForegroundColor()); 61 } 62 Object generate() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException { 63 Class<?>[] paramTypes = new Class[params.length]; 64 Object[] initargs = new Object[params.length]; 65 for(int i=0; i<params.length; i++) { 66 paramTypes[i] = params[i].getType(); 67 initargs[i] = params[i].getValue(); 68 } 69 return Class.forName(className).getConstructor(paramTypes).newInstance(initargs); 70 } 71 @Override 72 public String toString() { 73 StringBuilder sb = new StringBuilder("["); 74 sb.append("class=").append(className); 75 sb.append(",params=["); 76 for(int i=0; i<params.length; i++) sb.append(i > 0 ? "," : "").append(params[i]); 77 sb.append("]"); 78 return sb.append("]").toString(); 79 } 80 } 81 @SerializedName("object") 82 What what; 83 int start, end, flags; 84 85 @Override 86 public String toString() { 87 StringBuilder sb = new StringBuilder("["); 88 sb.append("what=").append(what); 89 sb.append(",start=").append(start); 90 sb.append(",end=").append(end); 91 sb.append(",flags=").append(flags); 92 return sb.append("]").toString(); 93 } 94 } 95 private String text; 96 private SpanObject[] spans; 97 98 EditedText(Spanned spanned) { 99 text = spanned.toString(); 100 Object[] spans = spanned.getSpans(0, spanned.length(), Object.class); 101 this.spans = new SpanObject[spans.length]; 102 for(int i=0; i<spans.length; i++) { 103 SpanObject span = new SpanObject(); 104 if(spans[i] instanceof AbsoluteSizeSpan) { 105 span.what = new SpanObject.What((AbsoluteSizeSpan)spans[i]); 106 } else if(spans[i] instanceof ForegroundColorSpan) { 107 span.what = new SpanObject.What((ForegroundColorSpan)spans[i]); 108 } 109 span.start = spanned.getSpanStart(spans[i]); 110 span.end = spanned.getSpanEnd(spans[i]); 111 span.flags = spanned.getSpanFlags(spans[i]); 112 this.spans[i] = span; 113 } 114 } 115 116 Spanned toSpanned() throws ClassNotFoundException, InvocationTargetException, NoSuchMethodException, IllegalAccessException, InstantiationException { 117 SpannableStringBuilder ssb = new SpannableStringBuilder(text); 118 for(SpanObject span : spans) ssb.setSpan(span.what.generate(), span.start, span.end, span.flags); 119 return ssb; 120 } 121 122 @Override 123 public String toString() { 124 StringBuilder sb = new StringBuilder("["); 125 sb.append("text=").append(text); 126 sb.append(",spans=["); 127 for(int i=0; i<spans.length; i++) sb.append(i > 0 ? "," : "").append(spans[i]); 128 sb.append("]"); 129 return sb.append("]").toString(); 130 } 131} 132 133public class MainActivity extends AppCompatActivity { 134 @Override 135 protected void onCreate(Bundle savedInstanceState) { 136 super.onCreate(savedInstanceState); 137 setContentView(R.layout.activity_main); 138 139 EditText editText = findViewById(R.id.editText); 140 TextView textView = findViewById(R.id.textView); 141 142 SpannableStringBuilder ssb = new SpannableStringBuilder("ACDEFG"); 143 ssb.setSpan(new AbsoluteSizeSpan(50, true), 2, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 144 ssb.setSpan(new ForegroundColorSpan(Color.RED), 3, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 145 //editText.setText("1\n2\n\n3\n\n\n4\n\n\n\n5\n\n\n\n\n6"); 146 editText.setText(ssb); 147 148 EditedText editedText = new EditedText(ssb); 149 Log.d("*** toString ***", editedText.toString()); 150 151 Gson gson = new Gson(); 152 String json = gson.toJson(editedText); 153 Log.d("*** toJson ***", json); 154 155 EditedText newEdtedText = gson.fromJson(json, EditedText.class); 156 try { 157 textView.setText(newEdtedText.toSpanned()); 158 } catch(ClassNotFoundException| 159 InvocationTargetException| 160 NoSuchMethodException| 161 IllegalAccessException| 162 InstantiationException e) { 163 e.printStackTrace(); 164 } 165 } 166 167 //コントロールコードを文字化 168 private String toText(Spanned htmlSpanned) { 169 StringBuilder sb = new StringBuilder(); 170 Pattern p = Pattern.compile("[\u0000-\u001F]"); 171 int last = 0; 172 for(Matcher m=p.matcher(htmlSpanned); m.find(last); last=m.end()) { 173 sb.append(htmlSpanned.subSequence(last, m.start())); 174 char c = m.group().charAt(0); 175 sb.append("^").append((char)(c + '@')); 176 } 177 sb.append(htmlSpanned.subSequence(last, htmlSpanned.length())); 178 return sb.toString(); 179 } 180}

res/layout/activity_main.xml

xml

1<?xml version="1.0" encoding="utf-8"?> 2<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 xmlns:app="http://schemas.android.com/apk/res-auto" 4 xmlns:tools="http://schemas.android.com/tools" 5 android:layout_width="match_parent" 6 android:layout_height="match_parent" 7 tools:context=".MainActivity"> 8 9 <EditText 10 android:id="@+id/editText" 11 android:layout_width="0dp" 12 android:layout_height="0dp" 13 android:textSize="30dp" 14 app:layout_constraintBottom_toTopOf="@id/textView" 15 app:layout_constraintEnd_toEndOf="parent" 16 app:layout_constraintStart_toStartOf="parent" 17 app:layout_constraintTop_toTopOf="parent" /> 18 <TextView 19 android:id="@+id/textView" 20 android:layout_width="0dp" 21 android:layout_height="0dp" 22 android:textSize="30dp" 23 android:background="#e0e0e0" 24 app:layout_constraintBottom_toBottomOf="parent" 25 app:layout_constraintEnd_toEndOf="parent" 26 app:layout_constraintStart_toStartOf="parent" 27 app:layout_constraintTop_toBottomOf="@id/editText" /> 28</androidx.constraintlayout.widget.ConstraintLayout>

エミュレータ実行結果
エミュレータ実行結果画面
実行時ログ

plain

1D/*** toString ***: [text=ACDEFG,spans=[[what=[class=android.text.style.AbsoluteSizeSpan,params=[[type=int,value=50],[type=boolean,value=true]]],start=2,end=4,flags=33],[what=[class=android.text.style.ForegroundColorSpan,params=[[type=int,value=-65536]]],start=3,end=5,flags=33]]] 2D/*** toJson ***: {"spans":[{"end":4,"flags":33,"start":2,"object":{"class":"android.text.style.AbsoluteSizeSpan","params":[{"type":"int","value":"50"},{"type":"boolean","value":"true"}]}},{"end":5,"flags":33,"start":3,"object":{"class":"android.text.style.ForegroundColorSpan","params":[{"type":"int","value":"-65536"}]}}],"text":"ACDEFG"}

投稿2023/03/01 20:51

編集2023/03/03 00:12
jimbe

総合スコア13318

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

bluvenz

2023/03/01 21:56

ありがとうございます! ご教示いただいた変更で無事に実現することができました。 公式も見たつもりだったのですが、見逃していたようで反省しています…。 ただ、今度は行頭に半角スペースが1つだけの場合に削除されてしまうことに気が付きました。 jimbe様の仰る通り別の方法を検討した方が良いのでしょうね。 もし思うところがあればご意見いただきたいのですが、こういった場合はHTMLを経由する以外でどういった方法が考えられますでしょうか。
jimbe

2023/03/02 04:35 編集

根本的には、 HTML 自体が "文字列を修飾するモノ" ではありません。 HTML は "文章構造を記述するモノ" で、たまたま(?) 修飾することが出来たために本来の使い方をされなくなってしまっただけです。 問題となっている文字列において、 "PARAGRAPH であるという情報" は無いのにも関わらず <p> タグが使用されている時点で、文字列の意図を HTML 化出来ていないとも言えます。 Spanned/Spannable を調べますと、内部データは String と Span で構成され、別々に取得できることが分かります。 そして、 Span は ParcelableSpan を implements し、 ParcelableSpan はその名の通り Parcelable を継承しているようです。 つまり、 Spanned/Spannable 自体は Parcelable ではありませんが、分解すればそれぞれが Serializable/Parcelable ですので"データ化"出来ることになります。 ※注:試していません
jimbe

2023/03/02 11:23

parcial は長期保存には向かないので、 json を用いるコードを作って回答に追加しました。
bluvenz

2023/03/03 06:41

HTMLについて、たまたま修飾ができているという点、納得いたしました。 先ほど書かせていただいた行頭の半角スペース問題などは、まさにHTMLで初心者が悩むような問題ですね。 また、サンプルまで作成いただいて本当にありがとうございます。 ニュアンスは解るのですが、理解しきれていない部分が多くあるため、1つずつ追って確認しようと思います。 多くのお時間を割いていただき感謝してもしきれません。本当にありがとうございました。
jimbe

2023/03/03 09:26

>ニュアンスは解るのですが、理解しきれていない部分が Spanned を EditedText の内部に展開し、 EditedText を json 化することで文字列にしています。 戻す時は逆に json から EditedText を生成して、 EditedText のメソッドで Spanned を生成しています。 スパンはクラス名とコンストラクタパラメータが分かればリフレクションで生成できます(Whatクラスのgenerateメソッド)ので、 instanceof でスパンのクラスを調べ(EditedTextクラスのコンストラクタ内のforループ)て、コンストラクタパラメータになるデータを取得してテキストとして保存しています(Whatクラスのコンストラクタ)。 new AbsoluteSizeSpan(50, true); を json で { "class":"android.text.style.AbsoluteSizeSpan", "params":[ {"type":"int", "value":"50"}, {"type":"boolean", "value":"true"}] } と表す風にしています。 kotlin 案件なのに java コードで申し訳ないです。 EditedText を別 java ファイルとして、 MainActivity くらいは kotlin で良かったかもしれません。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.31%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問