diff --git a/README.md b/README.md new file mode 100644 index 00000000..84051ecf --- /dev/null +++ b/README.md @@ -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 (``, ``, ``, ``) +* * Strong emphasis (``, ``) +* * SuperScript (``) +* * SubScript (``) +* * Underline (``) +* * Strike-through (``, ``, ``) + * other inline html is rendered via (`Html.fromHtml(...)`) + +### Emphasis +*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 + +### Strong emphasis +**Lorem ipsum dolor sit amet** +__Lorem ipsum dolor sit amet__ +Lorem ipsum dolor sit amet +Lorem ipsum dolor sit amet + +### Strike-through +~~Lorem ipsum dolor sit amet~~ +Lorem ipsum dolor sit amet +Lorem ipsum dolor sit amet +Lorem ipsum dolor sit amet + +--- +# Header 1 +## Header 2 +### Header 3 +#### Header 4 +##### Header 5 +###### Header 6 +--- + +### Links +[click me](https://github.com) +[click me][1] +[click me][github] +click me + +### 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 \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 8078c22c..608dece2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,4 +21,6 @@ dependencies { compile 'com.caverock:androidsvg:1.2.1' compile 'pl.droidsonroids.gif:android-gif-drawable:1.2.7' compile 'com.squareup.okhttp3:okhttp:3.8.0' + compile 'com.google.dagger:dagger:2.10' + annotationProcessor 'com.google.dagger:dagger-compiler:2.10' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 016c3e5d..bea26fe9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,22 +1,67 @@ - - + + android:theme="@style/AppThemeLight"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/assets/scrollable.md b/app/src/main/assets/scrollable.md deleted file mode 100644 index b6b67ecb..00000000 --- a/app/src/main/assets/scrollable.md +++ /dev/null @@ -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. - - -colorful_sample custom_overscroll_sample dialog_sample - -*Serving suggestion - -## Installation -```groovy -compile 'ru.noties:scrollable:1.3.0` -``` - -## Usage - -To start using this library `ScrollableLayout` must be aded to your layout. - -```xml - - - - - - - - - - -``` - -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. -``` diff --git a/app/src/main/assets/test.md b/app/src/main/assets/test.md deleted file mode 100644 index b0f1dd9e..00000000 --- a/app/src/main/assets/test.md +++ /dev/null @@ -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 - -

Yo! -Omg - - -ddffdg -

- -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 - - -jo - - -#### Code block - -```java -final String s = "this id code block"; -s.length() > 0; -``` ---- -okay, have a good day! - -Yo**2**2242 - -To compare~~13~~ - -~~Just strike it~~ - -



- -RED - -**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# \ No newline at end of file diff --git a/app/src/main/java/ru/noties/markwon/ActivityScope.java b/app/src/main/java/ru/noties/markwon/ActivityScope.java new file mode 100644 index 00000000..de2920a7 --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/ActivityScope.java @@ -0,0 +1,7 @@ +package ru.noties.markwon; + +import javax.inject.Scope; + +@Scope +@interface ActivityScope { +} diff --git a/app/src/main/java/ru/noties/markwon/App.java b/app/src/main/java/ru/noties/markwon/App.java new file mode 100644 index 00000000..6b185d78 --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/App.java @@ -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; + } +} diff --git a/app/src/main/java/ru/noties/markwon/AppBarItem.java b/app/src/main/java/ru/noties/markwon/AppBarItem.java new file mode 100644 index 00000000..c4960a2c --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/AppBarItem.java @@ -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() { + } +} diff --git a/app/src/main/java/ru/noties/markwon/AppComponent.java b/app/src/main/java/ru/noties/markwon/AppComponent.java new file mode 100644 index 00000000..1ff78c29 --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/AppComponent.java @@ -0,0 +1,11 @@ +package ru.noties.markwon; + +import javax.inject.Singleton; + +import dagger.Component; + +@Component(modules = AppModule.class) +@Singleton +interface AppComponent { + MainActivitySubcomponent mainActivitySubcomponent(); +} diff --git a/app/src/main/java/ru/noties/markwon/AppModule.java b/app/src/main/java/ru/noties/markwon/AppModule.java new file mode 100644 index 00000000..d9868304 --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/AppModule.java @@ -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); + } +} diff --git a/app/src/main/java/ru/noties/markwon/AsyncDrawableLoader.java b/app/src/main/java/ru/noties/markwon/AsyncDrawableLoader.java index 4a3f931f..d349e498 100644 --- a/app/src/main/java/ru/noties/markwon/AsyncDrawableLoader.java +++ b/app/src/main/java/ru/noties/markwon/AsyncDrawableLoader.java @@ -1,70 +1,82 @@ 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.PictureDrawable; -import android.net.Uri; import android.support.annotation.NonNull; -import android.widget.TextView; import com.caverock.androidsvg.SVG; import com.squareup.picasso.Picasso; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.Future; -import okhttp3.OkHttpClient; +import javax.inject.Inject; + import pl.droidsonroids.gif.GifDrawable; import ru.noties.debug.Debug; import ru.noties.markwon.spans.AsyncDrawable; +@ActivityScope public class AsyncDrawableLoader implements AsyncDrawable.Loader { - private final TextView view; - private final Picasso picasso; - private final OkHttpClient client; - private final ExecutorService executorService; - private final Map> requests; + @Inject + Resources resources; + + @Inject + Picasso picasso; + + @Inject + ExecutorService executorService; + + private final Map> requests = new HashMap<>(3); + private final CopyOnWriteArrayList targets = new CopyOnWriteArrayList<>(); // sh*t.. - public AsyncDrawableLoader(TextView view) { - this.view = view; - 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); + @Inject + public AsyncDrawableLoader() { } @Override public void load(@NonNull String destination, @NonNull AsyncDrawable drawable) { - Debug.i("destination: %s", destination); - if (destination.endsWith(".svg")) { // load svg requests.put(destination, loadSvg(destination, drawable)); } else if (destination.endsWith(".gif")) { requests.put(destination, loadGif(destination, drawable)); } 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 .load(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); picasso.cancelTag(destination); - final Future future = requests.get(destination); + final Future future = requests.remove(destination); if (future != null) { future.cancel(true); } @@ -84,12 +96,27 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader { @Override public void run() { try { + final URL url = new URL(destination); final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); final InputStream inputStream = connection.getInputStream(); + final SVG svg = SVG.getFromInputStream(inputStream); - final Picture picture = svg.renderToPicture(); - final Drawable drawable = new PictureDrawable(picture); + final float w = svg.getDocumentWidth(); + 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()); asyncDrawable.setResult(drawable); @@ -105,9 +132,11 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader { @Override public void run() { try { + final URL url = new URL(destination); final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); final InputStream inputStream = connection.getInputStream(); + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); final byte[] buffer = new byte[1024 * 8]; int read; diff --git a/app/src/main/java/ru/noties/markwon/AsyncDrawableTarget.java b/app/src/main/java/ru/noties/markwon/AsyncDrawableTarget.java new file mode 100644 index 00000000..1de0eaa1 --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/AsyncDrawableTarget.java @@ -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 list = (List) 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 list = (List) view.getTag(R.id.amazing); +// if (list != null) { +// list.remove(this); +// } +// } +} diff --git a/app/src/main/java/ru/noties/markwon/MainActivity.java b/app/src/main/java/ru/noties/markwon/MainActivity.java index 96e811a1..bda7c876 100644 --- a/app/src/main/java/ru/noties/markwon/MainActivity.java +++ b/app/src/main/java/ru/noties/markwon/MainActivity.java @@ -1,90 +1,110 @@ package ru.noties.markwon; import android.app.Activity; +import android.content.Intent; +import android.net.Uri; import android.os.Bundle; -import android.os.SystemClock; -import android.text.method.LinkMovementMethod; +import android.view.View; import android.widget.TextView; -import java.io.IOException; -import java.io.InputStream; -import java.util.Scanner; +import javax.inject.Inject; import ru.noties.debug.AndroidLogDebugOutput; import ru.noties.debug.Debug; -import ru.noties.markwon.spans.AsyncDrawable; public class MainActivity extends Activity { - // markdown, mdown, mkdn, mdwn, mkd, md - // markdown, mdown, mkdn, mkd, md, text - static { Debug.init(new AndroidLogDebugOutput(true)); } -// private List targets = new ArrayList<>(); + @Inject + MarkdownLoader markdownLoader; + + @Inject + MarkdownRenderer markdownRenderer; + + @Inject + Themes themes; @Override protected void onCreate(Bundle 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); - final TextView textView = (TextView) findViewById(R.id.activity_main); - final AsyncDrawable.Loader loader = new AsyncDrawableLoader(textView); - - new Thread(new Runnable() { + final AppBarItem.Renderer appBarRenderer + = new AppBarItem.Renderer(findViewById(R.id.app_bar), new View.OnClickListener() { @Override - public void run() { - InputStream stream = null; - Scanner scanner = null; - 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); - } - }); - } + public void onClick(View v) { + themes.toggle(); + recreate(); } - }).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(); } } diff --git a/app/src/main/java/ru/noties/markwon/MainActivitySubcomponent.java b/app/src/main/java/ru/noties/markwon/MainActivitySubcomponent.java new file mode 100644 index 00000000..077fcd31 --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/MainActivitySubcomponent.java @@ -0,0 +1,9 @@ +package ru.noties.markwon; + +import dagger.Subcomponent; + +@Subcomponent +@ActivityScope +interface MainActivitySubcomponent { + void inject(MainActivity activity); +} diff --git a/app/src/main/java/ru/noties/markwon/MarkdownLoader.java b/app/src/main/java/ru/noties/markwon/MarkdownLoader.java new file mode 100644 index 00000000..0c9f10db --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/MarkdownLoader.java @@ -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; + } +} diff --git a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java new file mode 100644 index 00000000..b6f51e67 --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java @@ -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; + } + } +} diff --git a/app/src/main/java/ru/noties/markwon/TextViewTarget.java b/app/src/main/java/ru/noties/markwon/TextViewTarget.java deleted file mode 100644 index c491beee..00000000 --- a/app/src/main/java/ru/noties/markwon/TextViewTarget.java +++ /dev/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 list = (List) 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 list = (List) view.getTag(R.id.amazing); - if (list != null) { - list.remove(this); - } - } -} diff --git a/app/src/main/java/ru/noties/markwon/Themes.java b/app/src/main/java/ru/noties/markwon/Themes.java new file mode 100644 index 00000000..05e51747 --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/Themes.java @@ -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(); + } +} diff --git a/app/src/main/java/ru/noties/markwon/UrlProvider.java b/app/src/main/java/ru/noties/markwon/UrlProvider.java new file mode 100644 index 00000000..82dc87c2 --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/UrlProvider.java @@ -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); +} diff --git a/app/src/main/java/ru/noties/markwon/UrlProviderImpl.java b/app/src/main/java/ru/noties/markwon/UrlProviderImpl.java new file mode 100644 index 00000000..d935454e --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/UrlProviderImpl.java @@ -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 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(); + } +} diff --git a/app/src/main/java/ru/noties/markwon/Views.java b/app/src/main/java/ru/noties/markwon/Views.java new file mode 100644 index 00000000..57c283ca --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/Views.java @@ -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 findView(@NonNull View view, @IdRes int id) { + //noinspection unchecked + return (V) view.findViewById(id); + } + + public static 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() {} +} diff --git a/app/src/main/res/drawable/bg_app_bar_shadow.xml b/app/src/main/res/drawable/bg_app_bar_shadow.xml new file mode 100644 index 00000000..70124dc0 --- /dev/null +++ b/app/src/main/res/drawable/bg_app_bar_shadow.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_app_bar_theme_dark.xml b/app/src/main/res/drawable/ic_app_bar_theme_dark.xml new file mode 100644 index 00000000..c3c5060d --- /dev/null +++ b/app/src/main/res/drawable/ic_app_bar_theme_dark.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_app_bar_theme_light.xml b/app/src/main/res/drawable/ic_app_bar_theme_light.xml new file mode 100644 index 00000000..b09b244b --- /dev/null +++ b/app/src/main/res/drawable/ic_app_bar_theme_light.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 3dacf3f3..b24d050e 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,19 +1,97 @@ - - + + + + + + + + + + + + + android:orientation="vertical"> - + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png index cde69bcc..cac0e405 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png index c133a0cb..25b79b68 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png index bfa42f0e..c7191395 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 324e72cd..5fda70fe 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index aee44e13..510044e8 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/values-v21/styles.xml b/app/src/main/res/values-v21/styles.xml index 4e1e75f3..1b3d1da6 100644 --- a/app/src/main/res/values-v21/styles.xml +++ b/app/src/main/res/values-v21/styles.xml @@ -1,10 +1,18 @@ - + + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 5b267e56..b8632f76 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,8 +1,16 @@ - + + + +