Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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