├── app ├── .gitignore ├── src │ └── main │ │ ├── res │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ ├── values │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ ├── values-v21 │ │ │ └── styles.xml │ │ ├── values-w820dp │ │ │ └── dimens.xml │ │ ├── menu │ │ │ └── menu_main.xml │ │ └── layout │ │ │ ├── content_main.xml │ │ │ └── activity_main.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── cn │ │ └── easydone │ │ └── componentizationapp │ │ └── MainActivity.java ├── proguard-rules.pro └── build.gradle ├── moduleA ├── .gitignore ├── src │ └── main │ │ ├── res │ │ └── values │ │ │ ├── strings.xml │ │ │ ├── colors.xml │ │ │ └── styles.xml │ │ ├── release │ │ └── AndroidManifest.xml │ │ └── debug │ │ └── AndroidManifest.xml ├── proguard-rules.pro └── build.gradle ├── Library ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ └── colors.xml │ │ │ └── color │ │ │ │ └── tab_text.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── linked │ │ │ │ └── erfli │ │ │ │ └── library │ │ │ │ ├── Library.java │ │ │ │ ├── utils │ │ │ │ ├── EventPool.java │ │ │ │ ├── operators │ │ │ │ │ ├── ThreadPool.java │ │ │ │ │ ├── OperatorConditionalBinding.java │ │ │ │ │ └── AppObservable.java │ │ │ │ ├── ConverterName.java │ │ │ │ ├── Utils.java │ │ │ │ ├── DisplayUtil.java │ │ │ │ └── AppClient.java │ │ │ │ ├── base │ │ │ │ └── BaseActivity.java │ │ │ │ └── widget │ │ │ │ ├── DividerOffsetDecoration.java │ │ │ │ ├── BackHandledFragment.java │ │ │ │ ├── WebViewProgressBar.java │ │ │ │ ├── RecyclerItemClickListener.java │ │ │ │ ├── ProgressWebView.java │ │ │ │ ├── DividerItemDecoration.java │ │ │ │ ├── RefreshLayout.java │ │ │ │ ├── CircleImageView.java │ │ │ │ └── MaterialProgressDrawable.java │ │ └── AndroidManifest.xml │ └── androidTest │ │ └── java │ │ └── com │ │ └── linked │ │ └── erfli │ │ └── library │ │ └── ExampleInstrumentedTest.java ├── proguard-rules.pro └── build.gradle ├── moduleb ├── .gitignore ├── src │ └── main │ │ └── res │ │ └── values │ │ └── strings.xml ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── ModuleB └── src │ └── main │ ├── res │ ├── drawable │ │ └── module2_news_item.xml │ └── layout │ │ ├── module2_include_toolbar.xml │ │ ├── module2_activity_recycler_view.xml │ │ ├── module2_item_zhihu.xml │ │ └── module2_activity_news_detail.xml │ ├── release │ ├── java │ │ └── com │ │ │ └── linked │ │ │ └── erfli │ │ │ └── moduleb │ │ │ └── ModuleB.java │ └── AndroidManifest.xml │ ├── debug │ ├── java │ │ └── com │ │ │ └── linked │ │ │ └── erfli │ │ │ └── moduleb │ │ │ └── ModuleB.java │ └── AndroidManifest.xml │ ├── java │ └── com │ │ └── linked │ │ └── erfli │ │ └── moduleb │ │ ├── news │ │ ├── NewsResponse.java │ │ ├── TopStory.java │ │ ├── NewsDetailResponse.java │ │ ├── NewsAdapter.java │ │ ├── Story.java │ │ ├── NewsDetailActivity.java │ │ └── NewsListActivity.java │ │ └── utils │ │ └── ZhihuApiHttp.java │ └── assets │ └── news.css ├── App └── src │ └── main │ ├── java │ └── cn │ │ └── easydone │ │ └── componentizationapp │ │ └── AppModule.java │ ├── debug │ └── java │ │ └── cn │ │ └── easydone │ │ └── componentizationapp │ │ └── ModularizationApplication.java │ └── release │ └── java │ └── cn │ └── easydone │ └── componentizationapp │ └── ModularizationApplication.java ├── ModuleA └── src │ └── main │ ├── release │ └── java │ │ └── cn │ │ └── easydone │ │ └── modulea │ │ └── ModuleA.java │ ├── debug │ └── java │ │ └── cn │ │ └── easydone │ │ └── modulea │ │ └── ModuleA.java │ ├── res │ ├── values │ │ └── dimens.xml │ └── layout │ │ ├── module1_fragment_books.xml │ │ ├── module1_fragment_detail.xml │ │ ├── module1_activity_library.xml │ │ ├── module1_book_item.xml │ │ └── module1_activity_appbar_detail.xml │ └── java │ └── cn │ └── easydone │ └── modulea │ ├── Utils │ └── BookApiHttp.java │ ├── book │ ├── ViewHolder.java │ ├── DetailFragment.java │ ├── MyAdapter.java │ ├── BooksFragment.java │ └── BookDetailActivity.java │ ├── module │ ├── BookResponse.java │ └── Book.java │ └── LibraryActivity.java ├── gradle.properties ├── README.md ├── Introduction.md ├── gradlew.bat └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /moduleA/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /Library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /moduleb/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':App', ':ModuleA', ':ModuleB', ':Library' -------------------------------------------------------------------------------- /Library/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Library 3 | 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wutongke/ModularizationApp/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /moduleA/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 图书 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wutongke/ModularizationApp/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wutongke/ModularizationApp/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wutongke/ModularizationApp/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wutongke/ModularizationApp/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wutongke/ModularizationApp/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /moduleb/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ModuleB 3 | 最新 4 | 5 | -------------------------------------------------------------------------------- /ModuleB/src/main/res/drawable/module2_news_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /Library/src/main/java/com/linked/erfli/library/Library.java: -------------------------------------------------------------------------------- 1 | package com.linked.erfli.library; 2 | 3 | import com.github.mzule.activityrouter.annotation.Module; 4 | 5 | /** 6 | * Created by erfli on 11/2/16. 7 | */ 8 | @Module("library") 9 | public class Library { 10 | } 11 | -------------------------------------------------------------------------------- /App/src/main/java/cn/easydone/componentizationapp/AppModule.java: -------------------------------------------------------------------------------- 1 | package cn.easydone.componentizationapp; 2 | 3 | import com.github.mzule.activityrouter.annotation.Module; 4 | 5 | /** 6 | * Created by erfli on 11/2/16. 7 | */ 8 | @Module("app") 9 | public class AppModule { 10 | } 11 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Dec 28 10:00:20 PST 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip 7 | -------------------------------------------------------------------------------- /moduleA/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 16dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Android组件化 3 | Settings 4 | 跳转到图书模块 5 | 跳转到知乎模块 6 | modularization 7 | 8 | -------------------------------------------------------------------------------- /ModuleA/src/main/release/java/cn/easydone/modulea/ModuleA.java: -------------------------------------------------------------------------------- 1 | package cn.easydone.modulea; 2 | 3 | import com.github.mzule.activityrouter.annotation.Module; 4 | import com.github.mzule.activityrouter.annotation.Modules; 5 | 6 | /** 7 | * Created by erfli on 11/2/16. 8 | */ 9 | @Module("moduleA") 10 | public class ModuleA { 11 | } 12 | -------------------------------------------------------------------------------- /ModuleB/src/main/release/java/com/linked/erfli/moduleb/ModuleB.java: -------------------------------------------------------------------------------- 1 | package com.linked.erfli.moduleb; 2 | 3 | import com.github.mzule.activityrouter.annotation.Module; 4 | import com.github.mzule.activityrouter.annotation.Modules; 5 | 6 | /** 7 | * Created by erfli on 11/2/16. 8 | */ 9 | @Module("moduleB") 10 | public class ModuleB { 11 | } 12 | -------------------------------------------------------------------------------- /ModuleA/src/main/debug/java/cn/easydone/modulea/ModuleA.java: -------------------------------------------------------------------------------- 1 | package cn.easydone.modulea; 2 | 3 | import com.github.mzule.activityrouter.annotation.Module; 4 | import com.github.mzule.activityrouter.annotation.Modules; 5 | 6 | /** 7 | * Created by erfli on 11/2/16. 8 | */ 9 | @Modules({"moduleA"}) 10 | @Module("moduleA") 11 | public class ModuleA { 12 | } 13 | -------------------------------------------------------------------------------- /ModuleB/src/main/debug/java/com/linked/erfli/moduleb/ModuleB.java: -------------------------------------------------------------------------------- 1 | package com.linked.erfli.moduleb; 2 | 3 | import com.github.mzule.activityrouter.annotation.Module; 4 | import com.github.mzule.activityrouter.annotation.Modules; 5 | 6 | /** 7 | * Created by erfli on 11/2/16. 8 | */ 9 | @Modules({"moduleB"}) 10 | @Module("moduleB") 11 | public class ModuleB { 12 | } 13 | -------------------------------------------------------------------------------- /Library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /Library/src/main/res/color/tab_text.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ModuleA/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 5dp 6 | 56dp 7 | 10dp> 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /Library/src/main/java/com/linked/erfli/library/utils/EventPool.java: -------------------------------------------------------------------------------- 1 | package com.linked.erfli.library.utils; 2 | 3 | /** 4 | * Created by erfli on 11/7/16. 5 | */ 6 | 7 | public class EventPool { 8 | public static abstract class BaseEvent { 9 | 10 | } 11 | 12 | public static class ActivityNotify extends BaseEvent { 13 | public String activityName; 14 | 15 | public ActivityNotify(String activityName) { 16 | this.activityName = activityName; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Library/src/main/java/com/linked/erfli/library/utils/operators/ThreadPool.java: -------------------------------------------------------------------------------- 1 | package com.linked.erfli.library.utils.operators; 2 | 3 | import java.util.concurrent.ScheduledThreadPoolExecutor; 4 | import java.util.concurrent.ThreadPoolExecutor; 5 | 6 | /** 7 | * Created by erfli on 6/14/16. 8 | */ 9 | public class ThreadPool { 10 | public static ThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(6); 11 | public static void clearExecut(){ 12 | executor.getQueue().clear(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Library/src/main/java/com/linked/erfli/library/utils/ConverterName.java: -------------------------------------------------------------------------------- 1 | package com.linked.erfli.library.utils; 2 | 3 | /** 4 | * Created by erfli on 6/18/16. 5 | */ 6 | 7 | import java.lang.annotation.Documented; 8 | import java.lang.annotation.Retention; 9 | import java.lang.annotation.Target; 10 | 11 | import static java.lang.annotation.ElementType.METHOD; 12 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 13 | 14 | @Documented 15 | @Target(METHOD) 16 | @Retention(RUNTIME) 17 | public @interface ConverterName 18 | { 19 | String value() default "gson"; 20 | } 21 | -------------------------------------------------------------------------------- /ModuleB/src/main/java/com/linked/erfli/moduleb/news/NewsResponse.java: -------------------------------------------------------------------------------- 1 | package com.linked.erfli.moduleb.news; 2 | 3 | import java.util.List; 4 | 5 | 6 | public class NewsResponse{ 7 | private List stories; 8 | private String date; 9 | public NewsResponse(){ 10 | 11 | } 12 | 13 | public void setStories(List stories) { 14 | this.stories = stories; 15 | } 16 | 17 | public List getStories() { 18 | return stories; 19 | } 20 | 21 | public void setDate(String date) { 22 | this.date = date; 23 | } 24 | 25 | public String getDate() { 26 | return date; 27 | } 28 | 29 | 30 | } -------------------------------------------------------------------------------- /moduleA/src/main/release/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 12 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /ModuleA/src/main/java/cn/easydone/modulea/Utils/BookApiHttp.java: -------------------------------------------------------------------------------- 1 | package cn.easydone.modulea.Utils; 2 | 3 | import com.linked.erfli.library.utils.AppClient; 4 | 5 | import java.util.Map; 6 | 7 | import cn.easydone.modulea.module.BookResponse; 8 | import retrofit2.http.GET; 9 | import retrofit2.http.QueryMap; 10 | import rx.Observable; 11 | 12 | /** 13 | * Created by erfli on 11/1/16. 14 | */ 15 | 16 | public interface BookApiHttp { 17 | interface Http { 18 | @GET("https://api.douban.com/v2/book/search") 19 | Observable getBooks(@QueryMap Map options); 20 | } 21 | 22 | public static Http http = AppClient.BaseRetrofit.create(Http.class); 23 | } 24 | -------------------------------------------------------------------------------- /Library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/erfli/Downloads/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /moduleb/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/erfli/Downloads/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/liang/Documents/android-sdk-macosx/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /moduleA/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/liang/Documents/android-sdk-macosx/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /Library/src/main/java/com/linked/erfli/library/base/BaseActivity.java: -------------------------------------------------------------------------------- 1 | package com.linked.erfli.library.base; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v7.app.AppCompatActivity; 6 | 7 | import com.linked.erfli.library.utils.EventPool; 8 | 9 | import org.greenrobot.eventbus.EventBus; 10 | 11 | /** 12 | * Created by erfli on 11/7/16. 13 | */ 14 | 15 | public abstract class BaseActivity extends AppCompatActivity { 16 | @Override 17 | protected void onCreate(@Nullable Bundle savedInstanceState) { 18 | super.onCreate(savedInstanceState); 19 | EventBus.getDefault().post(new EventPool.ActivityNotify(this.getPackageName() + "/" + this.getClass().getSimpleName())); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ModuleA/src/main/res/layout/module1_fragment_books.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 12 | 19 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | IsBuildMudle=true -------------------------------------------------------------------------------- /ModuleB/src/main/release/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 12 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 15 | 16 | 10 | 11 | 15 | 16 | 27 | 28 | -------------------------------------------------------------------------------- /ModuleB/src/main/res/layout/module2_item_zhihu.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 23 | 24 | 31 | -------------------------------------------------------------------------------- /ModuleA/src/main/res/layout/module1_activity_library.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 19 | 20 | 21 | 22 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Library/src/main/java/com/linked/erfli/library/widget/WebViewProgressBar.java: -------------------------------------------------------------------------------- 1 | package com.linked.erfli.library.widget; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Paint; 6 | import android.util.AttributeSet; 7 | import android.view.View; 8 | 9 | import com.linked.erfli.library.R; 10 | 11 | public class WebViewProgressBar extends View { 12 | private int progress = 1; 13 | private final static int HEIGHT = 5; 14 | private Paint paint; 15 | private final static int colors[] = new int[]{}; 16 | 17 | public WebViewProgressBar(Context context, AttributeSet attrs) { 18 | super(context, attrs); 19 | } 20 | 21 | public WebViewProgressBar(Context context) { 22 | super(context); 23 | paint = new Paint(Paint.DITHER_FLAG); 24 | paint.setStyle(Paint.Style.STROKE); 25 | paint.setStrokeWidth(HEIGHT); 26 | paint.setAntiAlias(true); 27 | paint.setColor(context.getResources().getColor(R.color.primary_light)); 28 | } 29 | 30 | public void setProgress(int progress) { 31 | this.progress = progress; 32 | invalidate(); 33 | } 34 | 35 | @Override 36 | protected void onDraw(Canvas canvas) { 37 | canvas.drawRect(0, 0, getWidth() * progress / 100, HEIGHT, paint); 38 | } 39 | } -------------------------------------------------------------------------------- /moduleA/build.gradle: -------------------------------------------------------------------------------- 1 | println IsBuildMudle.toBoolean() 2 | if (IsBuildMudle.toBoolean()) { 3 | apply plugin: 'com.android.application' 4 | } else { 5 | apply plugin: 'com.android.library' 6 | } 7 | android { 8 | compileSdkVersion 24 9 | buildToolsVersion "24.0.1" 10 | 11 | defaultConfig { 12 | minSdkVersion 14 13 | targetSdkVersion 24 14 | versionCode 1 15 | versionName "1.0" 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | sourceSets { 24 | main { 25 | if (IsBuildMudle.toBoolean()) { 26 | manifest.srcFile 'src/main/debug/AndroidManifest.xml' 27 | java.srcDirs += "src/main/debug/java/"; 28 | } else { 29 | manifest.srcFile 'src/main/release/AndroidManifest.xml' 30 | java.srcDirs += "src/main/release/java/"; 31 | } 32 | } 33 | } 34 | resourcePrefix "module1_" 35 | } 36 | 37 | dependencies { 38 | compile fileTree(dir: 'libs', include: ['*.jar']) 39 | compile project(':Library') 40 | //router 41 | annotationProcessor 'com.github.mzule.activityrouter:compiler:1.1.7' 42 | } 43 | 44 | -------------------------------------------------------------------------------- /ModuleA/src/main/java/cn/easydone/modulea/LibraryActivity.java: -------------------------------------------------------------------------------- 1 | package cn.easydone.modulea; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.support.v7.widget.Toolbar; 6 | import android.view.View; 7 | 8 | import com.github.mzule.activityrouter.annotation.Router; 9 | import com.linked.erfli.library.base.BaseActivity; 10 | 11 | import cn.easydone.modulea.book.BooksFragment; 12 | @Router("books_list") 13 | public class LibraryActivity extends BaseActivity { 14 | 15 | @Override 16 | protected void onCreate(Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | setContentView(R.layout.module1_activity_library); 19 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 20 | setSupportActionBar(toolbar); 21 | getSupportActionBar().setHomeButtonEnabled(true); 22 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 23 | toolbar.setNavigationOnClickListener(new View.OnClickListener() { 24 | @Override 25 | public void onClick(View view) { 26 | onBackPressed(); 27 | } 28 | }); 29 | BooksFragment fragment = new BooksFragment(); 30 | getSupportFragmentManager().beginTransaction().add(R.id.fragment, fragment).commit(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /moduleb/build.gradle: -------------------------------------------------------------------------------- 1 | if (IsBuildMudle.toBoolean()) { 2 | apply plugin: 'com.android.application' 3 | } else { 4 | apply plugin: 'com.android.library' 5 | } 6 | android { 7 | compileSdkVersion 24 8 | buildToolsVersion "24.0.3" 9 | 10 | defaultConfig { 11 | minSdkVersion 14 12 | targetSdkVersion 24 13 | versionCode 1 14 | versionName "1.0" 15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 16 | 17 | } 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | sourceSets { 25 | main { 26 | if (IsBuildMudle.toBoolean()) { 27 | manifest.srcFile 'src/main/debug/AndroidManifest.xml' 28 | java.srcDirs += "src/main/debug/java/"; 29 | } else { 30 | manifest.srcFile 'src/main/release/AndroidManifest.xml' 31 | java.srcDirs += "src/main/release/java/"; 32 | } 33 | } 34 | } 35 | resourcePrefix "module2_" 36 | } 37 | 38 | dependencies { 39 | compile fileTree(dir: 'libs', include: ['*.jar']) 40 | compile project(':Library') 41 | //router 42 | annotationProcessor 'com.github.mzule.activityrouter:compiler:1.1.7' 43 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android 组件化开发demo 2 | 组件化开发可以有效降低代码模块的耦合度,使代码架构更加清晰,同时模块化的编译可以有效减少编译时间,当然总的编译时间是不会减少的,只是App模块化之后开发某个模块时,只需要编译特定模块,可以快速编译调试。 3 | 4 | [详情参考](http://www.jianshu.com/p/186fa07fc48a) 5 | 6 | ![Paste_Image.png](http://upload-images.jianshu.io/upload_images/1407686-2bb840fdef5d9ac2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/200)![Paste_Image.png](http://upload-images.jianshu.io/upload_images/1407686-eb097c7ce3473583.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/200)![Paste_Image.png](http://upload-images.jianshu.io/upload_images/1407686-74e5145beda0702e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/200)![Paste_Image.png](http://upload-images.jianshu.io/upload_images/1407686-bb950d3382d8e90b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/200)![Paste_Image.png](http://upload-images.jianshu.io/upload_images/1407686-a5924d98324b3762.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/200) 7 | 8 | ### 编译运行 9 | 10 | 当在gradle.properties中设置IsBuildMudle=true时,可以独立运行每个module,包括app module,单独build module app时,由于没有编译moduleA和moduleB,点击两个模块时不会跳转。每个module可独立运行调试。 11 | 12 | 当设置IsBuildMudle=false,可以编译运行整个project,注意IsBuildMudle变量设置改变时,要对gradle进行sysn。 13 | 14 | 有什么奇怪问题,可以clean project再编译……^ - ^ 15 | 16 | ### 感谢 17 | 18 | http://kymjs.com/code/2016/10/18/01 19 | https://github.com/mzule/ActivityRouter 20 | https://github.com/liangzhitao/ComponentizationApp 21 | 22 | ### issue 23 | 24 | 关于url跳转传参数的一个问题 25 | 26 | https://github.com/mzule/ActivityRouter/issues/17 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | android { 3 | compileSdkVersion 24 4 | buildToolsVersion "24.0.3" 5 | 6 | defaultConfig { 7 | minSdkVersion 14 8 | targetSdkVersion 24 9 | versionCode 1 10 | versionName "1.0" 11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | compile fileTree(dir: 'libs', include: ['*.jar']) 23 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 24 | exclude group: 'com.android.support', module: 'support-annotations' 25 | }) 26 | 27 | compile 'com.android.support:design:24.0.0' 28 | compile 'com.android.support:cardview-v7:24.0.0' 29 | compile 'com.android.support:recyclerview-v7:24.0.0' 30 | 31 | compile 'com.android.support:appcompat-v7:24.2.1' 32 | compile 'com.squareup.retrofit2:retrofit:2.1.0' 33 | compile 'com.android.support:design:24.2.1' 34 | compile 'com.github.bumptech.glide:glide:3.6.0' 35 | compile 'com.google.code.gson:gson:2.4' 36 | compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0' 37 | 38 | compile 'io.reactivex:rxandroid:1.2.1' 39 | compile 'io.reactivex:rxjava:1.1.6' 40 | compile 'org.greenrobot:eventbus:3.0.0' 41 | compile 'com.github.mzule.activityrouter:activityrouter:1.2.2' 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 21 | 22 | 23 | 24 | 27 | 28 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /ModuleB/src/main/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Library/src/main/java/com/linked/erfli/library/widget/RecyclerItemClickListener.java: -------------------------------------------------------------------------------- 1 | package com.linked.erfli.library.widget; 2 | 3 | import android.content.Context; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.GestureDetector; 6 | import android.view.MotionEvent; 7 | import android.view.View; 8 | 9 | 10 | public class RecyclerItemClickListener implements RecyclerView.OnItemTouchListener { 11 | private OnItemClickListener mListener; 12 | 13 | public interface OnItemClickListener { 14 | void onItemClick(View view, int position); 15 | } 16 | 17 | GestureDetector mGestureDetector; 18 | 19 | public RecyclerItemClickListener(Context context, OnItemClickListener listener) { 20 | mListener = listener; 21 | mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { 22 | @Override 23 | public boolean onSingleTapUp(MotionEvent e) { 24 | return true; 25 | } 26 | }); 27 | } 28 | 29 | @Override 30 | public boolean onInterceptTouchEvent(RecyclerView view, MotionEvent e) { 31 | View childView = view.findChildViewUnder(e.getX(), e.getY()); 32 | if (childView != null && mListener != null && mGestureDetector.onTouchEvent(e)) { 33 | mListener.onItemClick(childView, view.getChildAdapterPosition(childView)); 34 | } 35 | return false; 36 | } 37 | 38 | @Override 39 | public void onTouchEvent(RecyclerView view, MotionEvent motionEvent) { } 40 | 41 | @Override 42 | public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { 43 | // do nothing 44 | } 45 | } -------------------------------------------------------------------------------- /Library/src/main/java/com/linked/erfli/library/utils/DisplayUtil.java: -------------------------------------------------------------------------------- 1 | package com.linked.erfli.library.utils; 2 | 3 | import android.content.Context; 4 | import android.util.DisplayMetrics; 5 | import android.view.View; 6 | import android.view.WindowManager; 7 | 8 | public class DisplayUtil { 9 | 10 | public static int SCREEN_WIDTH_PIXELS; 11 | public static int SCREEN_HEIGHT_PIXELS; 12 | public static float SCREEN_DENSITY; 13 | public static int SCREEN_WIDTH_DP; 14 | public static int SCREEN_HEIGHT_DP; 15 | 16 | public static void init(Context context) { 17 | if (context == null) { 18 | return; 19 | } 20 | DisplayMetrics dm = new DisplayMetrics(); 21 | WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 22 | wm.getDefaultDisplay().getMetrics(dm); 23 | SCREEN_WIDTH_PIXELS = dm.widthPixels; 24 | SCREEN_HEIGHT_PIXELS = dm.heightPixels; 25 | SCREEN_DENSITY = dm.density; 26 | SCREEN_WIDTH_DP = (int) (SCREEN_WIDTH_PIXELS / dm.density); 27 | SCREEN_HEIGHT_DP = (int) (SCREEN_HEIGHT_PIXELS / dm.density); 28 | } 29 | 30 | public static int dp2px(float dp) { 31 | final float scale = SCREEN_DENSITY; 32 | return (int) (dp * scale + 0.5f); 33 | } 34 | 35 | public static int designedDP2px(float designedDp) { 36 | if (SCREEN_WIDTH_DP != 320) { 37 | designedDp = designedDp * SCREEN_WIDTH_DP / 320f; 38 | } 39 | return dp2px(designedDp); 40 | } 41 | 42 | public static void setPadding(final View view, float left, float top, float right, float bottom) { 43 | view.setPadding(designedDP2px(left), dp2px(top), designedDP2px(right), dp2px(bottom)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Introduction.md: -------------------------------------------------------------------------------- 1 | Android 插件化技术发展到现在其实已经很成熟了,但是相应的问题,如果没有真正地去实践过,根本不了解其中有多少问题,会牵涉到多少技术细节,多少被外人膜拜的外表光鲜的技术大牛都被『插件化』这三个字折磨地死去活来,这对于 Android 整个生态的损害也让人无法忽视。 2 | 3 | 9月24日的 MDCC ,冯森林老师提出了一个很有意思的思路『组件化』。 4 | 5 | 我们首先要想一下,我们做插件化的目的是什么? 6 | 7 | - 为了满足产品随时上线的需求? 8 | - 为了修复因为我们对自己要求不严格而写出来的 bug ? 9 | - 为了向人炫耀自己的技术实力? 10 | 11 | 很抱歉,如果是为了这些目的,那就真的太对不起自己是『开发者』这个如此高逼格的身份了。 12 | 13 | 做插件化真正的目的:是为了去适应并行开发,是为了解耦各个模块,是为了避免模块之间的交叉依赖,是为了加快编译速度,从而提高并行开发效率。 14 | 15 | 明确了这些,我们再来看插件化的结果,每个模块都支持独立运行测试,分为稳定的 release 版本和不稳定的 snapshot 版本,每个模块都高度解耦,没有交叉依赖,不会出现一个模块依赖了另一个模块,其中一个人改了这个模块的代码,对另一个模块造成影响。这时候,我们再看冯老师的『组件化』思想。 16 | 17 | 那么这个『组件化』是什么意思呢?我说下我自己的理解,可能不对,还请指教: 18 | > 通过 gradle 配置的方式,将打 debug 包和 release 包分开。这样会有一个好处,开发一个模块,在 debug 的时候,可以打成一个 apk ,独立运行测试,可以完全独立于整个宿主 APP 的其他所有组件;待到要打 release 包的时候,再把这个模块作为一个 library ,打成 aar ,作为整个宿主 APP 的一部分。而 debug 和 release 的切换都是通过 gradle 配置,可以做到无缝切换。至于模块之间的跳转,可以用别名的方式,而不是用 Activity 和 Fragment 类名。这样所有的模块和宿主 APP 都是完全解耦的,彻底解决了并行开发的可能造成的交叉依赖等问题。 19 | 20 | 按照这个思路,我们再来看看一些其他的细节: 21 | 22 | 1. 在 Android 里有一个比较爽的一点是,作为 library 的时候,aar 里的引用依赖,在宿主 Application 里也有同样的引用依赖,并不会打包两份到宿主 Application 里; 23 | 2. 模块之间的跳转,除了使用别名的方式,我能想到的还有另外一种方式,同样是通过 gradle 脚本,将跳转用到的类打成一个 jar ,作为一个 API 服务提供给其他模块作为编译期依赖(provided)引入; 24 | 3. 各个 library 在 debug 的时候作为 apk ,要独立打包运行测试,这时就需要有一个启动 Activity ,而 library 是不需要的,我的想法是放置两个 AndroidManifest.xml ,使用 sourceSets 分别在 debug 和 release 的时候加载不同的 AndroidManifest.xml 。 25 | 26 | 怎么样?看上去是不是很像插件化 Atlas ?然而这个方案没有任何『黑科技』,不牵涉任何 hook ,跟 Atlas 的区别就是无需关心不同的 Context ,无需再关心类、资源怎么去加载,无需关心 Context 的安全问题,无需关心不同机型的兼容适配...技术成本可能连 Atlas 的十分之一都不到! 27 | 28 | 感兴趣的话,可以看看这个 slide 分享,地址在这儿[from-containerization-to-modularity](http://www.slideshare.net/oasisfeng/from-containerization-to-modularity),我也写了个小 sample 去实现这个想法[ComponentizationApp](GitHub - liangzhitao/ComponentizationApp)。 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /ModuleB/src/main/java/com/linked/erfli/moduleb/news/TopStory.java: -------------------------------------------------------------------------------- 1 | package com.linked.erfli.moduleb.news; 2 | 3 | 4 | public class TopStory { 5 | 6 | private static final String FIELD_TYPE = "type"; 7 | private static final String FIELD_ID = "id"; 8 | private static final String FIELD_GA_PREFIX = "ga_prefix"; 9 | private static final String FIELD_TITLE = "title"; 10 | private static final String FIELD_IMAGE = "image"; 11 | 12 | private int mType; 13 | private long mId; 14 | private int mGaPrefix; 15 | private String mTitle; 16 | private String mImage; 17 | 18 | public TopStory() { 19 | 20 | } 21 | 22 | public void setType(int type) { 23 | mType = type; 24 | } 25 | 26 | public int getType() { 27 | return mType; 28 | } 29 | 30 | public void setId(long id) { 31 | mId = id; 32 | } 33 | 34 | public long getId() { 35 | return mId; 36 | } 37 | 38 | public void setGaPrefix(int gaPrefix) { 39 | mGaPrefix = gaPrefix; 40 | } 41 | 42 | public int getGaPrefix() { 43 | return mGaPrefix; 44 | } 45 | 46 | public void setTitle(String title) { 47 | mTitle = title; 48 | } 49 | 50 | public String getTitle() { 51 | return mTitle; 52 | } 53 | 54 | public void setImage(String image) { 55 | mImage = image; 56 | } 57 | 58 | public String getImage() { 59 | return mImage; 60 | } 61 | 62 | @Override 63 | public boolean equals(Object obj) { 64 | if (obj instanceof TopStory) { 65 | return ((TopStory) obj).getId() == mId; 66 | } 67 | return false; 68 | } 69 | 70 | @Override 71 | public int hashCode() { 72 | return ((Long) mId).hashCode(); 73 | } 74 | 75 | } -------------------------------------------------------------------------------- /ModuleA/src/main/res/layout/module1_book_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 18 | 19 | 23 | 24 | 29 | 30 | 36 | 37 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/java/cn/easydone/componentizationapp/MainActivity.java: -------------------------------------------------------------------------------- 1 | package cn.easydone.componentizationapp; 2 | 3 | import android.net.Uri; 4 | import android.os.Bundle; 5 | import android.support.design.widget.FloatingActionButton; 6 | import android.support.design.widget.Snackbar; 7 | import android.support.v7.app.AppCompatActivity; 8 | import android.support.v7.widget.Toolbar; 9 | import android.view.View; 10 | import android.widget.Button; 11 | 12 | import com.github.mzule.activityrouter.router.Routers; 13 | 14 | import hugo.weaving.DebugLog; 15 | 16 | public class MainActivity extends AppCompatActivity { 17 | 18 | @Override 19 | protected void onCreate(Bundle savedInstanceState) { 20 | super.onCreate(savedInstanceState); 21 | setContentView(R.layout.activity_main); 22 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 23 | setSupportActionBar(toolbar); 24 | 25 | FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); 26 | fab.setOnClickListener(new View.OnClickListener() { 27 | @Override 28 | public void onClick(View view) { 29 | Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG) 30 | .setAction("Action", null).show(); 31 | } 32 | }); 33 | Button button = (Button) findViewById(R.id.buttonA); 34 | button.setOnClickListener(new View.OnClickListener() { 35 | @Override 36 | @DebugLog 37 | public void onClick(View v) { 38 | Routers.open(MainActivity.this, "modularization://books_list"); 39 | } 40 | }); 41 | findViewById(R.id.buttonB).setOnClickListener(new View.OnClickListener() { 42 | @Override 43 | @DebugLog 44 | public void onClick(View v) { 45 | Routers.open(MainActivity.this, Uri.parse("modularization://news_list")); 46 | } 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Library/src/main/java/com/linked/erfli/library/widget/ProgressWebView.java: -------------------------------------------------------------------------------- 1 | package com.linked.erfli.library.widget; 2 | 3 | import android.content.Context; 4 | import android.os.Handler; 5 | import android.util.AttributeSet; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.webkit.WebChromeClient; 9 | import android.webkit.WebView; 10 | import android.webkit.WebViewClient; 11 | 12 | 13 | public class ProgressWebView extends WebView { 14 | private WebViewProgressBar progressBar; 15 | private Handler handler; 16 | private WebView _this; 17 | public ProgressWebView(Context context, AttributeSet attrs) { 18 | super(context, attrs); 19 | progressBar = new WebViewProgressBar(context); 20 | progressBar.setLayoutParams(new ViewGroup.LayoutParams 21 | (ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); 22 | progressBar.setVisibility(GONE); 23 | addView(progressBar); 24 | handler = new Handler(); 25 | _this = this; 26 | setWebChromeClient(new MyWebChromeClient()); 27 | setWebViewClient(new MyWebClient()); 28 | 29 | } 30 | 31 | private class MyWebChromeClient extends WebChromeClient { 32 | @Override 33 | public void onProgressChanged(WebView view, int newProgress) { 34 | if(newProgress == 100){ 35 | progressBar.setProgress(100); 36 | handler.postDelayed(runnable,200); 37 | }else if(progressBar.getVisibility() == GONE){ 38 | progressBar.setVisibility(VISIBLE); 39 | } 40 | if(newProgress < 5){ 41 | newProgress = 5; 42 | } 43 | progressBar.setProgress(newProgress); 44 | super.onProgressChanged(view, newProgress); 45 | } 46 | } 47 | private class MyWebClient extends WebViewClient { 48 | @Override 49 | public boolean shouldOverrideUrlLoading(WebView view, String url) { 50 | _this.loadUrl(url); 51 | return true; 52 | } 53 | } 54 | private Runnable runnable = new Runnable() { 55 | @Override 56 | public void run() { 57 | progressBar.setVisibility(View.GONE); 58 | } 59 | }; 60 | } -------------------------------------------------------------------------------- /ModuleB/src/main/java/com/linked/erfli/moduleb/news/NewsDetailResponse.java: -------------------------------------------------------------------------------- 1 | package com.linked.erfli.moduleb.news; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Created by erfli on 6/16/16. 7 | */ 8 | public class NewsDetailResponse { 9 | private String body; 10 | 11 | private String image_source; 12 | 13 | private String title; 14 | 15 | private String image; 16 | 17 | private String share_url; 18 | 19 | private List js ; 20 | 21 | private String ga_prefix; 22 | 23 | private List images ; 24 | 25 | private int type; 26 | 27 | private int id; 28 | 29 | private List css ; 30 | 31 | public void setBody(String body){ 32 | this.body = body; 33 | } 34 | public String getBody(){ 35 | return this.body; 36 | } 37 | public void setImage_source(String image_source){ 38 | this.image_source = image_source; 39 | } 40 | public String getImage_source(){ 41 | return this.image_source; 42 | } 43 | public void setTitle(String title){ 44 | this.title = title; 45 | } 46 | public String getTitle(){ 47 | return this.title; 48 | } 49 | public void setImage(String image){ 50 | this.image = image; 51 | } 52 | public String getImage(){ 53 | return this.image; 54 | } 55 | public void setShare_url(String share_url){ 56 | this.share_url = share_url; 57 | } 58 | public String getShare_url(){ 59 | return this.share_url; 60 | } 61 | public void setJs(List js){ 62 | this.js = js; 63 | } 64 | public List getJs(){ 65 | return this.js; 66 | } 67 | public void setGa_prefix(String ga_prefix){ 68 | this.ga_prefix = ga_prefix; 69 | } 70 | public String getGa_prefix(){ 71 | return this.ga_prefix; 72 | } 73 | public void setImages(List images){ 74 | this.images = images; 75 | } 76 | public List getImages(){ 77 | return this.images; 78 | } 79 | public void setType(int type){ 80 | this.type = type; 81 | } 82 | public int getType(){ 83 | return this.type; 84 | } 85 | public void setId(int id){ 86 | this.id = id; 87 | } 88 | public int getId(){ 89 | return this.id; 90 | } 91 | public void setCss(List css){ 92 | this.css = css; 93 | } 94 | public List getCss(){ 95 | return this.css; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /ModuleA/src/main/java/cn/easydone/modulea/book/MyAdapter.java: -------------------------------------------------------------------------------- 1 | package cn.easydone.modulea.book; 2 | 3 | import android.content.Context; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.util.TypedValue; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | 10 | import com.bumptech.glide.Glide; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | import cn.easydone.modulea.R; 16 | import cn.easydone.modulea.module.Book; 17 | 18 | /** 19 | * Created by erfli on 6/15/16. 20 | */ 21 | public class MyAdapter extends RecyclerView.Adapter { 22 | private List mBooks = new ArrayList(); 23 | private final TypedValue mTypedValue = new TypedValue(); 24 | 25 | // Provide a suitable constructor (depends on the kind of dataset) 26 | public MyAdapter(BooksFragment booksFragment, Context context) { 27 | context.getTheme().resolveAttribute(R.attr.selectableItemBackground, mTypedValue, true); 28 | } 29 | 30 | public void updateItems(List books, boolean animated) { 31 | mBooks.addAll(books); 32 | notifyDataSetChanged(); 33 | } 34 | 35 | public void clearItems() { 36 | mBooks.clear(); 37 | notifyDataSetChanged(); 38 | } 39 | 40 | 41 | @Override 42 | public ViewHolder onCreateViewHolder(ViewGroup parent, 43 | int viewType) { 44 | View v = LayoutInflater.from(parent.getContext()) 45 | .inflate(R.layout.module1_book_item, parent, false); 46 | ViewHolder vh = new ViewHolder(v); 47 | return vh; 48 | } 49 | 50 | @Override 51 | public void onBindViewHolder(ViewHolder holder, int position) { 52 | Book book = mBooks.get(position); 53 | holder.tvTitle.setText(book.getTitle()); 54 | String desc = "作者: " + (book.getAuthor().length > 0 ? book.getAuthor()[0] : "") + "\n副标题: " + book.getSubtitle() 55 | + "\n出版年: " + book.getPubdate() + "\n页数: " + book.getPages() + "\n定价:" + book.getPrice(); 56 | holder.tvDesc.setText(desc); 57 | Glide.with(holder.ivBook.getContext()) 58 | .load(book.getImage()) 59 | .fitCenter() 60 | .into(holder.ivBook); 61 | } 62 | 63 | // Return the size of your dataset (invoked by the layout manager) 64 | @Override 65 | public int getItemCount() { 66 | return mBooks.size(); 67 | } 68 | 69 | 70 | public Book getBook(int pos) { 71 | return mBooks.get(pos); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /ModuleA/src/main/res/layout/module1_activity_appbar_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 23 | 24 | 25 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 52 | 53 | 59 | 60 | 64 | 65 | -------------------------------------------------------------------------------- /ModuleB/src/main/java/com/linked/erfli/moduleb/news/NewsAdapter.java: -------------------------------------------------------------------------------- 1 | package com.linked.erfli.moduleb.news; 2 | 3 | import android.content.Context; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.util.TypedValue; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.ImageView; 10 | import android.widget.TextView; 11 | 12 | import com.bumptech.glide.Glide; 13 | import com.linked.erfli.moduleb.R; 14 | 15 | import java.util.List; 16 | 17 | /** 18 | * Created by erfli on 6/15/16. 19 | */ 20 | public class NewsAdapter extends RecyclerView.Adapter { 21 | private Context context; 22 | private final int mBackground; 23 | private List mDataset; 24 | 25 | private final TypedValue mTypedValue = new TypedValue(); 26 | 27 | public class ViewHolder extends RecyclerView.ViewHolder { 28 | public TextView newsTitleTV; 29 | public ImageView newsIV; 30 | 31 | public ViewHolder(View v) { 32 | super(v); 33 | newsTitleTV = (TextView) v.findViewById(R.id.news_title); 34 | newsIV = (ImageView) v.findViewById(R.id.news_image); 35 | } 36 | } 37 | 38 | public NewsAdapter(Context context, List myDataset) { 39 | this.mDataset = myDataset; 40 | context.getTheme().resolveAttribute(R.attr.selectableItemBackground, mTypedValue, true); 41 | this.context = context; 42 | mBackground = mTypedValue.resourceId; 43 | } 44 | 45 | @Override 46 | public ViewHolder onCreateViewHolder(ViewGroup parent, 47 | int viewType) { 48 | View v = LayoutInflater.from(parent.getContext()) 49 | .inflate(R.layout.module2_item_zhihu, parent, false); 50 | ViewHolder vh = new ViewHolder(v); 51 | return vh; 52 | } 53 | 54 | @Override 55 | public void onBindViewHolder(ViewHolder holder, int position) { 56 | Story story = mDataset.get(position); 57 | holder.newsTitleTV.setText(mDataset.get(position).getTitle()); 58 | holder.newsTitleTV.setTextColor(context.getResources().getColor(R.color.primary_text)); 59 | Glide.clear(holder.newsIV); 60 | Glide.with(holder.newsIV.getContext()) 61 | .load(story.getImages().get(0)) 62 | .fitCenter() 63 | .into(holder.newsIV); 64 | } 65 | 66 | @Override 67 | public int getItemCount() { 68 | return mDataset.size(); 69 | } 70 | 71 | public Story getItem(int position) { 72 | return mDataset.get(position); 73 | } 74 | 75 | public void updateData(List stories) { 76 | mDataset.clear(); 77 | mDataset.addAll(stories); 78 | notifyDataSetChanged(); 79 | } 80 | 81 | public void addData(List stories) { 82 | mDataset.addAll(stories); 83 | notifyDataSetChanged(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /ModuleB/src/main/java/com/linked/erfli/moduleb/news/Story.java: -------------------------------------------------------------------------------- 1 | package com.linked.erfli.moduleb.news; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | import java.util.List; 7 | 8 | 9 | public class Story implements Parcelable { 10 | 11 | private static final String FIELD_TYPE = "type"; 12 | private static final String FIELD_ID = "id"; 13 | private static final String FIELD_GA_PREFIX = "ga_prefix"; 14 | private static final String FIELD_IMAGES = "images"; 15 | private static final String FIELD_TITLE = "title"; 16 | 17 | private int type; 18 | private long id; 19 | private int gaPrefix; 20 | private List images; 21 | private String title; 22 | 23 | public Story(){ 24 | 25 | } 26 | 27 | protected Story(Parcel in) { 28 | type = in.readInt(); 29 | id = in.readLong(); 30 | gaPrefix = in.readInt(); 31 | images = in.createStringArrayList(); 32 | title = in.readString(); 33 | } 34 | 35 | public static final Creator CREATOR = new Creator() { 36 | @Override 37 | public Story createFromParcel(Parcel in) { 38 | return new Story(in); 39 | } 40 | 41 | @Override 42 | public Story[] newArray(int size) { 43 | return new Story[size]; 44 | } 45 | }; 46 | 47 | public void setType(int type) { 48 | this.type = type; 49 | } 50 | 51 | public int getType() { 52 | return type; 53 | } 54 | 55 | public void setId(long id) { 56 | this.id = id; 57 | } 58 | 59 | public long getId() { 60 | return id; 61 | } 62 | 63 | public void setGaPrefix(int gaPrefix) { 64 | this.gaPrefix = gaPrefix; 65 | } 66 | 67 | public int getGaPrefix() { 68 | return gaPrefix; 69 | } 70 | 71 | public void setImages(List images) { 72 | this.images = images; 73 | } 74 | 75 | public List getImages() { 76 | return images; 77 | } 78 | 79 | public void setTitle(String title) { 80 | this.title = title; 81 | } 82 | 83 | public String getTitle() { 84 | return title; 85 | } 86 | 87 | @Override 88 | public boolean equals(Object obj){ 89 | if(obj instanceof Story){ 90 | return ((Story) obj).getId() == id; 91 | } 92 | return false; 93 | } 94 | 95 | @Override 96 | public int hashCode(){ 97 | return ((Long) id).hashCode(); 98 | } 99 | 100 | 101 | @Override 102 | public int describeContents() { 103 | return 0; 104 | } 105 | 106 | @Override 107 | public void writeToParcel(Parcel dest, int flags) { 108 | dest.writeInt(type); 109 | dest.writeLong(id); 110 | dest.writeInt(gaPrefix); 111 | dest.writeStringList(images); 112 | dest.writeString(title); 113 | } 114 | } -------------------------------------------------------------------------------- /ModuleB/src/main/res/layout/module2_activity_news_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 14 | 15 | 27 | 28 | 29 | 37 | 38 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 58 | 61 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /Library/src/main/java/com/linked/erfli/library/utils/AppClient.java: -------------------------------------------------------------------------------- 1 | package com.linked.erfli.library.utils; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.reflect.TypeToken; 5 | 6 | import java.io.IOException; 7 | import java.lang.annotation.Annotation; 8 | import java.lang.reflect.Type; 9 | 10 | import okhttp3.RequestBody; 11 | import okhttp3.ResponseBody; 12 | import retrofit2.Converter; 13 | import retrofit2.Retrofit; 14 | import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory; 15 | 16 | /** 17 | * Created by erfli on 6/14/16. 18 | */ 19 | public class AppClient { 20 | public interface HttpService { 21 | 22 | } 23 | public static Retrofit BaseRetrofit; 24 | static { 25 | initAppClient(); 26 | } 27 | public static void initAppClient() { 28 | BaseRetrofit = new Retrofit.Builder() 29 | .baseUrl("http://news-at.zhihu.com/api/4/") 30 | .addCallAdapterFactory((RxJavaCallAdapterFactory.create())) 31 | .addConverterFactory(new Converter.Factory() { 32 | Gson gson = new Gson(); 33 | @Override 34 | public Converter responseBodyConverter(final Type type, final Annotation[] annotations, Retrofit retrofit) { 35 | return new Converter() { 36 | @Override 37 | public Object convert(ResponseBody value) throws IOException { 38 | try { 39 | if (annotations.length > 1) { 40 | for (Annotation annotation : annotations) { 41 | if (annotation instanceof ConverterName && ((ConverterName) annotation).value().equals("string")) { 42 | return value.string(); 43 | } 44 | } 45 | } 46 | return gson.getAdapter(TypeToken.get(type)).fromJson(value.charStream()); 47 | } finally { 48 | value.close(); 49 | } 50 | } 51 | }; 52 | } 53 | 54 | @Override 55 | public Converter requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) { 56 | return super.requestBodyConverter(type, parameterAnnotations, methodAnnotations, retrofit); 57 | } 58 | 59 | @Override 60 | public Converter stringConverter(Type type, Annotation[] annotations, Retrofit retrofit) { 61 | return super.stringConverter(type, annotations, retrofit); 62 | } 63 | }) 64 | .build(); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /ModuleB/src/main/java/com/linked/erfli/moduleb/news/NewsDetailActivity.java: -------------------------------------------------------------------------------- 1 | package com.linked.erfli.moduleb.news; 2 | 3 | import android.os.Bundle; 4 | import android.support.design.widget.CollapsingToolbarLayout; 5 | import android.support.v7.widget.Toolbar; 6 | import android.view.View; 7 | import android.webkit.WebSettings; 8 | import android.webkit.WebView; 9 | import android.widget.ImageView; 10 | 11 | import com.bumptech.glide.Glide; 12 | import com.github.mzule.activityrouter.annotation.Router; 13 | import com.linked.erfli.library.base.BaseActivity; 14 | import com.linked.erfli.library.utils.Utils; 15 | import com.linked.erfli.library.utils.operators.AppObservable; 16 | import com.linked.erfli.moduleb.R; 17 | import com.linked.erfli.moduleb.utils.ZhihuApiHttp; 18 | 19 | import rx.functions.Action1; 20 | 21 | @Router("news_detail/:id/:title") 22 | public class NewsDetailActivity extends BaseActivity { 23 | private WebView webView; 24 | private ImageView titleImageView; 25 | private CollapsingToolbarLayout collapsingToolbarLayout; 26 | private Toolbar toolbar; 27 | 28 | @Override 29 | protected void onCreate(Bundle savedInstanceState) { 30 | super.onCreate(savedInstanceState); 31 | setContentView(R.layout.module2_activity_news_detail); 32 | initView(); 33 | } 34 | 35 | protected void initView() { 36 | collapsingToolbarLayout = (CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar); 37 | toolbar = (Toolbar) findViewById(R.id.toolbar); 38 | setSupportActionBar(toolbar); 39 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 40 | toolbar.setNavigationOnClickListener(new View.OnClickListener() { 41 | @Override 42 | public void onClick(View view) { 43 | onBackPressed(); 44 | } 45 | }); 46 | titleImageView = (ImageView) findViewById(R.id.ivImage); 47 | webView = (WebView) findViewById(R.id.webView); 48 | WebSettings webSettings = webView.getSettings(); 49 | webSettings.setJavaScriptEnabled(true); 50 | webSettings.setDomStorageEnabled(true); 51 | collapsingToolbarLayout.setTitle(Utils.decodeUrlParam(getIntent().getStringExtra("title"))); 52 | AppObservable.bindActivity(this, ZhihuApiHttp.http.getNewsDetail(getIntent().getStringExtra("id"))).subscribe(new Action1() { 53 | @Override 54 | public void call(final NewsDetailResponse value) { 55 | collapsingToolbarLayout.setTitle(value.getTitle()); 56 | Glide.with(NewsDetailActivity.this) 57 | .load(value.getImage()) 58 | .asBitmap() 59 | .into(titleImageView); 60 | if (value.getCss() != null && value.getCss().size() > 0) { 61 | String css = ""; 62 | String html = "" + css + "" + value.getBody() + ""; 63 | html = html.replace("
", ""); 64 | webView.loadDataWithBaseURL("file:///android_asset/", html, "text/html", "UTF-8", null); 65 | } else { 66 | webView.loadData(value.getBody(), "text/html", "utf-8"); 67 | } 68 | } 69 | }, new Action1() { 70 | @Override 71 | public void call(Throwable throwable) { 72 | Utils.showShortToast(NewsDetailActivity.this, "network error"); 73 | } 74 | }); 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /ModuleA/src/main/java/cn/easydone/modulea/book/BooksFragment.java: -------------------------------------------------------------------------------- 1 | package cn.easydone.modulea.book; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.annotation.Nullable; 6 | import android.support.v4.app.Fragment; 7 | import android.support.v7.widget.DefaultItemAnimator; 8 | import android.support.v7.widget.LinearLayoutManager; 9 | import android.support.v7.widget.RecyclerView; 10 | import android.view.LayoutInflater; 11 | import android.view.View; 12 | import android.view.ViewGroup; 13 | import android.widget.ProgressBar; 14 | 15 | import com.linked.erfli.library.utils.Utils; 16 | import com.linked.erfli.library.utils.operators.AppObservable; 17 | import com.linked.erfli.library.widget.RecyclerItemClickListener; 18 | 19 | import java.util.HashMap; 20 | import java.util.Map; 21 | 22 | import cn.easydone.modulea.R; 23 | import cn.easydone.modulea.Utils.BookApiHttp; 24 | import cn.easydone.modulea.module.Book; 25 | import cn.easydone.modulea.module.BookResponse; 26 | import rx.functions.Action1; 27 | 28 | /** 29 | * Created by Chenyc on 15/7/1. 30 | */ 31 | public class BooksFragment extends Fragment { 32 | 33 | private RecyclerView mRecyclerView; 34 | private MyAdapter mAdapter; 35 | private ProgressBar mProgressBar; 36 | 37 | 38 | @Nullable 39 | @Override 40 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 41 | View view = inflater.inflate(R.layout.module1_fragment_books, null); 42 | mRecyclerView = (RecyclerView) view.findViewById(R.id.recyclerView); 43 | mRecyclerView.setHasFixedSize(true); 44 | LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity()); 45 | mRecyclerView.setLayoutManager(layoutManager); 46 | mRecyclerView.addOnItemTouchListener(new RecyclerItemClickListener(getActivity(), onItemClickListener)); 47 | mRecyclerView.setItemAnimator(new DefaultItemAnimator()); 48 | mProgressBar = (ProgressBar) view.findViewById(R.id.progressBar); 49 | mAdapter = new MyAdapter(this, getActivity()); 50 | mRecyclerView.setAdapter(mAdapter); 51 | mProgressBar.setVisibility(View.VISIBLE); 52 | loadData(); 53 | return view; 54 | } 55 | 56 | private void loadData() { 57 | Map params = new HashMap<>(); 58 | params.put("q", "战争"); 59 | params.put("start", "0"); 60 | params.put("end", "50"); 61 | AppObservable.bindActivity(getActivity(), BookApiHttp.http.getBooks(params)).subscribe(new Action1() { 62 | @Override 63 | public void call(BookResponse bookResponse) { 64 | mProgressBar.setVisibility(View.GONE); 65 | mAdapter.updateItems(bookResponse.getBooks(), false); 66 | } 67 | }, new Action1() { 68 | @Override 69 | public void call(Throwable throwable) { 70 | mProgressBar.setVisibility(View.GONE); 71 | Utils.showShortToast(getContext(), "network error"); 72 | } 73 | }); 74 | } 75 | 76 | @Override 77 | public void onActivityCreated(@Nullable Bundle savedInstanceState) { 78 | super.onActivityCreated(savedInstanceState); 79 | } 80 | 81 | private RecyclerItemClickListener.OnItemClickListener onItemClickListener = new RecyclerItemClickListener.OnItemClickListener() { 82 | @Override 83 | public void onItemClick(View view, int position) { 84 | Book book = mAdapter.getBook(position); 85 | Intent intent = new Intent(getActivity(), BookDetailActivity.class); 86 | intent.putExtra("book", book); 87 | startActivity(intent); 88 | } 89 | }; 90 | } 91 | -------------------------------------------------------------------------------- /Library/src/main/java/com/linked/erfli/library/utils/operators/OperatorConditionalBinding.java: -------------------------------------------------------------------------------- 1 | package com.linked.erfli.library.utils.operators; 2 | 3 | 4 | /** 5 | * Copyright 2014 Netflix, Inc. 6 | *

7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | *

11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | *

13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | import android.util.Log; 21 | 22 | import rx.Observable; 23 | import rx.Subscriber; 24 | import rx.functions.Func1; 25 | 26 | /** 27 | * Ties a source sequence to the given target object using a predicate. If the predicate fails 28 | * to validate, the sequence unsubscribes itself and releases the bound reference. 29 | *

30 | * You can also pass in an optional predicate function, which whenever it evaluates to false 31 | * on the target object, will also result in the operator unsubscribing from the sequence. 32 | * 33 | * @param the type of the objects emitted to a subscriber 34 | * @param the type of the target object to bind to 35 | */ 36 | public final class OperatorConditionalBinding implements Observable.Operator { 37 | 38 | private static final String LOG_TAG = "ConditionalBinding"; 39 | private final Func1 predicate; 40 | private R boundRef; 41 | 42 | public OperatorConditionalBinding(R bound, Func1 predicate) { 43 | boundRef = bound; 44 | this.predicate = predicate; 45 | } 46 | 47 | @Override 48 | public Subscriber call(final Subscriber child) { 49 | return new Subscriber(child) { 50 | 51 | @Override 52 | public void onCompleted() { 53 | if (shouldForwardNotification()) { 54 | child.onCompleted(); 55 | } else { 56 | handleLostBinding("onCompleted"); 57 | } 58 | } 59 | 60 | @Override 61 | public void onError(Throwable e) { 62 | if (shouldForwardNotification()) { 63 | child.onError(e); 64 | } else { 65 | handleLostBinding("onError"); 66 | } 67 | } 68 | 69 | @Override 70 | public void onNext(T t) { 71 | if (shouldForwardNotification()) { 72 | child.onNext(t); 73 | } else { 74 | handleLostBinding("onNext"); 75 | } 76 | } 77 | 78 | private boolean shouldForwardNotification() { 79 | return boundRef != null && predicate.call(boundRef); 80 | } 81 | 82 | private void handleLostBinding(String context) { 83 | log("bound object has become invalid; skipping " + context); 84 | log("unsubscribing..."); 85 | boundRef = null; 86 | unsubscribe(); 87 | } 88 | 89 | private void log(String message) { 90 | if (Log.isLoggable(LOG_TAG, Log.DEBUG)) { 91 | Log.d(LOG_TAG, message); 92 | } 93 | } 94 | }; 95 | } 96 | 97 | /* Visible for testing */ 98 | R getBoundRef() { 99 | return boundRef; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /ModuleA/src/main/java/cn/easydone/modulea/book/BookDetailActivity.java: -------------------------------------------------------------------------------- 1 | package cn.easydone.modulea.book; 2 | 3 | import android.os.Bundle; 4 | import android.support.design.widget.CollapsingToolbarLayout; 5 | import android.support.design.widget.TabLayout; 6 | import android.support.v4.app.Fragment; 7 | import android.support.v4.app.FragmentManager; 8 | import android.support.v4.app.FragmentPagerAdapter; 9 | import android.support.v4.view.ViewPager; 10 | import android.support.v7.app.AppCompatActivity; 11 | import android.support.v7.widget.Toolbar; 12 | import android.view.View; 13 | import android.widget.ImageView; 14 | 15 | import com.bumptech.glide.Glide; 16 | import com.linked.erfli.library.base.BaseActivity; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | import cn.easydone.modulea.R; 22 | import cn.easydone.modulea.module.Book; 23 | 24 | /** 25 | * Created by Chenyc on 15/7/1. 26 | */ 27 | public class BookDetailActivity extends BaseActivity { 28 | 29 | 30 | private ViewPager mViewPager; 31 | private Book mBook; 32 | 33 | @Override 34 | protected void onCreate(Bundle savedInstanceState) { 35 | super.onCreate(savedInstanceState); 36 | setContentView(R.layout.module1_activity_appbar_detail); 37 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 38 | setSupportActionBar(toolbar); 39 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 40 | toolbar.setNavigationOnClickListener(new View.OnClickListener() { 41 | @Override 42 | public void onClick(View view) { 43 | onBackPressed(); 44 | } 45 | }); 46 | 47 | mBook = (Book) getIntent().getSerializableExtra("book"); 48 | CollapsingToolbarLayout collapsingToolbar = (CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar); 49 | collapsingToolbar.setTitle(mBook.getTitle()); 50 | 51 | ImageView ivImage = (ImageView) findViewById(R.id.ivImage); 52 | Glide.with(ivImage.getContext()) 53 | .load(mBook.getImages().getLarge()) 54 | .fitCenter() 55 | .into(ivImage); 56 | 57 | mViewPager = (ViewPager) findViewById(R.id.viewpager); 58 | setupViewPager(mViewPager); 59 | 60 | TabLayout tabLayout = (TabLayout) findViewById(R.id.sliding_tabs); 61 | tabLayout.addTab(tabLayout.newTab().setText("内容简介")); 62 | tabLayout.addTab(tabLayout.newTab().setText("作者简介")); 63 | tabLayout.addTab(tabLayout.newTab().setText("目录")); 64 | tabLayout.setupWithViewPager(mViewPager); 65 | } 66 | 67 | 68 | private void setupViewPager(ViewPager mViewPager) { 69 | MyPagerAdapter adapter = new MyPagerAdapter(getSupportFragmentManager()); 70 | adapter.addFragment(DetailFragment.newInstance(mBook.getSummary()), "内容简介"); 71 | adapter.addFragment(DetailFragment.newInstance(mBook.getAuthor_intro()), "作者简介"); 72 | adapter.addFragment(DetailFragment.newInstance(mBook.getCatalog()), "目录"); 73 | mViewPager.setAdapter(adapter); 74 | } 75 | 76 | 77 | static class MyPagerAdapter extends FragmentPagerAdapter { 78 | private final List mFragments = new ArrayList<>(); 79 | private final List mFragmentTitles = new ArrayList<>(); 80 | 81 | public MyPagerAdapter(FragmentManager fm) { 82 | super(fm); 83 | } 84 | 85 | public void addFragment(Fragment fragment, String title) { 86 | mFragments.add(fragment); 87 | mFragmentTitles.add(title); 88 | } 89 | 90 | @Override 91 | public Fragment getItem(int position) { 92 | return mFragments.get(position); 93 | } 94 | 95 | @Override 96 | public int getCount() { 97 | return mFragments.size(); 98 | } 99 | 100 | @Override 101 | public CharSequence getPageTitle(int position) { 102 | return mFragmentTitles.get(position); 103 | } 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /Library/src/main/java/com/linked/erfli/library/widget/DividerItemDecoration.java: -------------------------------------------------------------------------------- 1 | package com.linked.erfli.library.widget; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Canvas; 6 | import android.graphics.Rect; 7 | import android.graphics.drawable.Drawable; 8 | import android.support.v7.widget.LinearLayoutManager; 9 | import android.support.v7.widget.RecyclerView; 10 | import android.view.View; 11 | 12 | /* 13 | * Copyright (C) 2014 The Android Open Source Project 14 | * 15 | * Licensed under the Apache License, Version 2.0 (the "License"); 16 | * you may not use this file except in compliance with the License. 17 | * You may obtain a copy of the License at 18 | * 19 | * http://www.apache.org/licenses/LICENSE-2.0 20 | * 21 | * Unless required by applicable law or agreed to in writing, software 22 | * distributed under the License is distributed on an " IS" BASIS, 23 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 24 | * See the License for the specific language governing permissions and 25 | * limitations under the License. 26 | */ 27 | public class DividerItemDecoration extends RecyclerView.ItemDecoration { 28 | 29 | private static final int[] ATTRS = new int[]{ 30 | android.R.attr.listDivider 31 | }; 32 | 33 | public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL; 34 | 35 | public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL; 36 | 37 | private Drawable mDivider; 38 | 39 | private int mOrientation; 40 | 41 | public DividerItemDecoration(Context context, int orientation) { 42 | final TypedArray a = context.obtainStyledAttributes(ATTRS); 43 | mDivider = a.getDrawable(0); 44 | a.recycle(); 45 | setOrientation(orientation); 46 | } 47 | 48 | public void setOrientation(int orientation) { 49 | if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) { 50 | throw new IllegalArgumentException("invalid orientation"); 51 | } 52 | mOrientation = orientation; 53 | } 54 | 55 | public DividerItemDecoration setmDivider(Drawable mDivider) { 56 | this.mDivider = mDivider; 57 | return this; 58 | } 59 | 60 | @Override 61 | public void onDraw(Canvas c, RecyclerView parent) { 62 | if (mOrientation == VERTICAL_LIST) { 63 | drawVertical(c, parent); 64 | } else { 65 | drawHorizontal(c, parent); 66 | } 67 | } 68 | 69 | public void drawVertical(Canvas c, RecyclerView parent) { 70 | final int left = parent.getPaddingLeft(); 71 | final int right = parent.getWidth() - parent.getPaddingRight(); 72 | 73 | final int childCount = parent.getChildCount(); 74 | for (int i = 0; i < childCount; i++) { 75 | final View child = parent.getChildAt(i); 76 | final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child 77 | .getLayoutParams(); 78 | final int top = child.getBottom() + params.bottomMargin; 79 | final int bottom = top + mDivider.getIntrinsicHeight(); 80 | mDivider.setBounds(left, top, right, bottom); 81 | mDivider.draw(c); 82 | } 83 | } 84 | 85 | public void drawHorizontal(Canvas c, RecyclerView parent) { 86 | final int top = parent.getPaddingTop(); 87 | final int bottom = parent.getHeight() - parent.getPaddingBottom(); 88 | 89 | final int childCount = parent.getChildCount(); 90 | for (int i = 0; i < childCount; i++) { 91 | final View child = parent.getChildAt(i); 92 | final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child 93 | .getLayoutParams(); 94 | final int left = child.getRight() + params.rightMargin; 95 | final int right = left + mDivider.getIntrinsicHeight(); 96 | mDivider.setBounds(left, top, right, bottom); 97 | mDivider.draw(c); 98 | } 99 | } 100 | 101 | @Override 102 | public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) { 103 | if (mOrientation == VERTICAL_LIST) { 104 | outRect.set(0, 0, 0, mDivider.getIntrinsicHeight()); 105 | } else { 106 | outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0); 107 | } 108 | } 109 | } -------------------------------------------------------------------------------- /ModuleA/src/main/java/cn/easydone/modulea/module/Book.java: -------------------------------------------------------------------------------- 1 | package cn.easydone.modulea.module; 2 | 3 | import java.io.Serializable; 4 | import java.util.Arrays; 5 | 6 | /** 7 | * Created by Chenyc on 15/6/30. 8 | */ 9 | public class Book implements Serializable { 10 | 11 | private String subtitle; 12 | private String[] author; 13 | private String pubdate; 14 | private String origin_title; 15 | private String image; 16 | private String catalog; 17 | private String alt; 18 | private String id; 19 | private String publisher; 20 | private String title; 21 | private String url; 22 | private String author_intro; 23 | private String summary; 24 | private String price; 25 | private String pages; 26 | private Images images; 27 | 28 | 29 | public String getSubtitle() { 30 | return subtitle; 31 | } 32 | 33 | public void setSubtitle(String subtitle) { 34 | this.subtitle = subtitle; 35 | } 36 | 37 | public String[] getAuthor() { 38 | return author; 39 | } 40 | 41 | public void setAuthor(String[] author) { 42 | this.author = author; 43 | } 44 | 45 | public String getPubdate() { 46 | return pubdate; 47 | } 48 | 49 | public void setPubdate(String pubdate) { 50 | this.pubdate = pubdate; 51 | } 52 | 53 | public String getOrigin_title() { 54 | return origin_title; 55 | } 56 | 57 | public void setOrigin_title(String origin_title) { 58 | this.origin_title = origin_title; 59 | } 60 | 61 | public String getImage() { 62 | return image; 63 | } 64 | 65 | public void setImage(String image) { 66 | this.image = image; 67 | } 68 | 69 | public String getCatalog() { 70 | return catalog; 71 | } 72 | 73 | public void setCatalog(String catalog) { 74 | this.catalog = catalog; 75 | } 76 | 77 | public String getAlt() { 78 | return alt; 79 | } 80 | 81 | public void setAlt(String alt) { 82 | this.alt = alt; 83 | } 84 | 85 | public String getId() { 86 | return id; 87 | } 88 | 89 | public void setId(String id) { 90 | this.id = id; 91 | } 92 | 93 | public String getPublisher() { 94 | return publisher; 95 | } 96 | 97 | public void setPublisher(String publisher) { 98 | this.publisher = publisher; 99 | } 100 | 101 | public String getTitle() { 102 | return title; 103 | } 104 | 105 | public void setTitle(String title) { 106 | this.title = title; 107 | } 108 | 109 | public String getUrl() { 110 | return url; 111 | } 112 | 113 | public void setUrl(String url) { 114 | this.url = url; 115 | } 116 | 117 | public String getAuthor_intro() { 118 | return author_intro; 119 | } 120 | 121 | public void setAuthor_intro(String author_intro) { 122 | this.author_intro = author_intro; 123 | } 124 | 125 | public String getSummary() { 126 | return summary; 127 | } 128 | 129 | public void setSummary(String summary) { 130 | this.summary = summary; 131 | } 132 | 133 | public String getPrice() { 134 | return price; 135 | } 136 | 137 | public void setPrice(String price) { 138 | this.price = price; 139 | } 140 | 141 | public String getPages() { 142 | return pages; 143 | } 144 | 145 | public void setPages(String pages) { 146 | this.pages = pages; 147 | } 148 | 149 | public Images getImages() { 150 | return images; 151 | } 152 | 153 | public void setImages(Images images) { 154 | this.images = images; 155 | } 156 | 157 | public class Images implements Serializable { 158 | private String small; 159 | private String large; 160 | private String medium; 161 | 162 | public String getSmall() { 163 | return small; 164 | } 165 | 166 | public void setSmall(String small) { 167 | this.small = small; 168 | } 169 | 170 | public String getLarge() { 171 | return large; 172 | } 173 | 174 | public void setLarge(String large) { 175 | this.large = large; 176 | } 177 | 178 | public String getMedium() { 179 | return medium; 180 | } 181 | 182 | public void setMedium(String medium) { 183 | this.medium = medium; 184 | } 185 | } 186 | 187 | @Override 188 | public String toString() { 189 | return "Book{" + 190 | "subtitle='" + subtitle + '\'' + 191 | ", author=" + Arrays.toString(author) + 192 | ", pubdate='" + pubdate + '\'' + 193 | ", origin_title='" + origin_title + '\'' + 194 | ", image='" + image + '\'' + 195 | ", catalog='" + catalog + '\'' + 196 | ", alt='" + alt + '\'' + 197 | ", id='" + id + '\'' + 198 | ", publisher='" + publisher + '\'' + 199 | ", title='" + title + '\'' + 200 | ", url='" + url + '\'' + 201 | ", author_intro='" + author_intro + '\'' + 202 | ", summary='" + summary + '\'' + 203 | ", price='" + price + '\'' + 204 | '}'; 205 | } 206 | 207 | 208 | 209 | 210 | } 211 | -------------------------------------------------------------------------------- /Library/src/main/java/com/linked/erfli/library/widget/RefreshLayout.java: -------------------------------------------------------------------------------- 1 | package com.linked.erfli.library.widget; 2 | 3 | import android.content.Context; 4 | import android.support.v4.view.MotionEventCompat; 5 | import android.support.v4.view.ViewCompat; 6 | import android.util.AttributeSet; 7 | import android.view.MotionEvent; 8 | import android.view.ViewConfiguration; 9 | import android.view.animation.Animation; 10 | 11 | import com.linked.erfli.library.utils.DisplayUtil; 12 | 13 | 14 | public class RefreshLayout extends PullToRefreshLayout { 15 | 16 | private final int mTouchSlop; 17 | private OnLoadListener mOnLoadListener; 18 | private float mInitialDownY; 19 | private boolean mIsBeingDragged; 20 | private boolean isLoading = false; 21 | 22 | public RefreshLayout(Context context) { 23 | this(context, null); 24 | } 25 | 26 | public RefreshLayout(Context context, AttributeSet attrs) { 27 | super(context, attrs); 28 | mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 29 | initListener(); 30 | } 31 | 32 | private void initListener(){ 33 | mRefreshListener = new Animation.AnimationListener() { 34 | @Override 35 | public void onAnimationStart(Animation animation) { 36 | } 37 | 38 | @Override 39 | public void onAnimationRepeat(Animation animation) { 40 | } 41 | 42 | @Override 43 | public void onAnimationEnd(Animation animation) { 44 | if(!isLoading){ 45 | if (isRefreshing()) { 46 | // Make sure the progress view is fully visible 47 | mProgress.setAlpha(MAX_ALPHA); 48 | mProgress.start(); 49 | if (mNotify) { 50 | if (mListener != null) { 51 | mListener.onRefresh(); 52 | } 53 | } 54 | mCurrentTargetOffsetTop = mCircleView.getTop(); 55 | } else { 56 | reset(); 57 | } 58 | }else{ 59 | if(mOnLoadListener != null){ 60 | mOnLoadListener.onLoad(); 61 | } 62 | } 63 | } 64 | }; 65 | } 66 | @Override 67 | public boolean dispatchTouchEvent(MotionEvent ev) { 68 | final int action = MotionEventCompat.getActionMasked(ev); 69 | 70 | if (!isEnabled() || canChildScrollDown()) { 71 | // Fail fast if we're not in a state where a swipe is possible 72 | return super.dispatchTouchEvent(ev); 73 | } 74 | switch (action) { 75 | case MotionEvent.ACTION_DOWN: 76 | mIsBeingDragged = false; 77 | mInitialDownY = ev.getY(); 78 | break; 79 | 80 | case MotionEvent.ACTION_MOVE: { 81 | final float y = ev.getY(); 82 | final float overscrollTop = (mInitialDownY -y) * DRAG_RATE; 83 | if (overscrollTop > mTouchSlop) { 84 | mIsBeingDragged = true; 85 | if(!isLoading){ 86 | isLoading = true; 87 | setProgressViewOffset(true, getBottom() - mCircleView.getHeight(), (int)(getBottom() - mCircleView.getHeight() - mTotalDragDistance)); 88 | } 89 | moveSpinner(overscrollTop); 90 | return true; 91 | } 92 | break; 93 | } 94 | case MotionEventCompat.ACTION_POINTER_UP: 95 | case MotionEvent.ACTION_UP: { 96 | if (mIsBeingDragged) { 97 | final float y = ev.getY(); 98 | final float overscrollTop = (mInitialDownY - y) * DRAG_RATE; 99 | mIsBeingDragged = false; 100 | finishSpinner(overscrollTop); 101 | return true; 102 | } 103 | } 104 | } 105 | 106 | return super.dispatchTouchEvent(ev); 107 | } 108 | 109 | private boolean canChildScrollDown() { 110 | return ViewCompat.canScrollVertically(mTarget, 1); 111 | } 112 | 113 | public void setLoading(boolean loading) { 114 | if (mTarget == null) return; 115 | isLoading = loading; 116 | if (loading) { 117 | if (isRefreshing()) { 118 | super.setRefreshing(true); 119 | } 120 | mOnLoadListener.onLoad(); 121 | } else { 122 | super.setRefreshing(false); 123 | setProgressViewOffset(true,getTop(),(int)(DEFAULT_CIRCLE_TARGET * DisplayUtil.SCREEN_DENSITY)); 124 | mInitialDownY = 0; 125 | } 126 | } 127 | 128 | @Override 129 | public void setRefreshing(boolean refreshing) { 130 | if(isLoading){ 131 | setLoading(refreshing); 132 | }else{ 133 | super.setRefreshing(refreshing); 134 | } 135 | if(!refreshing){ 136 | setProgressViewOffset(true,-mCircleView.getHeight(), DEFAULT_CIRCLE_TARGET); 137 | } 138 | } 139 | 140 | public void setOnLoadListener(OnLoadListener loadListener) { 141 | mOnLoadListener = loadListener; 142 | } 143 | 144 | public interface OnLoadListener { 145 | public void onLoad(); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /Library/src/main/java/com/linked/erfli/library/widget/CircleImageView.java: -------------------------------------------------------------------------------- 1 | package com.linked.erfli.library.widget; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Color; 6 | import android.graphics.Paint; 7 | import android.graphics.RadialGradient; 8 | import android.graphics.Shader; 9 | import android.graphics.drawable.ShapeDrawable; 10 | import android.graphics.drawable.shapes.OvalShape; 11 | import android.support.v4.view.ViewCompat; 12 | import android.view.animation.Animation; 13 | import android.widget.ImageView; 14 | 15 | /** 16 | * Private class created to work around issues with AnimationListeners being 17 | * called before the animation is actually complete and support shadows on older 18 | * platforms. 19 | * 20 | * @hide 21 | */ 22 | class CircleImageView extends ImageView { 23 | 24 | private static final int KEY_SHADOW_COLOR = 0x1E000000; 25 | private static final int FILL_SHADOW_COLOR = 0x3D000000; 26 | // PX 27 | private static final float X_OFFSET = 0f; 28 | private static final float Y_OFFSET = 1.75f; 29 | private static final float SHADOW_RADIUS = 3.5f; 30 | private static final int SHADOW_ELEVATION = 4; 31 | 32 | private Animation.AnimationListener mListener; 33 | private int mShadowRadius; 34 | 35 | public CircleImageView(Context context, int color, final float radius) { 36 | super(context); 37 | final float density = getContext().getResources().getDisplayMetrics().density; 38 | final int diameter = (int) (radius * density * 2); 39 | final int shadowYOffset = (int) (density * Y_OFFSET); 40 | final int shadowXOffset = (int) (density * X_OFFSET); 41 | 42 | mShadowRadius = (int) (density * SHADOW_RADIUS); 43 | 44 | ShapeDrawable circle; 45 | if (elevationSupported()) { 46 | circle = new ShapeDrawable(new OvalShape()); 47 | ViewCompat.setElevation(this, SHADOW_ELEVATION * density); 48 | } else { 49 | OvalShape oval = new OvalShadow(mShadowRadius, diameter); 50 | circle = new ShapeDrawable(oval); 51 | ViewCompat.setLayerType(this, ViewCompat.LAYER_TYPE_SOFTWARE, circle.getPaint()); 52 | circle.getPaint().setShadowLayer(mShadowRadius, shadowXOffset, shadowYOffset, 53 | KEY_SHADOW_COLOR); 54 | final int padding = mShadowRadius; 55 | // set padding so the inner image sits correctly within the shadow. 56 | setPadding(padding, padding, padding, padding); 57 | } 58 | circle.getPaint().setColor(color); 59 | setBackgroundDrawable(circle); 60 | } 61 | 62 | private boolean elevationSupported() { 63 | return android.os.Build.VERSION.SDK_INT >= 21; 64 | } 65 | 66 | @Override 67 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 68 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 69 | if (!elevationSupported()) { 70 | setMeasuredDimension(getMeasuredWidth() + mShadowRadius*2, getMeasuredHeight() 71 | + mShadowRadius*2); 72 | } 73 | } 74 | 75 | public void setAnimationListener(Animation.AnimationListener listener) { 76 | mListener = listener; 77 | } 78 | 79 | @Override 80 | public void onAnimationStart() { 81 | super.onAnimationStart(); 82 | if (mListener != null) { 83 | mListener.onAnimationStart(getAnimation()); 84 | } 85 | } 86 | 87 | @Override 88 | public void onAnimationEnd() { 89 | super.onAnimationEnd(); 90 | if (mListener != null) { 91 | mListener.onAnimationEnd(getAnimation()); 92 | } 93 | } 94 | 95 | /** 96 | * Update the background color of the circle image view. 97 | * 98 | * @param colorRes Id of a color resource. 99 | */ 100 | public void setBackgroundColorRes(int colorRes) { 101 | setBackgroundColor(getContext().getResources().getColor(colorRes)); 102 | } 103 | 104 | @Override 105 | public void setBackgroundColor(int color) { 106 | if (getBackground() instanceof ShapeDrawable) { 107 | ((ShapeDrawable) getBackground()).getPaint().setColor(color); 108 | } 109 | } 110 | 111 | private class OvalShadow extends OvalShape { 112 | private RadialGradient mRadialGradient; 113 | private Paint mShadowPaint; 114 | private int mCircleDiameter; 115 | 116 | public OvalShadow(int shadowRadius, int circleDiameter) { 117 | super(); 118 | mShadowPaint = new Paint(); 119 | mShadowRadius = shadowRadius; 120 | mCircleDiameter = circleDiameter; 121 | mRadialGradient = new RadialGradient(mCircleDiameter / 2, mCircleDiameter / 2, 122 | mShadowRadius, new int[] { 123 | FILL_SHADOW_COLOR, Color.TRANSPARENT 124 | }, null, Shader.TileMode.CLAMP); 125 | mShadowPaint.setShader(mRadialGradient); 126 | } 127 | 128 | @Override 129 | public void draw(Canvas canvas, Paint paint) { 130 | final int viewWidth = CircleImageView.this.getWidth(); 131 | final int viewHeight = CircleImageView.this.getHeight(); 132 | canvas.drawCircle(viewWidth / 2, viewHeight / 2, (mCircleDiameter / 2 + mShadowRadius), 133 | mShadowPaint); 134 | canvas.drawCircle(viewWidth / 2, viewHeight / 2, (mCircleDiameter / 2), paint); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /Library/src/main/java/com/linked/erfli/library/utils/operators/AppObservable.java: -------------------------------------------------------------------------------- 1 | package com.linked.erfli.library.utils.operators; 2 | 3 | /** 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import android.app.Activity; 18 | import android.app.Fragment; 19 | import android.os.Build; 20 | 21 | 22 | 23 | import rx.Observable; 24 | import rx.functions.Func1; 25 | import rx.schedulers.Schedulers; 26 | 27 | import static rx.android.schedulers.AndroidSchedulers.mainThread; 28 | 29 | public final class AppObservable { 30 | public static final boolean USES_SUPPORT_FRAGMENTS; 31 | private static final Func1 ACTIVITY_VALIDATOR = new Func1() { 32 | @Override 33 | public Boolean call(Activity activity) { 34 | return !activity.isFinishing(); 35 | } 36 | }; 37 | private static final Func1 FRAGMENT_VALIDATOR = new Func1() { 38 | @Override 39 | public Boolean call(Fragment fragment) { 40 | return fragment.isAdded() && !fragment.getActivity().isFinishing(); 41 | } 42 | }; 43 | private static final Func1 FRAGMENTV4_VALIDATOR = 44 | new Func1() { 45 | @Override 46 | public Boolean call(android.support.v4.app.Fragment fragment) { 47 | return fragment.isAdded() && !fragment.getActivity().isFinishing(); 48 | } 49 | }; 50 | 51 | static { 52 | boolean supportFragmentsAvailable = false; 53 | try { 54 | Class.forName("android.support.v4.app.Fragment"); 55 | supportFragmentsAvailable = true; 56 | } catch (ClassNotFoundException e) { 57 | } 58 | 59 | USES_SUPPORT_FRAGMENTS = supportFragmentsAvailable; 60 | } 61 | 62 | private AppObservable() { 63 | throw new AssertionError("No instances"); 64 | } 65 | 66 | /** 67 | * Binds the given source sequence to an activity. 68 | *

69 | * This helper will schedule the given sequence to be observed on the main UI thread and ensure 70 | * that no notifications will be forwarded to the activity in case it is scheduled to finish. 71 | *

72 | * You should unsubscribe from the returned Observable in onDestroy at the latest, in order to not 73 | * leak the activity or an inner subscriber. Conversely, when the source sequence can outlive the activity, 74 | * make sure to bind to new instances of the activity again, e.g. after going through configuration changes. 75 | * Refer to the samples project for actual examples. 76 | * 77 | * @param activity the activity to bind the source sequence to 78 | * @param source the source sequence 79 | */ 80 | public static Observable bindActivity(Activity activity, Observable source) { 81 | /* 82 | WrapComponent wrap = null; 83 | if (activity instanceof LinkedinActivityBase) { 84 | wrap = ((LinkedinActivityBase) activity).wrapComponent; 85 | }else if (activity instanceof LinkedinActionBarActivityBase) { 86 | wrap = ((LinkedinActionBarActivityBase) activity).wrapComponent; 87 | } 88 | if (wrap != null) { 89 | return source.observeOn(mainThread()).compose(wrap.bindToLifecycle()); 90 | } 91 | */ 92 | return source.subscribeOn(Schedulers.from(ThreadPool.executor)).observeOn(mainThread()).lift(new OperatorConditionalBinding(activity, ACTIVITY_VALIDATOR)); 93 | } 94 | 95 | /** 96 | * Binds the given source sequence to a fragment (native or support-v4). 97 | *

98 | * This helper will schedule the given sequence to be observed on the main UI thread and ensure 99 | * that no notifications will be forwarded to the fragment in case it gets detached from its 100 | * activity or the activity is scheduled to finish. 101 | *

102 | * You should unsubscribe from the returned Observable in onDestroy for normal fragments, or in onDestroyView 103 | * for retained fragments, in order to not leak any references to the host activity or the fragment. 104 | * Refer to the samples project for actual examples. 105 | * 106 | * @param fragment the fragment to bind the source sequence to 107 | * @param source the source sequence 108 | */ 109 | public static Observable bindFragment(Object fragment, Observable source) { 110 | /* 111 | if (fragment instanceof LinkedinFragmentBase) { 112 | WrapComponent wrap = ((LinkedinFragmentBase) fragment).wrapComponent; 113 | return source.observeOn(mainThread()).compose(wrap.bindToLifecycle()); 114 | } 115 | */ 116 | final Observable o = source.subscribeOn(Schedulers.from(ThreadPool.executor)).observeOn(mainThread()); 117 | if (USES_SUPPORT_FRAGMENTS && fragment instanceof android.support.v4.app.Fragment) { 118 | android.support.v4.app.Fragment f = (android.support.v4.app.Fragment) fragment; 119 | return o.lift(new OperatorConditionalBinding(f, FRAGMENTV4_VALIDATOR)); 120 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB && fragment instanceof Fragment) { 121 | Fragment f = (Fragment) fragment; 122 | return o.lift(new OperatorConditionalBinding(f, FRAGMENT_VALIDATOR)); 123 | } else { 124 | throw new IllegalArgumentException("Target fragment is neither a native nor support library Fragment"); 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /ModuleB/src/main/java/com/linked/erfli/moduleb/news/NewsListActivity.java: -------------------------------------------------------------------------------- 1 | package com.linked.erfli.moduleb.news; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.support.v7.widget.DefaultItemAnimator; 6 | import android.support.v7.widget.LinearLayoutManager; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.support.v7.widget.Toolbar; 9 | import android.text.TextUtils; 10 | import android.view.View; 11 | import android.widget.Toast; 12 | 13 | import com.github.mzule.activityrouter.annotation.Router; 14 | import com.github.mzule.activityrouter.router.Routers; 15 | import com.linked.erfli.library.base.BaseActivity; 16 | import com.linked.erfli.library.utils.EventPool; 17 | import com.linked.erfli.library.utils.Utils; 18 | import com.linked.erfli.library.utils.operators.AppObservable; 19 | import com.linked.erfli.library.widget.DividerOffsetDecoration; 20 | import com.linked.erfli.library.widget.PullToRefreshLayout; 21 | import com.linked.erfli.library.widget.RecyclerItemClickListener; 22 | import com.linked.erfli.library.widget.RefreshLayout; 23 | import com.linked.erfli.moduleb.R; 24 | import com.linked.erfli.moduleb.utils.ZhihuApiHttp; 25 | 26 | import org.greenrobot.eventbus.EventBus; 27 | 28 | import java.util.ArrayList; 29 | import java.util.Date; 30 | import java.util.List; 31 | 32 | import rx.functions.Action1; 33 | 34 | @Router("news_list") 35 | public class NewsListActivity extends BaseActivity { 36 | 37 | private RecyclerView mRecyclerView; 38 | private LinearLayoutManager mLayoutManager; 39 | private List myDataset; 40 | private NewsAdapter mAdapter; 41 | RefreshLayout refreshLayout; 42 | 43 | private int page = 0; 44 | 45 | @Override 46 | protected void onCreate(Bundle savedInstanceState) { 47 | super.onCreate(savedInstanceState); 48 | setContentView(R.layout.module2_activity_recycler_view); 49 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 50 | toolbar.setTitle(R.string.module2_latest_news); 51 | setSupportActionBar(toolbar); 52 | getSupportActionBar().setHomeButtonEnabled(true); 53 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 54 | toolbar.setNavigationOnClickListener(new View.OnClickListener() { 55 | @Override 56 | public void onClick(View view) { 57 | onBackPressed(); 58 | } 59 | }); 60 | initRefreshView(); 61 | mRecyclerView = (RecyclerView) findViewById(R.id.recyclerView); 62 | mLayoutManager = new LinearLayoutManager(this); 63 | mRecyclerView.setLayoutManager(mLayoutManager); 64 | mRecyclerView.addItemDecoration(new DividerOffsetDecoration()); 65 | mRecyclerView.setItemAnimator(new DefaultItemAnimator()); 66 | initData(); 67 | } 68 | 69 | private void initRefreshView() { 70 | refreshLayout = (RefreshLayout) findViewById(R.id.refresh_layout); 71 | refreshLayout.setColorSchemeResources(R.color.google_blue, 72 | R.color.google_green, 73 | R.color.google_red, 74 | R.color.google_yellow); 75 | refreshLayout.setOnRefreshListener(new PullToRefreshLayout.OnRefreshListener() { 76 | @Override 77 | public void onRefresh() { 78 | getLatestData(); 79 | } 80 | }); 81 | refreshLayout.setOnLoadListener(new RefreshLayout.OnLoadListener() { 82 | @Override 83 | public void onLoad() { 84 | getHistoryData(); 85 | } 86 | }); 87 | } 88 | 89 | private void initData() { 90 | myDataset = new ArrayList<>(); 91 | mAdapter = new NewsAdapter(this, myDataset); 92 | mRecyclerView.setAdapter(mAdapter); 93 | mRecyclerView.addOnItemTouchListener(new RecyclerItemClickListener(this, new RecyclerItemClickListener.OnItemClickListener() { 94 | @Override 95 | public void onItemClick(View view, int position) { 96 | // Intent intent = new Intent(NewsListActivity.this, NewsDetailActivity.class); 97 | // intent.putExtra(NewsDetailActivity.NEWS, mAdapter.getItem(position)); 98 | // startActivity(intent); 99 | Story story = mAdapter.getItem(position); 100 | Routers.open(NewsListActivity.this, "modularization://news_detail/" + story.getId() + "/" + Utils.encodeUrlParam(story.getTitle())); 101 | } 102 | })); 103 | getLatestData(); 104 | } 105 | 106 | @Override 107 | public void onResume() { 108 | super.onResume(); 109 | } 110 | 111 | 112 | @Override 113 | public void onPause() { 114 | super.onPause(); 115 | } 116 | 117 | private void getLatestData() { 118 | page = 0; 119 | getStoryList(""); 120 | } 121 | 122 | private void getHistoryData() { 123 | String time = getDateString(new Date()); 124 | String key = String.valueOf(Long.valueOf(time) - page); 125 | page += 1; 126 | getStoryList(key); 127 | } 128 | 129 | private void getStoryList(String newsKey) { 130 | if (TextUtils.isEmpty(newsKey)) { 131 | AppObservable.bindActivity(this, ZhihuApiHttp.http.getLatestNews()).subscribe(new Action1() { 132 | @Override 133 | public void call(NewsResponse newsResponse) { 134 | refreshLayout.setRefreshing(false); 135 | myDataset.clear(); 136 | myDataset.addAll(newsResponse.getStories()); 137 | mAdapter.notifyDataSetChanged(); 138 | } 139 | }, new Action1() { 140 | @Override 141 | public void call(Throwable throwable) { 142 | Toast.makeText(NewsListActivity.this, "network error", Toast.LENGTH_SHORT).show(); 143 | refreshLayout.setRefreshing(false); 144 | } 145 | }); 146 | } else { 147 | AppObservable.bindActivity(this, ZhihuApiHttp.http.getHistoryNews(newsKey)).subscribe(new Action1() { 148 | @Override 149 | public void call(NewsResponse newsResponse) { 150 | refreshLayout.setRefreshing(false); 151 | myDataset.addAll(newsResponse.getStories()); 152 | mAdapter.notifyDataSetChanged(); 153 | } 154 | }, new Action1() { 155 | @Override 156 | public void call(Throwable throwable) { 157 | Toast.makeText(NewsListActivity.this, "network error", Toast.LENGTH_SHORT).show(); 158 | refreshLayout.setRefreshing(false); 159 | } 160 | }); 161 | } 162 | } 163 | 164 | public static String getDateString(Date date) { 165 | String year = (date.getYear() + 1900) + ""; 166 | String mm = (date.getMonth() + 1) + ""; 167 | if (Integer.valueOf(mm).intValue() < 10) { 168 | mm = "0" + mm; 169 | } 170 | String day = date.getDate() + ""; 171 | if (Integer.valueOf(day).intValue() < 10) day = "0" + day; 172 | return year + mm + day; 173 | } 174 | 175 | } 176 | -------------------------------------------------------------------------------- /ModuleB/src/main/assets/news.css: -------------------------------------------------------------------------------- 1 | article, 2 | aside, 3 | details, 4 | figcaption, 5 | figure, 6 | footer, 7 | header, 8 | hgroup, 9 | main, 10 | nav, 11 | section, 12 | summary { 13 | display: block; 14 | } 15 | audio, 16 | canvas, 17 | video { 18 | display: inline-block; 19 | } 20 | audio:not([controls]) { 21 | display: none; 22 | height: 0; 23 | } 24 | html { 25 | font-family: sans-serif; 26 | -webkit-text-size-adjust: 100%; 27 | } 28 | body { 29 | font-family: 'Helvetica Neue', Helvetica, Arial, Sans-serif; 30 | background: #fff; 31 | padding-top: 0; 32 | margin: 0; 33 | } 34 | a:focus { 35 | outline: thin dotted; 36 | } 37 | a:active, 38 | a:hover { 39 | outline: 0; 40 | } 41 | h1 { 42 | margin: .67em 0; 43 | } 44 | h1, 45 | h2, 46 | h3, 47 | h4, 48 | h5, 49 | h6 { 50 | font-size: 16px; 51 | } 52 | abbr[title] { 53 | border-bottom: 1px dotted; 54 | } 55 | hr { 56 | box-sizing: content-box; 57 | height: 0; 58 | } 59 | mark { 60 | background: #ff0; 61 | color: #000; 62 | } 63 | code, 64 | kbd, 65 | pre, 66 | samp { 67 | font-family: monospace,serif; 68 | font-size: 1em; 69 | } 70 | pre { 71 | white-space: pre-wrap; 72 | } 73 | q { 74 | quotes: \201C\201D\2018\2019; 75 | } 76 | small { 77 | font-size: 80%; 78 | } 79 | sub, 80 | sup { 81 | font-size: 75%; 82 | line-height: 0; 83 | position: relative; 84 | vertical-align: baseline; 85 | } 86 | sup { 87 | top: -0.5em; 88 | } 89 | sub { 90 | bottom: -0.25em; 91 | } 92 | img { 93 | border: 0; 94 | vertical-align: middle; 95 | color: transparent; 96 | font-size: 0; 97 | } 98 | svg:not(:root) { 99 | overflow: hidden; 100 | } 101 | figure { 102 | margin: 0; 103 | } 104 | fieldset { 105 | border: 1px solid silver; 106 | margin: 0 2px; 107 | padding: .35em .625em .75em; 108 | } 109 | legend { 110 | border: 0; 111 | padding: 0; 112 | } 113 | table { 114 | border-collapse: collapse; 115 | border-spacing: 0; 116 | overflow: hidden; 117 | } 118 | a { 119 | text-decoration: none; 120 | } 121 | blockquote { 122 | border-left: 3px solid #D0E5F2; 123 | font-style: normal; 124 | display: block; 125 | line-height: 1.4em; 126 | vertical-align: baseline; 127 | font-size: 100%; 128 | margin: .5em 0; 129 | padding: 0 0 0 .5em; 130 | } 131 | ul, 132 | ol { 133 | padding-left: 20px; 134 | } 135 | .main-wrap { 136 | max-width: 100%; 137 | min-width: 300px; 138 | margin: 0 auto; 139 | } 140 | .content-wrap { 141 | overflow: hidden; 142 | background-color: #f9f9f9; 143 | } 144 | .content-wrap a { 145 | word-break: break-all; 146 | } 147 | .headline { 148 | border-bottom: 4px solid #f6f6f6; 149 | } 150 | .headline-title.onlyheading { 151 | margin: 20px 0; 152 | } 153 | .headline img { 154 | max-width: 100%; 155 | vertical-align: top; 156 | } 157 | .headline-background-link { 158 | line-height: 2em; 159 | position: relative; 160 | display: block; 161 | padding: 20px 45px 20px 20px !important; 162 | } 163 | .icon-arrow-right { 164 | position: absolute; 165 | top: 50%; 166 | right: 20px; 167 | background-image: url(http://static.daily.zhihu.com/img/share-icons.png); 168 | background-repeat: no-repeat; 169 | display: inline-block; 170 | vertical-align: middle; 171 | background-position: -70px -20px; 172 | width: 10px; 173 | height: 15px; 174 | margin-top: -7.5px; 175 | } 176 | .headline-background .heading { 177 | color: #999; 178 | font-size: 15px!important; 179 | margin-bottom: 8px; 180 | line-height: 1em; 181 | } 182 | .headline-background .heading-content { 183 | color: #444; 184 | font-size: 17px!important; 185 | line-height: 1.2em; 186 | } 187 | .headline-title { 188 | line-height: 1.2em; 189 | color: #000; 190 | font-size: 22px; 191 | margin: 20px 0 10px; 192 | padding: 0 20px!important; 193 | font-weight: bold; 194 | } 195 | .meta { 196 | white-space: nowrap; 197 | text-overflow: ellipsis; 198 | overflow: hidden; 199 | font-size: 16px; 200 | color: #b8b8b8; 201 | } 202 | .meta .source-icon { 203 | width: 20px; 204 | height: 20px; 205 | margin-right: 4px; 206 | } 207 | .meta .time { 208 | float: right; 209 | margin-top: 2px; 210 | } 211 | .content { 212 | color: #444; 213 | line-height: 1.6em; 214 | font-size: 17px; 215 | margin: 10px 0 20px; 216 | } 217 | .content img { 218 | max-width: 100%; 219 | display: block; 220 | margin: 10px 0; 221 | } 222 | .content img[src*="zhihu.com/equation"] { 223 | display: inline-block; 224 | margin: 0 3px; 225 | } 226 | .content a { 227 | color: #259; 228 | } 229 | .content a:hover { 230 | text-decoration: underline; 231 | } 232 | .view-more { 233 | margin-bottom: 25px; 234 | text-align: center; 235 | } 236 | .view-more a { 237 | font-size: 16px; 238 | display: inline-block; 239 | width: 125px; 240 | height: 30px; 241 | line-height: 30px; 242 | background: #f0f0f0; 243 | color: #B8B8B8; 244 | } 245 | .question { 246 | overflow: hidden; 247 | padding: 0 20px!important; 248 | } 249 | .question + .question { 250 | border-top: 5px solid #f6f6f6; 251 | } 252 | .question-title { 253 | line-height: 1.4em; 254 | color: #000; 255 | font-weight: 700; 256 | font-size: 18px; 257 | margin: 20px 0; 258 | } 259 | .meta .author { 260 | color: #444; 261 | font-weight: 700; 262 | } 263 | .answer + .answer { 264 | border-top: 2px solid #f6f6f6; 265 | padding-top: 20px; 266 | } 267 | .footer { 268 | text-align: center; 269 | color: #b8b8b8; 270 | font-size: 13px; 271 | padding: 20px 0; 272 | } 273 | .footer a { 274 | color: #b8b8b8; 275 | } 276 | .question .view-more a { 277 | width: 100%; 278 | display: block; 279 | } 280 | .hot-comment { 281 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 282 | } 283 | .comment-label { 284 | font-size: 16px; 285 | color: #333; 286 | line-height: 1.5em; 287 | font-weight: 700; 288 | border-top: 1px solid #eee; 289 | border-bottom: 1px solid #eee; 290 | margin: 0; 291 | padding: 9px 20px; 292 | } 293 | .comment-list { 294 | margin-bottom: 20px; 295 | } 296 | .comment-item { 297 | font-size: 15px; 298 | color: #666; 299 | border-bottom: 1px solid #eee; 300 | padding: 15px 20px; 301 | } 302 | .comment-meta { 303 | position: relative; 304 | margin-bottom: 10px; 305 | } 306 | .comment-meta .author { 307 | vertical-align: middle; 308 | color: #444; 309 | } 310 | .comment-meta .vote { 311 | position: absolute; 312 | color: #b8b8b8; 313 | font-size: 12px; 314 | right: 0; 315 | } 316 | .night .comment-label { 317 | color: #b8b8b8; 318 | border-top: 1px solid #303030; 319 | border-bottom: 1px solid #303030; 320 | } 321 | .night .comment-item { 322 | color: #7f7f7f; 323 | border-bottom: 1px solid #303030; 324 | } 325 | .icon-vote, 326 | .icon-voted { 327 | background-repeat: no-repeat; 328 | display: inline-block; 329 | vertical-align: 0; 330 | width: 11px; 331 | height: 12px; 332 | margin-right: 4px; 333 | background-image: url(http://static.daily.zhihu.com/img/app/Comment_Vote.png) !important; 334 | } 335 | .icon-voted { 336 | background-image: url(http://static.daily.zhihu.com/img/app/Comment_Voted.png) !important; 337 | } 338 | .night .icon-vote { 339 | background-image: url(http://static.daily.zhihu.com/img/app/Dark_Comment_Vote.png) !important; 340 | } 341 | .img-wrap .headline-title { 342 | bottom: 5px; 343 | } 344 | .img-wrap .img-source { 345 | right: 10px!important; 346 | font-size: 9px; 347 | } 348 | .global-header { 349 | position: static; 350 | } 351 | .button { 352 | width: 60px; 353 | } 354 | .button i { 355 | margin-right: 0; 356 | } 357 | .headline .img-place-holder { 358 | height: 200px; 359 | } 360 | .from-column { 361 | width: 280px; 362 | line-height: 30px; 363 | height: 30px; 364 | padding-left: 90px; 365 | color: #2aacec; 366 | background-image: url(http://static.daily.zhihu.com/img/News_Column_Entrance.png); 367 | box-sizing: border-box; 368 | margin: 0 20px 20px; 369 | } 370 | .from-column:active { 371 | background-image: url(http://static.daily.zhihu.com/img/News_Column_Entrance_Highlight.png); 372 | } 373 | .night .headline { 374 | border-bottom: 4px solid #303030; 375 | } 376 | .night img { 377 | -webkit-mask-image: -webkit-gradient(linear, 0 0, 0 100%, from(rgba(0, 0, 0, 0.7)), to(rgba(0, 0, 0, 0.7))); 378 | } 379 | body.night, 380 | .night .content-wrap { 381 | background: #343434; 382 | } 383 | .night .answer + .answer { 384 | border-top: 2px solid #303030; 385 | } 386 | .night .question + .question { 387 | border-top: 4px solid #303030; 388 | } 389 | .night .view-more a { 390 | background: #292929; 391 | color: #666; 392 | } 393 | .night .icon-arrow-right { 394 | background-image: url(http://static.daily.zhihu.com/img/share-icons.png); 395 | background-repeat: no-repeat; 396 | display: inline-block; 397 | vertical-align: middle; 398 | background-position: -70px -35px; 399 | width: 10px; 400 | height: 15px; 401 | } 402 | .night blockquote, 403 | .night sup { 404 | border-left: 3px solid #666; 405 | } 406 | .night .content a { 407 | color: #698ebf; 408 | } 409 | .night .from-column { 410 | color: #2b82ac; 411 | background-image: url(http://static.daily.zhihu.com/img/Dark_News_Column_Entrance.png); 412 | } 413 | .night .from-column:active { 414 | background-image: url(http://static.daily.zhihu.com/img/Dark_News_Column_Entrance_Highlight.png); 415 | } 416 | .large .question-title { 417 | font-size: 24px; 418 | } 419 | .large .meta { 420 | font-size: 18px; 421 | } 422 | .large .content { 423 | font-size: 20px; 424 | } 425 | .large blockquote, 426 | .large sup { 427 | line-height: 1.6; 428 | } 429 | .meta .meta-item { 430 | -o-text-overflow: ellipsis; 431 | width: 39%; 432 | overflow: hidden; 433 | white-space: nowrap; 434 | text-overflow: ellipsis; 435 | display: inline-block; 436 | color: #929292; 437 | margin-right: 7px; 438 | } 439 | .headline .meta { 440 | white-space: nowrap; 441 | text-overflow: ellipsis; 442 | overflow: hidden; 443 | font-size: 11px; 444 | color: #b8b8b8; 445 | margin: 15px 0; 446 | padding: 0 20px; 447 | } 448 | .headline .meta a, 449 | .headline .meta a:hover { 450 | padding-left: 1em; 451 | margin-top: 2px; 452 | float: right; 453 | font-size: 11px; 454 | color: #0066cf; 455 | text-decoration: none; 456 | } 457 | .highlight { 458 | overflow: auto; 459 | word-wrap: normal; 460 | background: #fff; 461 | } 462 | .highlight pre { 463 | white-space: pre; 464 | } 465 | .highlight .hll { 466 | background-color: #ffc; 467 | } 468 | .highlight .err { 469 | color: #a61717; 470 | background-color: #e3d2d2; 471 | } 472 | .highlight .cp { 473 | color: #999; 474 | font-weight: 700; 475 | } 476 | .highlight .cs { 477 | color: #999; 478 | font-weight: 700; 479 | font-style: italic; 480 | } 481 | .highlight .gd { 482 | color: #000; 483 | background-color: #fdd; 484 | } 485 | .highlight .gi { 486 | color: #000; 487 | background-color: #dfd; 488 | } 489 | .highlight .gu { 490 | color: #aaa; 491 | } 492 | .highlight .ni { 493 | color: purple; 494 | } 495 | .highlight .nt { 496 | color: navy; 497 | } 498 | .highlight .w { 499 | color: #bbb; 500 | } 501 | .highlight .sr { 502 | color: olive; 503 | } 504 | [hidden], 505 | .button span { 506 | display: none; 507 | } 508 | b, 509 | strong, 510 | .highlight .k, 511 | .highlight .o, 512 | .highlight .gs, 513 | .highlight .kc, 514 | .highlight .kd, 515 | .highlight .kn, 516 | .highlight .kp, 517 | .highlight .kr, 518 | .highlight .ow { 519 | font-weight: 700; 520 | } 521 | dfn, 522 | .highlight .ge { 523 | font-style: italic; 524 | } 525 | .meta span, 526 | .meta .source { 527 | vertical-align: middle; 528 | } 529 | .meta .avatar, 530 | .comment-meta .avatar { 531 | width: 20px; 532 | height: 20px; 533 | border-radius: 2px; 534 | margin-right: 5px; 535 | } 536 | .meta .bio, 537 | .highlight .gh, 538 | .highlight .bp { 539 | color: #999; 540 | } 541 | .night .comment-meta .author, 542 | .night .content, 543 | .night .meta .author, 544 | .highlight .go { 545 | color: #888; 546 | } 547 | .night .headline-title, 548 | .night .headline-background .heading-content, 549 | .night .question-title { 550 | color: #B8B8B8; 551 | } 552 | .highlight .c, 553 | .highlight .cm, 554 | .highlight .c1 { 555 | color: #998; 556 | font-style: italic; 557 | } 558 | .highlight .gr, 559 | .highlight .gt { 560 | color: #a00; 561 | } 562 | .highlight .gp, 563 | .highlight .nn { 564 | color: #555; 565 | } 566 | .highlight .kt, 567 | .highlight .nc { 568 | color: #458; 569 | font-weight: 700; 570 | } 571 | .highlight .m, 572 | .highlight .mf, 573 | .highlight .mh, 574 | .highlight .mi, 575 | .highlight .mo, 576 | .highlight .il { 577 | color: #099; 578 | } 579 | .highlight .s, 580 | .highlight .sb, 581 | .highlight .sc, 582 | .highlight .sd, 583 | .highlight .s2, 584 | .highlight .se, 585 | .highlight .sh, 586 | .highlight .si, 587 | .highlight .sx, 588 | .highlight .s1, 589 | .highlight .ss { 590 | color: #d32; 591 | } 592 | .highlight .na, 593 | .highlight .nb, 594 | .highlight .no, 595 | .highlight .nv, 596 | .highlight .vc, 597 | .highlight .vg, 598 | .highlight .vi { 599 | color: teal; 600 | } 601 | .highlight .ne, 602 | .highlight .nf { 603 | color: #900; 604 | font-weight: 700; 605 | } 606 | .answer h1, 607 | .answer h2, 608 | .answer h3, 609 | .answer h4, 610 | .answer h5 { 611 | font-size: 19px; 612 | } 613 | @media only screen and (-webkit-min-device-pixel-ratio2), only screen and (min-device-pixel-ratio2) { 614 | .icon-arrow-right { 615 | background-image: url(http://static.daily.zhihu.com/img/share-icons@2x.png); 616 | -webkit-background-size: 82px 55px; 617 | background-size: 82px 55px; 618 | } 619 | .icon-vote, 620 | .icon-voted { 621 | background-image: url(http://static.daily.zhihu.com/img/app/Comment_Vote@2x.png) !important; 622 | background-size: 11px 12px; 623 | } 624 | .icon-voted { 625 | background-image: url(http://static.daily.zhihu.com/img/app/Comment_Voted@2x.png) !important; 626 | } 627 | .night .icon-vote { 628 | background-image: url(http://static.daily.zhihu.com/img/app/Dark_Comment_Vote@2x.png) !important; 629 | } 630 | .from-column { 631 | background-image: url(http://static.daily.zhihu.com/img/News_Column_Entrance@2x.png) !important; 632 | background-size: 280px 30px; 633 | } 634 | .from-column:active { 635 | background-image: url(http://static.daily.zhihu.com/img/News_Column_Entrance_Highlight@2x.png) !important; 636 | } 637 | .night .from-column { 638 | color: #2b82ac; 639 | background-image: url(http://static.daily.zhihu.com/img/Dark_News_Column_Entrance@2x.png) !important; 640 | } 641 | .night .from-column:active { 642 | background-image: url(http://static.daily.zhihu.com/img/Dark_News_Column_Entrance_Highlight@2x.png) !important; 643 | } 644 | } 645 | .meta .meta-item { 646 | width: 39%; 647 | overflow: hidden; 648 | white-space: nowrap; 649 | text-overflow: ellipsis; 650 | display: inline-block; 651 | color: #929292; 652 | margin-right: 7px; 653 | } 654 | .headline .meta { 655 | white-space: nowrap; 656 | text-overflow: ellipsis; 657 | overflow: hidden; 658 | font-size: 11px; 659 | color: #b8b8b8; 660 | margin: 20px 0; 661 | padding: 0 20px; 662 | } 663 | .headline .meta a, 664 | .headline .meta a:hover { 665 | margin-top: 2px; 666 | float: right; 667 | font-size: 11px; 668 | color: #0066cf; 669 | text-decoration: none; 670 | } 671 | .answer h1, 672 | .answer h2, 673 | .answer h3, 674 | .answer h4, 675 | .answer h5 { 676 | font-size: 19px; 677 | } 678 | .origin-source, 679 | a.origin-source:link { 680 | display: block; 681 | margin: 25px 0; 682 | height: 50px; 683 | overflow: hidden; 684 | background: #f0f0f0; 685 | color: #888; 686 | position: relative; 687 | -webkit-touch-callout: none; 688 | } 689 | .origin-source .source-logo, 690 | a.origin-source:link .source-logo { 691 | float: left; 692 | width: 50px; 693 | height: 50px; 694 | margin-right: 10px; 695 | } 696 | .origin-source .text, 697 | a.origin-source:link .text { 698 | line-height: 50px; 699 | height: 50px; 700 | font-size: 13px; 701 | } 702 | .origin-source.with-link .text { 703 | color: #333; 704 | } 705 | .origin-source.with-link:after { 706 | display: block; 707 | position: absolute; 708 | border-color: transparent transparent transparent #f0f0f0; 709 | border-width: 7px; 710 | border-style: solid; 711 | height: 0; 712 | width: 0; 713 | top: 18px; 714 | right: 4px; 715 | line-height: 0; 716 | content: ""; 717 | } 718 | .origin-source.with-link:before { 719 | display: block; 720 | height: 0; 721 | width: 0; 722 | position: absolute; 723 | top: 18px; 724 | right: 3px; 725 | border-color: transparent transparent transparent #000; 726 | border-width: 7px; 727 | border-style: solid; 728 | line-height: 0; 729 | content: ""; 730 | } 731 | .origin-source-wrap { 732 | position: relative; 733 | background: #f0f0f0; 734 | } 735 | .origin-source-wrap .focus-link { 736 | position: absolute; 737 | right: 0; 738 | top: 0; 739 | width: 45px; 740 | color: #00a2ed; 741 | height: 50px; 742 | display: none; 743 | text-align: center; 744 | font-size: 12px; 745 | -webkit-touch-callout: none; 746 | } 747 | .origin-source-wrap .focus-link .btn-label { 748 | text-align: center; 749 | display: block; 750 | margin-top: 8px; 751 | border-left: solid 1px #ccc; 752 | height: 34px; 753 | line-height: 34px; 754 | } 755 | .origin-source-wrap.unfocused .focus-link { 756 | display: block; 757 | } 758 | .origin-source-wrap.unfocused .origin-source:before, 759 | .origin-source-wrap.unfocused .origin-source:after { 760 | display: none; 761 | } 762 | .night .origin-source-wrap { 763 | background: #292929; 764 | } 765 | .night .origin-source-wrap .focus-link { 766 | color: #116f9e; 767 | } 768 | .night .origin-source-wrap .btn-label { 769 | border-left: solid 1px #3f3f3f; 770 | } 771 | .night .origin-source, 772 | .night .origin-source.with-link { 773 | background: #292929; 774 | color: #666; 775 | } 776 | .night .origin-source .text, 777 | .night .origin-source.with-link .text { 778 | color: #666; 779 | } 780 | .night .origin-source.with-link:after { 781 | border-color: transparent transparent transparent #292929; 782 | } 783 | .night .origin-source.with-link:before { 784 | border-color: transparent transparent transparent #666; 785 | } 786 | -------------------------------------------------------------------------------- /Library/src/main/java/com/linked/erfli/library/widget/MaterialProgressDrawable.java: -------------------------------------------------------------------------------- 1 | package com.linked.erfli.library.widget; 2 | 3 | import android.content.Context; 4 | import android.content.res.Resources; 5 | import android.graphics.Canvas; 6 | import android.graphics.Color; 7 | import android.graphics.ColorFilter; 8 | import android.graphics.Paint; 9 | import android.graphics.Paint.Style; 10 | import android.graphics.Path; 11 | import android.graphics.PixelFormat; 12 | import android.graphics.Rect; 13 | import android.graphics.RectF; 14 | import android.graphics.drawable.Animatable; 15 | import android.graphics.drawable.Drawable; 16 | import android.support.annotation.IntDef; 17 | import android.support.annotation.NonNull; 18 | import android.support.v4.view.animation.FastOutSlowInInterpolator; 19 | import android.util.DisplayMetrics; 20 | import android.view.View; 21 | import android.view.animation.Animation; 22 | import android.view.animation.Interpolator; 23 | import android.view.animation.LinearInterpolator; 24 | import android.view.animation.Transformation; 25 | 26 | import java.lang.annotation.Retention; 27 | import java.lang.annotation.RetentionPolicy; 28 | import java.util.ArrayList; 29 | 30 | /** 31 | * Fancy progress indicator for Material theme. 32 | * 33 | * @hide 34 | */ 35 | class MaterialProgressDrawable extends Drawable implements Animatable { 36 | private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); 37 | private static final Interpolator MATERIAL_INTERPOLATOR = new FastOutSlowInInterpolator(); 38 | 39 | private static final float FULL_ROTATION = 1080.0f; 40 | @Retention(RetentionPolicy.CLASS) 41 | @IntDef({LARGE, DEFAULT}) 42 | public @interface ProgressDrawableSize {} 43 | // Maps to ProgressBar.Large style 44 | static final int LARGE = 0; 45 | // Maps to ProgressBar default style 46 | static final int DEFAULT = 1; 47 | 48 | // Maps to ProgressBar default style 49 | private static final int CIRCLE_DIAMETER = 40; 50 | private static final float CENTER_RADIUS = 8.75f; //should add up to 10 when + stroke_width 51 | private static final float STROKE_WIDTH = 2.5f; 52 | 53 | // Maps to ProgressBar.Large style 54 | private static final int CIRCLE_DIAMETER_LARGE = 56; 55 | private static final float CENTER_RADIUS_LARGE = 12.5f; 56 | private static final float STROKE_WIDTH_LARGE = 3f; 57 | 58 | private final int[] COLORS = new int[] { 59 | Color.BLACK 60 | }; 61 | 62 | /** 63 | * The value in the linear interpolator for animating the drawable at which 64 | * the color transition should start 65 | */ 66 | private static final float COLOR_START_DELAY_OFFSET = 0.75f; 67 | private static final float END_TRIM_START_DELAY_OFFSET = 0.5f; 68 | private static final float START_TRIM_DURATION_OFFSET = 0.5f; 69 | 70 | /** The duration of a single progress spin in milliseconds. */ 71 | private static final int ANIMATION_DURATION = 1332; 72 | 73 | /** The number of points in the progress "star". */ 74 | private static final float NUM_POINTS = 5f; 75 | /** The list of animators operating on this drawable. */ 76 | private final ArrayList mAnimators = new ArrayList(); 77 | 78 | /** The indicator ring, used to manage animation state. */ 79 | private final Ring mRing; 80 | 81 | /** Canvas rotation in degrees. */ 82 | private float mRotation; 83 | 84 | /** Layout info for the arrowhead in dp */ 85 | private static final int ARROW_WIDTH = 10; 86 | private static final int ARROW_HEIGHT = 5; 87 | private static final float ARROW_OFFSET_ANGLE = 5; 88 | 89 | /** Layout info for the arrowhead for the large spinner in dp */ 90 | private static final int ARROW_WIDTH_LARGE = 12; 91 | private static final int ARROW_HEIGHT_LARGE = 6; 92 | private static final float MAX_PROGRESS_ARC = .8f; 93 | 94 | private Resources mResources; 95 | private View mParent; 96 | private Animation mAnimation; 97 | private float mRotationCount; 98 | private double mWidth; 99 | private double mHeight; 100 | boolean mFinishing; 101 | 102 | public MaterialProgressDrawable(Context context, View parent) { 103 | mParent = parent; 104 | mResources = context.getResources(); 105 | 106 | mRing = new Ring(mCallback); 107 | mRing.setColors(COLORS); 108 | 109 | updateSizes(DEFAULT); 110 | setupAnimators(); 111 | } 112 | 113 | private void setSizeParameters(double progressCircleWidth, double progressCircleHeight, 114 | double centerRadius, double strokeWidth, float arrowWidth, float arrowHeight) { 115 | final Ring ring = mRing; 116 | final DisplayMetrics metrics = mResources.getDisplayMetrics(); 117 | final float screenDensity = metrics.density; 118 | 119 | mWidth = progressCircleWidth * screenDensity; 120 | mHeight = progressCircleHeight * screenDensity; 121 | ring.setStrokeWidth((float) strokeWidth * screenDensity); 122 | ring.setCenterRadius(centerRadius * screenDensity); 123 | ring.setColorIndex(0); 124 | ring.setArrowDimensions(arrowWidth * screenDensity, arrowHeight * screenDensity); 125 | ring.setInsets((int) mWidth, (int) mHeight); 126 | } 127 | 128 | /** 129 | * Set the overall size for the progress spinner. This updates the radius 130 | * and stroke width of the ring. 131 | * 132 | * @param size One of {@link MaterialProgressDrawable.LARGE} or 133 | * {@link MaterialProgressDrawable.DEFAULT} 134 | */ 135 | public void updateSizes(@ProgressDrawableSize int size) { 136 | if (size == LARGE) { 137 | setSizeParameters(CIRCLE_DIAMETER_LARGE, CIRCLE_DIAMETER_LARGE, CENTER_RADIUS_LARGE, 138 | STROKE_WIDTH_LARGE, ARROW_WIDTH_LARGE, ARROW_HEIGHT_LARGE); 139 | } else { 140 | setSizeParameters(CIRCLE_DIAMETER, CIRCLE_DIAMETER, CENTER_RADIUS, STROKE_WIDTH, 141 | ARROW_WIDTH, ARROW_HEIGHT); 142 | } 143 | } 144 | 145 | /** 146 | * @param show Set to true to display the arrowhead on the progress spinner. 147 | */ 148 | public void showArrow(boolean show) { 149 | mRing.setShowArrow(show); 150 | } 151 | 152 | /** 153 | * @param scale Set the scale of the arrowhead for the spinner. 154 | */ 155 | public void setArrowScale(float scale) { 156 | mRing.setArrowScale(scale); 157 | } 158 | 159 | /** 160 | * Set the start and end trim for the progress spinner arc. 161 | * 162 | * @param startAngle start angle 163 | * @param endAngle end angle 164 | */ 165 | public void setStartEndTrim(float startAngle, float endAngle) { 166 | mRing.setStartTrim(startAngle); 167 | mRing.setEndTrim(endAngle); 168 | } 169 | 170 | /** 171 | * Set the amount of rotation to apply to the progress spinner. 172 | * 173 | * @param rotation Rotation is from [0..1] 174 | */ 175 | public void setProgressRotation(float rotation) { 176 | mRing.setRotation(rotation); 177 | } 178 | 179 | /** 180 | * Update the background color of the circle image view. 181 | */ 182 | public void setBackgroundColor(int color) { 183 | mRing.setBackgroundColor(color); 184 | } 185 | 186 | /** 187 | * Set the colors used in the progress animation from color resources. 188 | * The first color will also be the color of the bar that grows in response 189 | * to a user swipe gesture. 190 | * 191 | * @param colors 192 | */ 193 | public void setColorSchemeColors(int... colors) { 194 | mRing.setColors(colors); 195 | mRing.setColorIndex(0); 196 | } 197 | 198 | @Override 199 | public int getIntrinsicHeight() { 200 | return (int) mHeight; 201 | } 202 | 203 | @Override 204 | public int getIntrinsicWidth() { 205 | return (int) mWidth; 206 | } 207 | 208 | @Override 209 | public void draw(Canvas c) { 210 | final Rect bounds = getBounds(); 211 | final int saveCount = c.save(); 212 | c.rotate(mRotation, bounds.exactCenterX(), bounds.exactCenterY()); 213 | mRing.draw(c, bounds); 214 | c.restoreToCount(saveCount); 215 | } 216 | 217 | @Override 218 | public void setAlpha(int alpha) { 219 | mRing.setAlpha(alpha); 220 | } 221 | 222 | public int getAlpha() { 223 | return mRing.getAlpha(); 224 | } 225 | 226 | @Override 227 | public void setColorFilter(ColorFilter colorFilter) { 228 | mRing.setColorFilter(colorFilter); 229 | } 230 | 231 | @SuppressWarnings("unused") 232 | void setRotation(float rotation) { 233 | mRotation = rotation; 234 | invalidateSelf(); 235 | } 236 | 237 | @SuppressWarnings("unused") 238 | private float getRotation() { 239 | return mRotation; 240 | } 241 | 242 | @Override 243 | public int getOpacity() { 244 | return PixelFormat.TRANSLUCENT; 245 | } 246 | 247 | @Override 248 | public boolean isRunning() { 249 | final ArrayList animators = mAnimators; 250 | final int N = animators.size(); 251 | for (int i = 0; i < N; i++) { 252 | final Animation animator = animators.get(i); 253 | if (animator.hasStarted() && !animator.hasEnded()) { 254 | return true; 255 | } 256 | } 257 | return false; 258 | } 259 | 260 | @Override 261 | public void start() { 262 | mAnimation.reset(); 263 | mRing.storeOriginals(); 264 | // Already showing some part of the ring 265 | if (mRing.getEndTrim() != mRing.getStartTrim()) { 266 | mFinishing = true; 267 | mAnimation.setDuration(ANIMATION_DURATION/2); 268 | mParent.startAnimation(mAnimation); 269 | } else { 270 | mRing.setColorIndex(0); 271 | mRing.resetOriginals(); 272 | mAnimation.setDuration(ANIMATION_DURATION); 273 | mParent.startAnimation(mAnimation); 274 | } 275 | } 276 | 277 | @Override 278 | public void stop() { 279 | mParent.clearAnimation(); 280 | setRotation(0); 281 | mRing.setShowArrow(false); 282 | mRing.setColorIndex(0); 283 | mRing.resetOriginals(); 284 | } 285 | 286 | private float getMinProgressArc(Ring ring) { 287 | return (float) Math.toRadians( 288 | ring.getStrokeWidth() / (2 * Math.PI * ring.getCenterRadius())); 289 | } 290 | 291 | // Adapted from ArgbEvaluator.java 292 | private int evaluateColorChange(float fraction, int startValue, int endValue) { 293 | int startInt = (Integer) startValue; 294 | int startA = (startInt >> 24) & 0xff; 295 | int startR = (startInt >> 16) & 0xff; 296 | int startG = (startInt >> 8) & 0xff; 297 | int startB = startInt & 0xff; 298 | 299 | int endInt = (Integer) endValue; 300 | int endA = (endInt >> 24) & 0xff; 301 | int endR = (endInt >> 16) & 0xff; 302 | int endG = (endInt >> 8) & 0xff; 303 | int endB = endInt & 0xff; 304 | 305 | return (int)((startA + (int)(fraction * (endA - startA))) << 24) | 306 | (int)((startR + (int)(fraction * (endR - startR))) << 16) | 307 | (int)((startG + (int)(fraction * (endG - startG))) << 8) | 308 | (int)((startB + (int)(fraction * (endB - startB)))); 309 | } 310 | 311 | /** 312 | * Update the ring color if this is within the last 25% of the animation. 313 | * The new ring color will be a translation from the starting ring color to 314 | * the next color. 315 | */ 316 | private void updateRingColor(float interpolatedTime, Ring ring) { 317 | if (interpolatedTime > COLOR_START_DELAY_OFFSET) { 318 | // scale the interpolatedTime so that the full 319 | // transformation from 0 - 1 takes place in the 320 | // remaining time 321 | ring.setColor(evaluateColorChange((interpolatedTime - COLOR_START_DELAY_OFFSET) 322 | / (1.0f - COLOR_START_DELAY_OFFSET), ring.getStartingColor(), 323 | ring.getNextColor())); 324 | } 325 | } 326 | 327 | private void applyFinishTranslation(float interpolatedTime, Ring ring) { 328 | // shrink back down and complete a full rotation before 329 | // starting other circles 330 | // Rotation goes between [0..1]. 331 | updateRingColor(interpolatedTime, ring); 332 | float targetRotation = (float) (Math.floor(ring.getStartingRotation() / MAX_PROGRESS_ARC) 333 | + 1f); 334 | final float minProgressArc = getMinProgressArc(ring); 335 | final float startTrim = ring.getStartingStartTrim() 336 | + (ring.getStartingEndTrim() - minProgressArc - ring.getStartingStartTrim()) 337 | * interpolatedTime; 338 | ring.setStartTrim(startTrim); 339 | ring.setEndTrim(ring.getStartingEndTrim()); 340 | final float rotation = ring.getStartingRotation() 341 | + ((targetRotation - ring.getStartingRotation()) * interpolatedTime); 342 | ring.setRotation(rotation); 343 | } 344 | 345 | private void setupAnimators() { 346 | final Ring ring = mRing; 347 | final Animation animation = new Animation() { 348 | @Override 349 | public void applyTransformation(float interpolatedTime, Transformation t) { 350 | if (mFinishing) { 351 | applyFinishTranslation(interpolatedTime, ring); 352 | } else { 353 | // The minProgressArc is calculated from 0 to create an 354 | // angle that matches the stroke width. 355 | final float minProgressArc = getMinProgressArc(ring); 356 | final float startingEndTrim = ring.getStartingEndTrim(); 357 | final float startingTrim = ring.getStartingStartTrim(); 358 | final float startingRotation = ring.getStartingRotation(); 359 | 360 | updateRingColor(interpolatedTime, ring); 361 | 362 | // Moving the start trim only occurs in the first 50% of a 363 | // single ring animation 364 | if (interpolatedTime <= START_TRIM_DURATION_OFFSET) { 365 | // scale the interpolatedTime so that the full 366 | // transformation from 0 - 1 takes place in the 367 | // remaining time 368 | final float scaledTime = (interpolatedTime) 369 | / (1.0f - START_TRIM_DURATION_OFFSET); 370 | final float startTrim = startingTrim 371 | + ((MAX_PROGRESS_ARC - minProgressArc) * MATERIAL_INTERPOLATOR 372 | .getInterpolation(scaledTime)); 373 | ring.setStartTrim(startTrim); 374 | } 375 | 376 | // Moving the end trim starts after 50% of a single ring 377 | // animation completes 378 | if (interpolatedTime > END_TRIM_START_DELAY_OFFSET) { 379 | // scale the interpolatedTime so that the full 380 | // transformation from 0 - 1 takes place in the 381 | // remaining time 382 | final float minArc = MAX_PROGRESS_ARC - minProgressArc; 383 | float scaledTime = (interpolatedTime - START_TRIM_DURATION_OFFSET) 384 | / (1.0f - START_TRIM_DURATION_OFFSET); 385 | final float endTrim = startingEndTrim 386 | + (minArc * MATERIAL_INTERPOLATOR.getInterpolation(scaledTime)); 387 | ring.setEndTrim(endTrim); 388 | } 389 | 390 | final float rotation = startingRotation + (0.25f * interpolatedTime); 391 | ring.setRotation(rotation); 392 | 393 | float groupRotation = ((FULL_ROTATION / NUM_POINTS) * interpolatedTime) 394 | + (FULL_ROTATION * (mRotationCount / NUM_POINTS)); 395 | setRotation(groupRotation); 396 | } 397 | } 398 | }; 399 | animation.setRepeatCount(Animation.INFINITE); 400 | animation.setRepeatMode(Animation.RESTART); 401 | animation.setInterpolator(LINEAR_INTERPOLATOR); 402 | animation.setAnimationListener(new Animation.AnimationListener() { 403 | 404 | @Override 405 | public void onAnimationStart(Animation animation) { 406 | mRotationCount = 0; 407 | } 408 | 409 | @Override 410 | public void onAnimationEnd(Animation animation) { 411 | // do nothing 412 | } 413 | 414 | @Override 415 | public void onAnimationRepeat(Animation animation) { 416 | ring.storeOriginals(); 417 | ring.goToNextColor(); 418 | ring.setStartTrim(ring.getEndTrim()); 419 | if (mFinishing) { 420 | // finished closing the last ring from the swipe gesture; go 421 | // into progress mode 422 | mFinishing = false; 423 | animation.setDuration(ANIMATION_DURATION); 424 | ring.setShowArrow(false); 425 | } else { 426 | mRotationCount = (mRotationCount + 1) % (NUM_POINTS); 427 | } 428 | } 429 | }); 430 | mAnimation = animation; 431 | } 432 | 433 | private final Callback mCallback = new Callback() { 434 | @Override 435 | public void invalidateDrawable(Drawable d) { 436 | invalidateSelf(); 437 | } 438 | 439 | @Override 440 | public void scheduleDrawable(Drawable d, Runnable what, long when) { 441 | scheduleSelf(what, when); 442 | } 443 | 444 | @Override 445 | public void unscheduleDrawable(Drawable d, Runnable what) { 446 | unscheduleSelf(what); 447 | } 448 | }; 449 | 450 | private static class Ring { 451 | private final RectF mTempBounds = new RectF(); 452 | private final Paint mPaint = new Paint(); 453 | private final Paint mArrowPaint = new Paint(); 454 | 455 | private final Callback mCallback; 456 | 457 | private float mStartTrim = 0.0f; 458 | private float mEndTrim = 0.0f; 459 | private float mRotation = 0.0f; 460 | private float mStrokeWidth = 5.0f; 461 | private float mStrokeInset = 2.5f; 462 | 463 | private int[] mColors; 464 | // mColorIndex represents the offset into the available mColors that the 465 | // progress circle should currently display. As the progress circle is 466 | // animating, the mColorIndex moves by one to the next available color. 467 | private int mColorIndex; 468 | private float mStartingStartTrim; 469 | private float mStartingEndTrim; 470 | private float mStartingRotation; 471 | private boolean mShowArrow; 472 | private Path mArrow; 473 | private float mArrowScale; 474 | private double mRingCenterRadius; 475 | private int mArrowWidth; 476 | private int mArrowHeight; 477 | private int mAlpha; 478 | private final Paint mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); 479 | private int mBackgroundColor; 480 | private int mCurrentColor; 481 | 482 | public Ring(Callback callback) { 483 | mCallback = callback; 484 | 485 | mPaint.setStrokeCap(Paint.Cap.SQUARE); 486 | mPaint.setAntiAlias(true); 487 | mPaint.setStyle(Style.STROKE); 488 | 489 | mArrowPaint.setStyle(Paint.Style.FILL); 490 | mArrowPaint.setAntiAlias(true); 491 | } 492 | 493 | public void setBackgroundColor(int color) { 494 | mBackgroundColor = color; 495 | } 496 | 497 | /** 498 | * Set the dimensions of the arrowhead. 499 | * 500 | * @param width Width of the hypotenuse of the arrow head 501 | * @param height Height of the arrow point 502 | */ 503 | public void setArrowDimensions(float width, float height) { 504 | mArrowWidth = (int) width; 505 | mArrowHeight = (int) height; 506 | } 507 | 508 | /** 509 | * Draw the progress spinner 510 | */ 511 | public void draw(Canvas c, Rect bounds) { 512 | final RectF arcBounds = mTempBounds; 513 | arcBounds.set(bounds); 514 | arcBounds.inset(mStrokeInset, mStrokeInset); 515 | 516 | final float startAngle = (mStartTrim + mRotation) * 360; 517 | final float endAngle = (mEndTrim + mRotation) * 360; 518 | float sweepAngle = endAngle - startAngle; 519 | 520 | mPaint.setColor(mCurrentColor); 521 | c.drawArc(arcBounds, startAngle, sweepAngle, false, mPaint); 522 | 523 | drawTriangle(c, startAngle, sweepAngle, bounds); 524 | 525 | if (mAlpha < 255) { 526 | mCirclePaint.setColor(mBackgroundColor); 527 | mCirclePaint.setAlpha(255 - mAlpha); 528 | c.drawCircle(bounds.exactCenterX(), bounds.exactCenterY(), bounds.width() / 2, 529 | mCirclePaint); 530 | } 531 | } 532 | 533 | private void drawTriangle(Canvas c, float startAngle, float sweepAngle, Rect bounds) { 534 | if (mShowArrow) { 535 | if (mArrow == null) { 536 | mArrow = new android.graphics.Path(); 537 | mArrow.setFillType(android.graphics.Path.FillType.EVEN_ODD); 538 | } else { 539 | mArrow.reset(); 540 | } 541 | 542 | // Adjust the position of the triangle so that it is inset as 543 | // much as the arc, but also centered on the arc. 544 | float inset = (int) mStrokeInset / 2 * mArrowScale; 545 | float x = (float) (mRingCenterRadius * Math.cos(0) + bounds.exactCenterX()); 546 | float y = (float) (mRingCenterRadius * Math.sin(0) + bounds.exactCenterY()); 547 | 548 | // Update the path each time. This works around an issue in SKIA 549 | // where concatenating a rotation matrix to a scale matrix 550 | // ignored a starting negative rotation. This appears to have 551 | // been fixed as of API 21. 552 | mArrow.moveTo(0, 0); 553 | mArrow.lineTo(mArrowWidth * mArrowScale, 0); 554 | mArrow.lineTo((mArrowWidth * mArrowScale / 2), (mArrowHeight 555 | * mArrowScale)); 556 | mArrow.offset(x - inset, y); 557 | mArrow.close(); 558 | // draw a triangle 559 | mArrowPaint.setColor(mCurrentColor); 560 | c.rotate(startAngle + sweepAngle - ARROW_OFFSET_ANGLE, bounds.exactCenterX(), 561 | bounds.exactCenterY()); 562 | c.drawPath(mArrow, mArrowPaint); 563 | } 564 | } 565 | 566 | /** 567 | * Set the colors the progress spinner alternates between. 568 | * 569 | * @param colors Array of integers describing the colors. Must be non-null. 570 | */ 571 | public void setColors(@NonNull int[] colors) { 572 | mColors = colors; 573 | // if colors are reset, make sure to reset the color index as well 574 | setColorIndex(0); 575 | } 576 | 577 | /** 578 | * Set the absolute color of the progress spinner. This is should only 579 | * be used when animating between current and next color when the 580 | * spinner is rotating. 581 | * 582 | * @param color int describing the color. 583 | */ 584 | public void setColor(int color) { 585 | mCurrentColor = color; 586 | } 587 | 588 | /** 589 | * @param index Index into the color array of the color to display in 590 | * the progress spinner. 591 | */ 592 | public void setColorIndex(int index) { 593 | mColorIndex = index; 594 | mCurrentColor = mColors[mColorIndex]; 595 | } 596 | 597 | /** 598 | * @return int describing the next color the progress spinner should use when drawing. 599 | */ 600 | public int getNextColor() { 601 | return mColors[getNextColorIndex()]; 602 | } 603 | 604 | private int getNextColorIndex() { 605 | return (mColorIndex + 1) % (mColors.length); 606 | } 607 | 608 | /** 609 | * Proceed to the next available ring color. This will automatically 610 | * wrap back to the beginning of colors. 611 | */ 612 | public void goToNextColor() { 613 | setColorIndex(getNextColorIndex()); 614 | } 615 | 616 | public void setColorFilter(ColorFilter filter) { 617 | mPaint.setColorFilter(filter); 618 | invalidateSelf(); 619 | } 620 | 621 | /** 622 | * @param alpha Set the alpha of the progress spinner and associated arrowhead. 623 | */ 624 | public void setAlpha(int alpha) { 625 | mAlpha = alpha; 626 | } 627 | 628 | /** 629 | * @return Current alpha of the progress spinner and arrowhead. 630 | */ 631 | public int getAlpha() { 632 | return mAlpha; 633 | } 634 | 635 | /** 636 | * @param strokeWidth Set the stroke width of the progress spinner in pixels. 637 | */ 638 | public void setStrokeWidth(float strokeWidth) { 639 | mStrokeWidth = strokeWidth; 640 | mPaint.setStrokeWidth(strokeWidth); 641 | invalidateSelf(); 642 | } 643 | 644 | @SuppressWarnings("unused") 645 | public float getStrokeWidth() { 646 | return mStrokeWidth; 647 | } 648 | 649 | @SuppressWarnings("unused") 650 | public void setStartTrim(float startTrim) { 651 | mStartTrim = startTrim; 652 | invalidateSelf(); 653 | } 654 | 655 | @SuppressWarnings("unused") 656 | public float getStartTrim() { 657 | return mStartTrim; 658 | } 659 | 660 | public float getStartingStartTrim() { 661 | return mStartingStartTrim; 662 | } 663 | 664 | public float getStartingEndTrim() { 665 | return mStartingEndTrim; 666 | } 667 | 668 | public int getStartingColor() { 669 | return mColors[mColorIndex]; 670 | } 671 | 672 | @SuppressWarnings("unused") 673 | public void setEndTrim(float endTrim) { 674 | mEndTrim = endTrim; 675 | invalidateSelf(); 676 | } 677 | 678 | @SuppressWarnings("unused") 679 | public float getEndTrim() { 680 | return mEndTrim; 681 | } 682 | 683 | @SuppressWarnings("unused") 684 | public void setRotation(float rotation) { 685 | mRotation = rotation; 686 | invalidateSelf(); 687 | } 688 | 689 | @SuppressWarnings("unused") 690 | public float getRotation() { 691 | return mRotation; 692 | } 693 | 694 | public void setInsets(int width, int height) { 695 | final float minEdge = (float) Math.min(width, height); 696 | float insets; 697 | if (mRingCenterRadius <= 0 || minEdge < 0) { 698 | insets = (float) Math.ceil(mStrokeWidth / 2.0f); 699 | } else { 700 | insets = (float) (minEdge / 2.0f - mRingCenterRadius); 701 | } 702 | mStrokeInset = insets; 703 | } 704 | 705 | @SuppressWarnings("unused") 706 | public float getInsets() { 707 | return mStrokeInset; 708 | } 709 | 710 | /** 711 | * @param centerRadius Inner radius in px of the circle the progress 712 | * spinner arc traces. 713 | */ 714 | public void setCenterRadius(double centerRadius) { 715 | mRingCenterRadius = centerRadius; 716 | } 717 | 718 | public double getCenterRadius() { 719 | return mRingCenterRadius; 720 | } 721 | 722 | /** 723 | * @param show Set to true to show the arrow head on the progress spinner. 724 | */ 725 | public void setShowArrow(boolean show) { 726 | if (mShowArrow != show) { 727 | mShowArrow = show; 728 | invalidateSelf(); 729 | } 730 | } 731 | 732 | /** 733 | * @param scale Set the scale of the arrowhead for the spinner. 734 | */ 735 | public void setArrowScale(float scale) { 736 | if (scale != mArrowScale) { 737 | mArrowScale = scale; 738 | invalidateSelf(); 739 | } 740 | } 741 | 742 | /** 743 | * @return The amount the progress spinner is currently rotated, between [0..1]. 744 | */ 745 | public float getStartingRotation() { 746 | return mStartingRotation; 747 | } 748 | 749 | /** 750 | * If the start / end trim are offset to begin with, store them so that 751 | * animation starts from that offset. 752 | */ 753 | public void storeOriginals() { 754 | mStartingStartTrim = mStartTrim; 755 | mStartingEndTrim = mEndTrim; 756 | mStartingRotation = mRotation; 757 | } 758 | 759 | /** 760 | * Reset the progress spinner to default rotation, start and end angles. 761 | */ 762 | public void resetOriginals() { 763 | mStartingStartTrim = 0; 764 | mStartingEndTrim = 0; 765 | mStartingRotation = 0; 766 | setStartTrim(0); 767 | setEndTrim(0); 768 | setRotation(0); 769 | } 770 | 771 | private void invalidateSelf() { 772 | mCallback.invalidateDrawable(null); 773 | } 774 | } 775 | } 776 | --------------------------------------------------------------------------------