Sample app, bind list results, search
This commit is contained in:
parent
7e8ed3ea0b
commit
66f77f35fe
@ -1,6 +1,7 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
|
||||
android {
|
||||
|
||||
@ -40,14 +41,23 @@ kapt {
|
||||
}
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
features = ["parcelize"]
|
||||
}
|
||||
|
||||
dependencies {
|
||||
kapt project(':sample-utils:processor')
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
|
||||
implementation project(':markwon-core')
|
||||
|
||||
deps.with {
|
||||
api it['x-recycler-view']
|
||||
api it['adapt']
|
||||
api it['debug']
|
||||
implementation it['x-recycler-view']
|
||||
implementation it['x-cardview']
|
||||
implementation it['x-fragment']
|
||||
implementation it['gson']
|
||||
implementation it['adapt']
|
||||
implementation it['debug']
|
||||
}
|
||||
}
|
||||
|
166
app-sample/src/debug/res/layout/flowlayout_preview.xml
Normal file
166
app-sample/src/debug/res/layout/flowlayout_preview.xml
Normal file
@ -0,0 +1,166 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/content_padding"
|
||||
tools:ignore="MissingDefaultResource,HardcodedText">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Hey!" />
|
||||
|
||||
<io.noties.markwon.app.base.FlowLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="@dimen/content_padding"
|
||||
android:paddingBottom="@dimen/content_padding"
|
||||
app:fl_spacingHorizontal="@dimen/content_padding"
|
||||
app:fl_spacingVertical="4dip">
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_artifact"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_artifact"
|
||||
android:text="ext-latex" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_artifact"
|
||||
android:text="another" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_artifact"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_artifact"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_artifact"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_artifact"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_artifact"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_artifact"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_artifact"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_artifact"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_artifact"
|
||||
android:text="corejksjk sjkdf sdhjf sdjhf sjjksdjkjksd sdjksd hjsd hsdhjhjs shjsdhjhjsdhj sdj dshjs dhjsd sdhj sdjhsd hjsdh sjhd sdhjsd jhsd sdhj sdjhsd hjsd sjdh s" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_artifact"
|
||||
android:text="core" />
|
||||
|
||||
</io.noties.markwon.app.base.FlowLayout>
|
||||
|
||||
<io.noties.markwon.app.base.FlowLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="@dimen/content_padding"
|
||||
android:paddingBottom="@dimen/content_padding"
|
||||
app:fl_spacingHorizontal="@dimen/content_padding"
|
||||
app:fl_spacingVertical="4dip">
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_tag"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_tag"
|
||||
android:text="ext-latex" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_tag"
|
||||
android:text="another" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_tag"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_tag"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_tag"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_tag"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_tag"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_tag"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_tag"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_tag"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_tag"
|
||||
android:text="corejksjk sjkdf sdhjf sdjhf sjjksdjkjksd sdjksd hjsd hsdhjhjs shjsdhjhjsdhj sdj dshjs dhjsd sdhj sdjhsd hjsdh sjhd sdhjsd jhsd sdhj sdjhsd hjsd sjdh s" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_tag"
|
||||
android:text="core" />
|
||||
|
||||
</io.noties.markwon.app.base.FlowLayout>
|
||||
|
||||
</LinearLayout>
|
@ -3,12 +3,20 @@ package io.noties.markwon.app
|
||||
import android.app.Application
|
||||
import io.noties.debug.AndroidLogDebugOutput
|
||||
import io.noties.debug.Debug
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@Suppress("unused")
|
||||
class App : Application() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
Debug.init(AndroidLogDebugOutput(BuildConfig.DEBUG))
|
||||
|
||||
sampleManager = SampleManager(this, Executors.newCachedThreadPool())
|
||||
}
|
||||
|
||||
companion object {
|
||||
lateinit var sampleManager: SampleManager
|
||||
}
|
||||
}
|
@ -1,61 +1,19 @@
|
||||
package io.noties.markwon.app
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import android.view.ViewTreeObserver
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.noties.adapt.Adapt
|
||||
import io.noties.debug.Debug
|
||||
import io.noties.markwon.app.adapt.SampleItem
|
||||
import io.noties.markwon.app.base.SearchBar
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact
|
||||
import android.view.Window
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import io.noties.markwon.app.ui.SampleListFragment
|
||||
|
||||
class MainActivity : Activity() {
|
||||
class MainActivity : FragmentActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
val searchBar: SearchBar = findViewById(R.id.search_bar)
|
||||
searchBar.onSearchListener = {
|
||||
Debug.i("search: '$it'")
|
||||
}
|
||||
|
||||
val recyclerView: RecyclerView = findViewById(R.id.recycler_view)
|
||||
recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
recyclerView.itemAnimator = DefaultItemAnimator()
|
||||
recyclerView.setHasFixedSize(true)
|
||||
recyclerView.clipToPadding = false
|
||||
|
||||
searchBar.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
|
||||
override fun onPreDraw(): Boolean {
|
||||
searchBar.viewTreeObserver.removeOnPreDrawListener(this)
|
||||
recyclerView.setPadding(
|
||||
recyclerView.paddingLeft,
|
||||
recyclerView.paddingTop + searchBar.height,
|
||||
recyclerView.paddingRight,
|
||||
recyclerView.paddingBottom
|
||||
)
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
val adapt = Adapt.create()
|
||||
recyclerView.adapter = adapt
|
||||
|
||||
val list = listOf(
|
||||
MarkwonSampleItem(
|
||||
"first",
|
||||
"1",
|
||||
"Title first",
|
||||
"Description her egoes and goes ang goes, so will it ever stop?",
|
||||
listOf(MarkwonArtifact.CORE, MarkwonArtifact.EDITOR),
|
||||
listOf("first", "second")
|
||||
)
|
||||
)
|
||||
|
||||
adapt.setItems(list.map { SampleItem(it) })
|
||||
if (supportFragmentManager.findFragmentById(Window.ID_ANDROID_CONTENT) == null) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.add(Window.ID_ANDROID_CONTENT, SampleListFragment.init())
|
||||
.commitNowAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
package io.noties.markwon.app
|
||||
|
||||
abstract class MarkwonSample {
|
||||
|
||||
}
|
@ -1,12 +1,15 @@
|
||||
package io.noties.markwon.app
|
||||
|
||||
import android.os.Parcelable
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
data class MarkwonSampleItem(
|
||||
@Parcelize
|
||||
data class Sample(
|
||||
val javaClassName: String,
|
||||
val id: String,
|
||||
val title: String,
|
||||
val description: String,
|
||||
val artifacts: List<MarkwonArtifact>,
|
||||
val tags: List<String>
|
||||
)
|
||||
) : Parcelable
|
@ -0,0 +1,71 @@
|
||||
package io.noties.markwon.app
|
||||
|
||||
import android.content.Context
|
||||
import io.noties.markwon.app.utils.Cancellable
|
||||
import io.noties.markwon.app.utils.SampleUtils
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact
|
||||
import java.util.concurrent.ExecutorService
|
||||
|
||||
class SampleManager(
|
||||
private val context: Context,
|
||||
private val executorService: ExecutorService
|
||||
) {
|
||||
|
||||
private val samples: List<Sample> by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
|
||||
SampleUtils.readSamples(context)
|
||||
}
|
||||
|
||||
fun samples(search: SampleSearch?, callback: (List<Sample>) -> Unit): Cancellable {
|
||||
|
||||
var action: ((List<Sample>) -> Unit)? = callback
|
||||
|
||||
val future = executorService.submit {
|
||||
|
||||
val source = when (search) {
|
||||
is SampleSearch.Artifact -> samples.filter { it.artifacts.contains(search.artifact) }
|
||||
is SampleSearch.Tag -> samples.filter { it.tags.contains(search.tag) }
|
||||
else -> samples.toList() // just copy all
|
||||
}
|
||||
|
||||
val text = search?.text
|
||||
val results = if (text == null) {
|
||||
// no further filtering, just return the full source here
|
||||
source
|
||||
} else {
|
||||
source.filter { filter(it, text) }
|
||||
}
|
||||
|
||||
action?.invoke(results)
|
||||
}
|
||||
|
||||
return object : Cancellable {
|
||||
override val isCancelled: Boolean
|
||||
get() = future.isDone
|
||||
|
||||
override fun cancel() {
|
||||
action = null
|
||||
future.cancel(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if title contains,
|
||||
// if description contains,
|
||||
// if tags contains
|
||||
// if artifacts contains,
|
||||
private fun filter(sample: Sample, text: String): Boolean {
|
||||
return sample.javaClassName.contains(text, true)
|
||||
|| sample.title.contains(text, true)
|
||||
|| sample.description.contains(text, true)
|
||||
|| filterTags(sample.tags, text)
|
||||
|| filterArtifacts(sample.artifacts, text)
|
||||
}
|
||||
|
||||
private fun filterTags(tags: List<String>, text: String): Boolean {
|
||||
return tags.firstOrNull { it.contains(text, true) } != null
|
||||
}
|
||||
|
||||
private fun filterArtifacts(artifacts: List<MarkwonArtifact>, text: String): Boolean {
|
||||
return artifacts.firstOrNull { it.artifactName().contains(text, true) } != null
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package io.noties.markwon.app
|
||||
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact
|
||||
|
||||
sealed class SampleSearch(val text: String?) {
|
||||
class Artifact(text: String?, val artifact: MarkwonArtifact) : SampleSearch(text)
|
||||
class Tag(text: String?, val tag: String) : SampleSearch(text)
|
||||
class All(text: String?) : SampleSearch(text)
|
||||
|
||||
override fun toString(): String {
|
||||
return "SampleSearch(text=$text,type=${javaClass.simpleName})"
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package io.noties.markwon.app.adapt
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import io.noties.adapt.Item
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact
|
||||
|
||||
class ArtifactItem(artifact: MarkwonArtifact): Item<ArtifactItem.Holder>(artifact.name.hashCode().toLong()) {
|
||||
|
||||
override fun createHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
|
||||
return Holder(inflater.inflate(0, parent, false))
|
||||
}
|
||||
|
||||
override fun render(holder: Holder) {
|
||||
}
|
||||
|
||||
class Holder(itemView: View): Item.Holder(itemView) {
|
||||
val textView: TextView = requireView(0)
|
||||
}
|
||||
}
|
@ -1,89 +1,100 @@
|
||||
package io.noties.markwon.app.adapt
|
||||
|
||||
import android.graphics.Color
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.TextPaint
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.text.style.ClickableSpan
|
||||
import android.text.Spanned
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import io.noties.adapt.Item
|
||||
import io.noties.debug.Debug
|
||||
import io.noties.markwon.app.MarkwonSampleItem
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.app.R
|
||||
import io.noties.markwon.app.Sample
|
||||
import io.noties.markwon.app.base.FlowLayout
|
||||
import io.noties.markwon.app.utils.displayName
|
||||
import io.noties.markwon.app.utils.hidden
|
||||
import io.noties.markwon.app.utils.tagDisplayName
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact
|
||||
import io.noties.markwon.utils.NoCopySpannableFactory
|
||||
|
||||
class SampleItem(private val item: MarkwonSampleItem) : Item<SampleItem.Holder>(item.id.hashCode().toLong()) {
|
||||
class SampleItem(
|
||||
private val markwon: Markwon,
|
||||
private val sample: Sample,
|
||||
private val onArtifactClick: (MarkwonArtifact) -> Unit,
|
||||
private val onTagClick: (String) -> Unit,
|
||||
private val onSampleClick: (Sample) -> Unit
|
||||
) : Item<SampleItem.Holder>(sample.id.hashCode().toLong()) {
|
||||
|
||||
var search: String? = null
|
||||
// var search: String? = null
|
||||
|
||||
private val text: Spanned by lazy(LazyThreadSafetyMode.NONE) {
|
||||
markwon.toMarkdown(sample.description)
|
||||
}
|
||||
|
||||
override fun createHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
|
||||
val holder = Holder(inflater.inflate(R.layout.adapt_sample, parent, false))
|
||||
holder.artifactsAndTags.movementMethod = LinkMovementMethod.getInstance()
|
||||
return holder
|
||||
return Holder(inflater.inflate(R.layout.adapt_sample, parent, false)).apply {
|
||||
description.setSpannableFactory(NoCopySpannableFactory.getInstance())
|
||||
}
|
||||
}
|
||||
|
||||
override fun render(holder: Holder) {
|
||||
holder.apply {
|
||||
title.text = item.title
|
||||
description.text = item.description
|
||||
artifactsAndTags.text = buildArtifactsAndTags
|
||||
title.text = sample.title
|
||||
|
||||
val text = this@SampleItem.text
|
||||
if (text.isEmpty()) {
|
||||
description.text = ""
|
||||
description.hidden = true
|
||||
} else {
|
||||
markwon.setParsedMarkdown(description, text)
|
||||
description.hidden = false
|
||||
}
|
||||
|
||||
// there is no need to display the core artifact (it is implicit),
|
||||
// hide if empty (removed core)
|
||||
artifacts.ensure(sample.artifacts.size, R.layout.view_artifact)
|
||||
.zip(sample.artifacts)
|
||||
.forEach { (view, artifact) ->
|
||||
(view as TextView).text = artifact.displayName
|
||||
view.setOnClickListener {
|
||||
onArtifactClick(artifact)
|
||||
}
|
||||
}
|
||||
|
||||
private val buildArtifactsAndTags: CharSequence
|
||||
get() {
|
||||
val builder = SpannableStringBuilder()
|
||||
|
||||
item.artifacts
|
||||
.forEach {
|
||||
val length = builder.length
|
||||
builder.append("\u00a0${it.name}\u00a0")
|
||||
builder.setSpan(ArtifactSpan(it), length, builder.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
tags.ensure(sample.tags.size, R.layout.view_tag)
|
||||
.zip(sample.tags)
|
||||
.forEach { (view, tag) ->
|
||||
(view as TextView).text = tag.tagDisplayName
|
||||
view.setOnClickListener {
|
||||
onTagClick(tag)
|
||||
}
|
||||
}
|
||||
|
||||
if (!builder.isEmpty()) {
|
||||
builder.append("\n")
|
||||
itemView.setOnClickListener {
|
||||
onSampleClick(sample)
|
||||
}
|
||||
|
||||
item.tags
|
||||
.forEach {
|
||||
val length = builder.length
|
||||
builder.append("\u00a0$it\u00a0")
|
||||
builder.setSpan(TagSpan(it), length, builder.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
class Holder(itemView: View) : Item.Holder(itemView) {
|
||||
val title: TextView = requireView(R.id.title)
|
||||
val description: TextView = requireView(R.id.description)
|
||||
val artifactsAndTags: TextView = requireView(R.id.artifacts_and_tags)
|
||||
}
|
||||
|
||||
private class ArtifactSpan(val artifact: MarkwonArtifact) : ClickableSpan() {
|
||||
override fun onClick(widget: View) {
|
||||
Debug.i("clicked artifact: $artifact")
|
||||
}
|
||||
|
||||
override fun updateDrawState(ds: TextPaint) {
|
||||
ds.isUnderlineText = false
|
||||
ds.bgColor = Color.GREEN
|
||||
val artifacts: FlowLayout = requireView(R.id.artifacts)
|
||||
val tags: FlowLayout = requireView(R.id.tags)
|
||||
}
|
||||
}
|
||||
|
||||
private class TagSpan(val tag: String) : ClickableSpan() {
|
||||
override fun onClick(widget: View) {
|
||||
Debug.i("clicked tag: $tag")
|
||||
private fun FlowLayout.ensure(viewsCount: Int, layoutResId: Int): List<View> {
|
||||
if (viewsCount > childCount) {
|
||||
// inflate new views
|
||||
val inflater = LayoutInflater.from(context)
|
||||
for (i in 0 until (viewsCount - childCount)) {
|
||||
addView(inflater.inflate(layoutResId, this, false))
|
||||
}
|
||||
|
||||
override fun updateDrawState(ds: TextPaint) {
|
||||
ds.isUnderlineText = false
|
||||
ds.bgColor = Color.BLUE
|
||||
} else {
|
||||
// return requested vies and GONE the rest
|
||||
for (i in viewsCount until childCount) {
|
||||
getChildAt(i).hidden = true
|
||||
}
|
||||
}
|
||||
return (0 until viewsCount).map { getChildAt(it) }
|
||||
}
|
@ -3,8 +3,25 @@ package io.noties.markwon.app.base
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.ViewGroup
|
||||
import io.noties.markwon.app.R
|
||||
import kotlin.math.max
|
||||
|
||||
class FlowLayout(context: Context, attrs: AttributeSet) : ViewGroup(context, attrs) {
|
||||
|
||||
private val spacingVertical: Int
|
||||
private val spacingHorizontal: Int
|
||||
|
||||
init {
|
||||
val array = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout)
|
||||
try {
|
||||
val spacing = array.getDimensionPixelSize(R.styleable.FlowLayout_fl_spacing, 0)
|
||||
spacingVertical = array.getDimensionPixelSize(R.styleable.FlowLayout_fl_spacingVertical, spacing)
|
||||
spacingHorizontal = array.getDimensionPixelSize(R.styleable.FlowLayout_fl_spacingHorizontal, spacing)
|
||||
} finally {
|
||||
array.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
|
||||
for (i in 0 until childCount) {
|
||||
val child = getChildAt(i)
|
||||
@ -21,7 +38,61 @@ class FlowLayout(context: Context, attrs: AttributeSet) : ViewGroup(context, att
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val width = MeasureSpec.getSize(widthMeasureSpec)
|
||||
|
||||
// we must have width (match_parent or exact dimension)
|
||||
if (width <= 0) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
return
|
||||
}
|
||||
|
||||
val availableWidth = width - paddingLeft - paddingRight
|
||||
|
||||
// child must not exceed our width
|
||||
val childWidthSpec = MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST)
|
||||
|
||||
// we also could enforce flexible height here (instead of exact one)
|
||||
val childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
|
||||
|
||||
var x = 0
|
||||
var y = 0
|
||||
|
||||
var lineHeight = 0
|
||||
|
||||
for (i in 0 until childCount) {
|
||||
val child = getChildAt(i)
|
||||
|
||||
// measure
|
||||
child.measure(childWidthSpec, childHeightSpec)
|
||||
|
||||
val params = child.layoutParams as LayoutParams
|
||||
val measuredWidth = child.measuredWidth
|
||||
|
||||
if (measuredWidth > (availableWidth - x)) {
|
||||
// new line
|
||||
// make next child start at child measure width (starting at x = 0)
|
||||
params.x = 0
|
||||
params.y = y + lineHeight + spacingVertical
|
||||
|
||||
x = measuredWidth + spacingHorizontal
|
||||
// move vertically by max value of child height on this line
|
||||
y += lineHeight + spacingVertical
|
||||
|
||||
lineHeight = child.measuredHeight
|
||||
|
||||
} else {
|
||||
// we fit this line
|
||||
params.x = x
|
||||
params.y = y
|
||||
|
||||
x += measuredWidth + spacingHorizontal
|
||||
lineHeight = max(lineHeight, child.measuredHeight)
|
||||
}
|
||||
}
|
||||
|
||||
val height = y + lineHeight + paddingTop + paddingBottom
|
||||
|
||||
setMeasuredDimension(width, height)
|
||||
}
|
||||
|
||||
override fun generateDefaultLayoutParams(): ViewGroup.LayoutParams {
|
||||
|
@ -61,6 +61,12 @@ class SearchBar(context: Context, attrs: AttributeSet?) : LinearLayout(context,
|
||||
|
||||
clear.setOnClickListener {
|
||||
textField.setText("")
|
||||
// ensure that we have focus when clear is clicked
|
||||
if (!textField.hasFocus()) {
|
||||
textField.requestFocus()
|
||||
// additionally ensure keyboard is showing
|
||||
KeyboardUtils.show(textField)
|
||||
}
|
||||
}
|
||||
|
||||
cancel.setOnClickListener {
|
||||
@ -69,6 +75,10 @@ class SearchBar(context: Context, attrs: AttributeSet?) : LinearLayout(context,
|
||||
}
|
||||
}
|
||||
|
||||
fun search(text: String) {
|
||||
textField.setText(text)
|
||||
}
|
||||
|
||||
private fun textFieldChanged(text: CharSequence) {
|
||||
val isEmpty = text.isEmpty()
|
||||
clear.hidden = isEmpty
|
||||
|
@ -1,6 +1,6 @@
|
||||
package io.noties.markwon.app.samples
|
||||
|
||||
import io.noties.markwon.app.MarkwonSample
|
||||
import io.noties.markwon.app.ui.MarkwonSample
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo
|
||||
|
||||
|
@ -0,0 +1,5 @@
|
||||
package io.noties.markwon.app.ui
|
||||
|
||||
abstract class MarkwonSample {
|
||||
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package io.noties.markwon.app.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import io.noties.markwon.app.Sample
|
||||
|
||||
class SampleFragment : Fragment() {
|
||||
|
||||
companion object {
|
||||
private const val ARG_SAMPLE = "arg.Sample"
|
||||
|
||||
fun init(sample: Sample): SampleFragment {
|
||||
val fragment = SampleFragment()
|
||||
fragment.arguments = Bundle().apply {
|
||||
putParcelable(ARG_SAMPLE, sample)
|
||||
}
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,288 @@
|
||||
package io.noties.markwon.app.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.noties.adapt.Adapt
|
||||
import io.noties.adapt.DiffUtilDataSetChanged
|
||||
import io.noties.debug.Debug
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.app.App
|
||||
import io.noties.markwon.app.R
|
||||
import io.noties.markwon.app.Sample
|
||||
import io.noties.markwon.app.SampleManager
|
||||
import io.noties.markwon.app.SampleSearch
|
||||
import io.noties.markwon.app.adapt.SampleItem
|
||||
import io.noties.markwon.app.base.SearchBar
|
||||
import io.noties.markwon.app.utils.Cancellable
|
||||
import io.noties.markwon.app.utils.displayName
|
||||
import io.noties.markwon.app.utils.onPreDraw
|
||||
import io.noties.markwon.app.utils.recyclerView
|
||||
import io.noties.markwon.app.utils.tagDisplayName
|
||||
import io.noties.markwon.movement.MovementMethodPlugin
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
class SampleListFragment : Fragment() {
|
||||
|
||||
private val adapt: Adapt = Adapt.create(DiffUtilDataSetChanged.create())
|
||||
private lateinit var markwon: Markwon
|
||||
|
||||
private val type: Type by lazy(LazyThreadSafetyMode.NONE) {
|
||||
parseType(arguments!!)
|
||||
}
|
||||
|
||||
private var search: String? = null
|
||||
|
||||
// postpone state restoration
|
||||
private var pendingRecyclerScrollPosition: RecyclerScrollPosition? = null
|
||||
|
||||
private var cancellable: Cancellable? = null
|
||||
|
||||
private val sampleManager: SampleManager
|
||||
get() = App.sampleManager
|
||||
|
||||
override fun onAttach(context: Context?) {
|
||||
super.onAttach(context)
|
||||
|
||||
context?.also {
|
||||
markwon = markwon(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_sample_list, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
initAppBar(view)
|
||||
|
||||
val context = requireContext()
|
||||
|
||||
val searchBar: SearchBar = view.findViewById(R.id.search_bar)
|
||||
searchBar.onSearchListener = {
|
||||
search = it
|
||||
fetch()
|
||||
}
|
||||
|
||||
val recyclerView: RecyclerView = view.findViewById(R.id.recycler_view)
|
||||
recyclerView.layoutManager = LinearLayoutManager(context)
|
||||
recyclerView.itemAnimator = DefaultItemAnimator()
|
||||
recyclerView.setHasFixedSize(true)
|
||||
recyclerView.adapter = adapt
|
||||
|
||||
// additional padding for RecyclerView
|
||||
searchBar.onPreDraw {
|
||||
recyclerView.setPadding(
|
||||
recyclerView.paddingLeft,
|
||||
recyclerView.paddingTop + searchBar.height,
|
||||
recyclerView.paddingRight,
|
||||
recyclerView.paddingBottom
|
||||
)
|
||||
}
|
||||
|
||||
val state: State? = savedInstanceState?.getParcelable(STATE)
|
||||
pendingRecyclerScrollPosition = state?.recyclerScrollPosition
|
||||
if (state?.search != null) {
|
||||
searchBar.search(state.search)
|
||||
} else {
|
||||
fetch()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
val cancellable = this.cancellable
|
||||
if (cancellable != null && !cancellable.isCancelled) {
|
||||
cancellable.cancel()
|
||||
this.cancellable = null
|
||||
}
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
|
||||
val state = State(
|
||||
search,
|
||||
adapt.recyclerView?.scrollPosition
|
||||
)
|
||||
outState.putParcelable(STATE, state)
|
||||
}
|
||||
|
||||
private fun initAppBar(view: View) {
|
||||
val appBar = view.findViewById<View>(R.id.app_bar)
|
||||
|
||||
val appBarIcon: ImageView = appBar.findViewById(R.id.app_bar_icon)
|
||||
val appBarTitle: TextView = appBar.findViewById(R.id.app_bar_title)
|
||||
|
||||
val type = this.type
|
||||
if (type is Type.All) {
|
||||
return
|
||||
}
|
||||
|
||||
appBarIcon.setImageResource(R.drawable.ic_arrow_back_white_24dp)
|
||||
appBarIcon.setOnClickListener {
|
||||
requireActivity().onBackPressed()
|
||||
}
|
||||
|
||||
val (text, background) = when (type) {
|
||||
is Type.Artifact -> Pair(type.artifact.displayName, R.drawable.bg_artifact)
|
||||
is Type.Tag -> Pair(type.tag.tagDisplayName, R.drawable.bg_tag)
|
||||
else -> error("Unexpected type: $type")
|
||||
}
|
||||
|
||||
appBarTitle.text = text
|
||||
appBarTitle.setBackgroundResource(background)
|
||||
}
|
||||
|
||||
private fun bindSamples(samples: List<Sample>) {
|
||||
val items = samples.map {
|
||||
SampleItem(
|
||||
markwon,
|
||||
it,
|
||||
{ artifact -> openArtifact(artifact) },
|
||||
{ tag -> openTag(tag) },
|
||||
{ sample -> openSample(sample) }
|
||||
)
|
||||
}
|
||||
adapt.setItems(items)
|
||||
|
||||
val scrollPosition = pendingRecyclerScrollPosition
|
||||
if (scrollPosition != null) {
|
||||
pendingRecyclerScrollPosition = null
|
||||
val recyclerView = adapt.recyclerView ?: return
|
||||
recyclerView.onPreDraw {
|
||||
(recyclerView.layoutManager as? LinearLayoutManager)
|
||||
?.scrollToPositionWithOffset(scrollPosition.position, scrollPosition.offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openArtifact(artifact: MarkwonArtifact) {
|
||||
Debug.i(artifact)
|
||||
openResultFragment(init(artifact))
|
||||
}
|
||||
|
||||
private fun openTag(tag: String) {
|
||||
Debug.i(tag)
|
||||
openResultFragment(init(tag))
|
||||
}
|
||||
|
||||
private fun openResultFragment(fragment: SampleListFragment) {
|
||||
openFragment(fragment)
|
||||
}
|
||||
|
||||
private fun openSample(sample: Sample) {
|
||||
openFragment(SampleFragment.init(sample))
|
||||
}
|
||||
|
||||
private fun openFragment(fragment: Fragment) {
|
||||
fragmentManager!!.beginTransaction()
|
||||
.setCustomAnimations(R.anim.screen_in, R.anim.screen_out, R.anim.screen_in_pop, R.anim.screen_out_pop)
|
||||
.replace(Window.ID_ANDROID_CONTENT, fragment)
|
||||
.addToBackStack(null)
|
||||
.commitAllowingStateLoss()
|
||||
}
|
||||
|
||||
private fun fetch() {
|
||||
|
||||
val sampleSearch: SampleSearch = when (val type = this.type) {
|
||||
is Type.Artifact -> SampleSearch.Artifact(search, type.artifact)
|
||||
is Type.Tag -> SampleSearch.Tag(search, type.tag)
|
||||
else -> SampleSearch.All(search)
|
||||
}
|
||||
|
||||
// clear current
|
||||
cancellable?.let {
|
||||
if (!it.isCancelled) {
|
||||
it.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
cancellable = sampleManager.samples(sampleSearch) {
|
||||
bindSamples(it)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARG_ARTIFACT = "arg.Artifact"
|
||||
private const val ARG_TAG = "arg.Tag"
|
||||
private const val STATE = "key.State"
|
||||
|
||||
fun init(): SampleListFragment {
|
||||
val fragment = SampleListFragment()
|
||||
fragment.arguments = Bundle()
|
||||
return fragment
|
||||
}
|
||||
|
||||
fun init(artifact: MarkwonArtifact): SampleListFragment {
|
||||
val fragment = SampleListFragment()
|
||||
fragment.arguments = Bundle().apply {
|
||||
putString(ARG_ARTIFACT, artifact.name)
|
||||
}
|
||||
return fragment
|
||||
}
|
||||
|
||||
fun init(tag: String): SampleListFragment {
|
||||
val fragment = SampleListFragment()
|
||||
fragment.arguments = Bundle().apply {
|
||||
putString(ARG_TAG, tag)
|
||||
}
|
||||
return fragment
|
||||
}
|
||||
|
||||
fun markwon(context: Context): Markwon {
|
||||
return Markwon.builder(context)
|
||||
.usePlugin(MovementMethodPlugin.none())
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun parseType(arguments: Bundle): Type {
|
||||
val name = arguments.getString(ARG_ARTIFACT)
|
||||
val tag = arguments.getString(ARG_TAG)
|
||||
return when {
|
||||
name != null -> Type.Artifact(MarkwonArtifact.valueOf(name))
|
||||
tag != null -> Type.Tag(tag)
|
||||
else -> Type.All
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
private data class State(
|
||||
val search: String?,
|
||||
val recyclerScrollPosition: RecyclerScrollPosition?
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
private data class RecyclerScrollPosition(
|
||||
val position: Int,
|
||||
val offset: Int
|
||||
) : Parcelable
|
||||
|
||||
private val RecyclerView.scrollPosition: RecyclerScrollPosition?
|
||||
get() {
|
||||
val holder = findViewHolderForLayoutPosition(0) ?: return null
|
||||
val position = holder.adapterPosition
|
||||
val offset = holder.itemView.top
|
||||
return RecyclerScrollPosition(position, offset)
|
||||
}
|
||||
|
||||
private sealed class Type {
|
||||
class Artifact(val artifact: MarkwonArtifact) : Type()
|
||||
class Tag(val tag: String) : Type()
|
||||
object All : Type()
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package io.noties.markwon.app.utils
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.noties.adapt.Adapt
|
||||
import io.noties.debug.Debug
|
||||
|
||||
val Adapt.recyclerView: RecyclerView?
|
||||
get() {
|
||||
// internally throws if recycler is not present (detached from recyclerView)
|
||||
return try {
|
||||
recyclerView()
|
||||
} catch (t: Throwable) {
|
||||
Debug.e(t)
|
||||
null
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package io.noties.markwon.app.utils
|
||||
|
||||
interface Cancellable {
|
||||
val isCancelled: Boolean
|
||||
|
||||
fun cancel()
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package io.noties.markwon.app.utils;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.List;
|
||||
|
||||
import io.noties.markwon.app.Sample;
|
||||
|
||||
public abstract class SampleUtils {
|
||||
|
||||
@NonNull
|
||||
public static List<Sample> readSamples(@NonNull Context context) {
|
||||
|
||||
final Gson gson = new Gson();
|
||||
|
||||
try (InputStream inputStream = context.getAssets().open("samples.json")) {
|
||||
return gson.fromJson(
|
||||
new InputStreamReader(inputStream),
|
||||
new TypeToken<List<Sample>>() {
|
||||
}.getType()
|
||||
);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private SampleUtils() {
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package io.noties.markwon.app.utils
|
||||
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact
|
||||
|
||||
val MarkwonArtifact.displayName: String
|
||||
get() = "@${artifactName()}"
|
||||
|
||||
val String.tagDisplayName: String
|
||||
get() = "#$this"
|
@ -3,9 +3,24 @@ package io.noties.markwon.app.utils
|
||||
import android.view.View
|
||||
import android.view.View.GONE
|
||||
import android.view.View.VISIBLE
|
||||
import android.view.ViewTreeObserver
|
||||
|
||||
var View.hidden: Boolean
|
||||
get() = visibility == GONE
|
||||
set(value) {
|
||||
visibility = if (value) GONE else VISIBLE
|
||||
}
|
||||
|
||||
fun View.onPreDraw(action: () -> Unit) {
|
||||
viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
|
||||
override fun onPreDraw(): Boolean {
|
||||
val vto = viewTreeObserver
|
||||
if (vto.isAlive) {
|
||||
vto.removeOnPreDrawListener(this)
|
||||
}
|
||||
action()
|
||||
// do not block drawing
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
13
app-sample/src/main/res/anim/screen_in.xml
Normal file
13
app-sample/src/main/res/anim/screen_in.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="@android:integer/config_shortAnimTime">
|
||||
|
||||
<alpha
|
||||
android:fromAlpha="0"
|
||||
android:toAlpha="1" />
|
||||
|
||||
<translate
|
||||
android:fromXDelta="100%"
|
||||
android:toXDelta="0" />
|
||||
|
||||
</set>
|
13
app-sample/src/main/res/anim/screen_in_pop.xml
Normal file
13
app-sample/src/main/res/anim/screen_in_pop.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="@android:integer/config_shortAnimTime">
|
||||
|
||||
<alpha
|
||||
android:fromAlpha="0"
|
||||
android:toAlpha="1" />
|
||||
|
||||
<translate
|
||||
android:fromXDelta="-25%"
|
||||
android:toXDelta="0" />
|
||||
|
||||
</set>
|
13
app-sample/src/main/res/anim/screen_out.xml
Normal file
13
app-sample/src/main/res/anim/screen_out.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="@android:integer/config_shortAnimTime">
|
||||
|
||||
<alpha
|
||||
android:fromAlpha="1"
|
||||
android:toAlpha="0.25" />
|
||||
|
||||
<translate
|
||||
android:fromXDelta="0"
|
||||
android:toXDelta="-25%" />
|
||||
|
||||
</set>
|
13
app-sample/src/main/res/anim/screen_out_pop.xml
Normal file
13
app-sample/src/main/res/anim/screen_out_pop.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="@android:integer/config_shortAnimTime">
|
||||
|
||||
<alpha
|
||||
android:fromAlpha="1"
|
||||
android:toAlpha="0" />
|
||||
|
||||
<translate
|
||||
android:fromXDelta="0"
|
||||
android:toXDelta="100%" />
|
||||
|
||||
</set>
|
5
app-sample/src/main/res/drawable/bg_search_bar.xml
Normal file
5
app-sample/src/main/res/drawable/bg_search_bar.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@color/search_bar_background" />
|
||||
<corners android:radius="128dip" />
|
||||
</shape>
|
14
app-sample/src/main/res/drawable/bg_splash.xml
Normal file
14
app-sample/src/main/res/drawable/bg_splash.xml
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape>
|
||||
<solid android:color="@color/window_background" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/ic_launcher_foreground"
|
||||
android:tileMode="disabled" />
|
||||
</item>
|
||||
</layer-list>
|
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
|
||||
</vector>
|
@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_artifact"
|
||||
android:lines="1"
|
||||
android:maxLines="1"
|
||||
android:paddingStart="@dimen/content_padding"
|
||||
android:paddingTop="2dip"
|
||||
android:paddingEnd="@dimen/content_padding"
|
||||
android:paddingBottom="2dip"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textColor="@color/white"
|
||||
tools:text="core" />
|
@ -1,14 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="@dimen/content_padding"
|
||||
android:paddingEnd="@dimen/content_padding"
|
||||
android:paddingBottom="@dimen/content_padding"
|
||||
android:layout_marginStart="@dimen/content_padding"
|
||||
android:layout_marginTop="4dip"
|
||||
android:layout_marginEnd="@dimen/content_padding"
|
||||
android:layout_marginBottom="4dip"
|
||||
tools:ignore="RtlSymmetry">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="@dimen/content_padding_half"
|
||||
android:paddingEnd="@dimen/content_padding"
|
||||
android:paddingBottom="@dimen/content_padding">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@ -18,10 +27,11 @@
|
||||
<TextView
|
||||
android:layout_width="@dimen/adapt_sample_hash_width"
|
||||
android:layout_height="wrap_content"
|
||||
android:alpha="0.5"
|
||||
android:gravity="center"
|
||||
android:text="#"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:textColor="?android:attr/colorSecondary"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<TextView
|
||||
@ -29,6 +39,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:textStyle="bold"
|
||||
tools:text="Title" />
|
||||
|
||||
</LinearLayout>
|
||||
@ -38,14 +49,39 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/adapt_sample_hash_width"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:breakStrategy="simple"
|
||||
android:hyphenationFrequency="none"
|
||||
android:textAppearance="?android:attr/textAppearance"
|
||||
tools:text="Description goes here" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/artifacts_and_tags"
|
||||
<io.noties.markwon.app.base.FlowLayout
|
||||
android:id="@+id/artifacts"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/adapt_sample_hash_width"
|
||||
tools:text="recycler-view adapt another" />
|
||||
android:layout_marginTop="4dip"
|
||||
app:fl_spacing="@dimen/content_padding"
|
||||
app:fl_spacingVertical="2dip">
|
||||
|
||||
<!-- we are actually fine with pre-inflating a single view -->
|
||||
<include layout="@layout/view_artifact" />
|
||||
|
||||
</io.noties.markwon.app.base.FlowLayout>
|
||||
|
||||
<io.noties.markwon.app.base.FlowLayout
|
||||
android:id="@+id/tags"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/adapt_sample_hash_width"
|
||||
android:layout_marginTop="4dip"
|
||||
android:layout_marginBottom="4dip"
|
||||
app:fl_spacing="@dimen/content_padding"
|
||||
app:fl_spacingVertical="2dip">
|
||||
|
||||
<include layout="@layout/view_tag" />
|
||||
|
||||
</io.noties.markwon.app.base.FlowLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
@ -7,26 +7,40 @@
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/app_bar"
|
||||
style="@style/AppBarContainer"
|
||||
android:elevation="4dip"
|
||||
android:orientation="horizontal">
|
||||
android:orientation="horizontal"
|
||||
tools:ignore="UseCompoundDrawables">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/app_bar_icon"
|
||||
style="@style/AppBarIcon"
|
||||
android:contentDescription="@null"
|
||||
android:padding="8dip"
|
||||
android:src="@mipmap/ic_launcher" />
|
||||
|
||||
<TextView
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginEnd="@dimen/app_bar_height"
|
||||
android:gravity="center"
|
||||
android:layout_marginEnd="@dimen/app_bar_height">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/app_bar_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:paddingLeft="@dimen/content_padding_double"
|
||||
android:paddingTop="@dimen/content_padding_half"
|
||||
android:paddingRight="@dimen/content_padding_double"
|
||||
android:paddingBottom="@dimen/content_padding_half"
|
||||
android:text="@string/app_name"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:textColor="@color/white"
|
||||
android:textStyle="bold" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
@ -37,13 +51,18 @@
|
||||
android:id="@+id/recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:translationY="56dip" />
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:overScrollMode="never"
|
||||
android:paddingBottom="36dip"
|
||||
tools:layout_marginTop="56dip" />
|
||||
|
||||
<io.noties.markwon.app.base.SearchBar
|
||||
android:id="@+id/search_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:animateLayoutChanges="true"
|
||||
android:background="@color/search_bar_background_full"
|
||||
android:padding="@dimen/content_padding" />
|
||||
|
||||
</FrameLayout>
|
23
app-sample/src/main/res/layout/sample_text_view.xml
Normal file
23
app-sample/src/main/res/layout/sample_text_view.xml
Normal file
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/scroll_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="@dimen/content_padding"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
tools:text="Hello there" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</ScrollView>
|
6
app-sample/src/main/res/layout/view_artifact.xml
Normal file
6
app-sample/src/main/res/layout/view_artifact.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_artifact"
|
||||
tools:text="core" />
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:background="#eee"
|
||||
tools:background="#fff"
|
||||
tools:gravity="center_vertical"
|
||||
tools:layout_height="wrap_content"
|
||||
tools:layout_widht="match_parent"
|
||||
@ -13,7 +13,8 @@
|
||||
android:layout_width="0px"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:background="@color/white"
|
||||
android:animateLayoutChanges="true"
|
||||
android:background="@drawable/bg_search_bar"
|
||||
android:paddingTop="@dimen/content_padding"
|
||||
android:paddingBottom="@dimen/content_padding">
|
||||
|
||||
@ -31,10 +32,10 @@
|
||||
<ImageView
|
||||
android:layout_width="@dimen/search_bar_icon_side"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:contentDescription="@null"
|
||||
android:scaleType="centerInside"
|
||||
android:src="@drawable/ic_search_white_24dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:tint="@color/gray" />
|
||||
|
||||
<ImageView
|
||||
|
6
app-sample/src/main/res/layout/view_tag.xml
Normal file
6
app-sample/src/main/res/layout/view_tag.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_tag"
|
||||
tools:text="recycler-view" />
|
10
app-sample/src/main/res/values/attrs.xml
Normal file
10
app-sample/src/main/res/values/attrs.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<declare-styleable name="FlowLayout">
|
||||
<attr name="fl_spacing" format="dimension" />
|
||||
<attr name="fl_spacingVertical" format="dimension" />
|
||||
<attr name="fl_spacingHorizontal" format="dimension" />
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
@ -6,6 +6,10 @@
|
||||
<color name="gray_light">#999999</color>
|
||||
|
||||
<color name="white">#FFFFFF</color>
|
||||
<color name="window_background">#EEEEEE</color>
|
||||
<color name="window_background">@color/white</color>
|
||||
<color name="red">#FF0000</color>
|
||||
|
||||
<color name="search_bar_background">#eee</color>
|
||||
<color name="search_bar_background_full">#BFFFFFFF</color>
|
||||
|
||||
</resources>
|
@ -4,6 +4,7 @@
|
||||
|
||||
<dimen name="content_padding">8dip</dimen>
|
||||
<dimen name="content_padding_double">16dip</dimen>
|
||||
<dimen name="content_padding_half">4dip</dimen>
|
||||
|
||||
<dimen name="search_bar_icon_side">36dip</dimen>
|
||||
<dimen name="adapt_sample_hash_width">36dip</dimen>
|
||||
|
4
app-sample/src/main/res/values/ids.xml
Normal file
4
app-sample/src/main/res/values/ids.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<item name="text" type="id" />
|
||||
</resources>
|
@ -1,11 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<style name="AppThemeBase" parent="android:Theme.Material.Light.NoActionBar">
|
||||
<item name="android:colorAccent">@color/accent</item>
|
||||
<item name="android:colorPrimary">@color/gray_light</item>
|
||||
<item name="android:colorPrimaryDark">@color/gray</item>
|
||||
<item name="android:windowBackground">@color/window_background</item>
|
||||
<item name="android:windowSplashscreenContent" tools:ignore="NewApi">@drawable/bg_splash</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme" parent="AppThemeBase" />
|
||||
@ -23,4 +24,19 @@
|
||||
<item name="android:background">?android:attr/selectableItemBackgroundBorderless</item>
|
||||
</style>
|
||||
|
||||
<style name="ArtifactTagText">
|
||||
<item name="android:id">@id/text</item>
|
||||
<item name="android:layout_width">wrap_content</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:lines">1</item>
|
||||
<item name="android:singleLine">true</item>
|
||||
<item name="android:maxLines">1</item>
|
||||
<item name="android:paddingTop">@dimen/content_padding_half</item>
|
||||
<item name="android:paddingStart">@dimen/content_padding</item>
|
||||
<item name="android:paddingEnd">@dimen/content_padding</item>
|
||||
<item name="android:paddingBottom">@dimen/content_padding_half</item>
|
||||
<item name="android:textAppearance">?android:attr/textAppearance</item>
|
||||
<item name="android:textColor">@color/white</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
@ -6,7 +6,7 @@ buildscript {
|
||||
}
|
||||
dependencies {
|
||||
// on `3.5.3` tests are not run from CLI
|
||||
classpath 'com.android.tools.build:gradle:3.5.2'
|
||||
classpath 'com.android.tools.build:gradle:4.0.0'
|
||||
classpath 'com.github.ben-manes:gradle-versions-plugin:0.27.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
@ -69,6 +69,8 @@ ext {
|
||||
'x-recycler-view' : 'androidx.recyclerview:recyclerview:1.0.0',
|
||||
'x-core' : 'androidx.core:core:1.0.2',
|
||||
'x-appcompat' : 'androidx.appcompat:appcompat:1.1.0',
|
||||
'x-cardview' : 'androidx.cardview:cardview:1.0.0',
|
||||
'x-fragment' : 'androidx.fragment:fragment:1.0.0',
|
||||
'commonmark' : "com.atlassian.commonmark:commonmark:$commonMarkVersion",
|
||||
'commonmark-strikethrough': "com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:$commonMarkVersion",
|
||||
'commonmark-table' : "com.atlassian.commonmark:commonmark-ext-gfm-tables:$commonMarkVersion",
|
||||
|
3
gradle/wrapper/gradle-wrapper.properties
vendored
3
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,6 @@
|
||||
#Wed Jun 17 17:05:04 MSK 2020
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.5.1-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
|
||||
|
@ -1,5 +1,9 @@
|
||||
package io.noties.markwon.sample.annotations;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public enum MarkwonArtifact {
|
||||
CORE,
|
||||
EDITOR,
|
||||
@ -17,5 +21,10 @@ public enum MarkwonArtifact {
|
||||
RECYCLER,
|
||||
RECYCLER_TABLE,
|
||||
SIMPLE_EXT,
|
||||
SYNTAX_HIGHLIGHT
|
||||
SYNTAX_HIGHLIGHT;
|
||||
|
||||
@NonNull
|
||||
public String artifactName() {
|
||||
return name().toLowerCase(Locale.US).replace('_', '-');
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user