├── .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 |
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 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
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 | 
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 |
--------------------------------------------------------------------------------