Sample app, bind list results, search

This commit is contained in:
Dimitry Ivanov 2020-06-18 15:39:21 +03:00
parent 7e8ed3ea0b
commit 66f77f35fe
43 changed files with 1100 additions and 212 deletions

View File

@ -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']
}
}

View 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>

View File

@ -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
}
}

View File

@ -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'")
if (supportFragmentManager.findFragmentById(Window.ID_ANDROID_CONTENT) == null) {
supportFragmentManager.beginTransaction()
.add(Window.ID_ANDROID_CONTENT, SampleListFragment.init())
.commitNowAllowingStateLoss()
}
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) })
}
}

View File

@ -1,5 +0,0 @@
package io.noties.markwon.app
abstract class MarkwonSample {
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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})"
}
}

View File

@ -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)
}
}

View File

@ -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
}
}
private class TagSpan(val tag: String) : ClickableSpan() {
override fun onClick(widget: View) {
Debug.i("clicked tag: $tag")
}
override fun updateDrawState(ds: TextPaint) {
ds.isUnderlineText = false
ds.bgColor = Color.BLUE
}
val artifacts: FlowLayout = requireView(R.id.artifacts)
val tags: FlowLayout = requireView(R.id.tags)
}
}
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))
}
} 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) }
}

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,5 @@
package io.noties.markwon.app.ui
abstract class MarkwonSample {
}

View File

@ -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
}
}
}

View File

@ -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()
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,7 @@
package io.noties.markwon.app.utils
interface Cancellable {
val isCancelled: Boolean
fun cancel()
}

View File

@ -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() {
}
}

View File

@ -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"

View File

@ -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
}
})
}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@ -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>

View File

@ -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" />

View File

@ -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">
</LinearLayout>
<!-- 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>

View File

@ -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>

View 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>

View 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" />

View File

@ -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

View 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" />

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="text" type="id" />
</resources>

View File

@ -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>

View File

@ -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",

View File

@ -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

View File

@ -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('_', '-');
}
}