Created
October 21, 2018 16:59
-
-
Save seka/46b042df4ae8b37b86edce3f5ff83b9a to your computer and use it in GitHub Desktop.
HTML から Android のコンポーネントを作成してみるサンプル
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package jp.example.android.util.html | |
import android.content.Context | |
import android.graphics.Bitmap | |
import android.support.v4.content.ContextCompat | |
import android.text.SpannableString | |
import android.text.SpannableStringBuilder | |
import android.text.Spanned.SPAN_INCLUSIVE_INCLUSIVE | |
import android.text.style.TextAppearanceSpan | |
import android.view.Gravity.CENTER | |
import android.view.View | |
import android.view.ViewGroup.LayoutParams.MATCH_PARENT | |
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT | |
import android.widget.* | |
import android.widget.LinearLayout | |
import android.widget.LinearLayout.VERTICAL | |
import com.airbnb.paris.Paris | |
import com.bumptech.glide.Glide | |
import com.bumptech.glide.load.model.GlideUrl | |
import com.bumptech.glide.load.model.LazyHeaders | |
import life.medley.android.R | |
import life.medley.android.util.image.GlideLoggingListener | |
import okhttp3.HttpUrl | |
import org.jsoup.Jsoup | |
import org.jsoup.nodes.Element | |
class HTMLConverter( | |
private val context: Context, | |
private val html: String | |
) { | |
private val resultView = LinearLayout(context).apply { | |
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) | |
orientation = VERTICAL | |
} | |
fun parse() { | |
val body = Jsoup.parse(html) | |
.normalise() | |
.body() | |
inspect(body, ElementViewHolder()) | |
} | |
fun build() = resultView | |
private fun inspect(parentElement: Element, parent: ElementViewHolder) { | |
var brotherCount = 1 | |
parentElement.children().forEach { | |
val prevElement = it.previousElementSibling() | |
if (prevElement != null && prevElement.tagName() == it.tagName()) { | |
brotherCount++ | |
} else { | |
brotherCount = 1 | |
} | |
val type = ElementType.valueOf(it) | |
val view = when (type) { | |
ElementType.TABLE, | |
ElementType.TABLE_BODY -> parseTable(it) | |
ElementType.TABLE_ROW -> createTableRow() | |
ElementType.TABLE_DATA -> parseTableData(it) | |
ElementType.IMAGE -> parseImage(it) | |
ElementType.HEADER1, | |
ElementType.HEADER2, | |
ElementType.HEADER3, | |
ElementType.HEADER4, | |
ElementType.HEADER5, | |
ElementType.HEADER6 -> parseTitle(it, type) | |
ElementType.DEFINITION_TERM, | |
ElementType.PARAGRAPH -> parseParagraph(it) | |
ElementType.LINK -> parseLink(it) | |
ElementType.UNORDERED_LIST, | |
ElementType.ORDERED_LIST, | |
ElementType.DEFINITION_LIST -> createListView() | |
ElementType.LIST_ITEM, | |
ElementType.DEFINITION_DESCRIPTION -> parseListItem(it, parent, brotherCount) | |
else -> null | |
} | |
val children = it.children() | |
if (children.isNotEmpty()) { | |
val holder = ElementViewHolder(view, type, parent.depth + 1) | |
if (type == parent.type) { | |
holder.orderString = parent.orderString + "." + brotherCount | |
} else { | |
holder.orderString = brotherCount.toString() | |
} | |
inspect(it, holder) | |
} | |
// View が存在しないか、parent に合成できた場合は view に追加する必用がない | |
if (view == null || compose(ElementViewHolder(view, ElementType.valueOf(it)), parent)) { | |
return@forEach | |
} | |
if (type == ElementType.TABLE || type == ElementType.TABLE_BODY) { | |
val innerView = LinearLayout(context).apply { | |
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) | |
orientation = LinearLayout.HORIZONTAL | |
addView(view) | |
} | |
val scrollView = HorizontalScrollView(context).apply { | |
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) | |
addView(innerView) | |
} | |
resultView.addView(scrollView) | |
return@forEach | |
} | |
resultView.addView(view, MATCH_PARENT, WRAP_CONTENT) | |
} | |
} | |
private fun parseTable(el: Element): View? { | |
// 自分が table で小要素に tbody が合った場合は tbody で探索を行う必要があるため、探索を打ち切る | |
val child = el.children() | |
.map { ElementType.valueOf(it) } | |
.firstOrNull { it == ElementType.TABLE_BODY } | |
if (child !== null) { | |
return null | |
} | |
return TableLayout(context).apply { | |
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) | |
} | |
} | |
private fun createListView(): View? { | |
return TextView(context).apply { | |
Paris.style(this).apply(R.style.article_item) | |
} | |
} | |
private fun createTableRow(): View? { | |
return TableRow(context).apply { | |
gravity = CENTER | |
} | |
} | |
private fun parseImage(el: Element): View? { | |
val url = HttpUrl.parse(el.absUrl("src")) ?: return null | |
return ImageView(context).apply { | |
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) | |
Paris.style(this).apply(R.style.article_item) | |
val headers = LazyHeaders.Builder() | |
.addHeader("Content-Type", "image/bmp") | |
.build() | |
val glideUrl = GlideUrl(url.url(), headers) | |
Glide.with(context) | |
.asBitmap() | |
.load(glideUrl) | |
.listener(GlideLoggingListener<Bitmap>()) | |
.into(this) | |
} | |
} | |
private fun parseTitle(el: Element, type: ElementType): View? { | |
return TextView(context).apply { | |
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) | |
if (type == ElementType.HEADER2) { | |
background = ContextCompat.getDrawable(context, R.drawable.sub_title) | |
} | |
Paris.style(this).apply(R.style.subtitle) | |
text = el.text() | |
} | |
} | |
private fun parseParagraph(el: Element): View? { | |
// 空行の区切りに使用されているものがあるので無視する | |
if (el.text() == " ") { | |
return null | |
} | |
return TextView(context).apply { | |
Paris.style(this).apply(R.style.article_item) | |
text = SpannableString(el.text()) | |
} | |
} | |
private fun parseLink(el: Element): View? { | |
return TextView(context).apply { | |
Paris.style(this).apply(R.style.article_item) | |
text = el.text() | |
} | |
} | |
private fun parseListItem(el: Element, parent: ElementViewHolder, brotherCount: Int): View? { | |
return TextView(context).apply { | |
Paris.style(this).apply(R.style.article_item) | |
val prefix = "\t".repeat(parent.depth - 1) | |
text = when (parent.type) { | |
ElementType.UNORDERED_LIST -> prefix + UNORDERED_LIST_DECORATOR + el.text() | |
ElementType.ORDERED_LIST -> "$prefix$brotherCount. ${el.text()}" | |
ElementType.DEFINITION_LIST -> prefix + el.text() | |
else -> "" | |
} | |
} | |
} | |
private fun parseTableData(el: Element): View? { | |
return TextView(context).apply { | |
Paris.style(this).apply(R.style.table_data) | |
text = el.text() | |
} | |
} | |
private fun compose(self: ElementViewHolder, parent: ElementViewHolder): Boolean { | |
return when (self.type) { | |
ElementType.DEFINITION_TERM, | |
ElementType.PARAGRAPH, | |
ElementType.UNORDERED_LIST, | |
ElementType.ORDERED_LIST, | |
ElementType.DEFINITION_LIST, | |
ElementType.LIST_ITEM, | |
ElementType.DEFINITION_DESCRIPTION, | |
ElementType.TABLE_DATA -> composeText(self.view as TextView, parent) | |
ElementType.LINK -> composeLink(self.view as TextView, parent) | |
ElementType.TABLE_ROW -> composeTableRow(self.view as TableRow, parent) | |
else -> false | |
} | |
} | |
private fun composeText(self: TextView, parent: ElementViewHolder): Boolean { | |
when (parent.type) { | |
ElementType.DEFINITION_TERM, | |
ElementType.PARAGRAPH, | |
ElementType.UNORDERED_LIST, | |
ElementType.ORDERED_LIST, | |
ElementType.DEFINITION_LIST, | |
ElementType.LIST_ITEM, | |
ElementType.DEFINITION_DESCRIPTION -> { | |
val parentTextView = parent.view as TextView | |
parentTextView.apply { | |
text = String.format("%s\n%s", parentTextView.text.toString(), self.text.trim()) | |
} | |
} | |
ElementType.TABLE_DATA -> return true | |
ElementType.TABLE_ROW -> { | |
val tableRow = parent.view as TableRow | |
tableRow.addView(self) | |
} | |
else -> return false | |
} | |
return true | |
} | |
private fun composeLink(self: TextView, parent: ElementViewHolder): Boolean { | |
when (parent.type) { | |
ElementType.DEFINITION_TERM, | |
ElementType.PARAGRAPH, | |
ElementType.UNORDERED_LIST, | |
ElementType.ORDERED_LIST, | |
ElementType.DEFINITION_LIST, | |
ElementType.LIST_ITEM, | |
ElementType.DEFINITION_DESCRIPTION, | |
ElementType.TABLE_DATA -> { | |
val parentTextView = parent.view as TextView | |
val builder = SpannableStringBuilder(parentTextView.text) | |
Regex(self.text.toString()).findAll(parentTextView.text).forEach { | |
val style = TextAppearanceSpan(context, R.color.link) | |
builder.setSpan(style, it.range.start, it.range.last + 1, SPAN_INCLUSIVE_INCLUSIVE) | |
} | |
parentTextView.text = builder | |
} | |
ElementType.TABLE_ROW -> { | |
val castParentView = parent.view as TableRow | |
val builder = SpannableStringBuilder(self.text).apply { | |
val style = TextAppearanceSpan(context, R.color.link) | |
setSpan(style, 0, self.text.length - 1, SPAN_INCLUSIVE_INCLUSIVE) | |
} | |
self.text = builder | |
castParentView.addView(self) | |
} | |
else -> return false | |
} | |
return true | |
} | |
private fun composeTableRow(self: TableRow, parent: ElementViewHolder): Boolean { | |
val table = parent.view as? TableLayout ?: return false | |
table.addView(self) | |
return true | |
} | |
companion object { | |
private const val UNORDERED_LIST_DECORATOR = "・" | |
} | |
private enum class ElementType(val tagName: String, val level: Int = 0) { | |
UNKNOWN(""), | |
TABLE("table"), | |
TABLE_BODY("tbody"), | |
TABLE_ROW("tr"), | |
TABLE_DATA("td"), | |
IMAGE("img"), | |
HEADER1("h1", 1), | |
HEADER2("h2", 2), | |
HEADER3("h3", 3), | |
HEADER4("h4", 4), | |
HEADER5("h5", 5), | |
HEADER6("h6", 6), | |
PARAGRAPH("p"), | |
LINK("a"), | |
UNORDERED_LIST("ul"), | |
ORDERED_LIST("ol"), | |
LIST_ITEM("li"), | |
DEFINITION_LIST("dl"), | |
DEFINITION_TERM("dt"), | |
DEFINITION_DESCRIPTION("dd"); | |
companion object { | |
fun valueOf(element: Element) = values().firstOrNull { it.tagName === element.tagName() } ?: UNKNOWN | |
} | |
} | |
private data class ElementViewHolder( | |
var view: View? = null, | |
var type: ElementType = ElementType.UNKNOWN, | |
var depth: Int = 1, | |
var orderString: String = "" | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment