├── .gitignore
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── cn
│ │ └── saymagic
│ │ └── begonia
│ │ ├── BegoniaApplication.kt
│ │ ├── CrashHandler.java
│ │ ├── adapter
│ │ ├── AdapterEventListener.kt
│ │ ├── BaseNetRetryableAdapter.kt
│ │ ├── CollectionAdapter.kt
│ │ ├── CommonFragmentPageAdapter.kt
│ │ ├── PhotoListAdapter.kt
│ │ └── UserListAdapter.kt
│ │ ├── exception
│ │ └── UnexpectedCodeException.kt
│ │ ├── fundamental
│ │ ├── CircleTransform.java
│ │ ├── ImageExt.kt
│ │ └── SpacesItemDecoration.kt
│ │ ├── pojo
│ │ ├── AsyncResult.kt
│ │ └── DataSourceParam.kt
│ │ ├── repository
│ │ ├── NetworkState.kt
│ │ ├── datasource
│ │ │ ├── CollectionListDataSource.kt
│ │ │ ├── CollectionListDataSourceFactory.kt
│ │ │ ├── PhotoListDataSource.kt
│ │ │ ├── PhotoListDataSourceFactory.kt
│ │ │ ├── UserListDataSource.kt
│ │ │ └── UserListDataSourceFactory.kt
│ │ └── remote
│ │ │ ├── UserRepository.kt
│ │ │ └── ViewPhotoRepository.kt
│ │ ├── ui
│ │ ├── NetworkStateItemViewHolder.kt
│ │ ├── activity
│ │ │ ├── PhotoListActivity.kt
│ │ │ ├── SearchResultsActivity.kt
│ │ │ ├── UserNameUpdateActivity.kt
│ │ │ ├── UserProfileActivity.kt
│ │ │ └── ViewPhotoActivity.kt
│ │ └── fragment
│ │ │ ├── BottomPhotoListFragment.kt
│ │ │ ├── CollectionListFragment.kt
│ │ │ ├── PhotoListFragment.kt
│ │ │ ├── PhotoListManager.kt
│ │ │ └── UserListFragment.kt
│ │ ├── util
│ │ ├── AsyncTaskGuard.kt
│ │ ├── CollectionDiffCallback.kt
│ │ ├── Constants.kt
│ │ ├── PhotoDiffCallback.kt
│ │ ├── PhotoUtils.java
│ │ ├── UIController.kt
│ │ └── UserDiffCallback.kt
│ │ └── viewmodel
│ │ ├── CollectionListViewModel.kt
│ │ ├── PhotoListActivityViewModel.kt
│ │ ├── PhotoListViewModel.kt
│ │ ├── UserProfileModel.kt
│ │ ├── UserViewModel.kt
│ │ └── ViewPhotoModel.kt
│ └── res
│ ├── drawable-v24
│ └── ic_launcher_foreground.xml
│ ├── drawable
│ ├── circle.xml
│ ├── ic_error_black_24dp.xml
│ ├── ic_free_breakfast_black_24dp.xml
│ ├── ic_launcher_background.xml
│ ├── loading.xml
│ └── rectangle.xml
│ ├── layout
│ ├── activity_photo.xml
│ ├── activity_search_results.xml
│ ├── activity_user_name_update.xml
│ ├── activity_user_profile.xml
│ ├── activity_view_photo.xml
│ ├── fragment_collection_list.xml
│ ├── fragment_photo_list.xml
│ ├── fragment_user_list.xml
│ ├── item_collection.xml
│ ├── item_photo.xml
│ ├── item_user.xml
│ └── network_state_item.xml
│ ├── menu
│ ├── activity_main_drawer.xml
│ ├── search_menu.xml
│ └── view_photo_menu.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-mdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── values-v21
│ └── styles.xml
│ ├── values
│ ├── colors.xml
│ ├── dimens.xml
│ ├── strings.xml
│ └── styles.xml
│ └── xml
│ └── searchable.xml
├── build.gradle
├── client
├── .gitignore
├── build.gradle
└── src
│ └── main
│ └── java
│ └── cn
│ └── saymagic
│ └── begonia
│ └── client
│ └── Client.java
├── core
├── .gitignore
├── build.gradle
└── src
│ └── main
│ └── java
│ └── cn
│ └── saymagic
│ └── begonia
│ └── sdk
│ └── core
│ ├── Unsplash.java
│ ├── UnsplashConstants.java
│ ├── api
│ └── UnsplashService.java
│ └── pojo
│ ├── Collection.java
│ ├── Download.java
│ ├── Historical.java
│ ├── Links.java
│ ├── Photo.java
│ ├── Portfolio.java
│ ├── SearchResult.java
│ ├── Statistics.java
│ ├── StatisticsItem.java
│ ├── Urls.java
│ └── User.java
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── screens
└── 1.0
│ ├── 1.png
│ ├── 2.png
│ ├── 3.png
│ └── 4.png
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/libraries
5 | /.idea/modules.xml
6 | /.idea/workspace.xml
7 | .DS_Store
8 | /build
9 | /captures
10 | .externalNativeBuild
11 | .settings/
12 | *.class
13 | .project
14 | .idea/
15 | .classpath
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Begonia
2 |
3 | Begonia is an unofficial unsplash client for people to view high-resolution photos in android devices.
4 |
5 |
6 | ## Examples
7 |
8 |
9 |
15 |
16 | ## FQA
17 |
18 | #### How do I use it/install it?
19 |
20 | * Apply for your own unsplash application from here: [https://unsplash.com/oauth/applications](https://unsplash.com/oauth/applications)
21 |
22 | * Put your application key in gradle.properties file
23 |
24 | * Sync and run it like a normal android project
25 |
26 |
27 | ## Related Links
28 |
29 | [Architecture Components](https://developer.android.com/topic/libraries/architecture/)
30 |
31 | [Unsplash](https://unsplash.com/)
32 |
33 |
34 | ## License
35 |
36 | ```
37 |
38 | The MIT License
39 |
40 | Copyright (c) saymagic. https://blog.saymagic.cn
41 |
42 | Permission is hereby granted, free of charge, to any person obtaining a copy
43 | of this software and associated documentation files (the "Software"), to deal
44 | in the Software without restriction, including without limitation the rights
45 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
46 | copies of the Software, and to permit persons to whom the Software is
47 | furnished to do so, subject to the following conditions:
48 |
49 | The above copyright notice and this permission notice shall be included in
50 | all copies or substantial portions of the Software.
51 |
52 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
53 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
54 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
55 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
56 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
57 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
58 | THE SOFTWARE.
59 | ```
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | apply plugin: 'kotlin-android'
4 |
5 | apply plugin: 'kotlin-android-extensions'
6 |
7 | android {
8 | compileSdkVersion 27
9 | defaultConfig {
10 | applicationId "cn.saymagic.begonia"
11 | minSdkVersion 19
12 | targetSdkVersion 27
13 | versionCode 1
14 | versionName "1.0"
15 | buildConfigField "String", "APPLICATION_KEY", "\"" + getApplicationKey() + "\""
16 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
17 | }
18 | buildTypes {
19 | release {
20 | minifyEnabled false
21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
22 | }
23 | }
24 | }
25 |
26 | dependencies {
27 | def lifecycle_version = "1.1.1"
28 | def paging_version = "1.0.0"
29 |
30 | implementation fileTree(dir: 'libs', include: ['*.jar'])
31 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
32 | //noinspection GradleCompatible
33 | implementation 'com.android.support:appcompat-v7:27.1.1'
34 | implementation 'com.android.support:support-v4:27.1.1'
35 | implementation 'com.android.support:recyclerview-v7:27.1.1'
36 | implementation 'com.android.support:cardview-v7:27.1.1'
37 | implementation 'com.android.support:design:27.1.1'
38 | implementation 'com.android.support.constraint:constraint-layout:1.1.0'
39 | testImplementation 'junit:junit:4.12'
40 | androidTestImplementation 'com.android.support.test:runner:1.0.2'
41 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
42 | implementation project(":core")
43 | // LiveData + ViewModel
44 |
45 | implementation "android.arch.lifecycle:extensions:$lifecycle_version"
46 |
47 |
48 | implementation "android.arch.paging:runtime:$paging_version"
49 | implementation 'com.github.bumptech.glide:glide:4.7.1'
50 | implementation 'com.squareup.picasso:picasso:2.71828'
51 |
52 | implementation 'com.squareup.okhttp3:okhttp:3.10.0'
53 | implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0'
54 | implementation 'com.squareup.retrofit2:retrofit:2.4.0'
55 | implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
56 | implementation 'com.google.code.gson:gson:2.7'
57 | implementation 'com.github.chrisbanes:PhotoView:2.1.3'
58 | implementation 'com.github.bumptech.glide:glide:3.6.0'
59 | implementation 'com.github.vlonjatg:progress-activity:2.0.5'
60 | }
61 |
62 |
63 | String getApplicationKey() {
64 | def applicationKey = project.getProperties().get("APPLICATION_KEY")
65 | if (applicationKey == null || "" == applicationKey) {
66 | Properties properties = new Properties()
67 | properties.load(project.rootProject.file('local.properties').newDataInputStream())
68 | applicationKey = properties.getProperty('APPLICATION_KEY')
69 | if (applicationKey == null || "" == applicationKey) {
70 | throw new IllegalStateException("Did you forget set application key in gradle.properties file? Get your own unsplash application key from here : https://unsplash.com/oauth/applications")
71 | }
72 | }
73 | return applicationKey
74 | }
75 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
24 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
37 |
38 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/saymagic/begonia/BegoniaApplication.kt:
--------------------------------------------------------------------------------
1 | package cn.saymagic.begonia
2 |
3 | import android.app.Application
4 | import cn.saymagic.begonia.sdk.core.Unsplash
5 |
6 | class BegoniaApplication : Application() {
7 |
8 | override fun onCreate() {
9 | super.onCreate()
10 | CrashHandler.getInstance().init()
11 | Unsplash.init(BuildConfig.APPLICATION_KEY)
12 | }
13 |
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/cn/saymagic/begonia/CrashHandler.java:
--------------------------------------------------------------------------------
1 | /**
2 | * @(#)CrashHandler.java, 2013-4-9.
3 | *
4 | * Copyright 2013 Netease, Inc. All rights reserved.
5 | * NETEASE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
6 | */
7 | package cn.saymagic.begonia;
8 |
9 | import android.util.Log;
10 |
11 |
12 | import java.io.PrintWriter;
13 | import java.io.StringWriter;
14 | import java.io.Writer;
15 | import java.lang.Thread.UncaughtExceptionHandler;
16 | import java.text.DateFormat;
17 | import java.text.SimpleDateFormat;
18 | import java.util.HashMap;
19 | import java.util.Map;
20 |
21 |
22 | public final class CrashHandler implements UncaughtExceptionHandler {
23 |
24 | private static final String TAG = "CrashHandler";
25 |
26 | private Thread.UncaughtExceptionHandler defaultHandler;
27 |
28 | private static CrashHandler instance = new CrashHandler();
29 |
30 | private Map infos = new HashMap();
31 |
32 | private DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
33 |
34 | private CrashHandler() {}
35 |
36 | public static CrashHandler getInstance() {
37 | if (instance == null) {
38 | instance = new CrashHandler();
39 | }
40 | return instance;
41 | }
42 |
43 | public void init() {
44 | defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
45 | Thread.setDefaultUncaughtExceptionHandler(this);
46 | }
47 |
48 | @Override
49 | public void uncaughtException(Thread thread, Throwable ex) {
50 | handleException(ex);
51 | defaultHandler.uncaughtException(thread, ex);
52 | }
53 |
54 | private boolean handleException(Throwable ex) {
55 | if (ex == null) {
56 | return false;
57 | }
58 | writeCrashInfoToFile(ex);
59 | return true;
60 | }
61 |
62 | private void writeCrashInfoToFile(Throwable ex) {
63 | StringBuffer sb = new StringBuffer();
64 | for (Map.Entry entry: infos.entrySet()) {
65 | String key = entry.getKey();
66 | String value = entry.getValue();
67 | sb.append(key + "=" + value + "\n");
68 | }
69 |
70 | Writer writer = new StringWriter();
71 | PrintWriter printWriter = new PrintWriter(writer);
72 | ex.printStackTrace(printWriter);
73 | Throwable cause = ex.getCause();
74 | while (cause != null) {
75 | cause.printStackTrace(printWriter);
76 | cause = cause.getCause();
77 | }
78 | printWriter.close();
79 | String result = writer.toString();
80 | sb.append(result);
81 | Log.e("BegoniaCrash", sb.toString());
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/saymagic/begonia/adapter/AdapterEventListener.kt:
--------------------------------------------------------------------------------
1 | package cn.saymagic.begonia.adapter
2 |
3 | import android.view.View
4 |
5 | interface AdapterEventListener {
6 |
7 | fun onEvent(eventName: String, value: T, itemView: View)
8 |
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/cn/saymagic/begonia/adapter/BaseNetRetryableAdapter.kt:
--------------------------------------------------------------------------------
1 | package cn.saymagic.begonia.adapter
2 |
3 | import android.arch.paging.PagedListAdapter
4 | import android.support.v7.util.DiffUtil
5 | import android.support.v7.widget.RecyclerView
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import cn.saymagic.begonia.R
10 | import cn.saymagic.begonia.fundamental.into
11 | import cn.saymagic.begonia.repository.NetworkState
12 | import cn.saymagic.begonia.sdk.core.pojo.Photo
13 | import cn.saymagic.begonia.ui.NetworkStateItemViewHolder
14 | import kotlinx.android.synthetic.main.item_photo.view.*
15 |
16 | abstract class BaseNetRetryableAdapter (callback: DiffUtil.ItemCallback, val eventListener: AdapterEventListener, private val retry: () -> Unit) : PagedListAdapter(callback) {
17 |
18 | companion object {
19 | val EVENT_CLICK_IMG = "EVENT_CLICK_IMG"
20 | val EVENT_CLICK_USER = "EVENT_CLICK_USER"
21 | }
22 |
23 | var networkState: NetworkState? = null
24 |
25 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
26 | return when (viewType) {
27 | R.layout.network_state_item -> NetworkStateItemViewHolder.create(parent, {
28 | retry.invoke()
29 | })
30 | else -> onCreateContentViewHolder(parent, viewType)
31 | }
32 | }
33 |
34 | abstract fun onCreateContentViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
35 |
36 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
37 | if (holder is NetworkStateItemViewHolder) {
38 | holder.bindTo(networkState)
39 | } else {
40 | onBindContentViewHolder(holder, position)
41 | }
42 | }
43 |
44 | abstract fun onBindContentViewHolder(holder: RecyclerView.ViewHolder, position: Int)
45 |
46 | private fun hasExtraRow() = networkState != null && networkState != NetworkState.LOADED
47 |
48 | override fun getItemViewType(position: Int): Int {
49 | return if (hasExtraRow() && position == itemCount - 1) {
50 | R.layout.network_state_item
51 | } else {
52 | getItemContentViewType(position)
53 | }
54 | }
55 |
56 | abstract fun getItemContentViewType(position: Int): Int
57 |
58 | override fun getItemCount(): Int {
59 | return super.getItemCount() + if (hasExtraRow()) 1 else 0
60 | }
61 |
62 | fun changeNetworkState(newNetWorkState: NetworkState?) {
63 | val previousState = this.networkState
64 | val hadExtraRow = hasExtraRow()
65 | this.networkState = newNetWorkState
66 | val hasExtraRow = hasExtraRow()
67 | if (hadExtraRow != hasExtraRow) {
68 | if (hadExtraRow) {
69 | notifyItemRemoved(super.getItemCount())
70 | } else {
71 | notifyItemInserted(super.getItemCount())
72 | }
73 | } else if (hasExtraRow && previousState != newNetWorkState) {
74 | notifyItemChanged(itemCount - 1)
75 | }
76 | }
77 |
78 | }
79 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/saymagic/begonia/adapter/CollectionAdapter.kt:
--------------------------------------------------------------------------------
1 | package cn.saymagic.begonia.adapter
2 |
3 | import android.support.v7.util.DiffUtil
4 | import android.support.v7.widget.RecyclerView
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import cn.saymagic.begonia.R
9 | import cn.saymagic.begonia.fundamental.into
10 | import cn.saymagic.begonia.sdk.core.pojo.Collection
11 | import kotlinx.android.synthetic.main.item_collection.view.*
12 |
13 | class CollectionAdapter(callback: DiffUtil.ItemCallback, eventListener: AdapterEventListener, retry: () -> Unit) : BaseNetRetryableAdapter(callback, eventListener, retry) {
14 |
15 | override fun getItemContentViewType(position: Int): Int {
16 | return R.layout.item_collection
17 | }
18 |
19 | companion object {
20 | const val EVENT_CLICK_THUMB = "EVENT_CLICK_THUMB"
21 | }
22 | override fun onCreateContentViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
23 | return CollectionViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_collection, null), eventListener)
24 | }
25 |
26 | override fun onBindContentViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
27 | if (holder is CollectionViewHolder) {
28 | val collection = getItem(position)
29 | if (collection != null) {
30 | holder.bindTo(collection)
31 | }
32 | }
33 | }
34 |
35 | }
36 |
37 |
38 | class CollectionViewHolder(itemView: View, val eventListener: AdapterEventListener) : RecyclerView.ViewHolder(itemView) {
39 |
40 | lateinit var collection: Collection
41 |
42 | init {
43 | val lp = RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
44 | itemView.layoutParams = lp
45 | itemView.setOnClickListener {
46 | eventListener.onEvent(CollectionAdapter.EVENT_CLICK_THUMB, collection, itemView)
47 | }
48 | }
49 |
50 | fun bindTo(collection: Collection) {
51 | this.collection = collection
52 | itemView.collectionThumb.into(collection.coverPhoto?.urls?.regular?:"")
53 | itemView.collectionName.text = collection.title
54 | itemView.collectionDesc.text = "${collection.totalPhotos} photos. by ${collection.user?.name}"
55 | }
56 |
57 | }
--------------------------------------------------------------------------------
/app/src/main/java/cn/saymagic/begonia/adapter/CommonFragmentPageAdapter.kt:
--------------------------------------------------------------------------------
1 | package cn.saymagic.begonia.adapter
2 |
3 | import android.support.v4.app.Fragment
4 | import android.support.v4.app.FragmentManager
5 | import android.support.v4.app.FragmentPagerAdapter
6 | import cn.saymagic.begonia.pojo.DataSourceParam
7 | import cn.saymagic.begonia.ui.fragment.PhotoListFragment
8 | import cn.saymagic.begonia.util.Constants
9 |
10 | class CommonFragmentPageAdapter(fm: FragmentManager, private val titleSource: Array, private val fragmentCreator : (Int) -> Fragment) : FragmentPagerAdapter(fm) {
11 |
12 | override fun getItem(position: Int): Fragment = fragmentCreator.invoke(position)
13 |
14 | override fun getCount(): Int {
15 | return titleSource.size
16 | }
17 |
18 | override fun getPageTitle(position: Int): CharSequence? {
19 | return titleSource[position]
20 | }
21 | }
--------------------------------------------------------------------------------
/app/src/main/java/cn/saymagic/begonia/adapter/PhotoListAdapter.kt:
--------------------------------------------------------------------------------
1 | package cn.saymagic.begonia.adapter
2 |
3 | import android.arch.paging.PagedListAdapter
4 | import android.support.v7.util.DiffUtil
5 | import android.support.v7.widget.RecyclerView
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import cn.saymagic.begonia.R
10 | import cn.saymagic.begonia.fundamental.into
11 | import cn.saymagic.begonia.repository.NetworkState
12 | import cn.saymagic.begonia.sdk.core.pojo.Photo
13 | import cn.saymagic.begonia.ui.NetworkStateItemViewHolder
14 | import kotlinx.android.synthetic.main.item_photo.view.*
15 |
16 | class PhotoListAdapter(callback: DiffUtil.ItemCallback, private val eventListener: AdapterEventListener, private val retry: () -> Unit) : PagedListAdapter(callback) {
17 |
18 | companion object {
19 | val EVENT_CLICK_IMG = "EVENT_CLICK_IMG"
20 | val EVENT_CLICK_USER = "EVENT_CLICK_USER"
21 | }
22 |
23 | var networkState: NetworkState? = null
24 |
25 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
26 | return when (viewType) {
27 | R.layout.item_photo -> PhotoViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_photo, null), eventListener)
28 | R.layout.network_state_item -> NetworkStateItemViewHolder.create(parent, {
29 | retry.invoke()
30 | })
31 | else -> throw IllegalArgumentException("unknown view type $viewType")
32 | }
33 | }
34 |
35 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
36 | if (holder is PhotoViewHolder) {
37 | val photo = getItem(position)
38 | if (photo != null) {
39 | holder.bindTo(photo)
40 | }
41 | } else if (holder is NetworkStateItemViewHolder) {
42 | holder.bindTo(networkState)
43 | }
44 | }
45 |
46 | private fun hasExtraRow() = networkState != null && networkState != NetworkState.LOADED
47 |
48 | override fun getItemViewType(position: Int): Int {
49 | return if (hasExtraRow() && position == itemCount - 1) {
50 | R.layout.network_state_item
51 | } else {
52 | R.layout.item_photo
53 | }
54 | }
55 |
56 | override fun getItemCount(): Int {
57 | return super.getItemCount() + if (hasExtraRow()) 1 else 0
58 | }
59 |
60 | fun changeNetworkState(newNetWorkState: NetworkState?) {
61 | val previousState = this.networkState
62 | val hadExtraRow = hasExtraRow()
63 | this.networkState = newNetWorkState
64 | val hasExtraRow = hasExtraRow()
65 | if (hadExtraRow != hasExtraRow) {
66 | if (hadExtraRow) {
67 | notifyItemRemoved(super.getItemCount())
68 | } else {
69 | notifyItemInserted(super.getItemCount())
70 | }
71 | } else if (hasExtraRow && previousState != newNetWorkState) {
72 | notifyItemChanged(itemCount - 1)
73 | }
74 | }
75 |
76 | }
77 |
78 | class PhotoViewHolder(itemView: View, val eventListener: AdapterEventListener) : RecyclerView.ViewHolder(itemView) {
79 |
80 | lateinit var photo: Photo
81 |
82 | init {
83 | itemView.img.setOnClickListener {
84 | eventListener.onEvent(PhotoListAdapter.EVENT_CLICK_IMG, photo, itemView)
85 | }
86 | itemView.text.setOnClickListener {
87 | eventListener.onEvent(PhotoListAdapter.EVENT_CLICK_USER, photo, itemView)
88 | }
89 | itemView.photoUserAvatar.setOnClickListener {
90 | eventListener.onEvent(PhotoListAdapter.EVENT_CLICK_USER, photo, itemView)
91 | }
92 | }
93 |
94 | fun bindTo(photo: Photo) {
95 | this.photo = photo
96 | itemView.text.text = photo.user.username
97 | itemView.img.into(photo.urls?.small ?: "")
98 | itemView.photoUserAvatar.into(photo.user.profileImage.medium, true)
99 | }
100 |
101 | }
--------------------------------------------------------------------------------
/app/src/main/java/cn/saymagic/begonia/adapter/UserListAdapter.kt:
--------------------------------------------------------------------------------
1 | package cn.saymagic.begonia.adapter
2 |
3 | import android.support.v7.util.DiffUtil
4 | import android.support.v7.widget.RecyclerView
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import android.widget.ImageView
9 | import android.widget.TextView
10 | import cn.saymagic.begonia.R
11 | import cn.saymagic.begonia.fundamental.into
12 | import cn.saymagic.begonia.sdk.core.pojo.User
13 |
14 | class UserListAdapter(callback: DiffUtil.ItemCallback, eventListener: AdapterEventListener, retry: () -> Unit) : BaseNetRetryableAdapter(callback, eventListener, retry) {
15 |
16 | companion object {
17 | const val EVENT_CLICK_USER = "EVENT_CLICK_USER"
18 | }
19 |
20 | override fun onCreateContentViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
21 | return UserViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_user, null), eventListener)
22 | }
23 |
24 | override fun onBindContentViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
25 | if (holder is UserViewHolder) {
26 | val user = getItem(position)
27 | if (user != null) {
28 | holder.bindTo(user)
29 | }
30 | }
31 | }
32 |
33 | override fun getItemContentViewType(position: Int): Int {
34 | return R.layout.item_user
35 | }
36 |
37 | }
38 |
39 | class UserViewHolder(itemView: View, private val eventListener: AdapterEventListener) : RecyclerView.ViewHolder(itemView) {
40 |
41 | lateinit var user: User
42 | var userName: TextView
43 | var userDesc: TextView
44 | var userAvatar: ImageView
45 |
46 | init {
47 | val lp = RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
48 | itemView.layoutParams = lp
49 | itemView.setOnClickListener {
50 | eventListener.onEvent(UserListAdapter.EVENT_CLICK_USER, user, itemView)
51 | }
52 | userName = itemView.findViewById(R.id.user_name)
53 | userDesc = itemView.findViewById(R.id.user_desc)
54 | userAvatar = itemView.findViewById(R.id.user_avatar)
55 | }
56 |
57 | fun bindTo(user: User) {
58 | this.user = user
59 | userName.text = user.name
60 | userDesc.text = "${user.location ?: ""} ${user.badge?.slug ?: ""} "
61 | if (user.profileImage?.large != null) {
62 | userAvatar.into(user.profileImage.large, true)
63 | } else {
64 | userAvatar.setImageResource(R.drawable.circle)
65 | }
66 | }
67 |
68 | }
--------------------------------------------------------------------------------
/app/src/main/java/cn/saymagic/begonia/exception/UnexpectedCodeException.kt:
--------------------------------------------------------------------------------
1 | package cn.saymagic.begonia.exception
2 |
3 | class UnexpectedCodeException: Exception {
4 |
5 | constructor(code: Int?) : super( "unexpected code $code")
6 |
7 | }
--------------------------------------------------------------------------------
/app/src/main/java/cn/saymagic/begonia/fundamental/CircleTransform.java:
--------------------------------------------------------------------------------
1 | package cn.saymagic.begonia.fundamental;
2 |
3 | import android.graphics.Bitmap;
4 | import android.graphics.BitmapShader;
5 | import android.graphics.Canvas;
6 | import android.graphics.Paint;
7 |
8 | import com.squareup.picasso.Transformation;
9 |
10 | public class CircleTransform implements Transformation {
11 | @Override
12 | public Bitmap transform(Bitmap source) {
13 | int size = Math.min(source.getWidth(), source.getHeight());
14 |
15 | int x = (source.getWidth() - size) / 2;
16 | int y = (source.getHeight() - size) / 2;
17 |
18 | Bitmap squaredBitmap = Bitmap.createBitmap(source, x, y, size, size);
19 | if (squaredBitmap != source) {
20 | source.recycle();
21 | }
22 |
23 | Bitmap bitmap = Bitmap.createBitmap(size, size, source.getConfig());
24 |
25 | Canvas canvas = new Canvas(bitmap);
26 | Paint paint = new Paint();
27 | BitmapShader shader = new BitmapShader(squaredBitmap, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP);
28 | paint.setShader(shader);
29 | paint.setAntiAlias(true);
30 |
31 | float r = size/2f;
32 | canvas.drawCircle(r, r, r, paint);
33 |
34 | squaredBitmap.recycle();
35 | return bitmap;
36 | }
37 |
38 | @Override
39 | public String key() {
40 | return "circle";
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/main/java/cn/saymagic/begonia/fundamental/ImageExt.kt:
--------------------------------------------------------------------------------
1 | package cn.saymagic.begonia.fundamental
2 |
3 | import android.widget.ImageView
4 | import cn.saymagic.begonia.R
5 | import com.squareup.picasso.Picasso
6 |
7 |
8 | inline fun ImageView.into(url: String, circle : Boolean = false){
9 | if ("" != url) {
10 | if (circle) {
11 | Picasso.get().load(url).transform(CircleTransform()).placeholder(R.drawable.circle).into(this)
12 | } else {
13 | Picasso.get().load(url).placeholder(R.drawable.rectangle).into(this)
14 | }
15 | }
16 | }
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/saymagic/begonia/fundamental/SpacesItemDecoration.kt:
--------------------------------------------------------------------------------
1 | package cn.saymagic.begonia.fundamental
2 |
3 | import android.graphics.Rect
4 | import android.support.v7.widget.RecyclerView
5 | import android.view.View
6 |
7 | class SpacesItemDecoration : RecyclerView.ItemDecoration {
8 |
9 | private val space: Int
10 |
11 | constructor(space: Int) {
12 | this.space = space
13 | }
14 |
15 | override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
16 | outRect.left = space
17 | outRect.right = space
18 | outRect.bottom = space
19 | if (parent.getChildAdapterPosition(view) == 0) {
20 | outRect.top = space
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/cn/saymagic/begonia/pojo/AsyncResult.kt:
--------------------------------------------------------------------------------
1 | package cn.saymagic.begonia.pojo
2 |
3 | /**
4 | * Created by caoyanming on 2018/7/8.
5 | */
6 | class AsyncResult {
7 |
8 | var result: Any? = null
9 |
10 | var error: Throwable? = null
11 |
12 | constructor() {
13 | }
14 |
15 | constructor(result: Any) {
16 | this.result = result
17 | }
18 |
19 | constructor(error: Throwable) {
20 | this.error = error
21 | }
22 |
23 | fun isSuccess() : Boolean = error == null
24 |
25 | fun isFailed() : Boolean = !isSuccess()
26 |
27 | companion object {
28 | fun success(): AsyncResult {
29 | return AsyncResult()
30 | }
31 |
32 | fun success(result: Any): AsyncResult {
33 | return AsyncResult(result)
34 | }
35 |
36 | fun error(err: Throwable): AsyncResult {
37 | return AsyncResult(err)
38 | }
39 | }
40 | }
41 |
42 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/saymagic/begonia/pojo/DataSourceParam.kt:
--------------------------------------------------------------------------------
1 | package cn.saymagic.begonia.pojo
2 |
3 | import android.os.Bundle
4 | import android.os.Parcel
5 | import android.os.Parcelable
6 |
7 | class DataSourceParam(val type: String, private val bundle: Bundle = Bundle()) : Parcelable {
8 |
9 | constructor(parcel: Parcel) : this(parcel.readString(), parcel.readBundle())
10 |
11 | fun getString(key: String): String? = bundle.getString(key)
12 |
13 | fun putString(key: String, value: String): DataSourceParam = this.apply {
14 | bundle.putString(key, value)
15 | }
16 |
17 | override fun writeToParcel(parcel: Parcel, flags: Int) {
18 | parcel.writeString(type)
19 | parcel.writeBundle(bundle)
20 | }
21 |
22 | override fun describeContents(): Int {
23 | return 0
24 | }
25 |
26 | companion object CREATOR : Parcelable.Creator {
27 | override fun createFromParcel(parcel: Parcel): DataSourceParam {
28 | return DataSourceParam(parcel)
29 | }
30 |
31 | override fun newArray(size: Int): Array {
32 | return arrayOfNulls(size)
33 | }
34 | }
35 |
36 | }
--------------------------------------------------------------------------------
/app/src/main/java/cn/saymagic/begonia/repository/NetworkState.kt:
--------------------------------------------------------------------------------
1 |
2 |
3 | package cn.saymagic.begonia.repository
4 |
5 | enum class Status {
6 | RUNNING,
7 | SUCCESS,
8 | FAILED
9 | }
10 |
11 | @Suppress("DataClassPrivateConstructor")
12 | data class NetworkState private constructor(
13 | val status: Status,
14 | val msg: Throwable? = null) {
15 | companion object {
16 | val LOADED = NetworkState(Status.SUCCESS)
17 | val LOADING = NetworkState(Status.RUNNING)
18 | fun error(error: Throwable?) = NetworkState(Status.FAILED, error)
19 | }
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/cn/saymagic/begonia/repository/datasource/CollectionListDataSource.kt:
--------------------------------------------------------------------------------
1 | package cn.saymagic.begonia.repository.datasource
2 |
3 | import android.arch.lifecycle.MutableLiveData
4 | import android.arch.paging.PositionalDataSource
5 | import cn.saymagic.begonia.exception.UnexpectedCodeException
6 | import cn.saymagic.begonia.pojo.DataSourceParam
7 | import cn.saymagic.begonia.repository.NetworkState
8 | import cn.saymagic.begonia.sdk.core.Unsplash
9 | import cn.saymagic.begonia.sdk.core.api.UnsplashService
10 | import cn.saymagic.begonia.sdk.core.pojo.Collection
11 | import cn.saymagic.begonia.sdk.core.pojo.SearchResult
12 | import cn.saymagic.begonia.util.Constants
13 | import cn.saymagic.begonia.util.Constants.COLLECTION_DATA_PARAM_USERNAME
14 | import retrofit2.Call
15 | import retrofit2.Callback
16 | import retrofit2.Response
17 |
18 | class CollectionListDataSource(private val dataParam: DataSourceParam) : PositionalDataSource() {
19 |
20 | private val api: UnsplashService = Unsplash.getInstance().service
21 |
22 | val networkState: MutableLiveData = MutableLiveData()
23 | var retry: Runnable? = null
24 |
25 | private fun getCollectionListLoadRangeCallback(params: LoadRangeParams, callback: LoadRangeCallback): Callback> {
26 | return object : Callback> {
27 | override fun onFailure(call: Call>?, t: Throwable?) {
28 | retry = Runnable {
29 | loadRange(params, callback)
30 | }
31 | networkState.postValue(NetworkState.error(t))
32 | }
33 |
34 | override fun onResponse(call: Call>?, response: Response>?) {
35 | retry = if (response?.isSuccessful == true) {
36 | callback.onResult(response?.body()!!)
37 | networkState.postValue(NetworkState.LOADED)
38 | null
39 | } else {
40 | networkState.postValue(NetworkState.error(UnexpectedCodeException(response?.code())))
41 | Runnable {
42 | loadRange(params, callback)
43 | }
44 | }
45 | }
46 | }
47 | }
48 |
49 | private fun getSearchResultLoadRangeCallback(params: LoadRangeParams, callback: LoadRangeCallback): Callback> {
50 | return object : Callback> {
51 | override fun onFailure(call: Call>?, t: Throwable?) {
52 | retry = Runnable {
53 | loadRange(params, callback)
54 | }
55 | networkState.postValue(NetworkState.error(t))
56 | }
57 |
58 | override fun onResponse(call: Call>?, response: Response>?) {
59 | retry = if (response?.isSuccessful == true) {
60 | callback.onResult(response?.body()!!.results)
61 | networkState.postValue(NetworkState.LOADED)
62 | null
63 | } else {
64 | networkState.postValue(NetworkState.error(UnexpectedCodeException(response?.code())))
65 | Runnable {
66 | loadRange(params, callback)
67 | }
68 | }
69 | }
70 |
71 | }
72 | }
73 |
74 | override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback) {
75 | networkState.postValue(NetworkState.LOADING)
76 | when (dataParam.type) {
77 | Constants.COLLECTION_DATA_TYPE_USER -> api.getUserCollections(dataParam.getString(COLLECTION_DATA_PARAM_USERNAME), params.startPosition, params.loadSize)
78 | .enqueue(getCollectionListLoadRangeCallback(params, callback))
79 | Constants.COLLECTION_DATA_TYPE_SEARCH -> {
80 | api.searchCollections(dataParam.getString(Constants.COLLECTION_DATA_PARAM_SEARCH_TEXT), params.startPosition, params.loadSize)
81 | .enqueue(getSearchResultLoadRangeCallback(params, callback))
82 | }
83 | }
84 | }
85 |
86 | private fun getCollectionListLoadInitialCallback(params: LoadInitialParams, callback: LoadInitialCallback): Callback> {
87 | return object : Callback> {
88 | override fun onFailure(call: Call>?, t: Throwable?) {
89 | retry = Runnable {
90 | loadInitial(params, callback)
91 | }
92 | networkState.postValue(NetworkState.error(t))
93 | }
94 |
95 | override fun onResponse(call: Call>?, response: Response>?) {
96 | retry = if (response?.isSuccessful == true) {
97 | val total = try {
98 | Integer.parseInt(response.raw()?.header("x-total", "-1"))
99 | } catch (e: Exception) {
100 | -1
101 | }
102 | if (total >= 0) {
103 | callback.onResult(response?.body()!!, 0, total)
104 | } else {
105 | callback.onResult(response?.body()!!, response?.body()?.size
106 | ?: 0)
107 | }
108 | networkState.postValue(NetworkState.LOADED)
109 | null
110 | } else {
111 | networkState.postValue(NetworkState.error(UnexpectedCodeException(response?.code())))
112 | Runnable {
113 | loadInitial(params, callback)
114 | }
115 | }
116 | }
117 | }
118 | }
119 |
120 | private fun getSearchResultLoadInitialCallback(params: LoadInitialParams, callback: LoadInitialCallback): Callback> {
121 | return object : Callback> {
122 | override fun onFailure(call: Call>?, t: Throwable?) {
123 | retry = Runnable {
124 | loadInitial(params, callback)
125 | }
126 | networkState.postValue(NetworkState.error(t))
127 | }
128 |
129 | override fun onResponse(call: Call>?, response: Response>?) {
130 | retry = if (response?.isSuccessful == true) {
131 | val total = try {
132 | response.body()?.total
133 | ?: Integer.parseInt(response.raw()?.header("x-total", "-1"))
134 | } catch (e: Exception) {
135 | -1
136 | }
137 | if (total >= 0) {
138 | callback.onResult(response?.body()!!.results, 0, total)
139 | } else {
140 | callback.onResult(response?.body()!!.results, response?.body()?.results?.size
141 | ?: 0)
142 | }
143 | networkState.postValue(NetworkState.LOADED)
144 | null
145 | } else {
146 | networkState.postValue(NetworkState.error(UnexpectedCodeException(response?.code())))
147 | Runnable {
148 | loadInitial(params, callback)
149 | }
150 | }
151 | }
152 |
153 | }
154 | }
155 |
156 | override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) {
157 | networkState.postValue(NetworkState.LOADING)
158 | when (dataParam.type) {
159 | Constants.COLLECTION_DATA_TYPE_USER -> {
160 | api.getUserCollections(dataParam.getString(Constants.COLLECTION_DATA_PARAM_USERNAME), params.requestedStartPosition, params.requestedLoadSize)
161 | .enqueue(getCollectionListLoadInitialCallback(params, callback))
162 | }
163 | Constants.COLLECTION_DATA_TYPE_SEARCH -> {
164 | api.searchCollections(dataParam.getString(Constants.COLLECTION_DATA_PARAM_SEARCH_TEXT), params.requestedStartPosition, params.requestedLoadSize)
165 | .enqueue(getSearchResultLoadInitialCallback(params, callback))
166 | }
167 | }
168 | }
169 |
170 |
171 | }
--------------------------------------------------------------------------------
/app/src/main/java/cn/saymagic/begonia/repository/datasource/CollectionListDataSourceFactory.kt:
--------------------------------------------------------------------------------
1 | package cn.saymagic.begonia.repository.datasource
2 |
3 | import android.arch.lifecycle.MutableLiveData
4 | import android.arch.paging.DataSource
5 | import cn.saymagic.begonia.pojo.DataSourceParam
6 | import cn.saymagic.begonia.sdk.core.pojo.Collection
7 |
8 | class CollectionListDataSourceFactory(private val dataParam: DataSourceParam) : DataSource.Factory() {
9 |
10 | val sourceLiveListData : MutableLiveData = MutableLiveData()
11 |
12 | override fun create(): DataSource {
13 | val source = CollectionListDataSource(dataParam)
14 | sourceLiveListData.postValue(source)
15 | return source
16 | }
17 |
18 | }
--------------------------------------------------------------------------------
/app/src/main/java/cn/saymagic/begonia/repository/datasource/PhotoListDataSource.kt:
--------------------------------------------------------------------------------
1 | package cn.saymagic.begonia.repository.datasource
2 |
3 | import android.arch.lifecycle.MutableLiveData
4 | import android.arch.paging.PositionalDataSource
5 | import cn.saymagic.begonia.exception.UnexpectedCodeException
6 | import cn.saymagic.begonia.pojo.DataSourceParam
7 | import cn.saymagic.begonia.repository.NetworkState
8 | import cn.saymagic.begonia.sdk.core.Unsplash
9 | import cn.saymagic.begonia.sdk.core.UnsplashConstants
10 | import cn.saymagic.begonia.sdk.core.api.UnsplashService
11 | import cn.saymagic.begonia.sdk.core.pojo.Photo
12 | import cn.saymagic.begonia.sdk.core.pojo.SearchResult
13 | import cn.saymagic.begonia.util.Constants
14 | import retrofit2.Call
15 | import retrofit2.Callback
16 | import retrofit2.Response
17 |
18 | class PhotoListDataSource(private val dataParam: DataSourceParam) : PositionalDataSource() {
19 |
20 | private val api: UnsplashService = Unsplash.getInstance().service
21 |
22 | val networkState: MutableLiveData = MutableLiveData()
23 |
24 | var retry: Runnable? = null
25 |
26 | override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback) {
27 | networkState.postValue(NetworkState.LOADING)
28 |
29 | when (dataParam.type) {
30 | Constants.PHOTO_DATA_TYPE_NORMAL -> api.getPhotos(params.startPosition, params.loadSize, UnsplashConstants.ORDER_BY_LATEST)
31 | .enqueue(getPhotoListLoadRangeCallback(params, callback))
32 | Constants.PHOTO_DATA_TYPE_USER -> api.getUserPhotos(dataParam.getString(Constants.PHOTO_DATA_PARAM_USERNAME), params.startPosition, params.loadSize)
33 | .enqueue(getPhotoListLoadRangeCallback(params, callback))
34 | Constants.PHOTO_DATA_TYPE_USER_LIKED -> api.getUserLikes(dataParam.getString(Constants.PHOTO_DATA_PARAM_USERNAME), params.startPosition, params.loadSize, UnsplashConstants.ORDER_BY_LATEST)
35 | .enqueue(getPhotoListLoadRangeCallback(params, callback))
36 | Constants.PHOTO_DATA_TYPE_COLLECTION -> api.getCollectionPhotos(dataParam.getString(Constants.PHOTO_DATA_PARAM_COLLECTION_ID), params.startPosition, params.loadSize)
37 | .enqueue(getPhotoListLoadRangeCallback(params, callback))
38 | Constants.PHOTO_DATA_TYPE_SEARCH -> api.searchPhotos(dataParam.getString(Constants.PHOTO_DATA_PARAM_SEARCH_TEXT), params.startPosition, params.loadSize)
39 | .enqueue(getSearchResultLoadRangeCallback(params, callback))
40 | }
41 | }
42 |
43 | private fun getPhotoListLoadRangeCallback(params: LoadRangeParams, callback: LoadRangeCallback): Callback> {
44 | return object : Callback> {
45 | override fun onFailure(call: Call>?, t: Throwable?) {
46 | networkState.postValue(NetworkState.error(t))
47 | retry = Runnable {
48 | loadRange(params, callback)
49 | }
50 | }
51 |
52 | override fun onResponse(call: Call>?, response: Response>?) {
53 | retry = if (response?.isSuccessful == true) {
54 | callback.onResult(response?.body()!!)
55 | networkState.postValue(NetworkState.LOADED)
56 | null
57 | } else {
58 | networkState.postValue(NetworkState.error(UnexpectedCodeException(response?.code())))
59 | Runnable {
60 | loadRange(params, callback)
61 | }
62 | }
63 | }
64 | }
65 | }
66 |
67 | private fun getPhotoListLoadInitialCallback(params: LoadInitialParams, callback: LoadInitialCallback): Callback> {
68 | return object : Callback> {
69 | override fun onFailure(call: Call>?, t: Throwable?) {
70 | networkState.postValue(NetworkState.error(t))
71 | retry = Runnable {
72 | loadInitial(params, callback)
73 | }
74 | }
75 |
76 | override fun onResponse(call: Call>?, response: Response>?) {
77 | retry = if (response?.isSuccessful == true) {
78 | val total = try {
79 | Integer.parseInt(response.raw()?.header("x-total", "-1"))
80 | } catch (e: Exception) {
81 | -1
82 | }
83 | if (total >= 0) {
84 | callback.onResult(response?.body()!!, 0, total)
85 | } else {
86 | callback.onResult(response?.body()!!, response?.body()?.size ?: 0)
87 | }
88 | networkState.postValue(NetworkState.LOADED)
89 | null
90 | } else {
91 | networkState.postValue(NetworkState.error(UnexpectedCodeException(response?.code())))
92 | Runnable {
93 | loadInitial(params, callback)
94 | }
95 | }
96 | }
97 | }
98 | }
99 |
100 | private fun getSearchResultLoadRangeCallback(params: LoadRangeParams, callback: LoadRangeCallback): Callback> {
101 | return object : Callback> {
102 | override fun onFailure(call: Call>?, t: Throwable?) {
103 | networkState.postValue(NetworkState.error(t))
104 | retry = Runnable {
105 | loadRange(params, callback)
106 | }
107 | }
108 |
109 | override fun onResponse(call: Call>?, response: Response>?) {
110 | retry = if (response?.isSuccessful == true) {
111 | val total = try {
112 | response?.body()?.total?:Integer.parseInt(response.raw()?.header("x-total", "-1"))
113 | } catch (e: Exception) {
114 | -1
115 | }
116 | callback.onResult(response?.body()!!.results)
117 | networkState.postValue(NetworkState.LOADED)
118 | null
119 | } else {
120 | networkState.postValue(NetworkState.error(UnexpectedCodeException(response?.code())))
121 | Runnable {
122 | loadRange(params, callback)
123 | }
124 | }
125 | }
126 | }
127 | }
128 |
129 |
130 | private fun getSearchResultLoadInitialCallback(params: LoadInitialParams, callback: LoadInitialCallback): Callback> {
131 | return object : Callback> {
132 | override fun onFailure(call: Call>?, t: Throwable?) {
133 | networkState.postValue(NetworkState.error(t))
134 | retry = Runnable {
135 | loadInitial(params, callback)
136 | }
137 | }
138 |
139 | override fun onResponse(call: Call>?, response: Response>?) {
140 | retry = if (response?.isSuccessful == true) {
141 | val total = try {
142 | response?.body()?.total?:Integer.parseInt(response.raw()?.header("x-total", "-1"))
143 | } catch (e: Exception) {
144 | -1
145 | }
146 | if (total >= 0) {
147 | callback.onResult(response?.body()!!.results, 0, total)
148 | } else {
149 | callback.onResult(response?.body()!!.results, response?.body()?.results?.size ?: 0)
150 | }
151 | networkState.postValue(NetworkState.LOADED)
152 | null
153 | } else {
154 | networkState.postValue(NetworkState.error(UnexpectedCodeException(response?.code())))
155 | Runnable {
156 | loadInitial(params, callback)
157 | }
158 | }
159 | }
160 | }
161 | }
162 |
163 | override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) {
164 | networkState.postValue(NetworkState.LOADING)
165 | when (dataParam.type) {
166 | Constants.PHOTO_DATA_TYPE_NORMAL -> api.getPhotos(params.requestedStartPosition, params.requestedLoadSize, UnsplashConstants.ORDER_BY_LATEST)
167 | .enqueue(getPhotoListLoadInitialCallback(params, callback))
168 | Constants.PHOTO_DATA_TYPE_USER -> api.getUserPhotos(dataParam.getString(Constants.PHOTO_DATA_PARAM_USERNAME), params.requestedStartPosition, params.requestedLoadSize)
169 | .enqueue(getPhotoListLoadInitialCallback(params, callback))
170 | Constants.PHOTO_DATA_TYPE_USER_LIKED -> api.getUserLikes(dataParam.getString(Constants.PHOTO_DATA_PARAM_USERNAME), params.requestedStartPosition, params.requestedLoadSize, UnsplashConstants.ORDER_BY_LATEST)
171 | .enqueue(getPhotoListLoadInitialCallback(params, callback))
172 | Constants.PHOTO_DATA_TYPE_COLLECTION -> api.getCollectionPhotos(dataParam.getString(Constants.PHOTO_DATA_PARAM_COLLECTION_ID), params.requestedStartPosition, params.requestedLoadSize)
173 | .enqueue(getPhotoListLoadInitialCallback(params, callback))
174 | Constants.PHOTO_DATA_TYPE_SEARCH -> api.searchPhotos(dataParam.getString(Constants.PHOTO_DATA_PARAM_SEARCH_TEXT), params.requestedStartPosition, params.requestedLoadSize)
175 | .enqueue(getSearchResultLoadInitialCallback(params, callback))
176 | }
177 | }
178 |
179 |
180 | }
--------------------------------------------------------------------------------
/app/src/main/java/cn/saymagic/begonia/repository/datasource/PhotoListDataSourceFactory.kt:
--------------------------------------------------------------------------------
1 | package cn.saymagic.begonia.repository.datasource
2 |
3 | import android.arch.lifecycle.MutableLiveData
4 | import android.arch.paging.DataSource
5 | import cn.saymagic.begonia.pojo.DataSourceParam
6 | import cn.saymagic.begonia.sdk.core.pojo.Photo
7 |
8 | class PhotoListDataSourceFactory(val dataParam: DataSourceParam) : DataSource.Factory() {
9 |
10 | val sourceLiveData : MutableLiveData = MutableLiveData()
11 |
12 | override fun create(): DataSource {
13 | val source = PhotoListDataSource(dataParam)
14 | sourceLiveData.postValue(source)
15 | return source
16 | }
17 |
18 | }
--------------------------------------------------------------------------------
/app/src/main/java/cn/saymagic/begonia/repository/datasource/UserListDataSource.kt:
--------------------------------------------------------------------------------
1 | package cn.saymagic.begonia.repository.datasource
2 |
3 | import android.arch.lifecycle.MutableLiveData
4 | import android.arch.paging.PositionalDataSource
5 | import cn.saymagic.begonia.exception.UnexpectedCodeException
6 | import cn.saymagic.begonia.pojo.DataSourceParam
7 | import cn.saymagic.begonia.repository.NetworkState
8 | import cn.saymagic.begonia.sdk.core.Unsplash
9 | import cn.saymagic.begonia.sdk.core.api.UnsplashService
10 | import cn.saymagic.begonia.sdk.core.pojo.SearchResult
11 | import cn.saymagic.begonia.sdk.core.pojo.User
12 | import cn.saymagic.begonia.util.Constants
13 | import retrofit2.Call
14 | import retrofit2.Callback
15 | import retrofit2.Response
16 |
17 | class UserListDataSource(private val dataParam: DataSourceParam) : PositionalDataSource() {
18 |
19 | private val api: UnsplashService = Unsplash.getInstance().service
20 |
21 | val networkState: MutableLiveData = MutableLiveData()
22 |
23 | var retry: Runnable? = null
24 |
25 | override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) {
26 | networkState.postValue(NetworkState.LOADING)
27 | when (dataParam.type) {
28 | Constants.USER_DATA_TYPE_SEARCH -> api.searchUsers(dataParam.getString(Constants.USER_DATA_TYPE_PARAM_SEARCH_TEXT), params.requestedStartPosition, params.pageSize)
29 | .enqueue(getSearchResultLoadInitialCallback(params, callback))
30 | }
31 | }
32 |
33 | override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback) {
34 | networkState.postValue(NetworkState.LOADING)
35 | when (dataParam.type) {
36 | Constants.USER_DATA_TYPE_SEARCH -> api.searchUsers(dataParam.getString(Constants.USER_DATA_TYPE_PARAM_SEARCH_TEXT), params.startPosition, params.loadSize)
37 | .enqueue(getSearchResultLoadRangeCallback(params, callback))
38 | }
39 | }
40 |
41 | private fun getSearchResultLoadInitialCallback(params: PositionalDataSource.LoadInitialParams, callback: PositionalDataSource.LoadInitialCallback): Callback>? {
42 | return object : Callback> {
43 | override fun onFailure(call: Call>?, t: Throwable?) {
44 | retry = Runnable {
45 | loadInitial(params, callback)
46 | }
47 | networkState.postValue(NetworkState.error(t))
48 | }
49 |
50 | override fun onResponse(call: Call>?, response: Response>?) {
51 | retry = if (response?.isSuccessful == true) {
52 | val total = try {
53 | response?.body()?.total
54 | ?: Integer.parseInt(response.raw()?.header("x-total", "-1"))
55 | } catch (e: Exception) {
56 | -1
57 | }
58 | if (total >= 0) {
59 | callback.onResult(response?.body()!!.results, 0, total)
60 | } else {
61 | callback.onResult(response?.body()!!.results, response?.body()?.results?.size
62 | ?: 0)
63 | }
64 | networkState.postValue(NetworkState.LOADED)
65 | null
66 | } else {
67 | networkState.postValue(NetworkState.error(UnexpectedCodeException(response?.code())))
68 | Runnable {
69 | loadInitial(params, callback)
70 | }
71 | }
72 | }
73 | }
74 | }
75 |
76 | private fun getSearchResultLoadRangeCallback(params: PositionalDataSource.LoadRangeParams, callback: PositionalDataSource.LoadRangeCallback): Callback>? {
77 | return object : Callback> {
78 | override fun onFailure(call: Call>?, t: Throwable?) {
79 | retry = Runnable {
80 | loadRange(params, callback)
81 | }
82 | networkState.postValue(NetworkState.error(t))
83 | }
84 |
85 | override fun onResponse(call: Call>?, response: Response>?) {
86 | retry = if (response?.isSuccessful == true) {
87 | callback.onResult(response?.body()!!.results)
88 | networkState.postValue(NetworkState.LOADED)
89 | null
90 | } else {
91 | networkState.postValue(NetworkState.error(UnexpectedCodeException(response?.code())))
92 | Runnable {
93 | loadRange(params, callback)
94 | }
95 | }
96 | }
97 |
98 | }
99 | }
100 |
101 |
102 | }
--------------------------------------------------------------------------------
/app/src/main/java/cn/saymagic/begonia/repository/datasource/UserListDataSourceFactory.kt:
--------------------------------------------------------------------------------
1 | package cn.saymagic.begonia.repository.datasource
2 |
3 | import android.arch.lifecycle.MutableLiveData
4 | import android.arch.paging.DataSource
5 | import cn.saymagic.begonia.pojo.DataSourceParam
6 | import cn.saymagic.begonia.sdk.core.pojo.Collection
7 | import cn.saymagic.begonia.sdk.core.pojo.User
8 |
9 | class UserListDataSourceFactory(private val dataParam: DataSourceParam) : DataSource.Factory() {
10 |
11 | val sourceLiveData : MutableLiveData = MutableLiveData()
12 |
13 | override fun create(): DataSource {
14 | val source = UserListDataSource(dataParam)
15 | sourceLiveData.postValue(source)
16 | return source
17 | }
18 |
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/cn/saymagic/begonia/repository/remote/UserRepository.kt:
--------------------------------------------------------------------------------
1 | package cn.saymagic.begonia.repository.remote
2 |
3 | import android.arch.lifecycle.LiveData
4 | import android.arch.lifecycle.MutableLiveData
5 | import android.util.Log
6 | import cn.saymagic.begonia.repository.NetworkState
7 | import cn.saymagic.begonia.sdk.core.Unsplash
8 | import cn.saymagic.begonia.sdk.core.pojo.User
9 | import retrofit2.Call
10 | import retrofit2.Callback
11 | import retrofit2.Response
12 |
13 | class UserRepository {
14 |
15 | val service = Unsplash.getInstance().service
16 |
17 | fun getUser(userName: String) : UserProfileData{
18 | val networkState: MutableLiveData = MutableLiveData()
19 | val user: MutableLiveData = MutableLiveData()
20 | networkState.postValue(NetworkState.LOADING)
21 | service.getUser(userName).enqueue(object : Callback {
22 | override fun onFailure(call: Call?, t: Throwable?) {
23 | networkState.postValue(NetworkState.error(t))
24 | }
25 | override fun onResponse(call: Call?, response: Response?) {
26 | if (response?.isSuccessful == true) {
27 | user.postValue(response?.body())
28 | networkState.postValue(NetworkState.LOADED)
29 | } else {
30 | networkState.postValue(NetworkState.error(null))
31 | }
32 | }
33 | })
34 | return UserProfileData(user, networkState)
35 | }
36 |
37 | }
38 |
39 | class UserProfileData(val user: LiveData, val networkState: LiveData)
--------------------------------------------------------------------------------
/app/src/main/java/cn/saymagic/begonia/repository/remote/ViewPhotoRepository.kt:
--------------------------------------------------------------------------------
1 | package cn.saymagic.begonia.repository.remote
2 |
3 | import android.arch.lifecycle.LiveData
4 | import android.arch.lifecycle.MutableLiveData
5 | import android.graphics.Bitmap
6 | import android.graphics.drawable.Drawable
7 | import cn.saymagic.begonia.repository.NetworkState
8 | import cn.saymagic.begonia.sdk.core.Unsplash
9 | import cn.saymagic.begonia.sdk.core.pojo.Download
10 | import cn.saymagic.begonia.sdk.core.pojo.Photo
11 | import com.squareup.picasso.Picasso
12 | import com.squareup.picasso.Target
13 | import retrofit2.Call
14 | import retrofit2.Callback
15 | import retrofit2.Response
16 | import java.lang.Exception
17 |
18 | class ViewPhotoRepository {
19 |
20 | private val service = Unsplash.getInstance().service!!
21 |
22 | private var retry: Runnable? = null
23 |
24 | val networkState: MutableLiveData = MutableLiveData()
25 | val photoBitmapLiveData: MutableLiveData = MutableLiveData()
26 |
27 | val targetList: MutableList = ArrayList()
28 |
29 | fun getPhoto(photoId: String): PhotoData {
30 | networkState.postValue(NetworkState.LOADING)
31 | service.getPhoto(photoId).enqueue(object : Callback {
32 | override fun onFailure(call: Call?, t: Throwable?) {
33 | networkState.postValue(NetworkState.error(t))
34 | retry = Runnable {
35 | getPhoto(photoId)
36 | }
37 | }
38 |
39 | override fun onResponse(call: Call?, response: Response?) {
40 | val photo = response?.body()!!
41 | downloadToBitmap(photo)
42 | }
43 | })
44 | return PhotoData(networkState, photoBitmapLiveData, {
45 | retry?.run()
46 | })
47 | }
48 |
49 | private fun downloadToBitmap(photo: Photo) {
50 | val target = object : Target {
51 | override fun onPrepareLoad(placeHolderDrawable: Drawable?) {
52 | }
53 |
54 | override fun onBitmapFailed(e: Exception?, errorDrawable: Drawable?) {
55 | networkState.postValue(NetworkState.error(e))
56 | retry = Runnable {
57 | downloadToBitmap(photo)
58 | }
59 | targetList.remove(this)
60 | }
61 |
62 | override fun onBitmapLoaded(bitmap: Bitmap?, from: Picasso.LoadedFrom?) {
63 | networkState.postValue(NetworkState.LOADED)
64 | photoBitmapLiveData.postValue(bitmap)
65 | retry = null
66 | targetList.remove(this)
67 | }
68 |
69 | }
70 | targetList.add(target)
71 | Picasso.get().load(photo.urls?.regular ?: "").into(target)
72 | }
73 |
74 | fun onCleared() {
75 | targetList.clear()
76 | }
77 |
78 | }
79 |
80 | data class PhotoData(val networkState: LiveData, val photo: LiveData, val retry: () -> Unit)
81 |
82 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/saymagic/begonia/ui/NetworkStateItemViewHolder.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2017 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package cn.saymagic.begonia.ui
18 |
19 | import android.support.v7.widget.RecyclerView
20 | import android.view.LayoutInflater
21 | import android.view.View
22 | import android.view.ViewGroup
23 | import android.widget.Button
24 | import android.widget.ProgressBar
25 | import android.widget.TextView
26 | import cn.saymagic.begonia.R
27 | import cn.saymagic.begonia.repository.NetworkState
28 | import cn.saymagic.begonia.repository.Status
29 | import android.support.v7.widget.StaggeredGridLayoutManager
30 |
31 |
32 |
33 |
34 | /**
35 | * A View Holder that can display a loading or have click action.
36 | * It is used to show the network state of paging.
37 | */
38 | class NetworkStateItemViewHolder(view: View,
39 | private val retryCallback: () -> Unit)
40 | : RecyclerView.ViewHolder(view) {
41 | private val progressBar = view.findViewById(R.id.progress_bar)
42 | private val retry = view.findViewById