Working with sample app
109
README.md
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
# Markwon
|
||||||
|
Android library for rendering markdown as system-native Spannables. Based on [commonmark-java][commonmark-java]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Supported markdown features:
|
||||||
|
* Emphasis (`*`, `_`)
|
||||||
|
* Strong emphasis (`**`, `__`)
|
||||||
|
* Strike-through (`~~`)
|
||||||
|
* Headers (`#{1,6}`)
|
||||||
|
* Links (`[]()` && `[][]`)
|
||||||
|
* Images (_requires special handling_)
|
||||||
|
* Thematic break (`---`, `***`, `___`)
|
||||||
|
* Quotes & nested quotes (`>{1,}`)
|
||||||
|
* Ordered & non-ordered lists & nested ones
|
||||||
|
* Inline code
|
||||||
|
* Code blocks
|
||||||
|
* Small subset of inline-html (which is rendered by this library):
|
||||||
|
* * Emphasis (`<i>`, `<em>`, `<cite>`, `<dfn>`)
|
||||||
|
* * Strong emphasis (`<b>`, `<strong>`)
|
||||||
|
* * SuperScript (`<sup>`)
|
||||||
|
* * SubScript (`<sub>`)
|
||||||
|
* * Underline (`<u>`)
|
||||||
|
* * Strike-through (`<s>`, `<strike>`, `<del>`)
|
||||||
|
* other inline html is rendered via (`Html.fromHtml(...)`)
|
||||||
|
|
||||||
|
### Emphasis
|
||||||
|
*Lorem ipsum dolor sit amet*
|
||||||
|
_Lorem ipsum dolor sit amet_
|
||||||
|
<i>Lorem ipsum dolor sit amet</i>
|
||||||
|
<em>Lorem ipsum dolor sit amet</em>
|
||||||
|
<cite>Lorem ipsum dolor sit amet</cite>
|
||||||
|
<dfn>Lorem ipsum dolor sit amet</dfn>
|
||||||
|
|
||||||
|
### Strong emphasis
|
||||||
|
**Lorem ipsum dolor sit amet**
|
||||||
|
__Lorem ipsum dolor sit amet__
|
||||||
|
<b>Lorem ipsum dolor sit amet</b>
|
||||||
|
<strong>Lorem ipsum dolor sit amet</strong>
|
||||||
|
|
||||||
|
### Strike-through
|
||||||
|
~~Lorem ipsum dolor sit amet~~
|
||||||
|
<s>Lorem ipsum dolor sit amet</s>
|
||||||
|
<strike>Lorem ipsum dolor sit amet</strike>
|
||||||
|
<del>Lorem ipsum dolor sit amet</del>
|
||||||
|
|
||||||
|
---
|
||||||
|
# Header 1
|
||||||
|
## Header 2
|
||||||
|
### Header 3
|
||||||
|
#### Header 4
|
||||||
|
##### Header 5
|
||||||
|
###### Header 6
|
||||||
|
---
|
||||||
|
|
||||||
|
### Links
|
||||||
|
[click me](https://github.com)
|
||||||
|
[click me][1]
|
||||||
|
[click me][github]
|
||||||
|
<a href="https://github.com">click me</a>
|
||||||
|
|
||||||
|
### Images
|
||||||
|
// todo, normal ones & svg & gif
|
||||||
|
|
||||||
|
### Thematic break
|
||||||
|
---
|
||||||
|
***
|
||||||
|
___
|
||||||
|
|
||||||
|
### Quotes
|
||||||
|
> Lorem ipsum dolor sit amet
|
||||||
|
>> Lorem ipsum dolor sit amet
|
||||||
|
>>> Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
### Ordered lists
|
||||||
|
1. Lorem ipsum dolor sit amet
|
||||||
|
2. Lorem ipsum dolor sit amet
|
||||||
|
1. Lorem ipsum dolor sit amet
|
||||||
|
1. Lorem ipsum dolor sit amet
|
||||||
|
2. Lorem ipsum dolor sit amet
|
||||||
|
3. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
### Non-ordered lists
|
||||||
|
* Lorem ipsum dolor sit amet
|
||||||
|
* Lorem ipsum dolor sit amet
|
||||||
|
* * Lorem ipsum dolor sit amet
|
||||||
|
* * * * Lorem ipsum dolor sit amet
|
||||||
|
* * Lorem ipsum dolor sit amet
|
||||||
|
* Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
### Inline code
|
||||||
|
`Lorem` ipsum dolor sit amet
|
||||||
|
Lorem `ipsum` dolor sit amet
|
||||||
|
Lorem ipsum `dolor` sit amet
|
||||||
|
Lorem ipsum dolor `sit` amet
|
||||||
|
Lorem ipsum dolor sit `amet`
|
||||||
|
|
||||||
|
### Code block
|
||||||
|
// todo syntax higlight
|
||||||
|
```
|
||||||
|
Lorem ipsum dolor sit amet
|
||||||
|
Lorem ipsum dolor sit amet
|
||||||
|
Lorem ipsum dolor sit amet
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
[1]: https://github.com
|
||||||
|
[github]: https://github.com
|
||||||
|
[commonmark-java]: https://github.com/atlassian/commonmark-java
|
@ -21,4 +21,6 @@ dependencies {
|
|||||||
compile 'com.caverock:androidsvg:1.2.1'
|
compile 'com.caverock:androidsvg:1.2.1'
|
||||||
compile 'pl.droidsonroids.gif:android-gif-drawable:1.2.7'
|
compile 'pl.droidsonroids.gif:android-gif-drawable:1.2.7'
|
||||||
compile 'com.squareup.okhttp3:okhttp:3.8.0'
|
compile 'com.squareup.okhttp3:okhttp:3.8.0'
|
||||||
|
compile 'com.google.dagger:dagger:2.10'
|
||||||
|
annotationProcessor 'com.google.dagger:dagger-compiler:2.10'
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,67 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="ru.noties.markwon">
|
package="ru.noties.markwon">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".App"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppThemeLight">
|
||||||
|
|
||||||
<activity android:name=".MainActivity">
|
<activity android:name=".MainActivity">
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data
|
||||||
|
android:host="*"
|
||||||
|
android:scheme="http" />
|
||||||
|
|
||||||
|
<data
|
||||||
|
android:host="*"
|
||||||
|
android:scheme="file" />
|
||||||
|
|
||||||
|
<data
|
||||||
|
android:host="*"
|
||||||
|
android:scheme="https" />
|
||||||
|
|
||||||
|
<!--<data-->
|
||||||
|
<!--android:host="*"-->
|
||||||
|
<!--android:scheme="http"-->
|
||||||
|
<!--android:mimeType="text/markdown"/>-->
|
||||||
|
|
||||||
|
<!--<data-->
|
||||||
|
<!--android:host="*"-->
|
||||||
|
<!--android:scheme="file"-->
|
||||||
|
<!--android:mimeType="text/markdown"/>-->
|
||||||
|
|
||||||
|
<!--<data-->
|
||||||
|
<!--android:host="*"-->
|
||||||
|
<!--android:scheme="https"-->
|
||||||
|
<!--android:mimeType="text/markdown"/>-->
|
||||||
|
|
||||||
|
<data android:pathPattern=".*\\.markdown" />
|
||||||
|
<data android:pathPattern=".*\\.mdown" />
|
||||||
|
<data android:pathPattern=".*\\.mkdn" />
|
||||||
|
<data android:pathPattern=".*\\.mdwn" />
|
||||||
|
<data android:pathPattern=".*\\.mkd" />
|
||||||
|
<data android:pathPattern=".*\\.md" />
|
||||||
|
<data android:pathPattern=".*\\.text" />
|
||||||
|
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
@ -1,282 +0,0 @@
|
|||||||

|
|
||||||
|
|
||||||
[](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22scrollable%22)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Scrollable** is a library for an Android application to implement various scrolling technicks. It's all started with **scrolling tabs**, but now much more can be done with it. Scrollable supports all scrolling and non-scrolling views, including: **RecyclerView**, **ScrollView**, **ListView**, **WebView**, etc and any combination of those inside a **ViewPager**. Library is designed to let developer implement desired effect without imposing one solution. Library is small and has no dependencies.
|
|
||||||
|
|
||||||
## Preview
|
|
||||||
|
|
||||||
All GIFs here are taken from `sample` application module.
|
|
||||||
|
|
||||||
|
|
||||||
<img src="https://github.com/noties/Scrollable/raw/master/art/scrollable_colorful.gif" width="30%" alt="colorful_sample"/> <img src="https://github.com/noties/Scrollable/raw/master/art/scrollable_custom_overscroll.gif" width="30%" alt="custom_overscroll_sample"/> <img src="https://github.com/noties/Scrollable/raw/master/art/scrollable_dialog.gif" width="30%" alt="dialog_sample"/>
|
|
||||||
|
|
||||||
<sup>*Serving suggestion</sup>
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
```groovy
|
|
||||||
compile 'ru.noties:scrollable:1.3.0`
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
To start using this library `ScrollableLayout` must be aded to your layout.
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<ru.noties.scrollable.ScrollableLayout
|
|
||||||
android:id="@+id/scrollable_layout"
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
app:scrollable_autoMaxScroll="true"
|
|
||||||
app:scrollable_defaultCloseUp="true">
|
|
||||||
|
|
||||||
<ru.noties.scrollable.sample.SampleHeaderView
|
|
||||||
style="@style/HeaderStyle"
|
|
||||||
app:shv_title="@string/sample_title_fragment_pager"/>
|
|
||||||
|
|
||||||
<ru.noties.scrollable.sample.TabsLayout
|
|
||||||
android:id="@+id/tabs"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="@dimen/tabs_height"
|
|
||||||
android:background="@color/md_teal_500"/>
|
|
||||||
|
|
||||||
<android.support.v4.view.ViewPager
|
|
||||||
android:id="@+id/view_pager"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_marginTop="@dimen/tabs_height"/>
|
|
||||||
|
|
||||||
</ru.noties.scrollable.ScrollableLayout>
|
|
||||||
```
|
|
||||||
|
|
||||||
Please note, that `ScrollableLayout` positions its children like vertical `LinearLayout`, but measures them like a `FrameLayout`. It is crucial that scrolling content holder dimentions must be set to `match_parent` (minus possible *sticky* view that should be extracted from it, for example, by specifying `android:layoutMarginTop="height_of_sticky_view"`).
|
|
||||||
|
|
||||||
Next, `ScrollableLayout` must be initialized in code:
|
|
||||||
|
|
||||||
```java
|
|
||||||
final ScrollableLayout scrollableLayout = findView(R.id.scrollable_layout);
|
|
||||||
|
|
||||||
// this listener is absolute minimum that is required for `ScrollableLayout` to function
|
|
||||||
scrollableLayout.setCanScrollVerticallyDelegate(new CanScrollVerticallyDelegate() {
|
|
||||||
@Override
|
|
||||||
public boolean canScrollVertically(int direction) {
|
|
||||||
// Obtain a View that is a scroll container (RecyclerView, ListView, ScrollView, WebView, etc)
|
|
||||||
// and call its `canScrollVertically(int) method.
|
|
||||||
// Please note, that if `ViewPager is used, currently displayed View must be obtained
|
|
||||||
// because `ViewPager` doesn't delegate `canScrollVertically` method calls to it's children
|
|
||||||
final View view = getCurrentView();
|
|
||||||
return view.canScrollVertically(direction);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### Draggable View
|
|
||||||
|
|
||||||
This is a View, that can be *dragged* to change `ScrollableLayout` scroll state. For example, to expand header if tabs are dragged. To add this simply call:
|
|
||||||
```java
|
|
||||||
// Please note that `tabsLayout` must be a child (direct or indirect) of a ScrollableLayout
|
|
||||||
scrollableLayout.setDraggableView(tabsLayout);
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### OnScrollChangedListener
|
|
||||||
|
|
||||||
In order to apply custom logic for different scroll state of a `ScrollableLayout` `OnScrollChangedListener` can be used (to change color of a header, create parallax effect, sticky tabs, etc)
|
|
||||||
|
|
||||||
```java
|
|
||||||
scrollableLayout.addOnScrollChangedListener(new OnScrollChangedListener() {
|
|
||||||
@Override
|
|
||||||
public void onScrollChanged(int y, int oldY, int maxY) {
|
|
||||||
|
|
||||||
// `ratio` of current scroll state (from 0.0 to 1.0)
|
|
||||||
// 0.0 - means fully expanded
|
|
||||||
// 1.0 - means fully collapsed
|
|
||||||
final float ratio = (float) y / maxY;
|
|
||||||
|
|
||||||
// for example, we can hide header, if we are collapsed
|
|
||||||
// and show it when we are expanded (plus intermediate state)
|
|
||||||
header.setAlpha(1.F - ratio);
|
|
||||||
|
|
||||||
// to create a `sticky` effect for tabs this calculation can be used:
|
|
||||||
final float tabsTranslationY;
|
|
||||||
if (y < maxY) {
|
|
||||||
// natural position
|
|
||||||
tabsTranslationY = .0F;
|
|
||||||
} else {
|
|
||||||
// sticky position
|
|
||||||
tabsTranslationY = y - maxY;
|
|
||||||
}
|
|
||||||
tabsLayout.setTranslationY(tabsTranslationY);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### OnFlingOverListener
|
|
||||||
|
|
||||||
To *continue* a fling event for a scrolling container `OnFlingOverListener` can be used.
|
|
||||||
|
|
||||||
```java
|
|
||||||
scrollableLayout.setOnFlingOverListener(new OnFlingOverListener() {
|
|
||||||
@Override
|
|
||||||
public void onFlingOver(int y, long duration) {
|
|
||||||
recyclerView.smoothScrollBy(0, y);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### OverScrollListener
|
|
||||||
|
|
||||||
To create custom *overscroll* handler (for example, like in `SwipeRefreshLayout` for loading, or to zoom-in header when cannot scroll further) `OverScrollListener` can be used
|
|
||||||
|
|
||||||
```java
|
|
||||||
scrollableLayout.setOverScrollListener(new OverScrollListener() {
|
|
||||||
@Override
|
|
||||||
public void onOverScrolled(ScrollableLayout layout, int overScrollY) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasOverScroll(ScrollableLayout layout, int overScrollY) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCancelled(ScrollableLayout layout) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void clear() {
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
`OverScrollListener` gives you full controll of overscrolling, but it implies a lot of handling. For a simple case `OverScrollListenerBase` can be used
|
|
||||||
|
|
||||||
```java
|
|
||||||
scrollableLayout.setOverScrollListener(new OverScrollListenerBase() {
|
|
||||||
@Override
|
|
||||||
protected void onRatioChanged(ScrollableLayout layout, float ratio) {
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
For example, this is `onRatioChanged` method from `ZoomInHeaderOverScrollListener` from `sample` application:
|
|
||||||
```java
|
|
||||||
@Override
|
|
||||||
protected void onRatioChanged(ScrollableLayout layout, float ratio) {
|
|
||||||
final float scale = 1.F + (.33F * ratio);
|
|
||||||
mHeader.setScaleX(scale);
|
|
||||||
mHeader.setScaleY(scale);
|
|
||||||
|
|
||||||
final int headerHeight = mHeader.getHeight();
|
|
||||||
mContent.setTranslationY(((headerHeight * scale) - headerHeight) / 2.F);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Scrolling Header
|
|
||||||
|
|
||||||
There is support for scrolling header. This means that if header can scroll, it will scroll first to the final position and only after that scroll event will be redirected. There are no extra steps to enable this feature if scrolling header is the first view in `ScrollableLayout`. Otherwise a XML attribute `app:scrollable_scrollingHeaderId` can be used, it accepts an id of a view.
|
|
||||||
|
|
||||||
|
|
||||||
## Various customizations
|
|
||||||
|
|
||||||
### CloseUpAlgorithm
|
|
||||||
|
|
||||||
In order to *close-up* `ScrollableLayout` (do not leave in intermediate state, allow only two scrolling states: collapsed & expanded, etc), `CloseUpAlgorithm` can be used. Its signature is as follows:
|
|
||||||
|
|
||||||
```java
|
|
||||||
public interface CloseUpAlgorithm {
|
|
||||||
|
|
||||||
int getFlingFinalY(ScrollableLayout layout, boolean isScrollingBottom, int nowY, int suggestedY, int maxY);
|
|
||||||
|
|
||||||
int getIdleFinalY(ScrollableLayout layout, int nowY, int maxY);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
And usage is like this:
|
|
||||||
|
|
||||||
```java
|
|
||||||
scrollableLayout.setCloseUpAlgorithm(new MyCloseUpAlgorithm());
|
|
||||||
```
|
|
||||||
|
|
||||||
Library provides a `DefaultCloseUpAlgorithm` for a most common usage (to allow `ScrollableLayout` only 2 scrolling states: collapsed and expanded). It can be set via java code: `scrollableLayout.setCloseUpAlgorithm(new DefaultCloseUpAlgorithm())` and via XML with `app:scrollable_defaultCloseUp="true"`.
|
|
||||||
|
|
||||||
Also, there is an option to set duration after which CloseUpAlgorithm should be evaluated (idle state - no touch events). Java: `scrollableLayout.setConsiderIdleMillis(100L)` and XML: `app:scrollable_considerIdleMillis="100"`. `100L` is the default value and may be omitted.
|
|
||||||
|
|
||||||
If *close-up* need to have different animation times, `CloseUpIdleAnimationTime` can be used. Its signature:
|
|
||||||
|
|
||||||
```java
|
|
||||||
public interface CloseUpIdleAnimationTime {
|
|
||||||
long compute(ScrollableLayout layout, int nowY, int endY, int maxY);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
If animation time is constant (do not depend on current scroll state), `SimpleCloseUpIdleAnimationTime` can be used. Java: `scrollableLayout.setCloseUpIdleAnimationTime(new SimpleCloseUpIdleAnimationTime(200L))`, XML: `app:app:scrollable_closeUpAnimationMillis="200"`. `200L` is default value and can be omitted.
|
|
||||||
|
|
||||||
If one want to get control of `ValueAnimator` that is used to animate between scroll states, `CloseUpAnimatorConfigurator` can be used. Its signature:
|
|
||||||
|
|
||||||
```java
|
|
||||||
public interface CloseUpAnimatorConfigurator {
|
|
||||||
// when called will already have a duration set
|
|
||||||
void configure(ValueAnimator animator);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
If only `Interpolator` must be configured, a `InterpolatorCloseUpAnimatorConfigurator` can be used. Java: `scrollableLayout.setCloseAnimatorConfigurator(new InterpolatorCloseUpAnimatorConfigurator(interpolator))`, XML: `app:scrollable_closeUpAnimatorInterpolator="app:scrollable_closeUpAnimatorInterpolator="@android:interpolator/decelerate_cubic"`
|
|
||||||
|
|
||||||
|
|
||||||
### Auto Max Scroll
|
|
||||||
|
|
||||||
If you layout has a header with dynamic height, or it's height should be obtained at runtime, there is an option to automatically obtain it. Java: `scrollableLayout.setAutoMaxScroll(true)`, XML: `app:scrollable_autoMaxScroll="true"`. With this option there is no need manually set `maxScrollY`. Please note, that if not specified explicitly this option will be set to `true` if `maxScroll` option is not set (equals `0`). Also, if at runtime called `scrollableLayout.setMaxScroll(int)`, `autoMaxScroll` if set to `true`, will be set to `false`.
|
|
||||||
|
|
||||||
By default the first View will be used to calculate height, but if different one must be used, there is an option to specify `id` of this view. XML: `app:scrollable_autoMaxScrollViewId="@id/header"`
|
|
||||||
|
|
||||||
|
|
||||||
### Disable Handling
|
|
||||||
|
|
||||||
If `ScrollableLayout` must not evaluate its scrolling logic (skip all touch events), `scrollableLayout.setSelfUpdateScroll(boolean)` can be used. Pass `true` to disable all handling, `false` to enable it.
|
|
||||||
|
|
||||||
|
|
||||||
### Animate Scroll
|
|
||||||
|
|
||||||
To animate scroll state of a `ScrollableLayout`, `animateScroll(int)` can be used:
|
|
||||||
|
|
||||||
```java
|
|
||||||
// returns ValueAnimator, that can be configured as desired
|
|
||||||
// `0` - expand fully
|
|
||||||
// `scrollableLayout.getMaxScroll()` - collapse
|
|
||||||
scrollableLayout.animateScroll(0)
|
|
||||||
.setDuration(250L)
|
|
||||||
.start();
|
|
||||||
```
|
|
||||||
|
|
||||||
Please note that `ScrollableLayout` caches returned `ValueAnimator` and reuses it. First of all because it doesn't make sense to have two different scrolling animations on one `ScrollableLayout`. So, it's advisable to clear all possible custom state before running animation (just like `View` handles `ViewPropertyAnimator`)
|
|
||||||
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
```
|
|
||||||
Copyright 2015 Dimitry Ivanov (mail@dimitryivanov.ru)
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
```
|
|
@ -1,114 +0,0 @@
|
|||||||

|
|
||||||
|
|
||||||
# Hello!
|
|
||||||
|
|
||||||
**bold *italic*** _just italic_
|
|
||||||
|
|
||||||
[](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22scrollable%22)
|
|
||||||
|
|
||||||
> Quote
|
|
||||||
>> Second Quote
|
|
||||||
>>> Third one, yuhuu!
|
|
||||||
|
|
||||||
`can a code have **markdown?**` so good it doesn't
|
|
||||||
|
|
||||||
<h1>Yo!
|
|
||||||
Omg
|
|
||||||
|
|
||||||
|
|
||||||
ddffdg
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
1. First
|
|
||||||
2. Second
|
|
||||||
3. Third
|
|
||||||
* Interesting
|
|
||||||
4. Forth
|
|
||||||
|
|
||||||
## Unordered list
|
|
||||||
|
|
||||||
* first
|
|
||||||
* second
|
|
||||||
* * second first
|
|
||||||
* * second __second__ jks8feif fdsuif yuweru sdfoisdfu wutwe iower wtew ruweir weoir wutywr wer woeirwr wieyriow eryowe rwyeor oweryower o
|
|
||||||
* * * hm, is it actually a thing?
|
|
||||||
* * * * and this?!
|
|
||||||
* * * * * omg
|
|
||||||
* third `and some code`
|
|
||||||
|
|
||||||
|
|
||||||
1. okay
|
|
||||||
2. okay 2
|
|
||||||
1. okay again
|
|
||||||
* it's also here
|
|
||||||
2. and this
|
|
||||||
3. and that
|
|
||||||
1. Another one nested this time and a lot of text here, well, at least some to check how multiline will be handled
|
|
||||||
1. And it goes on and on
|
|
||||||
2. And it goes on and on
|
|
||||||
3. And it goes on and on
|
|
||||||
4. And it goes on and on
|
|
||||||
5. And it goes on and on
|
|
||||||
6. And it goes on and on
|
|
||||||
7. And it goes on and on
|
|
||||||
8. And it goes on and on
|
|
||||||
9. And it goes on and on
|
|
||||||
10. And it goes on and on
|
|
||||||
11. And it goes on and on
|
|
||||||
12. And it goes on and on
|
|
||||||
13. And it goes on and on
|
|
||||||
14. And it goes on and on
|
|
||||||
15. And it goes on and on
|
|
||||||
16. And it goes on and on
|
|
||||||
17. And it goes on and on
|
|
||||||
18. And it goes on and on
|
|
||||||
19. And it goes on and on
|
|
||||||
20. And it goes on and on
|
|
||||||
21. And it goes on and on
|
|
||||||
22. And it goes on and on
|
|
||||||
23. And it goes on and on
|
|
||||||
24. And it goes on and on
|
|
||||||
25. And it goes on and on
|
|
||||||
26. And it goes on and on
|
|
||||||
27. And it goes on and on
|
|
||||||
28. And it goes on and on
|
|
||||||
29. And it goes on and on
|
|
||||||
30. And it goes on and on
|
|
||||||
31. And it goes on and on
|
|
||||||
32. And it goes on and on
|
|
||||||
333333333. And it goes on and on
|
|
||||||
|
|
||||||
|
|
||||||
### Quoted list
|
|
||||||
|
|
||||||
> * first
|
|
||||||
> * second
|
|
||||||
> * third
|
|
||||||
> * * third first
|
|
||||||
>> * yo #1
|
|
||||||
>> * yo #2
|
|
||||||
|
|
||||||
|
|
||||||
<b>j<i><del>o</del></i></b>
|
|
||||||
|
|
||||||
|
|
||||||
#### Code block
|
|
||||||
|
|
||||||
```java
|
|
||||||
final String s = "this id code block";
|
|
||||||
s.length() > 0;
|
|
||||||
```
|
|
||||||
---
|
|
||||||
okay, have a good day!
|
|
||||||
|
|
||||||
Yo<sup>**2**<sup>22</sup><sub>42</sub></sup>
|
|
||||||
|
|
||||||
To compare<sub>~~13~~</sub>
|
|
||||||
|
|
||||||
~~Just strike it~~
|
|
||||||
|
|
||||||
<br /><br /><br /><br />
|
|
||||||
|
|
||||||
<font color="#FF0000">RED</font>
|
|
||||||
|
|
||||||
**PS** additional text to check if this view scrolls gracefully, sofihweo fwfw fuwf weyf pwefiowef twe weuifphw efwepfuwoefh wfypiwe fuwoef wiefg wtefw uf ywfyw fweouf wpfyw fwfe#
|
|
7
app/src/main/java/ru/noties/markwon/ActivityScope.java
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package ru.noties.markwon;
|
||||||
|
|
||||||
|
import javax.inject.Scope;
|
||||||
|
|
||||||
|
@Scope
|
||||||
|
@interface ActivityScope {
|
||||||
|
}
|
23
app/src/main/java/ru/noties/markwon/App.java
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package ru.noties.markwon;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
public class App extends Application {
|
||||||
|
|
||||||
|
private AppComponent component;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
|
||||||
|
component = DaggerAppComponent.builder()
|
||||||
|
.appModule(new AppModule(this))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AppComponent component(@NonNull Context context) {
|
||||||
|
return ((App) context.getApplicationContext()).component;
|
||||||
|
}
|
||||||
|
}
|
41
app/src/main/java/ru/noties/markwon/AppBarItem.java
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package ru.noties.markwon;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
abstract class AppBarItem {
|
||||||
|
|
||||||
|
static class State {
|
||||||
|
final String title;
|
||||||
|
final String subtitle;
|
||||||
|
|
||||||
|
State(String title, String subtitle) {
|
||||||
|
this.title = title;
|
||||||
|
this.subtitle = subtitle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Renderer {
|
||||||
|
|
||||||
|
final TextView title;
|
||||||
|
final TextView subtitle;
|
||||||
|
|
||||||
|
Renderer(@NonNull View view, @NonNull View.OnClickListener themeChangeClicked) {
|
||||||
|
this.title = Views.findView(view, R.id.app_bar_title);
|
||||||
|
this.subtitle = Views.findView(view, R.id.app_bar_subtitle);
|
||||||
|
view.findViewById(R.id.app_bar_theme_changer)
|
||||||
|
.setOnClickListener(themeChangeClicked);
|
||||||
|
}
|
||||||
|
|
||||||
|
void render(@NonNull State state) {
|
||||||
|
title.setText(state.title);
|
||||||
|
subtitle.setText(state.subtitle);
|
||||||
|
Views.setVisible(subtitle, !TextUtils.isEmpty(state.subtitle));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private AppBarItem() {
|
||||||
|
}
|
||||||
|
}
|
11
app/src/main/java/ru/noties/markwon/AppComponent.java
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package ru.noties.markwon;
|
||||||
|
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import dagger.Component;
|
||||||
|
|
||||||
|
@Component(modules = AppModule.class)
|
||||||
|
@Singleton
|
||||||
|
interface AppComponent {
|
||||||
|
MainActivitySubcomponent mainActivitySubcomponent();
|
||||||
|
}
|
66
app/src/main/java/ru/noties/markwon/AppModule.java
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package ru.noties.markwon;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
|
||||||
|
import com.squareup.picasso.Picasso;
|
||||||
|
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
|
||||||
|
@Module
|
||||||
|
class AppModule {
|
||||||
|
|
||||||
|
private final App app;
|
||||||
|
|
||||||
|
AppModule(App app) {
|
||||||
|
this.app = app;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
Context context() {
|
||||||
|
return app.getApplicationContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
Resources resources() {
|
||||||
|
return app.getResources();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
OkHttpClient client() {
|
||||||
|
return new OkHttpClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
ExecutorService executorService() {
|
||||||
|
return Executors.newCachedThreadPool();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
Handler mainThread() {
|
||||||
|
return new Handler(Looper.getMainLooper());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
UrlProvider urlProvider() {
|
||||||
|
return new UrlProviderImpl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
Picasso picasso(Context context) {
|
||||||
|
return Picasso.with(context);
|
||||||
|
}
|
||||||
|
}
|
@ -1,70 +1,82 @@
|
|||||||
package ru.noties.markwon;
|
package ru.noties.markwon;
|
||||||
|
|
||||||
import android.graphics.Picture;
|
import android.content.res.Resources;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.drawable.BitmapDrawable;
|
||||||
|
import android.graphics.drawable.ColorDrawable;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.graphics.drawable.PictureDrawable;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.caverock.androidsvg.SVG;
|
import com.caverock.androidsvg.SVG;
|
||||||
import com.squareup.picasso.Picasso;
|
import com.squareup.picasso.Picasso;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
import okhttp3.OkHttpClient;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import pl.droidsonroids.gif.GifDrawable;
|
import pl.droidsonroids.gif.GifDrawable;
|
||||||
import ru.noties.debug.Debug;
|
import ru.noties.debug.Debug;
|
||||||
import ru.noties.markwon.spans.AsyncDrawable;
|
import ru.noties.markwon.spans.AsyncDrawable;
|
||||||
|
|
||||||
|
@ActivityScope
|
||||||
public class AsyncDrawableLoader implements AsyncDrawable.Loader {
|
public class AsyncDrawableLoader implements AsyncDrawable.Loader {
|
||||||
|
|
||||||
private final TextView view;
|
@Inject
|
||||||
private final Picasso picasso;
|
Resources resources;
|
||||||
private final OkHttpClient client;
|
|
||||||
private final ExecutorService executorService;
|
@Inject
|
||||||
private final Map<String, Future<?>> requests;
|
Picasso picasso;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ExecutorService executorService;
|
||||||
|
|
||||||
|
private final Map<String, Future<?>> requests = new HashMap<>(3);
|
||||||
|
private final CopyOnWriteArrayList<AsyncDrawableTarget> targets = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
// sh*t..
|
// sh*t..
|
||||||
public AsyncDrawableLoader(TextView view) {
|
@Inject
|
||||||
this.view = view;
|
public AsyncDrawableLoader() {
|
||||||
this.picasso = new Picasso.Builder(view.getContext())
|
|
||||||
.listener(new Picasso.Listener() {
|
|
||||||
@Override
|
|
||||||
public void onImageLoadFailed(Picasso picasso, Uri uri, Exception exception) {
|
|
||||||
Debug.e(exception, picasso, uri);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.build();
|
|
||||||
this.client = new OkHttpClient();
|
|
||||||
this.executorService = Executors.newCachedThreadPool();
|
|
||||||
this.requests = new HashMap<>(3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void load(@NonNull String destination, @NonNull AsyncDrawable drawable) {
|
public void load(@NonNull String destination, @NonNull AsyncDrawable drawable) {
|
||||||
|
|
||||||
Debug.i("destination: %s", destination);
|
|
||||||
|
|
||||||
if (destination.endsWith(".svg")) {
|
if (destination.endsWith(".svg")) {
|
||||||
// load svg
|
// load svg
|
||||||
requests.put(destination, loadSvg(destination, drawable));
|
requests.put(destination, loadSvg(destination, drawable));
|
||||||
} else if (destination.endsWith(".gif")) {
|
} else if (destination.endsWith(".gif")) {
|
||||||
requests.put(destination, loadGif(destination, drawable));
|
requests.put(destination, loadGif(destination, drawable));
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
|
final Drawable error = new ColorDrawable(0xFFff0000);
|
||||||
|
final Drawable placeholder = new ColorDrawable(0xFF00ff00);
|
||||||
|
error.setBounds(0, 0, 100, 100);
|
||||||
|
placeholder.setBounds(0, 0, 50, 50);
|
||||||
|
|
||||||
|
final AsyncDrawableTarget target = new AsyncDrawableTarget(resources, drawable, new AsyncDrawableTarget.DoneListener() {
|
||||||
|
@Override
|
||||||
|
public void onLoadingDone(AsyncDrawableTarget target) {
|
||||||
|
targets.remove(target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
targets.add(target);
|
||||||
|
|
||||||
picasso
|
picasso
|
||||||
.load(destination)
|
.load(destination)
|
||||||
.tag(destination)
|
.tag(destination)
|
||||||
.into(new TextViewTarget(view, drawable));
|
.placeholder(placeholder)
|
||||||
|
.error(error)
|
||||||
|
.into(target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,7 +85,7 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
|
|||||||
Debug.i("destination: %s", destination);
|
Debug.i("destination: %s", destination);
|
||||||
picasso.cancelTag(destination);
|
picasso.cancelTag(destination);
|
||||||
|
|
||||||
final Future<?> future = requests.get(destination);
|
final Future<?> future = requests.remove(destination);
|
||||||
if (future != null) {
|
if (future != null) {
|
||||||
future.cancel(true);
|
future.cancel(true);
|
||||||
}
|
}
|
||||||
@ -84,12 +96,27 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
final URL url = new URL(destination);
|
final URL url = new URL(destination);
|
||||||
final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||||
final InputStream inputStream = connection.getInputStream();
|
final InputStream inputStream = connection.getInputStream();
|
||||||
|
|
||||||
final SVG svg = SVG.getFromInputStream(inputStream);
|
final SVG svg = SVG.getFromInputStream(inputStream);
|
||||||
final Picture picture = svg.renderToPicture();
|
final float w = svg.getDocumentWidth();
|
||||||
final Drawable drawable = new PictureDrawable(picture);
|
final float h = svg.getDocumentHeight();
|
||||||
|
Debug.i("w: %s, h: %s", w, h);
|
||||||
|
|
||||||
|
final float density = resources.getDisplayMetrics().density;
|
||||||
|
Debug.i(density);
|
||||||
|
|
||||||
|
final int width = (int) (w * density + .5F);
|
||||||
|
final int height = (int) (h * density + .5F);
|
||||||
|
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444);
|
||||||
|
final Canvas canvas = new Canvas(bitmap);
|
||||||
|
canvas.scale(density, density);
|
||||||
|
svg.renderToCanvas(canvas);
|
||||||
|
|
||||||
|
final Drawable drawable = new BitmapDrawable(resources, bitmap);
|
||||||
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
|
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
|
||||||
asyncDrawable.setResult(drawable);
|
asyncDrawable.setResult(drawable);
|
||||||
|
|
||||||
@ -105,9 +132,11 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
final URL url = new URL(destination);
|
final URL url = new URL(destination);
|
||||||
final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||||
final InputStream inputStream = connection.getInputStream();
|
final InputStream inputStream = connection.getInputStream();
|
||||||
|
|
||||||
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
final byte[] buffer = new byte[1024 * 8];
|
final byte[] buffer = new byte[1024 * 8];
|
||||||
int read;
|
int read;
|
||||||
|
81
app/src/main/java/ru/noties/markwon/AsyncDrawableTarget.java
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package ru.noties.markwon;
|
||||||
|
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.drawable.BitmapDrawable;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
|
||||||
|
import com.squareup.picasso.Picasso;
|
||||||
|
import com.squareup.picasso.Target;
|
||||||
|
|
||||||
|
import ru.noties.markwon.spans.AsyncDrawable;
|
||||||
|
|
||||||
|
public class AsyncDrawableTarget implements Target {
|
||||||
|
|
||||||
|
interface DoneListener {
|
||||||
|
void onLoadingDone(AsyncDrawableTarget target);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Resources resources;
|
||||||
|
private final AsyncDrawable asyncDrawable;
|
||||||
|
private final DoneListener listener;
|
||||||
|
|
||||||
|
public AsyncDrawableTarget(Resources resources, AsyncDrawable asyncDrawable, DoneListener listener) {
|
||||||
|
this.resources = resources;
|
||||||
|
this.asyncDrawable = asyncDrawable;
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
|
||||||
|
if (bitmap != null) {
|
||||||
|
final Drawable drawable = new BitmapDrawable(resources, bitmap);
|
||||||
|
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
|
||||||
|
asyncDrawable.setResult(drawable);
|
||||||
|
}
|
||||||
|
notifyDone();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBitmapFailed(Drawable errorDrawable) {
|
||||||
|
if (errorDrawable != null) {
|
||||||
|
asyncDrawable.setResult(errorDrawable);
|
||||||
|
}
|
||||||
|
notifyDone();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPrepareLoad(Drawable placeHolderDrawable) {
|
||||||
|
if (placeHolderDrawable != null) {
|
||||||
|
asyncDrawable.setResult(placeHolderDrawable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyDone() {
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onLoadingDone(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// private void attach() {
|
||||||
|
//
|
||||||
|
// // amazing stuff here, in order to keep this target alive (picasso stores target in a WeakReference)
|
||||||
|
// // we need to do this
|
||||||
|
//
|
||||||
|
// //noinspection unchecked
|
||||||
|
// List<AsyncDrawableTarget> list = (List<AsyncDrawableTarget>) view.getTag(R.id.amazing);
|
||||||
|
// if (list == null) {
|
||||||
|
// list = new ArrayList<>(2);
|
||||||
|
// view.setTag(R.id.amazing, list);
|
||||||
|
// }
|
||||||
|
// list.add(this);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private void detach() {
|
||||||
|
// //noinspection unchecked
|
||||||
|
// final List<AsyncDrawableTarget> list = (List<AsyncDrawableTarget>) view.getTag(R.id.amazing);
|
||||||
|
// if (list != null) {
|
||||||
|
// list.remove(this);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
@ -1,90 +1,110 @@
|
|||||||
package ru.noties.markwon;
|
package ru.noties.markwon;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.SystemClock;
|
import android.view.View;
|
||||||
import android.text.method.LinkMovementMethod;
|
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import java.io.IOException;
|
import javax.inject.Inject;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.Scanner;
|
|
||||||
|
|
||||||
import ru.noties.debug.AndroidLogDebugOutput;
|
import ru.noties.debug.AndroidLogDebugOutput;
|
||||||
import ru.noties.debug.Debug;
|
import ru.noties.debug.Debug;
|
||||||
import ru.noties.markwon.spans.AsyncDrawable;
|
|
||||||
|
|
||||||
public class MainActivity extends Activity {
|
public class MainActivity extends Activity {
|
||||||
|
|
||||||
// markdown, mdown, mkdn, mdwn, mkd, md
|
|
||||||
// markdown, mdown, mkdn, mkd, md, text
|
|
||||||
|
|
||||||
static {
|
static {
|
||||||
Debug.init(new AndroidLogDebugOutput(true));
|
Debug.init(new AndroidLogDebugOutput(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
// private List<Target> targets = new ArrayList<>();
|
@Inject
|
||||||
|
MarkdownLoader markdownLoader;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
MarkdownRenderer markdownRenderer;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
Themes themes;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
App.component(this)
|
||||||
|
.mainActivitySubcomponent()
|
||||||
|
.inject(this);
|
||||||
|
|
||||||
|
themes.apply(this);
|
||||||
|
|
||||||
|
// how can we obtain SpannableConfiguration after theme was applied?
|
||||||
|
|
||||||
setContentView(R.layout.activity_main);
|
setContentView(R.layout.activity_main);
|
||||||
|
|
||||||
final TextView textView = (TextView) findViewById(R.id.activity_main);
|
|
||||||
|
|
||||||
final AsyncDrawable.Loader loader = new AsyncDrawableLoader(textView);
|
final AppBarItem.Renderer appBarRenderer
|
||||||
|
= new AppBarItem.Renderer(findViewById(R.id.app_bar), new View.OnClickListener() {
|
||||||
new Thread(new Runnable() {
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void onClick(View v) {
|
||||||
InputStream stream = null;
|
themes.toggle();
|
||||||
Scanner scanner = null;
|
recreate();
|
||||||
String md = null;
|
|
||||||
try {
|
|
||||||
stream = getAssets().open("scrollable.md");
|
|
||||||
// stream = getAssets().open("test.md");
|
|
||||||
scanner = new Scanner(stream).useDelimiter("\\A");
|
|
||||||
if (scanner.hasNext()) {
|
|
||||||
md = scanner.next();
|
|
||||||
}
|
|
||||||
} catch (Throwable t) {
|
|
||||||
Debug.e(t);
|
|
||||||
} finally {
|
|
||||||
if (stream != null) {
|
|
||||||
try {
|
|
||||||
stream.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (scanner != null) {
|
|
||||||
scanner.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (md != null) {
|
final TextView textView = Views.findView(this, R.id.text);
|
||||||
|
final View progress = findViewById(R.id.progress);
|
||||||
|
|
||||||
final long start = SystemClock.uptimeMillis();
|
appBarRenderer.render(appBarState());
|
||||||
|
|
||||||
final SpannableConfiguration configuration = SpannableConfiguration.builder(MainActivity.this)
|
markdownLoader.load(uri(), new MarkdownLoader.OnMarkdownTextLoaded() {
|
||||||
.asyncDrawableLoader(loader)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
final CharSequence text = Markwon.markdown(configuration, md);
|
|
||||||
|
|
||||||
final long end = SystemClock.uptimeMillis();
|
|
||||||
Debug.i("Rendered: %d ms, length: %d", end - start, text.length());
|
|
||||||
|
|
||||||
textView.post(new Runnable() {
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void apply(String text) {
|
||||||
// NB! LinkMovementMethod forces frequent updates...
|
markdownRenderer.render(MainActivity.this, text, new MarkdownRenderer.MarkdownReadyListener() {
|
||||||
textView.setMovementMethod(LinkMovementMethod.getInstance());
|
@Override
|
||||||
textView.setText(text);
|
public void onMarkdownReady(CharSequence markdown) {
|
||||||
Markwon.scheduleDrawables(textView);
|
Markwon.setText(textView, markdown);
|
||||||
|
Views.setVisible(progress, false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}).start();
|
|
||||||
|
private AppBarItem.State appBarState() {
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final String subtitle;
|
||||||
|
|
||||||
|
// two possible states: just opened from launcher (no subtitle)
|
||||||
|
// opened to display external resource (subtitle as a path/url/whatever)
|
||||||
|
|
||||||
|
final Uri uri = uri();
|
||||||
|
|
||||||
|
Debug.i(uri);
|
||||||
|
|
||||||
|
if (uri != null) {
|
||||||
|
title = uri.getLastPathSegment();
|
||||||
|
subtitle = uri.toString();
|
||||||
|
} else {
|
||||||
|
title = getString(R.string.app_name);
|
||||||
|
subtitle = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AppBarItem.State(title, subtitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Uri uri() {
|
||||||
|
final Intent intent = getIntent();
|
||||||
|
return intent != null
|
||||||
|
? intent.getData()
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
|
||||||
|
markdownLoader.cancel();
|
||||||
|
markdownRenderer.cancel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
package ru.noties.markwon;
|
||||||
|
|
||||||
|
import dagger.Subcomponent;
|
||||||
|
|
||||||
|
@Subcomponent
|
||||||
|
@ActivityScope
|
||||||
|
interface MainActivitySubcomponent {
|
||||||
|
void inject(MainActivity activity);
|
||||||
|
}
|
193
app/src/main/java/ru/noties/markwon/MarkdownLoader.java
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
package ru.noties.markwon;
|
||||||
|
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
import okhttp3.ResponseBody;
|
||||||
|
import ru.noties.debug.Debug;
|
||||||
|
|
||||||
|
@ActivityScope
|
||||||
|
public class MarkdownLoader {
|
||||||
|
|
||||||
|
public interface OnMarkdownTextLoaded {
|
||||||
|
void apply(String text);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
Context context;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ExecutorService service;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
Handler handler;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
OkHttpClient client;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
UrlProvider urlProvider;
|
||||||
|
|
||||||
|
private Future<?> task;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
MarkdownLoader() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public void load(@Nullable final Uri uri, @NonNull final OnMarkdownTextLoaded loaded) {
|
||||||
|
cancel();
|
||||||
|
task = service.submit(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
deliver(loaded, text(uri));
|
||||||
|
} catch (Throwable t) {
|
||||||
|
Debug.e(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancel() {
|
||||||
|
if (task != null) {
|
||||||
|
task.cancel(true);
|
||||||
|
task = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deliver(@NonNull final OnMarkdownTextLoaded loaded, final String text) {
|
||||||
|
if (task != null
|
||||||
|
&& !task.isCancelled()) {
|
||||||
|
handler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
loaded.apply(text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String text(@Nullable Uri uri) {
|
||||||
|
final String out;
|
||||||
|
if (uri == null) {
|
||||||
|
out = loadReadMe();
|
||||||
|
} else {
|
||||||
|
out = loadExternalResource(uri);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String loadReadMe() {
|
||||||
|
InputStream stream = null;
|
||||||
|
try {
|
||||||
|
stream = context.getAssets().open("README.md");
|
||||||
|
} catch (IOException e) {
|
||||||
|
Debug.e(e);
|
||||||
|
}
|
||||||
|
return readStream(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String loadExternalResource(@NonNull Uri uri) {
|
||||||
|
final String out;
|
||||||
|
final String scheme = uri.getScheme();
|
||||||
|
if (!TextUtils.isEmpty(scheme) && ContentResolver.SCHEME_FILE.equals(scheme)) {
|
||||||
|
out = loadExternalFile(uri);
|
||||||
|
} else {
|
||||||
|
out = loadExternalUrl(uri);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String loadExternalFile(@NonNull Uri uri) {
|
||||||
|
InputStream stream = null;
|
||||||
|
try {
|
||||||
|
stream = new FileInputStream(new File(uri.getPath()));
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
Debug.e(e);
|
||||||
|
}
|
||||||
|
return readStream(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String loadExternalUrl(@NonNull Uri uri) {
|
||||||
|
|
||||||
|
final String url = urlProvider.provide(uri);
|
||||||
|
|
||||||
|
final Request request = new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Response response = null;
|
||||||
|
try {
|
||||||
|
response = client.newCall(request).execute();
|
||||||
|
} catch (IOException e) {
|
||||||
|
Debug.e(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
final ResponseBody body = response != null
|
||||||
|
? response.body()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
String out = null;
|
||||||
|
|
||||||
|
if (body != null) {
|
||||||
|
try {
|
||||||
|
out = body.string();
|
||||||
|
} catch (IOException e) {
|
||||||
|
Debug.e(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String readStream(@Nullable InputStream inputStream) {
|
||||||
|
|
||||||
|
String out = null;
|
||||||
|
|
||||||
|
if (inputStream != null) {
|
||||||
|
BufferedReader reader = null;
|
||||||
|
try {
|
||||||
|
reader = new BufferedReader(new InputStreamReader(inputStream));
|
||||||
|
final StringBuilder builder = new StringBuilder();
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
builder.append(line)
|
||||||
|
.append('\n');
|
||||||
|
}
|
||||||
|
out = builder.toString();
|
||||||
|
} catch (IOException e) {
|
||||||
|
Debug.e(e);
|
||||||
|
} finally {
|
||||||
|
if (reader != null) {
|
||||||
|
try {
|
||||||
|
reader.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
// no op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
60
app/src/main/java/ru/noties/markwon/MarkdownRenderer.java
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package ru.noties.markwon;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
@ActivityScope
|
||||||
|
public class MarkdownRenderer {
|
||||||
|
|
||||||
|
interface MarkdownReadyListener {
|
||||||
|
void onMarkdownReady(CharSequence markdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
AsyncDrawableLoader loader;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ExecutorService service;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
Handler handler;
|
||||||
|
|
||||||
|
private Future<?> task;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
MarkdownRenderer() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public void render(@NonNull final Context context, @NonNull final String markdown, @NonNull final MarkdownReadyListener listener) {
|
||||||
|
cancel();
|
||||||
|
task = service.submit(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
final SpannableConfiguration configuration = SpannableConfiguration.builder(context)
|
||||||
|
.asyncDrawableLoader(loader)
|
||||||
|
.build();
|
||||||
|
final CharSequence text = Markwon.markdown(configuration, markdown);
|
||||||
|
handler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
listener.onMarkdownReady(text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
task = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancel() {
|
||||||
|
if (task != null) {
|
||||||
|
task.cancel(true);
|
||||||
|
task = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,74 +0,0 @@
|
|||||||
package ru.noties.markwon;
|
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.drawable.BitmapDrawable;
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.squareup.picasso.Picasso;
|
|
||||||
import com.squareup.picasso.Target;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import ru.noties.markwon.spans.AsyncDrawable;
|
|
||||||
|
|
||||||
public class TextViewTarget implements Target {
|
|
||||||
|
|
||||||
private final TextView view;
|
|
||||||
private final AsyncDrawable asyncDrawable;
|
|
||||||
|
|
||||||
public TextViewTarget(TextView view, AsyncDrawable asyncDrawable) {
|
|
||||||
this.view = view;
|
|
||||||
this.asyncDrawable = asyncDrawable;
|
|
||||||
|
|
||||||
attach();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
|
|
||||||
if (bitmap != null) {
|
|
||||||
final Drawable drawable = new BitmapDrawable(view.getResources(), bitmap);
|
|
||||||
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
|
|
||||||
asyncDrawable.setResult(drawable);
|
|
||||||
}
|
|
||||||
detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBitmapFailed(Drawable errorDrawable) {
|
|
||||||
if (errorDrawable != null) {
|
|
||||||
asyncDrawable.setResult(errorDrawable);
|
|
||||||
}
|
|
||||||
detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPrepareLoad(Drawable placeHolderDrawable) {
|
|
||||||
if (placeHolderDrawable != null) {
|
|
||||||
asyncDrawable.setResult(placeHolderDrawable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void attach() {
|
|
||||||
|
|
||||||
// amazing stuff here, in order to keep this target alive (picasso stores target in a WeakReference)
|
|
||||||
// we need to do this
|
|
||||||
|
|
||||||
//noinspection unchecked
|
|
||||||
List<TextViewTarget> list = (List<TextViewTarget>) view.getTag(R.id.amazing);
|
|
||||||
if (list == null) {
|
|
||||||
list = new ArrayList<>(2);
|
|
||||||
view.setTag(R.id.amazing, list);
|
|
||||||
}
|
|
||||||
list.add(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void detach() {
|
|
||||||
//noinspection unchecked
|
|
||||||
final List<TextViewTarget> list = (List<TextViewTarget>) view.getTag(R.id.amazing);
|
|
||||||
if (list != null) {
|
|
||||||
list.remove(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
46
app/src/main/java/ru/noties/markwon/Themes.java
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package ru.noties.markwon;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public class Themes {
|
||||||
|
|
||||||
|
private static final String PREF_NAME = "theme";
|
||||||
|
private static final String KEY_THEME_DARK = "key.tD";
|
||||||
|
|
||||||
|
private SharedPreferences preferences;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
Themes(Context context) {
|
||||||
|
this.preferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void apply(@NonNull Context context) {
|
||||||
|
final boolean dark = preferences.getBoolean(KEY_THEME_DARK, false);
|
||||||
|
// we have only 2 themes and Light one is default, so no need to apply it
|
||||||
|
final int theme;
|
||||||
|
if (dark) {
|
||||||
|
theme = R.style.AppThemeBaseDark;
|
||||||
|
} else {
|
||||||
|
theme = R.style.AppThemeBaseLight;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Context appContext = context.getApplicationContext();
|
||||||
|
if (appContext != context) {
|
||||||
|
appContext.setTheme(theme);
|
||||||
|
}
|
||||||
|
context.setTheme(theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void toggle() {
|
||||||
|
final boolean newValue = !preferences.getBoolean(KEY_THEME_DARK, false);
|
||||||
|
preferences.edit()
|
||||||
|
.putBoolean(KEY_THEME_DARK, newValue)
|
||||||
|
.apply();
|
||||||
|
}
|
||||||
|
}
|
8
app/src/main/java/ru/noties/markwon/UrlProvider.java
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package ru.noties.markwon;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
public interface UrlProvider {
|
||||||
|
String provide(@NonNull Uri uri);
|
||||||
|
}
|
42
app/src/main/java/ru/noties/markwon/UrlProviderImpl.java
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package ru.noties.markwon;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
class UrlProviderImpl implements UrlProvider {
|
||||||
|
|
||||||
|
private static final String GITHUB = "github.com";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String provide(@NonNull Uri uri) {
|
||||||
|
|
||||||
|
// hm... github, even having a README.md in path will return rendered HTML
|
||||||
|
|
||||||
|
if (GITHUB.equals(uri.getAuthority())) {
|
||||||
|
final List<String> segments = uri.getPathSegments();
|
||||||
|
if (segments != null
|
||||||
|
&& segments.contains("blob")) {
|
||||||
|
// we need to modify the final uri
|
||||||
|
final Uri.Builder builder = new Uri.Builder()
|
||||||
|
.scheme(uri.getScheme())
|
||||||
|
.authority(uri.getAuthority())
|
||||||
|
.fragment(uri.getFragment())
|
||||||
|
.query(uri.getQuery());
|
||||||
|
for (String segment: segments) {
|
||||||
|
final String part;
|
||||||
|
if ("blob".equals(segment)) {
|
||||||
|
part = "raw";
|
||||||
|
} else {
|
||||||
|
part = segment;
|
||||||
|
}
|
||||||
|
builder.appendPath(part);
|
||||||
|
}
|
||||||
|
uri = builder.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return uri.toString();
|
||||||
|
}
|
||||||
|
}
|
36
app/src/main/java/ru/noties/markwon/Views.java
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package ru.noties.markwon;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.support.annotation.IdRes;
|
||||||
|
import android.support.annotation.IntDef;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
public abstract class Views {
|
||||||
|
|
||||||
|
@IntDef({View.INVISIBLE, View.GONE})
|
||||||
|
@interface NotVisible {}
|
||||||
|
|
||||||
|
public static <V extends View> V findView(@NonNull View view, @IdRes int id) {
|
||||||
|
//noinspection unchecked
|
||||||
|
return (V) view.findViewById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <V extends View> V findView(@NonNull Activity activity, @IdRes int id) {
|
||||||
|
//noinspection unchecked
|
||||||
|
return (V) activity.findViewById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setVisible(@NonNull View view, boolean visible) {
|
||||||
|
setVisible(view, visible, View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setVisible(@NonNull View view, boolean visible, @NotVisible int notVisible) {
|
||||||
|
final int visibility = visible
|
||||||
|
? View.VISIBLE
|
||||||
|
: notVisible;
|
||||||
|
view.setVisibility(visibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Views() {}
|
||||||
|
}
|
11
app/src/main/res/drawable/bg_app_bar_shadow.xml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
|
||||||
|
<gradient
|
||||||
|
android:startColor="#40000000"
|
||||||
|
android:endColor="#00000000"
|
||||||
|
android:angle="270"/>
|
||||||
|
|
||||||
|
</shape>
|
9
app/src/main/res/drawable/ic_app_bar_theme_dark.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="oval">
|
||||||
|
|
||||||
|
<solid
|
||||||
|
android:color="@color/colorPrimaryDark"/>
|
||||||
|
|
||||||
|
</shape>
|
9
app/src/main/res/drawable/ic_app_bar_theme_light.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="oval">
|
||||||
|
|
||||||
|
<solid
|
||||||
|
android:color="#EEE"/>
|
||||||
|
|
||||||
|
</shape>
|
@ -1,19 +1,97 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<ScrollView
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:id="@+id/scroll_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginTop="?android:attr/actionBarSize">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/activity_main"
|
android:id="@+id/text"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="16dip"
|
android:layout_margin="16dip"
|
||||||
android:textSize="16sp"
|
|
||||||
android:lineSpacingExtra="2dip"
|
android:lineSpacingExtra="2dip"
|
||||||
android:textColor="#333"
|
android:textSize="16sp"
|
||||||
tools:context="ru.noties.markwon.MainActivity"
|
tools:context="ru.noties.markwon.MainActivity"
|
||||||
tools:text="yo\nman" />
|
tools:text="yo\nman" />
|
||||||
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/progress"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginTop="?android:attr/actionBarSize">
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:layout_width="64dip"
|
||||||
|
android:layout_height="64dip"
|
||||||
|
android:layout_gravity="center" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/app_bar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?android:attr/actionBarSize"
|
||||||
|
android:background="@color/colorPrimary"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingLeft="16dip">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0px"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/app_bar_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:text="@string/app_name"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||||
|
android:textColor="#fff"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/app_bar_subtitle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||||
|
android:textColor="#fff"
|
||||||
|
tools:text="This is a subtitle, hello there are we long enogh now already?" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/app_bar_theme_changer"
|
||||||
|
android:layout_width="?android:attr/actionBarSize"
|
||||||
|
android:layout_height="?android:attr/actionBarSize"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:padding="20dip"
|
||||||
|
android:src="?attr/ic_app_bar_theme" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="8dip"
|
||||||
|
android:background="@drawable/bg_app_bar_shadow" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 7.3 KiB |
@ -1,10 +1,18 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<style name="AppThemeBase" parent="android:Theme.Material.Light">
|
<style name="AppThemeBaseDark" parent="android:Theme.Material.NoActionBar">
|
||||||
<item name="android:colorAccent">@color/colorAccent</item>
|
<item name="android:colorAccent">@color/colorAccent</item>
|
||||||
<item name="android:colorPrimary">@color/colorPrimary</item>
|
<item name="android:colorPrimary">@color/colorPrimary</item>
|
||||||
<item name="android:colorPrimaryDark">@color/colorPrimaryDark</item>
|
<item name="android:colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
|
<item name="ic_app_bar_theme">@drawable/ic_app_bar_theme_light</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="AppThemeBaseLight" parent="android:Theme.Material.Light.NoActionBar">
|
||||||
|
<item name="android:colorAccent">@color/colorAccent</item>
|
||||||
|
<item name="android:colorPrimary">@color/colorPrimary</item>
|
||||||
|
<item name="android:colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
|
<item name="ic_app_bar_theme">@drawable/ic_app_bar_theme_dark</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
@ -1,8 +1,16 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<style name="AppThemeBase" parent="android:Theme.Holo.Light"/>
|
<attr name="ic_app_bar_theme" format="reference"/>
|
||||||
|
|
||||||
<!-- Base application theme. -->
|
<style name="AppThemeBaseDark" parent="android:Theme.Holo.NoActionBar">
|
||||||
<style name="AppTheme" parent="AppThemeBase"/>
|
<item name="ic_app_bar_theme">@drawable/ic_app_bar_theme_light</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="AppThemeBaseLight" parent="android:Theme.Holo.Light">
|
||||||
|
<item name="ic_app_bar_theme">@drawable/ic_app_bar_theme_dark</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="AppThemeLight" parent="AppThemeBaseLight" />
|
||||||
|
<style name="AppThemeDark" parent="AppThemeBaseDark" />
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -22,8 +22,7 @@ abstract class DrawablesScheduler {
|
|||||||
|
|
||||||
static void schedule(@NonNull final TextView textView) {
|
static void schedule(@NonNull final TextView textView) {
|
||||||
|
|
||||||
final List<AsyncDrawable> list = extract(textView);
|
final List<Pair> list = extract(textView, true);
|
||||||
Debug.i(list);
|
|
||||||
if (list.size() > 0) {
|
if (list.size() > 0) {
|
||||||
textView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
|
textView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
|
||||||
@Override
|
@Override
|
||||||
@ -38,24 +37,22 @@ abstract class DrawablesScheduler {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
for (AsyncDrawable d : list) {
|
for (Pair pair : list) {
|
||||||
Debug.i(d);
|
pair.drawable.setCallback2(new DrawableCallbackImpl(textView, pair.coordinatesProvider, pair.drawable.getBounds()));
|
||||||
d.setCallback2(new DrawableCallbackImpl(textView, null, d.getBounds()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// must be called when text manually changed in TextView
|
// must be called when text manually changed in TextView
|
||||||
static void unschedule(@NonNull TextView view) {
|
static void unschedule(@NonNull TextView view) {
|
||||||
Debug.i();
|
for (Pair pair : extract(view, false)) {
|
||||||
for (AsyncDrawable d : extract(view)) {
|
pair.drawable.setCallback2(null);
|
||||||
d.setCallback2(null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<AsyncDrawable> extract(@NonNull TextView view) {
|
private static List<Pair> extract(@NonNull TextView view, boolean coordinates) {
|
||||||
|
|
||||||
final List<AsyncDrawable> list;
|
final List<Pair> list;
|
||||||
|
|
||||||
final CharSequence cs = view.getText();
|
final CharSequence cs = view.getText();
|
||||||
final int length = cs != null
|
final int length = cs != null
|
||||||
@ -75,14 +72,20 @@ abstract class DrawablesScheduler {
|
|||||||
|
|
||||||
for (Object span : spans) {
|
for (Object span : spans) {
|
||||||
if (span instanceof AsyncDrawableSpan) {
|
if (span instanceof AsyncDrawableSpan) {
|
||||||
list.add(((AsyncDrawableSpan) span).getDrawable());
|
|
||||||
|
final AsyncDrawableSpan asyncDrawableSpan = (AsyncDrawableSpan) span;
|
||||||
|
final CoordinatesProvider provider = coordinates
|
||||||
|
? new AsyncDrawableSpanCoordinatesProvider(asyncDrawableSpan)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
list.add(new Pair(asyncDrawableSpan.getDrawable(), provider));
|
||||||
} else if (span instanceof DynamicDrawableSpan) {
|
} else if (span instanceof DynamicDrawableSpan) {
|
||||||
// it's really not optimal thing because it stores Drawable in WeakReference...
|
// it's really not optimal thing because it stores Drawable in WeakReference...
|
||||||
// which is why it will be most likely already de-referenced...
|
// which is why it will be most likely already de-referenced...
|
||||||
final Drawable d = ((DynamicDrawableSpan) span).getDrawable();
|
final Drawable d = ((DynamicDrawableSpan) span).getDrawable();
|
||||||
if (d != null
|
if (d != null
|
||||||
&& d instanceof AsyncDrawable) {
|
&& d instanceof AsyncDrawable) {
|
||||||
list.add((AsyncDrawable) d);
|
list.add(new Pair((AsyncDrawable) d, null));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -100,6 +103,7 @@ abstract class DrawablesScheduler {
|
|||||||
|
|
||||||
private interface CoordinatesProvider {
|
private interface CoordinatesProvider {
|
||||||
int getX();
|
int getX();
|
||||||
|
|
||||||
int getY();
|
int getY();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,21 +142,30 @@ abstract class DrawablesScheduler {
|
|||||||
view.setText(view.getText());
|
view.setText(view.getText());
|
||||||
previousBounds = new Rect(rect);
|
previousBounds = new Rect(rect);
|
||||||
} else {
|
} else {
|
||||||
// // if bounds are the same then simple invalidate would do
|
|
||||||
// if (coordinatesProvider != null) {
|
// if bounds are the same then simple invalidate would do
|
||||||
// final int x = coordinatesProvider.getX();
|
|
||||||
// final int y = coordinatesProvider.getY();
|
if (coordinatesProvider != null) {
|
||||||
// view.postInvalidate(
|
final int x = coordinatesProvider.getX();
|
||||||
// x + rect.left,
|
final int y = coordinatesProvider.getY();
|
||||||
// y + rect.top,
|
view.postInvalidate(
|
||||||
// x + rect.right,
|
x + rect.left,
|
||||||
// y + rect.bottom
|
y + rect.top,
|
||||||
// );
|
x + rect.right,
|
||||||
// } else {
|
y + rect.bottom
|
||||||
// // else all we can do is request full re-draw... maybe system is smart enough not re-draw what is not on screen?
|
);
|
||||||
// view.postInvalidate();
|
Debug.i(x + rect.left,
|
||||||
// }
|
y + rect.top,
|
||||||
|
x + rect.right,
|
||||||
|
y + rect.bottom);
|
||||||
|
} else {
|
||||||
|
Debug.i();
|
||||||
|
// else all we can do is request full re-draw... maybe system is smart enough not re-draw what is not on screen?
|
||||||
view.postInvalidate();
|
view.postInvalidate();
|
||||||
|
// we do not need to invalidate if, for example, a gif is playing somewhere out of current viewPort...
|
||||||
|
// but i do not see...
|
||||||
|
}
|
||||||
|
// view.postInvalidate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,11 +181,11 @@ abstract class DrawablesScheduler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class AsyncDrawableCoordinatesProvider implements CoordinatesProvider {
|
private static class AsyncDrawableSpanCoordinatesProvider implements CoordinatesProvider {
|
||||||
|
|
||||||
private final AsyncDrawableSpan span;
|
private final AsyncDrawableSpan span;
|
||||||
|
|
||||||
private AsyncDrawableCoordinatesProvider(AsyncDrawableSpan span) {
|
private AsyncDrawableSpanCoordinatesProvider(AsyncDrawableSpan span) {
|
||||||
this.span = span;
|
this.span = span;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,4 +199,14 @@ abstract class DrawablesScheduler {
|
|||||||
return span.lastKnownDrawY();
|
return span.lastKnownDrawY();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class Pair {
|
||||||
|
final AsyncDrawable drawable;
|
||||||
|
final CoordinatesProvider coordinatesProvider;
|
||||||
|
|
||||||
|
Pair(AsyncDrawable drawable, CoordinatesProvider coordinatesProvider) {
|
||||||
|
this.drawable = drawable;
|
||||||
|
this.coordinatesProvider = coordinatesProvider;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import android.content.Context;
|
|||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import android.text.method.LinkMovementMethod;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension;
|
import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension;
|
||||||
@ -17,6 +18,62 @@ import ru.noties.markwon.renderer.SpannableRenderer;
|
|||||||
@SuppressWarnings("WeakerAccess")
|
@SuppressWarnings("WeakerAccess")
|
||||||
public abstract class Markwon {
|
public abstract class Markwon {
|
||||||
|
|
||||||
|
public static Parser createParser() {
|
||||||
|
return new Parser.Builder()
|
||||||
|
.extensions(Collections.singleton(StrikethroughExtension.create()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setMarkdown(@NonNull TextView view, @NonNull String markdown) {
|
||||||
|
setMarkdown(view, SpannableConfiguration.create(view.getContext()), markdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setMarkdown(
|
||||||
|
@NonNull TextView view,
|
||||||
|
@NonNull SpannableConfiguration configuration,
|
||||||
|
@Nullable String markdown
|
||||||
|
) {
|
||||||
|
|
||||||
|
setText(view, markdown(configuration, markdown));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setText(@NonNull TextView view, CharSequence text) {
|
||||||
|
|
||||||
|
unscheduleDrawables(view);
|
||||||
|
|
||||||
|
// update movement method (for links to be clickable)
|
||||||
|
view.setMovementMethod(LinkMovementMethod.getInstance());
|
||||||
|
view.setText(text);
|
||||||
|
|
||||||
|
// schedule drawables (dynamic drawables that can change bounds/animate will be correctly updated)
|
||||||
|
scheduleDrawables(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
// with default configuration
|
||||||
|
public static CharSequence markdown(@NonNull Context context, @Nullable String markdown) {
|
||||||
|
final CharSequence out;
|
||||||
|
if (TextUtils.isEmpty(markdown)) {
|
||||||
|
out = null;
|
||||||
|
} else {
|
||||||
|
final SpannableConfiguration configuration = SpannableConfiguration.create(context);
|
||||||
|
out = markdown(configuration, markdown);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CharSequence markdown(@NonNull SpannableConfiguration configuration, @Nullable String markdown) {
|
||||||
|
final CharSequence out;
|
||||||
|
if (TextUtils.isEmpty(markdown)) {
|
||||||
|
out = null;
|
||||||
|
} else {
|
||||||
|
final Parser parser = createParser();
|
||||||
|
final Node node = parser.parse(markdown);
|
||||||
|
final SpannableRenderer renderer = new SpannableRenderer();
|
||||||
|
out = renderer.render(configuration, node);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
public static void scheduleDrawables(@NonNull TextView view) {
|
public static void scheduleDrawables(@NonNull TextView view) {
|
||||||
DrawablesScheduler.schedule(view);
|
DrawablesScheduler.schedule(view);
|
||||||
}
|
}
|
||||||
@ -25,33 +82,6 @@ public abstract class Markwon {
|
|||||||
DrawablesScheduler.unschedule(view);
|
DrawablesScheduler.unschedule(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
// with default configuration
|
|
||||||
public static CharSequence markdown(@NonNull Context context, @Nullable String text) {
|
|
||||||
final CharSequence out;
|
|
||||||
if (TextUtils.isEmpty(text)) {
|
|
||||||
out = null;
|
|
||||||
} else {
|
|
||||||
final SpannableConfiguration configuration = SpannableConfiguration.create(context);
|
|
||||||
out = markdown(configuration, text);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static CharSequence markdown(@NonNull SpannableConfiguration configuration, @Nullable String text) {
|
|
||||||
final CharSequence out;
|
|
||||||
if (TextUtils.isEmpty(text)) {
|
|
||||||
out = null;
|
|
||||||
} else {
|
|
||||||
final Parser parser = new Parser.Builder()
|
|
||||||
.extensions(Collections.singleton(StrikethroughExtension.create()))
|
|
||||||
.build();
|
|
||||||
final Node node = parser.parse(text);
|
|
||||||
final SpannableRenderer renderer = new SpannableRenderer();
|
|
||||||
out = renderer.render(configuration, node);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Markwon() {
|
private Markwon() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package ru.noties.markwon.spans;
|
|||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.ColorFilter;
|
import android.graphics.ColorFilter;
|
||||||
import android.graphics.PixelFormat;
|
import android.graphics.PixelFormat;
|
||||||
|
import android.graphics.drawable.Animatable;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.support.annotation.IntRange;
|
import android.support.annotation.IntRange;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
@ -54,7 +55,13 @@ public class AsyncDrawable extends Drawable {
|
|||||||
loader.load(destination, this);
|
loader.load(destination, this);
|
||||||
} else {
|
} else {
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
|
|
||||||
result.setCallback(null);
|
result.setCallback(null);
|
||||||
|
|
||||||
|
// let's additionally stop if it Animatable
|
||||||
|
if (result instanceof Animatable) {
|
||||||
|
((Animatable) result).stop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
loader.cancel(destination);
|
loader.cancel(destination);
|
||||||
}
|
}
|
||||||
|