Search code examples
androidhtmlregexbbcode

Handling spoiler BBcode Android


I'm developing an application for a forum.

I need to get a spoiler button like the Tapatalk's one

enter image description here

that hides a text portion, that shows only when the user clicks on the button.

I'm getting all the text of the post, including the spoiler BBCode. I've managed to get the content of the spoiler with the following regex:

\[SPOILER\](.+?)\[\/SPOILER\]

My problem is that I want to add a spoiler button but all my text is HTML-like, as in every object (images, links, formatting code, etc...) is translated in HTML, then handled by Android with the method Html.fromHtml().

Here's the parsing method, that "translates" the BBCode into HTML:

private static String parsePostContent(String text){
    String html = text;

    Map<String,String> bbMap = new HashMap<>();

    bbMap.put("(\r\n|\r|\n|\n\r)", "<br/>");
    bbMap.put("\\[b\\](.+?)\\[/b\\]", "<strong>$1</strong>");
    bbMap.put("\\[i\\](.+?)\\[/i\\]", "<span style='font-style:italic;'>$1</span>");
    bbMap.put("\\[u\\](.+?)\\[/u\\]", "<span style='text-decoration:underline;'>$1</span>");
    bbMap.put("\\[h1\\](.+?)\\[/h1\\]", "<h1>$1</h1>");
    bbMap.put("\\[h2\\](.+?)\\[/h2\\]", "<h2>$1</h2>");
    bbMap.put("\\[h3\\](.+?)\\[/h3\\]", "<h3>$1</h3>");
    bbMap.put("\\[h4\\](.+?)\\[/h4\\]", "<h4>$1</h4>");
    bbMap.put("\\[h5\\](.+?)\\[/h5\\]", "<h5>$1</h5>");
    bbMap.put("\\[h6\\](.+?)\\[/h6\\]", "<h6>$1</h6>");
    bbMap.put("\\[quote\\](.+?)\\[/quote\\]", "<blockquote>$1</blockquote>");
    bbMap.put("(?s)^\\[quote name=\"([^\"]+)\".*\\](.+)\\[\\/quote\\]", "<span style='font-style:italic;'>Citazione di: $1</span> <blockquote>$2</blockquote>");
    bbMap.put("\\[p\\](.+?)\\[/p\\]", "<p>$1</p>");
    bbMap.put("\\[p=(.+?),(.+?)\\](.+?)\\[/p\\]", "<p style='text-indent:$1px;line-height:$2%;'>$3</p>");
    bbMap.put("\\[center\\](.+?)\\[/center\\]", "<div align='center'>$1");
    bbMap.put("\\[align=(.+?)\\](.+?)\\[/align\\]", "<div align='$1'>$2");
    bbMap.put("\\[color=(.+?)\\](.+?)\\[/color\\]", "<span style='color:$1;'>$2</span>");
    bbMap.put("\\[size=(.+?)\\](.+?)\\[/size\\]", "<span style='font-size:$1;'>$2</span>");
    bbMap.put("\\[img\\](.+?)\\[/img\\]", "<img src='$1' />");
    bbMap.put("\\[img=(.+?),(.+?)\\](.+?)\\[/img\\]", "<img width='$1' height='$2' src='$3' />");
    bbMap.put("\\[email\\](.+?)\\[/email\\]", "<a href='mailto:$1'>$1</a>");
    bbMap.put("\\[email=(.+?)\\](.+?)\\[/email\\]", "<a href='mailto:$1'>$2</a>");
    bbMap.put("\\[url\\](.+?)\\[/url\\]", "<a href='$1'>$1</a>");
    bbMap.put("\\[url=(.+?)\\](.+?)\\[/url\\]", "<a href='$1'>$2</a>");
    bbMap.put("\\[youtube\\](.+?)\\[/youtube\\]", "<object width='640' height='380'><param name='movie' value='http://www.youtube.com/v/$1'></param><embed src='http://www.youtube.com/v/$1' type='application/x-shockwave-flash' width='640' height='380'></embed></object>");
    bbMap.put("\\[video\\](.+?)\\[/video\\]", "<video src='$1' />");
    bbMap.put("\\[SPOILER\\](.+?)\\[\\/SPOILER\\]", "$1");

    for (Map.Entry entry: bbMap.entrySet()) {
        html = html.replaceAll(entry.getKey().toString(), entry.getValue().toString());
    }

    return html;
}

Note that the SPOILER row is there just for testing purposes.

Then the string is set in the TextView in the Adapter class:

postText.setText(Html.fromHtml(post.getPostText()));

The post has this layout:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
    android:id="@+id/postAuthor"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentLeft="true"
    android:layout_alignParentStart="true"
    android:layout_alignParentTop="true"
    android:layout_marginLeft="10dp"
    android:layout_marginTop="10dp"
    android:text="New Text"
    android:textStyle="bold" />

<TextView
    android:id="@+id/postDate"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignLeft="@+id/postAuthor"
    android:layout_alignStart="@+id/postAuthor"
    android:layout_below="@+id/postAuthor"
    android:layout_marginTop="5dp"
    android:text="New Text"
    android:textStyle="italic" />

<TextView
    android:id="@+id/postText"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignLeft="@+id/postDate"
    android:layout_alignStart="@+id/postDate"
    android:layout_below="@+id/postDate"
    android:layout_marginBottom="15dp"
    android:layout_marginTop="20dp"
    android:autoLink="web"
    android:linksClickable="true"
    android:text="New Text" />
</RelativeLayout>

Every post object is placed in a ListView.

My question is: how can I create a similar layout, even with HTML or some sort of external library?

Thanks.


Solution

  • After hours, code versions and some curses, I've managed to get this straight. Here's the final code, but I think it still need some more fixes to be perfect. (The post containing the SPOILER button will disapper if the user will scroll to the bottom of the list.)

            String[] textArray = postText.split("<spoiler>");
            Spanned spoilerText;
    
            String preSpoilerText = "";
            String postSpoilerText = "";
    
            List<String> noSpoilerTextList = new ArrayList<>();
            for (int i = index - 1; i < textArray.length; i += 2) {
                if (!textArray[i].equals("</p>")) {
                    String temp = textArray[i].replace("\n", "");
                    temp = temp.replace("[/SPOILER]", "");
                    if (!temp.equals("")) {
                        noSpoilerTextList.add(temp);
                    }
                }
            }
            List<String> spoilerNameList = new ArrayList<>();
            List<String> spoilerTextList = new ArrayList<>();
            for (int i = index; i < textArray.length; i += 2) {
                if (!textArray[i].contains("</p>") || !textArray[i].equals("")) {
                    String temp = textArray[i].replace("\n", "");
                    temp = temp.replace("[SPOILER]", "");
                    if (!temp.equals("")) {
                        if (temp.contains("<name>")) {
                            String[] spoilerTextArray = temp.split("<name>");
                            spoilerNameList.add(spoilerTextArray[1]);
                            spoilerTextList.add(spoilerTextArray[2]);
                        } else {
                            spoilerNameList.add("SPOILER");
                            spoilerTextList.add(temp);
                        }
                    }
                }
            }
    
            if (noSpoilerTextList.size() == spoilerTextList.size()) {
                noSpoilerTextList.add(new String(""));
            }
    
            for (int i = 0; i < spoilerTextList.size(); i++) {
                int buttonHeight = 0;
                float metrics = context.getResources().getDisplayMetrics().density;
                if (metrics == 3.0) {
                    Log.i("DYSPLAYSIZE: ", "xxhdpi");
                    buttonHeight = 140;
                } else if (metrics == 2.0) {
                    Log.i("DYSPLAYSIZE: ", "xhdpi");
                    buttonHeight = 120;
                } else if (metrics == 1.5) {
                    Log.i("DYSPLAYSIZE: ", "hdpi");
                    buttonHeight = 100;
                }
                final TextView spoilerTextView = new TextView(context);
                TextView preSpoilerTextView = new TextView(context);
                TextView postSpoilerTextView = new TextView(context);
                spoilerTextView.setLinksClickable(true);
                preSpoilerTextView.setLinksClickable(true);
                postSpoilerTextView.setLinksClickable(true);
                spoilerTextView.setAutoLinkMask(Linkify.WEB_URLS);
                preSpoilerTextView.setAutoLinkMask(Linkify.WEB_URLS);
                postSpoilerTextView.setAutoLinkMask(Linkify.WEB_URLS);
                Button spoilerButton = new Button(context);
                spoilerButton.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, buttonHeight));
                spoilerText = Html.fromHtml(spoilerTextList.get(i), new Html.ImageGetter() {
                    @Override
                    public Drawable getDrawable(String source) {
                        LevelListDrawable d = new LevelListDrawable();
                        Drawable empty = context.getResources().getDrawable(R.drawable.abc_ab_share_pack_mtrl_alpha);
                        d.addLevel(0, 0, empty);
                        d.setBounds(0, 0, empty.getIntrinsicWidth(), empty.getIntrinsicHeight());
                        new ImageGetterAsyncTask(context, source, d).execute(spoilerTextView);
                        return d;
                    }
                }, null);
                spoilerTextView.setText(spoilerText);
                spoilerTextView.setTypeface(null, Typeface.ITALIC);
                spoilerTextView.setVisibility(View.GONE);
                LinearLayout.LayoutParams spoilerMargins = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
                spoilerMargins.setMargins(50, 5, 15, 15);
                final boolean[] visible = {false};
                spoilerButton.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if (!visible[0]) {
                            spoilerTextView.setVisibility(View.VISIBLE);
                            visible[0] = true;
                        } else {
                            spoilerTextView.setVisibility(View.GONE);
                            visible[0] = false;
                        }
                    }
                });
    
                if (index == 1) {
                    preSpoilerText = noSpoilerTextList.get(0);
                    postSpoilerText = noSpoilerTextList.get(1);
                    preSpoilerTextView.setText(Html.fromHtml(preSpoilerText));
                    postSpoilerTextView.setText(Html.fromHtml(postSpoilerText));
                    postContent.addView(preSpoilerTextView);
                } else {
                    if (i != spoilerTextList.size() - 1) {
                        postSpoilerText = noSpoilerTextList.get(i + 1);
                        postSpoilerTextView.setText(Html.fromHtml(postSpoilerText));
                    } else {
                        if (spoilerTextList.size() != noSpoilerTextList.size()) {
                            postSpoilerText = noSpoilerTextList.get(i + 1);
                        } else {
                            postSpoilerText = noSpoilerTextList.get(i);
                        }
                        postSpoilerTextView.setText(Html.fromHtml(postSpoilerText));
                    }
    
                }
                spoilerButton.setText(spoilerNameList.get(i));
                postContent.addView(spoilerButton);
                postContent.addView(spoilerTextView, spoilerMargins);
                postContent.addView(postSpoilerTextView);