Skip to content

Instantly share code, notes, and snippets.

@seka
Created October 21, 2018 16:59
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save seka/46b042df4ae8b37b86edce3f5ff83b9a to your computer and use it in GitHub Desktop.
Save seka/46b042df4ae8b37b86edce3f5ff83b9a to your computer and use it in GitHub Desktop.
HTML から Android のコンポーネントを作成してみるサンプル
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() == "&nbsp;") {
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