diff --git a/.travis.yml b/.travis.yml index 1cc9b02..badb566 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,9 +10,10 @@ android: - tools # to get the new `repository-11.xml` - tools # to install latest Android SDK tools - platform-tools - - build-tools-25.0.1 + - build-tools-25.0.3 - android-25 - extra-android-m2repository # Design Support library + - android-21 - sys-img-armeabi-v7a-android-21 # system image (emulator) licenses: - android-sdk-license-.+ diff --git a/build.gradle b/build.gradle index e747c9f..1f9ae80 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,12 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.0.3' + ext.kotlin_version = '1.1.2-5' repositories { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.2.2' + classpath 'com.android.tools.build:gradle:2.3.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong diff --git a/codeview/build.gradle b/codeview/build.gradle index 79feaca..ad2d870 100644 --- a/codeview/build.gradle +++ b/codeview/build.gradle @@ -3,13 +3,13 @@ apply plugin: 'kotlin-android' android { compileSdkVersion 25 - buildToolsVersion "25.0.1" + buildToolsVersion '25.0.3' defaultConfig { minSdkVersion 15 targetSdkVersion 25 versionCode 1 - versionName "1.0" + versionName '1.3.0' } buildTypes { release { @@ -17,19 +17,14 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } - sourceSets { - main.java.srcDirs += 'src/main/kotlin' + lintOptions { + abortOnError false } } dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - compile 'com.android.support:appcompat-v7:25.0.1' - compile 'com.android.support:recyclerview-v7:25.0.1' -} -repositories { - mavenCentral() -} -buildscript { + compile 'com.android.support:appcompat-v7:25.3.1' + compile 'com.android.support:recyclerview-v7:25.3.1' } diff --git a/codeview/src/main/AndroidManifest.xml b/codeview/src/main/AndroidManifest.xml index c5cd343..0e08aa1 100644 --- a/codeview/src/main/AndroidManifest.xml +++ b/codeview/src/main/AndroidManifest.xml @@ -2,7 +2,6 @@ xmlns:android="http://schemas.android.com/apk/res/android"> diff --git a/codeview/src/main/assets/fonts/Consolas.ttf b/codeview/src/main/assets/fonts/Consolas.ttf new file mode 100644 index 0000000..a79deb5 Binary files /dev/null and b/codeview/src/main/assets/fonts/Consolas.ttf differ diff --git a/codeview/src/main/assets/fonts/CourierNew.ttf b/codeview/src/main/assets/fonts/CourierNew.ttf new file mode 100755 index 0000000..5357eeb Binary files /dev/null and b/codeview/src/main/assets/fonts/CourierNew.ttf differ diff --git a/codeview/src/main/assets/fonts/DejaVuSansMono.ttf b/codeview/src/main/assets/fonts/DejaVuSansMono.ttf new file mode 100755 index 0000000..8b7bb2a Binary files /dev/null and b/codeview/src/main/assets/fonts/DejaVuSansMono.ttf differ diff --git a/codeview/src/main/assets/fonts/Inconsolata.ttf b/codeview/src/main/assets/fonts/Inconsolata.ttf new file mode 100644 index 0000000..592ccd2 Binary files /dev/null and b/codeview/src/main/assets/fonts/Inconsolata.ttf differ diff --git a/codeview/src/main/assets/fonts/Monaco.ttf b/codeview/src/main/assets/fonts/Monaco.ttf new file mode 100644 index 0000000..f33c9a7 Binary files /dev/null and b/codeview/src/main/assets/fonts/Monaco.ttf differ diff --git a/codeview/src/main/java/io/github/kbiakov/codeview/CodeView.kt b/codeview/src/main/java/io/github/kbiakov/codeview/CodeView.kt index 830b266..0d3d4e2 100644 --- a/codeview/src/main/java/io/github/kbiakov/codeview/CodeView.kt +++ b/codeview/src/main/java/io/github/kbiakov/codeview/CodeView.kt @@ -1,6 +1,8 @@ package io.github.kbiakov.codeview import android.content.Context +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.RecyclerView import android.util.AttributeSet @@ -10,6 +12,8 @@ import io.github.kbiakov.codeview.Thread.delayed import io.github.kbiakov.codeview.adapters.AbstractCodeAdapter import io.github.kbiakov.codeview.adapters.CodeWithNotesAdapter import io.github.kbiakov.codeview.adapters.Options +import io.github.kbiakov.codeview.highlight.ColorThemeData +import io.github.kbiakov.codeview.highlight.color /** * @class CodeView @@ -20,43 +24,48 @@ import io.github.kbiakov.codeview.adapters.Options */ class CodeView(context: Context, attrs: AttributeSet) : RelativeLayout(context, attrs) { - private val vShadowRight: View - private val vShadowBottomLine: View - private val vShadowBottomContent: View - private val vCodeList: RecyclerView + private val vShadows: Map /** * Primary constructor. */ init { - val isAnimateOnStart = visibility == VISIBLE && { ctx: Context, ats: AttributeSet -> - val a = ctx.theme.obtainStyledAttributes(ats, R.styleable.CodeView, 0, 0) + inflate(context, R.layout.layout_code_view, this) + checkStartAnimation(attrs) - try { - a.getBoolean(R.styleable.CodeView_animateOnStart, true) - } finally { - a.recycle() - } - }(context, attrs) + vCodeList = findViewById(R.id.rv_code_content) as RecyclerView + vCodeList.layoutManager = LinearLayoutManager(context) + vCodeList.isNestedScrollingEnabled = true - alpha = if (isAnimateOnStart) 0f else Consts.ALPHA + vShadows = mapOf( + ShadowPosition.RightBorder to R.id.shadow_right_border, + ShadowPosition.NumBottom to R.id.shadow_num_bottom, + ShadowPosition.ContentBottom to R.id.shadow_content_bottom + ).mapValues { findViewById(it.value) } + } - inflate(context, R.layout.layout_code_view, this) + private fun checkStartAnimation(attrs: AttributeSet) { + if (visibility == VISIBLE && attrs.isAnimateOnStart(context)) { + alpha = Const.Alpha.Invisible - if (isAnimateOnStart) animate() - .setDuration(Consts.DELAY * 5) - .alpha(Consts.ALPHA) - - // TODO: add shadow color customization - vShadowRight = findViewById(R.id.v_shadow_right) - vShadowBottomLine = findViewById(R.id.v_shadow_bottom_line) - vShadowBottomContent = findViewById(R.id.v_shadow_bottom_content) + .setDuration(Const.DefaultDelay * 5) + .alpha(Const.Alpha.Initial) + } else + alpha = Const.Alpha.Initial + } - vCodeList = findViewById(R.id.rv_code_content) as RecyclerView - vCodeList.layoutManager = LinearLayoutManager(context) - vCodeList.isNestedScrollingEnabled = true + private fun AbstractCodeAdapter<*>.checkHighlightAnimation(action: () -> Unit) { + if (options.animateOnHighlight) { + animate() + .setDuration(Const.DefaultDelay * 2) + .alpha(Const.Alpha.AlmostInvisible) + delayed { + animate().alpha(Const.Alpha.Visible) + action() + } + } else action() } /** @@ -64,15 +73,9 @@ class CodeView(context: Context, attrs: AttributeSet) : RelativeLayout(context, * It holds the placeholder on view until code is not highlighted. */ private fun highlight() { - getAdapter()?.highlight { - - animate() - .setDuration(Consts.DELAY * 2) - .alpha(.1f) - - delayed { - animate().alpha(1f) - getAdapter()?.notifyDataSetChanged() + getAdapter()?.apply { + highlight { + checkHighlightAnimation(this::notifyDataSetChanged) } } } @@ -81,14 +84,15 @@ class CodeView(context: Context, attrs: AttributeSet) : RelativeLayout(context, * Border shadows will shown if full listing presented. * It helps to see what part of code is scrolled & hidden. * - * @param isShadows Is shadows needed + * @param isVisible Is shadows visible */ - private fun setupShadows(isShadows: Boolean) { - val visibility = if (isShadows) VISIBLE else GONE - - vShadowRight.visibility = visibility - vShadowBottomLine.visibility = visibility - vShadowBottomContent.visibility = visibility + fun setupShadows(isVisible: Boolean) { + val visibility = if (isVisible) VISIBLE else GONE + val theme = getOptionsOrDefault().theme + vShadows.forEach { (pos, view) -> + view.visibility = visibility + view.setSafeBackground(pos.createShadow(theme)) + } } // - Initialization @@ -112,7 +116,6 @@ class CodeView(context: Context, attrs: AttributeSet) : RelativeLayout(context, */ fun setAdapter(adapter: AbstractCodeAdapter<*>) { vCodeList.adapter = adapter - setupShadows(adapter.options.shadows) highlight() } @@ -121,7 +124,7 @@ class CodeView(context: Context, attrs: AttributeSet) : RelativeLayout(context, /** * View options accessor. */ - fun getOptions(): Options? = getAdapter()?.options + fun getOptions() = getAdapter()?.options fun getOptionsOrDefault() = getOptions() ?: Options(context) /** @@ -130,10 +133,14 @@ class CodeView(context: Context, attrs: AttributeSet) : RelativeLayout(context, * @param options Options */ fun updateOptions(options: Options) { - if (getAdapter() == null) - setOptions(options) - else - getAdapter()!!.options = options + getAdapter() ?: setOptions(options) + getAdapter()?.options = options + setupShadows(options.shadows) + } + + fun updateOptions(body: Options.() -> Unit) { + val options = getOptions() ?: getOptionsOrDefault() + updateOptions(options.apply(body)) } // - Adapter @@ -169,7 +176,7 @@ class CodeView(context: Context, attrs: AttributeSet) : RelativeLayout(context, */ fun setCode(code: String) { getAdapter() ?: prepare() - getAdapter()!!.updateCode(code) + getAdapter()?.updateCode(code) } /** @@ -185,7 +192,40 @@ class CodeView(context: Context, attrs: AttributeSet) : RelativeLayout(context, fun setCode(code: String, language: String) { val options = getOptionsOrDefault() updateOptions(options.withLanguage(language)) - getAdapter()!!.updateCode(code) + getAdapter()?.updateCode(code) + } + + companion object { + + private fun AttributeSet.isAnimateOnStart(context: Context): Boolean { + context.theme.obtainStyledAttributes(this, R.styleable.CodeView, 0, 0).apply { + val flag = getBoolean(R.styleable.CodeView_animateOnStart, false) + recycle() + return@isAnimateOnStart flag + } + return false + } + + private fun View.setSafeBackground(newBackground: Drawable) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { + background = newBackground + } + } + } + + private enum class ShadowPosition { + RightBorder, + NumBottom, + ContentBottom; + + fun createShadow(theme: ColorThemeData) = when (this) { + RightBorder -> GradientDrawable.Orientation.LEFT_RIGHT to theme.bgContent + NumBottom -> GradientDrawable.Orientation.TOP_BOTTOM to theme.bgNum + ContentBottom -> GradientDrawable.Orientation.TOP_BOTTOM to theme.bgContent + }.let { + val colors = arrayOf(android.R.color.transparent, it.second) + GradientDrawable(it.first, colors.map(Int::color).toIntArray()) + } } } diff --git a/codeview/src/main/java/io/github/kbiakov/codeview/Utils.kt b/codeview/src/main/java/io/github/kbiakov/codeview/Utils.kt index 66b3283..fa5823e 100644 --- a/codeview/src/main/java/io/github/kbiakov/codeview/Utils.kt +++ b/codeview/src/main/java/io/github/kbiakov/codeview/Utils.kt @@ -4,15 +4,24 @@ import android.content.Context import android.os.Handler import android.os.Looper import android.text.Html +import android.text.SpannableString import android.text.Spanned +import android.text.TextUtils import android.util.TypedValue import java.io.BufferedReader import java.io.InputStreamReader import java.util.concurrent.Executors -object Consts { - val ALPHA = 0.7F - val DELAY = 250L +object Const { + val DefaultDelay = 250L + + object Alpha { + val Visible = 1f + val Initial = 0.7f + + val AlmostInvisible = 0.1f + val Invisible = 0f + } } /** @@ -22,7 +31,7 @@ object Consts { * @param dp Dip value * @return Converted to px value */ -fun dpToPx(context: Context, dp: Int): Int = +fun dpToPx(context: Context, dp: Int) = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), context.resources.displayMetrics).toInt() @@ -42,6 +51,14 @@ fun spaceSplit(source: String) = source.split("\\s".toRegex()) */ fun extractLines(source: String) = listOf(*source.split("\n").toTypedArray()) +/** + * Slice list by index. + * + * @param idx Index to slice + * @return Pair of lists with head and tail + */ +fun List.slice(idx: Int) = Pair(subList(0, idx), subList(idx, lastIndex)) + /** * Get HTML from string. * @@ -49,11 +66,21 @@ fun extractLines(source: String) = listOf(*source.split("\n").toTypedArray()) * @return Spanned HTML string */ @Suppress("deprecation") -fun html(content: String): Spanned = - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) - Html.fromHtml(content, Html.FROM_HTML_MODE_LEGACY) - else - Html.fromHtml(content) +fun html(content: String): Spanned { + val spanned = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) + Html.fromHtml(content, Html.FROM_HTML_MODE_LEGACY) + else + Html.fromHtml(content) + val spaces = content.startSpacesForTaggedString() + return SpannableString(TextUtils.concat(spaces, spanned)) +} + +private fun String.startSpacesForTaggedString(): String { + val startIdx = indexOf('>') + 1 + val escaped = substring(startIdx) + val count = escaped.indexOf(escaped.trim()) + return " ".repeat(count) +} object Thread { /** @@ -80,15 +107,14 @@ object Thread { * @param body Operation body * @param delayMs Delay in m */ - fun delayed(delayMs: Long = Consts.DELAY, body: () -> Unit) = - Handler().postDelayed(body, delayMs) + fun delayed(delayMs: Long = Const.DefaultDelay, body: () -> Unit) { + Handler().postDelayed(body, delayMs) + } // - Extensions for block manipulations fun (() -> Unit).ui(isUi: Boolean = true) { - if (isUi) ui { - this() - } else this() + if (isUi) ui(this) else this() } } @@ -113,15 +139,12 @@ object Files { var content = "" ls(context, path).forEach { filename -> - val input = context.assets.open(path + '/' + filename) + val input = context.assets.open("$path/$filename") BufferedReader(InputStreamReader(input, "UTF-8")).useLines { - it.forEach { line -> - content += line - } + content += it.reduce { acc, line -> acc + line } } } - return content } } diff --git a/codeview/src/main/java/io/github/kbiakov/codeview/adapters/AbstractCodeAdapter.kt b/codeview/src/main/java/io/github/kbiakov/codeview/adapters/AbstractCodeAdapter.kt index e6decf3..4dafb47 100644 --- a/codeview/src/main/java/io/github/kbiakov/codeview/adapters/AbstractCodeAdapter.kt +++ b/codeview/src/main/java/io/github/kbiakov/codeview/adapters/AbstractCodeAdapter.kt @@ -1,6 +1,8 @@ package io.github.kbiakov.codeview.adapters +import android.annotation.SuppressLint import android.content.Context +import android.graphics.Typeface import android.support.v7.widget.RecyclerView import android.view.LayoutInflater import android.view.View @@ -10,6 +12,8 @@ import android.widget.TextView import io.github.kbiakov.codeview.* import io.github.kbiakov.codeview.Thread.async import io.github.kbiakov.codeview.Thread.ui +import io.github.kbiakov.codeview.adapters.AbstractCodeAdapter.ViewHolderType.Companion.BordersCount +import io.github.kbiakov.codeview.adapters.AbstractCodeAdapter.ViewHolderType.Companion.LineStartIdx import io.github.kbiakov.codeview.classifier.CodeClassifier import io.github.kbiakov.codeview.classifier.CodeProcessor import io.github.kbiakov.codeview.highlight.* @@ -55,17 +59,13 @@ abstract class AbstractCodeAdapter : RecyclerView.Adapter : RecyclerView.Adapter : RecyclerView.Adapter : RecyclerView.Adapter Unit) { - async() { + async { val language = options.language ?: classifyContent() highlighting(language, onReady) } @@ -148,7 +148,6 @@ abstract class AbstractCodeAdapter : RecyclerView.Adapter Unit) { // TODO: highlight by 10 lines val code = CodeHighlighter.highlight(language, options.code, options.theme) - updateContent(code, onReady) } @@ -159,15 +158,10 @@ abstract class AbstractCodeAdapter : RecyclerView.Adapter Unit) { options.code = code prepareCodeLines() - - ui { - onUpdated() - } + ui(onUpdated) } - private fun monoTypeface() = MonoFontCache.getInstance(context).typeface - - // - View holder + // - View holder callbacks override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val inflater = LayoutInflater.from(parent.context) @@ -175,110 +169,134 @@ abstract class AbstractCodeAdapter : RecyclerView.Adapter - val footerView = createFooter(context, entity, isFirst) + it.forEachIndexed { idx, entity -> + val footerView = createFooter(context, entity, idx == 0) holder.llLineFooter.addView(footerView) - isFirst = false } } } - private fun addExtraPadding(position: Int, holder: ViewHolder) { - if (position.isBorder()) { - val dp8 = dpToPx(context, 8) - val topPadding = if (position.isJustFirst()) dp8 else 0 - val bottomPadding = if (position.isJustLast()) dp8 else 0 - holder.tvLineNum.setPadding(0, topPadding, 0, bottomPadding) - holder.tvLineContent.setPadding(0, topPadding, 0, bottomPadding) - } else { - holder.tvLineNum.setPadding(0, 0, 0, 0) - holder.tvLineContent.setPadding(0, 0, 0, 0) - } + companion object { + private const val MaxShortcutLines = 6 + + private fun Pair, List>.linesToShow() = first + private fun Pair, List>.droppedLines() = second } - companion object { - internal const val MAX_SHORTCUT_LINES = 6 + // - View holder types + + enum class ViewHolderType(val viewType: Int) { + Line(0), Border(1); + + companion object { + const val LineStartIdx = 1 + const val BordersCount = 2 + + fun Int.lineEndIdx() = this - BordersCount + + fun get(pos: Int, n: Int) = when (pos) { + in LineStartIdx .. n.lineEndIdx() -> + ViewHolderType.Line.viewType + else -> + ViewHolderType.Border.viewType + } + } } /** * View holder for code adapter. * Stores all views related to code line layout. */ - class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - val tvLineNum: TextView - val tvLineContent: TextView - val llLineFooter: LinearLayout + open class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val tvLineNum = itemView.findViewById(R.id.tv_line_num) as TextView + val tvLineContent = itemView.findViewById(R.id.tv_line_content) as TextView + val llLineFooter = itemView.findViewById(R.id.ll_line_footer) as LinearLayout var mItem: String? = null - init { - tvLineNum = itemView.findViewById(R.id.tv_line_num) as TextView - tvLineContent = itemView.findViewById(R.id.tv_line_content) as TextView - llLineFooter = itemView.findViewById(R.id.ll_line_footer) as LinearLayout - } - override fun toString() = "${super.toString()} '$mItem'" } + + class LineViewHolder(itemView: View) : ViewHolder(itemView) + + /** + * View holder for padding. + * Stores all views related to code line layout. + */ + class BorderViewHolder(itemView: View) : ViewHolder(itemView) } /** @@ -290,6 +308,9 @@ abstract class AbstractCodeAdapter : RecyclerView.Adapter.getColor(result: ParseResult) = + private operator fun HashMap.get(result: ParseResult) = this[result.styleKeys[0]] ?: this["pln"] /** @@ -72,31 +56,30 @@ object CodeHighlighter { * @param colorTheme Color theme * @return Colors map built from color theme */ - private fun buildColorsMap(colorTheme: ColorThemeData) = - object : HashMap() { - init { - val syntaxColors = colorTheme.syntaxColors - - put("typ", syntaxColors.type.hex()) - put("kwd", syntaxColors.keyword.hex()) - put("lit", syntaxColors.literal.hex()) - put("com", syntaxColors.comment.hex()) - put("str", syntaxColors.string.hex()) - put("pun", syntaxColors.punctuation.hex()) - put("pln", syntaxColors.plain.hex()) - put("tag", syntaxColors.tag.hex()) - put("dec", syntaxColors.declaration.hex()) - put("src", syntaxColors.plain.hex()) - put("atn", syntaxColors.attrName.hex()) - put("atv", syntaxColors.attrValue.hex()) - put("nocode", syntaxColors.plain.hex()) - } - } - - // - Escaping/extracting "lower then" symbol - - private fun String.escapeLT() = replace(LT_BRACE, LT_TMP) - private fun String.expandLT() = replace(LT_TMP, LT_REGULAR) + private fun buildColorsMap(theme: ColorThemeData): HashMap { + fun color(body: SyntaxColors.() -> Int) = + theme.syntaxColors.let { body(it).hex() } + return hashMapOf( + "typ" to color { type }, + "kwd" to color { keyword }, + "lit" to color { literal }, + "com" to color { comment }, + "str" to color { string }, + "pun" to color { punctuation }, + "pln" to color { plain }, + "tag" to color { tag }, + "dec" to color { declaration }, + "src" to color { plain }, + "atn" to color { attrName }, + "atv" to color { attrValue }, + "nocode" to color { plain }) + } + + // - Escaping/extracting "less then" symbol + + private fun String.safeLT(op: String.() -> String) = escapeLT().op().expandLT() + private fun String.escapeLT() = replace("<", "^") + private fun String.expandLT() = replace("^", "<") } /** @@ -205,6 +188,24 @@ data class SyntaxColors( val attrName: Int = 0x268BD2, val attrValue: Int = 0x269186) +/** + * Font presets. + */ +enum class Font { + Consolas, + CourierNew, + DejaVuSansMono, + DroidSansMonoSlashed, + Inconsolata, + Monaco; + + companion object { + val Default = DroidSansMonoSlashed + } +} + +// - Helpers + /** * @return Converted hex int to color by adding alpha-channel */ @@ -223,41 +224,43 @@ fun Int.hex() = "#${Integer.toHexString(this)}" * @return Is value equals to found or not condition */ fun Int.isFound() = this >= 0 - fun Int.notFound() = this == -1 /** * Apply font params to string. * - * @param color Color as formatter string - * @return Formatted string + * @param color Color + * @return Parametrized string */ -fun String.withFontParams(color: String?): String { - val parametrizedString = StringBuilder() - +infix fun String.applyFontParams(color: String?): String { + var parametrizedString = "" var idx = 0 var newIdx = indexOf("\n") if (newIdx.notFound()) // covers expected tag coverage (within only one line) - parametrizedString.append(inFontTag(color)) + parametrizedString += inFontTag(color) else { // may contain multiple lines with line breaks // put tag on the borders (end & start of line, ..., end of tag) do { // until closing tag is reached - val part = substring(idx..newIdx - 1).inFontTag(color).plus("\n") - parametrizedString.append(part) + parametrizedString += (substring(idx .. newIdx - 1) inFontTag color) + "\n" idx = newIdx + 1 newIdx = indexOf("\n", idx) } while (newIdx.isFound()) if (idx != indexOf("\n")) // if not replaced only once (for multiline tag coverage) - parametrizedString.append(substring(idx).inFontTag(color)) + parametrizedString += substring(idx) inFontTag color } - - return parametrizedString.toString() + return parametrizedString } +/** + * @return String wrapped in font tag + */ +private infix fun String.inFontTag(color: String?) = + "${escLineBreakAtStart()}" + /** * @return String with escaped line break at start */ @@ -265,9 +268,3 @@ fun String.escLineBreakAtStart() = if (startsWith("\n") && length >= 1) substring(1) else this - -/** - * @return String surrounded by font tag - */ -fun String.inFontTag(color: String?) = - "${escLineBreakAtStart()}" diff --git a/codeview/src/main/java/io/github/kbiakov/codeview/highlight/FontCache.java b/codeview/src/main/java/io/github/kbiakov/codeview/highlight/FontCache.java new file mode 100644 index 0000000..dac0e4c --- /dev/null +++ b/codeview/src/main/java/io/github/kbiakov/codeview/highlight/FontCache.java @@ -0,0 +1,80 @@ +package io.github.kbiakov.codeview.highlight; + +import android.content.Context; +import android.graphics.Typeface; +import android.util.Log; + +import java.util.Map; +import java.util.WeakHashMap; + +import io.github.kbiakov.codeview.adapters.AbstractCodeAdapter; + +/** + * Font cache. + * + * @see AbstractCodeAdapter + * @author Kirill Biakov + */ +public class FontCache { + + private static volatile FontCache instance; + + public static FontCache get(Context context) { + FontCache localInstance = instance; + if (localInstance == null) { + synchronized (FontCache.class) { + localInstance = instance; + if (localInstance == null) { + instance = localInstance = new FontCache(context); + } + } + } + return localInstance; + } + + private Map fonts; + + private FontCache(final Context context) { + this.fonts = new WeakHashMap() {{ + String fontPath = getLocalFontPath(Font.Companion.getDefault()); + put(fontPath, loadFont(context, fontPath)); + }}; + } + + private static String getLocalFontPath(Font font) { + return String.format("%s.ttf", getLocalFontPath(font.name())); + } + + private static String getLocalFontPath(String fontName) { + return String.format("fonts/%s", fontName); + } + + private static Typeface loadFont(Context context, String fontPath) { + return Typeface.createFromAsset(context.getAssets(), fontPath); + } + + // - Public methods + + public Typeface getTypeface(Context context) { + return getTypeface(context, Font.Companion.getDefault()); + } + + public Typeface getTypeface(Context context, Font font) { + return getTypeface(context, getLocalFontPath(font)); + } + + public Typeface getLocalTypeface(Context context, String fontPath) { + return getTypeface(context, getLocalFontPath(fontPath)); + } + + public Typeface getTypeface(Context context, String fontPath) { + Typeface font = fonts.get(fontPath); + if (font != null) { + return font; + } else { + font = loadFont(context, fontPath); + fonts.put(fontPath, font); + return font; + } + } +} diff --git a/codeview/src/main/java/io/github/kbiakov/codeview/highlight/MonoFontCache.java b/codeview/src/main/java/io/github/kbiakov/codeview/highlight/MonoFontCache.java deleted file mode 100644 index 50cd2d4..0000000 --- a/codeview/src/main/java/io/github/kbiakov/codeview/highlight/MonoFontCache.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.github.kbiakov.codeview.highlight; - -import android.content.Context; -import android.graphics.Typeface; - -import io.github.kbiakov.codeview.adapters.AbstractCodeAdapter; - -/** - * @class MonoFontCache - * - * Simple font cache. - * - * @see AbstractCodeAdapter - * @author Kirill Biakov - */ -public class MonoFontCache { - - private static final String FONT_NAME = "DroidSansMonoSlashed"; - - private static volatile MonoFontCache instance; - - public static MonoFontCache getInstance(Context context) { - MonoFontCache localInstance = instance; - if (localInstance == null) { - synchronized (MonoFontCache.class) { - localInstance = instance; - if (localInstance == null) { - instance = localInstance = new MonoFontCache(context); - } - } - } - return localInstance; - } - - private MonoFontCache(Context context) { - this.fontTypeface = Typeface.createFromAsset(context.getAssets(), - String.format("fonts/%s.ttf", FONT_NAME)); - } - - private Typeface fontTypeface; - - public Typeface getTypeface() { - return fontTypeface; - } -} diff --git a/codeview/src/main/java/io/github/kbiakov/codeview/views/BidirectionalScrollView.kt b/codeview/src/main/java/io/github/kbiakov/codeview/views/BidirectionalScrollView.kt index 6eedfb1..5c90791 100644 --- a/codeview/src/main/java/io/github/kbiakov/codeview/views/BidirectionalScrollView.kt +++ b/codeview/src/main/java/io/github/kbiakov/codeview/views/BidirectionalScrollView.kt @@ -33,25 +33,20 @@ class BidirectionalScrollView : HorizontalScrollView { currentY = event.rawY.toInt() return super.dispatchTouchEvent(event) } - MotionEvent.ACTION_MOVE -> { val deltaX = Math.abs(currentX - event.rawX) val deltaY = Math.abs(currentY - event.rawY) scroll(event) val movedOnDistance = dpToPx(context, 2) - if (deltaX > movedOnDistance || deltaY > movedOnDistance) isMoved = true } - MotionEvent.ACTION_UP -> { if (!isMoved) return super.dispatchTouchEvent(event) - isMoved = false } - MotionEvent.ACTION_CANCEL -> isMoved = false } return true @@ -69,21 +64,18 @@ class BidirectionalScrollView : HorizontalScrollView { } override fun measureChild(child: View, parentWidthMeasureSpec: Int, parentHeightMeasureSpec: Int) { - val childWidthMeasureSpec = makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) - val childHeightMeasureSpec = makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) - child.measure(childWidthMeasureSpec, childHeightMeasureSpec) + val measureSpecZero = makeMeasureSpec(0) + child.measure(measureSpecZero, measureSpecZero) } override fun measureChildWithMargins(child: View, parentWidthMeasureSpec: Int, widthUsed: Int, parentHeightMeasureSpec: Int, heightUsed: Int) { val params = child.layoutParams as MarginLayoutParams - - val childWidthMeasureSpec = makeMeasureSpec( - params.leftMargin + params.rightMargin, MeasureSpec.UNSPECIFIED) - val childHeightMeasureSpec = makeMeasureSpec( - params.topMargin + params.bottomMargin, MeasureSpec.UNSPECIFIED) - + val childWidthMeasureSpec = makeMeasureSpec(params.leftMargin + params.rightMargin) + val childHeightMeasureSpec = makeMeasureSpec(params.topMargin + params.bottomMargin) child.measure(childWidthMeasureSpec, childHeightMeasureSpec) } + + private fun makeMeasureSpec(size: Int) = makeMeasureSpec(size, MeasureSpec.UNSPECIFIED) } diff --git a/codeview/src/main/java/io/github/kbiakov/codeview/views/LineDiffView.kt b/codeview/src/main/java/io/github/kbiakov/codeview/views/LineDiffView.kt index 12380fb..996840d 100644 --- a/codeview/src/main/java/io/github/kbiakov/codeview/views/LineDiffView.kt +++ b/codeview/src/main/java/io/github/kbiakov/codeview/views/LineDiffView.kt @@ -6,7 +6,7 @@ import android.view.LayoutInflater import android.widget.RelativeLayout import android.widget.TextView import io.github.kbiakov.codeview.R -import io.github.kbiakov.codeview.highlight.MonoFontCache +import io.github.kbiakov.codeview.highlight.FontCache /** * @class CodeDiffView @@ -15,15 +15,12 @@ import io.github.kbiakov.codeview.highlight.MonoFontCache * * @author Kirill Biakov */ -class LineDiffView : RelativeLayout { +class LineDiffView(context: Context) : RelativeLayout(context) { private val tvLineDiff: TextView private val tvLineContent: TextView - /** - * Default constructor. - */ - constructor(context: Context) : super(context) { + init { val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater inflater.inflate(R.layout.item_code_diff, this, true) @@ -39,19 +36,16 @@ class LineDiffView : RelativeLayout { * @param model Diff model * @return Created line diff view */ - fun create(context: Context, model: DiffModel): LineDiffView { - val diffView = LineDiffView(context) - diffView.tvLineDiff.text = if (model.isAddition) "+" else "-" - diffView.tvLineContent.text = model.content - diffView.tvLineContent.typeface = MonoFontCache.getInstance(context).typeface + fun create(context: Context, model: DiffModel) = LineDiffView(context).apply { + tvLineDiff.text = if (model.isAddition) "+" else "-" + tvLineContent.text = model.content + tvLineContent.typeface = FontCache.get(context).getTypeface(context) - diffView.setBackgroundColor(ContextCompat.getColor(context, + setBackgroundColor(ContextCompat.getColor(context, if (model.isAddition) R.color.diff_add_background else R.color.diff_del_background)) - - return diffView } } } diff --git a/codeview/src/main/res/drawable/shadow_bottom_content.xml b/codeview/src/main/res/drawable/shadow_bottom_content.xml deleted file mode 100644 index 13a0fd6..0000000 --- a/codeview/src/main/res/drawable/shadow_bottom_content.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - diff --git a/codeview/src/main/res/drawable/shadow_bottom_line.xml b/codeview/src/main/res/drawable/shadow_bottom_line.xml deleted file mode 100644 index 2c631ee..0000000 --- a/codeview/src/main/res/drawable/shadow_bottom_line.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - diff --git a/codeview/src/main/res/drawable/shadow_right.xml b/codeview/src/main/res/drawable/shadow_right.xml deleted file mode 100644 index 1018a36..0000000 --- a/codeview/src/main/res/drawable/shadow_right.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/codeview/src/main/res/layout/item_code_diff.xml b/codeview/src/main/res/layout/item_code_diff.xml index 7aa1549..709e52d 100644 --- a/codeview/src/main/res/layout/item_code_diff.xml +++ b/codeview/src/main/res/layout/item_code_diff.xml @@ -9,7 +9,6 @@ android:layout_width="@dimen/line_num_width" android:layout_height="@dimen/line_height" android:gravity="center" - android:fontFamily="monospace" android:textSize="@dimen/line_text_size" android:text="@string/stub_line_num"/> @@ -17,12 +16,12 @@ android:id="@+id/tv_line_content" android:layout_width="wrap_content" android:layout_height="@dimen/line_height" - android:layout_marginLeft="16dp" - android:layout_marginRight="16dp" + android:layout_marginLeft="@dimen/default_margin" + android:layout_marginRight="@dimen/default_margin" + android:layout_toEndOf="@+id/tv_line_diff" android:layout_toRightOf="@+id/tv_line_diff" android:gravity="center_vertical" - android:fontFamily="monospace" - android:singleLine="true" + android:maxLines="1" android:textSize="@dimen/line_text_size" android:text="@string/stub_line_content"/> diff --git a/codeview/src/main/res/layout/item_code_line.xml b/codeview/src/main/res/layout/item_code_line.xml index d4f1ef2..3fcc642 100644 --- a/codeview/src/main/res/layout/item_code_line.xml +++ b/codeview/src/main/res/layout/item_code_line.xml @@ -1,30 +1,30 @@ + android:layout_height="@dimen/line_height"> + tools:text="@string/stub_line_num"/> + tools:text="@string/stub_line_content"/> + android:layout_alignParentEnd="true" + android:layout_alignParentRight="true"/> + android:orientation="horizontal"> + android:id="@+id/shadow_num_bottom" + android:layout_width="@dimen/shadow_width" + android:layout_height="match_parent"/> + android:layout_height="match_parent"/> diff --git a/codeview/src/main/res/values/colors.xml b/codeview/src/main/res/values/colors.xml index e35f45e..18b28f4 100644 --- a/codeview/src/main/res/values/colors.xml +++ b/codeview/src/main/res/values/colors.xml @@ -1,11 +1,5 @@ - #99a8b7 - #f2f2f6 - #2c2d30 - #e9edf4 - #4c5d6e - #EAFFEA #FFECEC diff --git a/codeview/src/main/res/values/dimens.xml b/codeview/src/main/res/values/dimens.xml index aa53e29..c3db8fe 100644 --- a/codeview/src/main/res/values/dimens.xml +++ b/codeview/src/main/res/values/dimens.xml @@ -1,6 +1,10 @@ + 16dp 32dp 24dp + 4dp 12sp - \ No newline at end of file + 24dp + 16dp + diff --git a/codeview/src/main/res/values/strings.xml b/codeview/src/main/res/values/strings.xml index aa335ce..517a801 100644 --- a/codeview/src/main/res/values/strings.xml +++ b/codeview/src/main/res/values/strings.xml @@ -4,5 +4,5 @@ 1 public static final int main(String[] args) { Show all - + ... diff --git a/example/build.gradle b/example/build.gradle index 2ef892b..7ad5510 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -2,15 +2,15 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' android { - compileSdkVersion 24 - buildToolsVersion "24.0.2" + compileSdkVersion 25 + buildToolsVersion '25.0.3' defaultConfig { - applicationId "io.github.kbiakov.codeviewexample" + applicationId 'io.github.kbiakov.codeviewexample' minSdkVersion 15 - targetSdkVersion 24 + targetSdkVersion 25 versionCode 1 - versionName "1.0" + versionName '1.0' } buildTypes { release { @@ -18,13 +18,13 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } - sourceSets { - main.java.srcDirs += 'src/main/kotlin' + lintOptions { + abortOnError false } } dependencies { - compile 'com.android.support:appcompat-v7:24.2.0' + compile 'com.android.support:appcompat-v7:25.3.1' compile project(path: ':codeview') compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" } diff --git a/example/src/main/AndroidManifest.xml b/example/src/main/AndroidManifest.xml index 1aefc25..e879559 100644 --- a/example/src/main/AndroidManifest.xml +++ b/example/src/main/AndroidManifest.xml @@ -4,7 +4,6 @@ - \ No newline at end of file + diff --git a/example/src/main/java/io/github/kbiakov/codeviewexample/ListingsActivity.java b/example/src/main/java/io/github/kbiakov/codeviewexample/ListingsActivity.java index 1771daf..84cb031 100644 --- a/example/src/main/java/io/github/kbiakov/codeviewexample/ListingsActivity.java +++ b/example/src/main/java/io/github/kbiakov/codeviewexample/ListingsActivity.java @@ -10,10 +10,15 @@ import io.github.kbiakov.codeview.CodeView; import io.github.kbiakov.codeview.OnCodeLineClickListener; import io.github.kbiakov.codeview.adapters.CodeWithDiffsAdapter; +import io.github.kbiakov.codeview.adapters.Format; import io.github.kbiakov.codeview.adapters.Options; import io.github.kbiakov.codeview.highlight.ColorTheme; import io.github.kbiakov.codeview.highlight.ColorThemeData; +import io.github.kbiakov.codeview.highlight.Font; +import io.github.kbiakov.codeview.highlight.FontCache; import io.github.kbiakov.codeview.views.DiffModel; +import kotlin.Unit; +import kotlin.jvm.functions.Function1; public class ListingsActivity extends AppCompatActivity { @@ -24,95 +29,151 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { final CodeView codeView = (CodeView) findViewById(R.id.code_view); - /** - * 1: set code content - */ - - // auto language recognition - codeView.setCode(getString(R.string.listing_js)); - - // specify language for code listing - codeView.setCode(getString(R.string.listing_py), "py"); - - /** - * 2: working with options - */ - - // you can change params as follows (unsafe, initialized view only) - codeView.getOptions() - .withCode(R.string.listing_java) - .withTheme(ColorTheme.MONOKAI); - - // short initialization with default params (can be expanded using with() methods) - codeView.setOptions(Options.Default.get(this) - .withLanguage("python") - .withCode(R.string.listing_py) - .withTheme(ColorTheme.MONOKAI)); - - // expanded form of initialization - codeView.setOptions(new Options( - this, // context - getString(R.string.listing_js), // code - "js", // language - ColorTheme.MONOKAI.theme(), // theme (data) - true, // shadows - true, // shortcut - getString(R.string.show_all), // shortcut note - 10, // max lines - new OnCodeLineClickListener() { // line click listener - @Override - public void onCodeLineClicked(int n, @NotNull String line) { - Log.i("ListingsActivity", "On " + (n + 1) + " line clicked"); - } - })); - - /** - * 3: color themes - */ - - codeView.getOptions().setTheme(ColorTheme.SOLARIZED_LIGHT); - - // custom theme - ColorThemeData myTheme = ColorTheme.SOLARIZED_LIGHT.theme() - .withBgContent(android.R.color.black) - .withNoteColor(android.R.color.white); - - codeView.getOptions().setTheme(myTheme); - - /** - * 4: custom adapter with footer views - */ - - final CustomAdapter myAdapter = new CustomAdapter(this, getString(R.string.listing_md)); - codeView.setAdapter(myAdapter); - codeView.getOptions() - .withLanguage("md") - .addCodeLineClickListener(new OnCodeLineClickListener() { - @Override - public void onCodeLineClicked(int n, @NotNull String line) { - myAdapter.addFooterEntity(n, new CustomAdapter.CustomModel("Line " + (n + 1), line)); - } - }); - - /** - * 5: diff adapter with footer views - */ - - final CodeWithDiffsAdapter diffsAdapter = new CodeWithDiffsAdapter(this); - codeView.getOptions() - .withLanguage("python") - .setCode(getString(R.string.listing_py)); - codeView.updateAdapter(diffsAdapter); - - diffsAdapter.addFooterEntity(16, new DiffModel(getString(R.string.py_addition_16), true)); - diffsAdapter.addFooterEntity(11, new DiffModel(getString(R.string.py_deletion_11), false)); - - - /** - * 6: shortcut adapter with footer views - */ - - codeView.getOptions() - .shortcut(10, "Show all"); +// /* +// * 1: set code content +// */ +// +// // auto language recognition +// codeView.setCode(getString(R.string.listing_js)); +// +// // specify language for code listing +// codeView.setCode(getString(R.string.listing_py), "py"); +// +// /* +// * 2: working with options +// */ +// +// // you can change params as follows (unsafe, initialized view only) +// codeView.getOptions() +// .withCode(R.string.listing_java) +// .withTheme(ColorTheme.MONOKAI); +// +// // short initialization with default params (can be expanded using with() methods) +// codeView.setOptions(Options.Default.get(this) +// .withLanguage("python") +// .withCode(R.string.listing_py) +// .withTheme(ColorTheme.MONOKAI) +// .withFont(Font.Consolas)); +// +// // expanded form of initialization +// codeView.setOptions(new Options( +// this, // context +// getString(R.string.listing_js), // code +// "js", // language +// ColorTheme.MONOKAI.theme(), // theme (data) +// FontCache.get(this).getTypeface(this), // font +// Format.Default.getCompact(), // format +// true, // animate on highlight +// true, // shadows visible +// true, // shortcut +// getString(R.string.show_all), // shortcut note +// 10, // max lines +// new OnCodeLineClickListener() { // line click listener +// @Override +// public void onCodeLineClicked(int n, @NotNull String line) { +// Log.i("ListingsActivity", "On " + (n + 1) + " line clicked"); +// } +// })); +// +// /* +// * 3: color themes +// */ +// +// codeView.getOptions().setTheme(ColorTheme.SOLARIZED_LIGHT); +// +// // custom theme +// ColorThemeData myTheme = ColorTheme.SOLARIZED_LIGHT.theme() +// .withBgContent(android.R.color.black) +// .withNoteColor(android.R.color.white); +// +// codeView.getOptions().setTheme(myTheme); +// +// /* +// * 4: custom adapter with footer views +// */ +// +// final CustomAdapter myAdapter = new CustomAdapter(this, getString(R.string.listing_md)); +// codeView.setAdapter(myAdapter); +// codeView.getOptions() +// .withLanguage("md") +// .addCodeLineClickListener(new OnCodeLineClickListener() { +// @Override +// public void onCodeLineClicked(int n, @NotNull String line) { +// myAdapter.addFooterEntity(n, new CustomAdapter.CustomModel("Line " + (n + 1), line)); +// } +// }); +// +// /* +// * 5: diff adapter with footer views +// */ +// +// final CodeWithDiffsAdapter diffsAdapter = new CodeWithDiffsAdapter(this); +// codeView.getOptions() +// .withLanguage("python") +// .setCode(getString(R.string.listing_py)); +// codeView.updateAdapter(diffsAdapter); +// +// diffsAdapter.addFooterEntity(16, new DiffModel(getString(R.string.py_addition_16), true)); +// diffsAdapter.addFooterEntity(11, new DiffModel(getString(R.string.py_deletion_11), false)); +// +// /* +// * 6: shortcut adapter with footer views +// */ +// +// codeView.getOptions() +// .shortcut(10, "Show all"); + + // - Playground + + codeView.setCode("" + + "package io.github.kbiakov.codeviewexample;\n" + + "\n" + + "import android.os.Bundle;\n" + + "import android.support.annotation.Nullable;\n" + + "import android.support.v7.app.AppCompatActivity;\n" + + "import android.util.Log;\n" + + "\n" + + "import org.jetbrains.annotations.NotNull;\n" + + "\n" + + "import io.github.kbiakov.codeview.CodeView;\n" + + "import io.github.kbiakov.codeview.OnCodeLineClickListener;\n" + + "import io.github.kbiakov.codeview.adapters.CodeWithDiffsAdapter;\n" + + "import io.github.kbiakov.codeview.adapters.Options;\n" + + "import io.github.kbiakov.codeview.highlight.ColorTheme;\n" + + "import io.github.kbiakov.codeview.highlight.ColorThemeData;\n" + + "import io.github.kbiakov.codeview.highlight.Font;\n" + + "import io.github.kbiakov.codeview.highlight.FontCache;\n" + + "import io.github.kbiakov.codeview.views.DiffModel;\n" + + "\n" + + "public class ListingsActivity extends AppCompatActivity {\n" + + "\n" + + " @Override\n" + + " protected void onCreate(@Nullable Bundle savedInstanceState) {\n" + + " super.onCreate(savedInstanceState);\n" + + " setContentView(R.layout.activity_listings);\n" + + "\n" + + " final CodeView codeView = (CodeView) findViewById(R.id.code_view);\n" + + "\n" + + " /*\n" + + " * 1: set code content\n" + + " */\n" + + "\n" + + " // auto language recognition\n" + + " codeView.setCode(getString(R.string.listing_js));\n" + + "\n" + + " // specify language for code listing\n" + + " codeView.setCode(getString(R.string.listing_py), \"py\");" + + " }\n" + + "}", "java"); + codeView.updateOptions(new Function1() { + @Override + public Unit invoke(Options options) { + options.withFont(Font.Consolas) + .withTheme(ColorTheme.SOLARIZED_LIGHT) + .withShadows() + .setShortcut(false); + return null; + } + }); } } diff --git a/example/src/main/res/layout/activity_listings.xml b/example/src/main/res/layout/activity_listings.xml index 442ae1d..57e5276 100644 --- a/example/src/main/res/layout/activity_listings.xml +++ b/example/src/main/res/layout/activity_listings.xml @@ -1,6 +1,7 @@ + android:layout_height="wrap_content" + app:animateOnStart="true"/> diff --git a/example/src/main/res/values-w820dp/dimens.xml b/example/src/main/res/values-w820dp/dimens.xml deleted file mode 100644 index 63fc816..0000000 --- a/example/src/main/res/values-w820dp/dimens.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - 64dp - diff --git a/example/src/main/res/values/dimens.xml b/example/src/main/res/values/dimens.xml deleted file mode 100644 index 47c8224..0000000 --- a/example/src/main/res/values/dimens.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 16dp - 16dp - diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index cb45e12..c1b0c03 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Aug 17 15:40:39 MSK 2016 +#Sat Aug 26 20:57:29 MSK 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip