├── .gitignore ├── .idea ├── .name ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── gradle.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── example │ │ └── administrator │ │ └── suitablerecyclerview │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── asdzheng │ │ │ └── suitedrecyclerview │ │ │ ├── MyApplication.java │ │ │ ├── bean │ │ │ ├── BaseDto.java │ │ │ ├── BaseResponse.java │ │ │ ├── ChannelBean.java │ │ │ ├── NewChannelInfoDetailChannelDto.java │ │ │ ├── NewChannelInfoDetailDto.java │ │ │ ├── NewChannelInfoDetailUserDto.java │ │ │ ├── NewChannelInfoDto.java │ │ │ ├── UserChannelInfoDto.java │ │ │ └── UserInfo.java │ │ │ ├── http │ │ │ ├── GsonRequest.java │ │ │ ├── RequestHelper.java │ │ │ └── UrlUtil.java │ │ │ ├── ui │ │ │ ├── activity │ │ │ │ ├── BaseActivity.java │ │ │ │ ├── PhotoActivity.java │ │ │ │ └── PhotoDetailActivity.java │ │ │ ├── adapter │ │ │ │ └── PhotosAdapter.java │ │ │ └── view │ │ │ │ ├── MaterialProgressBar.java │ │ │ │ ├── SuitedImageView.java │ │ │ │ └── waveswiperefreshlayout │ │ │ │ ├── CircleImageView.java │ │ │ │ ├── MaterialProgressDrawable.java │ │ │ │ ├── SolidBelowWave.java │ │ │ │ ├── WaterWave.java │ │ │ │ ├── Wave.java │ │ │ │ └── WaveSwipeRefreshLayout.java │ │ │ └── utils │ │ │ ├── ConfigConstants.java │ │ │ ├── DisplayUtils.java │ │ │ ├── LogUtil.java │ │ │ └── transition │ │ │ ├── ActivityTransitionEnterHelper.java │ │ │ └── ActivityTransitionExitHelper.java │ └── res │ │ ├── layout │ │ ├── activity_channel_photo.xml │ │ └── activity_channel_photo_detail.xml │ │ ├── menu │ │ └── menu_main.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-v19 │ │ ├── dimens.xml │ │ └── styles.xml │ │ ├── values-v21 │ │ └── styles.xml │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── example │ └── administrator │ └── suitablerecyclerview │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── record.gif ├── screenshot.png ├── settings.gradle └── suitedrecyclerview ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src └── main ├── AndroidManifest.xml ├── java └── com │ └── asdzheng │ └── layoutmanager │ ├── LogUtil.java │ ├── Size.java │ ├── SizeCaculator.java │ ├── SuitUrlUtil.java │ ├── SuitedItemDecoration.java │ └── SuitedLayoutManager.java └── res └── values └── strings.xml /.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 | SuitedRecyclerView -------------------------------------------------------------------------------- /.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/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 效果图 2 | ![](https://github.com/asdzheng/SuitedRecyclerView/blob/master/screenshot.png) 3 | 4 | ## 最基本的用法 5 | - RecyclerView 的设置 6 | 7 | mPhotosAdapter = new PhotosAdapter(list, this); 8 | recyclerView.setAdapter(mPhotosAdapter); 9 | //自定义LayoutManager 10 | SuitedLayoutManager layoutManager = new SuitedLayoutManager(mPhotosAdapter); 11 | recyclerView.setLayoutManager(layoutManager); 12 | //设置最大的图片显示高度,默认为600px 13 | layoutManager.setMaxRowHeight(getResources().getDisplayMetrics().heightPixels / 3); 14 | //设置Item之间的空隙 15 | recyclerView.addItemDecoration(new SuitedItemDecoration(DisplayUtils.dpToPx(4.0f, this))); 16 | 17 | - PhotosAdapter需要实现SizeCaculator.SizeCalculatorDelegate接口里的aspectRatioForIndex(int position)方法,返回图片宽高比 18 | 19 | 20 | @Override 21 | public double aspectRatioForIndex(int position) { 22 | if (position < getItemCount()) { 23 | PhotoInfo info = mPhotos.get(position); 24 | //如果你的图片url是以_w750_h750.jpg这样的格式结尾,可以用SuitUrlUtil这个工具类获取它的宽高比 25 | double ratio = SuitUrlUtil.getAspectRadioFromUrl(info.photo); 26 | return ratio; 27 | } 28 | return 1.0; 29 | } 30 | 31 | ##内存的优化使用 32 | 33 | 由于这个LayoutManager会根据自身宽高和相邻图片宽高的比率最后计算出每张图片的大小,所以每张图片大小几乎都不一样,recyclerView里View的复用情况很少,这种情况下如果不对图片进行等比缩小和对滑动做优化,将会造成严重的卡顿。 34 | 35 | 在我个人实践的过程中呢,有两处优化的点可以让图片滑动时更为流畅。 36 | 37 | - 在ImageView确定Width和Height后,再进行网络请求,具体可参考library里的SuitImageView的做法 38 | 39 | @Override 40 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 41 | super.onSizeChanged(w, h, oldw, oldh); 42 | if(w != 0 && h != 0) { 43 | Picasso.with(getContext()).load(mPhoto).tag(getContext()).resize(w,h).into(this); 44 | } 45 | 46 | - 滑动过程中,中断图片请求,以使用Picasso框架请求图片为例: 47 | 48 | recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { 49 | @Override 50 | public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 51 | super.onScrollStateChanged(recyclerView, newState); 52 | if (newState == RecyclerView.SCROLL_STATE_IDLE) { 53 | Picasso.with(PhotoActivity.this).resumeTag(PhotoActivity.this); 54 | } else if (newState == RecyclerView.SCROLL_STATE_DRAGGING) { 55 | Picasso.with(PhotoActivity.this).pauseTag(PhotoActivity.this); 56 | } else if (newState == RecyclerView.SCROLL_STATE_SETTLING) { 57 | Picasso.with(PhotoActivity.this).pauseTag(PhotoActivity.this); 58 | } 59 | } 60 | }); 61 | 62 | ##Demo的滑动效果 63 | 64 | ![](https://github.com/asdzheng/SuitedRecyclerView/blob/master/record.gif) 65 | 66 | 67 | 下拉刷新上拉加载更多框架修改自[WaveSwipeRefreshLayout](https://github.com/fishCoder/WaveSwipeRefreshLayout)开源库 68 | 69 | 70 | ##License Apache 2.0 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /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.asdzheng.suitedrecyclerview" 9 | minSdkVersion 16 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(dir: 'libs', include: ['*.jar']) 24 | compile 'com.android.support:design:23.1.1' 25 | compile 'com.mcxiaoke.volley:library:1.0.19' 26 | compile 'com.google.code.gson:gson:2.2.+' 27 | compile 'com.squareup.picasso:picasso:2.5.2' 28 | compile 'com.squareup.okhttp:okhttp:2.7.0' 29 | compile 'jp.wasabeef:recyclerview-animators:2.1.0' 30 | compile 'com.jakewharton:butterknife:7.0.1' 31 | compile 'com.commit451:PhotoView:1.2.4' 32 | compile project(':suitedrecyclerview') 33 | } 34 | -------------------------------------------------------------------------------- /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 G:\Program Files\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/example/administrator/suitablerecyclerview/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.example.administrator.suitablerecyclerview; 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 | 6 | 7 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/asdzheng/suitedrecyclerview/MyApplication.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.suitedrecyclerview; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import android.graphics.Bitmap; 6 | 7 | import com.asdzheng.suitedrecyclerview.utils.ConfigConstants; 8 | import com.squareup.picasso.LruCache; 9 | import com.squareup.picasso.OkHttpDownloader; 10 | import com.squareup.picasso.Picasso; 11 | 12 | 13 | /** 14 | * Created by asdzheng on 2015/12/10. 15 | */ 16 | public class MyApplication extends Application { 17 | 18 | public static Context context; 19 | 20 | @Override 21 | public void onCreate() { 22 | super.onCreate(); 23 | context = this.getApplicationContext(); 24 | configure(); 25 | } 26 | 27 | public void configure() { 28 | Picasso.setSingletonInstance(new Picasso.Builder(context.getApplicationContext()). 29 | downloader(new OkHttpDownloader(context.getApplicationContext(), ConfigConstants.MAX_DISK_CACHE_SIZE)). 30 | memoryCache(new LruCache(ConfigConstants.MAX_MEMORY_CACHE_SIZE)).defaultBitmapConfig(Bitmap.Config.RGB_565). 31 | build()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/asdzheng/suitedrecyclerview/bean/BaseDto.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.suitedrecyclerview.bean; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * Created by asdzheng on 2015/12/10. 7 | */ 8 | public class BaseDto implements Serializable 9 | { 10 | private static final long serialVersionUID = 1L; 11 | 12 | public boolean check() { 13 | return true; 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/asdzheng/suitedrecyclerview/bean/BaseResponse.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.suitedrecyclerview.bean; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * 登录接口 7 | * @author hq3 8 | * 9 | */ 10 | 11 | public class BaseResponse implements Serializable{ 12 | 13 | private static final long serialVersionUID = 1L; 14 | private String used_time; 15 | private int code; 16 | 17 | public String getUsed_time() { 18 | return used_time; 19 | } 20 | 21 | public void setUsed_time(String used_time) { 22 | this.used_time = used_time; 23 | } 24 | 25 | public int getCode() { 26 | return code; 27 | } 28 | 29 | public void setCode(int code) { 30 | this.code = code; 31 | } 32 | 33 | public static long getSerialversionuid() { 34 | return serialVersionUID; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/asdzheng/suitedrecyclerview/bean/ChannelBean.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.suitedrecyclerview.bean; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * Created by Administrator on 2016-1-18. 7 | */ 8 | public class ChannelBean implements Serializable{ 9 | 10 | private String channelName; 11 | private String channelPhoto; 12 | private String channelUrl; 13 | 14 | public String getChannelUrl() { 15 | return channelUrl; 16 | } 17 | 18 | public void setChannelUrl(String channelUrl) { 19 | this.channelUrl = channelUrl; 20 | } 21 | 22 | public String getChannelName() { 23 | return channelName; 24 | } 25 | 26 | public void setChannelName(String channelName) { 27 | this.channelName = channelName; 28 | } 29 | 30 | public String getChannelPhoto() { 31 | return channelPhoto; 32 | } 33 | 34 | public void setChannelPhoto(String channelPhoto) { 35 | this.channelPhoto = channelPhoto; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/asdzheng/suitedrecyclerview/bean/NewChannelInfoDetailChannelDto.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.suitedrecyclerview.bean; 2 | 3 | /** 4 | * Created by asdzheng on 2015/12/10. 5 | */ 6 | public class NewChannelInfoDetailChannelDto extends BaseDto 7 | { 8 | private static final long serialVersionUID = 3914531728122301893L; 9 | private String author; 10 | private String book_times; 11 | private int cate; 12 | private String create_at; 13 | private String description; 14 | private String icon; 15 | private long id; 16 | public int is_private; 17 | private int mode; 18 | private String name; 19 | private String times; 20 | private String user_id; 21 | 22 | public String getAuthor() { 23 | return this.author; 24 | } 25 | 26 | public String getBook_times() { 27 | return this.book_times; 28 | } 29 | 30 | public int getCate() { 31 | return this.cate; 32 | } 33 | 34 | public String getCreate_at() { 35 | return this.create_at; 36 | } 37 | 38 | public String getDescription() { 39 | return this.description; 40 | } 41 | 42 | public String getIcon() { 43 | return this.icon; 44 | } 45 | 46 | public long getId() { 47 | return this.id; 48 | } 49 | 50 | public int getMode() { 51 | return this.mode; 52 | } 53 | 54 | public String getName() { 55 | return this.name; 56 | } 57 | 58 | public String getTimes() { 59 | return this.times; 60 | } 61 | 62 | public String getUser_id() { 63 | return this.user_id; 64 | } 65 | 66 | public void setAuthor(final String author) { 67 | this.author = author; 68 | } 69 | 70 | public void setBook_times(final String book_times) { 71 | this.book_times = book_times; 72 | } 73 | 74 | public void setCate(final int cate) { 75 | this.cate = cate; 76 | } 77 | 78 | public void setCreate_at(final String create_at) { 79 | this.create_at = create_at; 80 | } 81 | 82 | public void setDescription(final String description) { 83 | this.description = description; 84 | } 85 | 86 | public void setIcon(final String icon) { 87 | this.icon = icon; 88 | } 89 | 90 | public void setId(final long id) { 91 | this.id = id; 92 | } 93 | 94 | public void setMode(final int mode) { 95 | this.mode = mode; 96 | } 97 | 98 | public void setName(final String name) { 99 | this.name = name; 100 | } 101 | 102 | public void setTimes(final String times) { 103 | this.times = times; 104 | } 105 | 106 | public void setUser_id(final String user_id) { 107 | this.user_id = user_id; 108 | } 109 | } 110 | 111 | -------------------------------------------------------------------------------- /app/src/main/java/com/asdzheng/suitedrecyclerview/bean/NewChannelInfoDetailDto.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.suitedrecyclerview.bean; 2 | 3 | /** 4 | * Created by asdzheng on 2015/12/10. 5 | */ 6 | public class NewChannelInfoDetailDto { 7 | public static final int TYPE_ELLIPSIS = 1; 8 | public static final int TYPE_EXPENDED = 0; 9 | public NewChannelInfoDetailChannelDto channel; 10 | public transient int collects; 11 | public String created_at; 12 | public int destroyed; 13 | public String id; 14 | public transient int isCollect; 15 | public transient Boolean isExpanded; 16 | public int is_liked; 17 | public int likes; 18 | public String photo; 19 | public transient int rankListExpendedType; 20 | public String txt; 21 | public NewChannelInfoDetailUserDto user; 22 | public int views; 23 | 24 | public NewChannelInfoDetailDto() { 25 | this.isExpanded = false; 26 | this.isCollect = 0; 27 | this.rankListExpendedType = 1; 28 | } 29 | } 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/asdzheng/suitedrecyclerview/bean/NewChannelInfoDetailUserDto.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.suitedrecyclerview.bean; 2 | 3 | /** 4 | * Created by asdzheng on 2015/12/10. 5 | */ 6 | public class NewChannelInfoDetailUserDto extends BaseDto 7 | { 8 | private String avatar; 9 | private String id; 10 | public UserMetaDto meta; 11 | private String username; 12 | 13 | public String getAvatar() { 14 | return this.avatar; 15 | } 16 | 17 | public String getId() { 18 | return this.id; 19 | } 20 | 21 | public String getUsername() { 22 | return this.username; 23 | } 24 | 25 | public void setAvatar(final String avatar) { 26 | this.avatar = avatar; 27 | } 28 | 29 | public void setId(final String id) { 30 | this.id = id; 31 | } 32 | 33 | public void setUsername(final String username) { 34 | this.username = username; 35 | } 36 | 37 | public static class UserMetaDto 38 | { 39 | public long firstblood; 40 | 41 | public UserMetaDto(final long firstblood) { 42 | this.firstblood = -1L; 43 | this.firstblood = firstblood; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/asdzheng/suitedrecyclerview/bean/NewChannelInfoDto.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.suitedrecyclerview.bean; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Created by asdzheng on 2015/12/24. 7 | */ 8 | public class NewChannelInfoDto extends BaseResponse { 9 | 10 | private ChannelInfo data; 11 | 12 | public ChannelInfo getData() { 13 | return data; 14 | } 15 | 16 | public void setData(ChannelInfo data) { 17 | this.data = data; 18 | } 19 | 20 | public static class ChannelInfo { 21 | private int count; 22 | private String next; 23 | private String prev; 24 | private List results; 25 | 26 | public int getCount() { 27 | return this.count; 28 | } 29 | 30 | public String getNext() { 31 | return this.next; 32 | } 33 | 34 | public String getPrev() { 35 | return this.prev; 36 | } 37 | 38 | public List getResults() { 39 | return this.results; 40 | } 41 | 42 | public void setCount(final int count) { 43 | this.count = count; 44 | } 45 | 46 | public void setNext(final String next) { 47 | this.next = next; 48 | } 49 | 50 | public void setPrev(final String prev) { 51 | this.prev = prev; 52 | } 53 | 54 | public void setResults(final List results) { 55 | this.results = results; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/com/asdzheng/suitedrecyclerview/bean/UserChannelInfoDto.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.suitedrecyclerview.bean; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Created by asdzheng on 2015/12/10. 7 | */ 8 | public class UserChannelInfoDto extends BaseDto 9 | {private static final long serialVersionUID = 6590658010445846280L; 10 | private int count; 11 | private int infos; 12 | private String next; 13 | private String prev; 14 | private List results; 15 | 16 | public int getCount() { 17 | return this.count; 18 | } 19 | 20 | public int getInfos() { 21 | return this.infos; 22 | } 23 | 24 | public String getNext() { 25 | return this.next; 26 | } 27 | 28 | public String getPrev() { 29 | return this.prev; 30 | } 31 | 32 | public List getResults() { 33 | return this.results; 34 | } 35 | 36 | public void setCount(final int count) { 37 | this.count = count; 38 | } 39 | 40 | public void setInfos(final int infos) { 41 | this.infos = infos; 42 | } 43 | 44 | public void setNext(final String next) { 45 | this.next = next; 46 | } 47 | 48 | public void setPrev(final String prev) { 49 | this.prev = prev; 50 | } 51 | 52 | public void setResults(final List results) { 53 | this.results = results; 54 | } 55 | } -------------------------------------------------------------------------------- /app/src/main/java/com/asdzheng/suitedrecyclerview/bean/UserInfo.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.suitedrecyclerview.bean; 2 | 3 | /** 4 | * Created by asdzheng on 2015/12/10. 5 | */ 6 | public class UserInfo { 7 | 8 | public String code; 9 | public UserChannelInfoDto data; 10 | public String used_time; 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/asdzheng/suitedrecyclerview/http/GsonRequest.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.suitedrecyclerview.http; 2 | 3 | import android.util.Log; 4 | 5 | import com.android.volley.AuthFailureError; 6 | import com.android.volley.NetworkResponse; 7 | import com.android.volley.ParseError; 8 | import com.android.volley.Request; 9 | import com.android.volley.Response; 10 | import com.android.volley.toolbox.HttpHeaderParser; 11 | import com.asdzheng.suitedrecyclerview.utils.LogUtil; 12 | import com.google.gson.Gson; 13 | 14 | import java.io.UnsupportedEncodingException; 15 | import java.util.HashMap; 16 | import java.util.Map; 17 | 18 | /** 19 | * Created by asdzheng on 2015/10/31. 20 | */ 21 | public class GsonRequest extends Request { 22 | private final String TAG = this.getClass().getSimpleName(); 23 | 24 | private final Response.Listener mListener; 25 | 26 | private Gson mGson; 27 | 28 | private Class mClass; 29 | 30 | public GsonRequest(int method, String url, Class mClass, Response.Listener listener, Response.ErrorListener errorListener) { 31 | super(method, url, errorListener); 32 | LogUtil.i(TAG, url); 33 | mListener = listener; 34 | mGson = new Gson(); 35 | this.mClass = mClass; 36 | } 37 | 38 | @Override 39 | protected Response parseNetworkResponse(NetworkResponse response) { 40 | try { 41 | 42 | String jsonString = new String(response.data, 43 | HttpHeaderParser.parseCharset(response.headers)); 44 | Log.w(TAG, "Json String = " + jsonString); 45 | return Response.success(mGson.fromJson(jsonString, mClass), 46 | HttpHeaderParser.parseCacheHeaders(response)); 47 | 48 | } catch (UnsupportedEncodingException e) { 49 | return Response.error(new ParseError(e)); 50 | } 51 | } 52 | 53 | // GET /user/4276367/channel/1033563/senses HTTP/1.1 54 | // Authorization: Token 1448701921-415909295e2e7ab9-1419308 55 | // User-Agent: same/313 56 | // X-same-Client-Version: 313 57 | // machine: android|301|android4.4.2|SM701|864516020267118|1080|1920 58 | // extrainfo: offical 59 | // Connection: keep-alive 60 | // x-same-device-uuid: 864516020267118 61 | // Host: v2.same.com 62 | // Accept-Encoding: gzip 63 | 64 | 65 | @Override 66 | public Map getHeaders() throws AuthFailureError { 67 | Map hashMap = new HashMap<>(); 68 | hashMap.put("Connection", "keep-alive"); 69 | // hashMap.put("Authorization", "Token " + "1448701921-415909295e2e7ab9-1419308"); 70 | // Log.d("HttpAPI", "Authorization: Token " + LocalUserInfoUtils.getSharedInstance().getUserToken()); 71 | hashMap.put("machine", "android|301|android4.4.2|SM701|864516020267118|1080|1920"); 72 | hashMap.put("extrainfo", "offical"); 73 | hashMap.put("x-same-device-uuid","864516020267118"); 74 | hashMap.put("X-same-Client-Version", "313"); 75 | return hashMap; 76 | } 77 | 78 | @Override 79 | protected void deliverResponse(T response) { 80 | mListener.onResponse(response); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/com/asdzheng/suitedrecyclerview/http/RequestHelper.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.suitedrecyclerview.http; 2 | 3 | /** 4 | * Created by asdzheng on 2016/1/8. 5 | */ 6 | 7 | import android.content.Context; 8 | 9 | import com.android.volley.RequestQueue; 10 | import com.android.volley.toolbox.Volley; 11 | import com.asdzheng.suitedrecyclerview.MyApplication; 12 | 13 | public class RequestHelper { 14 | 15 | // 默认请求接口的超时时间30s 16 | private static final int DEFAULT_TIMEOUT_MS = 30 * 1000; 17 | 18 | public static final String SUCCESS_STATUS = "0000"; 19 | 20 | private static final long delayTime = 800; 21 | 22 | private final String TAG = this.getClass().getSimpleName(); 23 | 24 | private RequestQueue queue; 25 | 26 | private static RequestHelper helper; 27 | 28 | private Context appcontext; 29 | 30 | private RequestHelper() { 31 | appcontext = MyApplication.context; 32 | queue = Volley.newRequestQueue(appcontext); 33 | } 34 | 35 | public static synchronized RequestHelper getInstance() { 36 | if (helper == null) { 37 | helper = new RequestHelper(); 38 | } 39 | return helper; 40 | } 41 | 42 | // public String requestData(Object tag, HttpResponse http, String next) { 43 | // String url = UrlUtil.getBaseUrl(next); 44 | // request(Request.Method.GET, tag, url, http); 45 | // return url; 46 | // } 47 | 48 | // /** 49 | // * @param method 请求的方法,现在都为GET,POST还未做处理 50 | // * @param tag 为每个请求打的标签,退出界面时可取消请求 51 | // * @param url 请求的url 52 | // * @param http 请求的回调接口 53 | // * @param requestParams 请求需要的参数和返回的的类 54 | // */ 55 | // @SuppressWarnings("unchecked") 56 | // private void request(int method, Object tag, final String url, final HttpResponse http) { 57 | // final String tagName = tag.getClass().getSimpleName(); 58 | // 59 | // LogUtil.w(tagName, "Sending action " + requestParams.get("action") + " request to " + url); 60 | // 61 | // final long startRequestTime = System.currentTimeMillis(); 62 | // 63 | // GsonRequest request = new GsonRequest(method, url, re.getResponseClass(), new Response.Listener() { 64 | // 65 | // @Override 66 | // public void onResponse(final BaseResponse baseResponse) { 67 | // } 68 | // }, new Response.ErrorListener() { 69 | // 70 | // @Override 71 | // public void onErrorResponse(VolleyError error) { 72 | // LogUtil.e(tagName, error.toString()); 73 | // 74 | // } 75 | // }, requestParams); 76 | // 77 | // request.setTag(tag); 78 | // // 现在的接口数据都不需要Volley做缓存 79 | // request.setShouldCache(false); 80 | // // 重试策略的设置,Volley默认超时时间为2.5s,现改为10s 81 | // request.setRetryPolicy(new DefaultRetryPolicy(DEFAULT_TIMEOUT_MS, 82 | // DefaultRetryPolicy.DEFAULT_MAX_RETRIES, DefaultRetryPolicy.DEFAULT_BACKOFF_MULT) 83 | // ); 84 | // 85 | // queue.add(request); 86 | // } 87 | 88 | public void cancelAll(Object tag) { 89 | queue.cancelAll(tag); 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /app/src/main/java/com/asdzheng/suitedrecyclerview/http/UrlUtil.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.suitedrecyclerview.http; 2 | 3 | /** 4 | * Created by asdzheng on 2015/12/25. 5 | */ 6 | public class UrlUtil { 7 | private static String base_url = "https://v2.same.com"; 8 | 9 | //SexyChannel 10 | public static final String SEXY_CHANNEL = "/channel/1033563/senses"; 11 | public static final String BEAUTY_CHANNEL = "/channel/1015326/senses"; 12 | public static final String SIGHT = "/channel/1089364/senses"; 13 | 14 | public static String getBaseUrl(String next) { 15 | return base_url + next; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/asdzheng/suitedrecyclerview/ui/activity/BaseActivity.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.suitedrecyclerview.ui.activity; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | 6 | import com.squareup.picasso.Picasso; 7 | 8 | import butterknife.ButterKnife; 9 | 10 | /** 11 | * Created by asdzheng on 2016/1/6. 12 | */ 13 | public abstract class BaseActivity extends AppCompatActivity { 14 | 15 | @Override 16 | public void onCreate(Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | 19 | if(setLayout() != 0) { 20 | setContentView(setLayout()); 21 | ButterKnife.bind(this); 22 | } 23 | 24 | initViews(); 25 | initData(savedInstanceState); 26 | } 27 | 28 | /** 29 | * 设置布局,如果子类不想设置,返回0就可以 30 | */ 31 | protected abstract int setLayout(); 32 | 33 | /** 34 | * 初始化控件 35 | */ 36 | protected abstract void initViews(); 37 | 38 | /** 39 | * 为控件设置内容或者监听器 40 | */ 41 | protected abstract void initData(Bundle savedInstanceState); 42 | 43 | @Override 44 | protected void onDestroy() { 45 | super.onDestroy(); 46 | Picasso.with(this).cancelTag(this); 47 | ButterKnife.unbind(this); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/asdzheng/suitedrecyclerview/ui/activity/PhotoActivity.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.suitedrecyclerview.ui.activity; 2 | 3 | import android.os.Bundle; 4 | import android.os.Handler; 5 | import android.support.annotation.NonNull; 6 | import android.support.v7.widget.RecyclerView; 7 | import android.support.v7.widget.Toolbar; 8 | import android.util.Log; 9 | import android.view.MenuItem; 10 | import android.widget.Toast; 11 | 12 | import com.android.volley.Request; 13 | import com.android.volley.RequestQueue; 14 | import com.android.volley.Response; 15 | import com.android.volley.VolleyError; 16 | import com.android.volley.toolbox.Volley; 17 | import com.asdzheng.layoutmanager.SuitUrlUtil; 18 | import com.asdzheng.layoutmanager.SuitedItemDecoration; 19 | import com.asdzheng.layoutmanager.SuitedLayoutManager; 20 | import com.asdzheng.suitedrecyclerview.R; 21 | import com.asdzheng.suitedrecyclerview.bean.NewChannelInfoDetailDto; 22 | import com.asdzheng.suitedrecyclerview.bean.NewChannelInfoDto; 23 | import com.asdzheng.suitedrecyclerview.http.GsonRequest; 24 | import com.asdzheng.suitedrecyclerview.http.UrlUtil; 25 | import com.asdzheng.suitedrecyclerview.ui.adapter.PhotosAdapter; 26 | import com.asdzheng.suitedrecyclerview.ui.view.waveswiperefreshlayout.WaveSwipeRefreshLayout; 27 | import com.asdzheng.suitedrecyclerview.utils.DisplayUtils; 28 | import com.squareup.picasso.Picasso; 29 | 30 | import java.util.ArrayList; 31 | import java.util.List; 32 | 33 | import butterknife.Bind; 34 | 35 | public class PhotoActivity extends BaseActivity implements WaveSwipeRefreshLayout.OnRefreshListener { 36 | 37 | @Bind(R.id.recycler_channel_view) 38 | RecyclerView recyclerView; 39 | @Bind(R.id.wave_channel) 40 | WaveSwipeRefreshLayout waveChannel; 41 | 42 | int page = 1; 43 | RequestQueue queue; 44 | List list; 45 | @Bind(R.id.toolbar_channel_photo) 46 | Toolbar toolbarChannelPhoto; 47 | private String nextStr = UrlUtil.SIGHT; 48 | private PhotosAdapter mPhotosAdapter; 49 | 50 | 51 | @NonNull 52 | private void requestData(final String next) { 53 | GsonRequest request = new GsonRequest<>(Request.Method.GET, UrlUtil.getBaseUrl(next), NewChannelInfoDto.class, 54 | new Response.Listener() { 55 | 56 | @Override 57 | public void onResponse(NewChannelInfoDto response) { 58 | if (response.getData().getResults() != null) { 59 | if (page == 1) { 60 | mPhotosAdapter.clear(); 61 | mPhotosAdapter.bind(filterEmptyPhotos(response.getData().getResults())); 62 | } else { 63 | mPhotosAdapter.bind(filterEmptyPhotos(response.getData().getResults())); 64 | } 65 | } 66 | nextStr = response.getData().getNext(); 67 | if (page == 1) { 68 | waveChannel.setRefreshing(false); 69 | } else { 70 | waveChannel.setLoading(false); 71 | } 72 | 73 | page++; 74 | 75 | if (mPhotosAdapter.getItemCount() > 0) { 76 | waveChannel.setCanLoadMore(true); 77 | } else { 78 | waveChannel.setCanLoadMore(false); 79 | } 80 | 81 | } 82 | }, new Response.ErrorListener() { 83 | @Override 84 | public void onErrorResponse(VolleyError error) { 85 | 86 | if (mPhotosAdapter.getItemCount() == 0) { 87 | waveChannel.setCanLoadMore(false); 88 | } 89 | 90 | Toast.makeText(PhotoActivity.this, "网络连接错误", Toast.LENGTH_SHORT).show(); 91 | Log.w("main", error.toString()); 92 | waveChannel.setRefreshing(false); 93 | waveChannel.setLoading(false); 94 | 95 | } 96 | }); 97 | request.setTag(this); 98 | queue.add(request); 99 | } 100 | 101 | private List filterEmptyPhotos(List results) { 102 | List infos = new ArrayList<>(); 103 | for (NewChannelInfoDetailDto info : results) { 104 | if (SuitUrlUtil.isNotEmpty(info.photo)) { 105 | infos.add(info); 106 | } 107 | } 108 | return infos; 109 | } 110 | 111 | private void setupRecyclerView() { 112 | mPhotosAdapter = new PhotosAdapter(list, this); 113 | // recyclerView.setAdapter(new ScaleInAnimationAdapter(mPhotosAdapter)); 114 | recyclerView.setAdapter(mPhotosAdapter); 115 | SuitedLayoutManager layoutManager = new SuitedLayoutManager(mPhotosAdapter); 116 | recyclerView.setLayoutManager(layoutManager); 117 | //设置最大的图片显示高度,默认为600px 118 | layoutManager.setMaxRowHeight(getResources().getDisplayMetrics().heightPixels / 3); 119 | recyclerView.addItemDecoration(new SuitedItemDecoration(DisplayUtils.dpToPx(4.0f, this))); 120 | 121 | recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { 122 | 123 | @Override 124 | public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 125 | super.onScrollStateChanged(recyclerView, newState); 126 | if (newState == RecyclerView.SCROLL_STATE_IDLE) { 127 | Picasso.with(PhotoActivity.this).resumeTag(PhotoActivity.this); 128 | } else if (newState == RecyclerView.SCROLL_STATE_DRAGGING) { 129 | Picasso.with(PhotoActivity.this).pauseTag(PhotoActivity.this); 130 | } else if (newState == RecyclerView.SCROLL_STATE_SETTLING) { 131 | Picasso.with(PhotoActivity.this).pauseTag(PhotoActivity.this); 132 | } 133 | } 134 | }); 135 | } 136 | 137 | @Override 138 | public void onRefresh() { 139 | page = 1; 140 | nextStr = UrlUtil.SIGHT; 141 | requestData(nextStr); 142 | } 143 | 144 | @Override 145 | public void onLoad() { 146 | requestData(nextStr); 147 | } 148 | 149 | @Override 150 | protected int setLayout() { 151 | return R.layout.activity_channel_photo; 152 | } 153 | 154 | @Override 155 | protected void initViews() { 156 | int homepage_refresh_spacing = 40; 157 | 158 | waveChannel.setProgressViewOffset(false, -homepage_refresh_spacing * 2, homepage_refresh_spacing); 159 | waveChannel.setOnRefreshListener(this); 160 | waveChannel.setCanRefresh(true); 161 | } 162 | 163 | @Override 164 | protected void initData(Bundle savedInstanceState) { 165 | setSupportActionBar(toolbarChannelPhoto); 166 | getSupportActionBar().setDisplayShowTitleEnabled(false); 167 | // getSupportActionBar().setDisplayHomeAsUpEnabled(true); 168 | toolbarChannelPhoto.setTitle("美图"); 169 | 170 | queue = Volley.newRequestQueue(this); 171 | list = new ArrayList<>(); 172 | setupRecyclerView(); 173 | 174 | new Handler().post(new Runnable() { 175 | @Override 176 | public void run() { 177 | waveChannel.setRefreshing(true); 178 | requestData(nextStr); 179 | } 180 | }); 181 | } 182 | 183 | @Override 184 | protected void onDestroy() { 185 | queue.cancelAll(this); 186 | super.onDestroy(); 187 | } 188 | 189 | @Override 190 | public boolean onOptionsItemSelected(MenuItem item) { 191 | switch (item.getItemId()) { 192 | case android.R.id.home: 193 | finish(); 194 | } 195 | return super.onOptionsItemSelected(item); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /app/src/main/java/com/asdzheng/suitedrecyclerview/ui/activity/PhotoDetailActivity.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.suitedrecyclerview.ui.activity; 2 | 3 | import android.os.Bundle; 4 | import android.view.GestureDetector; 5 | import android.view.MotionEvent; 6 | import android.view.View; 7 | 8 | import com.asdzheng.suitedrecyclerview.R; 9 | import com.asdzheng.suitedrecyclerview.ui.view.MaterialProgressBar; 10 | import com.asdzheng.suitedrecyclerview.utils.LogUtil; 11 | import com.asdzheng.suitedrecyclerview.utils.transition.ActivityTransitionExitHelper; 12 | import com.squareup.picasso.Callback; 13 | import com.squareup.picasso.Picasso; 14 | 15 | 16 | /** 17 | * Created by Administrator on 2016-1-4. 18 | */ 19 | public class PhotoDetailActivity extends BaseActivity { 20 | 21 | private final String TAG = this.getClass().getSimpleName(); 22 | 23 | private uk.co.senab.photoview.PhotoView imageView; 24 | 25 | private ActivityTransitionExitHelper transitionExitHelper; 26 | 27 | private boolean isFinishing = false; 28 | 29 | private MaterialProgressBar progressBar; 30 | 31 | @Override 32 | protected int setLayout() { 33 | return R.layout.activity_channel_photo_detail; 34 | } 35 | 36 | @Override 37 | protected void initViews() { 38 | imageView = (uk.co.senab.photoview.PhotoView) findViewById(R.id.iv_channel_photo_detail); 39 | progressBar = (MaterialProgressBar) findViewById(R.id.mp_photo_detail); 40 | } 41 | 42 | @Override 43 | protected void initData(Bundle savedInstanceState) { 44 | String photo = getIntent().getStringExtra("photo"); 45 | Picasso.with(this).load(photo).tag(this).into(imageView, new Callback() { 46 | 47 | @Override 48 | public void onSuccess() { 49 | progressBar.setVisibility(View.GONE); 50 | LogUtil.i("PhotoDetailActivity", "imageView.getWidth " + imageView.getDisplayRect().width() 51 | + " | imageView.getHeight()" + imageView.getDisplayRect().height()); 52 | } 53 | 54 | @Override 55 | public void onError() { 56 | progressBar.setVisibility(View.GONE); 57 | 58 | } 59 | 60 | } 61 | ); 62 | 63 | transitionExitHelper = ActivityTransitionExitHelper.with(getIntent()) 64 | .toView(imageView).background(findViewById(R.id.rl_photo_detail)).start(savedInstanceState); 65 | 66 | 67 | imageView.setMediumScale(2.0f); 68 | imageView.setMaximumScale(2.00001f); 69 | //默认能过缩放两次,现在设置为一次 70 | imageView.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() { 71 | @Override 72 | public boolean onSingleTapConfirmed(MotionEvent e) { 73 | animExitActivity(); 74 | LogUtil.w(TAG, "onSingleTapConfirmed"); 75 | return false; 76 | } 77 | 78 | @Override 79 | public boolean onDoubleTap(MotionEvent e) { 80 | try { 81 | float scale = imageView.getScale(); 82 | float x = e.getX(); 83 | float y = e.getY(); 84 | 85 | if (scale < imageView.getMediumScale()) { 86 | imageView.setScale(imageView.getMediumScale(), x, y, true); 87 | } else { 88 | imageView.setScale(imageView.getMinimumScale(), x, y, true); 89 | } 90 | } catch (ArrayIndexOutOfBoundsException ex) { 91 | // Can sometimes happen hen getX() and getY() is called 92 | } 93 | return true; 94 | } 95 | 96 | @Override 97 | public boolean onDoubleTapEvent(MotionEvent e) { 98 | LogUtil.w(TAG, "onDoubleTapEvent"); 99 | return false; 100 | } 101 | }); 102 | } 103 | 104 | @Override 105 | public void onBackPressed() { 106 | if (progressBar.getVisibility() == View.GONE) { 107 | animExitActivity(); 108 | } else { 109 | finish(); 110 | } 111 | } 112 | 113 | private void animExitActivity() { 114 | if (!transitionExitHelper.isStarting && !isFinishing) { 115 | isFinishing = true; 116 | transitionExitHelper.runExitAnimation(new Runnable() { 117 | @Override 118 | public void run() { 119 | finish(); 120 | } 121 | }); 122 | } 123 | } 124 | 125 | @Override 126 | public void finish() { 127 | super.finish(); 128 | overridePendingTransition(0, 0); 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /app/src/main/java/com/asdzheng/suitedrecyclerview/ui/adapter/PhotosAdapter.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.suitedrecyclerview.ui.adapter; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.support.annotation.NonNull; 6 | import android.support.v7.widget.RecyclerView; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | 10 | import com.asdzheng.layoutmanager.SizeCaculator; 11 | import com.asdzheng.layoutmanager.SuitUrlUtil; 12 | import com.asdzheng.suitedrecyclerview.bean.NewChannelInfoDetailDto; 13 | import com.asdzheng.suitedrecyclerview.ui.activity.PhotoDetailActivity; 14 | import com.asdzheng.suitedrecyclerview.ui.view.SuitedImageView; 15 | import com.asdzheng.suitedrecyclerview.utils.transition.ActivityTransitionEnterHelper; 16 | 17 | import java.util.List; 18 | 19 | /** 20 | * Created by asdzheng on 2015/12/28. 21 | */ 22 | public class PhotosAdapter extends RecyclerView.Adapter implements SizeCaculator.SizeCalculatorDelegate { 23 | private List mPhotos; 24 | 25 | // private ArrayMap photoAspectRatios; 26 | 27 | private Context mContext; 28 | 29 | public PhotosAdapter(List mPhotos, Context context) { 30 | this.mPhotos = mPhotos; 31 | mContext = context; 32 | } 33 | 34 | @Override 35 | public double aspectRatioForIndex(int position) { 36 | if (position < getItemCount()) { 37 | NewChannelInfoDetailDto info = mPhotos.get(position); 38 | double ratio = SuitUrlUtil.getAspectRadioFromUrl(info.photo); 39 | return ratio; 40 | } 41 | return 1.0; 42 | } 43 | 44 | public void bind(@NonNull List mPhotos) { 45 | this.mPhotos.addAll(mPhotos); 46 | notifyDataSetChanged(); 47 | } 48 | 49 | public void clear() { 50 | mPhotos.clear(); 51 | notifyDataSetChanged(); 52 | } 53 | 54 | @Override 55 | public PhotoViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 56 | return new PhotoViewHolder(new SuitedImageView(parent.getContext())); 57 | } 58 | 59 | @Override 60 | public void onBindViewHolder(PhotoViewHolder holder, int position) { 61 | // LogUtil.i("PhotosAdapter", "ionBindViewHolder" + position); 62 | ((SuitedImageView) holder.itemView).setPhoto(mPhotos.get(position).photo); 63 | // Picasso.with(mContext).load(mPhotos.get(position).photo).into((ImageView)holder.itemView); 64 | holder.itemView.setTag(mPhotos.get(position).photo); 65 | } 66 | 67 | @Override 68 | public int getItemCount() { 69 | return mPhotos.size(); 70 | } 71 | 72 | public class PhotoViewHolder extends RecyclerView.ViewHolder { 73 | public PhotoViewHolder(View view) { 74 | super(view); 75 | view.setOnClickListener(new View.OnClickListener() { 76 | @Override 77 | public void onClick(View v) { 78 | scaleUpAnimation(v); 79 | } 80 | }); 81 | } 82 | } 83 | 84 | private void scaleUpAnimation(View view) { 85 | Activity context = (Activity) view.getContext(); 86 | ActivityTransitionEnterHelper.with(context).fromView(view). 87 | start(PhotoDetailActivity.class); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /app/src/main/java/com/asdzheng/suitedrecyclerview/ui/view/MaterialProgressBar.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.suitedrecyclerview.ui.view; 2 | 3 | import android.content.Context; 4 | import android.content.res.Resources; 5 | import android.graphics.Canvas; 6 | import android.graphics.Color; 7 | import android.graphics.Paint; 8 | import android.graphics.Paint.Style; 9 | import android.graphics.RectF; 10 | import android.os.Parcel; 11 | import android.os.Parcelable; 12 | import android.os.SystemClock; 13 | import android.support.annotation.NonNull; 14 | import android.util.AttributeSet; 15 | import android.util.DisplayMetrics; 16 | import android.util.TypedValue; 17 | import android.view.View; 18 | 19 | /** 20 | * Material Progress 21 | * by Bruce too 22 | * 使用方法: 23 | * 1.不确定进度的使用 24 | * 只需要设置view 就Ok 另外可设置 颜色setBarColor.宽度setBarWidth.速度等等... 25 | * 2.确定进度的使用 26 | * setInstantProgress()初始化进度条,会自动停止 spin()的拉伸动画 27 | * setProgerss()设置进度 setCallback()设置进度更新回调 28 | */ 29 | public class MaterialProgressBar extends View { 30 | private static final String TAG = MaterialProgressBar.class.getSimpleName(); 31 | 32 | /** 33 | * ********* 34 | * DEFAULTS * 35 | * ********** 36 | */ 37 | //Sizes (with defaults in DP) 38 | private int circleRadius = 28; //默认圆半径 39 | private int barWidth = 5; //边界宽度 拉伸效过的边界 40 | private int rimWidth = 2; //边界宽度 封闭区域的边界 41 | 42 | private final int barLength = 16; //边界最小长度 43 | private final int barMaxLength = 270; //边界最大 44 | 45 | private boolean fillRadius = false; // 画圆是否填充 46 | 47 | private double timeStartGrowing = 0; 48 | private double barSpinCycleTime = 460; 49 | private float barExtraLength = 0; 50 | private boolean barGrowingFromFront = true; 51 | 52 | private long pausedTimeWithoutGrowing = 0; 53 | private final long pauseGrowingTime = 200; 54 | 55 | //Colors (with defaults) 56 | private int barColor = Color.parseColor("#cfcfcf"); 57 | private int rimColor = 0x00FFFFFF; 58 | 59 | //Paints 60 | private Paint barPaint = new Paint(); 61 | private Paint rimPaint = new Paint(); 62 | 63 | //Rectangles 64 | private RectF circleBounds = new RectF(); 65 | 66 | //Animation 67 | //The amount of degrees per second 68 | private float spinSpeed = 230.0f; 69 | //private float spinSpeed = 120.0f; 70 | // The last time the spinner was animated 71 | private long lastTimeAnimated = 0; 72 | 73 | private boolean linearProgress; 74 | 75 | private float mProgress = 0.0f; 76 | private float mTargetProgress = 0.0f; 77 | private boolean isSpinning = false; 78 | 79 | private ProgressCallback callback; 80 | 81 | /** 82 | * The constructor for the ProgressWheel 83 | * 84 | * @param context 85 | * @param attrs 86 | */ 87 | public MaterialProgressBar(Context context, AttributeSet attrs) { 88 | super(context, attrs); 89 | 90 | // parseAttributes(context.obtainStyledAttributes(attrs, 91 | // R.styleable.ProgressWheel)); 92 | parseAttributes(); 93 | } 94 | 95 | /** 96 | * The constructor for the ProgressWheel 97 | * 98 | * @param context 99 | */ 100 | public MaterialProgressBar(Context context) { 101 | super(context); 102 | } 103 | 104 | //---------------------------------- 105 | //Setting up stuff 106 | //---------------------------------- 107 | 108 | @Override 109 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 110 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 111 | 112 | int viewWidth = circleRadius + this.getPaddingLeft() + this.getPaddingRight(); 113 | int viewHeight = circleRadius + this.getPaddingTop() + this.getPaddingBottom(); 114 | 115 | int widthMode = MeasureSpec.getMode(widthMeasureSpec); 116 | int widthSize = MeasureSpec.getSize(widthMeasureSpec); 117 | int heightMode = MeasureSpec.getMode(heightMeasureSpec); 118 | int heightSize = MeasureSpec.getSize(heightMeasureSpec); 119 | 120 | int width; 121 | int height; 122 | 123 | //Measure Width 124 | if (widthMode == MeasureSpec.EXACTLY) { 125 | //Must be this size 126 | width = widthSize; 127 | } else if (widthMode == MeasureSpec.AT_MOST) { 128 | //Can't be bigger than... 129 | width = Math.min(viewWidth, widthSize); 130 | } else { 131 | //Be whatever you want 132 | width = viewWidth; 133 | } 134 | 135 | //Measure Height 136 | if (heightMode == MeasureSpec.EXACTLY || widthMode == MeasureSpec.EXACTLY) { 137 | //Must be this size 138 | height = heightSize; 139 | } else if (heightMode == MeasureSpec.AT_MOST) { 140 | //Can't be bigger than... 141 | height = Math.min(viewHeight, heightSize); 142 | } else { 143 | //Be whatever you want 144 | height = viewHeight; 145 | } 146 | 147 | setMeasuredDimension(width, height); 148 | } 149 | 150 | /** 151 | * Use onSizeChanged instead of onAttachedToWindow to get the dimensions of the view, 152 | * because this method is called after measuring the dimensions of MATCH_PARENT & WRAP_CONTENT. 153 | * Use this dimensions to setup the bounds and paints. 154 | */ 155 | @Override 156 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 157 | super.onSizeChanged(w, h, oldw, oldh); 158 | 159 | setupBounds(w, h); 160 | setupPaints(); 161 | invalidate(); 162 | } 163 | 164 | /** 165 | * Set the properties of the paints we're using to 166 | * draw the progress wheel 167 | */ 168 | private void setupPaints() { 169 | barPaint.setColor(barColor); 170 | barPaint.setAntiAlias(true); 171 | barPaint.setStyle(Style.STROKE); 172 | barPaint.setStrokeWidth(barWidth); 173 | 174 | rimPaint.setColor(rimColor); 175 | rimPaint.setAntiAlias(true); 176 | rimPaint.setStyle(Style.STROKE); 177 | rimPaint.setStrokeWidth(rimWidth); 178 | } 179 | 180 | /** 181 | * Set the bounds of the component 182 | */ 183 | private void setupBounds(int layout_width, int layout_height) { 184 | int paddingTop = getPaddingTop(); 185 | int paddingBottom = getPaddingBottom(); 186 | int paddingLeft = getPaddingLeft(); 187 | int paddingRight = getPaddingRight(); 188 | 189 | if (!fillRadius) { 190 | // Width should equal to Height, find the min value to setup the circle 191 | int minValue = Math.min(layout_width - paddingLeft - paddingRight, 192 | layout_height - paddingBottom - paddingTop); 193 | 194 | int circleDiameter = Math.min(minValue, circleRadius * 2 - barWidth * 2); 195 | 196 | // Calc the Offset if needed for centering the wheel in the available space 197 | int xOffset = (layout_width - paddingLeft - paddingRight - circleDiameter) / 2 + paddingLeft; 198 | int yOffset = (layout_height - paddingTop - paddingBottom - circleDiameter) / 2 + paddingTop; 199 | 200 | circleBounds = new RectF(xOffset + barWidth, 201 | yOffset + barWidth, 202 | xOffset + circleDiameter - barWidth, 203 | yOffset + circleDiameter - barWidth); 204 | } else { 205 | circleBounds = new RectF(paddingLeft + barWidth, 206 | paddingTop + barWidth, 207 | layout_width - paddingRight - barWidth, 208 | layout_height - paddingBottom - barWidth); 209 | } 210 | } 211 | 212 | /** 213 | * Parse the attributes passed to the view from the XML 214 | */ 215 | private void parseAttributes() { 216 | // We transform the default values from DIP to pixels 217 | DisplayMetrics metrics = getContext().getResources().getDisplayMetrics(); 218 | barWidth = dpToPx(barWidth,getResources()); 219 | rimWidth = dpToPx(rimWidth,getResources()); 220 | circleRadius = dpToPx(circleRadius,getResources()); 221 | fillRadius = false; 222 | 223 | float baseSpinSpeed = spinSpeed / 360.0f; 224 | spinSpeed = baseSpinSpeed * 360; 225 | 226 | //barSpinCycleTime = barSpinCycleTime; 227 | 228 | // barColor = a.getColor(R.styleable.ProgressWheel_matProg_barColor, barColor); 229 | // 230 | // rimColor = a.getColor(R.styleable.ProgressWheel_matProg_rimColor, rimColor); 231 | 232 | linearProgress = false; 233 | 234 | // if (a.getBoolean(R.styleable.ProgressWheel_matProg_progressIndeterminate, false)) { 235 | spin(); 236 | // } 237 | 238 | // Recycle 239 | // a.recycle(); 240 | } 241 | 242 | public void setCallback(ProgressCallback progressCallback) { 243 | callback = progressCallback; 244 | 245 | if (!isSpinning) { 246 | runCallback(); 247 | } 248 | } 249 | 250 | //---------------------------------- 251 | //Animation stuff 252 | //---------------------------------- 253 | 254 | protected void onDraw(Canvas canvas) { 255 | super.onDraw(canvas); 256 | 257 | canvas.drawArc(circleBounds, 360, 360, false, rimPaint); 258 | 259 | boolean mustInvalidate = false; 260 | 261 | if (isSpinning) { 262 | //Draw the spinning bar 263 | mustInvalidate = true; 264 | 265 | long deltaTime = (SystemClock.uptimeMillis() - lastTimeAnimated); 266 | float deltaNormalized = deltaTime * spinSpeed / 1000.0f; 267 | 268 | updateBarLength(deltaTime); 269 | 270 | mProgress += deltaNormalized; 271 | if (mProgress > 360) { 272 | mProgress -= 360f; 273 | 274 | // A full turn has been completed 275 | // we run the callback with -1 in case we want to 276 | // do something, like changing the color 277 | runCallback(-1.0f); 278 | } 279 | lastTimeAnimated = SystemClock.uptimeMillis(); 280 | 281 | float from = mProgress - 90; 282 | float length = barLength + barExtraLength; 283 | 284 | if (isInEditMode()) { 285 | from = 0; 286 | length = 135; 287 | } 288 | 289 | canvas.drawArc(circleBounds, from, length, false, 290 | barPaint); 291 | } else { 292 | float oldProgress = mProgress; 293 | 294 | if (mProgress != mTargetProgress) { 295 | //We smoothly increase the progress bar 296 | mustInvalidate = true; 297 | 298 | float deltaTime = (float) (SystemClock.uptimeMillis() - lastTimeAnimated) / 1000; 299 | float deltaNormalized = deltaTime * spinSpeed; 300 | 301 | mProgress = Math.min(mProgress + deltaNormalized, mTargetProgress); 302 | lastTimeAnimated = SystemClock.uptimeMillis(); 303 | } 304 | 305 | if (oldProgress != mProgress) { 306 | runCallback(); 307 | } 308 | 309 | float offset = 0.0f; 310 | float progress = mProgress; 311 | if (!linearProgress) { 312 | float factor = 2.0f; 313 | offset = (float) (1.0f - Math.pow(1.0f - mProgress / 360.0f, 2.0f * factor)) * 360.0f; 314 | progress = (float) (1.0f - Math.pow(1.0f - mProgress / 360.0f, factor)) * 360.0f; 315 | } 316 | 317 | if (isInEditMode()) { 318 | progress = 360; 319 | } 320 | 321 | canvas.drawArc(circleBounds, offset - 90, progress, false, barPaint); 322 | } 323 | 324 | if (mustInvalidate) { 325 | invalidate(); 326 | } 327 | } 328 | 329 | @Override 330 | protected void onVisibilityChanged(@NonNull View changedView, int visibility) { 331 | super.onVisibilityChanged(changedView, visibility); 332 | 333 | if (visibility == VISIBLE) { 334 | lastTimeAnimated = SystemClock.uptimeMillis(); 335 | } 336 | } 337 | 338 | private void updateBarLength(long deltaTimeInMilliSeconds) { 339 | if (pausedTimeWithoutGrowing >= pauseGrowingTime) { 340 | timeStartGrowing += deltaTimeInMilliSeconds; 341 | 342 | if (timeStartGrowing > barSpinCycleTime) { 343 | // We completed a size change cycle 344 | // (growing or shrinking) 345 | timeStartGrowing -= barSpinCycleTime; 346 | //if(barGrowingFromFront) { 347 | pausedTimeWithoutGrowing = 0; 348 | //} 349 | barGrowingFromFront = !barGrowingFromFront; 350 | } 351 | 352 | float distance = (float) Math.cos((timeStartGrowing / barSpinCycleTime + 1) * Math.PI) / 2 + 0.5f; 353 | float destLength = (barMaxLength - barLength); 354 | 355 | if (barGrowingFromFront) { 356 | barExtraLength = distance * destLength; 357 | } else { 358 | float newLength = destLength * (1 - distance); 359 | mProgress += (barExtraLength - newLength); 360 | barExtraLength = newLength; 361 | } 362 | } else { 363 | pausedTimeWithoutGrowing += deltaTimeInMilliSeconds; 364 | } 365 | } 366 | 367 | /** 368 | * Check if the wheel is currently spinning 369 | */ 370 | 371 | public boolean isSpinning() { 372 | return isSpinning; 373 | } 374 | 375 | /** 376 | * Reset the count (in increment mode) 377 | */ 378 | public void resetCount() { 379 | mProgress = 0.0f; 380 | mTargetProgress = 0.0f; 381 | invalidate(); 382 | } 383 | 384 | /** 385 | * Turn off spin mode 386 | */ 387 | public void stopSpinning() { 388 | isSpinning = false; 389 | mProgress = 0.0f; 390 | mTargetProgress = 0.0f; 391 | invalidate(); 392 | } 393 | 394 | /** 395 | * Puts the view on spin mode 396 | */ 397 | public void spin() { 398 | lastTimeAnimated = SystemClock.uptimeMillis(); 399 | isSpinning = true; 400 | invalidate(); 401 | } 402 | 403 | private void runCallback(float value) { 404 | if (callback != null) { 405 | callback.onProgressUpdate(value); 406 | } 407 | } 408 | 409 | private void runCallback() { 410 | if (callback != null) { 411 | float normalizedProgress = (float) Math.round(mProgress * 100 / 360.0f) / 100; 412 | callback.onProgressUpdate(normalizedProgress); 413 | } 414 | } 415 | 416 | /** 417 | * Set the progress to a specific value, 418 | * the bar will smoothly animate until that value 419 | * 420 | * @param progress the progress between 0 and 1 421 | */ 422 | public void setProgress(float progress) { 423 | if (isSpinning) { 424 | mProgress = 0.0f; 425 | isSpinning = false; 426 | 427 | runCallback(); 428 | } 429 | 430 | if (progress > 1.0f) { 431 | progress -= 1.0f; 432 | } else if (progress < 0) { 433 | progress = 0; 434 | } 435 | 436 | if (progress == mTargetProgress) { 437 | return; 438 | } 439 | 440 | // If we are currently in the right position 441 | // we set again the last time animated so the 442 | // animation starts smooth from here 443 | if (mProgress == mTargetProgress) { 444 | lastTimeAnimated = SystemClock.uptimeMillis(); 445 | } 446 | 447 | mTargetProgress = Math.min(progress * 360.0f, 360.0f); 448 | 449 | invalidate(); 450 | } 451 | 452 | /** 453 | * Set the progress to a specific value, 454 | * the bar will be set instantly to that value 455 | * 456 | * @param progress the progress between 0 and 1 457 | */ 458 | public void setInstantProgress(float progress) { 459 | if (isSpinning) { 460 | mProgress = 0.0f; 461 | isSpinning = false; 462 | } 463 | 464 | if (progress > 1.0f) { 465 | progress -= 1.0f; 466 | } else if (progress < 0) { 467 | progress = 0; 468 | } 469 | 470 | if (progress == mTargetProgress) { 471 | return; 472 | } 473 | 474 | mTargetProgress = Math.min(progress * 360.0f, 360.0f); 475 | mProgress = mTargetProgress; 476 | lastTimeAnimated = SystemClock.uptimeMillis(); 477 | invalidate(); 478 | } 479 | 480 | // Great way to save a view's state http://stackoverflow.com/a/7089687/1991053 481 | @Override 482 | public Parcelable onSaveInstanceState() { 483 | Parcelable superState = super.onSaveInstanceState(); 484 | 485 | WheelSavedState ss = new WheelSavedState(superState); 486 | 487 | // We save everything that can be changed at runtime 488 | ss.mProgress = this.mProgress; 489 | ss.mTargetProgress = this.mTargetProgress; 490 | ss.isSpinning = this.isSpinning; 491 | ss.spinSpeed = this.spinSpeed; 492 | ss.barWidth = this.barWidth; 493 | ss.barColor = this.barColor; 494 | ss.rimWidth = this.rimWidth; 495 | ss.rimColor = this.rimColor; 496 | ss.circleRadius = this.circleRadius; 497 | ss.linearProgress = this.linearProgress; 498 | ss.fillRadius = this.fillRadius; 499 | 500 | return ss; 501 | } 502 | 503 | @Override 504 | public void onRestoreInstanceState(Parcelable state) { 505 | if (!(state instanceof WheelSavedState)) { 506 | super.onRestoreInstanceState(state); 507 | return; 508 | } 509 | 510 | WheelSavedState ss = (WheelSavedState) state; 511 | super.onRestoreInstanceState(ss.getSuperState()); 512 | 513 | this.mProgress = ss.mProgress; 514 | this.mTargetProgress = ss.mTargetProgress; 515 | this.isSpinning = ss.isSpinning; 516 | this.spinSpeed = ss.spinSpeed; 517 | this.barWidth = ss.barWidth; 518 | this.barColor = ss.barColor; 519 | this.rimWidth = ss.rimWidth; 520 | this.rimColor = ss.rimColor; 521 | this.circleRadius = ss.circleRadius; 522 | this.linearProgress = ss.linearProgress; 523 | this.fillRadius = ss.fillRadius; 524 | 525 | this.lastTimeAnimated = SystemClock.uptimeMillis(); 526 | } 527 | 528 | //---------------------------------- 529 | //Getters + setters 530 | //---------------------------------- 531 | 532 | /** 533 | * @return the current progress between 0.0 and 1.0, 534 | * if the wheel is indeterminate, then the result is -1 535 | */ 536 | public float getProgress() { 537 | return isSpinning ? -1 : mProgress / 360.0f; 538 | } 539 | 540 | /** 541 | * Sets the determinate progress mode 542 | * 543 | * @param isLinear if the progress should increase linearly 544 | */ 545 | public void setLinearProgress(boolean isLinear) { 546 | linearProgress = isLinear; 547 | if (!isSpinning) { 548 | invalidate(); 549 | } 550 | } 551 | 552 | /** 553 | * @return the radius of the wheel in pixels 554 | */ 555 | public int getCircleRadius() { 556 | return circleRadius; 557 | } 558 | 559 | /** 560 | * Sets the radius of the wheel 561 | * 562 | * @param circleRadius the expected radius, in pixels 563 | */ 564 | public void setCircleRadius(int circleRadius) { 565 | this.circleRadius = circleRadius; 566 | if (!isSpinning) { 567 | invalidate(); 568 | } 569 | } 570 | 571 | /** 572 | * @return the width of the spinning bar 573 | */ 574 | public int getBarWidth() { 575 | return barWidth; 576 | } 577 | 578 | /** 579 | * Sets the width of the spinning bar 580 | * 581 | * @param barWidth the spinning bar width in pixels 582 | */ 583 | public void setBarWidth(int barWidth) { 584 | this.barWidth = barWidth; 585 | if (!isSpinning) { 586 | invalidate(); 587 | } 588 | } 589 | 590 | /** 591 | * @return the color of the spinning bar 592 | */ 593 | public int getBarColor() { 594 | return barColor; 595 | } 596 | 597 | /** 598 | * Sets the color of the spinning bar 599 | * 600 | * @param barColor The spinning bar color 601 | */ 602 | public void setBarColor(int barColor) { 603 | this.barColor = barColor; 604 | setupPaints(); 605 | if (!isSpinning) { 606 | invalidate(); 607 | } 608 | } 609 | 610 | /** 611 | * @return the color of the wheel's contour 612 | */ 613 | public int getRimColor() { 614 | return rimColor; 615 | } 616 | 617 | /** 618 | * Sets the color of the wheel's contour 619 | * 620 | * @param rimColor the color for the wheel 621 | */ 622 | public void setRimColor(int rimColor) { 623 | this.rimColor = rimColor; 624 | setupPaints(); 625 | if (!isSpinning) { 626 | invalidate(); 627 | } 628 | } 629 | 630 | /** 631 | * @return the base spinning speed, in full circle turns per second 632 | * (1.0 equals on full turn in one second), this value also is applied for 633 | * the smoothness when setting a progress 634 | */ 635 | public float getSpinSpeed() { 636 | return spinSpeed / 360.0f; 637 | } 638 | 639 | /** 640 | * Sets the base spinning speed, in full circle turns per second 641 | * (1.0 equals on full turn in one second), this value also is applied for 642 | * the smoothness when setting a progress 643 | * 644 | * @param spinSpeed the desired base speed in full turns per second 645 | */ 646 | public void setSpinSpeed(float spinSpeed) { 647 | this.spinSpeed = spinSpeed * 360.0f; 648 | } 649 | 650 | /** 651 | * @return the width of the wheel's contour in pixels 652 | */ 653 | public int getRimWidth() { 654 | return rimWidth; 655 | } 656 | 657 | /** 658 | * Sets the width of the wheel's contour 659 | * 660 | * @param rimWidth the width in pixels 661 | */ 662 | public void setRimWidth(int rimWidth) { 663 | this.rimWidth = rimWidth; 664 | if (!isSpinning) { 665 | invalidate(); 666 | } 667 | } 668 | 669 | static class WheelSavedState extends BaseSavedState { 670 | float mProgress; 671 | float mTargetProgress; 672 | boolean isSpinning; 673 | float spinSpeed; 674 | int barWidth; 675 | int barColor; 676 | int rimWidth; 677 | int rimColor; 678 | int circleRadius; 679 | boolean linearProgress; 680 | boolean fillRadius; 681 | 682 | WheelSavedState(Parcelable superState) { 683 | super(superState); 684 | } 685 | 686 | private WheelSavedState(Parcel in) { 687 | super(in); 688 | this.mProgress = in.readFloat(); 689 | this.mTargetProgress = in.readFloat(); 690 | this.isSpinning = in.readByte() != 0; 691 | this.spinSpeed = in.readFloat(); 692 | this.barWidth = in.readInt(); 693 | this.barColor = in.readInt(); 694 | this.rimWidth = in.readInt(); 695 | this.rimColor = in.readInt(); 696 | this.circleRadius = in.readInt(); 697 | this.linearProgress = in.readByte() != 0; 698 | this.fillRadius = in.readByte() != 0; 699 | } 700 | 701 | @Override 702 | public void writeToParcel(Parcel out, int flags) { 703 | super.writeToParcel(out, flags); 704 | out.writeFloat(this.mProgress); 705 | out.writeFloat(this.mTargetProgress); 706 | out.writeByte((byte) (isSpinning ? 1 : 0)); 707 | out.writeFloat(this.spinSpeed); 708 | out.writeInt(this.barWidth); 709 | out.writeInt(this.barColor); 710 | out.writeInt(this.rimWidth); 711 | out.writeInt(this.rimColor); 712 | out.writeInt(this.circleRadius); 713 | out.writeByte((byte) (linearProgress ? 1 : 0)); 714 | out.writeByte((byte) (fillRadius ? 1 : 0)); 715 | } 716 | 717 | //required field that makes Parcelables from a Parcel 718 | public static final Creator CREATOR = 719 | new Creator() { 720 | public WheelSavedState createFromParcel(Parcel in) { 721 | return new WheelSavedState(in); 722 | } 723 | 724 | public WheelSavedState[] newArray(int size) { 725 | return new WheelSavedState[size]; 726 | } 727 | }; 728 | } 729 | 730 | /** 731 | * 更新progress的回调方法 732 | */ 733 | public interface ProgressCallback { 734 | /** 735 | * Method to call when the progress reaches a value 736 | * in order to avoid float precision issues, the progress 737 | * is rounded to a float with two decimals. 738 | * 739 | * In indeterminate mode, the callback is called each time 740 | * the wheel completes an animation cycle, with, the progress value is -1.0f 741 | * 742 | * @param progress a double value between 0.00 and 1.00 both included 743 | */ 744 | public void onProgressUpdate(float progress); 745 | } 746 | 747 | public int dpToPx(float dp, Resources resources){ 748 | float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.getDisplayMetrics()); 749 | return (int) px; 750 | } 751 | } 752 | -------------------------------------------------------------------------------- /app/src/main/java/com/asdzheng/suitedrecyclerview/ui/view/SuitedImageView.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.suitedrecyclerview.ui.view; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.ViewGroup; 6 | import android.widget.ImageView; 7 | 8 | import com.asdzheng.suitedrecyclerview.R; 9 | import com.squareup.picasso.Picasso; 10 | 11 | 12 | /** 13 | * Created by asdzheng on 2015/12/28. 14 | */ 15 | public class SuitedImageView extends ImageView { 16 | 17 | private String mPhoto; 18 | 19 | @Override 20 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 21 | super.onSizeChanged(w, h, oldw, oldh); 22 | // LogUtil.i("imageView", "onSizeChanged"); 23 | if(w != 0 && h != 0) { 24 | Picasso.with(getContext()).load(mPhoto).tag(getContext()).resize(w,h).into(this); 25 | } 26 | } 27 | 28 | public SuitedImageView(Context context) { 29 | super(context); 30 | this.init(null, 0); 31 | } 32 | 33 | public SuitedImageView(Context context, AttributeSet set) { 34 | super(context, set); 35 | } 36 | 37 | public SuitedImageView(Context context, AttributeSet set, int n) { 38 | super(context, set, n); 39 | this.init(set, n); 40 | } 41 | 42 | private void init(AttributeSet set, int n) { 43 | this.setScaleType(ScaleType.FIT_XY); 44 | this.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); 45 | this.setBackgroundColor(getResources().getColor(R.color.default_image_bg)); 46 | } 47 | 48 | public void setPhoto(String s) { 49 | mPhoto = s; 50 | } 51 | 52 | public String getPhoto() { 53 | return mPhoto; 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/asdzheng/suitedrecyclerview/ui/view/waveswiperefreshlayout/CircleImageView.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.suitedrecyclerview.ui.view.waveswiperefreshlayout; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Color; 6 | import android.graphics.Paint; 7 | import android.graphics.RadialGradient; 8 | import android.graphics.Shader; 9 | import android.graphics.drawable.ShapeDrawable; 10 | import android.graphics.drawable.shapes.OvalShape; 11 | import android.support.v4.view.ViewCompat; 12 | import android.view.animation.Animation; 13 | import android.widget.ImageView; 14 | 15 | /** 16 | * Created by Administrator on 2015/12/22 0022. 17 | */ 18 | class CircleImageView extends ImageView { 19 | 20 | private static final int KEY_SHADOW_COLOR = 0x1E000000; 21 | private static final int FILL_SHADOW_COLOR = 0x3D000000; 22 | // PX 23 | private static final float X_OFFSET = 0f; 24 | private static final float Y_OFFSET = 1.75f; 25 | private static final float SHADOW_RADIUS = 3.5f; 26 | private static final int SHADOW_ELEVATION = 4; 27 | 28 | private Animation.AnimationListener mListener; 29 | private int mShadowRadius; 30 | 31 | public CircleImageView(Context context, int color, final float radius) { 32 | super(context); 33 | final float density = getContext().getResources().getDisplayMetrics().density; 34 | final int diameter = (int) (radius * density * 2); 35 | final int shadowYOffset = (int) (density * Y_OFFSET); 36 | final int shadowXOffset = (int) (density * X_OFFSET); 37 | 38 | mShadowRadius = (int) (density * SHADOW_RADIUS); 39 | 40 | ShapeDrawable circle; 41 | if (elevationSupported()) { 42 | circle = new ShapeDrawable(new OvalShape()); 43 | ViewCompat.setElevation(this, SHADOW_ELEVATION * density); 44 | } else { 45 | OvalShape oval = new OvalShadow(mShadowRadius, diameter); 46 | circle = new ShapeDrawable(oval); 47 | ViewCompat.setLayerType(this, ViewCompat.LAYER_TYPE_SOFTWARE, circle.getPaint()); 48 | circle.getPaint().setShadowLayer(mShadowRadius, shadowXOffset, shadowYOffset, 49 | KEY_SHADOW_COLOR); 50 | final int padding = mShadowRadius; 51 | // set padding so the inner image sits correctly within the shadow. 52 | setPadding(padding, padding, padding, padding); 53 | } 54 | circle.getPaint().setColor(color); 55 | setBackgroundDrawable(circle); 56 | } 57 | 58 | private boolean elevationSupported() { 59 | return android.os.Build.VERSION.SDK_INT >= 21; 60 | } 61 | 62 | @Override 63 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 64 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 65 | if (!elevationSupported()) { 66 | setMeasuredDimension(getMeasuredWidth() + mShadowRadius*2, getMeasuredHeight() 67 | + mShadowRadius*2); 68 | } 69 | } 70 | 71 | public void setAnimationListener(Animation.AnimationListener listener) { 72 | mListener = listener; 73 | } 74 | 75 | @Override 76 | public void onAnimationStart() { 77 | super.onAnimationStart(); 78 | if (mListener != null) { 79 | mListener.onAnimationStart(getAnimation()); 80 | } 81 | } 82 | 83 | @Override 84 | public void onAnimationEnd() { 85 | super.onAnimationEnd(); 86 | if (mListener != null) { 87 | mListener.onAnimationEnd(getAnimation()); 88 | } 89 | } 90 | 91 | /** 92 | * Update the background color of the circle image view. 93 | * 94 | * @param colorRes Id of a color resource. 95 | */ 96 | public void setBackgroundColorRes(int colorRes) { 97 | setBackgroundColor(getContext().getResources().getColor(colorRes)); 98 | } 99 | 100 | @Override 101 | public void setBackgroundColor(int color) { 102 | if (getBackground() instanceof ShapeDrawable) { 103 | ((ShapeDrawable) getBackground()).getPaint().setColor(color); 104 | } 105 | } 106 | 107 | private class OvalShadow extends OvalShape { 108 | private RadialGradient mRadialGradient; 109 | private Paint mShadowPaint; 110 | private int mCircleDiameter; 111 | 112 | public OvalShadow(int shadowRadius, int circleDiameter) { 113 | super(); 114 | mShadowPaint = new Paint(); 115 | mShadowRadius = shadowRadius; 116 | mCircleDiameter = circleDiameter; 117 | mRadialGradient = new RadialGradient(mCircleDiameter / 2, mCircleDiameter / 2, 118 | mShadowRadius, new int[] { 119 | FILL_SHADOW_COLOR, Color.TRANSPARENT 120 | }, null, Shader.TileMode.CLAMP); 121 | mShadowPaint.setShader(mRadialGradient); 122 | } 123 | 124 | @Override 125 | public void draw(Canvas canvas, Paint paint) { 126 | final int viewWidth = CircleImageView.this.getWidth(); 127 | final int viewHeight = CircleImageView.this.getHeight(); 128 | canvas.drawCircle(viewWidth / 2, viewHeight / 2, (mCircleDiameter / 2 + mShadowRadius), 129 | mShadowPaint); 130 | canvas.drawCircle(viewWidth / 2, viewHeight / 2, (mCircleDiameter / 2), paint); 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /app/src/main/java/com/asdzheng/suitedrecyclerview/ui/view/waveswiperefreshlayout/MaterialProgressDrawable.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.suitedrecyclerview.ui.view.waveswiperefreshlayout; 2 | 3 | import android.content.Context; 4 | import android.content.res.Resources; 5 | import android.graphics.Canvas; 6 | import android.graphics.Color; 7 | import android.graphics.ColorFilter; 8 | import android.graphics.Paint; 9 | import android.graphics.Path; 10 | import android.graphics.PixelFormat; 11 | import android.graphics.Rect; 12 | import android.graphics.RectF; 13 | import android.graphics.drawable.Animatable; 14 | import android.graphics.drawable.Drawable; 15 | import android.support.annotation.IntDef; 16 | import android.support.annotation.NonNull; 17 | import android.support.v4.view.animation.FastOutSlowInInterpolator; 18 | import android.util.DisplayMetrics; 19 | import android.view.View; 20 | import android.view.animation.Animation; 21 | import android.view.animation.Interpolator; 22 | import android.view.animation.LinearInterpolator; 23 | import android.view.animation.Transformation; 24 | 25 | import java.lang.annotation.Retention; 26 | import java.lang.annotation.RetentionPolicy; 27 | import java.util.ArrayList; 28 | 29 | /** 30 | * Created by Administrator on 2015/12/22 0022. 31 | */ 32 | class MaterialProgressDrawable extends Drawable implements Animatable { 33 | private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); 34 | private static final Interpolator MATERIAL_INTERPOLATOR = new FastOutSlowInInterpolator(); 35 | 36 | private static final float FULL_ROTATION = 1080.0f; 37 | @Retention(RetentionPolicy.CLASS) 38 | @IntDef({LARGE, DEFAULT}) 39 | public @interface ProgressDrawableSize {} 40 | // Maps to ProgressBar.Large style 41 | static final int LARGE = 0; 42 | // Maps to ProgressBar default style 43 | static final int DEFAULT = 1; 44 | 45 | // Maps to ProgressBar default style 46 | private static final int CIRCLE_DIAMETER = 40; 47 | private static final float CENTER_RADIUS = 8.75f; //should add up to 10 when + stroke_width 48 | private static final float STROKE_WIDTH = 2.5f; 49 | 50 | // Maps to ProgressBar.Large style 51 | private static final int CIRCLE_DIAMETER_LARGE = 56; 52 | private static final float CENTER_RADIUS_LARGE = 12.5f; 53 | private static final float STROKE_WIDTH_LARGE = 3f; 54 | 55 | private final int[] COLORS = new int[] { 56 | Color.BLACK 57 | }; 58 | 59 | /** 60 | * The value in the linear interpolator for animating the drawable at which 61 | * the color transition should start 62 | */ 63 | private static final float COLOR_START_DELAY_OFFSET = 0.75f; 64 | private static final float END_TRIM_START_DELAY_OFFSET = 0.5f; 65 | private static final float START_TRIM_DURATION_OFFSET = 0.5f; 66 | 67 | /** The duration of a single progress spin in milliseconds. */ 68 | private static final int ANIMATION_DURATION = 1332; 69 | 70 | /** The number of points in the progress "star". */ 71 | private static final float NUM_POINTS = 5f; 72 | /** The list of animators operating on this drawable. */ 73 | private final ArrayList mAnimators = new ArrayList(); 74 | 75 | /** The indicator ring, used to manage animation state. */ 76 | private final Ring mRing; 77 | 78 | /** Canvas rotation in degrees. */ 79 | private float mRotation; 80 | 81 | /** Layout info for the arrowhead in dp */ 82 | private static final int ARROW_WIDTH = 10; 83 | private static final int ARROW_HEIGHT = 5; 84 | private static final float ARROW_OFFSET_ANGLE = 5; 85 | 86 | /** Layout info for the arrowhead for the large spinner in dp */ 87 | private static final int ARROW_WIDTH_LARGE = 12; 88 | private static final int ARROW_HEIGHT_LARGE = 6; 89 | private static final float MAX_PROGRESS_ARC = .8f; 90 | 91 | private Resources mResources; 92 | private View mParent; 93 | private Animation mAnimation; 94 | private float mRotationCount; 95 | private double mWidth; 96 | private double mHeight; 97 | boolean mFinishing; 98 | 99 | public MaterialProgressDrawable(Context context, View parent) { 100 | mParent = parent; 101 | mResources = context.getResources(); 102 | 103 | mRing = new Ring(mCallback); 104 | mRing.setColors(COLORS); 105 | 106 | updateSizes(DEFAULT); 107 | setupAnimators(); 108 | } 109 | 110 | private void setSizeParameters(double progressCircleWidth, double progressCircleHeight, 111 | double centerRadius, double strokeWidth, float arrowWidth, float arrowHeight) { 112 | final Ring ring = mRing; 113 | final DisplayMetrics metrics = mResources.getDisplayMetrics(); 114 | final float screenDensity = metrics.density; 115 | 116 | mWidth = progressCircleWidth * screenDensity; 117 | mHeight = progressCircleHeight * screenDensity; 118 | ring.setStrokeWidth((float) strokeWidth * screenDensity); 119 | ring.setCenterRadius(centerRadius * screenDensity); 120 | ring.setColorIndex(0); 121 | ring.setArrowDimensions(arrowWidth * screenDensity, arrowHeight * screenDensity); 122 | ring.setInsets((int) mWidth, (int) mHeight); 123 | } 124 | 125 | /** 126 | * Set the overall size for the progress spinner. This updates the radius 127 | * and stroke width of the ring. 128 | * 129 | * @param size One of {@link MaterialProgressDrawable.LARGE} or 130 | * {@link MaterialProgressDrawable.DEFAULT} 131 | */ 132 | public void updateSizes(@ProgressDrawableSize int size) { 133 | if (size == LARGE) { 134 | setSizeParameters(CIRCLE_DIAMETER_LARGE, CIRCLE_DIAMETER_LARGE, CENTER_RADIUS_LARGE, 135 | STROKE_WIDTH_LARGE, ARROW_WIDTH_LARGE, ARROW_HEIGHT_LARGE); 136 | } else { 137 | setSizeParameters(CIRCLE_DIAMETER, CIRCLE_DIAMETER, CENTER_RADIUS, STROKE_WIDTH, 138 | ARROW_WIDTH, ARROW_HEIGHT); 139 | } 140 | } 141 | 142 | /** 143 | * @param show Set to true to display the arrowhead on the progress spinner. 144 | */ 145 | public void showArrow(boolean show) { 146 | mRing.setShowArrow(show); 147 | } 148 | 149 | /** 150 | * @param scale Set the scale of the arrowhead for the spinner. 151 | */ 152 | public void setArrowScale(float scale) { 153 | mRing.setArrowScale(scale); 154 | } 155 | 156 | /** 157 | * Set the start and end trim for the progress spinner arc. 158 | * 159 | * @param startAngle start angle 160 | * @param endAngle end angle 161 | */ 162 | public void setStartEndTrim(float startAngle, float endAngle) { 163 | mRing.setStartTrim(startAngle); 164 | mRing.setEndTrim(endAngle); 165 | } 166 | 167 | /** 168 | * Set the amount of rotation to apply to the progress spinner. 169 | * 170 | * @param rotation Rotation is from [0..1] 171 | */ 172 | public void setProgressRotation(float rotation) { 173 | mRing.setRotation(rotation); 174 | } 175 | 176 | /** 177 | * Update the background color of the circle image view. 178 | */ 179 | public void setBackgroundColor(int color) { 180 | mRing.setBackgroundColor(color); 181 | } 182 | 183 | /** 184 | * Set the colors used in the progress animation from color resources. 185 | * The first color will also be the color of the bar that grows in response 186 | * to a user swipe gesture. 187 | * 188 | * @param colors 189 | */ 190 | public void setColorSchemeColors(int... colors) { 191 | mRing.setColors(colors); 192 | mRing.setColorIndex(0); 193 | } 194 | 195 | @Override 196 | public int getIntrinsicHeight() { 197 | return (int) mHeight; 198 | } 199 | 200 | @Override 201 | public int getIntrinsicWidth() { 202 | return (int) mWidth; 203 | } 204 | 205 | @Override 206 | public void draw(Canvas c) { 207 | final Rect bounds = getBounds(); 208 | final int saveCount = c.save(); 209 | c.rotate(mRotation, bounds.exactCenterX(), bounds.exactCenterY()); 210 | mRing.draw(c, bounds); 211 | c.restoreToCount(saveCount); 212 | } 213 | 214 | @Override 215 | public void setAlpha(int alpha) { 216 | mRing.setAlpha(alpha); 217 | } 218 | 219 | public int getAlpha() { 220 | return mRing.getAlpha(); 221 | } 222 | 223 | @Override 224 | public void setColorFilter(ColorFilter colorFilter) { 225 | mRing.setColorFilter(colorFilter); 226 | } 227 | 228 | @SuppressWarnings("unused") 229 | void setRotation(float rotation) { 230 | mRotation = rotation; 231 | invalidateSelf(); 232 | } 233 | 234 | @SuppressWarnings("unused") 235 | private float getRotation() { 236 | return mRotation; 237 | } 238 | 239 | @Override 240 | public int getOpacity() { 241 | return PixelFormat.TRANSLUCENT; 242 | } 243 | 244 | @Override 245 | public boolean isRunning() { 246 | final ArrayList animators = mAnimators; 247 | final int N = animators.size(); 248 | for (int i = 0; i < N; i++) { 249 | final Animation animator = animators.get(i); 250 | if (animator.hasStarted() && !animator.hasEnded()) { 251 | return true; 252 | } 253 | } 254 | return false; 255 | } 256 | 257 | @Override 258 | public void start() { 259 | mAnimation.reset(); 260 | mRing.storeOriginals(); 261 | // Already showing some part of the ring 262 | if (mRing.getEndTrim() != mRing.getStartTrim()) { 263 | mFinishing = true; 264 | mAnimation.setDuration(ANIMATION_DURATION/2); 265 | mParent.startAnimation(mAnimation); 266 | } else { 267 | mRing.setColorIndex(0); 268 | mRing.resetOriginals(); 269 | mAnimation.setDuration(ANIMATION_DURATION); 270 | mParent.startAnimation(mAnimation); 271 | } 272 | } 273 | 274 | @Override 275 | public void stop() { 276 | mParent.clearAnimation(); 277 | setRotation(0); 278 | mRing.setShowArrow(false); 279 | mRing.setColorIndex(0); 280 | mRing.resetOriginals(); 281 | } 282 | 283 | private float getMinProgressArc(Ring ring) { 284 | return (float) Math.toRadians( 285 | ring.getStrokeWidth() / (2 * Math.PI * ring.getCenterRadius())); 286 | } 287 | 288 | // Adapted from ArgbEvaluator.java 289 | private int evaluateColorChange(float fraction, int startValue, int endValue) { 290 | int startInt = (Integer) startValue; 291 | int startA = (startInt >> 24) & 0xff; 292 | int startR = (startInt >> 16) & 0xff; 293 | int startG = (startInt >> 8) & 0xff; 294 | int startB = startInt & 0xff; 295 | 296 | int endInt = (Integer) endValue; 297 | int endA = (endInt >> 24) & 0xff; 298 | int endR = (endInt >> 16) & 0xff; 299 | int endG = (endInt >> 8) & 0xff; 300 | int endB = endInt & 0xff; 301 | 302 | return (int)((startA + (int)(fraction * (endA - startA))) << 24) | 303 | (int)((startR + (int)(fraction * (endR - startR))) << 16) | 304 | (int)((startG + (int)(fraction * (endG - startG))) << 8) | 305 | (int)((startB + (int)(fraction * (endB - startB)))); 306 | } 307 | 308 | /** 309 | * Update the ring color if this is within the last 25% of the animation. 310 | * The new ring color will be a translation from the starting ring color to 311 | * the next color. 312 | */ 313 | private void updateRingColor(float interpolatedTime, Ring ring) { 314 | if (interpolatedTime > COLOR_START_DELAY_OFFSET) { 315 | // scale the interpolatedTime so that the full 316 | // transformation from 0 - 1 takes place in the 317 | // remaining time 318 | ring.setColor(evaluateColorChange((interpolatedTime - COLOR_START_DELAY_OFFSET) 319 | / (1.0f - COLOR_START_DELAY_OFFSET), ring.getStartingColor(), 320 | ring.getNextColor())); 321 | } 322 | } 323 | 324 | private void applyFinishTranslation(float interpolatedTime, Ring ring) { 325 | // shrink back down and complete a full rotation before 326 | // starting other circles 327 | // Rotation goes between [0..1]. 328 | updateRingColor(interpolatedTime, ring); 329 | float targetRotation = (float) (Math.floor(ring.getStartingRotation() / MAX_PROGRESS_ARC) 330 | + 1f); 331 | final float minProgressArc = getMinProgressArc(ring); 332 | final float startTrim = ring.getStartingStartTrim() 333 | + (ring.getStartingEndTrim() - minProgressArc - ring.getStartingStartTrim()) 334 | * interpolatedTime; 335 | ring.setStartTrim(startTrim); 336 | ring.setEndTrim(ring.getStartingEndTrim()); 337 | final float rotation = ring.getStartingRotation() 338 | + ((targetRotation - ring.getStartingRotation()) * interpolatedTime); 339 | ring.setRotation(rotation); 340 | } 341 | 342 | private void setupAnimators() { 343 | final Ring ring = mRing; 344 | final Animation animation = new Animation() { 345 | @Override 346 | public void applyTransformation(float interpolatedTime, Transformation t) { 347 | if (mFinishing) { 348 | applyFinishTranslation(interpolatedTime, ring); 349 | } else { 350 | // The minProgressArc is calculated from 0 to create an 351 | // angle that matches the stroke width. 352 | final float minProgressArc = getMinProgressArc(ring); 353 | final float startingEndTrim = ring.getStartingEndTrim(); 354 | final float startingTrim = ring.getStartingStartTrim(); 355 | final float startingRotation = ring.getStartingRotation(); 356 | 357 | updateRingColor(interpolatedTime, ring); 358 | 359 | // Moving the start trim only occurs in the first 50% of a 360 | // single ring animation 361 | if (interpolatedTime <= START_TRIM_DURATION_OFFSET) { 362 | // scale the interpolatedTime so that the full 363 | // transformation from 0 - 1 takes place in the 364 | // remaining time 365 | final float scaledTime = (interpolatedTime) 366 | / (1.0f - START_TRIM_DURATION_OFFSET); 367 | final float startTrim = startingTrim 368 | + ((MAX_PROGRESS_ARC - minProgressArc) * MATERIAL_INTERPOLATOR 369 | .getInterpolation(scaledTime)); 370 | ring.setStartTrim(startTrim); 371 | } 372 | 373 | // Moving the end trim starts after 50% of a single ring 374 | // animation completes 375 | if (interpolatedTime > END_TRIM_START_DELAY_OFFSET) { 376 | // scale the interpolatedTime so that the full 377 | // transformation from 0 - 1 takes place in the 378 | // remaining time 379 | final float minArc = MAX_PROGRESS_ARC - minProgressArc; 380 | float scaledTime = (interpolatedTime - START_TRIM_DURATION_OFFSET) 381 | / (1.0f - START_TRIM_DURATION_OFFSET); 382 | final float endTrim = startingEndTrim 383 | + (minArc * MATERIAL_INTERPOLATOR.getInterpolation(scaledTime)); 384 | ring.setEndTrim(endTrim); 385 | } 386 | 387 | final float rotation = startingRotation + (0.25f * interpolatedTime); 388 | ring.setRotation(rotation); 389 | 390 | float groupRotation = ((FULL_ROTATION / NUM_POINTS) * interpolatedTime) 391 | + (FULL_ROTATION * (mRotationCount / NUM_POINTS)); 392 | setRotation(groupRotation); 393 | } 394 | } 395 | }; 396 | animation.setRepeatCount(Animation.INFINITE); 397 | animation.setRepeatMode(Animation.RESTART); 398 | animation.setInterpolator(LINEAR_INTERPOLATOR); 399 | animation.setAnimationListener(new Animation.AnimationListener() { 400 | 401 | @Override 402 | public void onAnimationStart(Animation animation) { 403 | mRotationCount = 0; 404 | } 405 | 406 | @Override 407 | public void onAnimationEnd(Animation animation) { 408 | // do nothing 409 | } 410 | 411 | @Override 412 | public void onAnimationRepeat(Animation animation) { 413 | ring.storeOriginals(); 414 | ring.goToNextColor(); 415 | ring.setStartTrim(ring.getEndTrim()); 416 | if (mFinishing) { 417 | // finished closing the last ring from the swipe gesture; go 418 | // into progress mode 419 | mFinishing = false; 420 | animation.setDuration(ANIMATION_DURATION); 421 | ring.setShowArrow(false); 422 | } else { 423 | mRotationCount = (mRotationCount + 1) % (NUM_POINTS); 424 | } 425 | } 426 | }); 427 | mAnimation = animation; 428 | } 429 | 430 | private final Callback mCallback = new Callback() { 431 | @Override 432 | public void invalidateDrawable(Drawable d) { 433 | invalidateSelf(); 434 | } 435 | 436 | @Override 437 | public void scheduleDrawable(Drawable d, Runnable what, long when) { 438 | scheduleSelf(what, when); 439 | } 440 | 441 | @Override 442 | public void unscheduleDrawable(Drawable d, Runnable what) { 443 | unscheduleSelf(what); 444 | } 445 | }; 446 | 447 | private static class Ring { 448 | private final RectF mTempBounds = new RectF(); 449 | private final Paint mPaint = new Paint(); 450 | private final Paint mArrowPaint = new Paint(); 451 | 452 | private final Callback mCallback; 453 | 454 | private float mStartTrim = 0.0f; 455 | private float mEndTrim = 0.0f; 456 | private float mRotation = 0.0f; 457 | private float mStrokeWidth = 5.0f; 458 | private float mStrokeInset = 2.5f; 459 | 460 | private int[] mColors; 461 | // mColorIndex represents the offset into the available mColors that the 462 | // progress circle should currently display. As the progress circle is 463 | // animating, the mColorIndex moves by one to the next available color. 464 | private int mColorIndex; 465 | private float mStartingStartTrim; 466 | private float mStartingEndTrim; 467 | private float mStartingRotation; 468 | private boolean mShowArrow; 469 | private Path mArrow; 470 | private float mArrowScale; 471 | private double mRingCenterRadius; 472 | private int mArrowWidth; 473 | private int mArrowHeight; 474 | private int mAlpha; 475 | private final Paint mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); 476 | private int mBackgroundColor; 477 | private int mCurrentColor; 478 | 479 | public Ring(Callback callback) { 480 | mCallback = callback; 481 | 482 | mPaint.setStrokeCap(Paint.Cap.SQUARE); 483 | mPaint.setAntiAlias(true); 484 | mPaint.setStyle(Paint.Style.STROKE); 485 | 486 | mArrowPaint.setStyle(Paint.Style.FILL); 487 | mArrowPaint.setAntiAlias(true); 488 | } 489 | 490 | public void setBackgroundColor(int color) { 491 | mBackgroundColor = color; 492 | } 493 | 494 | /** 495 | * Set the dimensions of the arrowhead. 496 | * 497 | * @param width Width of the hypotenuse of the arrow head 498 | * @param height Height of the arrow point 499 | */ 500 | public void setArrowDimensions(float width, float height) { 501 | mArrowWidth = (int) width; 502 | mArrowHeight = (int) height; 503 | } 504 | 505 | /** 506 | * Draw the progress spinner 507 | */ 508 | public void draw(Canvas c, Rect bounds) { 509 | final RectF arcBounds = mTempBounds; 510 | arcBounds.set(bounds); 511 | arcBounds.inset(mStrokeInset, mStrokeInset); 512 | 513 | final float startAngle = (mStartTrim + mRotation) * 360; 514 | final float endAngle = (mEndTrim + mRotation) * 360; 515 | float sweepAngle = endAngle - startAngle; 516 | 517 | mPaint.setColor(mCurrentColor); 518 | c.drawArc(arcBounds, startAngle, sweepAngle, false, mPaint); 519 | 520 | drawTriangle(c, startAngle, sweepAngle, bounds); 521 | 522 | if (mAlpha < 255) { 523 | mCirclePaint.setColor(mBackgroundColor); 524 | mCirclePaint.setAlpha(255 - mAlpha); 525 | c.drawCircle(bounds.exactCenterX(), bounds.exactCenterY(), bounds.width() / 2, 526 | mCirclePaint); 527 | } 528 | } 529 | 530 | private void drawTriangle(Canvas c, float startAngle, float sweepAngle, Rect bounds) { 531 | if (mShowArrow) { 532 | if (mArrow == null) { 533 | mArrow = new Path(); 534 | mArrow.setFillType(Path.FillType.EVEN_ODD); 535 | } else { 536 | mArrow.reset(); 537 | } 538 | 539 | // Adjust the position of the triangle so that it is inset as 540 | // much as the arc, but also centered on the arc. 541 | float inset = (int) mStrokeInset / 2 * mArrowScale; 542 | float x = (float) (mRingCenterRadius * Math.cos(0) + bounds.exactCenterX()); 543 | float y = (float) (mRingCenterRadius * Math.sin(0) + bounds.exactCenterY()); 544 | 545 | // Update the path each time. This works around an issue in SKIA 546 | // where concatenating a rotation matrix to a scale matrix 547 | // ignored a starting negative rotation. This appears to have 548 | // been fixed as of API 21. 549 | mArrow.moveTo(0, 0); 550 | mArrow.lineTo(mArrowWidth * mArrowScale, 0); 551 | mArrow.lineTo((mArrowWidth * mArrowScale / 2), (mArrowHeight 552 | * mArrowScale)); 553 | mArrow.offset(x - inset, y); 554 | mArrow.close(); 555 | // draw a triangle 556 | mArrowPaint.setColor(mCurrentColor); 557 | c.rotate(startAngle + sweepAngle - ARROW_OFFSET_ANGLE, bounds.exactCenterX(), 558 | bounds.exactCenterY()); 559 | c.drawPath(mArrow, mArrowPaint); 560 | } 561 | } 562 | 563 | /** 564 | * Set the colors the progress spinner alternates between. 565 | * 566 | * @param colors Array of integers describing the colors. Must be non-null. 567 | */ 568 | public void setColors(@NonNull int[] colors) { 569 | mColors = colors; 570 | // if colors are reset, make sure to reset the color index as well 571 | setColorIndex(0); 572 | } 573 | 574 | /** 575 | * Set the absolute color of the progress spinner. This is should only 576 | * be used when animating between current and next color when the 577 | * spinner is rotating. 578 | * 579 | * @param color int describing the color. 580 | */ 581 | public void setColor(int color) { 582 | mCurrentColor = color; 583 | } 584 | 585 | /** 586 | * @param index Index into the color array of the color to display in 587 | * the progress spinner. 588 | */ 589 | public void setColorIndex(int index) { 590 | mColorIndex = index; 591 | mCurrentColor = mColors[mColorIndex]; 592 | } 593 | 594 | /** 595 | * @return int describing the next color the progress spinner should use when drawing. 596 | */ 597 | public int getNextColor() { 598 | return mColors[getNextColorIndex()]; 599 | } 600 | 601 | private int getNextColorIndex() { 602 | return (mColorIndex + 1) % (mColors.length); 603 | } 604 | 605 | /** 606 | * Proceed to the next available ring color. This will automatically 607 | * wrap back to the beginning of colors. 608 | */ 609 | public void goToNextColor() { 610 | setColorIndex(getNextColorIndex()); 611 | } 612 | 613 | public void setColorFilter(ColorFilter filter) { 614 | mPaint.setColorFilter(filter); 615 | invalidateSelf(); 616 | } 617 | 618 | /** 619 | * @param alpha Set the alpha of the progress spinner and associated arrowhead. 620 | */ 621 | public void setAlpha(int alpha) { 622 | mAlpha = alpha; 623 | } 624 | 625 | /** 626 | * @return Current alpha of the progress spinner and arrowhead. 627 | */ 628 | public int getAlpha() { 629 | return mAlpha; 630 | } 631 | 632 | /** 633 | * @param strokeWidth Set the stroke width of the progress spinner in pixels. 634 | */ 635 | public void setStrokeWidth(float strokeWidth) { 636 | mStrokeWidth = strokeWidth; 637 | mPaint.setStrokeWidth(strokeWidth); 638 | invalidateSelf(); 639 | } 640 | 641 | @SuppressWarnings("unused") 642 | public float getStrokeWidth() { 643 | return mStrokeWidth; 644 | } 645 | 646 | @SuppressWarnings("unused") 647 | public void setStartTrim(float startTrim) { 648 | mStartTrim = startTrim; 649 | invalidateSelf(); 650 | } 651 | 652 | @SuppressWarnings("unused") 653 | public float getStartTrim() { 654 | return mStartTrim; 655 | } 656 | 657 | public float getStartingStartTrim() { 658 | return mStartingStartTrim; 659 | } 660 | 661 | public float getStartingEndTrim() { 662 | return mStartingEndTrim; 663 | } 664 | 665 | public int getStartingColor() { 666 | return mColors[mColorIndex]; 667 | } 668 | 669 | @SuppressWarnings("unused") 670 | public void setEndTrim(float endTrim) { 671 | mEndTrim = endTrim; 672 | invalidateSelf(); 673 | } 674 | 675 | @SuppressWarnings("unused") 676 | public float getEndTrim() { 677 | return mEndTrim; 678 | } 679 | 680 | @SuppressWarnings("unused") 681 | public void setRotation(float rotation) { 682 | mRotation = rotation; 683 | invalidateSelf(); 684 | } 685 | 686 | @SuppressWarnings("unused") 687 | public float getRotation() { 688 | return mRotation; 689 | } 690 | 691 | public void setInsets(int width, int height) { 692 | final float minEdge = (float) Math.min(width, height); 693 | float insets; 694 | if (mRingCenterRadius <= 0 || minEdge < 0) { 695 | insets = (float) Math.ceil(mStrokeWidth / 2.0f); 696 | } else { 697 | insets = (float) (minEdge / 2.0f - mRingCenterRadius); 698 | } 699 | mStrokeInset = insets; 700 | } 701 | 702 | @SuppressWarnings("unused") 703 | public float getInsets() { 704 | return mStrokeInset; 705 | } 706 | 707 | /** 708 | * @param centerRadius Inner radius in px of the circle the progress 709 | * spinner arc traces. 710 | */ 711 | public void setCenterRadius(double centerRadius) { 712 | mRingCenterRadius = centerRadius; 713 | } 714 | 715 | public double getCenterRadius() { 716 | return mRingCenterRadius; 717 | } 718 | 719 | /** 720 | * @param show Set to true to show the arrow head on the progress spinner. 721 | */ 722 | public void setShowArrow(boolean show) { 723 | if (mShowArrow != show) { 724 | mShowArrow = show; 725 | invalidateSelf(); 726 | } 727 | } 728 | 729 | /** 730 | * @param scale Set the scale of the arrowhead for the spinner. 731 | */ 732 | public void setArrowScale(float scale) { 733 | if (scale != mArrowScale) { 734 | mArrowScale = scale; 735 | invalidateSelf(); 736 | } 737 | } 738 | 739 | /** 740 | * @return The amount the progress spinner is currently rotated, between [0..1]. 741 | */ 742 | public float getStartingRotation() { 743 | return mStartingRotation; 744 | } 745 | 746 | /** 747 | * If the start / end trim are offset to begin with, store them so that 748 | * animation starts from that offset. 749 | */ 750 | public void storeOriginals() { 751 | mStartingStartTrim = mStartTrim; 752 | mStartingEndTrim = mEndTrim; 753 | mStartingRotation = mRotation; 754 | } 755 | 756 | /** 757 | * Reset the progress spinner to default rotation, start and end angles. 758 | */ 759 | public void resetOriginals() { 760 | mStartingStartTrim = 0; 761 | mStartingEndTrim = 0; 762 | mStartingRotation = 0; 763 | setStartTrim(0); 764 | setEndTrim(0); 765 | setRotation(0); 766 | } 767 | 768 | private void invalidateSelf() { 769 | mCallback.invalidateDrawable(null); 770 | } 771 | } 772 | } 773 | -------------------------------------------------------------------------------- /app/src/main/java/com/asdzheng/suitedrecyclerview/ui/view/waveswiperefreshlayout/SolidBelowWave.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.suitedrecyclerview.ui.view.waveswiperefreshlayout; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Paint; 6 | import android.util.AttributeSet; 7 | import android.view.View; 8 | import android.widget.LinearLayout; 9 | 10 | 11 | public class SolidBelowWave extends View { 12 | 13 | private Paint aboveWavePaint; 14 | private Paint blowWavePaint; 15 | 16 | public SolidBelowWave(Context context, AttributeSet attrs) { 17 | this(context, attrs, 0); 18 | } 19 | 20 | public SolidBelowWave(Context context, AttributeSet attrs, int defStyleAttr) { 21 | super(context, attrs, defStyleAttr); 22 | LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); 23 | params.weight = 1; 24 | setLayoutParams(params); 25 | } 26 | 27 | public void setAboveWavePaint(Paint aboveWavePaint) { 28 | this.aboveWavePaint = aboveWavePaint; 29 | } 30 | 31 | public void setBlowWavePaint(Paint blowWavePaint) { 32 | this.blowWavePaint = blowWavePaint; 33 | } 34 | 35 | @Override 36 | protected void onDraw(Canvas canvas) { 37 | super.onDraw(canvas); 38 | canvas.drawRect(getLeft(), 0, getRight(), getBottom(), blowWavePaint); 39 | canvas.drawRect(getLeft(), 0, getRight(), getBottom(), aboveWavePaint); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/asdzheng/suitedrecyclerview/ui/view/waveswiperefreshlayout/WaterWave.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.suitedrecyclerview.ui.view.waveswiperefreshlayout; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Color; 6 | import android.os.Parcel; 7 | import android.os.Parcelable; 8 | import android.util.AttributeSet; 9 | import android.view.ViewGroup; 10 | import android.widget.LinearLayout; 11 | 12 | import com.asdzheng.suitedrecyclerview.R; 13 | 14 | public class WaterWave extends LinearLayout { 15 | protected static final int LARGE = 1; 16 | protected static final int MIDDLE = 2; 17 | protected static final int LITTLE = 3; 18 | 19 | private int mAboveWaveColor; 20 | private int mBlowWaveColor; 21 | private int mProgress; 22 | private int mWaveHeight; 23 | private int mWaveMultiple; 24 | private int mWaveHz; 25 | 26 | private int mWaveToTop; 27 | 28 | private Wave mWave; 29 | private SolidBelowWave mSolidBelowWave; 30 | 31 | private final int DEFAULT_ABOVE_WAVE_COLOR = Color.WHITE; 32 | private final int DEFAULT_BLOW_WAVE_COLOR = Color.WHITE; 33 | private final int DEFAULT_PROGRESS = 0; 34 | 35 | 36 | public WaterWave(Context context, AttributeSet attrs) { 37 | super(context, attrs); 38 | setOrientation(VERTICAL); 39 | 40 | 41 | final TypedArray attributes = context.obtainStyledAttributes(attrs, R.styleable.WaterWave); 42 | mAboveWaveColor = attributes.getColor(R.styleable.WaterWave_above_wave_color, DEFAULT_ABOVE_WAVE_COLOR); 43 | mBlowWaveColor = attributes.getColor(R.styleable.WaterWave_blow_wave_color, DEFAULT_BLOW_WAVE_COLOR); 44 | mProgress = attributes.getInt(R.styleable.WaterWave_progress, DEFAULT_PROGRESS); 45 | mWaveHeight = attributes.getInt(R.styleable.WaterWave_wave_height, MIDDLE); 46 | mWaveMultiple = attributes.getInt(R.styleable.WaterWave_wave_length, LARGE); 47 | mWaveHz = attributes.getInt(R.styleable.WaterWave_wave_hz, LARGE); 48 | attributes.recycle(); 49 | 50 | mWave = new Wave(context, null); 51 | mWave.initializeWaveSize(mWaveMultiple, mWaveHeight, mWaveHz); 52 | mWave.setAboveWaveColor(mAboveWaveColor); 53 | mWave.setBlowWaveColor(mBlowWaveColor); 54 | mWave.initializePainters(); 55 | 56 | mSolidBelowWave = new SolidBelowWave(context, null); 57 | mSolidBelowWave.setAboveWavePaint(mWave.getAboveWavePaint()); 58 | mSolidBelowWave.setBlowWavePaint(mWave.getBlowWavePaint()); 59 | 60 | addView(mWave); 61 | addView(mSolidBelowWave); 62 | 63 | setProgress(mProgress); 64 | 65 | } 66 | 67 | public void setProgress(int progress) { 68 | this.mProgress = progress > 100 ? 100 : progress; 69 | computeWaveToTop(); 70 | } 71 | 72 | @Override 73 | public void onWindowFocusChanged(boolean hasWindowFocus) { 74 | super.onWindowFocusChanged(hasWindowFocus); 75 | if (hasWindowFocus) { 76 | computeWaveToTop(); 77 | } 78 | } 79 | 80 | private void computeWaveToTop() { 81 | mWaveToTop = (int) (getHeight() * (1f - mProgress / 100f)); 82 | ViewGroup.LayoutParams params = mWave.getLayoutParams(); 83 | if (params != null) { 84 | ((LayoutParams) params).topMargin = mWaveToTop; 85 | } 86 | mWave.setLayoutParams(params); 87 | } 88 | 89 | @Override 90 | public Parcelable onSaveInstanceState() { 91 | 92 | Parcelable superState = super.onSaveInstanceState(); 93 | SavedState ss = new SavedState(superState); 94 | ss.progress = mProgress; 95 | return ss; 96 | } 97 | 98 | @Override 99 | public void onRestoreInstanceState(Parcelable state) { 100 | SavedState ss = (SavedState) state; 101 | super.onRestoreInstanceState(ss.getSuperState()); 102 | setProgress(ss.progress); 103 | } 104 | 105 | private static class SavedState extends BaseSavedState { 106 | int progress; 107 | 108 | 109 | SavedState(Parcelable superState) { 110 | super(superState); 111 | } 112 | 113 | 114 | private SavedState(Parcel in) { 115 | super(in); 116 | progress = in.readInt(); 117 | } 118 | 119 | @Override 120 | public void writeToParcel(Parcel out, int flags) { 121 | super.writeToParcel(out, flags); 122 | out.writeInt(progress); 123 | } 124 | 125 | public static final Creator CREATOR = new Creator() { 126 | public SavedState createFromParcel(Parcel in) { 127 | return new SavedState(in); 128 | } 129 | 130 | public SavedState[] newArray(int size) { 131 | return new SavedState[size]; 132 | } 133 | }; 134 | } 135 | 136 | 137 | 138 | 139 | } 140 | -------------------------------------------------------------------------------- /app/src/main/java/com/asdzheng/suitedrecyclerview/ui/view/waveswiperefreshlayout/Wave.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.suitedrecyclerview.ui.view.waveswiperefreshlayout; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Paint; 6 | import android.graphics.Path; 7 | import android.util.AttributeSet; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | 11 | public class Wave extends View { 12 | private final int WAVE_HEIGHT_LARGE = 46; 13 | private final int WAVE_HEIGHT_MIDDLE = 12; 14 | private final int WAVE_HEIGHT_LITTLE = 5; 15 | 16 | private final float WAVE_LENGTH_MULTIPLE_LARGE = 1.5f; 17 | private final float WAVE_LENGTH_MULTIPLE_MIDDLE = 1f; 18 | private final float WAVE_LENGTH_MULTIPLE_LITTLE = 0.5f; 19 | 20 | private final float WAVE_HZ_FAST = 0.12f; 21 | private final float WAVE_HZ_NORMAL = 0.09f; 22 | private final float WAVE_HZ_SLOW = 0.05f; 23 | 24 | public final int DEFAULT_ABOVE_WAVE_ALPHA = 100; 25 | public final int DEFAULT_BLOW_WAVE_ALPHA = 200; 26 | 27 | private final float X_SPACE = 20; 28 | private final double PI2 = 2 * Math.PI; 29 | 30 | private Path mAboveWavePath = new Path(); 31 | private Path mBlowWavePath = new Path(); 32 | 33 | private Paint mAboveWavePaint = new Paint(); 34 | private Paint mBlowWavePaint = new Paint(); 35 | 36 | private int mAboveWaveColor; 37 | private int mBlowWaveColor; 38 | 39 | private float mWaveMultiple; 40 | private float mWaveLength; 41 | private int mWaveHeight; 42 | private float mMaxRight; 43 | private float mWaveHz; 44 | 45 | private float mAboveOffset = 0.0f; 46 | private float mBlowOffset; 47 | 48 | private RefreshProgressRunnable mRefreshProgressRunnable; 49 | 50 | private int left, right, bottom; 51 | 52 | private double omega; 53 | 54 | public Wave(Context context, AttributeSet attrs) { 55 | super(context, attrs); 56 | } 57 | 58 | 59 | @Override 60 | protected void onDraw(Canvas canvas) { 61 | super.onDraw(canvas); 62 | 63 | canvas.drawPath(mBlowWavePath, mBlowWavePaint); 64 | canvas.drawPath(mAboveWavePath, mAboveWavePaint); 65 | } 66 | 67 | public void setAboveWaveColor(int aboveWaveColor) { 68 | this.mAboveWaveColor = aboveWaveColor; 69 | } 70 | 71 | public void setBlowWaveColor(int blowWaveColor) { 72 | this.mBlowWaveColor = blowWaveColor; 73 | } 74 | 75 | public Paint getAboveWavePaint() { 76 | return mAboveWavePaint; 77 | } 78 | 79 | public Paint getBlowWavePaint() { 80 | return mBlowWavePaint; 81 | } 82 | 83 | public void initializeWaveSize(int waveMultiple, int waveHeight, int waveHz) { 84 | mWaveMultiple = getWaveMultiple(waveMultiple); 85 | mWaveHeight = getWaveHeight(waveHeight); 86 | mWaveHz = getWaveHz(waveHz); 87 | mBlowOffset = mWaveHeight * 0.8f; 88 | ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 89 | mWaveHeight * 2); 90 | setLayoutParams(params); 91 | } 92 | 93 | public void initializePainters() { 94 | mAboveWavePaint.setColor(mAboveWaveColor); 95 | mAboveWavePaint.setAlpha(DEFAULT_ABOVE_WAVE_ALPHA); 96 | mAboveWavePaint.setStyle(Paint.Style.FILL); 97 | mAboveWavePaint.setAntiAlias(true); 98 | 99 | mBlowWavePaint.setColor(mBlowWaveColor); 100 | mBlowWavePaint.setAlpha(DEFAULT_BLOW_WAVE_ALPHA); 101 | mBlowWavePaint.setStyle(Paint.Style.FILL); 102 | mBlowWavePaint.setAntiAlias(true); 103 | } 104 | 105 | private float getWaveMultiple(int size) { 106 | switch (size) { 107 | case WaterWave.LARGE: 108 | return WAVE_LENGTH_MULTIPLE_LARGE; 109 | case WaterWave.MIDDLE: 110 | return WAVE_LENGTH_MULTIPLE_MIDDLE; 111 | case WaterWave.LITTLE: 112 | return WAVE_LENGTH_MULTIPLE_LITTLE; 113 | } 114 | return 0; 115 | } 116 | 117 | private int getWaveHeight(int size) { 118 | switch (size) { 119 | case WaterWave.LARGE: 120 | return WAVE_HEIGHT_LARGE; 121 | case WaterWave.MIDDLE: 122 | return WAVE_HEIGHT_MIDDLE; 123 | case WaterWave.LITTLE: 124 | return WAVE_HEIGHT_LITTLE; 125 | } 126 | return 0; 127 | } 128 | 129 | private float getWaveHz(int size) { 130 | switch (size) { 131 | case WaterWave.LARGE: 132 | return WAVE_HZ_FAST; 133 | case WaterWave.MIDDLE: 134 | return WAVE_HZ_NORMAL; 135 | case WaterWave.LITTLE: 136 | return WAVE_HZ_SLOW; 137 | } 138 | return 0; 139 | } 140 | 141 | /** 142 | * calculate wave track 143 | */ 144 | private void calculatePath() { 145 | mAboveWavePath.reset(); 146 | mBlowWavePath.reset(); 147 | 148 | getWaveOffset(); 149 | 150 | float y; 151 | mAboveWavePath.moveTo(left, bottom); 152 | for (float x = 0; x <= mMaxRight; x += X_SPACE) { 153 | y = (float) (mWaveHeight * Math.sin(omega * x + mAboveOffset) + mWaveHeight); 154 | mAboveWavePath.lineTo(x, y); 155 | } 156 | mAboveWavePath.lineTo(right, bottom); 157 | 158 | mBlowWavePath.moveTo(left, bottom); 159 | for (float x = 0; x <= mMaxRight; x += X_SPACE) { 160 | y = (float) (mWaveHeight * Math.sin(omega * x + mBlowOffset) + mWaveHeight); 161 | mBlowWavePath.lineTo(x, y); 162 | } 163 | mBlowWavePath.lineTo(right, bottom); 164 | } 165 | 166 | @Override 167 | protected void onWindowVisibilityChanged(int visibility) { 168 | super.onWindowVisibilityChanged(visibility); 169 | if (View.GONE == visibility) { 170 | removeCallbacks(mRefreshProgressRunnable); 171 | } else { 172 | removeCallbacks(mRefreshProgressRunnable); 173 | mRefreshProgressRunnable = new RefreshProgressRunnable(); 174 | post(mRefreshProgressRunnable); 175 | } 176 | } 177 | 178 | @Override 179 | protected void onDetachedFromWindow() { 180 | super.onDetachedFromWindow(); 181 | } 182 | 183 | @Override 184 | public void onWindowFocusChanged(boolean hasWindowFocus) { 185 | super.onWindowFocusChanged(hasWindowFocus); 186 | if (hasWindowFocus) { 187 | if (mWaveLength == 0) { 188 | startWave(); 189 | } 190 | } 191 | } 192 | 193 | private void startWave() { 194 | if (getWidth() != 0) { 195 | int width = getWidth(); 196 | mWaveLength = width * mWaveMultiple; 197 | left = getLeft(); 198 | right = getRight(); 199 | bottom = getBottom() + 2; 200 | mMaxRight = right + X_SPACE; 201 | omega = PI2 / mWaveLength; 202 | } 203 | } 204 | 205 | private void getWaveOffset() { 206 | if (mBlowOffset > Float.MAX_VALUE - 100) { 207 | mBlowOffset = 0; 208 | } else { 209 | mBlowOffset += mWaveHz; 210 | } 211 | 212 | if (mAboveOffset > Float.MAX_VALUE - 100) { 213 | mAboveOffset = 0; 214 | } else { 215 | mAboveOffset += mWaveHz; 216 | } 217 | } 218 | 219 | private class RefreshProgressRunnable implements Runnable { 220 | public void run() { 221 | synchronized (Wave.this) { 222 | long start = System.currentTimeMillis(); 223 | 224 | calculatePath(); 225 | 226 | invalidate(); 227 | 228 | long gap = 16 - (System.currentTimeMillis() - start); 229 | postDelayed(this, gap < 0 ? 0 : gap); 230 | } 231 | } 232 | } 233 | 234 | } 235 | -------------------------------------------------------------------------------- /app/src/main/java/com/asdzheng/suitedrecyclerview/utils/ConfigConstants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file provided by Facebook is for non-commercial testing and evaluation 3 | * purposes only. Facebook reserves all rights not expressly granted. 4 | * 5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | package com.asdzheng.suitedrecyclerview.utils; 14 | 15 | 16 | public class ConfigConstants { 17 | 18 | public static final int KB = 1024; 19 | public static final int MB = 1024 * KB; 20 | 21 | private static final int MAX_HEAP_SIZE = (int) Runtime.getRuntime().maxMemory(); 22 | 23 | public static final int MAX_DISK_CACHE_SIZE = 50 * MB; 24 | public static final int MAX_MEMORY_CACHE_SIZE = MAX_HEAP_SIZE / 4; 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/asdzheng/suitedrecyclerview/utils/DisplayUtils.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.suitedrecyclerview.utils; 2 | 3 | import android.content.Context; 4 | import android.util.DisplayMetrics; 5 | import android.util.TypedValue; 6 | import android.view.WindowManager; 7 | 8 | import com.asdzheng.suitedrecyclerview.MyApplication; 9 | 10 | /** 11 | * Created by asdzheng on 2015/12/28. 12 | */ 13 | public class DisplayUtils { 14 | 15 | public static int dpToPx(final float n, final Context context) { 16 | return (int) TypedValue.applyDimension(1, n, context.getResources().getDisplayMetrics()); 17 | } 18 | 19 | public static int pxToDp(final int n, final Context context) { 20 | return (int)TypedValue.applyDimension(0, (float)n, context.getResources().getDisplayMetrics()); 21 | } 22 | 23 | // 获取 设备的Hight 24 | public static int getDeviceHeight(Context context) { 25 | DisplayMetrics dm = new DisplayMetrics(); 26 | WindowManager wm = (WindowManager) MyApplication.context.getSystemService(Context.WINDOW_SERVICE); 27 | wm.getDefaultDisplay().getMetrics(dm); 28 | return dm.heightPixels; 29 | } 30 | 31 | // 获取 设备的Width 32 | public static int getDisplayWidth() { 33 | DisplayMetrics dm = new DisplayMetrics(); 34 | WindowManager wm = (WindowManager) MyApplication.context.getSystemService(Context.WINDOW_SERVICE); 35 | wm.getDefaultDisplay().getMetrics(dm); 36 | return dm.widthPixels; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/asdzheng/suitedrecyclerview/utils/LogUtil.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.suitedrecyclerview.utils; 2 | 3 | import android.util.Log; 4 | 5 | /** 6 | * Created by asdzheng on 2015/12/28. 7 | */ 8 | public class LogUtil { 9 | 10 | public static final int VERBOSE = 1; 11 | public static final int DEBUG = 2; 12 | public static final int INFO = 3; 13 | public static final int WARN = 4; 14 | public static final int ERROR = 5; 15 | 16 | public static final int NOTHING = 6; 17 | 18 | public static final int LEVEL = 1; 19 | 20 | public static void v(String tag, String msg) { 21 | if (LEVEL <= VERBOSE) { 22 | Log.v(tag, msg); 23 | } 24 | } 25 | 26 | public static void d(String tag, String msg) { 27 | if (LEVEL <= DEBUG) { 28 | Log.d(tag, msg); 29 | } 30 | } 31 | 32 | public static void i(String tag, String msg) { 33 | if (LEVEL <= INFO) { 34 | Log.i(tag, msg); 35 | } 36 | } 37 | 38 | public static void w(String tag, String msg) { 39 | if (LEVEL <= WARN) { 40 | Log.w(tag, msg); 41 | } 42 | } 43 | 44 | public static void e(String tag, String msg) { 45 | if (LEVEL <= ERROR) { 46 | Log.e(tag, msg); 47 | } 48 | } 49 | 50 | 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/asdzheng/suitedrecyclerview/utils/transition/ActivityTransitionEnterHelper.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.suitedrecyclerview.utils.transition; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.view.View; 7 | 8 | import com.asdzheng.layoutmanager.Size; 9 | import com.asdzheng.suitedrecyclerview.utils.DisplayUtils; 10 | 11 | /** 12 | * Created by Bruce Too 13 | * On 9/26/15. 14 | * At 15:13 15 | * ActivityTransitionExitHelper和 ActivityTransitionEnterHelper 16 | * 实现了Activity直接切换的时候自定义view动画 17 | */ 18 | public class ActivityTransitionEnterHelper { 19 | 20 | public final static String PRE_NAME = "ActivityTransitionEnterHelper"; 21 | private final Activity activity; 22 | private View fromView;//the view where u click 23 | private String imgUrl;// the resource url of imageView etc.. 24 | private Bundle bundle; 25 | 26 | public ActivityTransitionEnterHelper(Activity activity) { 27 | this.activity = activity; 28 | } 29 | 30 | public static ActivityTransitionEnterHelper with(Activity activity) { 31 | return new ActivityTransitionEnterHelper(activity); 32 | } 33 | 34 | public ActivityTransitionEnterHelper fromView(View fromView) { 35 | //因为宽在大图中一直是全屏,高预估为宽放大的比例 36 | Size detailSize = new Size(DisplayUtils.getDisplayWidth(), (int)(fromView.getHeight() * 37 | (float) DisplayUtils.getDisplayWidth() / fromView.getWidth())); 38 | 39 | // LogUtil.w("Adapter", "Size = " + detailSize + " | getHeight = " + view.getHeight() + " scale =" + 40 | // Float.parseFloat(String.format("%.2f",(float)DisplayUtils.getDisplayWidth(context) / view.getWidth()))); 41 | 42 | bundle = new Bundle(); 43 | bundle.putSerializable("size", detailSize); 44 | bundle.putString("photo", fromView.getTag().toString()); 45 | this.fromView = fromView; 46 | return this; 47 | } 48 | 49 | public ActivityTransitionEnterHelper imageUrl(String imgUrl) { 50 | this.imgUrl = imgUrl; 51 | return this; 52 | } 53 | 54 | public ActivityTransitionEnterHelper bundle(Bundle bundle) { 55 | this.bundle = bundle; 56 | return this; 57 | } 58 | 59 | public void start(Class target) { 60 | 61 | Intent intent = new Intent(activity, target); 62 | int[] screenLocation = new int[2]; 63 | fromView.getLocationOnScreen(screenLocation); 64 | intent.putExtra(PRE_NAME + ".left",screenLocation[0]). 65 | putExtra(PRE_NAME + ".top", screenLocation[1]). 66 | putExtra(PRE_NAME + ".width", fromView.getWidth()). 67 | putExtra(PRE_NAME + ".height", fromView.getHeight()); 68 | // putExtra(PRE_NAME + ".x", fromView.getPivotX()). 69 | // putExtra(PRE_NAME + ".y", fromView.getPivotY()); 70 | 71 | 72 | if(bundle != null) { 73 | intent.putExtras(bundle); 74 | } 75 | 76 | activity.startActivity(intent); 77 | activity.overridePendingTransition(0, 0); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /app/src/main/java/com/asdzheng/suitedrecyclerview/utils/transition/ActivityTransitionExitHelper.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.suitedrecyclerview.utils.transition; 2 | 3 | import android.animation.ObjectAnimator; 4 | import android.content.Intent; 5 | import android.graphics.Color; 6 | import android.graphics.drawable.ColorDrawable; 7 | import android.os.Bundle; 8 | import android.os.Handler; 9 | import android.view.View; 10 | import android.view.ViewTreeObserver; 11 | import android.view.animation.AccelerateDecelerateInterpolator; 12 | import android.view.animation.DecelerateInterpolator; 13 | import android.view.animation.Interpolator; 14 | 15 | import com.asdzheng.layoutmanager.Size; 16 | import com.asdzheng.suitedrecyclerview.utils.LogUtil; 17 | 18 | import uk.co.senab.photoview.PhotoView; 19 | 20 | /** 21 | * Created by Bruce Too 22 | * On 9/26/15. 23 | * At 15:13 24 | * ActivityTransitionExit 25 | * Don`t forget add transparent theme in target activity 26 | * 31 | */ 32 | public class ActivityTransitionExitHelper { 33 | 34 | private final String TAG = this.getClass().getSimpleName(); 35 | 36 | private final DecelerateInterpolator decelerator = new DecelerateInterpolator(); 37 | private final Interpolator accelerator = new AccelerateDecelerateInterpolator(); 38 | private static final int DEFUALT_ANIM_DURATION = 300; 39 | private static final int SCALE_ANIM_DURATION = 500; 40 | 41 | private Intent fromIntent;//intent from pre activity 42 | private PhotoView toView;//target view show in this activity 43 | private View background; //root view of this activity 44 | private ColorDrawable bgDrawable; //background color 45 | private float leftDelta; 46 | private float topDelta; 47 | private float widthDelta; 48 | private float heightDelta; 49 | 50 | private int thumbnailTop; 51 | private int thumbnailLeft; 52 | private int thumbnailWidth; 53 | private int thumbnailHeight; 54 | 55 | private int animDuration = DEFUALT_ANIM_DURATION; 56 | 57 | public boolean isStarting = true; 58 | 59 | public ActivityTransitionExitHelper(Intent fromIntent) { 60 | this.fromIntent = fromIntent; 61 | } 62 | 63 | public static ActivityTransitionExitHelper with(Intent intent) { 64 | return new ActivityTransitionExitHelper(intent); 65 | } 66 | 67 | /** 68 | * add target view 69 | * 70 | * @param toView 71 | * @return 72 | */ 73 | public ActivityTransitionExitHelper toView(View toView) { 74 | this.toView = (PhotoView) toView; 75 | return this; 76 | } 77 | 78 | /** 79 | * add root view of this layout 80 | * 81 | * @param background 82 | * @return 83 | */ 84 | public ActivityTransitionExitHelper background(View background) { 85 | this.background = background; 86 | return this; 87 | } 88 | 89 | /** 90 | * @param savedInstanceState if savedInstanceState != null 91 | * we don`t have to perform the transition animation 92 | */ 93 | public ActivityTransitionExitHelper start(Bundle savedInstanceState) { 94 | if (savedInstanceState == null) { 95 | thumbnailTop = fromIntent.getIntExtra(ActivityTransitionEnterHelper.PRE_NAME + ".top", 0); 96 | thumbnailLeft = fromIntent.getIntExtra(ActivityTransitionEnterHelper.PRE_NAME + ".left", 0); 97 | thumbnailWidth = fromIntent.getIntExtra(ActivityTransitionEnterHelper.PRE_NAME + ".width", 0); 98 | thumbnailHeight = fromIntent.getIntExtra(ActivityTransitionEnterHelper.PRE_NAME + ".height", 0); 99 | 100 | final Size size = (Size) fromIntent.getSerializableExtra("size"); 101 | 102 | bgDrawable = new ColorDrawable(Color.BLACK); 103 | background.setBackground(bgDrawable); 104 | toView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 105 | @Override 106 | public boolean onPreDraw() { 107 | //remove default 108 | toView.getViewTreeObserver().removeOnPreDrawListener(this); 109 | int viewLocation[] = new int[2]; 110 | toView.getLocationOnScreen(viewLocation); 111 | leftDelta = thumbnailLeft - toView.getLeft(); 112 | //Note: widthDelta must be float 113 | widthDelta = (float) thumbnailWidth / toView.getWidth(); 114 | 115 | //如果drawble还没有缓存到内存中,使用传过来预估值(有误差) 116 | if (toView.getDrawable() == null) { 117 | if (size.getHeight() > toView.getHeight()) { 118 | size.setHeight(toView.getHeight()); 119 | } 120 | // heightDelta = (float) thumbnailHeight / size.getHeight(); 121 | 122 | setScaleHeight(size.getHeight()); 123 | setTranslationY(size.getHeight()); 124 | 125 | // topDelta = thumbnailTop - toView.getTop(); 126 | // topDelta = topDelta - ((toView.getHeight() - size.getHeight()) / 2) * heightDelta; 127 | // LogUtil.i(TAG, size.getHeight() + " topDelta = " + topDelta); 128 | } else { 129 | // heightDelta = (float) thumbnailHeight / toView.getDisplayRect().height(); 130 | setScaleHeight(toView.getDisplayRect().height()); 131 | setTranslationY(toView.getDisplayRect().height()); 132 | // 133 | // topDelta = thumbnailTop - toView.getTop(); 134 | // topDelta = topDelta - ((toView.getHeight() - toView.getDisplayRect().height()) / 2) * heightDelta; 135 | 136 | // LogUtil.w(TAG, " topDelta = " + topDelta + "toView Height " + toView.getDisplayRect().height()); 137 | } 138 | 139 | runEnterAnimation(); 140 | return true; 141 | } 142 | }); 143 | } 144 | return this; 145 | } 146 | 147 | private void runEnterAnimation() { 148 | isStarting = true; 149 | toView.setPivotX(0); 150 | toView.setPivotY(0); //axis 151 | toView.setScaleX(widthDelta); 152 | toView.setScaleY(heightDelta); 153 | toView.setTranslationX(leftDelta); 154 | toView.setTranslationY(topDelta); 155 | 156 | toView.animate().translationX(0).translationY(0) 157 | .scaleX(1).scaleY(1).setDuration(DEFUALT_ANIM_DURATION) 158 | .setInterpolator(accelerator).start(); 159 | 160 | ObjectAnimator bgAnim = ObjectAnimator.ofInt(bgDrawable, "alpha", 0, 255); 161 | bgAnim.setInterpolator(accelerator); 162 | bgAnim.setDuration(DEFUALT_ANIM_DURATION); 163 | bgAnim.start(); 164 | 165 | //防止双击,在还没显示全就退出 166 | new Handler().postDelayed(new Runnable() { 167 | @Override 168 | public void run() { 169 | isStarting = false; 170 | } 171 | }, 500); 172 | 173 | } 174 | 175 | public void runExitAnimation(final Runnable exit) { 176 | 177 | // LogUtil.i(TAG, "toView Height " + 178 | // toView.getDisplayRect().height() + " | scale " + toView.getScale()); 179 | 180 | setScaleHeight(toView.getDisplayRect().height() / toView.getScale()); 181 | setTranslationY(toView.getDisplayRect().height() / toView.getScale()); 182 | 183 | // heightDelta = (float) thumbnailHeight / (toView.getDisplayRect().height() / toView.getScale()); 184 | // topDelta = thumbnailTop - toView.getTop(); 185 | // topDelta = topDelta - ((toView.getHeight() - (toView.getDisplayRect().height() / toView.getScale())) / 2) * heightDelta; 186 | 187 | //由于photoView在缩小的时候被放大了,所以缩小前需要将其先恢复到正常状态 188 | toView.setZoomTransitionDuration(300); 189 | toView.setScale(1.0f, true); 190 | 191 | //targetApi 16 192 | toView.animate().translationX(leftDelta).translationY(topDelta) 193 | .scaleX(widthDelta).scaleY(heightDelta) 194 | .setInterpolator(decelerator) 195 | .setDuration(animDuration) 196 | .withEndAction(new Runnable() { 197 | @Override 198 | public void run() { 199 | background.setVisibility(View.GONE); 200 | toView.setVisibility(View.GONE); //let background and target view invisible 201 | exit.run(); 202 | } 203 | }).start(); 204 | 205 | // //animate color drawable of background 206 | ObjectAnimator bgAnim = ObjectAnimator.ofInt(bgDrawable, "alpha", 0); 207 | bgAnim.setInterpolator(decelerator); 208 | bgAnim.setDuration(animDuration); 209 | bgAnim.start(); 210 | } 211 | 212 | private void setScaleHeight(float photoNoScaleHeight) { 213 | heightDelta = thumbnailHeight / photoNoScaleHeight; 214 | } 215 | 216 | private void setTranslationY(float photoNoScaleHeight) { 217 | LogUtil.w(TAG, " toView.getTop() = " + toView.getTop() + "toView Height " + toView.getHeight()); 218 | 219 | //因为大图有上下黑边,下面的算法就是在扩大和缩小时考虑到上下黑边所带来的影响 220 | topDelta = thumbnailTop - toView.getTop(); 221 | topDelta = topDelta - ((toView.getHeight() - photoNoScaleHeight) / 2) * heightDelta; 222 | } 223 | 224 | 225 | } 226 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_channel_photo.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 16 | 17 | 18 | 27 | 28 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_channel_photo_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asdzheng/SuitedRecyclerView/50f75244f547c12cb5b22cd0feceda394248326c/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asdzheng/SuitedRecyclerView/50f75244f547c12cb5b22cd0feceda394248326c/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asdzheng/SuitedRecyclerView/50f75244f547c12cb5b22cd0feceda394248326c/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asdzheng/SuitedRecyclerView/50f75244f547c12cb5b22cd0feceda394248326c/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asdzheng/SuitedRecyclerView/50f75244f547c12cb5b22cd0feceda394248326c/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-v19/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 25dp 4 | -------------------------------------------------------------------------------- /app/src/main/res/values-v19/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | > 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/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 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | 8 | #90CAF9 9 | #2196F3 10 | 11 | #e1e4eb 12 | #ff9999 13 | 14 | #ffb9c1c7 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 16dp 6 | 8dp 7 | 0dp 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | SuitableRecyclerView 3 | Settings 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/test/java/com/example/administrator/suitablerecyclerview/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.example.administrator.suitablerecyclerview; 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 | } -------------------------------------------------------------------------------- /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 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:1.5.0' 10 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.4' 11 | classpath 'com.github.dcendents:android-maven-gradle-plugin:1.3' 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | jcenter() 21 | mavenCentral() 22 | } 23 | } 24 | 25 | task clean(type: Delete) { 26 | delete rootProject.buildDir 27 | } 28 | -------------------------------------------------------------------------------- /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/asdzheng/SuitedRecyclerView/50f75244f547c12cb5b22cd0feceda394248326c/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 | -------------------------------------------------------------------------------- /record.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asdzheng/SuitedRecyclerView/50f75244f547c12cb5b22cd0feceda394248326c/record.gif -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asdzheng/SuitedRecyclerView/50f75244f547c12cb5b22cd0feceda394248326c/screenshot.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':suitedrecyclerview' 2 | -------------------------------------------------------------------------------- /suitedrecyclerview/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /suitedrecyclerview/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | ext { 4 | bintrayRepo = 'maven'////bintray上的仓库名,一般为maven 5 | bintrayName = 'suitedrecyclerview'//bintray上的项目名 6 | 7 | publishedGroupId = 'com.asdzheng'//JCenter的GroupId 8 | artifact = 'suitedrecyclerview'//JCenter的ArtifactId 9 | 10 | siteUrl = 'https://github.com/asdzheng/SuitedRecyclerView' 11 | gitUrl = 'https://github.com/asdzheng/SuitedRecyclerView' 12 | 13 | libraryVersion = '1.0.0'//版本号 14 | libraryName = 'suitedrecyclerview'//项目名字,没什么用 15 | libraryDescription = 'A custom layoutmanager for Android'//项目描述,没什么用 16 | 17 | //开发者信息 18 | developerId = 'asdzheng' 19 | developerName = 'asdzheng' 20 | developerEmail = 'zhengjiabo@gmail.com' 21 | 22 | //以上所有信息自行修改,以下不变 23 | licenseName = 'The Apache Software License, Version 2.0' 24 | licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt' 25 | allLicenses = ["Apache-2.0"] 26 | } 27 | apply from:'https://raw.githubusercontent.com/Jude95/JCenter/master/install.gradle' 28 | apply from:'https://raw.githubusercontent.com/Jude95/JCenter/master/bintray.gradle' 29 | 30 | android { 31 | compileSdkVersion 23 32 | buildToolsVersion "23.0.2" 33 | 34 | defaultConfig { 35 | minSdkVersion 16 36 | targetSdkVersion 23 37 | versionCode 1 38 | versionName "1.0" 39 | } 40 | buildTypes { 41 | release { 42 | minifyEnabled false 43 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 44 | } 45 | } 46 | } 47 | 48 | dependencies { 49 | compile fileTree(dir: 'libs', include: ['*.jar']) 50 | compile 'com.android.support:recyclerview-v7:23.1.1' 51 | } 52 | -------------------------------------------------------------------------------- /suitedrecyclerview/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 G:\Program Files\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 | -------------------------------------------------------------------------------- /suitedrecyclerview/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /suitedrecyclerview/src/main/java/com/asdzheng/layoutmanager/LogUtil.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.layoutmanager; 2 | 3 | import android.util.Log; 4 | 5 | /** 6 | * Created by asdzheng on 2015/12/28. 7 | */ 8 | public class LogUtil { 9 | 10 | public static final int VERBOSE = 1; 11 | public static final int DEBUG = 2; 12 | public static final int INFO = 3; 13 | public static final int WARN = 4; 14 | public static final int ERROR = 5; 15 | 16 | public static final int NOTHING = 6; 17 | 18 | public static final int LEVEL = 1; 19 | 20 | public static void v(String tag, String msg) { 21 | if (LEVEL <= VERBOSE) { 22 | Log.v(tag, msg); 23 | } 24 | } 25 | 26 | public static void d(String tag, String msg) { 27 | if (LEVEL <= DEBUG) { 28 | Log.d(tag, msg); 29 | } 30 | } 31 | 32 | public static void i(String tag, String msg) { 33 | if (LEVEL <= INFO) { 34 | Log.i(tag, msg); 35 | } 36 | } 37 | 38 | public static void w(String tag, String msg) { 39 | if (LEVEL <= WARN) { 40 | Log.w(tag, msg); 41 | } 42 | } 43 | 44 | public static void e(String tag, String msg) { 45 | if (LEVEL <= ERROR) { 46 | Log.e(tag, msg); 47 | } 48 | } 49 | 50 | 51 | } 52 | -------------------------------------------------------------------------------- /suitedrecyclerview/src/main/java/com/asdzheng/layoutmanager/Size.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.layoutmanager; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * Created by asdzheng on 2015/12/26. 7 | */ 8 | public class Size implements Serializable{ 9 | private int height; 10 | private int width; 11 | 12 | public Size(int width, int height) { 13 | this.width = width; 14 | this.height = height; 15 | } 16 | 17 | public int getHeight() { 18 | return this.height; 19 | } 20 | 21 | public int getWidth() { 22 | return this.width; 23 | } 24 | 25 | public void setHeight(int height) { 26 | this.height = height; 27 | } 28 | 29 | public void setWidth(int width) { 30 | this.width = width; 31 | } 32 | 33 | @Override 34 | public String toString() { 35 | return "withd = " + width + " | height = " + height; 36 | } 37 | } -------------------------------------------------------------------------------- /suitedrecyclerview/src/main/java/com/asdzheng/layoutmanager/SizeCaculator.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.layoutmanager; 2 | 3 | 4 | import java.util.ArrayList; 5 | import java.util.Iterator; 6 | import java.util.List; 7 | 8 | /** 9 | * 纵横比计算工具类 10 | * Created by asdzheng on 2015/12/26. 11 | */ 12 | public class SizeCaculator { 13 | private static final int DEFAULT_MAX_ROW_HEIGHT = 600; 14 | private static final int INVALID_CONTENT_WIDTH = -1; 15 | private static final String TAG; 16 | private int mMaxRowHeight; 17 | private int mContentWidth; 18 | //每一行第一个可见的View的position 19 | private List mFirstChildPositionForRow; 20 | //记录每个child在哪一行 21 | private List mRowForChildPosition; 22 | //view的横纵比 23 | private SizeCalculatorDelegate mSizeCalculatorDelegate; 24 | //记录每个childView的宽高 25 | private List mSizeForChildAtPosition; 26 | 27 | static { 28 | TAG = SizeCaculator.class.getName(); 29 | //每个view的最大高度 30 | } 31 | 32 | 33 | public SizeCaculator(SizeCalculatorDelegate mSizeCalculatorDelegate) { 34 | mContentWidth = INVALID_CONTENT_WIDTH; 35 | this.mSizeCalculatorDelegate = mSizeCalculatorDelegate; 36 | mSizeForChildAtPosition = new ArrayList<>(); 37 | mFirstChildPositionForRow = new ArrayList<>(); 38 | mRowForChildPosition = new ArrayList<>(); 39 | mMaxRowHeight = DEFAULT_MAX_ROW_HEIGHT; 40 | } 41 | 42 | private void computeFirstChildPositionsUpToRow(int row) { 43 | while (row >= mFirstChildPositionForRow.size()) { 44 | // LogUtil.i(TAG, "row = " + row + " | mSizeForChildAtPosition.size()" + mSizeForChildAtPosition.size()); 45 | computeChildSizesUpToPosition(); 46 | } 47 | } 48 | 49 | private void computeChildSizesUpToPosition() { 50 | if (mContentWidth == -1) { 51 | throw new RuntimeException("Invalid content width. Did you forget to set it?"); 52 | } 53 | if (mSizeCalculatorDelegate == null) { 54 | throw new RuntimeException("Size calculator delegate is missing. Did you forget to set it?"); 55 | } 56 | //已经计算过的子View的size 57 | int neverComputeChildIndex = mSizeForChildAtPosition.size(); 58 | 59 | int newRow; 60 | if (mRowForChildPosition.size() > 0) { 61 | int lastPositionForRow = mRowForChildPosition.size() - 1; 62 | newRow = 1 + mRowForChildPosition.get(lastPositionForRow); 63 | } else { 64 | newRow = 0; 65 | } 66 | //宽高比 67 | double aspectRatioWidth = 0.0; 68 | ArrayList list = new ArrayList<>(); 69 | 70 | int rowHeight = Integer.MAX_VALUE; 71 | /** 72 | * 计算childView大小的主要算法,每行的子View都不能大于设置的最大高度 73 | */ 74 | while (rowHeight > mMaxRowHeight) { 75 | //获取还未测量过的宽高比 76 | double aspectRatioForIndex = mSizeCalculatorDelegate.aspectRatioForIndex(neverComputeChildIndex); 77 | list.add(aspectRatioForIndex); 78 | 79 | aspectRatioWidth = aspectRatioWidth + aspectRatioForIndex; 80 | 81 | rowHeight = (int)Math.ceil(mContentWidth / aspectRatioWidth); 82 | //当aspectRatioWidth凑成到一定数量后,rowHeight终于小于maxHeight,在从list里拿出刚才不同的宽高比来分配一行中不同view所占的大小 83 | if (rowHeight <= mMaxRowHeight) { 84 | mFirstChildPositionForRow.add(neverComputeChildIndex - (list.size() - 1)); 85 | //剩余的宽度 86 | int leftContentWidth = this.mContentWidth; 87 | Iterator iterator = list.iterator(); 88 | 89 | while (iterator.hasNext()) { 90 | //取剩余的宽度和原本缩小的宽度较小值(因为最后计算宽度的view可能有误差,因为ceil的原因会大点) 91 | int minWidth = Math.min(leftContentWidth, (int)Math.ceil(rowHeight * iterator.next())); 92 | mSizeForChildAtPosition.add(new Size(minWidth, rowHeight)); 93 | mRowForChildPosition.add(newRow); 94 | leftContentWidth = leftContentWidth - minWidth; 95 | } 96 | } 97 | neverComputeChildIndex ++; 98 | } 99 | } 100 | 101 | /** 102 | * 得到某一列第一个View的position 103 | * @param row 104 | * @return 105 | */ 106 | int getFirstChildPositionForRow(int row) { 107 | //如果list里没有row行的数据,就需要重新计算 108 | if (row >= mFirstChildPositionForRow.size()) { 109 | computeFirstChildPositionsUpToRow(row); 110 | } 111 | return mFirstChildPositionForRow.get(row); 112 | } 113 | 114 | /** 115 | * 得到positionView的行数 116 | * @param position 117 | * @return 118 | */ 119 | int getRowForChildPosition(int position) { 120 | if (position >= mRowForChildPosition.size()) { 121 | computeChildSizesUpToPosition(); 122 | } 123 | return mRowForChildPosition.get(position); 124 | } 125 | 126 | void reset() { 127 | mSizeForChildAtPosition.clear(); 128 | mFirstChildPositionForRow.clear(); 129 | mRowForChildPosition.clear(); 130 | } 131 | 132 | public void setContentWidth(int mContentWidth) { 133 | if (this.mContentWidth != mContentWidth) { 134 | this.mContentWidth = mContentWidth; 135 | reset(); 136 | } 137 | } 138 | 139 | public void setMaxRowHeight(int mMaxRowHeight) { 140 | if (this.mMaxRowHeight != mMaxRowHeight) { 141 | this.mMaxRowHeight = mMaxRowHeight; 142 | reset(); 143 | } 144 | } 145 | 146 | /** 147 | * 得到postionView的Size 148 | * @param position 149 | * @return 150 | */ 151 | Size sizeForChildAtPosition(int position) { 152 | if (position >= mSizeForChildAtPosition.size()) { 153 | computeChildSizesUpToPosition(); 154 | } 155 | return mSizeForChildAtPosition.get(position); 156 | } 157 | 158 | public interface SizeCalculatorDelegate 159 | { 160 | double aspectRatioForIndex(int position); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /suitedrecyclerview/src/main/java/com/asdzheng/layoutmanager/SuitUrlUtil.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.layoutmanager; 2 | 3 | /** 4 | * Created by asdzheng on 2015/12/28. 5 | */ 6 | public class SuitUrlUtil { 7 | /** 8 | * 字符串为null和""都返回true 9 | * 10 | * @param text 11 | * @return 12 | */ 13 | public static boolean isEmpty(CharSequence text) { 14 | boolean empty = false; 15 | if (text == null || text.toString().trim().length() == 0) { 16 | empty = true; 17 | } 18 | return empty; 19 | } 20 | 21 | public static boolean isNotEmpty(CharSequence text) { 22 | return !isEmpty(text); 23 | } 24 | 25 | public static double getAspectRadioFromUrl(String url) { 26 | if (isNotEmpty(url) && url.contains("_w") && url.contains("_h")) { 27 | double width = 0; 28 | double height = 0; 29 | 30 | String widthAndHeight = url.substring(url.indexOf("_w"), url.lastIndexOf("."));//得到的应该是_w123_h123这样的格式 31 | 32 | String widthStr = widthAndHeight.substring(2, widthAndHeight.lastIndexOf("_")); 33 | 34 | String heightStr = widthAndHeight.substring(widthAndHeight.lastIndexOf("_") + 2, widthAndHeight.length()); 35 | 36 | width = Integer.parseInt(widthStr); 37 | height = Integer.parseInt(heightStr); 38 | 39 | return width / height; 40 | } else { 41 | return 1; 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /suitedrecyclerview/src/main/java/com/asdzheng/layoutmanager/SuitedItemDecoration.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.layoutmanager; 2 | 3 | import android.graphics.Rect; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.View; 6 | 7 | /** 8 | * Created by asdzheng on 2015/12/28. 9 | */ 10 | public class SuitedItemDecoration extends RecyclerView.ItemDecoration { 11 | public static int DEFAULT_SPACING; 12 | private static String TAG; 13 | private static int mSpacing; 14 | 15 | static { 16 | TAG = SuitedItemDecoration.class.getSimpleName(); 17 | SuitedItemDecoration.DEFAULT_SPACING = 12; 18 | } 19 | 20 | public SuitedItemDecoration() { 21 | this(SuitedItemDecoration.DEFAULT_SPACING); 22 | } 23 | 24 | public SuitedItemDecoration(int mSpacing) { 25 | this.mSpacing = mSpacing; 26 | } 27 | 28 | private boolean isLeftChild(int position, SizeCaculator caculator) { 29 | return caculator.getFirstChildPositionForRow(caculator.getRowForChildPosition(position)) == position; 30 | } 31 | 32 | private boolean isTopChild(int position, SizeCaculator caculator) { 33 | return caculator.getRowForChildPosition(position) == 0; 34 | } 35 | 36 | @Override 37 | public void getItemOffsets(Rect rect, View view, RecyclerView recyclerView, RecyclerView.State state) { 38 | if (!(recyclerView.getLayoutManager() instanceof SuitedLayoutManager)) { 39 | throw new IllegalArgumentException(String.format("The %s must be used with a %s", SuitedItemDecoration.class.getSimpleName(), SuitedLayoutManager.class.getSimpleName())); 40 | } 41 | 42 | int childAdapterPosition = recyclerView.getChildAdapterPosition(view); 43 | 44 | SizeCaculator sizeCalculator = ((SuitedLayoutManager) recyclerView.getLayoutManager()).getSizeCalculator(); 45 | rect.top = 0; 46 | rect.bottom = this.mSpacing; 47 | rect.left = 0; 48 | rect.right = this.mSpacing; 49 | //只有在顶部的View才需要设置top Decoration 50 | if (isTopChild(childAdapterPosition, sizeCalculator)) { 51 | rect.top = this.mSpacing; 52 | } 53 | //只有在最左边的View才需要设置left Decoration 54 | if (isLeftChild(childAdapterPosition, sizeCalculator)) { 55 | rect.left = this.mSpacing; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /suitedrecyclerview/src/main/java/com/asdzheng/layoutmanager/SuitedLayoutManager.java: -------------------------------------------------------------------------------- 1 | package com.asdzheng.layoutmanager; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.util.Log; 5 | import android.util.SparseArray; 6 | import android.view.View; 7 | 8 | /** 9 | * Created by asdzheng on 2015/12/26. 10 | */ 11 | public class SuitedLayoutManager extends RecyclerView.LayoutManager { 12 | private static final String TAG; 13 | //第一个可见的position 14 | private int mFirstVisiblePosition; 15 | //最后一个可见的position 16 | private int mLastVisiblePosition; 17 | 18 | private int mFirstVisibleRow; 19 | private boolean mForceClearOffsets; 20 | private SizeCaculator mSizeCalculator; 21 | 22 | int onLayoutChildTimes = 0; 23 | 24 | static { 25 | TAG = SuitedLayoutManager.class.getSimpleName(); 26 | } 27 | 28 | public SuitedLayoutManager(SizeCaculator.SizeCalculatorDelegate sizeCalculatorDelegate) { 29 | mSizeCalculator = new SizeCaculator(sizeCalculatorDelegate); 30 | } 31 | 32 | private int getContentHeight() { 33 | return getHeight() - getPaddingTop() - getPaddingBottom(); 34 | } 35 | 36 | private int getContentWidth() { 37 | return getWidth() - getPaddingLeft() - getPaddingRight(); 38 | } 39 | 40 | private int preFillGrid(Direction direction, int dy, int childDecorateTop, RecyclerView.Recycler recycler, RecyclerView.State state) { 41 | int firstChildPositionForRow = mSizeCalculator.getFirstChildPositionForRow(mFirstVisibleRow); 42 | SparseArray sparseArray = new SparseArray(getChildCount()); 43 | int paddingLeft = getPaddingLeft(); 44 | int decoratedTop = childDecorateTop + getPaddingTop(); 45 | 46 | // LogUtil.e(TAG, "preFillGrid = getChildCount : " + getChildCount()); 47 | 48 | if (getChildCount() != 0) { 49 | decoratedTop = getDecoratedTop(getChildAt(0)); 50 | // LogUtil.w(TAG, "preFillGrid = decoratedTop : " + decoratedTop); 51 | 52 | if (mFirstVisiblePosition != firstChildPositionForRow) { 53 | switch (direction) { 54 | case UP: 55 | int upNewViewPosition = mFirstVisiblePosition - 1 ; 56 | int upNewViewHeight = mSizeCalculator.sizeForChildAtPosition(upNewViewPosition).getHeight(); 57 | // LogUtil.i(TAG, "UP = oldTop " + decoratedTop + " | upNewViewHeight = " + upNewViewHeight + " | newTop = " + (decoratedTop - upNewViewHeight )); 58 | decoratedTop = decoratedTop - upNewViewHeight; 59 | break; 60 | case DOWN: 61 | int firstViewHeight = mSizeCalculator.sizeForChildAtPosition(mFirstVisiblePosition).getHeight(); 62 | // LogUtil.i(TAG, "DOWN = oldTop " + decoratedTop + " | firstViewHeight = " + firstViewHeight + " | newTop = " + (decoratedTop + firstViewHeight )); 63 | decoratedTop = decoratedTop + firstViewHeight; 64 | break; 65 | } 66 | } 67 | 68 | for (int i = 0; i < getChildCount(); ++i) { 69 | sparseArray.put(i + mFirstVisiblePosition, getChildAt(i)); 70 | } 71 | for (int j = 0; j < sparseArray.size(); ++j) { 72 | detachView((View) sparseArray.valueAt(j)); 73 | } 74 | } 75 | //将新的firstChild赋给全局mFirstChild 76 | mFirstVisiblePosition = firstChildPositionForRow; 77 | int childPaddingLeft = paddingLeft; 78 | int childPaddingTop = decoratedTop; 79 | 80 | for (int i = mFirstVisiblePosition; i < state.getItemCount(); i++) { 81 | Size sizeForChildAtPosition = mSizeCalculator.sizeForChildAtPosition(i); 82 | //是否加上下一个view就超过屏幕的宽度 83 | if (childPaddingLeft + sizeForChildAtPosition.getWidth() > getContentWidth()) { 84 | childPaddingLeft = paddingLeft; 85 | //上一个view的坐标 86 | int lastPosition = i - 1; 87 | //childPaddingTop一直叠加每一行view的高度 88 | int lastViewHeight = mSizeCalculator.sizeForChildAtPosition(lastPosition).getHeight(); 89 | childPaddingTop = childPaddingTop + lastViewHeight; 90 | } 91 | 92 | if(direction == Direction.DOWN) { 93 | //是否已经到了不可见的view(不可见的view,不用测量和绘制) 94 | if (childPaddingTop >= dy + getContentHeight()) { 95 | break; 96 | } 97 | } else { 98 | //是否已经到了不可见的view 99 | if (childPaddingTop >= getContentHeight()) { 100 | break; 101 | } 102 | } 103 | 104 | View view = (View) sparseArray.get(i); 105 | if (view == null) { 106 | final View newView = recycler.getViewForPosition(i); 107 | 108 | // LogUtil.w(TAG, "view == null i = " + i + " | sizeForChildAtPosition = " + sizeForChildAtPosition); 109 | addView(newView); 110 | measureChildWithMargins(newView, 0, 0); 111 | layoutDecorated(newView, childPaddingLeft, childPaddingTop, childPaddingLeft + sizeForChildAtPosition.getWidth(), childPaddingTop + sizeForChildAtPosition.getHeight()); 112 | 113 | } else { 114 | // LogUtil.i(TAG, "view != null i = " + i + " | sizeForChildAtPosition = " + sizeForChildAtPosition); 115 | attachView(view); 116 | sparseArray.remove(i); 117 | } 118 | childPaddingLeft += sizeForChildAtPosition.getWidth(); 119 | } 120 | 121 | for (int k = 0; k < sparseArray.size(); k++) { 122 | recycler.recycleView((View) sparseArray.valueAt(k)); 123 | } 124 | int childCount = getChildCount(); 125 | int lastVisibleViewBottom = 0; 126 | if (childCount > 0) { 127 | lastVisibleViewBottom = getChildAt(childCount - 1).getBottom(); 128 | } 129 | 130 | mLastVisiblePosition = mFirstVisiblePosition + childCount - 1; 131 | 132 | return lastVisibleViewBottom; 133 | } 134 | 135 | @Override 136 | public boolean canScrollVertically() { 137 | return true; 138 | } 139 | 140 | public int findFirstVisibleItemPosition() { 141 | return mFirstVisiblePosition; 142 | } 143 | 144 | public int getmLastVisiblePosition() { 145 | return mLastVisiblePosition; 146 | } 147 | 148 | @Override 149 | public RecyclerView.LayoutParams generateDefaultLayoutParams() { 150 | return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT, RecyclerView.LayoutParams.WRAP_CONTENT); 151 | } 152 | 153 | public SizeCaculator getSizeCalculator() { 154 | return mSizeCalculator; 155 | } 156 | 157 | @Override 158 | public void onAdapterChanged(RecyclerView.Adapter adapter, RecyclerView.Adapter adapter2) { 159 | removeAllViews(); 160 | mSizeCalculator.reset(); 161 | } 162 | 163 | @Override 164 | public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 165 | if (getItemCount() == 0) { 166 | detachAndScrapAttachedViews(recycler); 167 | return; 168 | } 169 | 170 | if(state.isPreLayout() || !state.didStructureChange()) { 171 | return; 172 | } 173 | 174 | onLayoutChildTimes++; 175 | 176 | // LogUtil.e(TAG, "onLayoutChildren times = " + onLayoutChildTimes + " | hasPendingAdapterUpdates " + ); 177 | mSizeCalculator.setContentWidth(getContentWidth()); 178 | mSizeCalculator.reset(); 179 | int decoratedTop; 180 | if (getChildCount() == 0) { 181 | mFirstVisiblePosition = 0; 182 | mFirstVisibleRow = 0; 183 | decoratedTop = 0; 184 | } else { 185 | View child = getChildAt(0); 186 | if (mForceClearOffsets) { 187 | mForceClearOffsets = false; 188 | decoratedTop = 0; 189 | } else { 190 | //代替getTop()获取子视图的 top 边缘 191 | decoratedTop = getDecoratedTop(child); 192 | } 193 | } 194 | //解绑之前所有绑定在一起的view进recycle bin 195 | detachAndScrapAttachedViews(recycler); 196 | preFillGrid(Direction.NONE, 0, decoratedTop, recycler, state); 197 | } 198 | 199 | @Override 200 | public void scrollToPosition(int position) { 201 | // LogUtil.i(TAG, "scrollToPosition n : " + n); 202 | 203 | if (position >= getItemCount()) { 204 | Log.w(SuitedLayoutManager.TAG, String.format("Cannot scroll to %d, item count is %d", position, getItemCount())); 205 | return; 206 | } 207 | mForceClearOffsets = true; 208 | mFirstVisibleRow = mSizeCalculator.getRowForChildPosition(position); 209 | mFirstVisiblePosition = mSizeCalculator.getFirstChildPositionForRow(mFirstVisibleRow); 210 | requestLayout(); 211 | } 212 | 213 | /** 214 | * Scroll vertically by dy pixels in screen coordinates and return the distance traveled. 215 | * The default implementation does nothing and returns 0. 216 | * 217 | * @param dy distance to scroll in pixels. Y increases as scroll position 218 | * approaches the bottom. 219 | * @param recycler Recycler to use for fetching potentially cached views for a 220 | * position 221 | * @param state Transient state of RecyclerView 222 | * @return The actual distance scrolled. The return value will be negative if dy was 223 | * negative and scrolling proceeeded in that direction. 224 | * Math.abs(result) may be less than dy if a boundary was reached. 225 | */ 226 | @Override 227 | public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { 228 | if (getChildCount() == 0 || dy == 0) { 229 | return 0; 230 | } 231 | View firstChild = getChildAt(0); 232 | View lastChild = getChildAt(-1 + getChildCount()); 233 | int toBottomTopDistance = getContentHeight(); 234 | if (dy > 0) { 235 | //向下滑动,dy大于0 236 | if (mFirstVisiblePosition + getChildCount() >= getItemCount()) { 237 | //最后面的所有视图已经可见 238 | toBottomTopDistance = getDecoratedBottom(lastChild) - getContentHeight(); 239 | } else if (getDecoratedBottom(firstChild) - dy <= 0) { 240 | //第一行的的View底部将移出屏幕 241 | mFirstVisibleRow++; 242 | toBottomTopDistance = preFillGrid(Direction.DOWN, Math.abs(dy), 0, recycler, state); 243 | 244 | } else if (getDecoratedBottom(lastChild) - dy < getContentHeight()) { 245 | toBottomTopDistance = preFillGrid(Direction.DOWN, Math.abs(dy), 0, recycler, state); 246 | } 247 | } else if (mFirstVisibleRow == 0 && getDecoratedTop(firstChild) - dy >= 0) { 248 | //第一行可见,向上滑动的距离大于从现在到顶部的距离 249 | toBottomTopDistance = -getDecoratedTop(firstChild); 250 | } else if (getDecoratedTop(firstChild) - dy >= 0) { 251 | //顶部将出现一排新的view 252 | mFirstVisibleRow--; 253 | toBottomTopDistance = preFillGrid(Direction.UP, Math.abs(dy), 0, recycler, state); 254 | } else if (getDecoratedTop(lastChild) - dy > getContentHeight()) { 255 | toBottomTopDistance = preFillGrid(Direction.UP, Math.abs(dy), 0, recycler, state); 256 | } 257 | 258 | //实际移动的距离 259 | int actualDy; 260 | if (Math.abs(dy) > toBottomTopDistance) { 261 | //signum返回的是-1或者1,看dy的正负符号 262 | actualDy = toBottomTopDistance * (int) Math.signum(dy); 263 | } else { 264 | actualDy = dy; 265 | } 266 | 267 | //向下滚动,对于子view来说,它的移动方向是和滚动方向相反的,所以传入的是负数 268 | offsetChildrenVertical(-actualDy); 269 | return actualDy; 270 | } 271 | 272 | public void setMaxRowHeight(int maxRowHeight) { 273 | mSizeCalculator.setMaxRowHeight(maxRowHeight); 274 | } 275 | 276 | private enum Direction { 277 | DOWN, 278 | NONE, 279 | UP 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /suitedrecyclerview/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | library 3 | 4 | --------------------------------------------------------------------------------