├── .gitignore ├── .idea ├── .name ├── checkstyle-idea.xml ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── findbugs-idea.xml ├── gradle.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── xlf │ │ └── nrl │ │ └── demo │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── xlf │ │ │ └── nrl │ │ │ └── demo │ │ │ ├── MainActivity.java │ │ │ └── TestRecyclerAdapter.java │ └── res │ │ ├── layout │ │ ├── activity_main.xml │ │ └── item_test.xml │ │ ├── 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-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── xlf │ └── nrl │ └── demo │ └── ExampleUnitTest.java ├── art └── demo.gif ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── nrl ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── xlf │ │ └── nrl │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── xlf │ │ │ └── nrl │ │ │ ├── CircleProgressBar.java │ │ │ ├── LoadView.java │ │ │ ├── MaterialProgressDrawable.java │ │ │ ├── NrlUtils.java │ │ │ ├── NsRefreshLayout.java │ │ │ └── SimpleAnimatorListener.java │ └── res │ │ └── values │ │ ├── attrs.xml │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── xlf │ └── nrl │ └── ExampleUnitTest.java ├── principles.md └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | NsRefreshLayout -------------------------------------------------------------------------------- /.idea/checkstyle-idea.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/findbugs-idea.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 32 | 203 | 216 | 225 | 226 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NsRefreshLayout 2 | 支持任意View的下拉刷新控件,同时支持上拉加载更多。实现原理:http://blog.csdn.net/xiaomoit/article/details/50469810 3 | 4 | ## 效果预览 5 | 6 | ![demo](https://github.com/xiaolifan/NsRefreshLayout/blob/master/art/demo.gif?raw=true) 7 | 8 | ## 属性说明 9 | 10 | ```xml 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ``` 32 | 33 | ## 举例 34 | 35 | ```xml 36 | 37 | 46 | 47 | 53 | 54 | ``` 55 | 56 | ```java 57 | package com.xlf.nrl.demo; 58 | 59 | import android.os.Bundle; 60 | import android.support.v7.app.AppCompatActivity; 61 | import android.support.v7.widget.RecyclerView; 62 | import android.view.Menu; 63 | import android.view.MenuItem; 64 | 65 | import com.xlf.nrl.NsRefreshLayout; 66 | 67 | public class MainActivity extends AppCompatActivity implements 68 | NsRefreshLayout.NsRefreshLayoutController, NsRefreshLayout.NsRefreshLayoutListener { 69 | 70 | private boolean loadMoreEnable = true; 71 | private NsRefreshLayout refreshLayout; 72 | private RecyclerView rvTest; 73 | 74 | @Override 75 | protected void onCreate(Bundle savedInstanceState) { 76 | super.onCreate(savedInstanceState); 77 | setContentView(R.layout.activity_main); 78 | refreshLayout = (NsRefreshLayout) findViewById(R.id.nrl_test); 79 | refreshLayout.setRefreshLayoutController(this); 80 | refreshLayout.setRefreshLayoutListener(this); 81 | 82 | rvTest = (RecyclerView) findViewById(R.id.rv_test); 83 | TestRecyclerAdapter adapter = new TestRecyclerAdapter(this); 84 | rvTest.setAdapter(adapter); 85 | } 86 | 87 | @Override 88 | public boolean onCreateOptionsMenu(Menu menu) { 89 | MenuItem item = menu.add(0, 0, 0, "禁用上拉加载功能"); 90 | item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); 91 | return true; 92 | } 93 | 94 | @Override 95 | public boolean onOptionsItemSelected(MenuItem item) { 96 | loadMoreEnable = false; 97 | return true; 98 | } 99 | 100 | @Override 101 | public boolean isPullRefreshEnable() { 102 | return true; 103 | } 104 | 105 | @Override 106 | public boolean isPullLoadEnable() { 107 | return loadMoreEnable; 108 | } 109 | 110 | @Override 111 | public void onRefresh() { 112 | refreshLayout.postDelayed(new Runnable() { 113 | @Override 114 | public void run() { 115 | refreshLayout.finishPullRefresh(); 116 | } 117 | }, 1000); 118 | } 119 | 120 | @Override 121 | public void onLoadMore() { 122 | refreshLayout.postDelayed(new Runnable() { 123 | @Override 124 | public void run() { 125 | refreshLayout.finishPullLoad(); 126 | } 127 | }, 1000); 128 | } 129 | 130 | } 131 | ``` 132 | 133 | ## License 134 | ------- 135 | 136 | Mozilla Public License, version 2.0 137 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.2" 6 | 7 | defaultConfig { 8 | applicationId "com.xlf.nrl.demo" 9 | minSdkVersion 14 10 | targetSdkVersion 23 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(include: ['*.jar'], dir: 'libs') 24 | testCompile 'junit:junit:4.12' 25 | compile 'com.android.support:appcompat-v7:23.1.1' 26 | compile project(':nrl') 27 | compile 'com.android.support:recyclerview-v7:23.1.1' 28 | } 29 | -------------------------------------------------------------------------------- /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 D:\ADT\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/src/androidTest/java/com/xlf/nrl/demo/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.xlf.nrl.demo; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/xlf/nrl/demo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.xlf.nrl.demo; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.view.Menu; 7 | import android.view.MenuItem; 8 | 9 | import com.xlf.nrl.NsRefreshLayout; 10 | 11 | public class MainActivity extends AppCompatActivity implements 12 | NsRefreshLayout.NsRefreshLayoutController, NsRefreshLayout.NsRefreshLayoutListener { 13 | 14 | private boolean loadMoreEnable = true; 15 | private NsRefreshLayout refreshLayout; 16 | private RecyclerView rvTest; 17 | 18 | @Override 19 | protected void onCreate(Bundle savedInstanceState) { 20 | super.onCreate(savedInstanceState); 21 | setContentView(R.layout.activity_main); 22 | refreshLayout = (NsRefreshLayout) findViewById(R.id.nrl_test); 23 | refreshLayout.setRefreshLayoutController(this); 24 | refreshLayout.setRefreshLayoutListener(this); 25 | 26 | rvTest = (RecyclerView) findViewById(R.id.rv_test); 27 | TestRecyclerAdapter adapter = new TestRecyclerAdapter(this); 28 | rvTest.setAdapter(adapter); 29 | } 30 | 31 | @Override 32 | public boolean onCreateOptionsMenu(Menu menu) { 33 | MenuItem item = menu.add(0, 0, 0, "禁用上拉加载功能"); 34 | item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); 35 | return true; 36 | } 37 | 38 | @Override 39 | public boolean onOptionsItemSelected(MenuItem item) { 40 | loadMoreEnable = false; 41 | return true; 42 | } 43 | 44 | @Override 45 | public boolean isPullRefreshEnable() { 46 | return true; 47 | } 48 | 49 | @Override 50 | public boolean isPullLoadEnable() { 51 | return loadMoreEnable; 52 | } 53 | 54 | @Override 55 | public void onRefresh() { 56 | refreshLayout.postDelayed(new Runnable() { 57 | @Override 58 | public void run() { 59 | refreshLayout.finishPullRefresh(); 60 | } 61 | }, 1000); 62 | } 63 | 64 | @Override 65 | public void onLoadMore() { 66 | refreshLayout.postDelayed(new Runnable() { 67 | @Override 68 | public void run() { 69 | refreshLayout.finishPullLoad(); 70 | } 71 | }, 1000); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/java/com/xlf/nrl/demo/TestRecyclerAdapter.java: -------------------------------------------------------------------------------- 1 | package com.xlf.nrl.demo; 2 | 3 | import android.content.Context; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.TextView; 9 | import android.widget.Toast; 10 | 11 | /** 12 | * Created by xiaolifan on 2015/12/25. 13 | * QQ: 1147904198 14 | * Email: xiao_lifan@163.com 15 | */ 16 | public class TestRecyclerAdapter extends RecyclerView.Adapter { 17 | 18 | private Context context; 19 | 20 | public TestRecyclerAdapter(Context context) { 21 | this.context = context; 22 | } 23 | 24 | @Override 25 | public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 26 | return new TestViewHolder(LayoutInflater.from(context).inflate(R.layout.item_test, 27 | parent, false)); 28 | } 29 | 30 | @Override 31 | public void onBindViewHolder(TestViewHolder holder, final int position) { 32 | holder.textView.setText("Item Data " + position); 33 | holder.textView.setOnClickListener(new View.OnClickListener() { 34 | @Override 35 | public void onClick(View v) { 36 | Toast.makeText(context, "You have click position: " + position, Toast.LENGTH_SHORT) 37 | .show(); 38 | } 39 | }); 40 | } 41 | 42 | @Override 43 | public int getItemCount() { 44 | return 100; 45 | } 46 | 47 | static class TestViewHolder extends RecyclerView.ViewHolder { 48 | 49 | TextView textView; 50 | 51 | public TestViewHolder(View itemView) { 52 | super(itemView); 53 | textView = (TextView) itemView; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_test.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laomengzhu/NsRefreshLayout/196a8d597c7ad70ef3adf3ddce1f2a213aad3033/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laomengzhu/NsRefreshLayout/196a8d597c7ad70ef3adf3ddce1f2a213aad3033/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laomengzhu/NsRefreshLayout/196a8d597c7ad70ef3adf3ddce1f2a213aad3033/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laomengzhu/NsRefreshLayout/196a8d597c7ad70ef3adf3ddce1f2a213aad3033/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laomengzhu/NsRefreshLayout/196a8d597c7ad70ef3adf3ddce1f2a213aad3033/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/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 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | NsRefreshLayout 3 | 4 | 成长经历 5 | 6 | Stack Overflow 由 Jeff Atwood 和 Joel Spolsky 这两个非常著名的 Blogger 在 2008 年创建,7月小范围的进行 Beta 测试,直到 9 月份才开始公开的 Beta 测试。Stack Ov 7 | erflow 面向编程人员群体。到2010年年末,Stack Overflow 单个站点在 Alexa 的Rank 是 160 ,月度独立访客超过 1600 万,每月Page View 超过 7200 万 (refer)。Stack Exchange Network 在 2010 年 5 月接受了来自 Union Square Ventures 的 600 万美元的投资,在 2010 年扩大并完善了整个团队,从三个全职工程师发展到了 20 多人的队伍,搬进了 7500 平方英尺的豪华装修的办公室(当然每个人都坐着1000美元一把的椅子)。 8 | 成功原因 9 | 10 | 技术和社区基因 11 | 当然可以说是众包(Crowdsourcing)的功劳,但哪一个成功的社区能少了众包的功劳呢?如果实际一点说,不可或缺的因素我想是两个创始人的技术和社区基因。作为两个著名的 Blogger,没有人会质疑 Joel 和 Jeff 在 目标用户(开发人员)需求的精准把握。何况在上线前后,Jeff 通过技术社群又进行了大量的调研和反馈(Joel 倒是似乎第一次做 Web 项目,Fog Creek 主要是软件开发)。此前市场上已经有Experts-Exchange之类的老牌产品,Stack Overflow 则反其道而行之(Anti-Experts-Exchange),作为技术人员,你一定遇到过搜索技术问题到了 Experts-Exchange 网站,但是你发现问题下面并没有合适的解答,仅仅有人提问,但是没有有效的激励回答者则是没有价值的。Stack Overflow 参考 Reddit 等网站的用户激励机制,关注问题质量,其做法是通过威望值(Reputation Point) 与徽章(Badge) 建立起信任评价体系,并且做到对参与者的有效激励。我是否说过技术人员都是"好面子"的?没有,那么现在记住这句话吧。 12 | 秉承独特的设计理念 13 | Stack Overflow 绝对没有多余的或是跟风的功能(比如一些不必要的 Social Network 特性)。如果看过 Joel 的书或是订阅他的 Blog,你应该知道他是个相当偏执的家伙,尤其是在产品设计方面,他认为对的事情绝不会妥协,参见他在《软件随想录》中的《别给用户太多选择》以及《用软件搭建社区》等章节。我不知道究竟团队在功能设计上是怎么分工的,但 Joel 一定是毫无质疑的植入自己的设计理念。另外要补充的是,Stack Overflow 重新将"标签"化腐朽为神奇,也是相当值得称道的。 14 | 横向的业务扩展模式 15 | Stack Exchange network 采取攻其一点,再进攻其余的方式。在面向开发人员的 Stack Overflow 获得验证并且成功之后,向类似话题领域扩展;然后与不同团队进行合作,逐渐引入更多的主题(比如 Ubuntu、面向物理学的话题等等)。最后,如果把几十个话题合起来,恰好是一个庞大的 -- 论坛。Stack Overflow是否重新"改造"了论坛这个古老的交流模式? 16 | 技术?是个关键因素,但不是主要因素 17 | 作为 Startup,罕见的使用了微软的技术体系进行开发,但也用开源软件。观察 Stack Overflow 所用的技术方案,会觉得是个大杂烩,除了 C# 、ASP 、SQL Server 等,也有 HAproxy、Redis 这些解决方案。 据 Joel 说,效率和成本也还不错。扩展模式上则首选 "Scale Up", 总之,就是有点特别。但是,用户体验相当好,这个是最难模仿的一个地方(另一个是运营套路)。 18 | 缩写 19 | 20 | S.O. (Stack Overflow),此网站浏览者常用的对自己网站的称呼。 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/test/java/com/xlf/nrl/demo/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.xlf.nrl.demo; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * To work on unit tests, switch the Test Artifact in the Build Variants view. 9 | */ 10 | public class ExampleUnitTest { 11 | @Test 12 | public void addition_isCorrect() throws Exception { 13 | assertEquals(4, 2 + 2); 14 | } 15 | } -------------------------------------------------------------------------------- /art/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laomengzhu/NsRefreshLayout/196a8d597c7ad70ef3adf3ddce1f2a213aad3033/art/demo.gif -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:1.5.0' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | 21 | task clean(type: Delete) { 22 | delete rootProject.buildDir 23 | } 24 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laomengzhu/NsRefreshLayout/196a8d597c7ad70ef3adf3ddce1f2a213aad3033/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Oct 21 11:34:03 PDT 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.8-all.zip 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /nrl/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /nrl/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.2" 6 | 7 | defaultConfig { 8 | minSdkVersion 14 9 | targetSdkVersion 23 10 | versionCode 1 11 | versionName "1.0" 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 | testCompile 'junit:junit:4.12' 24 | compile 'com.android.support:appcompat-v7:23.1.1' 25 | } 26 | -------------------------------------------------------------------------------- /nrl/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 D:\ADT\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 | -------------------------------------------------------------------------------- /nrl/src/androidTest/java/com/xlf/nrl/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.xlf.nrl; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /nrl/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /nrl/src/main/java/com/xlf/nrl/CircleProgressBar.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 The Android Open Source Project 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 | package com.xlf.nrl; 18 | 19 | import android.content.Context; 20 | import android.content.res.Resources; 21 | import android.content.res.TypedArray; 22 | import android.graphics.Canvas; 23 | import android.graphics.Color; 24 | import android.graphics.Paint; 25 | import android.graphics.RadialGradient; 26 | import android.graphics.Shader; 27 | import android.graphics.drawable.Drawable; 28 | import android.graphics.drawable.ShapeDrawable; 29 | import android.graphics.drawable.shapes.OvalShape; 30 | import android.net.Uri; 31 | import android.support.v4.view.ViewCompat; 32 | import android.util.AttributeSet; 33 | import android.util.Log; 34 | import android.view.animation.Animation; 35 | import android.widget.ImageView; 36 | 37 | /** 38 | * The author is lsjwzh(MaterialLoadingProgressBar) link{https://github.com/lsjwzh/MaterialLoadingProgressBar}, 39 | * I did some slight modifications 40 | */ 41 | public class CircleProgressBar extends ImageView { 42 | 43 | private static final int KEY_SHADOW_COLOR = 0x1E000000; 44 | private static final int FILL_SHADOW_COLOR = 0x3D000000; 45 | // PX 46 | private static final float X_OFFSET = 0f; 47 | private static final float Y_OFFSET = 1.75f; 48 | private static final float SHADOW_RADIUS = 3.5f; 49 | private static final int SHADOW_ELEVATION = 4; 50 | 51 | 52 | public static final int DEFAULT_CIRCLE_BG_LIGHT = 0xFFFAFAFA; 53 | public static final int DEFAULT_CIRCLE_COLOR = 0xFFF00000; 54 | private static final int DEFAULT_CIRCLE_DIAMETER = 40; 55 | private static final int STROKE_WIDTH_LARGE = 3; 56 | public static final int DEFAULT_TEXT_SIZE = 9; 57 | 58 | private Animation.AnimationListener mListener; 59 | private int mShadowRadius; 60 | private int mBackGroundColor; 61 | private int mProgressColor; 62 | private int mProgressStokeWidth; 63 | private int mArrowWidth; 64 | private int mArrowHeight; 65 | private int mProgress; 66 | private int mMax; 67 | private int mDiameter; 68 | private int mInnerRadius; 69 | private Paint mTextPaint; 70 | private int mTextColor; 71 | private int mTextSize; 72 | private boolean mIfDrawText; 73 | private boolean mShowArrow; 74 | public MaterialProgressDrawable mProgressDrawable; 75 | private ShapeDrawable mBgCircle; 76 | private boolean mCircleBackgroundEnabled; 77 | private int[] mColors = new int[]{Color.BLACK}; 78 | 79 | public CircleProgressBar(Context context) { 80 | super(context); 81 | init(context, null, 0); 82 | 83 | } 84 | 85 | public CircleProgressBar(Context context, AttributeSet attrs) { 86 | super(context, attrs); 87 | init(context, attrs, 0); 88 | 89 | } 90 | 91 | public CircleProgressBar(Context context, AttributeSet attrs, int defStyleAttr) { 92 | super(context, attrs, defStyleAttr); 93 | init(context, attrs, defStyleAttr); 94 | } 95 | 96 | 97 | private void init(Context context, AttributeSet attrs, int defStyleAttr) { 98 | final TypedArray a = context.obtainStyledAttributes( 99 | attrs, R.styleable.CircleProgressBar, defStyleAttr, 0); 100 | 101 | final float density = getContext().getResources().getDisplayMetrics().density; 102 | 103 | mBackGroundColor = a.getColor( 104 | R.styleable.CircleProgressBar_background_color, DEFAULT_CIRCLE_BG_LIGHT); 105 | 106 | mProgressColor = a.getColor( 107 | R.styleable.CircleProgressBar_progress_color, DEFAULT_CIRCLE_COLOR); 108 | mColors = new int[]{mProgressColor}; 109 | 110 | mInnerRadius = a.getDimensionPixelOffset( 111 | R.styleable.CircleProgressBar_inner_radius, -1); 112 | 113 | mProgressStokeWidth = a.getDimensionPixelOffset( 114 | R.styleable.CircleProgressBar_progress_stoke_width, (int) (STROKE_WIDTH_LARGE * density)); 115 | mArrowWidth = a.getDimensionPixelOffset( 116 | R.styleable.CircleProgressBar_arrow_width, -1); 117 | mArrowHeight = a.getDimensionPixelOffset( 118 | R.styleable.CircleProgressBar_arrow_height, -1); 119 | mTextSize = a.getDimensionPixelOffset( 120 | R.styleable.CircleProgressBar_progress_text_size, (int) (DEFAULT_TEXT_SIZE * density)); 121 | mTextColor = a.getColor( 122 | R.styleable.CircleProgressBar_progress_text_color, Color.BLACK); 123 | 124 | mShowArrow = a.getBoolean(R.styleable.CircleProgressBar_show_arrow, true); 125 | mCircleBackgroundEnabled = a.getBoolean(R.styleable.CircleProgressBar_enable_circle_background, true); 126 | 127 | 128 | mProgress = a.getInt(R.styleable.CircleProgressBar_progress, 0); 129 | mMax = a.getInt(R.styleable.CircleProgressBar_max, 100); 130 | int textVisible = a.getInt(R.styleable.CircleProgressBar_progress_text_visibility, 1); 131 | if (textVisible != 1) { 132 | mIfDrawText = true; 133 | } 134 | 135 | mTextPaint = new Paint(); 136 | mTextPaint.setStyle(Paint.Style.FILL); 137 | mTextPaint.setColor(mTextColor); 138 | mTextPaint.setTextSize(mTextSize); 139 | mTextPaint.setAntiAlias(true); 140 | a.recycle(); 141 | mProgressDrawable = new MaterialProgressDrawable(getContext(), this); 142 | super.setImageDrawable(mProgressDrawable); 143 | } 144 | 145 | public void setProgressBackGroundColor(int color) { 146 | this.mBackGroundColor = color; 147 | } 148 | 149 | public void setTextColor(int color) { 150 | this.mTextColor = color; 151 | } 152 | 153 | private boolean elevationSupported() { 154 | return android.os.Build.VERSION.SDK_INT >= 21; 155 | } 156 | 157 | @Override 158 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 159 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 160 | if (!elevationSupported()) { 161 | setMeasuredDimension(getMeasuredWidth() + mShadowRadius * 2, getMeasuredHeight() 162 | + mShadowRadius * 2); 163 | } 164 | } 165 | 166 | public int getProgressStokeWidth() { 167 | return mProgressStokeWidth; 168 | } 169 | 170 | public void setProgressStokeWidth(int mProgressStokeWidth) { 171 | final float density = getContext().getResources().getDisplayMetrics().density; 172 | this.mProgressStokeWidth = (int) (mProgressStokeWidth * density); 173 | } 174 | 175 | @Override 176 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 177 | super.onLayout(changed, left, top, right, bottom); 178 | final float density = getContext().getResources().getDisplayMetrics().density; 179 | mDiameter = Math.min(getMeasuredWidth(), getMeasuredHeight()); 180 | if (mDiameter <= 0) { 181 | mDiameter = (int) density * DEFAULT_CIRCLE_DIAMETER; 182 | } 183 | if (getBackground() == null && mCircleBackgroundEnabled) { 184 | final int shadowYOffset = (int) (density * Y_OFFSET); 185 | final int shadowXOffset = (int) (density * X_OFFSET); 186 | mShadowRadius = (int) (density * SHADOW_RADIUS); 187 | 188 | if (elevationSupported()) { 189 | mBgCircle = new ShapeDrawable(new OvalShape()); 190 | ViewCompat.setElevation(this, SHADOW_ELEVATION * density); 191 | } else { 192 | OvalShape oval = new OvalShadow(mShadowRadius, mDiameter - mShadowRadius * 2); 193 | mBgCircle = new ShapeDrawable(oval); 194 | ViewCompat.setLayerType(this, ViewCompat.LAYER_TYPE_SOFTWARE, mBgCircle.getPaint()); 195 | mBgCircle.getPaint().setShadowLayer(mShadowRadius, shadowXOffset, shadowYOffset, 196 | KEY_SHADOW_COLOR); 197 | final int padding = (int) mShadowRadius; 198 | // set padding so the inner image sits correctly within the shadow. 199 | setPadding(padding, padding, padding, padding); 200 | } 201 | mBgCircle.getPaint().setColor(mBackGroundColor); 202 | setBackgroundDrawable(mBgCircle); 203 | } 204 | mProgressDrawable.setBackgroundColor(mBackGroundColor); 205 | mProgressDrawable.setColorSchemeColors(mColors); 206 | mProgressDrawable.setSizeParameters(mDiameter, mDiameter, 207 | mInnerRadius <= 0 ? (mDiameter - mProgressStokeWidth * 2) / 4 : mInnerRadius, 208 | mProgressStokeWidth, 209 | mArrowWidth < 0 ? mProgressStokeWidth * 4 : mArrowWidth, 210 | mArrowHeight < 0 ? mProgressStokeWidth * 2 : mArrowHeight); 211 | if (isShowArrow()) { 212 | mProgressDrawable.showArrowOnFirstStart(true); 213 | mProgressDrawable.setArrowScale(1f); 214 | mProgressDrawable.showArrow(true); 215 | } 216 | super.setImageDrawable(null); 217 | super.setImageDrawable(mProgressDrawable); 218 | mProgressDrawable.setAlpha(255); 219 | if (getVisibility() == VISIBLE) { 220 | mProgressDrawable.setStartEndTrim(0, (float) 0.8); 221 | } 222 | } 223 | 224 | @Override 225 | protected void onDraw(Canvas canvas) { 226 | super.onDraw(canvas); 227 | if (mIfDrawText) { 228 | String text = String.format("%s%%", mProgress); 229 | int x = getWidth() / 2 - text.length() * mTextSize / 4; 230 | int y = getHeight() / 2 + mTextSize / 4; 231 | canvas.drawText(text, x, y, mTextPaint); 232 | } 233 | } 234 | 235 | @Override 236 | final public void setImageResource(int resId) { 237 | 238 | } 239 | 240 | 241 | public boolean isShowArrow() { 242 | return mShowArrow; 243 | } 244 | 245 | public void setShowArrow(boolean showArrow) { 246 | this.mShowArrow = showArrow; 247 | } 248 | 249 | 250 | @Override 251 | final public void setImageURI(Uri uri) { 252 | super.setImageURI(uri); 253 | } 254 | 255 | @Override 256 | final public void setImageDrawable(Drawable drawable) { 257 | } 258 | 259 | public void setAnimationListener(Animation.AnimationListener listener) { 260 | mListener = listener; 261 | } 262 | 263 | @Override 264 | public void onAnimationStart() { 265 | super.onAnimationStart(); 266 | if (mListener != null) { 267 | mListener.onAnimationStart(getAnimation()); 268 | } 269 | } 270 | 271 | @Override 272 | public void onAnimationEnd() { 273 | super.onAnimationEnd(); 274 | if (mListener != null) { 275 | mListener.onAnimationEnd(getAnimation()); 276 | } 277 | } 278 | 279 | 280 | /** 281 | * Set the color resources used in the progress animation from color resources. 282 | * The first color will also be the color of the bar that grows in response 283 | * to a user swipe gesture. 284 | * 285 | * @param colorResIds 286 | */ 287 | public void setColorSchemeResources(int... colorResIds) { 288 | final Resources res = getResources(); 289 | int[] colorRes = new int[colorResIds.length]; 290 | for (int i = 0; i < colorResIds.length; i++) { 291 | colorRes[i] = res.getColor(colorResIds[i]); 292 | } 293 | setColorSchemeColors(colorRes); 294 | } 295 | 296 | /** 297 | * Set the colors used in the progress animation. The first 298 | * color will also be the color of the bar that grows in response to a user 299 | * swipe gesture. 300 | * 301 | * @param colors 302 | */ 303 | public void setColorSchemeColors(int... colors) { 304 | mColors = colors; 305 | if (mProgressDrawable != null) { 306 | mProgressDrawable.setColorSchemeColors(colors); 307 | } 308 | } 309 | 310 | /** 311 | * Update the background color of the mBgCircle image view. 312 | */ 313 | public void setBackgroundColorResource(int colorRes) { 314 | if (getBackground() instanceof ShapeDrawable) { 315 | final Resources res = getResources(); 316 | ((ShapeDrawable) getBackground()).getPaint().setColor(res.getColor(colorRes)); 317 | } 318 | } 319 | 320 | public void setBackgroundColor(int color) { 321 | if (getBackground() instanceof ShapeDrawable) { 322 | final Resources res = getResources(); 323 | ((ShapeDrawable) getBackground()).getPaint().setColor(color); 324 | } 325 | } 326 | 327 | public boolean isShowProgressText() { 328 | return mIfDrawText; 329 | } 330 | 331 | public void setShowProgressText(boolean mIfDrawText) { 332 | this.mIfDrawText = mIfDrawText; 333 | } 334 | 335 | public int getMax() { 336 | return mMax; 337 | } 338 | 339 | public void setMax(int max) { 340 | mMax = max; 341 | } 342 | 343 | public int getProgress() { 344 | return mProgress; 345 | } 346 | 347 | public void setProgress(int progress) { 348 | if (getMax() > 0) { 349 | mProgress = progress; 350 | } 351 | invalidate(); 352 | Log.i("cjj_log", "progress------->>>>" + progress); 353 | } 354 | 355 | 356 | public boolean circleBackgroundEnabled() { 357 | return mCircleBackgroundEnabled; 358 | } 359 | 360 | public void setCircleBackgroundEnabled(boolean enableCircleBackground) { 361 | this.mCircleBackgroundEnabled = enableCircleBackground; 362 | } 363 | 364 | @Override 365 | public int getVisibility() { 366 | return super.getVisibility(); 367 | } 368 | 369 | @Override 370 | public void setVisibility(int visibility) { 371 | super.setVisibility(visibility); 372 | // if (mProgressDrawable != null) { 373 | // mProgressDrawable.setVisible(visibility == VISIBLE, false); 374 | // if (visibility != VISIBLE) { 375 | // mProgressDrawable.stop(); 376 | // } else { 377 | // if (mProgressDrawable.isRunning()) { 378 | // mProgressDrawable.stop(); 379 | // } 380 | // mProgressDrawable.start(); 381 | // } 382 | // } 383 | } 384 | 385 | @Override 386 | protected void onAttachedToWindow() { 387 | super.onAttachedToWindow(); 388 | if (mProgressDrawable != null) { 389 | mProgressDrawable.stop(); 390 | mProgressDrawable.setVisible(getVisibility() == VISIBLE, false); 391 | } 392 | } 393 | 394 | @Override 395 | protected void onDetachedFromWindow() { 396 | super.onDetachedFromWindow(); 397 | if (mProgressDrawable != null) { 398 | mProgressDrawable.stop(); 399 | mProgressDrawable.setVisible(false, false); 400 | } 401 | } 402 | 403 | private class OvalShadow extends OvalShape { 404 | private RadialGradient mRadialGradient; 405 | private int mShadowRadius; 406 | private Paint mShadowPaint; 407 | private int mCircleDiameter; 408 | 409 | public OvalShadow(int shadowRadius, int circleDiameter) { 410 | super(); 411 | mShadowPaint = new Paint(); 412 | mShadowRadius = shadowRadius; 413 | mCircleDiameter = circleDiameter; 414 | mRadialGradient = new RadialGradient(mCircleDiameter / 2, mCircleDiameter / 2, 415 | mShadowRadius, new int[]{ 416 | FILL_SHADOW_COLOR, Color.TRANSPARENT 417 | }, null, Shader.TileMode.CLAMP); 418 | mShadowPaint.setShader(mRadialGradient); 419 | } 420 | 421 | @Override 422 | public void draw(Canvas canvas, Paint paint) { 423 | final int viewWidth = CircleProgressBar.this.getWidth(); 424 | final int viewHeight = CircleProgressBar.this.getHeight(); 425 | canvas.drawCircle(viewWidth / 2, viewHeight / 2, (mCircleDiameter / 2 + mShadowRadius), 426 | mShadowPaint); 427 | canvas.drawCircle(viewWidth / 2, viewHeight / 2, (mCircleDiameter / 2), paint); 428 | } 429 | } 430 | 431 | /** 432 | * 开始动画 433 | */ 434 | public void start() { 435 | mProgressDrawable.start(); 436 | } 437 | 438 | /** 439 | * 设置动画起始位置 440 | */ 441 | public void setStartEndTrim(float startAngle, float endAngle) { 442 | mProgressDrawable.setStartEndTrim(startAngle, endAngle); 443 | } 444 | 445 | /** 446 | * 停止动画 447 | */ 448 | public void stop() { 449 | mProgressDrawable.stop(); 450 | } 451 | 452 | public void setProgressRotation(float rotation) { 453 | mProgressDrawable.setProgressRotation(rotation); 454 | } 455 | } -------------------------------------------------------------------------------- /nrl/src/main/java/com/xlf/nrl/LoadView.java: -------------------------------------------------------------------------------- 1 | package com.xlf.nrl; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.os.Build; 6 | import android.util.AttributeSet; 7 | import android.view.Gravity; 8 | import android.widget.LinearLayout; 9 | import android.widget.TextView; 10 | 11 | /** 12 | * Created by xiaolifan on 2015/12/21. 13 | * QQ: 1147904198 14 | * Email: xiao_lifan@163.com 15 | */ 16 | public class LoadView extends LinearLayout { 17 | 18 | private static final int DEFAULT_CIRCLE_SIZE = 42; 19 | private CircleProgressBar circleProgressBar; 20 | private TextView tvLoad; 21 | 22 | public LoadView(Context context) { 23 | super(context); 24 | setupViews(); 25 | } 26 | 27 | public LoadView(Context context, AttributeSet attrs) { 28 | super(context, attrs); 29 | setupViews(); 30 | } 31 | 32 | public LoadView(Context context, AttributeSet attrs, int defStyleAttr) { 33 | super(context, attrs, defStyleAttr); 34 | setupViews(); 35 | } 36 | 37 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 38 | public LoadView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 39 | super(context, attrs, defStyleAttr, defStyleRes); 40 | setupViews(); 41 | } 42 | 43 | /** 44 | * 添加View 45 | */ 46 | private void setupViews() { 47 | this.setOrientation(HORIZONTAL); 48 | this.setGravity(Gravity.CENTER); 49 | 50 | circleProgressBar = new CircleProgressBar(getContext()); 51 | LayoutParams lp = new LayoutParams((int) NrlUtils.dipToPx(getContext(), DEFAULT_CIRCLE_SIZE), 52 | (int) NrlUtils.dipToPx(getContext(), DEFAULT_CIRCLE_SIZE)); 53 | lp.rightMargin = (int) NrlUtils.dipToPx(getContext(), 10); 54 | addView(circleProgressBar, lp); 55 | tvLoad = new TextView(getContext()); 56 | addView(tvLoad); 57 | } 58 | 59 | public void setLoadText(String loadtText) { 60 | this.tvLoad.setText(loadtText); 61 | } 62 | 63 | public void setLoadTextColor(int color) { 64 | tvLoad.setTextColor(color); 65 | } 66 | 67 | public void setProgressBgColor(int color) { 68 | circleProgressBar.setBackgroundColor(color); 69 | } 70 | 71 | public void setProgressColor(int color) { 72 | circleProgressBar.setColorSchemeColors(color); 73 | } 74 | 75 | /** 76 | * 开始动画 77 | */ 78 | public void start() { 79 | circleProgressBar.start(); 80 | } 81 | 82 | /** 83 | * 设置动画起始位置 84 | */ 85 | public void setStartEndTrim(float startAngle, float endAngle) { 86 | circleProgressBar.setStartEndTrim(startAngle, endAngle); 87 | } 88 | 89 | /** 90 | * 停止动画 91 | */ 92 | public void stop() { 93 | circleProgressBar.stop(); 94 | } 95 | 96 | public void setProgressRotation(float rotation) { 97 | circleProgressBar.setProgressRotation(rotation); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /nrl/src/main/java/com/xlf/nrl/MaterialProgressDrawable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 The Android Open Source Project 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 | package com.xlf.nrl; 18 | 19 | import android.content.Context; 20 | import android.content.res.Resources; 21 | import android.graphics.Canvas; 22 | import android.graphics.Color; 23 | import android.graphics.ColorFilter; 24 | import android.graphics.Paint; 25 | import android.graphics.Paint.Style; 26 | import android.graphics.Path; 27 | import android.graphics.PixelFormat; 28 | import android.graphics.Rect; 29 | import android.graphics.RectF; 30 | import android.graphics.drawable.Animatable; 31 | import android.graphics.drawable.Drawable; 32 | import android.util.DisplayMetrics; 33 | import android.view.View; 34 | import android.view.animation.AccelerateDecelerateInterpolator; 35 | import android.view.animation.Animation; 36 | import android.view.animation.Interpolator; 37 | import android.view.animation.LinearInterpolator; 38 | import android.view.animation.Transformation; 39 | 40 | import java.util.ArrayList; 41 | 42 | public class MaterialProgressDrawable extends Drawable implements Animatable { 43 | // Maps to ProgressBar.Large style 44 | public static final int LARGE = 0; 45 | // Maps to ProgressBar default style 46 | public static final int DEFAULT = 1; 47 | private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); 48 | private static final Interpolator END_CURVE_INTERPOLATOR = new EndCurveInterpolator(); 49 | private static final Interpolator START_CURVE_INTERPOLATOR = new StartCurveInterpolator(); 50 | private static final Interpolator EASE_INTERPOLATOR = new AccelerateDecelerateInterpolator(); 51 | // Maps to ProgressBar default style 52 | private static final int CIRCLE_DIAMETER = 40; 53 | private static final float CENTER_RADIUS = 8.75f; //should add up to 10 when + stroke_width 54 | private static final float STROKE_WIDTH = 2.5f; 55 | // Maps to ProgressBar.Large style 56 | private static final int CIRCLE_DIAMETER_LARGE = 56; 57 | private static final float CENTER_RADIUS_LARGE = 12.5f; 58 | static final float STROKE_WIDTH_LARGE = 3f; 59 | /** 60 | * The duration of a single progress spin in milliseconds. 61 | */ 62 | private static final int ANIMATION_DURATION = 1000 * 80 / 60; 63 | /** 64 | * The number of points in the progress "star". 65 | */ 66 | private static final float NUM_POINTS = 5f; 67 | /** 68 | * Layout info for the arrowhead in dp 69 | */ 70 | private static final int ARROW_WIDTH = 10; 71 | private static final int ARROW_HEIGHT = 5; 72 | private static final float ARROW_OFFSET_ANGLE = 0; 73 | /** 74 | * Layout info for the arrowhead for the large spinner in dp 75 | */ 76 | static final int ARROW_WIDTH_LARGE = 12; 77 | static final int ARROW_HEIGHT_LARGE = 6; 78 | private static final float MAX_PROGRESS_ARC = .8f; 79 | private final int[] COLORS = new int[]{ 80 | Color.BLACK 81 | }; 82 | /** 83 | * The list of animators operating on this drawable. 84 | */ 85 | private final ArrayList mAnimators = new ArrayList(); 86 | /** 87 | * The indicator ring, used to manage animation state. 88 | */ 89 | private final Ring mRing; 90 | private final Callback mCallback = new Callback() { 91 | @Override 92 | public void invalidateDrawable(Drawable d) { 93 | invalidateSelf(); 94 | } 95 | 96 | @Override 97 | public void scheduleDrawable(Drawable d, Runnable what, long when) { 98 | scheduleSelf(what, when); 99 | } 100 | 101 | @Override 102 | public void unscheduleDrawable(Drawable d, Runnable what) { 103 | unscheduleSelf(what); 104 | } 105 | }; 106 | boolean mFinishing; 107 | /** 108 | * Canvas rotation in degrees. 109 | */ 110 | private float mRotation; 111 | private Resources mResources; 112 | private View mAnimExcutor; 113 | private Animation mAnimation; 114 | private float mRotationCount; 115 | private double mWidth; 116 | private double mHeight; 117 | private boolean mShowArrowOnFirstStart = false; 118 | 119 | public MaterialProgressDrawable(Context context, View animExcutor) { 120 | mAnimExcutor = animExcutor; 121 | mResources = context.getResources(); 122 | 123 | mRing = new Ring(mCallback); 124 | mRing.setColors(COLORS); 125 | 126 | updateSizes(DEFAULT); 127 | setupAnimators(); 128 | } 129 | 130 | public void setSizeParameters(double progressCircleWidth, double progressCircleHeight, 131 | double centerRadius, double strokeWidth, float arrowWidth, float arrowHeight) { 132 | final Ring ring = mRing; 133 | mWidth = progressCircleWidth; 134 | mHeight = progressCircleHeight ; 135 | ring.setStrokeWidth((float) strokeWidth ); 136 | ring.setCenterRadius(centerRadius); 137 | ring.setColorIndex(0); 138 | ring.setArrowDimensions(arrowWidth , arrowHeight ); 139 | ring.setInsets((int) mWidth, (int) mHeight); 140 | } 141 | 142 | /** 143 | * Set the overall size for the progress spinner. This updates the radius 144 | * and stroke width of the ring. 145 | * 146 | */ 147 | public void updateSizes(@ProgressDrawableSize int size) { 148 | final DisplayMetrics metrics = mResources.getDisplayMetrics(); 149 | final float screenDensity = metrics.density; 150 | 151 | if (size == LARGE) { 152 | setSizeParameters(CIRCLE_DIAMETER_LARGE*screenDensity, CIRCLE_DIAMETER_LARGE*screenDensity, CENTER_RADIUS_LARGE*screenDensity, 153 | STROKE_WIDTH_LARGE*screenDensity, ARROW_WIDTH_LARGE*screenDensity, ARROW_HEIGHT_LARGE*screenDensity); 154 | } else { 155 | setSizeParameters(CIRCLE_DIAMETER*screenDensity, CIRCLE_DIAMETER*screenDensity, CENTER_RADIUS*screenDensity, STROKE_WIDTH*screenDensity, 156 | ARROW_WIDTH*screenDensity, ARROW_HEIGHT*screenDensity); 157 | } 158 | } 159 | 160 | /** 161 | * @param show Set to true to display the arrowhead on the progress spinner. 162 | */ 163 | public void showArrow(boolean show) { 164 | mRing.setShowArrow(show); 165 | } 166 | 167 | /** 168 | * @param scale Set the scale of the arrowhead for the spinner. 169 | */ 170 | public void setArrowScale(float scale) { 171 | mRing.setArrowScale(scale); 172 | } 173 | 174 | /** 175 | * Set the start and end trim for the progress spinner arc. 176 | * 177 | * @param startAngle start angle 178 | * @param endAngle end angle 179 | */ 180 | public void setStartEndTrim(float startAngle, float endAngle) { 181 | mRing.setStartTrim(startAngle); 182 | mRing.setEndTrim(endAngle); 183 | } 184 | 185 | /** 186 | * Set the amount of rotation to apply to the progress spinner. 187 | * 188 | * @param rotation Rotation is from [0..1] 189 | */ 190 | public void setProgressRotation(float rotation) { 191 | mRing.setRotation(rotation); 192 | } 193 | 194 | /** 195 | * Update the background color of the circle image view. 196 | */ 197 | public void setBackgroundColor(int color) { 198 | mRing.setBackgroundColor(color); 199 | } 200 | 201 | /** 202 | * Set the colors used in the progress animation from color resources. 203 | * The first color will also be the color of the bar that grows in response 204 | * to a user swipe gesture. 205 | * 206 | * @param colors 207 | */ 208 | public void setColorSchemeColors(int... colors) { 209 | mRing.setColors(colors); 210 | mRing.setColorIndex(0); 211 | } 212 | 213 | @Override 214 | public int getIntrinsicHeight() { 215 | return (int) mHeight; 216 | } 217 | 218 | @Override 219 | public int getIntrinsicWidth() { 220 | return (int) mWidth; 221 | } 222 | 223 | @Override 224 | public void draw(Canvas c) { 225 | final Rect bounds = getBounds(); 226 | final int saveCount = c.save(); 227 | c.rotate(mRotation, bounds.exactCenterX(), bounds.exactCenterY()); 228 | mRing.draw(c, bounds); 229 | c.restoreToCount(saveCount); 230 | } 231 | 232 | public int getAlpha() { 233 | return mRing.getAlpha(); 234 | } 235 | 236 | @Override 237 | public void setAlpha(int alpha) { 238 | mRing.setAlpha(alpha); 239 | } 240 | 241 | @Override 242 | public void setColorFilter(ColorFilter colorFilter) { 243 | mRing.setColorFilter(colorFilter); 244 | } 245 | 246 | @SuppressWarnings("unused") 247 | private float getRotation() { 248 | return mRotation; 249 | } 250 | 251 | @SuppressWarnings("unused") 252 | void setRotation(float rotation) { 253 | mRotation = rotation; 254 | invalidateSelf(); 255 | } 256 | 257 | @Override 258 | public int getOpacity() { 259 | return PixelFormat.TRANSLUCENT; 260 | } 261 | 262 | @Override 263 | public boolean isRunning() { 264 | return !this.mAnimation.hasEnded(); 265 | } 266 | 267 | @Override 268 | public void start() { 269 | mAnimation.reset(); 270 | mRing.storeOriginals(); 271 | mRing.setShowArrow(mShowArrowOnFirstStart); 272 | 273 | // Already showing some part of the ring 274 | if (mRing.getEndTrim() != mRing.getStartTrim()) { 275 | mFinishing = true; 276 | mAnimation.setDuration(ANIMATION_DURATION / 2); 277 | mAnimExcutor.startAnimation(mAnimation); 278 | } else { 279 | mRing.setColorIndex(0); 280 | mRing.resetOriginals(); 281 | mAnimation.setDuration(ANIMATION_DURATION); 282 | mAnimExcutor.startAnimation(mAnimation); 283 | } 284 | } 285 | 286 | @Override 287 | public void stop() { 288 | mAnimExcutor.clearAnimation(); 289 | setRotation(0); 290 | mRing.setShowArrow(false); 291 | mRing.setColorIndex(0); 292 | mRing.resetOriginals(); 293 | } 294 | 295 | private void applyFinishTranslation(float interpolatedTime, Ring ring) { 296 | // shrink back down and complete a full rotation before 297 | // starting other circles 298 | // Rotation goes between [0..1]. 299 | float targetRotation = (float) (Math.floor(ring.getStartingRotation() / MAX_PROGRESS_ARC) 300 | + 1f); 301 | final float startTrim = ring.getStartingStartTrim() 302 | + (ring.getStartingEndTrim() - ring.getStartingStartTrim()) * interpolatedTime; 303 | ring.setStartTrim(startTrim); 304 | final float rotation = ring.getStartingRotation() 305 | + ((targetRotation - ring.getStartingRotation()) * interpolatedTime); 306 | ring.setRotation(rotation); 307 | } 308 | 309 | private void setupAnimators() { 310 | final Ring ring = mRing; 311 | final Animation animation = new Animation() { 312 | @Override 313 | public void applyTransformation(float interpolatedTime, Transformation t) { 314 | if (mFinishing) { 315 | applyFinishTranslation(interpolatedTime, ring); 316 | } else { 317 | // The minProgressArc is calculated from 0 to create an 318 | // angle that 319 | // matches the stroke width. 320 | final float minProgressArc = (float) Math.toRadians( 321 | ring.getStrokeWidth() / (2 * Math.PI * ring.getCenterRadius())); 322 | final float startingEndTrim = ring.getStartingEndTrim(); 323 | final float startingTrim = ring.getStartingStartTrim(); 324 | final float startingRotation = ring.getStartingRotation(); 325 | 326 | // Offset the minProgressArc to where the endTrim is 327 | // located. 328 | final float minArc = MAX_PROGRESS_ARC - minProgressArc; 329 | float endTrim = startingEndTrim + (minArc 330 | * START_CURVE_INTERPOLATOR.getInterpolation(interpolatedTime)); 331 | float startTrim = startingTrim + (MAX_PROGRESS_ARC 332 | * END_CURVE_INTERPOLATOR.getInterpolation(interpolatedTime)); 333 | 334 | final float sweepTrim = endTrim-startTrim; 335 | //Avoid the ring to be a full circle 336 | if(Math.abs(sweepTrim)>=1){ 337 | endTrim = startTrim+0.5f; 338 | } 339 | 340 | ring.setEndTrim(endTrim); 341 | 342 | ring.setStartTrim(startTrim); 343 | 344 | final float rotation = startingRotation + (0.25f * interpolatedTime); 345 | ring.setRotation(rotation); 346 | 347 | float groupRotation = ((720.0f / NUM_POINTS) * interpolatedTime) 348 | + (720.0f * (mRotationCount / NUM_POINTS)); 349 | setRotation(groupRotation); 350 | } 351 | } 352 | }; 353 | animation.setRepeatCount(Animation.INFINITE); 354 | animation.setRepeatMode(Animation.RESTART); 355 | animation.setInterpolator(LINEAR_INTERPOLATOR); 356 | animation.setAnimationListener(new Animation.AnimationListener() { 357 | 358 | @Override 359 | public void onAnimationStart(Animation animation) { 360 | mRotationCount = 0; 361 | } 362 | 363 | @Override 364 | public void onAnimationEnd(Animation animation) { 365 | // do nothing 366 | } 367 | 368 | @Override 369 | public void onAnimationRepeat(Animation animation) { 370 | ring.storeOriginals(); 371 | ring.goToNextColor(); 372 | ring.setStartTrim(ring.getEndTrim()); 373 | if (mFinishing) { 374 | // finished closing the last ring from the swipe gesture; go 375 | // into progress mode 376 | mFinishing = false; 377 | animation.setDuration(ANIMATION_DURATION); 378 | ring.setShowArrow(false); 379 | } else { 380 | mRotationCount = (mRotationCount + 1) % (NUM_POINTS); 381 | } 382 | } 383 | }); 384 | mAnimation = animation; 385 | } 386 | 387 | public void showArrowOnFirstStart(boolean showArrowOnFirstStart) { 388 | this.mShowArrowOnFirstStart = showArrowOnFirstStart; 389 | } 390 | 391 | 392 | public @interface ProgressDrawableSize { 393 | } 394 | 395 | private static class Ring { 396 | private final RectF mTempBounds = new RectF(); 397 | private final Paint mPaint = new Paint(); 398 | private final Paint mArrowPaint = new Paint(); 399 | 400 | private final Callback mCallback; 401 | private final Paint mCirclePaint = new Paint(); 402 | private float mStartTrim = 0.0f; 403 | private float mEndTrim = 0.0f; 404 | private float mRotation = 0.0f; 405 | private float mStrokeWidth = 5.0f; 406 | private float mStrokeInset = 2.5f; 407 | private int[] mColors; 408 | // mColorIndex represents the offset into the available mColors that the 409 | // progress circle should currently display. As the progress circle is 410 | // animating, the mColorIndex moves by one to the next available color. 411 | private int mColorIndex; 412 | private float mStartingStartTrim; 413 | private float mStartingEndTrim; 414 | private float mStartingRotation; 415 | private boolean mShowArrow; 416 | private Path mArrow; 417 | private float mArrowScale; 418 | private double mRingCenterRadius; 419 | private int mArrowWidth; 420 | private int mArrowHeight; 421 | private int mAlpha; 422 | private int mBackgroundColor; 423 | 424 | public Ring(Callback callback) { 425 | mCallback = callback; 426 | 427 | mPaint.setStrokeCap(Paint.Cap.SQUARE); 428 | mPaint.setAntiAlias(true); 429 | mPaint.setStyle(Style.STROKE); 430 | 431 | mArrowPaint.setStyle(Style.FILL); 432 | mArrowPaint.setAntiAlias(true); 433 | } 434 | 435 | public void setBackgroundColor(int color) { 436 | mBackgroundColor = color; 437 | } 438 | 439 | /** 440 | * Set the dimensions of the arrowhead. 441 | * 442 | * @param width Width of the hypotenuse of the arrow head 443 | * @param height Height of the arrow point 444 | */ 445 | public void setArrowDimensions(float width, float height) { 446 | mArrowWidth = (int) width; 447 | mArrowHeight = (int) height; 448 | } 449 | 450 | /** 451 | * Draw the progress spinner 452 | */ 453 | public void draw(Canvas c, Rect bounds) { 454 | final RectF arcBounds = mTempBounds; 455 | arcBounds.set(bounds); 456 | arcBounds.inset(mStrokeInset, mStrokeInset); 457 | 458 | final float startAngle = (mStartTrim + mRotation) * 360; 459 | final float endAngle = (mEndTrim + mRotation) * 360; 460 | float sweepAngle = endAngle - startAngle; 461 | mPaint.setColor(mColors[mColorIndex]); 462 | c.drawArc(arcBounds, startAngle, sweepAngle, false, mPaint); 463 | 464 | drawTriangle(c, startAngle, sweepAngle, bounds); 465 | 466 | if (mAlpha < 255) { 467 | mCirclePaint.setColor(mBackgroundColor); 468 | mCirclePaint.setAlpha(255 - mAlpha); 469 | c.drawCircle(bounds.exactCenterX(), bounds.exactCenterY(), bounds.width() / 2, 470 | mCirclePaint); 471 | } 472 | } 473 | 474 | private void drawTriangle(Canvas c, float startAngle, float sweepAngle, Rect bounds) { 475 | if (mShowArrow) { 476 | if (mArrow == null) { 477 | mArrow = new Path(); 478 | mArrow.setFillType(Path.FillType.EVEN_ODD); 479 | } else { 480 | mArrow.reset(); 481 | } 482 | 483 | // Adjust the position of the triangle so that it is inset as 484 | // much as the arc, but also centered on the arc. 485 | float x = (float) (mRingCenterRadius * Math.cos(0) + bounds.exactCenterX()); 486 | float y = (float) (mRingCenterRadius * Math.sin(0) + bounds.exactCenterY()); 487 | 488 | // Update the path each time. This works around an issue in SKIA 489 | // where concatenating a rotation matrix to a scale matrix 490 | // ignored a starting negative rotation. This appears to have 491 | // been fixed as of API 21. 492 | mArrow.moveTo(0, 0); 493 | mArrow.lineTo((mArrowWidth) * mArrowScale, 0); 494 | mArrow.lineTo(((mArrowWidth) * mArrowScale / 2), (mArrowHeight 495 | * mArrowScale)); 496 | mArrow.offset(x-((mArrowWidth) * mArrowScale / 2), y); 497 | mArrow.close(); 498 | // draw a triangle 499 | mArrowPaint.setColor(mColors[mColorIndex]); 500 | //when sweepAngle < 0 adjust the position of the arrow 501 | c.rotate(startAngle + (sweepAngle<0?0:sweepAngle) - ARROW_OFFSET_ANGLE, bounds.exactCenterX(), 502 | bounds.exactCenterY()); 503 | c.drawPath(mArrow, mArrowPaint); 504 | } 505 | } 506 | 507 | /** 508 | * Set the colors the progress spinner alternates between. 509 | * 510 | * @param colors Array of integers describing the colors. Must be non-null. 511 | */ 512 | public void setColors(int[] colors) { 513 | mColors = colors; 514 | // if colors are reset, make sure to reset the color index as well 515 | setColorIndex(0); 516 | } 517 | 518 | /** 519 | * @param index Index into the color array of the color to display in 520 | * the progress spinner. 521 | */ 522 | public void setColorIndex(int index) { 523 | mColorIndex = index; 524 | } 525 | 526 | /** 527 | * Proceed to the next available ring color. This will automatically 528 | * wrap back to the beginning of colors. 529 | */ 530 | public void goToNextColor() { 531 | mColorIndex = (mColorIndex + 1) % (mColors.length); 532 | } 533 | 534 | public void setColorFilter(ColorFilter filter) { 535 | mPaint.setColorFilter(filter); 536 | invalidateSelf(); 537 | } 538 | 539 | /** 540 | * @return Current alpha of the progress spinner and arrowhead. 541 | */ 542 | public int getAlpha() { 543 | return mAlpha; 544 | } 545 | 546 | /** 547 | * @param alpha Set the alpha of the progress spinner and associated arrowhead. 548 | */ 549 | public void setAlpha(int alpha) { 550 | mAlpha = alpha; 551 | } 552 | 553 | @SuppressWarnings("unused") 554 | public float getStrokeWidth() { 555 | return mStrokeWidth; 556 | } 557 | 558 | /** 559 | * @param strokeWidth Set the stroke width of the progress spinner in pixels. 560 | */ 561 | public void setStrokeWidth(float strokeWidth) { 562 | mStrokeWidth = strokeWidth; 563 | mPaint.setStrokeWidth(strokeWidth); 564 | invalidateSelf(); 565 | } 566 | 567 | @SuppressWarnings("unused") 568 | public float getStartTrim() { 569 | return mStartTrim; 570 | } 571 | 572 | @SuppressWarnings("unused") 573 | public void setStartTrim(float startTrim) { 574 | mStartTrim = startTrim; 575 | invalidateSelf(); 576 | } 577 | 578 | public float getStartingStartTrim() { 579 | return mStartingStartTrim; 580 | } 581 | 582 | public float getStartingEndTrim() { 583 | return mStartingEndTrim; 584 | } 585 | 586 | @SuppressWarnings("unused") 587 | public float getEndTrim() { 588 | return mEndTrim; 589 | } 590 | 591 | @SuppressWarnings("unused") 592 | public void setEndTrim(float endTrim) { 593 | mEndTrim = endTrim; 594 | invalidateSelf(); 595 | } 596 | 597 | @SuppressWarnings("unused") 598 | public float getRotation() { 599 | return mRotation; 600 | } 601 | 602 | @SuppressWarnings("unused") 603 | public void setRotation(float rotation) { 604 | mRotation = rotation; 605 | invalidateSelf(); 606 | } 607 | 608 | public void setInsets(int width, int height) { 609 | final float minEdge = (float) Math.min(width, height); 610 | float insets; 611 | if (mRingCenterRadius <= 0 || minEdge < 0) { 612 | insets = (float) Math.ceil(mStrokeWidth / 2.0f); 613 | } else { 614 | insets = (float) (minEdge / 2.0f - mRingCenterRadius); 615 | } 616 | mStrokeInset = insets; 617 | } 618 | 619 | @SuppressWarnings("unused") 620 | public float getInsets() { 621 | return mStrokeInset; 622 | } 623 | 624 | public double getCenterRadius() { 625 | return mRingCenterRadius; 626 | } 627 | 628 | /** 629 | * @param centerRadius Inner radius in px of the circle the progress 630 | * spinner arc traces. 631 | */ 632 | public void setCenterRadius(double centerRadius) { 633 | mRingCenterRadius = centerRadius; 634 | } 635 | 636 | /** 637 | * @param show Set to true to show the arrow head on the progress spinner. 638 | */ 639 | public void setShowArrow(boolean show) { 640 | if (mShowArrow != show) { 641 | mShowArrow = show; 642 | invalidateSelf(); 643 | } 644 | } 645 | 646 | /** 647 | * @param scale Set the scale of the arrowhead for the spinner. 648 | */ 649 | public void setArrowScale(float scale) { 650 | if (scale != mArrowScale) { 651 | mArrowScale = scale; 652 | invalidateSelf(); 653 | } 654 | } 655 | 656 | /** 657 | * @return The amount the progress spinner is currently rotated, between [0..1]. 658 | */ 659 | public float getStartingRotation() { 660 | return mStartingRotation; 661 | } 662 | 663 | /** 664 | * If the start / end trim are offset to begin with, store them so that 665 | * animation starts from that offset. 666 | */ 667 | public void storeOriginals() { 668 | mStartingStartTrim = mStartTrim; 669 | mStartingEndTrim = mEndTrim; 670 | mStartingRotation = mRotation; 671 | } 672 | 673 | /** 674 | * Reset the progress spinner to default rotation, start and end angles. 675 | */ 676 | public void resetOriginals() { 677 | mStartingStartTrim = 0; 678 | mStartingEndTrim = 0; 679 | mStartingRotation = 0; 680 | setStartTrim(0); 681 | setEndTrim(0); 682 | setRotation(0); 683 | } 684 | 685 | private void invalidateSelf() { 686 | mCallback.invalidateDrawable(null); 687 | } 688 | } 689 | 690 | /** 691 | * Squishes the interpolation curve into the second half of the animation. 692 | */ 693 | private static class EndCurveInterpolator extends AccelerateDecelerateInterpolator { 694 | @Override 695 | public float getInterpolation(float input) { 696 | return super.getInterpolation(Math.max(0, (input - 0.5f) * 2.0f)); 697 | } 698 | } 699 | 700 | /** 701 | * Squishes the interpolation curve into the first half of the animation. 702 | */ 703 | private static class StartCurveInterpolator extends AccelerateDecelerateInterpolator { 704 | @Override 705 | public float getInterpolation(float input) { 706 | return super.getInterpolation(Math.min(1, input * 2.0f)); 707 | } 708 | } 709 | } -------------------------------------------------------------------------------- /nrl/src/main/java/com/xlf/nrl/NrlUtils.java: -------------------------------------------------------------------------------- 1 | package com.xlf.nrl; 2 | 3 | import android.content.Context; 4 | import android.util.DisplayMetrics; 5 | import android.util.TypedValue; 6 | 7 | /** 8 | * Created by xiaolifan on 2015/12/22. 9 | * QQ: 1147904198 10 | * Email: xiao_lifan@163.com 11 | */ 12 | public abstract class NrlUtils { 13 | 14 | public static float dipToPx(Context context, float value) { 15 | DisplayMetrics metrics = context.getResources().getDisplayMetrics(); 16 | return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, metrics); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /nrl/src/main/java/com/xlf/nrl/NsRefreshLayout.java: -------------------------------------------------------------------------------- 1 | package com.xlf.nrl; 2 | 3 | import android.animation.Animator; 4 | import android.animation.ValueAnimator; 5 | import android.annotation.TargetApi; 6 | import android.content.Context; 7 | import android.content.res.Resources; 8 | import android.content.res.TypedArray; 9 | import android.graphics.Color; 10 | import android.os.Build; 11 | import android.support.v4.view.ViewCompat; 12 | import android.text.TextUtils; 13 | import android.util.AttributeSet; 14 | import android.view.Gravity; 15 | import android.view.MotionEvent; 16 | import android.view.View; 17 | import android.view.ViewTreeObserver; 18 | import android.widget.AbsListView; 19 | import android.widget.FrameLayout; 20 | 21 | /** 22 | * Created by xiaolifan on 2015/12/21. 23 | * QQ: 1147904198 24 | * Email: xiao_lifan@163.com 25 | */ 26 | public class NsRefreshLayout extends FrameLayout { 27 | 28 | private static final int LOADING_VIEW_FINAL_HEIGHT_DP = 80; 29 | 30 | private static final int ACTION_PULL_DOWN_REFRESH = 0; 31 | private static final int ACTION_PULL_UP_LOAD_MORE = 1; 32 | 33 | 34 | private LoadView headerView; 35 | private LoadView footerView; 36 | private View mContentView; 37 | 38 | /** 39 | * 是否支持下拉刷新 40 | */ 41 | private boolean mPullRefreshEnable = true; 42 | /** 43 | * 是否支持上拉加载更多 44 | */ 45 | private boolean mPullLoadEnable = true; 46 | /** 47 | * 是否自动加载更多:false-释放后加载更多,true-到达上拉加载条件后自动触发 48 | */ 49 | private boolean mAutoLoadMore; 50 | 51 | private NsRefreshLayoutListener refreshLayoutListener; 52 | private NsRefreshLayoutController refreshLayoutController; 53 | 54 | /** 55 | * 上一次触摸的Y位置 56 | */ 57 | private float preY; 58 | private float preX; 59 | 60 | /** 61 | * 是否正在进行刷新操作 62 | */ 63 | private boolean isRefreshing = false; 64 | 65 | /** 66 | * 加载视图最终展示的高度 67 | */ 68 | private float loadingViewFinalHeight = 0; 69 | /** 70 | * 加载视图回弹的高度 71 | */ 72 | private float loadingViewOverHeight = 0; 73 | 74 | private boolean actionDetermined = false; 75 | private int mCurrentAction = -1; 76 | 77 | //控件属性 78 | /** 79 | * LoadView背景颜色 80 | */ 81 | private int mLoadViewBgColor; 82 | /** 83 | * 进度条背景颜色 84 | */ 85 | private int mProgressBgColor; 86 | /** 87 | * 进度条颜色 88 | */ 89 | private int mProgressColor; 90 | /** 91 | * LoadView文字颜色 92 | */ 93 | private int mLoadViewTextColor; 94 | /** 95 | * 下拉刷新文字描述 96 | */ 97 | private String mPullRefreshText; 98 | /** 99 | * 上拉加载文字描述 100 | */ 101 | private String mPullLoadText; 102 | 103 | /** 104 | * 点击移动距离的误差值(点击操作可能会导致轻微的滑动) 105 | */ 106 | private static final int CLICK_TOUCH_DEVIATION = 4; 107 | 108 | public NsRefreshLayout(Context context) { 109 | super(context); 110 | initAttrs(context, null); 111 | } 112 | 113 | public NsRefreshLayout(Context context, AttributeSet attrs) { 114 | super(context, attrs); 115 | initAttrs(context, attrs); 116 | } 117 | 118 | public NsRefreshLayout(Context context, AttributeSet attrs, int defStyleAttr) { 119 | super(context, attrs, defStyleAttr); 120 | initAttrs(context, attrs); 121 | } 122 | 123 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 124 | public NsRefreshLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 125 | super(context, attrs, defStyleAttr, defStyleRes); 126 | initAttrs(context, attrs); 127 | } 128 | 129 | /** 130 | * 初始化控件属性 131 | */ 132 | private void initAttrs(Context context, AttributeSet attrs) { 133 | if (getChildCount() > 1) { 134 | throw new RuntimeException("can only have one child"); 135 | } 136 | loadingViewFinalHeight = NrlUtils.dipToPx(context, LOADING_VIEW_FINAL_HEIGHT_DP); 137 | loadingViewOverHeight = loadingViewFinalHeight * 2; 138 | 139 | if (isInEditMode() && attrs == null) { 140 | return; 141 | } 142 | 143 | int resId; 144 | TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.NsRefreshLayout); 145 | Resources resources = context.getResources(); 146 | 147 | //LoadView背景颜色 148 | resId = ta.getResourceId(R.styleable.NsRefreshLayout_load_view_bg_color, -1); 149 | if (resId == -1) { 150 | mLoadViewBgColor = ta.getColor(R.styleable.NsRefreshLayout_load_view_bg_color, 151 | Color.WHITE); 152 | } else { 153 | mLoadViewBgColor = resources.getColor(resId); 154 | } 155 | 156 | //加载文字颜色 157 | resId = ta.getResourceId(R.styleable.NsRefreshLayout_load_text_color, -1); 158 | if (resId == -1) { 159 | mLoadViewTextColor = ta.getColor(R.styleable.NsRefreshLayout_load_text_color, 160 | Color.BLACK); 161 | } else { 162 | mLoadViewTextColor = resources.getColor(resId); 163 | } 164 | 165 | //进度条背景颜色 166 | resId = ta.getResourceId(R.styleable.NsRefreshLayout_progress_bg_color, -1); 167 | if (resId == -1) { 168 | mProgressBgColor = ta.getColor(R.styleable.NsRefreshLayout_progress_bg_color, 169 | Color.WHITE); 170 | } else { 171 | mProgressBgColor = resources.getColor(resId); 172 | } 173 | 174 | //进度条颜色 175 | resId = ta.getResourceId(R.styleable.NsRefreshLayout_progress_bar_color, -1); 176 | if (resId == -1) { 177 | mProgressColor = ta.getColor(R.styleable.NsRefreshLayout_progress_bar_color, 178 | Color.RED); 179 | } else { 180 | mProgressColor = resources.getColor(resId); 181 | } 182 | 183 | //下拉刷新文字描述 184 | resId = ta.getResourceId(R.styleable.NsRefreshLayout_pull_refresh_text, -1); 185 | if (resId == -1) { 186 | mPullRefreshText = ta.getString(R.styleable.NsRefreshLayout_pull_refresh_text); 187 | } else { 188 | mPullRefreshText = resources.getString(resId); 189 | } 190 | 191 | //上拉加载文字描述 192 | resId = ta.getResourceId(R.styleable.NsRefreshLayout_pull_load_text, -1); 193 | if (resId == -1) { 194 | mPullLoadText = ta.getString(R.styleable.NsRefreshLayout_pull_load_text); 195 | } else { 196 | mPullLoadText = resources.getString(resId); 197 | } 198 | 199 | mAutoLoadMore = ta.getBoolean(R.styleable.NsRefreshLayout_auto_load_more, false); 200 | mPullRefreshEnable = ta.getBoolean(R.styleable.NsRefreshLayout_pull_refresh_enable, true); 201 | mPullLoadEnable = ta.getBoolean(R.styleable.NsRefreshLayout_pull_load_enable, true); 202 | } 203 | 204 | @Override 205 | protected void onFinishInflate() { 206 | super.onFinishInflate(); 207 | mContentView = getChildAt(0); 208 | setupViews(); 209 | } 210 | 211 | private void setupViews() { 212 | //下拉刷新视图 213 | LayoutParams lp; 214 | if (mPullRefreshEnable) { 215 | lp = new LayoutParams(LayoutParams.MATCH_PARENT, 0); 216 | headerView = new LoadView(getContext()); 217 | headerView.setLoadText(TextUtils.isEmpty(mPullRefreshText) ? 218 | getContext().getString(R.string.default_pull_refresh_text) : mPullRefreshText); 219 | headerView.setStartEndTrim(0, 0.75f); 220 | headerView.setBackgroundColor(mLoadViewBgColor); 221 | headerView.setLoadTextColor(mLoadViewTextColor); 222 | headerView.setProgressBgColor(mProgressBgColor); 223 | headerView.setProgressColor(mProgressColor); 224 | addView(headerView, lp); 225 | } 226 | 227 | if (mPullLoadEnable) { 228 | //上拉加载更多视图 229 | lp = new LayoutParams(LayoutParams.MATCH_PARENT, 0); 230 | lp.gravity = Gravity.BOTTOM; 231 | footerView = new LoadView(getContext()); 232 | footerView.setLoadText(TextUtils.isEmpty(mPullLoadText) ? 233 | getContext().getString(R.string.default_pull_load_text) : mPullLoadText); 234 | footerView.setStartEndTrim(0.5f, 1.25f); 235 | footerView.setBackgroundColor(mLoadViewBgColor); 236 | footerView.setLoadTextColor(mLoadViewTextColor); 237 | footerView.setProgressBgColor(mProgressBgColor); 238 | footerView.setProgressColor(mProgressColor); 239 | addView(footerView, lp); 240 | } 241 | } 242 | 243 | @Override 244 | public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { 245 | super.requestDisallowInterceptTouchEvent(disallowIntercept); 246 | } 247 | 248 | @Override 249 | public boolean onInterceptTouchEvent(MotionEvent ev) { 250 | if ((!mPullRefreshEnable && !mPullLoadEnable) || isRefreshing) { 251 | return super.onInterceptTouchEvent(ev); 252 | } 253 | 254 | switch (ev.getActionMasked()) { 255 | case MotionEvent.ACTION_DOWN: { 256 | if (refreshLayoutController != null) { 257 | mPullRefreshEnable = refreshLayoutController.isPullRefreshEnable(); 258 | mPullLoadEnable = refreshLayoutController.isPullLoadEnable(); 259 | } 260 | preY = ev.getY(); 261 | preX = ev.getX(); 262 | actionDetermined = false; 263 | return super.onInterceptTouchEvent(ev); 264 | } 265 | 266 | case MotionEvent.ACTION_MOVE: { 267 | float currentY = ev.getY(); 268 | float currentX = ev.getX(); 269 | float dy = currentY - preY; 270 | float dx = currentX - preX; 271 | preY = currentY; 272 | preX = currentX; 273 | if (!actionDetermined) { 274 | actionDetermined = true; 275 | //判断是下拉刷新还是上拉加载更多 276 | if (dy > 0 && !canChildScrollUp() && mPullRefreshEnable) { 277 | mCurrentAction = ACTION_PULL_DOWN_REFRESH; 278 | } else if (dy < 0 && !canChildScrollDown() && mPullLoadEnable) { 279 | mCurrentAction = ACTION_PULL_UP_LOAD_MORE; 280 | } else { 281 | mCurrentAction = -1; 282 | } 283 | } 284 | 285 | if (mCurrentAction != -1) { 286 | return true; 287 | } else { 288 | return super.onInterceptTouchEvent(ev); 289 | } 290 | } 291 | 292 | default: { 293 | return super.onInterceptTouchEvent(ev); 294 | } 295 | } 296 | } 297 | 298 | @Override 299 | public boolean onTouchEvent(MotionEvent event) { 300 | if ((!mPullRefreshEnable && !mPullLoadEnable) || isRefreshing) { 301 | return false; 302 | } 303 | 304 | switch (event.getActionMasked()) { 305 | case MotionEvent.ACTION_MOVE: { 306 | float currentY = event.getY(); 307 | float currentX = event.getX(); 308 | float dy = currentY - preY; 309 | float dx = currentX - preX; 310 | preY = currentY; 311 | preX = currentX; 312 | handleScroll(dy); 313 | observerArriveBottom(); 314 | return true; 315 | } 316 | 317 | case MotionEvent.ACTION_UP: 318 | case MotionEvent.ACTION_CANCEL: { 319 | return releaseTouch(); 320 | } 321 | 322 | default: { 323 | return super.onTouchEvent(event); 324 | } 325 | } 326 | 327 | } 328 | 329 | private void observerArriveBottom() { 330 | if (isRefreshing || !mAutoLoadMore || !mPullLoadEnable) { 331 | return; 332 | } 333 | mContentView.getViewTreeObserver().addOnScrollChangedListener( 334 | new ViewTreeObserver.OnScrollChangedListener() { 335 | 336 | @Override 337 | public void onScrollChanged() { 338 | mContentView.removeCallbacks(flingRunnable); 339 | mContentView.postDelayed(flingRunnable, 6); 340 | } 341 | }); 342 | } 343 | 344 | private Runnable flingRunnable = new Runnable() { 345 | @Override 346 | public void run() { 347 | if (isRefreshing || !mAutoLoadMore || !mPullLoadEnable) { 348 | return; 349 | } 350 | 351 | if (!canChildScrollDown()) { 352 | mCurrentAction = ACTION_PULL_UP_LOAD_MORE; 353 | isRefreshing = true; 354 | startPullUpLoadMore(0); 355 | } 356 | } 357 | }; 358 | 359 | /** 360 | * 处理滚动 361 | */ 362 | private boolean handleScroll(float distanceY) { 363 | if (!canChildScrollUp() && mCurrentAction == ACTION_PULL_DOWN_REFRESH && 364 | mPullRefreshEnable) { 365 | //下拉刷新 366 | LayoutParams lp = (LayoutParams) headerView.getLayoutParams(); 367 | lp.height += distanceY; 368 | if (lp.height < 0) { 369 | lp.height = 0; 370 | } else if (lp.height > loadingViewOverHeight) { 371 | lp.height = (int) loadingViewOverHeight; 372 | } 373 | headerView.setLayoutParams(lp); 374 | if (lp.height < loadingViewOverHeight) { 375 | headerView.setLoadText(TextUtils.isEmpty(mPullRefreshText) ? 376 | getContext().getString(R.string.default_pull_refresh_text) : mPullRefreshText); 377 | } else { 378 | headerView.setLoadText(getContext().getString(R.string.release_to_refresh)); 379 | } 380 | headerView.setProgressRotation(lp.height / loadingViewOverHeight); 381 | adjustContentViewHeight(lp.height); 382 | if (lp.height > 0) { 383 | return true; 384 | } 385 | 386 | } else if (!canChildScrollDown() && mCurrentAction == ACTION_PULL_UP_LOAD_MORE && mPullLoadEnable) { 387 | //上拉加载更多 388 | LayoutParams lp = (LayoutParams) footerView.getLayoutParams(); 389 | lp.height -= distanceY; 390 | if (lp.height < 0) { 391 | lp.height = 0; 392 | } else if (lp.height > loadingViewOverHeight) { 393 | lp.height = (int) loadingViewOverHeight; 394 | } 395 | footerView.setLayoutParams(lp); 396 | if (lp.height < loadingViewOverHeight) { 397 | footerView.setLoadText(TextUtils.isEmpty(mPullLoadText) ? 398 | getContext().getString(R.string.default_pull_load_text) : mPullLoadText); 399 | } else { 400 | footerView.setLoadText(getContext().getString(R.string.release_to_load)); 401 | } 402 | footerView.setProgressRotation(lp.height / loadingViewOverHeight); 403 | adjustContentViewHeight(-lp.height); 404 | if (lp.height > 0) { 405 | return true; 406 | } 407 | } 408 | return false; 409 | } 410 | 411 | private void adjustContentViewHeight(float h) { 412 | mContentView.setTranslationY(h); 413 | //下面的方式可以看到完整内容,但是有掉帧现象 414 | /*if (mCurrentAction == ACTION_PULL_DOWN_REFRESH) { 415 | mContentView.setTranslationY(h); 416 | } 417 | LayoutParams lp = (LayoutParams) mContentView.getLayoutParams(); 418 | lp.height = (int) (getMeasuredHeight() - Math.abs(h)); 419 | mContentView.setLayoutParams(lp);*/ 420 | 421 | } 422 | 423 | private boolean releaseTouch() { 424 | boolean result = false; 425 | LayoutParams lp; 426 | if (mPullRefreshEnable && mCurrentAction == ACTION_PULL_DOWN_REFRESH) { 427 | lp = (LayoutParams) headerView.getLayoutParams(); 428 | if (lp.height >= loadingViewOverHeight) { 429 | //触发下拉刷新 430 | startPullDownRefresh(lp.height); 431 | result = true; 432 | } else if (lp.height > 0) { 433 | //未满足下拉刷新触发条件,重置状态 434 | resetPullDownRefresh(lp.height); 435 | result = lp.height >= CLICK_TOUCH_DEVIATION; 436 | } else { 437 | resetPullRefreshState(); 438 | } 439 | } 440 | 441 | if (mPullLoadEnable && mCurrentAction == ACTION_PULL_UP_LOAD_MORE) { 442 | lp = (LayoutParams) footerView.getLayoutParams(); 443 | if (lp.height >= loadingViewOverHeight) { 444 | //触发上拉加载更多 445 | startPullUpLoadMore(lp.height); 446 | result = true; 447 | } else if (lp.height > 0) { 448 | //未满足上拉加载更多触发条件,重置状态 449 | resetPullUpLoadMore(lp.height); 450 | result = lp.height >= CLICK_TOUCH_DEVIATION; 451 | } else { 452 | resetPullLoadState(); 453 | } 454 | } 455 | return result; 456 | } 457 | 458 | private void startPullDownRefresh(int headerViewHeight) { 459 | isRefreshing = true; 460 | ValueAnimator animator = ValueAnimator.ofFloat(headerViewHeight, loadingViewFinalHeight); 461 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 462 | @Override 463 | public void onAnimationUpdate(ValueAnimator animation) { 464 | LayoutParams lp = (LayoutParams) headerView.getLayoutParams(); 465 | lp.height = (int) ((Float) animation.getAnimatedValue()).floatValue(); 466 | headerView.setLayoutParams(lp); 467 | adjustContentViewHeight(lp.height); 468 | } 469 | }); 470 | animator.addListener(new SimpleAnimatorListener() { 471 | @Override 472 | public void onAnimationEnd(Animator animation) { 473 | headerView.start(); 474 | headerView.setLoadText(getContext().getString(R.string.refresh_text)); 475 | 476 | if (refreshLayoutListener != null) { 477 | refreshLayoutListener.onRefresh(); 478 | } 479 | } 480 | }); 481 | animator.setDuration(300); 482 | animator.start(); 483 | } 484 | 485 | /** 486 | * 重置下拉刷新状态 487 | * 488 | * @param headerViewHeight 当前下拉刷新视图的高度 489 | */ 490 | private void resetPullDownRefresh(int headerViewHeight) { 491 | headerView.stop(); 492 | //headerView.setStartEndTrim(0, 0.75f); 493 | ValueAnimator animator = ValueAnimator.ofFloat(headerViewHeight, 0); 494 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 495 | @Override 496 | public void onAnimationUpdate(ValueAnimator animation) { 497 | LayoutParams lp = (LayoutParams) headerView.getLayoutParams(); 498 | lp.height = (int) ((Float) animation.getAnimatedValue()).floatValue(); 499 | headerView.setLayoutParams(lp); 500 | adjustContentViewHeight(lp.height); 501 | } 502 | }); 503 | animator.addListener(new SimpleAnimatorListener() { 504 | @Override 505 | public void onAnimationEnd(Animator animation) { 506 | resetPullRefreshState(); 507 | 508 | } 509 | }); 510 | animator.setDuration(300); 511 | animator.start(); 512 | } 513 | 514 | private void resetPullRefreshState() { 515 | //重置动画结束才算完全完成刷新动作 516 | isRefreshing = false; 517 | actionDetermined = false; 518 | mCurrentAction = -1; 519 | headerView.setLoadText(TextUtils.isEmpty(mPullRefreshText) ? 520 | getContext().getString(R.string.default_pull_refresh_text) : mPullRefreshText); 521 | } 522 | 523 | private void startPullUpLoadMore(int headerViewHeight) { 524 | isRefreshing = true; 525 | ValueAnimator animator = ValueAnimator.ofFloat(headerViewHeight, loadingViewFinalHeight); 526 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 527 | @Override 528 | public void onAnimationUpdate(ValueAnimator animation) { 529 | LayoutParams lp = (LayoutParams) footerView.getLayoutParams(); 530 | lp.height = (int) ((Float) animation.getAnimatedValue()).floatValue(); 531 | footerView.setLayoutParams(lp); 532 | adjustContentViewHeight(-lp.height); 533 | } 534 | }); 535 | animator.addListener(new SimpleAnimatorListener() { 536 | @Override 537 | public void onAnimationEnd(Animator animation) { 538 | footerView.start(); 539 | footerView.setLoadText(getContext().getString(R.string.load_text)); 540 | 541 | if (refreshLayoutListener != null) { 542 | refreshLayoutListener.onLoadMore(); 543 | } 544 | } 545 | }); 546 | animator.setDuration(300); 547 | animator.start(); 548 | } 549 | 550 | /** 551 | * 重置下拉刷新状态 552 | * 553 | * @param headerViewHeight 当前下拉刷新视图的高度 554 | */ 555 | private void resetPullUpLoadMore(int headerViewHeight) { 556 | footerView.stop(); 557 | //footerView.setStartEndTrim(0.5f, 1.25f); 558 | ValueAnimator animator = ValueAnimator.ofFloat(headerViewHeight, 0); 559 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 560 | @Override 561 | public void onAnimationUpdate(ValueAnimator animation) { 562 | LayoutParams lp = (LayoutParams) footerView.getLayoutParams(); 563 | lp.height = (int) ((Float) animation.getAnimatedValue()).floatValue(); 564 | footerView.setLayoutParams(lp); 565 | adjustContentViewHeight(-lp.height); 566 | } 567 | }); 568 | animator.addListener(new SimpleAnimatorListener() { 569 | @Override 570 | public void onAnimationEnd(Animator animation) { 571 | resetPullLoadState(); 572 | 573 | } 574 | }); 575 | animator.setDuration(300); 576 | animator.start(); 577 | } 578 | 579 | private void resetPullLoadState() { 580 | //重置动画结束才算完全完成刷新动作 581 | isRefreshing = false; 582 | actionDetermined = false; 583 | mCurrentAction = -1; 584 | footerView.setLoadText(TextUtils.isEmpty(mPullLoadText) ? 585 | getContext().getString(R.string.default_pull_load_text) : mPullLoadText); 586 | } 587 | 588 | /** 589 | * @return 子视图是否可以下拉 590 | */ 591 | public boolean canChildScrollUp() { 592 | if (mContentView == null) { 593 | return false; 594 | } 595 | if (Build.VERSION.SDK_INT < 14) { 596 | if (mContentView instanceof AbsListView) { 597 | final AbsListView absListView = (AbsListView) mContentView; 598 | return absListView.getChildCount() > 0 599 | && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0) 600 | .getTop() < absListView.getPaddingTop()); 601 | } else { 602 | return ViewCompat.canScrollVertically(mContentView, -1) || mContentView.getScrollY() > 0; 603 | } 604 | } else { 605 | return ViewCompat.canScrollVertically(mContentView, -1); 606 | } 607 | } 608 | 609 | /** 610 | * @return 子视图是否可以上划 611 | */ 612 | public boolean canChildScrollDown() { 613 | if (mContentView == null) { 614 | return false; 615 | } 616 | if (Build.VERSION.SDK_INT < 14) { 617 | if (mContentView instanceof AbsListView) { 618 | final AbsListView absListView = (AbsListView) mContentView; 619 | if (absListView.getChildCount() > 0) { 620 | int lastChildBottom = absListView.getChildAt(absListView.getChildCount() - 1) 621 | .getBottom(); 622 | return absListView.getLastVisiblePosition() == absListView.getAdapter().getCount() - 1 623 | && lastChildBottom <= absListView.getMeasuredHeight(); 624 | } else { 625 | return false; 626 | } 627 | 628 | } else { 629 | return ViewCompat.canScrollVertically(mContentView, 1) || mContentView.getScrollY() > 0; 630 | } 631 | } else { 632 | return ViewCompat.canScrollVertically(mContentView, 1); 633 | } 634 | } 635 | 636 | public void setRefreshLayoutListener(NsRefreshLayoutListener refreshLayoutListener) { 637 | this.refreshLayoutListener = refreshLayoutListener; 638 | } 639 | 640 | public interface NsRefreshLayoutListener { 641 | void onRefresh(); 642 | 643 | void onLoadMore(); 644 | } 645 | 646 | public void setRefreshLayoutController(NsRefreshLayoutController nsRefreshLayoutController) { 647 | this.refreshLayoutController = nsRefreshLayoutController; 648 | } 649 | 650 | public interface NsRefreshLayoutController { 651 | /** 652 | * 当前下拉刷新是否可用 653 | */ 654 | boolean isPullRefreshEnable(); 655 | 656 | /** 657 | * 当前上拉加载是否可用,比如列表已无更多数据,可禁用上拉加载功能 658 | */ 659 | boolean isPullLoadEnable(); 660 | } 661 | 662 | /** 663 | * 完成下拉刷新动作 664 | */ 665 | public void finishPullRefresh() { 666 | if (mCurrentAction == ACTION_PULL_DOWN_REFRESH) { 667 | resetPullDownRefresh(headerView == null ? 0 : headerView.getMeasuredHeight()); 668 | } 669 | } 670 | 671 | /** 672 | * 完成上拉加载更多动作 673 | */ 674 | public void finishPullLoad() { 675 | if (mCurrentAction == ACTION_PULL_UP_LOAD_MORE) { 676 | resetPullUpLoadMore(footerView == null ? 0 : footerView.getMeasuredHeight()); 677 | } 678 | } 679 | } 680 | -------------------------------------------------------------------------------- /nrl/src/main/java/com/xlf/nrl/SimpleAnimatorListener.java: -------------------------------------------------------------------------------- 1 | package com.xlf.nrl; 2 | 3 | import android.animation.Animator; 4 | 5 | /** 6 | * Created by xiaolifan on 2015/12/22. 7 | * QQ: 1147904198 8 | * Email: xiao_lifan@163.com 9 | */ 10 | public class SimpleAnimatorListener implements Animator.AnimatorListener { 11 | @Override 12 | public void onAnimationStart(Animator animation) { 13 | 14 | } 15 | 16 | @Override 17 | public void onAnimationEnd(Animator animation) { 18 | 19 | } 20 | 21 | @Override 22 | public void onAnimationCancel(Animator animation) { 23 | 24 | } 25 | 26 | @Override 27 | public void onAnimationRepeat(Animator animation) { 28 | 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /nrl/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /nrl/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | NsRefreshLayout 3 | 下拉刷新 4 | 上拉加载更多 5 | 正在刷新 6 | 正在加载 7 | 松手刷新 8 | 松手加载更多 9 | 10 | -------------------------------------------------------------------------------- /nrl/src/test/java/com/xlf/nrl/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.xlf.nrl; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * To work on unit tests, switch the Test Artifact in the Build Variants view. 9 | */ 10 | public class ExampleUnitTest { 11 | @Test 12 | public void addition_isCorrect() throws Exception { 13 | assertEquals(4, 2 + 2); 14 | } 15 | } -------------------------------------------------------------------------------- /principles.md: -------------------------------------------------------------------------------- 1 | ## 动机 2 | 3 | 项目中,需要一个支持任意View的下拉刷新+上拉加载控件,GitHub上有很多现成的实现,如[Android-PullToRefresh](https://github.com/chrisbanes/Android-PullToRefresh), [android-Ultra-Pull-To-Refresh](https://github.com/liaohuqiu/android-Ultra-Pull-To-Refresh)等,这些Library都非常优秀,但是[Android-PullToRefresh](https://github.com/chrisbanes/Android-PullToRefresh) 4 | 已经不在维护了,[android-Ultra-Pull-To-Refresh](https://github.com/liaohuqiu/android-Ultra-Pull-To-Refresh)本身并不支持上拉加载更多,经过一番纠结后决定自己写一个。 5 | 6 | ## 原理 7 | 8 |     无论是下拉刷新还是上拉加载更多,原理都是在内容View(ListView、RecyclerView...)不能下拉或者上划时响应用户的触摸事件,在顶部或者底部显示一个刷新视图,在程序刷新操作完成后再隐藏掉。 9 | 10 | ## 实现 11 | 12 |     既然要在头部和顶部添加刷新视图,我们的控件应该是个ViewGroup,我是直接继承FrameLayout,这个控件的名字叫[NsRefreshLayoutController](https://github.com/xiaolifan/NsRefreshLayout)。然后我们需要定义一些属性,如是否自动触发上拉加载更多、刷新视图中的文字颜色等。 13 | 14 | ### 属性定义 15 | 16 | ```xml 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | ``` 38 | 39 | ### 属性读取 40 | 41 | ```java 42 | /** 43 | * 初始化控件属性 44 | */ 45 | private void initAttrs(Context context, AttributeSet attrs) { 46 | if (getChildCount() > 1) { 47 | throw new RuntimeException("can only have one child"); 48 | } 49 | loadingViewFinalHeight = NrlUtils.dipToPx(context, LOADING_VIEW_FINAL_HEIGHT_DP); 50 | loadingViewOverHeight = loadingViewFinalHeight * 2; 51 | 52 | if (isInEditMode() && attrs == null) { 53 | return; 54 | } 55 | 56 | int resId; 57 | TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.NsRefreshLayout); 58 | Resources resources = context.getResources(); 59 | 60 | //LoadView背景颜色 61 | resId = ta.getResourceId(R.styleable.NsRefreshLayout_load_view_bg_color, -1); 62 | if (resId == -1) { 63 | mLoadViewBgColor = ta.getColor(R.styleable.NsRefreshLayout_load_view_bg_color, 64 | Color.WHITE); 65 | } else { 66 | mLoadViewBgColor = resources.getColor(resId); 67 | } 68 | 69 | //加载文字颜色 70 | resId = ta.getResourceId(R.styleable.NsRefreshLayout_load_text_color, -1); 71 | if (resId == -1) { 72 | mLoadViewTextColor = ta.getColor(R.styleable.NsRefreshLayout_load_text_color, 73 | Color.BLACK); 74 | } else { 75 | mLoadViewTextColor = resources.getColor(resId); 76 | } 77 | 78 | //进度条背景颜色 79 | resId = ta.getResourceId(R.styleable.NsRefreshLayout_progress_bg_color, -1); 80 | if (resId == -1) { 81 | mProgressBgColor = ta.getColor(R.styleable.NsRefreshLayout_progress_bg_color, 82 | Color.WHITE); 83 | } else { 84 | mProgressBgColor = resources.getColor(resId); 85 | } 86 | 87 | //进度条颜色 88 | resId = ta.getResourceId(R.styleable.NsRefreshLayout_progress_bar_color, -1); 89 | if (resId == -1) { 90 | mProgressColor = ta.getColor(R.styleable.NsRefreshLayout_progress_bar_color, 91 | Color.RED); 92 | } else { 93 | mProgressColor = resources.getColor(resId); 94 | } 95 | 96 | //下拉刷新文字描述 97 | resId = ta.getResourceId(R.styleable.NsRefreshLayout_pull_refresh_text, -1); 98 | if (resId == -1) { 99 | mPullRefreshText = ta.getString(R.styleable.NsRefreshLayout_pull_refresh_text); 100 | } else { 101 | mPullRefreshText = resources.getString(resId); 102 | } 103 | 104 | //上拉加载文字描述 105 | resId = ta.getResourceId(R.styleable.NsRefreshLayout_pull_load_text, -1); 106 | if (resId == -1) { 107 | mPullLoadText = ta.getString(R.styleable.NsRefreshLayout_pull_load_text); 108 | } else { 109 | mPullLoadText = resources.getString(resId); 110 | } 111 | 112 | mAutoLoadMore = ta.getBoolean(R.styleable.NsRefreshLayout_auto_load_more, false); 113 | mPullRefreshEnable = ta.getBoolean(R.styleable.NsRefreshLayout_pull_refresh_enable, true); 114 | mPullLoadEnable = ta.getBoolean(R.styleable.NsRefreshLayout_pull_load_enable, true); 115 | 116 | ta.recycle(); 117 | } 118 | ``` 119 | 120 | ### 属性使用 121 | 122 |     在内容View布局完成后(onFinishInflate),根据设置的属性,来确定是否需要添加下拉刷新视图、上拉加载更多视图,以及视图中的文字颜色、进度条颜色等。 123 | 124 | ```java 125 | @Override 126 | protected void onFinishInflate() { 127 | super.onFinishInflate(); 128 | mContentView = getChildAt(0); 129 | setupViews(); 130 | } 131 | 132 | private void setupViews() { 133 | //下拉刷新视图 134 | LayoutParams lp; 135 | if (mPullRefreshEnable) { 136 | lp = new LayoutParams(LayoutParams.MATCH_PARENT, 0); 137 | headerView = new LoadView(getContext()); 138 | headerView.setLoadText(TextUtils.isEmpty(mPullRefreshText) ? 139 | getContext().getString(R.string.default_pull_refresh_text) : mPullRefreshText); 140 | headerView.setStartEndTrim(0, 0.75f); 141 | headerView.setBackgroundColor(mLoadViewBgColor); 142 | headerView.setLoadTextColor(mLoadViewTextColor); 143 | headerView.setProgressBgColor(mProgressBgColor); 144 | headerView.setProgressColor(mProgressColor); 145 | addView(headerView, lp); 146 | } 147 | 148 | if (mPullLoadEnable) { 149 | //上拉加载更多视图 150 | lp = new LayoutParams(LayoutParams.MATCH_PARENT, 0); 151 | lp.gravity = Gravity.BOTTOM; 152 | footerView = new LoadView(getContext()); 153 | footerView.setLoadText(TextUtils.isEmpty(mPullLoadText) ? 154 | getContext().getString(R.string.default_pull_load_text) : mPullLoadText); 155 | footerView.setStartEndTrim(0.5f, 1.25f); 156 | footerView.setBackgroundColor(mLoadViewBgColor); 157 | footerView.setLoadTextColor(mLoadViewTextColor); 158 | footerView.setProgressBgColor(mProgressBgColor); 159 | footerView.setProgressColor(mProgressColor); 160 | addView(footerView, lp); 161 | } 162 | } 163 | ``` 164 | 165 | ### 动态响应用户配置变化 166 | 167 |     有这样一种需求,一个列表分页加载,每一页10条,如果在上拉加载更多后只返回8条,说明已经没有更多数据了,所以在列表达到底部,用户再次上划时就不需要触发上拉加载更多了。基于这种需求,我设计了一个接口NsRefreshLayoutController。 168 | 169 | ```java 170 | public interface NsRefreshLayoutController { 171 | /** 172 | * 当前下拉刷新是否可用 173 | */ 174 | boolean isPullRefreshEnable(); 175 | 176 | /** 177 | * 当前上拉加载是否可用,比如列表已无更多数据,可禁用上拉加载功能 178 | */ 179 | boolean isPullLoadEnable(); 180 | } 181 | ``` 182 | 使用时,实现这个接口,根据当前数据的情况返回True或者False启用或者禁用两个功能了。控件内部,我们在用户每次触发触摸事件的时候获取接口返回值。 183 | 184 | ```java 185 | @Override 186 | public boolean onInterceptTouchEvent(MotionEvent ev) { 187 | if (refreshLayoutController != null) { 188 | mPullRefreshEnable = refreshLayoutController.isPullRefreshEnable(); 189 | mPullLoadEnable = refreshLayoutController.isPullLoadEnable(); 190 | } 191 | return super.onInterceptTouchEvent(ev); 192 | } 193 | ``` 194 | 195 | ### 处理Touch事件 196 | 197 |     我们需要做到对Touch事件的处理不影响内容视图的功能,所以我们只处理Touch事件,不消耗Touch事件,一个合适的回调很重要,找来找去我选择了dispatchTouchEvent,官方文档对这个函数的描述如下: 198 | 199 | 200 | 201 |     处理Touch事件的流程如下,ACTION\_DOWN、ACTION\_MOVE时记录Touch的位置,ACTION\_MOVE时用当前Touch的位置减去上次DOWN或者MOVE的位置,得到手指滑动的距离,用这个距离来控制内容视图、刷新视图的显示位置,当达到触发刷新的位置后,提示用户松手触发刷新,用户松手后开始刷新动画并通知程序开始刷新。代码如下: 202 | 203 | ```java 204 | @Override 205 | public boolean dispatchTouchEvent(MotionEvent event) { 206 | if (!mPullRefreshEnable && !mPullLoadEnable) { 207 | return super.dispatchTouchEvent(event); 208 | } 209 | 210 | if (isRefreshing) { 211 | return super.dispatchTouchEvent(event); 212 | } 213 | 214 | switch (event.getActionMasked()) { 215 | case MotionEvent.ACTION_DOWN: { 216 | preY = event.getY(); 217 | preX = event.getX(); 218 | break; 219 | } 220 | 221 | case MotionEvent.ACTION_MOVE: { 222 | float currentY = event.getY(); 223 | float currentX = event.getX(); 224 | float dy = currentY - preY; 225 | float dx = currentX - preX; 226 | preY = currentY; 227 | preX = currentX; 228 | if (!actionDetermined) { 229 | //判断是下拉刷新还是上拉加载更多 230 | if (dy > 0 && !canChildScrollUp() && mPullRefreshEnable) { 231 | mCurrentAction = ACTION_PULL_DOWN_REFRESH; 232 | actionDetermined = true; 233 | } else if (dy < 0 && !canChildScrollDown() && mPullLoadEnable) { 234 | mCurrentAction = ACTION_PULL_UP_LOAD_MORE; 235 | actionDetermined = true; 236 | } 237 | } 238 | handleScroll(dy); 239 | observerArriveBottom(); 240 | break; 241 | } 242 | 243 | case MotionEvent.ACTION_UP: 244 | case MotionEvent.ACTION_CANCEL: { 245 | //用户松手后需要判断当前的滑动距离是否满足触发刷新的条件 246 | if (releaseTouch()) { 247 | MotionEvent cancelEvent = MotionEvent.obtain(event); 248 | cancelEvent.setAction(MotionEvent.ACTION_CANCEL); 249 | return super.dispatchTouchEvent(cancelEvent); 250 | } 251 | break; 252 | } 253 | } 254 | 255 | return super.dispatchTouchEvent(event); 256 | } 257 | 258 | /** 259 | * 处理滚动 260 | */ 261 | private boolean handleScroll(float distanceY) { 262 | if (!canChildScrollUp() && mCurrentAction == ACTION_PULL_DOWN_REFRESH && 263 | mPullRefreshEnable) { 264 | //下拉刷新 265 | LayoutParams lp = (LayoutParams) headerView.getLayoutParams(); 266 | lp.height += distanceY; 267 | if (lp.height < 0) { 268 | lp.height = 0; 269 | } else if (lp.height > loadingViewOverHeight) { 270 | lp.height = (int) loadingViewOverHeight; 271 | } 272 | headerView.setLayoutParams(lp); 273 | if (lp.height < loadingViewOverHeight) { 274 | headerView.setLoadText(TextUtils.isEmpty(mPullRefreshText) ? 275 | getContext().getString(R.string.default_pull_refresh_text) : mPullRefreshText); 276 | } else { 277 | headerView.setLoadText(getContext().getString(R.string.release_to_refresh)); 278 | } 279 | headerView.setProgressRotation(lp.height / loadingViewOverHeight); 280 | adjustContentViewHeight(lp.height); 281 | return true; 282 | 283 | } else if (!canChildScrollDown() && mCurrentAction == ACTION_PULL_UP_LOAD_MORE && mPullLoadEnable) { 284 | //上拉加载更多 285 | LayoutParams lp = (LayoutParams) footerView.getLayoutParams(); 286 | lp.height -= distanceY; 287 | if (lp.height < 0) { 288 | lp.height = 0; 289 | } else if (lp.height > loadingViewOverHeight) { 290 | lp.height = (int) loadingViewOverHeight; 291 | } 292 | footerView.setLayoutParams(lp); 293 | if (lp.height < loadingViewOverHeight) { 294 | footerView.setLoadText(TextUtils.isEmpty(mPullLoadText) ? 295 | getContext().getString(R.string.default_pull_load_text) : mPullLoadText); 296 | } else { 297 | footerView.setLoadText(getContext().getString(R.string.release_to_load)); 298 | } 299 | footerView.setProgressRotation(lp.height / loadingViewOverHeight); 300 | adjustContentViewHeight(-lp.height); 301 | return true; 302 | } 303 | return false; 304 | } 305 | 306 | private void adjustContentViewHeight(float h) { 307 | mContentView.setTranslationY(h); 308 | //下面的方式可以看到完整内容,但是有掉帧现象 309 | /*if (mCurrentAction == ACTION_PULL_DOWN_REFRESH) { 310 | mContentView.setTranslationY(h); 311 | } 312 | LayoutParams lp = (LayoutParams) mContentView.getLayoutParams(); 313 | lp.height = (int) (getMeasuredHeight() - Math.abs(h)); 314 | mContentView.setLayoutParams(lp);*/ 315 | 316 | } 317 | 318 | private boolean releaseTouch() { 319 | boolean result = false; 320 | LayoutParams lp; 321 | if (mPullRefreshEnable && mCurrentAction == ACTION_PULL_DOWN_REFRESH) { 322 | lp = (LayoutParams) headerView.getLayoutParams(); 323 | if (lp.height >= loadingViewOverHeight) { 324 | //触发下拉刷新 325 | startPullDownRefresh(lp.height); 326 | result = true; 327 | } else if (lp.height > 0) { 328 | //未满足下拉刷新触发条件,重置状态 329 | resetPullDownRefresh(lp.height); 330 | result = lp.height >= CLICK_TOUCH_DEVIATION; 331 | } else { 332 | resetPullRefreshState(); 333 | } 334 | } 335 | 336 | if (mPullLoadEnable && mCurrentAction == ACTION_PULL_UP_LOAD_MORE) { 337 | lp = (LayoutParams) footerView.getLayoutParams(); 338 | if (lp.height >= loadingViewOverHeight) { 339 | //触发上拉加载更多 340 | startPullUpLoadMore(lp.height); 341 | result = true; 342 | } else if (lp.height > 0) { 343 | //未满足上拉加载更多触发条件,重置状态 344 | resetPullUpLoadMore(lp.height); 345 | result = lp.height >= CLICK_TOUCH_DEVIATION; 346 | } else { 347 | resetPullLoadState(); 348 | } 349 | } 350 | return result; 351 | } 352 | 353 | private void startPullDownRefresh(int headerViewHeight) { 354 | isRefreshing = true; 355 | ValueAnimator animator = ValueAnimator.ofFloat(headerViewHeight, loadingViewFinalHeight); 356 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 357 | @Override 358 | public void onAnimationUpdate(ValueAnimator animation) { 359 | LayoutParams lp = (LayoutParams) headerView.getLayoutParams(); 360 | lp.height = (int) ((Float) animation.getAnimatedValue()).floatValue(); 361 | headerView.setLayoutParams(lp); 362 | adjustContentViewHeight(lp.height); 363 | } 364 | }); 365 | animator.addListener(new SimpleAnimatorListener() { 366 | @Override 367 | public void onAnimationEnd(Animator animation) { 368 | headerView.start(); 369 | headerView.setLoadText(getContext().getString(R.string.refresh_text)); 370 | 371 | if (refreshLayoutListener != null) { 372 | refreshLayoutListener.onRefresh(); 373 | } 374 | } 375 | }); 376 | animator.setDuration(300); 377 | animator.start(); 378 | } 379 | 380 | /** 381 | * 重置下拉刷新状态 382 | * 383 | * @param headerViewHeight 当前下拉刷新视图的高度 384 | */ 385 | private void resetPullDownRefresh(int headerViewHeight) { 386 | headerView.stop(); 387 | //headerView.setStartEndTrim(0, 0.75f); 388 | ValueAnimator animator = ValueAnimator.ofFloat(headerViewHeight, 0); 389 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 390 | @Override 391 | public void onAnimationUpdate(ValueAnimator animation) { 392 | LayoutParams lp = (LayoutParams) headerView.getLayoutParams(); 393 | lp.height = (int) ((Float) animation.getAnimatedValue()).floatValue(); 394 | headerView.setLayoutParams(lp); 395 | adjustContentViewHeight(lp.height); 396 | } 397 | }); 398 | animator.addListener(new SimpleAnimatorListener() { 399 | @Override 400 | public void onAnimationEnd(Animator animation) { 401 | resetPullRefreshState(); 402 | 403 | } 404 | }); 405 | animator.setDuration(300); 406 | animator.start(); 407 | } 408 | 409 | private void resetPullRefreshState() { 410 | //重置动画结束才算完全完成刷新动作 411 | isRefreshing = false; 412 | actionDetermined = false; 413 | mCurrentAction = -1; 414 | headerView.setLoadText(TextUtils.isEmpty(mPullRefreshText) ? 415 | getContext().getString(R.string.default_pull_refresh_text) : mPullRefreshText); 416 | } 417 | 418 | private void startPullUpLoadMore(int headerViewHeight) { 419 | isRefreshing = true; 420 | ValueAnimator animator = ValueAnimator.ofFloat(headerViewHeight, loadingViewFinalHeight); 421 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 422 | @Override 423 | public void onAnimationUpdate(ValueAnimator animation) { 424 | LayoutParams lp = (LayoutParams) footerView.getLayoutParams(); 425 | lp.height = (int) ((Float) animation.getAnimatedValue()).floatValue(); 426 | footerView.setLayoutParams(lp); 427 | adjustContentViewHeight(-lp.height); 428 | } 429 | }); 430 | animator.addListener(new SimpleAnimatorListener() { 431 | @Override 432 | public void onAnimationEnd(Animator animation) { 433 | footerView.start(); 434 | footerView.setLoadText(getContext().getString(R.string.load_text)); 435 | 436 | if (refreshLayoutListener != null) { 437 | refreshLayoutListener.onLoadMore(); 438 | } 439 | } 440 | }); 441 | animator.setDuration(300); 442 | animator.start(); 443 | } 444 | 445 | /** 446 | * 重置下拉刷新状态 447 | * 448 | * @param headerViewHeight 当前下拉刷新视图的高度 449 | */ 450 | private void resetPullUpLoadMore(int headerViewHeight) { 451 | footerView.stop(); 452 | //footerView.setStartEndTrim(0.5f, 1.25f); 453 | ValueAnimator animator = ValueAnimator.ofFloat(headerViewHeight, 0); 454 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 455 | @Override 456 | public void onAnimationUpdate(ValueAnimator animation) { 457 | LayoutParams lp = (LayoutParams) footerView.getLayoutParams(); 458 | lp.height = (int) ((Float) animation.getAnimatedValue()).floatValue(); 459 | footerView.setLayoutParams(lp); 460 | adjustContentViewHeight(-lp.height); 461 | } 462 | }); 463 | animator.addListener(new SimpleAnimatorListener() { 464 | @Override 465 | public void onAnimationEnd(Animator animation) { 466 | resetPullLoadState(); 467 | 468 | } 469 | }); 470 | animator.setDuration(300); 471 | animator.start(); 472 | } 473 | 474 | private void resetPullLoadState() { 475 | //重置动画结束才算完全完成刷新动作 476 | isRefreshing = false; 477 | actionDetermined = false; 478 | mCurrentAction = -1; 479 | footerView.setLoadText(TextUtils.isEmpty(mPullLoadText) ? 480 | getContext().getString(R.string.default_pull_load_text) : mPullLoadText); 481 | } 482 | 483 | /** 484 | * @return 子视图是否可以下拉 485 | */ 486 | public boolean canChildScrollUp() { 487 | if (mContentView == null) { 488 | return false; 489 | } 490 | if (Build.VERSION.SDK_INT < 14) { 491 | if (mContentView instanceof AbsListView) { 492 | final AbsListView absListView = (AbsListView) mContentView; 493 | return absListView.getChildCount() > 0 494 | && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0) 495 | .getTop() < absListView.getPaddingTop()); 496 | } else { 497 | return ViewCompat.canScrollVertically(mContentView, -1) || mContentView.getScrollY() > 0; 498 | } 499 | } else { 500 | return ViewCompat.canScrollVertically(mContentView, -1); 501 | } 502 | } 503 | 504 | /** 505 | * @return 子视图是否可以上划 506 | */ 507 | public boolean canChildScrollDown() { 508 | if (mContentView == null) { 509 | return false; 510 | } 511 | if (Build.VERSION.SDK_INT < 14) { 512 | if (mContentView instanceof AbsListView) { 513 | final AbsListView absListView = (AbsListView) mContentView; 514 | if (absListView.getChildCount() > 0) { 515 | int lastChildBottom = absListView.getChildAt(absListView.getChildCount() - 1) 516 | .getBottom(); 517 | return absListView.getLastVisiblePosition() == absListView.getAdapter().getCount() - 1 518 | && lastChildBottom <= absListView.getMeasuredHeight(); 519 | } else { 520 | return false; 521 | } 522 | 523 | } else { 524 | return ViewCompat.canScrollVertically(mContentView, 1) || mContentView.getScrollY() > 0; 525 | } 526 | } else { 527 | return ViewCompat.canScrollVertically(mContentView, 1); 528 | } 529 | } 530 | 531 | public void setRefreshLayoutListener(NsRefreshLayoutListener refreshLayoutListener) { 532 | this.refreshLayoutListener = refreshLayoutListener; 533 | } 534 | ``` 535 | 536 |     上面代码中有一个变量CLICK\_TOUCH\_DEVIATION,这个变量表示对用户点击事件的容错值,用户进行点击动作时,会产生很小的滑动距离,如果不做容错处理会出现刷新视图抖动出现的问题。 537 | 538 |     另外还有一个observerArriveBottom(); 这个函数就是处理自动加载更多的关键。该函数在Touch事件产生滑动距离后,采取类似轮询的机制,判断滑动是否已经停止,滑动事件停止后,根据内容控件当前状态、用户配置来确定是否触发加载更多事件。代码如下: 539 | 540 | ```java 541 | private void observerArriveBottom() { 542 | if (isRefreshing || !mAutoLoadMore || !mPullLoadEnable) { 543 | return; 544 | } 545 | mContentView.getViewTreeObserver().addOnScrollChangedListener( 546 | new ViewTreeObserver.OnScrollChangedListener() { 547 | 548 | @Override 549 | public void onScrollChanged() { 550 | mContentView.removeCallbacks(flingRunnable); 551 | mContentView.postDelayed(flingRunnable, 6); 552 | } 553 | }); 554 | } 555 | 556 | private Runnable flingRunnable = new Runnable() { 557 | @Override 558 | public void run() { 559 | if (isRefreshing || !mAutoLoadMore || !mPullLoadEnable) { 560 | return; 561 | } 562 | 563 | if (!canChildScrollDown()) { 564 | mCurrentAction = ACTION_PULL_UP_LOAD_MORE; 565 | isRefreshing = true; 566 | startPullUpLoadMore(0); 567 | } 568 | } 569 | }; 570 | ``` 571 | 572 | ### 对外接口 573 | 574 | ```java 575 | public interface NsRefreshLayoutListener { 576 | void onRefresh(); 577 | 578 | void onLoadMore(); 579 | } 580 | ``` 581 | 582 | ## 搞定 583 | 584 |     整个控件实现就是这样的,代码我已经放到GitHub上了,欢迎大家拍砖。 585 | https://github.com/xiaolifan/NsRefreshLayout 586 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':nrl' 2 | --------------------------------------------------------------------------------