├── .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 |
10 | 11 | 12 | 13 | 14 |
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