LoginSignup
3
4

More than 5 years have passed since last update.

TextViewでHTMLのLIタグを表示しようとして深い泥沼に足を突っ込んでしまったお話(事件編)

Last updated at Posted at 2015-07-03

基本事項

AndroidでHTMLファイルを表示する場合,はじめに考えるのがWebViewだと思います.
しかし,結構表示が重いし,CSSなどは使っていない場合はTextViewで表示ができます.
Activityで

TextView textView = new TextView(this);
String text = "<h1>Test</h1><h2>A</h2><p>a</p><h2>B</h2><p>b</p>";
textView.setText(Html.fromHtml(text));

などと書けば,簡単なHTMLソースであれば容易にTextViewに整形されたテキストを表示することが可能です.

応用

しかし,このHtml.fromHtmlが解析できるタグは本当に少数で,br, p, div, em, b, strong, cite, dfn, i, big, small, font, blockquote, tt, monospace, a, u, sup, sub, imgのみだそうです.この時困るのが,割とよく使うol, liやul, liのセットではないでしょうか.調べても結構出てきます.
それで,どのようにして箇条書きを表示すれば良いか,というのはこちらのGistに載っています.
ただし,このGist,若干間違えており,多分そのままではolやulの子にol, ulを持っていると落ちます.なので私の実装を置いておきます.なお,ulは使わなかったので消しました.もし使いたい場合はhandleTag内にolとほぼ同等の(ただし追加する文字は箇条書きの点)処理をすれば利用可能です.

FixedHtmlTagHandler.java
import java.util.Stack;
import org.xml.sax.XMLReader;
import android.text.Editable;
import android.text.Html;
import android.text.style.LeadingMarginSpan;

/**
 * Don't think. Feel.
 */
public class FixedHtmlTagHandler implements Html.TagHandler {
    private Stack<Integer> mListItemCounts = new Stack<Integer>();
    private Stack<Integer> mListParents = new Stack<Integer>();
    private int mTextWidth;
    public FixedHtmlTagHandler(int textWidth){
        mTextWidth = textWidth;
    }

    public FixedHtmlTagHandler(){
        mTextWidth = 25;
    }

    @Override
    public void handleTag(final boolean opening, final String tag, Editable output, final XMLReader xmlReader) {

        if (tag.equals("ol")) {
            if (opening) {
                if(output.length() < 1 || output.charAt(output.length()-1)!='\n'){
                    output.append('\n');
                }
                if(mListParents.size()>0){
                    int start = mListParents.pop(), end = output.length();
                    LeadingMarginSpan span = new CustomizedMarginSpan(mListParents.size() * 2 * mTextWidth, (mListParents.size() * 2 + 3) * mTextWidth);
                    output.setSpan(span, start, end, 0);
                    mListParents.push(null);
                }
                mListParents.push(output.length());
                mListItemCounts.push(0);
            } else {
                if(output.charAt(output.length()-1)!='\n'){
                    output.append("\n");
                }
                LeadingMarginSpan span = new CustomizedMarginSpan(mListParents.size() * mTextWidth * 2,  (mListParents.size() * 2 + 3) * mTextWidth);
                int start = mListParents.pop(), end = output.length();
                output.setSpan(span, start, end, 0);
                if(mListParents.size() > 0) {
                    mListParents.pop();
                    mListParents.push(output.length());
                }
                mListItemCounts.pop();
            }
        } else if (tag.equals("li")) {
            if(opening) {
                handleListTag(output);
            } else {
                output.append("\n");
            }
        }
    }

    private void handleListTag(Editable output) {
        int itemCount = mListItemCounts.pop()+1;
        String appendText = itemCount + ". ";
        output.append(appendText);
        mListItemCounts.push(itemCount);
    }
}

ただこれも問題があり,ol内にliが10個以上来るとズレます.処理が面倒なのでやめました.(するのであればliごとにLeadingMarginSpanを設定する必要があります)

LeadingMarginSpanについて

まず,LeadingMarginSpanについての日本語のドキュメントがあまりないようですが,要はインデントを付けるときに使います.
どういうことかというと,例えば
image
という構造にしたいとき,タイトル以外の行はインデントを付けたい,ということがありますよね?そのようなときに使います.

LeadingMarginSpan span = new LeadingMarginSpan.Standard(75);
editable.setSpan(span, start, end, 0);

のようにすることで簡単に75ピクセルのインデントがstart, endの間に作成されます.
ここで気をつけたいのが,start-1, end-1文字目は,start, endが先頭か終端の場合を除いて改行文字でないといけないことです.ここが原因で,先ほどのGistのコードは落ちています.また,次のような

  1. あいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえお
  2. かきくけこかきくけこかきくけこかきくけこかきくけこかきくけこかきくけこかきくけこかきくけこかきくけこかきくけこかきくけこかきくけこかきくけこかきくけこかきくけこかきくけこかきくけこかきくけこかきくけこ

という箇条書きがあったとしましょう.ここで,1.や2.が表示されている行と,右側で折り返してきた後の行の開始位置が異なることがわかると思います.この1行目はインデントをつけず,2行目には3文字分のインデントを付けたい,という場合

LeadingMarginSpan span = new LeadingMarginSpan.Standard(0, 75);
editable.setSpan(span, start, end, 0);

とすることで最初の行はインデントを付けず,2行目以降は75ピクセルのインデントが付けられます.
では,次の場合はどうしましょう.

  1. あいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえお
    • おえういあおえういあおえういあおえういあおえういあおえういあおえういあおえういあおえういあおえういあおえういあおえういあおえういあおえういあおえういあおえういあおえういあおえういあおえういあおえういあおえういあ
  2. かきくけこかきくけこかきくけこかきくけこかきくけこかきくけこかきくけこかきくけこかきくけこかきくけこかきくけこかきくけこかきくけこかきくけこかきくけこかきくけこかきくけこかきくけこかきくけこかきくけこ
    • こけくきかこけくきかこけくきかこけくきかこけくきかこけくきかこけくきかこけくきかこけくきかこけくきかこけくきかこけくきかこけくきかこけくきかこけくきかこけくきかこけくきかこけくきかこけくきかこけくきかこけくきか

この場合,ulが始まる前に一旦その前までにインデントを付け,その後にulの部分にインデントを付けるなどするなどする必要があります.今回の話とはそれますので,詳しくは私の上のコードを御覧ください.

ここまで読んで,実際に実装してくださった方はもうお気づきかもしれませんが,折り返す際,右端がオーバーして切れる行があります.
ここが深い泥沼と呼んだ点です.
実は,Android 2.3までは正しく表示できていました(実機で確認したわけではありませんが,できていたはずです).
原因は,この折り返しを実装しているStaticLayoutです.
189行目のfirstWidthLineLimitが438行目でデクリメントされ,0以下になった時,190, 201行目で設定した2行目以降の幅(restWidth)になります.しかし,なぜこのような実装をしたのか全く見当がつきませんが,firstWidthLineLimitの定義時にmLineCountが加えられているせいで (mLineCountは637行目でインクリメントされますが,恐らく段落数を記録していると思われます),それまでの段落数+1行目にfirstWidth(最初の行の幅),それ以降にrestWidthが適用されるようになっています.
つまり,2段落目にこのインデントを適用するのであれば,3行目から正しい幅が適用されるため,2行目がはみ出します.正直意味がわかりません.
この実装はどうもAndroid 3.0の頃に加えられたようで,Android 2.3.7までのStaticLayoutでは正しく動いていそうなことが確認できます.

つまりGoogleのせいで,しかも一時的にAndroidがクローズドソースになっていた時期のせいか,バグがそのまま放置されているんですね.実装を変えたということは,何か意味があったのかもしれませんが,ここの部分を何とかしてほしいと思います.

まとめ

LeadingMarginSpanは闇

追記

解決策を書き忘れていました.
このStaticLayoutはTextViewの中で呼び出されるDynamicLayoutの中で呼び出されているので,解決するにはStaticLayout以外にTextViewとDynamicLayoutの再実装が必要です.そんなこと私はしたくないです.

なんとか解決しました.
TextViewでHTMLのLIタグを表示しようとして深い泥沼に足を突っ込んでしまったお話(解決編)

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4