Working with sample app

This commit is contained in:
Dimitry Ivanov 2017-05-18 16:44:41 +03:00
parent 99f2879f6a
commit 0dfc968464
35 changed files with 1180 additions and 639 deletions

109
README.md Normal file
View 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

View File

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

View File

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

View File

@ -1,282 +0,0 @@
![logo](https://github.com/noties/Scrollable/raw/master/art/scrollable_big_logo.png)
[![Maven Central](https://img.shields.io/maven-central/v/ru.noties/scrollable.svg)](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.
```

View File

@ -1,114 +0,0 @@
![logo](https://github.com/noties/Scrollable/raw/master/art/scrollable_big_logo.png)
# Hello!
**bold *italic*** _just italic_
[![Maven Central](https://img.shields.io/maven-central/v/ru.noties/scrollable.svg)](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#

View File

@ -0,0 +1,7 @@
package ru.noties.markwon;
import javax.inject.Scope;
@Scope
@interface ActivityScope {
}

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

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

View 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();
}

View 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);
}
}

View File

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

View 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);
// }
// }
}

View File

@ -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 long start = SystemClock.uptimeMillis();
final SpannableConfiguration configuration = SpannableConfiguration.builder(MainActivity.this)
.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
public void run() {
// NB! LinkMovementMethod forces frequent updates...
textView.setMovementMethod(LinkMovementMethod.getInstance());
textView.setText(text);
Markwon.scheduleDrawables(textView);
}
});
}
} }
}).start(); });
final TextView textView = Views.findView(this, R.id.text);
final View progress = findViewById(R.id.progress);
appBarRenderer.render(appBarState());
markdownLoader.load(uri(), new MarkdownLoader.OnMarkdownTextLoaded() {
@Override
public void apply(String text) {
markdownRenderer.render(MainActivity.this, text, new MarkdownRenderer.MarkdownReadyListener() {
@Override
public void onMarkdownReady(CharSequence markdown) {
Markwon.setText(textView, markdown);
Views.setVisible(progress, false);
}
});
}
});
}
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();
} }
} }

View File

@ -0,0 +1,9 @@
package ru.noties.markwon;
import dagger.Subcomponent;
@Subcomponent
@ActivityScope
interface MainActivitySubcomponent {
void inject(MainActivity activity);
}

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

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

View File

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

View 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();
}
}

View 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);
}

View 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();
}
}

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

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

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

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

View File

@ -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">
<TextView <ScrollView
android:id="@+id/activity_main" android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="?android:attr/actionBarSize">
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dip"
android:lineSpacingExtra="2dip"
android:textSize="16sp"
tools:context="ru.noties.markwon.MainActivity"
tools:text="yo\nman" />
</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_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="16dip" android:orientation="vertical">
android:textSize="16sp"
android:lineSpacingExtra="2dip"
android:textColor="#333"
tools:context="ru.noties.markwon.MainActivity"
tools:text="yo\nman" />
</ScrollView> <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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

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

View File

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

View File

@ -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,
view.postInvalidate(); 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();
// 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;
}
}
} }

View File

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

View File

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