├── .idea ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── me │ │ └── mrrobot97 │ │ └── designer │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── fonts │ │ │ └── New Walt Disney.ttf │ ├── java │ │ └── me │ │ │ └── mrrobot97 │ │ │ └── designer │ │ │ ├── Glide │ │ │ └── CustomGlideModule.java │ │ │ ├── MyApplication.java │ │ │ ├── SwipeActivity │ │ │ ├── ActivityLifeCycleHelper.java │ │ │ ├── SwipeBackActivity.java │ │ │ └── TouchHelper.java │ │ │ ├── Utils │ │ │ ├── BitmapUtils.java │ │ │ ├── FileUtils.java │ │ │ ├── NetUtils.java │ │ │ ├── ScreenUtils.java │ │ │ ├── SharedPreferencesUtils.java │ │ │ └── StringUtils.java │ │ │ ├── adapter │ │ │ ├── AttachmentsAdapter.java │ │ │ └── ShotsAdapter.java │ │ │ ├── contracts │ │ │ ├── BrowseContract.java │ │ │ ├── DetailContract.java │ │ │ └── PlayerContract.java │ │ │ ├── customViews │ │ │ ├── CircleImageView.java │ │ │ ├── FullScreenView.java │ │ │ └── HoverView.java │ │ │ ├── model │ │ │ ├── Attachment.java │ │ │ ├── Comment.java │ │ │ ├── IModel.java │ │ │ ├── Images.java │ │ │ ├── Links.java │ │ │ ├── ModelImpl.java │ │ │ ├── Shot.java │ │ │ └── User.java │ │ │ ├── presenter │ │ │ ├── BrowsePresenter.java │ │ │ ├── DetailPresenter.java │ │ │ └── PlayerPresenter.java │ │ │ ├── retrofit │ │ │ ├── ApiClient.java │ │ │ └── DribbbleService.java │ │ │ └── view │ │ │ ├── BaseFragment.java │ │ │ ├── BrowseActivity.java │ │ │ ├── DetailActivity.java │ │ │ ├── LoginActivity.java │ │ │ └── PlayerActivity.java │ └── res │ │ ├── drawable │ │ ├── browser.png │ │ ├── comments.png │ │ ├── dribbble.png │ │ ├── group.png │ │ ├── ic_account.png │ │ ├── ic_arrow.png │ │ ├── ic_menu.png │ │ ├── link.png │ │ ├── loading.gif │ │ ├── placeholder.png │ │ ├── round_rectangle.xml │ │ ├── share.png │ │ ├── twitter.png │ │ └── user.png │ │ ├── layout │ │ ├── activity_browse.xml │ │ ├── activity_detail.xml │ │ ├── activity_login.xml │ │ ├── activity_player.xml │ │ ├── author_and_shot_layout.xml │ │ ├── comment_item_layout.xml │ │ ├── fragment_layout.xml │ │ ├── internet_error_view.xml │ │ ├── recyclerview_item_layout.xml │ │ └── user_menu_item_view.xml │ │ ├── menu │ │ └── menu_browse.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── icon.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── me │ └── mrrobot97 │ └── designer │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > Dribbble是一个优秀的设计师网站,这里有上万优秀设计师为移动开发人员提供了海量精美的UI资源。Dribbble很早就开放了API,也有许多优秀的第三方客户端,本着学习的目的,我在课余时间写了这个还很粗糙的客户端。目前的功能还很简陋,用户体验也不是很完善,主要是因为Dribbble的API服务器在国外,国内加载资源很慢。不过,我会慢慢更新的,一点一点把它变得更好。 2 | 3 | [项目github地址](https://github.com/mrrobot97/Designer) 4 | 5 | 先看一下预览图: 6 | 7 | ![](https://blog-1256554550.cos.ap-beijing.myqcloud.com/screener_20161029%2800_13_49%29.jpg)![](https://blog-1256554550.cos.ap-beijing.myqcloud.com/screener_20161029%2800_13_49%29.jpg) 8 | 9 | ![](https://blog-1256554550.cos.ap-beijing.myqcloud.com/screener_20161028%2823_54_36%29.jpg)![](https://blog-1256554550.cos.ap-beijing.myqcloud.com/screener_20161028%2823_54_36%29.jpg) 10 | 11 | ![](https://blog-1256554550.cos.ap-beijing.myqcloud.com/screener_20161029%2800_14_23%29.png)![](https://blog-1256554550.cos.ap-beijing.myqcloud.com/screener_20161029%2800_14_23%29.png) 12 | 13 | ![](https://blog-1256554550.cos.ap-beijing.myqcloud.com/screener_20161029%2800_15_02%29.png)![](https://blog-1256554550.cos.ap-beijing.myqcloud.com/screener_20161029%2800_15_02%29.png) 14 | 15 | ![](https://blog-1256554550.cos.ap-beijing.myqcloud.com/screener_20161029%2800_15_24%29.png)![](https://blog-1256554550.cos.ap-beijing.myqcloud.com/screener_20161029%2800_15_24%29.png) 16 | 17 | ![](https://blog-1256554550.cos.ap-beijing.myqcloud.com/screener_20161029%2800_15_39%29.png)![](https://blog-1256554550.cos.ap-beijing.myqcloud.com/screener_20161029%2800_15_39%29.png) 18 | 19 | 主要使用的技术和开源项目: 20 | 21 | ``` 22 | 1. MVP设计模式。 23 | 2. RxJAVA 24 | 3. Retrofit 25 | 4. OkHttp 26 | 5. Glide 27 | ``` 28 | 29 | feature: 30 | 31 | ``` 32 | 1.支持页面滑动返回 33 | 2.支持离线缓存浏览 34 | 3.支持JAVA 8 35 | 4.支持Oauth2.0认证登录 36 | ``` 37 | 38 | todo list: 39 | 40 | 1. ~~使用数据库离线缓存,节省客户端流量。~~ (已使用OKHttp离线缓存代替) 41 | 42 | 2. 使用palette动态改变背景色,增加美观性。 43 | 44 | 3. 使用Dribbble提供的Oauth2认证允许用户登录,并对每个设计进行评论,点赞,收藏等功能。(已初步实现Oauth2.0认证登陆) 45 | 46 | 4. 改善图片加载速度,提供友好的用户反馈。 47 | 48 | 5. 发现并消灭BUG,提高软件使用的稳定性。 49 | 50 | 51 | 欢迎fork、issue、star。 52 | 53 | # ChangeLog 54 | 55 | ## version 0.1.1 56 | 57 | ``` 58 | 1.加入Oauth2.0登录入口,用户可以登录并查看自己的个人信息。 59 | 2.利用OKHttp加入离线缓存,没有网也可以愉快的玩耍了。 60 | ``` 61 | 62 | ## [version 0.1.1 apk download](https://blog-1256554550.cos.ap-beijing.myqcloud.com/release0.1.1.apk) 63 | 64 | ## [version 0.1.0 apk download](https://blog-1256554550.cos.ap-beijing.myqcloud.com/Designer.apk) 65 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 24 5 | buildToolsVersion "24.0.1" 6 | defaultConfig { 7 | applicationId "me.mrrobot97.designer" 8 | minSdkVersion 14 9 | targetSdkVersion 24 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | jackOptions { 14 | enabled true 15 | } 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | compileOptions { 24 | sourceCompatibility JavaVersion.VERSION_1_8 25 | targetCompatibility JavaVersion.VERSION_1_8 26 | } 27 | } 28 | 29 | dependencies { 30 | compile fileTree(dir: 'libs', include: ['*.jar']) 31 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 32 | exclude group: 'com.android.support', module: 'support-annotations' 33 | }) 34 | compile 'com.android.support:appcompat-v7:24.2.0' 35 | compile 'com.android.support:design:24.2.0' 36 | testCompile 'junit:junit:4.12' 37 | compile 'com.android.support:palette-v7:24.2.0' 38 | compile 'com.android.support:support-v4:24.2.0' 39 | 40 | 41 | compile 'io.reactivex:rxandroid:1.2.1' 42 | compile 'io.reactivex:rxjava:1.1.6' 43 | compile 'com.google.code.gson:gson:2.6.2' 44 | compile 'com.squareup.retrofit2:retrofit:2.1.0' 45 | compile 'com.squareup.retrofit2:converter-gson:2.1.0' 46 | compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0' 47 | compile 'com.github.bumptech.glide:okhttp3-integration:1.4.0@aar' 48 | compile 'com.squareup.okhttp3:okhttp:3.4.1' 49 | compile 'com.jakewharton:butterknife:8.4.0' 50 | annotationProcessor 'com.jakewharton:butterknife-compiler:8.4.0' 51 | compile 'com.github.bumptech.glide:glide:3.7.0' 52 | 53 | debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5' 54 | releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5' 55 | testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5' 56 | 57 | } 58 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/mrrobot/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/me/mrrobot97/designer/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("me.mrrobot97.designer", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 17 | 20 | 21 | 22 | 23 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/assets/fonts/New Walt Disney.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrrobot97/Designer/d2e156f3613896b568329f52ea468a029b5b80df/app/src/main/assets/fonts/New Walt Disney.ttf -------------------------------------------------------------------------------- /app/src/main/java/me/mrrobot97/designer/Glide/CustomGlideModule.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer.Glide; 2 | 3 | import android.content.Context; 4 | 5 | import com.bumptech.glide.Glide; 6 | import com.bumptech.glide.GlideBuilder; 7 | import com.bumptech.glide.load.engine.cache.ExternalCacheDiskCacheFactory; 8 | import com.bumptech.glide.load.engine.cache.LruResourceCache; 9 | import com.bumptech.glide.module.GlideModule; 10 | 11 | /** 12 | * Created by mrrobot on 16/11/4. 13 | */ 14 | 15 | public class CustomGlideModule implements GlideModule { 16 | private int cacheSize = 1024 * 1024 * 200; //200MB 17 | private int memoryCacheSize=1024*30; //30MB 18 | 19 | @Override 20 | public void applyOptions(Context context, GlideBuilder builder) { 21 | builder.setDiskCache(new ExternalCacheDiskCacheFactory(context, cacheSize)) 22 | .setMemoryCache(new LruResourceCache(memoryCacheSize)); 23 | } 24 | 25 | @Override 26 | public void registerComponents(Context context, Glide glide) { 27 | 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/me/mrrobot97/designer/MyApplication.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | import com.squareup.leakcanary.LeakCanary; 7 | 8 | import me.mrrobot97.designer.SwipeActivity.ActivityLifeCycleHelper; 9 | 10 | /** 11 | * Created by mrrobot on 16/10/21. 12 | */ 13 | 14 | public class MyApplication extends Application { 15 | public ActivityLifeCycleHelper getHelper() { 16 | return mHelper; 17 | } 18 | 19 | private static Context mContext; 20 | 21 | private ActivityLifeCycleHelper mHelper; 22 | @Override 23 | public void onCreate() { 24 | super.onCreate(); 25 | if (LeakCanary.isInAnalyzerProcess(this)) { 26 | // This process is dedicated to LeakCanary for heap analysis. 27 | // You should not init your app in this process. 28 | return; 29 | } 30 | LeakCanary.install(this); 31 | 32 | mHelper=new ActivityLifeCycleHelper(); 33 | //store all the activities 34 | registerActivityLifecycleCallbacks(mHelper); 35 | mContext=this.getApplicationContext(); 36 | } 37 | 38 | public static Context getContext(){ 39 | return mContext; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/me/mrrobot97/designer/SwipeActivity/ActivityLifeCycleHelper.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer.SwipeActivity; 2 | 3 | import android.app.Activity; 4 | import android.app.Application; 5 | import android.os.Bundle; 6 | 7 | import java.util.Stack; 8 | 9 | /** 10 | * Created by mrrobot on 16/10/21. 11 | */ 12 | 13 | public class ActivityLifeCycleHelper implements Application.ActivityLifecycleCallbacks { 14 | private Stack mActivities; 15 | 16 | public ActivityLifeCycleHelper(){ 17 | mActivities=new Stack<>(); 18 | } 19 | 20 | @Override 21 | public void onActivityCreated(Activity activity, Bundle bundle) { 22 | mActivities.add(activity); 23 | } 24 | 25 | @Override 26 | public void onActivityStarted(Activity activity) { 27 | 28 | } 29 | 30 | @Override 31 | public void onActivityResumed(Activity activity) { 32 | 33 | } 34 | 35 | @Override 36 | public void onActivityPaused(Activity activity) { 37 | 38 | } 39 | 40 | @Override 41 | public void onActivityStopped(Activity activity) { 42 | 43 | } 44 | 45 | @Override 46 | public void onActivitySaveInstanceState(Activity activity, Bundle bundle) { 47 | 48 | } 49 | 50 | @Override 51 | public void onActivityDestroyed(Activity activity) { 52 | mActivities.remove(activity); 53 | } 54 | 55 | public Activity getPreActivity(){ 56 | int size=mActivities.size(); 57 | if(size<2) return null; 58 | else return mActivities.get(size-2); 59 | } 60 | } -------------------------------------------------------------------------------- /app/src/main/java/me/mrrobot97/designer/SwipeActivity/SwipeBackActivity.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer.SwipeActivity; 2 | 3 | import android.support.v7.app.AppCompatActivity; 4 | import android.view.MotionEvent; 5 | 6 | /** 7 | * Created by mrrobot on 16/10/21. 8 | */ 9 | 10 | public class SwipeBackActivity extends AppCompatActivity { 11 | private TouchHelper mTouchHelepr; 12 | 13 | @Override 14 | public boolean dispatchTouchEvent(MotionEvent ev) { 15 | if(mTouchHelepr==null) 16 | mTouchHelepr=new TouchHelper(getWindow()); 17 | boolean consume=mTouchHelepr.processTouchEvent(ev); 18 | if(!consume) return super.dispatchTouchEvent(ev); 19 | return false; 20 | //return super.dispatchTouchEvent(ev)||mTouchHelepr.processTouchEvent(ev); 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/me/mrrobot97/designer/SwipeActivity/TouchHelper.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer.SwipeActivity; 2 | 3 | import android.animation.Animator; 4 | import android.animation.ValueAnimator; 5 | import android.app.Activity; 6 | import android.content.Context; 7 | import android.graphics.Canvas; 8 | import android.graphics.drawable.Drawable; 9 | import android.graphics.drawable.GradientDrawable; 10 | import android.view.MotionEvent; 11 | import android.view.View; 12 | import android.view.ViewGroup; 13 | import android.view.Window; 14 | import android.view.animation.DecelerateInterpolator; 15 | import android.widget.FrameLayout; 16 | import me.mrrobot97.designer.MyApplication; 17 | 18 | /** 19 | * Created by mrrobot on 16/10/21. 20 | */ 21 | 22 | public class TouchHelper { 23 | //定义当前所处的状态 24 | private boolean isIdle=true; 25 | private boolean isSlinding=false; 26 | private boolean isAnimating=false; 27 | 28 | //左边触发的宽度 29 | private int triggerWidth=50; 30 | 31 | private int SHADOW_WIDTH=30; 32 | 33 | 34 | private Window mWindow; 35 | private ViewGroup preContentView; 36 | private ViewGroup curContentView; 37 | private ViewGroup curView; 38 | private ViewGroup preView; 39 | private Activity preActivity; 40 | 41 | private ShadowView mShadowView; 42 | 43 | 44 | 45 | public TouchHelper(Window window){ 46 | mWindow=window; 47 | } 48 | 49 | private Context getContext(){ 50 | return mWindow.getContext(); 51 | } 52 | 53 | 54 | //决定是否拦截事件 55 | public boolean processTouchEvent(MotionEvent event){ 56 | if(isAnimating) return true; 57 | float x=event.getRawX(); 58 | switch (event.getAction()){ 59 | case MotionEvent.ACTION_DOWN: 60 | if(x<=triggerWidth){ 61 | isIdle=false; 62 | isSlinding=true; 63 | startSlide(); 64 | return true; 65 | } 66 | break; 67 | case MotionEvent.ACTION_POINTER_DOWN: 68 | if(isSlinding) return true; 69 | break; 70 | case MotionEvent.ACTION_MOVE: 71 | if(isSlinding){ 72 | if(event.getActionIndex()!=0) return true; 73 | sliding(x); 74 | } 75 | break; 76 | case MotionEvent.ACTION_CANCEL: 77 | case MotionEvent.ACTION_UP: 78 | if(!isSlinding) return false; 79 | int width=getContext().getResources().getDisplayMetrics().widthPixels; 80 | isAnimating=true; 81 | isSlinding=false; 82 | startAnimating(width/x<=3,x); 83 | return true; 84 | default: 85 | break; 86 | } 87 | return false; 88 | } 89 | 90 | private void startAnimating(final boolean isFinishing, float x) { 91 | int width=getContext().getResources().getDisplayMetrics().widthPixels; 92 | ValueAnimator animator=ValueAnimator.ofFloat(x,isFinishing?width:0); 93 | animator.setInterpolator(new DecelerateInterpolator()); 94 | animator.setDuration(200); 95 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 96 | @Override 97 | public void onAnimationUpdate(ValueAnimator valueAnimator) { 98 | sliding((Float) valueAnimator.getAnimatedValue()); 99 | } 100 | }); 101 | animator.addListener(new Animator.AnimatorListener() { 102 | @Override 103 | public void onAnimationStart(Animator animator) { 104 | 105 | } 106 | 107 | @Override 108 | public void onAnimationEnd(Animator animator) { 109 | doEndWorks(isFinishing); 110 | } 111 | 112 | @Override 113 | public void onAnimationCancel(Animator animator) { 114 | 115 | } 116 | 117 | @Override 118 | public void onAnimationRepeat(Animator animator) { 119 | 120 | } 121 | }); 122 | animator.start(); 123 | } 124 | 125 | private void doEndWorks(boolean isFinishing) { 126 | if(preActivity==null) return; 127 | if(isFinishing){ 128 | //更改当前activity的底view为preView,防止当前activity finish时的白屏闪烁 129 | BackView view=new BackView(getContext()); 130 | view.cacheView(preView); 131 | curContentView.addView(view,0); 132 | } 133 | curContentView.removeView(mShadowView); 134 | if(curContentView==null||preContentView==null) return; 135 | curContentView.removeView(preView); 136 | preContentView.addView(preView); 137 | if(isFinishing){ 138 | ((Activity)getContext()).finish(); 139 | ((Activity)getContext()).overridePendingTransition(0,0); 140 | } 141 | isAnimating=false; 142 | isSlinding=false; 143 | isIdle=true; 144 | preView=null; 145 | curView=null; 146 | } 147 | 148 | private void sliding(float rawX) { 149 | if(preActivity==null) return; 150 | curView.setX(rawX); 151 | preView.setX(-preView.getWidth()/3+rawX/3); 152 | mShadowView.setX(-SHADOW_WIDTH+rawX); 153 | } 154 | 155 | private void startSlide() { 156 | preActivity=((MyApplication)getContext().getApplicationContext()).getHelper().getPreActivity(); 157 | if(preActivity==null) return; 158 | preContentView=(ViewGroup) preActivity.getWindow().findViewById(Window.ID_ANDROID_CONTENT); 159 | preView= (ViewGroup) preContentView.getChildAt(0); 160 | preContentView.removeView(preView); 161 | curContentView=(ViewGroup) mWindow.findViewById(Window.ID_ANDROID_CONTENT); 162 | curView= (ViewGroup) curContentView.getChildAt(0); 163 | preView.setX(-preView.getWidth()/3); 164 | //经过实际验证,addView中的index越大,则显示的越靠上,即越靠后绘制 165 | curContentView.addView(preView,0); 166 | if(mShadowView==null){ 167 | mShadowView=new ShadowView(getContext()); 168 | } 169 | FrameLayout.LayoutParams params=new FrameLayout.LayoutParams(SHADOW_WIDTH, FrameLayout.LayoutParams.MATCH_PARENT); 170 | curContentView.addView(mShadowView,1,params); 171 | mShadowView.setX(-SHADOW_WIDTH); 172 | } 173 | } 174 | 175 | //用于防止白屏闪烁 176 | class BackView extends View { 177 | 178 | private View mView; 179 | public BackView(Context context) { 180 | super(context); 181 | } 182 | 183 | public void cacheView(View view){ 184 | mView=view; 185 | invalidate(); 186 | } 187 | 188 | @Override 189 | protected void onDraw(Canvas canvas) { 190 | super.onDraw(canvas); 191 | if(mView!=null){ 192 | mView.draw(canvas); 193 | mView=null; 194 | } 195 | } 196 | } 197 | 198 | class ShadowView extends View{ 199 | 200 | private Drawable mDrawable; 201 | 202 | public ShadowView(Context context) { 203 | super(context); 204 | int[] colors=new int[]{0x00000000, 0x17000000, 0x43000000}; 205 | mDrawable=new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT,colors); 206 | } 207 | 208 | @Override 209 | protected void onDraw(Canvas canvas) { 210 | super.onDraw(canvas); 211 | mDrawable.setBounds(0,0,getMeasuredWidth(),getMeasuredHeight()); 212 | mDrawable.draw(canvas); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /app/src/main/java/me/mrrobot97/designer/Utils/BitmapUtils.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer.Utils; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.os.Build; 6 | import android.renderscript.Allocation; 7 | import android.renderscript.Element; 8 | import android.renderscript.RenderScript; 9 | import android.renderscript.ScriptIntrinsicBlur; 10 | import android.support.annotation.RequiresApi; 11 | import android.util.Log; 12 | 13 | /** 14 | * Created by mrrobot on 16/11/5. 15 | */ 16 | 17 | public class BitmapUtils { 18 | private static final float BITMAP_SCALE = 0.4f; 19 | private static final float BLUR_RADIUS = 7.5f; 20 | 21 | //这种方法对.jpg格式图片支持有问题 22 | @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1) 23 | public static Bitmap blur(Context context, Bitmap image) { 24 | int width = Math.round(image.getWidth() * BITMAP_SCALE); 25 | int height = Math.round(image.getHeight() * BITMAP_SCALE); 26 | 27 | Bitmap inputBitmap = Bitmap.createScaledBitmap(image, width, height, false); 28 | Bitmap outputBitmap = Bitmap.createBitmap(inputBitmap); 29 | 30 | RenderScript rs = RenderScript.create(context); 31 | ScriptIntrinsicBlur theIntrinsic = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)); 32 | Allocation tmpIn = Allocation.createFromBitmap(rs, inputBitmap); 33 | Allocation tmpOut = Allocation.createFromBitmap(rs, outputBitmap); 34 | theIntrinsic.setRadius(BLUR_RADIUS); 35 | theIntrinsic.setInput(tmpIn); 36 | theIntrinsic.forEach(tmpOut); 37 | tmpOut.copyTo(outputBitmap); 38 | 39 | return outputBitmap; 40 | } 41 | 42 | public static Bitmap fastblur(Bitmap sentBitmap, float scale, int radius) { 43 | 44 | int width = Math.round(sentBitmap.getWidth() * scale); 45 | int height = Math.round(sentBitmap.getHeight() * scale); 46 | sentBitmap = Bitmap.createScaledBitmap(sentBitmap, width, height, false); 47 | 48 | Bitmap bitmap = sentBitmap.copy(sentBitmap.getConfig(), true); 49 | 50 | if (radius < 1) { 51 | return (null); 52 | } 53 | 54 | int w = bitmap.getWidth(); 55 | int h = bitmap.getHeight(); 56 | 57 | int[] pix = new int[w * h]; 58 | Log.e("pix", w + " " + h + " " + pix.length); 59 | bitmap.getPixels(pix, 0, w, 0, 0, w, h); 60 | 61 | int wm = w - 1; 62 | int hm = h - 1; 63 | int wh = w * h; 64 | int div = radius + radius + 1; 65 | 66 | int r[] = new int[wh]; 67 | int g[] = new int[wh]; 68 | int b[] = new int[wh]; 69 | int rsum, gsum, bsum, x, y, i, p, yp, yi, yw; 70 | int vmin[] = new int[Math.max(w, h)]; 71 | 72 | int divsum = (div + 1) >> 1; 73 | divsum *= divsum; 74 | int dv[] = new int[256 * divsum]; 75 | for (i = 0; i < 256 * divsum; i++) { 76 | dv[i] = (i / divsum); 77 | } 78 | 79 | yw = yi = 0; 80 | 81 | int[][] stack = new int[div][3]; 82 | int stackpointer; 83 | int stackstart; 84 | int[] sir; 85 | int rbs; 86 | int r1 = radius + 1; 87 | int routsum, goutsum, boutsum; 88 | int rinsum, ginsum, binsum; 89 | 90 | for (y = 0; y < h; y++) { 91 | rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; 92 | for (i = -radius; i <= radius; i++) { 93 | p = pix[yi + Math.min(wm, Math.max(i, 0))]; 94 | sir = stack[i + radius]; 95 | sir[0] = (p & 0xff0000) >> 16; 96 | sir[1] = (p & 0x00ff00) >> 8; 97 | sir[2] = (p & 0x0000ff); 98 | rbs = r1 - Math.abs(i); 99 | rsum += sir[0] * rbs; 100 | gsum += sir[1] * rbs; 101 | bsum += sir[2] * rbs; 102 | if (i > 0) { 103 | rinsum += sir[0]; 104 | ginsum += sir[1]; 105 | binsum += sir[2]; 106 | } else { 107 | routsum += sir[0]; 108 | goutsum += sir[1]; 109 | boutsum += sir[2]; 110 | } 111 | } 112 | stackpointer = radius; 113 | 114 | for (x = 0; x < w; x++) { 115 | 116 | r[yi] = dv[rsum]; 117 | g[yi] = dv[gsum]; 118 | b[yi] = dv[bsum]; 119 | 120 | rsum -= routsum; 121 | gsum -= goutsum; 122 | bsum -= boutsum; 123 | 124 | stackstart = stackpointer - radius + div; 125 | sir = stack[stackstart % div]; 126 | 127 | routsum -= sir[0]; 128 | goutsum -= sir[1]; 129 | boutsum -= sir[2]; 130 | 131 | if (y == 0) { 132 | vmin[x] = Math.min(x + radius + 1, wm); 133 | } 134 | p = pix[yw + vmin[x]]; 135 | 136 | sir[0] = (p & 0xff0000) >> 16; 137 | sir[1] = (p & 0x00ff00) >> 8; 138 | sir[2] = (p & 0x0000ff); 139 | 140 | rinsum += sir[0]; 141 | ginsum += sir[1]; 142 | binsum += sir[2]; 143 | 144 | rsum += rinsum; 145 | gsum += ginsum; 146 | bsum += binsum; 147 | 148 | stackpointer = (stackpointer + 1) % div; 149 | sir = stack[(stackpointer) % div]; 150 | 151 | routsum += sir[0]; 152 | goutsum += sir[1]; 153 | boutsum += sir[2]; 154 | 155 | rinsum -= sir[0]; 156 | ginsum -= sir[1]; 157 | binsum -= sir[2]; 158 | 159 | yi++; 160 | } 161 | yw += w; 162 | } 163 | for (x = 0; x < w; x++) { 164 | rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; 165 | yp = -radius * w; 166 | for (i = -radius; i <= radius; i++) { 167 | yi = Math.max(0, yp) + x; 168 | 169 | sir = stack[i + radius]; 170 | 171 | sir[0] = r[yi]; 172 | sir[1] = g[yi]; 173 | sir[2] = b[yi]; 174 | 175 | rbs = r1 - Math.abs(i); 176 | 177 | rsum += r[yi] * rbs; 178 | gsum += g[yi] * rbs; 179 | bsum += b[yi] * rbs; 180 | 181 | if (i > 0) { 182 | rinsum += sir[0]; 183 | ginsum += sir[1]; 184 | binsum += sir[2]; 185 | } else { 186 | routsum += sir[0]; 187 | goutsum += sir[1]; 188 | boutsum += sir[2]; 189 | } 190 | 191 | if (i < hm) { 192 | yp += w; 193 | } 194 | } 195 | yi = x; 196 | stackpointer = radius; 197 | for (y = 0; y < h; y++) { 198 | // Preserve alpha channel: ( 0xff000000 & pix[yi] ) 199 | pix[yi] = ( 0xff000000 & pix[yi] ) | ( dv[rsum] << 16 ) | ( dv[gsum] << 8 ) | dv[bsum]; 200 | 201 | rsum -= routsum; 202 | gsum -= goutsum; 203 | bsum -= boutsum; 204 | 205 | stackstart = stackpointer - radius + div; 206 | sir = stack[stackstart % div]; 207 | 208 | routsum -= sir[0]; 209 | goutsum -= sir[1]; 210 | boutsum -= sir[2]; 211 | 212 | if (x == 0) { 213 | vmin[y] = Math.min(y + r1, hm) * w; 214 | } 215 | p = x + vmin[y]; 216 | 217 | sir[0] = r[p]; 218 | sir[1] = g[p]; 219 | sir[2] = b[p]; 220 | 221 | rinsum += sir[0]; 222 | ginsum += sir[1]; 223 | binsum += sir[2]; 224 | 225 | rsum += rinsum; 226 | gsum += ginsum; 227 | bsum += binsum; 228 | 229 | stackpointer = (stackpointer + 1) % div; 230 | sir = stack[stackpointer]; 231 | 232 | routsum += sir[0]; 233 | goutsum += sir[1]; 234 | boutsum += sir[2]; 235 | 236 | rinsum -= sir[0]; 237 | ginsum -= sir[1]; 238 | binsum -= sir[2]; 239 | 240 | yi += w; 241 | } 242 | } 243 | 244 | Log.e("pix", w + " " + h + " " + pix.length); 245 | bitmap.setPixels(pix, 0, w, 0, 0, w, h); 246 | 247 | return (bitmap); 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /app/src/main/java/me/mrrobot97/designer/Utils/FileUtils.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer.Utils; 2 | 3 | import android.content.Context; 4 | import android.os.Environment; 5 | import android.widget.Toast; 6 | import java.io.BufferedOutputStream; 7 | import java.io.File; 8 | import java.io.FileOutputStream; 9 | import java.io.IOException; 10 | import me.mrrobot97.designer.retrofit.ApiClient; 11 | import okhttp3.OkHttpClient; 12 | import okhttp3.Request; 13 | import okhttp3.Response; 14 | import rx.Observable; 15 | import rx.Observer; 16 | import rx.android.schedulers.AndroidSchedulers; 17 | import rx.functions.Func0; 18 | import rx.schedulers.Schedulers; 19 | 20 | /** 21 | * Created by mrrobot on 16/11/4. 22 | */ 23 | 24 | public class FileUtils { 25 | public static void deleteFileOrDir(String path){ 26 | File file=new File(path); 27 | if(file.exists()){ 28 | if(file.isFile()){ 29 | file.delete(); 30 | }else{ 31 | for (File f:file.listFiles()){ 32 | if(f.isFile()){ 33 | f.delete(); 34 | }else{ 35 | deleteFileOrDir(f.getAbsolutePath()); 36 | } 37 | } 38 | } 39 | } 40 | } 41 | 42 | public static boolean isExternalStorageWritable() { 43 | if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { 44 | return true; 45 | } 46 | return false; 47 | } 48 | 49 | public static void saveImage(Context context,String url) { 50 | OkHttpClient client= ApiClient.getClient(); 51 | if (!isExternalStorageWritable()) { 52 | Toast.makeText(context, "SD card not available", Toast.LENGTH_SHORT).show(); 53 | return; 54 | } 55 | File dir = new File(Environment.getExternalStorageDirectory(), "Designer"); 56 | if (!dir.exists()) dir.mkdir(); 57 | final File file = new File(dir, 58 | url.substring(url.lastIndexOf(File.separator), url.length())); 59 | Observable.defer(new Func0>() { 60 | @Override public Observable call() { 61 | Request request = new Request.Builder().url(url).build(); 62 | Response response = null; 63 | BufferedOutputStream bos = null; 64 | try { 65 | response = client.newCall(request).execute(); 66 | bos = new BufferedOutputStream(new FileOutputStream(file)); 67 | byte[] buffer = response.body().bytes(); 68 | bos.write(buffer, 0, buffer.length); 69 | } catch (IOException e) { 70 | e.printStackTrace(); 71 | } finally { 72 | if (bos != null) { 73 | try { 74 | bos.close(); 75 | } catch (IOException e) { 76 | e.printStackTrace(); 77 | } 78 | } 79 | } 80 | return Observable.just(null); 81 | } 82 | }) 83 | .subscribeOn(Schedulers.io()) 84 | .observeOn(AndroidSchedulers.mainThread()) 85 | .subscribe(new Observer() { 86 | 87 | @Override public void onCompleted() { 88 | 89 | } 90 | 91 | @Override public void onError(Throwable e) { 92 | Toast.makeText(context, "Error", Toast.LENGTH_SHORT).show(); 93 | } 94 | 95 | @Override public void onNext(Void aVoid) { 96 | Toast.makeText(context, "Save image to " + file.getAbsolutePath(), 97 | Toast.LENGTH_SHORT).show(); 98 | } 99 | }); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /app/src/main/java/me/mrrobot97/designer/Utils/NetUtils.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer.Utils; 2 | 3 | import android.content.Context; 4 | import android.net.ConnectivityManager; 5 | import android.net.NetworkInfo; 6 | 7 | /** 8 | * Created by mrrobot on 16/10/26. 9 | */ 10 | 11 | public class NetUtils { 12 | public static boolean isNetworkOnline(Context context) { 13 | boolean status=false; 14 | try{ 15 | ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 16 | NetworkInfo netInfo = cm.getNetworkInfo(0); 17 | if (netInfo != null && netInfo.getState()==NetworkInfo.State.CONNECTED) { 18 | status= true; 19 | }else { 20 | netInfo = cm.getNetworkInfo(1); 21 | if(netInfo!=null && netInfo.getState()==NetworkInfo.State.CONNECTED) 22 | status= true; 23 | } 24 | }catch(Exception e){ 25 | e.printStackTrace(); 26 | return false; 27 | } 28 | return status; 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/me/mrrobot97/designer/Utils/ScreenUtils.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer.Utils; 2 | 3 | /** 4 | * Created by mrrobot on 16/10/21. 5 | */ 6 | 7 | import android.app.Activity; 8 | import android.content.Context; 9 | import android.graphics.Bitmap; 10 | import android.util.DisplayMetrics; 11 | import android.util.TypedValue; 12 | import android.view.View; 13 | 14 | /** 15 | * Created by mrrobot on 16/9/13. 16 | */ 17 | public class ScreenUtils { 18 | 19 | public static int dp2px(Context context, int dpVal){ 20 | return (int) (dpVal*context.getResources().getDisplayMetrics().density); 21 | } 22 | 23 | public static int px2dp(Context context,int pxVal){ 24 | return (int) (pxVal/context.getResources().getDisplayMetrics().density); 25 | } 26 | 27 | public static int sp2px(Context cOntext,int spVal){ 28 | return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP 29 | ,spVal,cOntext.getResources().getDisplayMetrics()); 30 | } 31 | 32 | public static int px2sp(Context context,int pxVal){ 33 | return (int) (pxVal/context.getResources().getDisplayMetrics().scaledDensity); 34 | } 35 | 36 | //return the size of screen in pixel unit 37 | public static int[] getScreenWidthAndHeight(Context context){ 38 | int []wh=new int[2]; 39 | DisplayMetrics metrics=context.getResources().getDisplayMetrics(); 40 | wh[0]=metrics.widthPixels; 41 | wh[1]=metrics.heightPixels; 42 | return wh; 43 | } 44 | 45 | //return the height of statusBar 46 | public static int getStatusBarHeight(Context context){ 47 | int statusBarHeight=0; 48 | int resourceId=context.getResources().getIdentifier("status_bar_height","dimen","android"); 49 | if (resourceId>0){ 50 | statusBarHeight=context.getResources().getDimensionPixelSize(resourceId); 51 | } 52 | return statusBarHeight; 53 | } 54 | 55 | //return a screen capture bitmap 56 | public static Bitmap getScreenCapture(Activity activity){ 57 | View view=activity.getWindow().getDecorView(); 58 | view.setDrawingCacheEnabled(true); 59 | view.buildDrawingCache(); 60 | Bitmap bitmap=view.getDrawingCache(); 61 | int []wh=getScreenWidthAndHeight(activity); 62 | Bitmap scaled=Bitmap.createBitmap(bitmap,0,0,wh[0],wh[1]); 63 | view.setDrawingCacheEnabled(false); 64 | view.destroyDrawingCache(); 65 | return scaled; 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /app/src/main/java/me/mrrobot97/designer/Utils/SharedPreferencesUtils.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer.Utils; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import java.util.Set; 6 | 7 | /** 8 | * Created by mrrobot on 16/11/1. 9 | */ 10 | 11 | public class SharedPreferencesUtils { 12 | public static String SHARED_FILE_NAME="spfs"; 13 | 14 | 15 | //sharedpreferences util 16 | public static void putToSpfs(Context context, String key, Object value){ 17 | SharedPreferences.Editor editor=context.getSharedPreferences(SHARED_FILE_NAME,Context.MODE_PRIVATE).edit(); 18 | if (value instanceof Integer){ 19 | editor.putInt(key, (Integer) value); 20 | }else if(value instanceof String){ 21 | editor.putString(key, (String) value); 22 | }else if(value instanceof Boolean){ 23 | editor.putBoolean(key, (Boolean) value); 24 | }else if(value instanceof Long){ 25 | editor.putLong(key, (Long) value); 26 | }else if(value instanceof Float){ 27 | editor.putFloat(key, (Float) value); 28 | }else if(value instanceof Set){ 29 | editor.putStringSet(key, (Set) value); 30 | } 31 | editor.apply(); 32 | } 33 | 34 | public static Object getFromSpfs(Context context,String key,Object defVal){ 35 | SharedPreferences spfs=context.getSharedPreferences(SHARED_FILE_NAME,Context.MODE_PRIVATE); 36 | if (defVal instanceof Integer){ 37 | return spfs.getInt(key, (Integer) defVal); 38 | }else if(defVal instanceof String){ 39 | return spfs.getString(key, (String) defVal); 40 | }else if(defVal instanceof Boolean){ 41 | return spfs.getBoolean(key, (Boolean) defVal); 42 | }else if(defVal instanceof Long){ 43 | return spfs.getLong(key, (Long) defVal); 44 | }else if(defVal instanceof Float){ 45 | return spfs.getFloat(key, (Float) defVal); 46 | }else if(defVal instanceof Set){ 47 | return spfs.getStringSet(key, (Set) defVal); 48 | } 49 | return null; 50 | } 51 | 52 | public static void removeInSpfs(Context context,String key){ 53 | SharedPreferences.Editor editor=context.getSharedPreferences(SHARED_FILE_NAME,Context.MODE_PRIVATE).edit(); 54 | editor.remove(key); 55 | editor.apply(); 56 | } 57 | 58 | public static void clearSpfs(Context context){ 59 | SharedPreferences.Editor editor=context.getSharedPreferences(SHARED_FILE_NAME,Context.MODE_PRIVATE).edit(); 60 | editor.clear(); 61 | editor.apply(); 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /app/src/main/java/me/mrrobot97/designer/Utils/StringUtils.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer.Utils; 2 | 3 | /** 4 | * Created by mrrobot on 16/11/5. 5 | */ 6 | 7 | public class StringUtils { 8 | public static boolean checkAvailable(String string){ 9 | if(string==null||string.trim().length()==0) return false; 10 | return true; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/me/mrrobot97/designer/adapter/AttachmentsAdapter.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer.adapter; 2 | 3 | import android.content.Context; 4 | import android.graphics.Rect; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.ImageView; 10 | import butterknife.BindView; 11 | import butterknife.ButterKnife; 12 | import com.bumptech.glide.Glide; 13 | import java.util.List; 14 | import me.mrrobot97.designer.R; 15 | import me.mrrobot97.designer.Utils.ScreenUtils; 16 | import me.mrrobot97.designer.model.Attachment; 17 | 18 | /** 19 | * Created by mrrobot on 16/10/25. 20 | */ 21 | 22 | public class AttachmentsAdapter extends RecyclerView.Adapter { 23 | public void setData(List data) { 24 | mData = data; 25 | } 26 | 27 | public static final int ANIM_DURATION = 500; 28 | private List mData; 29 | private int screenWidth; 30 | private Context mContext; 31 | private static final int offset = 8; 32 | 33 | public AttachmentsAdapter(List data, Context context) { 34 | mData = data; 35 | mContext = context; 36 | screenWidth = ScreenUtils.getScreenWidthAndHeight(mContext)[0]; 37 | } 38 | 39 | private OnItemClickListener mListener; 40 | 41 | public void setListener(OnItemClickListener listener) { 42 | mListener = listener; 43 | } 44 | 45 | private void runAnim(View view) { 46 | view.setTranslationX(screenWidth); 47 | view.animate().translationX(0).setDuration(ANIM_DURATION).setStartDelay(300).start(); 48 | } 49 | 50 | @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 51 | View view = LayoutInflater.from(mContext).inflate(R.layout.recyclerview_item_layout, null); 52 | //设置宽度 53 | int width = (screenWidth - offset * 4) / 3; 54 | int height = width * 3 / 4; 55 | ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(width, height); 56 | view.setLayoutParams(params); 57 | MyHolder holder = new MyHolder(view); 58 | runAnim(view); 59 | return holder; 60 | } 61 | 62 | @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) { 63 | Attachment attachment = mData.get(position); 64 | Glide.with(mContext).load(attachment.getThumbnail_url()).into(((MyHolder) holder).mImageView); 65 | ((MyHolder) holder).mImageView.setOnClickListener(view -> { 66 | if (mListener != null) { 67 | mListener.onItemClicked(position); 68 | } 69 | }); 70 | } 71 | 72 | @Override public int getItemCount() { 73 | return mData.size(); 74 | } 75 | 76 | public static class MyHolder extends RecyclerView.ViewHolder { 77 | public ImageView getImageView() { 78 | return mImageView; 79 | } 80 | 81 | @BindView(R.id.image_view) ImageView mImageView; 82 | 83 | public MyHolder(View itemView) { 84 | super(itemView); 85 | ButterKnife.bind(this, itemView); 86 | } 87 | } 88 | 89 | public static class MyItemDecoration extends RecyclerView.ItemDecoration { 90 | @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, 91 | RecyclerView.State state) { 92 | super.getItemOffsets(outRect, view, parent, state); 93 | outRect.set(offset, offset, offset, offset); 94 | } 95 | } 96 | 97 | public interface OnItemClickListener { 98 | void onItemClicked(int position); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /app/src/main/java/me/mrrobot97/designer/adapter/ShotsAdapter.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer.adapter; 2 | 3 | import android.content.Context; 4 | import android.graphics.Rect; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.ImageView; 10 | 11 | import com.bumptech.glide.Glide; 12 | 13 | import java.util.List; 14 | 15 | import butterknife.BindView; 16 | import butterknife.ButterKnife; 17 | import me.mrrobot97.designer.R; 18 | import me.mrrobot97.designer.Utils.ScreenUtils; 19 | import me.mrrobot97.designer.model.Shot; 20 | 21 | /** 22 | * Created by mrrobot on 16/10/24. 23 | */ 24 | public class ShotsAdapter extends RecyclerView.Adapter { 25 | 26 | public void setData(List data) { 27 | mData = data; 28 | } 29 | 30 | private List mData; 31 | private int screenWidth; 32 | private int screenHeight; 33 | private Context mContext; 34 | private static final int offset = 4; 35 | public static final int ANIM_DURATION = 300; 36 | 37 | 38 | public ShotsAdapter(List data, Context context) { 39 | mData = data; 40 | mContext = context; 41 | screenWidth = ScreenUtils.getScreenWidthAndHeight(mContext)[0]; 42 | screenHeight = ScreenUtils.getScreenWidthAndHeight(mContext)[1]; 43 | } 44 | 45 | private OnItemClickListener mListener; 46 | 47 | public void setListener(OnItemClickListener listener) { 48 | mListener = listener; 49 | } 50 | 51 | private void runAnimation(View view) { 52 | view.setScaleX(0.69f); 53 | view.setScaleY(0.69f); 54 | view.animate() 55 | .scaleX(1f) 56 | .scaleY(1f) 57 | .setDuration(ANIM_DURATION) 58 | .start(); 59 | } 60 | 61 | @Override 62 | public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 63 | View view = LayoutInflater.from(mContext).inflate(R.layout.recyclerview_item_layout, null); 64 | //设置宽度 65 | int width = (screenWidth - offset * 4) / 3; 66 | int height = width * 3 / 4; 67 | ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(width, height); 68 | view.setLayoutParams(params); 69 | MyHolder holder = new MyHolder(view); 70 | return holder; 71 | } 72 | 73 | @Override 74 | public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) { 75 | runAnimation(holder.itemView); 76 | Shot shot = mData.get(position); 77 | //根据实际屏幕分辨率确定要加载的缩略图的尺寸 78 | String url = null; 79 | if (screenWidth >= 720) { 80 | url = shot.getImages().getNormal(); 81 | if (!checkAvailable(url)) url = shot.getImages().getTeaser(); 82 | } else { 83 | url = shot.getImages().getTeaser(); 84 | } 85 | Glide.with(mContext) 86 | .load(url) 87 | .crossFade() 88 | .into(((MyHolder) holder).mImageView); 89 | ((MyHolder) holder).mImageView.setOnClickListener(view -> { 90 | if (mListener != null) { 91 | mListener.OnItemClicked(position); 92 | } 93 | }); 94 | } 95 | 96 | private boolean checkAvailable(String string) { 97 | if (string == null || string.trim().length() == 0) return false; 98 | return true; 99 | } 100 | 101 | @Override 102 | public int getItemCount() { 103 | return mData.size(); 104 | } 105 | 106 | public static class MyHolder extends RecyclerView.ViewHolder { 107 | public ImageView getImageView() { 108 | return mImageView; 109 | } 110 | 111 | @BindView(R.id.image_view) 112 | ImageView mImageView; 113 | 114 | public MyHolder(View itemView) { 115 | super(itemView); 116 | ButterKnife.bind(this, itemView); 117 | } 118 | } 119 | 120 | public static class MyItemDecoration extends RecyclerView.ItemDecoration { 121 | @Override 122 | public void getItemOffsets(Rect outRect, View view, RecyclerView parent, 123 | RecyclerView.State state) { 124 | super.getItemOffsets(outRect, view, parent, state); 125 | outRect.set(offset, offset, offset, offset); 126 | } 127 | } 128 | 129 | public interface OnItemClickListener { 130 | void OnItemClicked(int position); 131 | } 132 | } 133 | 134 | 135 | -------------------------------------------------------------------------------- /app/src/main/java/me/mrrobot97/designer/contracts/BrowseContract.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer.contracts; 2 | 3 | import java.util.List; 4 | import me.mrrobot97.designer.model.Shot; 5 | 6 | /** 7 | * Created by mrrobot on 16/11/7. 8 | */ 9 | 10 | public interface BrowseContract { 11 | public interface IBrowseView { 12 | void loadShots(int position,List shots,boolean success); 13 | void refreshShots(int position,List shots,boolean success); 14 | void loadMore(int position,List shots,boolean success); 15 | void requestPermissions(); 16 | void showUserProfile(); 17 | } 18 | 19 | public interface IBrowsePresenter { 20 | void load(int position); 21 | void refresh(int position); 22 | void loadMore(int position); 23 | } 24 | 25 | 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/me/mrrobot97/designer/contracts/DetailContract.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer.contracts; 2 | 3 | import java.util.List; 4 | import me.mrrobot97.designer.model.Attachment; 5 | import me.mrrobot97.designer.model.Comment; 6 | 7 | /** 8 | * Created by mrrobot on 16/11/7. 9 | */ 10 | 11 | public interface DetailContract { 12 | interface IDetailView { 13 | void showComments(List commments); 14 | void showAttachments(List attachments); 15 | void showIfCommentSuccess(Comment comment,boolean success); 16 | } 17 | 18 | interface IDetailPresenter { 19 | void loadComments(String shotId); 20 | 21 | void loadAttachments(String id); 22 | 23 | void postComment(String id, String comment); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/me/mrrobot97/designer/contracts/PlayerContract.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer.contracts; 2 | 3 | import java.util.List; 4 | import me.mrrobot97.designer.model.Shot; 5 | import me.mrrobot97.designer.model.User; 6 | 7 | /** 8 | * Created by mrrobot on 16/11/7. 9 | */ 10 | 11 | public interface PlayerContract { 12 | interface IPlayerView { 13 | void showPlayerInfo(); 14 | void showPlayerInfoAsync(User user); 15 | void showShots(List shots); 16 | } 17 | 18 | interface IPlayerPresenter { 19 | void loadUserShots(String userId); 20 | void loadUserProfile(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/me/mrrobot97/designer/customViews/CircleImageView.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer.customViews; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Bitmap; 6 | import android.graphics.BitmapFactory; 7 | import android.graphics.BitmapShader; 8 | import android.graphics.Canvas; 9 | import android.graphics.Matrix; 10 | import android.graphics.Paint; 11 | import android.graphics.Shader; 12 | import android.util.AttributeSet; 13 | import android.view.View; 14 | import me.mrrobot97.designer.R; 15 | import me.mrrobot97.designer.Utils.ScreenUtils; 16 | 17 | 18 | /** 19 | * Created by mrrobot on 16/10/21. 20 | */ 21 | 22 | public class CircleImageView extends View { 23 | 24 | private Paint mPaint; 25 | private Bitmap bitmap; 26 | private int resId=R.drawable.ic_account; 27 | private int width; 28 | private int height; 29 | private int radius; 30 | private BitmapShader mBitmapShader; 31 | private Matrix matrix; 32 | private int bitmapWidth; 33 | private int bitmapHeigh; 34 | private float scaleSize=1f; 35 | 36 | public CircleImageView(Context context) { 37 | this(context,null); 38 | } 39 | 40 | public CircleImageView(Context context, AttributeSet attrs) { 41 | this(context, attrs,0); 42 | } 43 | 44 | public CircleImageView(Context context, AttributeSet attrs, int defStyleAttr) { 45 | super(context, attrs, defStyleAttr); 46 | TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.CircleImageView); 47 | resId=ta.getResourceId(R.styleable.CircleImageView_src,resId); 48 | bitmap= BitmapFactory.decodeResource(getResources(),resId); 49 | ta.recycle(); 50 | init(); 51 | } 52 | 53 | @Override 54 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 55 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 56 | int widthMode=MeasureSpec.getMode(widthMeasureSpec); 57 | width=MeasureSpec.getSize(widthMeasureSpec); 58 | int heightMode=MeasureSpec.getMode(heightMeasureSpec); 59 | height=MeasureSpec.getSize(heightMeasureSpec); 60 | if(widthMode==MeasureSpec.AT_MOST){ 61 | width= ScreenUtils.dp2px(getContext(),50); 62 | } 63 | if(heightMode==MeasureSpec.AT_MOST){ 64 | height=ScreenUtils.dp2px(getContext(),50); 65 | } 66 | setMeasuredDimension(width,height); 67 | radius=Math.min(width,height)/2; 68 | setBitmap(bitmap); 69 | } 70 | 71 | private void init() { 72 | mPaint=new Paint(); 73 | mPaint.setAntiAlias(true); 74 | matrix=new Matrix(); 75 | } 76 | 77 | @Override 78 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 79 | super.onSizeChanged(w, h, oldw, oldh); 80 | width=w; 81 | height=h; 82 | radius=Math.min(width,height)/2; 83 | setBitmap(bitmap); 84 | } 85 | 86 | public void setBitmap(Bitmap bitmap){ 87 | matrix.reset(); 88 | this.bitmap=bitmap; 89 | mBitmapShader=new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT); 90 | bitmapWidth=bitmap.getWidth(); 91 | bitmapHeigh=bitmap.getHeight(); 92 | if(bitmapWidth>width&&bitmapHeigh>height){ 93 | scaleSize=Math.min(height*1f/bitmapHeigh,width*1f/bitmapWidth); 94 | }else if(bitmapWidth shots, boolean success); 28 | } 29 | 30 | interface ShotListener { 31 | void onShotLoaded(Shot shot); 32 | } 33 | 34 | interface UserListener { 35 | void onUserLoaded(User user); 36 | } 37 | 38 | interface CommentsListener { 39 | void onCommentsLoader(List comments); 40 | } 41 | 42 | interface UserShotsLoadListener { 43 | void onUserShotsLoaded(List shots); 44 | } 45 | 46 | interface AttachmentsLoadListener { 47 | void onAttachmentsLoaded(List attachments); 48 | } 49 | 50 | interface CommentPostListener { 51 | void onCommentPosted(Comment comment,boolean success); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/me/mrrobot97/designer/model/Images.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer.model; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | import java.io.Serializable; 6 | 7 | /** 8 | * Created by mrrobot on 16/10/21. 9 | */ 10 | 11 | public class Images implements Serializable{ 12 | @SerializedName("hidpi") 13 | private String hidpi; 14 | @SerializedName("normal") 15 | private String normal; 16 | @SerializedName("teaser") 17 | private String teaser; 18 | 19 | public Images(String hidpi, String normal, String teaser) { 20 | this.hidpi = hidpi; 21 | this.normal = normal; 22 | this.teaser = teaser; 23 | } 24 | 25 | public String getHidpi() { 26 | 27 | return hidpi; 28 | } 29 | 30 | public void setHidpi(String hidpi) { 31 | this.hidpi = hidpi; 32 | } 33 | 34 | public String getNormal() { 35 | return normal; 36 | } 37 | 38 | public void setNormal(String normal) { 39 | this.normal = normal; 40 | } 41 | 42 | public String getTeaser() { 43 | return teaser; 44 | } 45 | 46 | public void setTeaser(String teaser) { 47 | this.teaser = teaser; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/me/mrrobot97/designer/model/Links.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer.model; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | import java.io.Serializable; 6 | 7 | /** 8 | * Created by mrrobot on 16/10/24. 9 | */ 10 | 11 | public class Links implements Serializable{ 12 | @SerializedName("web") 13 | private String web; 14 | @SerializedName("twitter") 15 | private String twitter; 16 | 17 | public String getWeb() { 18 | return web; 19 | } 20 | 21 | public void setWeb(String web) { 22 | this.web = web; 23 | } 24 | 25 | public String getTwitter() { 26 | return twitter; 27 | } 28 | 29 | public void setTwitter(String twitter) { 30 | this.twitter = twitter; 31 | } 32 | 33 | public Links(String web, String twitter) { 34 | 35 | this.web = web; 36 | this.twitter = twitter; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/me/mrrobot97/designer/model/ModelImpl.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer.model; 2 | 3 | import android.util.Log; 4 | import java.util.List; 5 | import me.mrrobot97.designer.retrofit.ApiClient; 6 | import me.mrrobot97.designer.retrofit.DribbbleService; 7 | import rx.Subscriber; 8 | import rx.android.schedulers.AndroidSchedulers; 9 | import rx.schedulers.Schedulers; 10 | 11 | /** 12 | * Created by mrrobot on 16/10/21. 13 | */ 14 | 15 | public class ModelImpl implements IModel { 16 | private DribbbleService observable; 17 | 18 | public ModelImpl() { 19 | observable= ApiClient.getRetrofit().create(DribbbleService.class); 20 | } 21 | 22 | @Override 23 | public void loadShots(String sort, int page, int per_page, final ShotsListener listener) { 24 | if(sort.equals("debuts")){ 25 | observable.loadDebutsShots(sort,page,per_page) 26 | .subscribeOn(Schedulers.newThread()) 27 | .observeOn(AndroidSchedulers.mainThread()) 28 | .subscribe(new Subscriber>() { 29 | @Override 30 | public void onCompleted() { 31 | 32 | } 33 | 34 | @Override 35 | public void onError(Throwable e) { 36 | Log.d("yjw","onError"); 37 | listener.onShotsLoaded(null,false); 38 | } 39 | 40 | @Override 41 | public void onNext(List shots) { 42 | listener.onShotsLoaded(shots,true); 43 | } 44 | }); 45 | }else{ 46 | observable.loadShots(sort,page,per_page) 47 | .subscribeOn(Schedulers.newThread()) 48 | .observeOn(AndroidSchedulers.mainThread()) 49 | .subscribe(new Subscriber>() { 50 | @Override 51 | public void onCompleted() { 52 | 53 | } 54 | 55 | @Override 56 | public void onError(Throwable e) { 57 | Log.d("yjw","onError"); 58 | listener.onShotsLoaded(null,false); 59 | } 60 | 61 | @Override 62 | public void onNext(List shots) { 63 | listener.onShotsLoaded(shots,true); 64 | } 65 | }); 66 | } 67 | 68 | } 69 | 70 | @Override 71 | public void loadShotDetail(String id, final ShotListener listener) { 72 | observable.loadShot(id) 73 | .subscribeOn(Schedulers.newThread()) 74 | .observeOn(AndroidSchedulers.mainThread()) 75 | .subscribe(new Subscriber() { 76 | @Override 77 | public void onCompleted() { 78 | Log.d("yjw","onCompleted"); 79 | } 80 | 81 | @Override 82 | public void onError(Throwable e) { 83 | Log.d("yjw","onError"); 84 | } 85 | 86 | @Override 87 | public void onNext(Shot shot) { 88 | listener.onShotLoaded(shot); 89 | } 90 | }); 91 | 92 | } 93 | 94 | @Override 95 | public void loadUser(String id, final UserListener listener) { 96 | observable.loadUser(id) 97 | .subscribeOn(Schedulers.newThread()) 98 | .observeOn(AndroidSchedulers.mainThread()) 99 | .subscribe(new Subscriber() { 100 | @Override 101 | public void onCompleted() { 102 | Log.d("yjw","onCompleted"); 103 | } 104 | 105 | @Override 106 | public void onError(Throwable e) { 107 | Log.d("yjw","onError"); 108 | } 109 | 110 | @Override 111 | public void onNext(User user) { 112 | listener.onUserLoaded(user); 113 | } 114 | }); 115 | } 116 | 117 | @Override 118 | public void loadComments(String shotId, final CommentsListener listener) { 119 | observable.loadComments(shotId) 120 | .subscribeOn(Schedulers.newThread()) 121 | .observeOn(AndroidSchedulers.mainThread()) 122 | .subscribe(new Subscriber>() { 123 | @Override 124 | public void onCompleted() { 125 | 126 | } 127 | 128 | @Override 129 | public void onError(Throwable e) { 130 | Log.d("yjw","comments loading error"); 131 | } 132 | 133 | @Override 134 | public void onNext(List comments) { 135 | listener.onCommentsLoader(comments); 136 | } 137 | }); 138 | } 139 | 140 | @Override 141 | public void loadUserShots(String userId, final UserShotsLoadListener listener) { 142 | observable.loadUserShots(userId) 143 | .subscribeOn(Schedulers.newThread()) 144 | .observeOn(AndroidSchedulers.mainThread()) 145 | .subscribe(new Subscriber>() { 146 | @Override 147 | public void onCompleted() { 148 | 149 | } 150 | 151 | @Override 152 | public void onError(Throwable e) { 153 | Log.d("yjw","shots loading error"); 154 | } 155 | 156 | @Override 157 | public void onNext(List shots) { 158 | listener.onUserShotsLoaded(shots); 159 | } 160 | }); 161 | } 162 | 163 | @Override 164 | public void loadAttachments(String id, final AttachmentsLoadListener listener) { 165 | observable.loadAttachments(id) 166 | .subscribeOn(Schedulers.newThread()) 167 | .observeOn(AndroidSchedulers.mainThread()) 168 | .subscribe(new Subscriber>() { 169 | @Override 170 | public void onCompleted() { 171 | 172 | } 173 | 174 | @Override 175 | public void onError(Throwable e) { 176 | Log.d("yjw","shots loading error"); 177 | } 178 | 179 | @Override 180 | public void onNext(List attachments) { 181 | listener.onAttachmentsLoaded(attachments); 182 | } 183 | }); 184 | } 185 | 186 | @Override public void loadUserProfile( final UserListener listener) { 187 | observable.loadUserProfile() 188 | .subscribeOn(Schedulers.newThread()) 189 | .observeOn(AndroidSchedulers.mainThread()) 190 | .subscribe(new Subscriber() { 191 | @Override public void onCompleted() { 192 | 193 | } 194 | 195 | @Override public void onError(Throwable e) { 196 | Log.d("yjw","user profile load error"); 197 | } 198 | 199 | @Override public void onNext(User user) { 200 | listener.onUserLoaded(user); 201 | } 202 | }); 203 | } 204 | 205 | @Override 206 | public void postComment(String id, String comment, CommentPostListener listener) { 207 | observable.postComment(id,comment) 208 | .subscribeOn(Schedulers.newThread()) 209 | .observeOn(AndroidSchedulers.mainThread()) 210 | .subscribe(new Subscriber() { 211 | @Override public void onCompleted() { 212 | 213 | } 214 | 215 | @Override public void onError(Throwable e) { 216 | Log.d("yjw","comment failed"); 217 | listener.onCommentPosted(null,false); 218 | } 219 | 220 | @Override public void onNext(Comment comment) { 221 | listener.onCommentPosted(comment,true); 222 | } 223 | }); 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /app/src/main/java/me/mrrobot97/designer/model/Shot.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer.model; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | import java.io.Serializable; 6 | 7 | /** 8 | * Created by mrrobot on 16/10/21. 9 | */ 10 | 11 | public class Shot implements Serializable{ 12 | public Shot(String id, String title, String description, int width, String height, Images images, int comments_count 13 | , int views_count, int likes_count, int attachments_count, String created_at, String html_url, String attachments_url 14 | , String comments_url, boolean animated, String[] tags, User user) { 15 | this.id = id; 16 | this.title = title; 17 | this.description = description; 18 | this.width = width; 19 | this.height = height; 20 | this.images = images; 21 | this.comments_count = comments_count; 22 | this.views_count = views_count; 23 | this.likes_count = likes_count; 24 | this.attachments_count = attachments_count; 25 | this.created_at = created_at; 26 | this.html_url = html_url; 27 | this.attachments_url = attachments_url; 28 | this.comments_url = comments_url; 29 | this.animated = animated; 30 | this.tags = tags; 31 | this.user = user; 32 | } 33 | 34 | public String getId() { 35 | return id; 36 | } 37 | 38 | public void setId(String id) { 39 | this.id = id; 40 | } 41 | 42 | public String getTitle() { 43 | return title; 44 | } 45 | 46 | public void setTitle(String title) { 47 | this.title = title; 48 | } 49 | 50 | public String getDescription() { 51 | return description; 52 | } 53 | 54 | public void setDescription(String description) { 55 | this.description = description; 56 | } 57 | 58 | public int getWidth() { 59 | return width; 60 | } 61 | 62 | public void setWidth(int width) { 63 | this.width = width; 64 | } 65 | 66 | public String getHeight() { 67 | return height; 68 | } 69 | 70 | public void setHeight(String height) { 71 | this.height = height; 72 | } 73 | 74 | public Images getImages() { 75 | return images; 76 | } 77 | 78 | public void setImages(Images images) { 79 | this.images = images; 80 | } 81 | 82 | public int getViews_count() { 83 | return views_count; 84 | } 85 | 86 | public void setViews_count(int views_count) { 87 | this.views_count = views_count; 88 | } 89 | 90 | public int getLikes_count() { 91 | return likes_count; 92 | } 93 | 94 | public void setLikes_count(int likes_count) { 95 | this.likes_count = likes_count; 96 | } 97 | 98 | public int getAttachments_count() { 99 | return attachments_count; 100 | } 101 | 102 | public void setAttachments_count(int attachments_count) { 103 | this.attachments_count = attachments_count; 104 | } 105 | 106 | public String getCreated_at() { 107 | return created_at; 108 | } 109 | 110 | public void setCreated_at(String created_at) { 111 | this.created_at = created_at; 112 | } 113 | 114 | public String getHtml_url() { 115 | return html_url; 116 | } 117 | 118 | public void setHtml_url(String html_url) { 119 | this.html_url = html_url; 120 | } 121 | 122 | public String getAttachments_url() { 123 | return attachments_url; 124 | } 125 | 126 | public void setAttachments_url(String attachments_url) { 127 | this.attachments_url = attachments_url; 128 | } 129 | 130 | public String getComments_url() { 131 | return comments_url; 132 | } 133 | 134 | public void setComments_url(String comments_url) { 135 | this.comments_url = comments_url; 136 | } 137 | 138 | public boolean isAnimated() { 139 | return animated; 140 | } 141 | 142 | public void setAnimated(boolean animated) { 143 | this.animated = animated; 144 | } 145 | 146 | public String[] getTags() { 147 | return tags; 148 | } 149 | 150 | public void setTags(String[] tags) { 151 | this.tags = tags; 152 | } 153 | 154 | public User getUser() { 155 | return user; 156 | } 157 | 158 | public void setUser(User user) { 159 | this.user = user; 160 | } 161 | 162 | public int getComments_count() { 163 | return comments_count; 164 | } 165 | 166 | public void setComments_count(int comments_count) { 167 | this.comments_count = comments_count; 168 | } 169 | 170 | @SerializedName("id") 171 | private String id; 172 | @SerializedName("title") 173 | private String title; 174 | @SerializedName("description") 175 | private String description; 176 | @SerializedName("width") 177 | private int width; 178 | @SerializedName("height") 179 | private String height; 180 | @SerializedName("images") 181 | private Images images; 182 | @SerializedName("comments_count") 183 | private int comments_count; 184 | @SerializedName("views_count") 185 | private int views_count; 186 | @SerializedName("likes_count") 187 | private int likes_count; 188 | @SerializedName("attachments_count") 189 | private int attachments_count; 190 | @SerializedName("created_at") 191 | private String created_at; 192 | @SerializedName("html_url") 193 | private String html_url; 194 | @SerializedName("attachments_url") 195 | private String attachments_url; 196 | @SerializedName("comments_url") 197 | private String comments_url; 198 | @SerializedName("animated") 199 | private boolean animated; 200 | @SerializedName("tags") 201 | private String[] tags; 202 | @SerializedName("user") 203 | private User user; 204 | } 205 | -------------------------------------------------------------------------------- /app/src/main/java/me/mrrobot97/designer/model/User.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer.model; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | import java.io.Serializable; 6 | 7 | /** 8 | * Created by mrrobot on 16/10/21. 9 | */ 10 | 11 | public class User implements Serializable{ 12 | @SerializedName("id") 13 | private String id; 14 | @SerializedName("name") 15 | private String name; 16 | @SerializedName("username") 17 | private String username; 18 | @SerializedName("html_url") 19 | private String html_url; 20 | @SerializedName("avatar_url") 21 | private String avatar_url; 22 | @SerializedName("buckets_count") 23 | private int buckets_count; 24 | @SerializedName("comments_received_count") 25 | private int comments_received_count; 26 | @SerializedName("followers_count") 27 | private int followers_count; 28 | @SerializedName("followings_count") 29 | private int followings_count; 30 | @SerializedName("projects_count") 31 | private int projects_count; 32 | @SerializedName("shots_count") 33 | private int shots_count; 34 | @SerializedName("type") 35 | private String type; 36 | @SerializedName("buckets_url") 37 | private String buckets_url; 38 | @SerializedName("followers_url") 39 | private String followers_url; 40 | @SerializedName("following_url") 41 | private String following_url; 42 | @SerializedName("projects_url") 43 | private String projects_url; 44 | @SerializedName("shots_url") 45 | private String shots_url; 46 | @SerializedName("created_at") 47 | private String created_at; 48 | @SerializedName("location") 49 | private String location; 50 | @SerializedName("links") 51 | private Links links; 52 | 53 | public User(String id, String name, String username, String html_url, String avatar_url, int buckets_count, int comments_received_count, 54 | int followers_count, int followings_count, int projects_count, int shots_count, String type, String buckets_url, 55 | String followers_url, String following_url, String projects_url, String shots_url, String created_at, String location, 56 | Links links) { 57 | this.id = id; 58 | this.name = name; 59 | this.username = username; 60 | this.html_url = html_url; 61 | this.avatar_url = avatar_url; 62 | this.buckets_count = buckets_count; 63 | this.comments_received_count = comments_received_count; 64 | this.followers_count = followers_count; 65 | this.followings_count = followings_count; 66 | this.projects_count = projects_count; 67 | this.shots_count = shots_count; 68 | this.type = type; 69 | this.buckets_url = buckets_url; 70 | this.followers_url = followers_url; 71 | this.following_url = following_url; 72 | this.projects_url = projects_url; 73 | this.shots_url = shots_url; 74 | this.created_at = created_at; 75 | this.location = location; 76 | this.links = links; 77 | } 78 | 79 | public String getLocation() { 80 | return location; 81 | } 82 | 83 | public void setLocation(String location) { 84 | this.location = location; 85 | } 86 | 87 | 88 | public String getId() { 89 | return id; 90 | } 91 | 92 | public void setId(String id) { 93 | this.id = id; 94 | } 95 | 96 | public String getName() { 97 | return name; 98 | } 99 | 100 | public void setName(String name) { 101 | this.name = name; 102 | } 103 | 104 | public String getUsername() { 105 | return username; 106 | } 107 | 108 | public void setUsername(String username) { 109 | this.username = username; 110 | } 111 | 112 | public String getHtml_url() { 113 | return html_url; 114 | } 115 | 116 | public void setHtml_url(String html_url) { 117 | this.html_url = html_url; 118 | } 119 | 120 | public String getAvatar_url() { 121 | return avatar_url; 122 | } 123 | 124 | public void setAvatar_url(String avatar_url) { 125 | this.avatar_url = avatar_url; 126 | } 127 | 128 | public int getBuckets_count() { 129 | return buckets_count; 130 | } 131 | 132 | public void setBuckets_count(int buckets_count) { 133 | this.buckets_count = buckets_count; 134 | } 135 | 136 | public int getComments_received_count() { 137 | return comments_received_count; 138 | } 139 | 140 | public void setComments_received_count(int comments_received_count) { 141 | this.comments_received_count = comments_received_count; 142 | } 143 | 144 | public int getFollowers_count() { 145 | return followers_count; 146 | } 147 | 148 | public void setFollowers_count(int follwers_count) { 149 | this.followers_count = follwers_count; 150 | } 151 | 152 | public int getFollowings_count() { 153 | return followings_count; 154 | } 155 | 156 | public void setFollwings_count(int follwings_count) { 157 | this.followings_count = follwings_count; 158 | } 159 | 160 | public int getProjects_count() { 161 | return projects_count; 162 | } 163 | 164 | public void setProjects_count(int projects_count) { 165 | this.projects_count = projects_count; 166 | } 167 | 168 | public int getShots_count() { 169 | return shots_count; 170 | } 171 | 172 | public void setShots_count(int shots_count) { 173 | this.shots_count = shots_count; 174 | } 175 | 176 | public String getType() { 177 | return type; 178 | } 179 | 180 | public void setType(String type) { 181 | this.type = type; 182 | } 183 | 184 | public String getBuckets_url() { 185 | return buckets_url; 186 | } 187 | 188 | public void setBuckets_url(String buckets_url) { 189 | this.buckets_url = buckets_url; 190 | } 191 | 192 | public String getFollowers_url() { 193 | return followers_url; 194 | } 195 | 196 | public void setFollowers_url(String followers_url) { 197 | this.followers_url = followers_url; 198 | } 199 | 200 | public String getFollowing_url() { 201 | return following_url; 202 | } 203 | 204 | public void setFollowing_url(String following_url) { 205 | this.following_url = following_url; 206 | } 207 | 208 | public String getProjects_url() { 209 | return projects_url; 210 | } 211 | 212 | public void setProjects_url(String projects_url) { 213 | this.projects_url = projects_url; 214 | } 215 | 216 | public String getShots_url() { 217 | return shots_url; 218 | } 219 | 220 | public void setShots_url(String shots_url) { 221 | this.shots_url = shots_url; 222 | } 223 | 224 | public String getCreated_at() { 225 | return created_at; 226 | } 227 | 228 | public void setCreated_at(String created_at) { 229 | this.created_at = created_at; 230 | } 231 | 232 | public Links getLinks() { 233 | return links; 234 | } 235 | 236 | public void setLinks(Links links) { 237 | this.links = links; 238 | } 239 | 240 | } 241 | -------------------------------------------------------------------------------- /app/src/main/java/me/mrrobot97/designer/presenter/BrowsePresenter.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer.presenter; 2 | 3 | import java.util.List; 4 | import me.mrrobot97.designer.contracts.BrowseContract; 5 | import me.mrrobot97.designer.model.IModel; 6 | import me.mrrobot97.designer.model.ModelImpl; 7 | import me.mrrobot97.designer.model.Shot; 8 | 9 | /** 10 | * Created by mrrobot on 16/10/21. 11 | */ 12 | 13 | public class BrowsePresenter implements BrowseContract.IBrowsePresenter { 14 | 15 | private BrowseContract.IBrowseView mView; 16 | private IModel mModel; 17 | private int[] pages=new int[]{1,1,1}; 18 | private final int PER_PAGE=30; 19 | private String[] sorts=new String[]{"popular","debuts","recent"}; 20 | 21 | public BrowsePresenter(BrowseContract.IBrowseView view) { 22 | mView = view; 23 | mModel=new ModelImpl(); 24 | } 25 | 26 | 27 | @Override 28 | public void load(final int position) { 29 | pages[position]=1; 30 | mModel.loadShots(sorts[position], pages[position], PER_PAGE, (List shots,boolean success)-> { 31 | mView.loadShots(position,shots,success); 32 | if(success) pages[position]++; 33 | }); 34 | } 35 | 36 | @Override 37 | public void refresh(final int position) { 38 | pages[position]=1; 39 | mModel.loadShots(sorts[position], pages[position], PER_PAGE, (List shots,boolean success) -> { 40 | mView.refreshShots(position,shots,success); 41 | if(success)pages[position]++; 42 | }); 43 | } 44 | 45 | @Override 46 | public void loadMore(final int position) { 47 | mModel.loadShots(sorts[position], pages[position], PER_PAGE, (List shots,boolean success) -> { 48 | mView.loadMore(position,shots,success); 49 | if(success)pages[position]++; 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/me/mrrobot97/designer/presenter/DetailPresenter.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer.presenter; 2 | 3 | import me.mrrobot97.designer.contracts.DetailContract; 4 | import me.mrrobot97.designer.model.IModel; 5 | import me.mrrobot97.designer.model.ModelImpl; 6 | 7 | /** 8 | * Created by mrrobot on 16/10/23. 9 | */ 10 | 11 | public class DetailPresenter implements DetailContract.IDetailPresenter { 12 | private DetailContract.IDetailView mView; 13 | private IModel mModel; 14 | 15 | public DetailPresenter(DetailContract.IDetailView view) { 16 | mView = view; 17 | mModel=new ModelImpl(); 18 | } 19 | 20 | @Override 21 | public void loadComments(String shotId) { 22 | mModel.loadComments(shotId, comments -> mView.showComments(comments)); 23 | } 24 | 25 | @Override 26 | public void loadAttachments(String id) { 27 | mModel.loadAttachments(id, attachments -> mView.showAttachments(attachments)); 28 | } 29 | 30 | @Override public void postComment(String id, String comment) { 31 | mModel.postComment(id,comment,(comment1,success) -> mView.showIfCommentSuccess(comment1,success)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/me/mrrobot97/designer/presenter/PlayerPresenter.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer.presenter; 2 | 3 | import me.mrrobot97.designer.contracts.PlayerContract; 4 | import me.mrrobot97.designer.model.IModel; 5 | import me.mrrobot97.designer.model.ModelImpl; 6 | 7 | /** 8 | * Created by mrrobot on 16/10/24. 9 | */ 10 | 11 | public class PlayerPresenter implements PlayerContract.IPlayerPresenter { 12 | private PlayerContract.IPlayerView mView; 13 | private IModel mModel; 14 | 15 | public PlayerPresenter(PlayerContract.IPlayerView view) { 16 | mView = view; 17 | mModel=new ModelImpl(); 18 | } 19 | 20 | @Override 21 | public void loadUserShots(String userId) { 22 | mModel.loadUserShots(userId, shots -> mView.showShots(shots)); 23 | } 24 | 25 | @Override public void loadUserProfile() { 26 | mModel.loadUserProfile( user -> mView.showPlayerInfoAsync(user)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/me/mrrobot97/designer/retrofit/ApiClient.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer.retrofit; 2 | 3 | import java.io.File; 4 | import me.mrrobot97.designer.MyApplication; 5 | import me.mrrobot97.designer.Utils.NetUtils; 6 | import me.mrrobot97.designer.Utils.SharedPreferencesUtils; 7 | import okhttp3.Cache; 8 | import okhttp3.CacheControl; 9 | import okhttp3.Interceptor; 10 | import okhttp3.OkHttpClient; 11 | import okhttp3.Request; 12 | import okhttp3.Response; 13 | import retrofit2.Retrofit; 14 | import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory; 15 | import retrofit2.converter.gson.GsonConverterFactory; 16 | 17 | /** 18 | * Created by mrrobot on 16/10/21. 19 | */ 20 | 21 | public class ApiClient { 22 | public static final String CACHE_DIR="CacheFile"; 23 | public static final String baseUrl = "https://api.dribbble.com/v1/"; 24 | public static final String access_token = 25 | "a62b88ea291c0d0e5b9295fdb8930936f945027bb84ff747ef6b89f8a9cd4da1"; 26 | public static String user_access_token; 27 | 28 | private static Retrofit retrofit; 29 | private static OkHttpClient client; 30 | 31 | public static OkHttpClient getClient(){ 32 | return client; 33 | } 34 | 35 | public static Retrofit getRetrofit() { 36 | user_access_token= 37 | (String) SharedPreferencesUtils.getFromSpfs(MyApplication.getContext(),"access_token",null); 38 | String token=user_access_token; 39 | if(token==null) token=access_token; 40 | if (retrofit == null) { 41 | synchronized (Retrofit.class) { 42 | if (retrofit == null) { 43 | String finalToken = token; 44 | Interceptor interceptor = chain -> { 45 | Request original = chain.request(); 46 | Request request = null; 47 | if (NetUtils.isNetworkOnline(MyApplication.getContext())) { 48 | request = original.newBuilder() 49 | .addHeader("Authorization", "Bearer " + finalToken) 50 | .build(); 51 | } else { 52 | request = original.newBuilder() 53 | .addHeader("Authorization", "Bearer " + finalToken) 54 | .cacheControl(CacheControl.FORCE_CACHE) 55 | .build(); 56 | } 57 | Response response = chain.proceed(request); 58 | if (NetUtils.isNetworkOnline(MyApplication.getContext())) { 59 | int maxAge = 60 * 60 * 1; // read from cache for 1 hour 60 | return response.newBuilder() 61 | .removeHeader("Pragma") 62 | .removeHeader("Cache-Control") 63 | .header("Cache-Control", "public, max-age=" + maxAge) 64 | .build(); 65 | } else { 66 | int maxStale = 60 * 60 * 24 * 7; // tolerate one week 67 | return response.newBuilder() 68 | .removeHeader("Pragma") 69 | .removeHeader("Cache-Control") 70 | .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale) 71 | .build(); 72 | } 73 | }; 74 | //设置OKHttpClient的缓存目录 75 | File cacheDir = MyApplication.getContext().getCacheDir(); 76 | File cacheFile = new File(cacheDir, CACHE_DIR); 77 | 78 | client = 79 | new OkHttpClient.Builder() 80 | .cache(new Cache(cacheFile, 1024 * 1024 * 50)) 81 | .addNetworkInterceptor(interceptor) 82 | .build(); 83 | 84 | retrofit = new Retrofit.Builder().baseUrl(baseUrl) 85 | .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) 86 | .addConverterFactory(GsonConverterFactory.create()) 87 | .client(client) 88 | .build(); 89 | } 90 | } 91 | } 92 | return retrofit; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/src/main/java/me/mrrobot97/designer/retrofit/DribbbleService.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer.retrofit; 2 | 3 | import java.util.List; 4 | import me.mrrobot97.designer.model.Attachment; 5 | import me.mrrobot97.designer.model.Comment; 6 | import me.mrrobot97.designer.model.Shot; 7 | import me.mrrobot97.designer.model.User; 8 | import retrofit2.http.Field; 9 | import retrofit2.http.FormUrlEncoded; 10 | import retrofit2.http.GET; 11 | import retrofit2.http.POST; 12 | import retrofit2.http.Path; 13 | import retrofit2.http.Query; 14 | import rx.Observable; 15 | 16 | /** 17 | * Created by mrrobot on 16/10/21. 18 | */ 19 | 20 | public interface DribbbleService { 21 | @GET("users/{id}") 22 | Observable loadUser(@Path("id") String id); 23 | 24 | @GET("shots") 25 | Observable> loadShots(@Query("sort")String sort,@Query("page")int page,@Query("per_page")int per_page); 26 | 27 | @GET("shots") 28 | Observable> loadDebutsShots(@Query("list")String list,@Query("page")int page,@Query("per_page")int per_page); 29 | 30 | @GET("shots/{id}") 31 | Observable loadShot(@Path("id")String id); 32 | 33 | @GET("shots/{id}/comments") 34 | Observable> loadComments(@Path("id")String id); 35 | 36 | @GET("users/{id}/shots") 37 | Observable> loadUserShots(@Path("id")String userId); 38 | 39 | @GET("shots/{id}/attachments") 40 | Observable> loadAttachments(@Path("id")String id); 41 | 42 | @GET("user") 43 | Observable loadUserProfile(); 44 | 45 | //账号没有comment权限,无法测试功能是否正常 46 | @FormUrlEncoded 47 | @POST("shots/{id}/comments") 48 | Observable postComment(@Path("id")String id,@Field("body") String comment); 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/me/mrrobot97/designer/view/BaseFragment.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer.view; 2 | 3 | import android.content.Intent; 4 | import android.os.Build; 5 | import android.os.Bundle; 6 | import android.support.annotation.LayoutRes; 7 | import android.support.annotation.Nullable; 8 | import android.support.v4.app.ActivityCompat; 9 | import android.support.v4.app.ActivityOptionsCompat; 10 | import android.support.v4.app.Fragment; 11 | import android.support.v7.widget.GridLayoutManager; 12 | import android.support.v7.widget.RecyclerView; 13 | import android.view.LayoutInflater; 14 | import android.view.View; 15 | import android.view.ViewGroup; 16 | import android.widget.TextView; 17 | import butterknife.BindView; 18 | import butterknife.ButterKnife; 19 | import java.util.List; 20 | import me.mrrobot97.designer.R; 21 | import me.mrrobot97.designer.adapter.ShotsAdapter; 22 | import me.mrrobot97.designer.model.Shot; 23 | 24 | /** 25 | * Created by mrrobot on 16/10/21. 26 | */ 27 | 28 | public class BaseFragment extends Fragment { 29 | 30 | @BindView(R.id.recycler_view)RecyclerView mRecyclerView; 31 | @BindView(R.id.view_loading)TextView loadingView; 32 | 33 | private static final String LAYOUT_ID="layout_id"; 34 | private int layoutId; 35 | private ShotsAdapter mAdapter; 36 | private List mData; 37 | 38 | public void setLoading(boolean loading) { 39 | isLoading = loading; 40 | } 41 | 42 | private boolean isLoading=false; 43 | 44 | public interface SlideToBottomListener{ 45 | void whenSlideToBottom(); 46 | } 47 | private SlideToBottomListener mListener; 48 | 49 | public void setListener(SlideToBottomListener listener) { 50 | mListener = listener; 51 | } 52 | 53 | @Nullable 54 | @Override 55 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 56 | layoutId=getArguments().getInt(LAYOUT_ID); 57 | View view=inflater.inflate(layoutId,container,false); 58 | ButterKnife.bind(this,view); 59 | mRecyclerView.setLayoutManager(new GridLayoutManager(getContext(),3)); 60 | mRecyclerView.addItemDecoration(new ShotsAdapter.MyItemDecoration()); 61 | mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { 62 | @Override 63 | public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 64 | if(isSlideToBottom()){ 65 | if(mListener!=null&&!isLoading){ 66 | showLoadingView(); 67 | mListener.whenSlideToBottom(); 68 | } 69 | } 70 | } 71 | }); 72 | return view; 73 | } 74 | 75 | public void setData(List shots){ 76 | if(mData!=null){ 77 | throw new RuntimeException("You can call setData() only once!"); 78 | } 79 | mData=shots; 80 | mAdapter=new ShotsAdapter(mData,getContext()); 81 | mAdapter.setListener(position -> { 82 | Intent intent=new Intent(getContext(),DetailActivity.class); 83 | intent.putExtra("shot",mData.get(position)); 84 | if(Build.VERSION.SDK_INT shots){ 99 | mData=shots; 100 | mAdapter.notifyDataSetChanged(); 101 | loadingView.setVisibility(View.GONE); 102 | isLoading=false; 103 | } 104 | 105 | public void loadMoreData(List shots){ 106 | if(mData==null){ 107 | throw new RuntimeException("You must call setData() before refreshData()!"); 108 | } 109 | for(Shot shot:shots){ 110 | mData.add(shot); 111 | } 112 | mAdapter.notifyDataSetChanged(); 113 | loadingView.setVisibility(View.GONE); 114 | isLoading=false; 115 | } 116 | 117 | public void showLoadingView(){ 118 | if(loadingView!=null){ 119 | loadingView.setVisibility(View.VISIBLE); 120 | isLoading=true; 121 | } 122 | } 123 | 124 | public void cancleLoadingView(){ 125 | loadingView.setVisibility(View.GONE); 126 | } 127 | 128 | private boolean isSlideToBottom(){ 129 | if(mRecyclerView==null) return false; 130 | if(mRecyclerView.computeVerticalScrollExtent()+mRecyclerView.computeVerticalScrollOffset() 131 | >=mRecyclerView.computeVerticalScrollRange()) 132 | return true; 133 | return false; 134 | } 135 | 136 | public static BaseFragment newInstance(@LayoutRes int layout_id){ 137 | Bundle bundle=new Bundle(); 138 | bundle.putInt(LAYOUT_ID,layout_id); 139 | BaseFragment fragment=new BaseFragment(); 140 | fragment.setArguments(bundle); 141 | return fragment; 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /app/src/main/java/me/mrrobot97/designer/view/BrowseActivity.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer.view; 2 | 3 | import android.Manifest; 4 | import android.content.Intent; 5 | import android.content.pm.PackageManager; 6 | import android.os.Bundle; 7 | import android.support.annotation.NonNull; 8 | import android.support.design.widget.Snackbar; 9 | import android.support.design.widget.TabLayout; 10 | import android.support.v4.app.ActivityCompat; 11 | import android.support.v4.app.Fragment; 12 | import android.support.v4.app.FragmentPagerAdapter; 13 | import android.support.v4.view.ViewPager; 14 | import android.support.v4.widget.SwipeRefreshLayout; 15 | import android.support.v7.app.AppCompatActivity; 16 | import android.support.v7.widget.Toolbar; 17 | import android.view.Menu; 18 | import android.view.MenuItem; 19 | import android.widget.RelativeLayout; 20 | import android.widget.Toast; 21 | import butterknife.BindView; 22 | import butterknife.ButterKnife; 23 | import com.bumptech.glide.Glide; 24 | import java.io.File; 25 | import java.util.List; 26 | import me.mrrobot97.designer.R; 27 | import me.mrrobot97.designer.Utils.FileUtils; 28 | import me.mrrobot97.designer.Utils.NetUtils; 29 | import me.mrrobot97.designer.Utils.ScreenUtils; 30 | import me.mrrobot97.designer.Utils.SharedPreferencesUtils; 31 | import me.mrrobot97.designer.contracts.BrowseContract; 32 | import me.mrrobot97.designer.model.Shot; 33 | import me.mrrobot97.designer.presenter.BrowsePresenter; 34 | import me.mrrobot97.designer.retrofit.ApiClient; 35 | 36 | public class BrowseActivity extends AppCompatActivity implements BrowseContract.IBrowseView { 37 | @BindView(R.id.activity_browse) RelativeLayout mContainer; 38 | @BindView(R.id.tool_bar) Toolbar mToolbar; 39 | @BindView(R.id.swipe_refresh_layout) SwipeRefreshLayout mRefreshLayout; 40 | @BindView(R.id.tab_layout) TabLayout mTabLayout; 41 | @BindView(R.id.view_pager) ViewPager mViewPager; 42 | 43 | public static final int ANIM_DURATION=500; 44 | private Snackbar mSnackBar; 45 | private FragmentPagerAdapter mAdapter; 46 | private String[] mTitles = new String[] { "Popular", "Debuts", "Recent" }; 47 | private static final int REQUEST_CODE = 0; 48 | private boolean isSnackBarShown = false; 49 | private boolean isLogin = false; 50 | private Runnable runnable = ()->{ 51 | if (mRefreshLayout != null) { 52 | mRefreshLayout.setRefreshing(false); 53 | }}; 54 | 55 | private BaseFragment[] mFragments = new BaseFragment[3]; 56 | 57 | BrowseContract.IBrowsePresenter mPresenter; 58 | private MenuItem userProfileItem; 59 | 60 | @Override protected void onCreate(Bundle savedInstanceState) { 61 | super.onCreate(savedInstanceState); 62 | setContentView(R.layout.activity_browse); 63 | ButterKnife.bind(this); 64 | setSupportActionBar(mToolbar); 65 | getSupportActionBar().setDisplayShowTitleEnabled(false); 66 | //getSupportActionBar().setDisplayShowHomeEnabled(true); 67 | //mToolbar.setNavigationIcon(R.drawable.ic_menu); 68 | mToolbar.setOnMenuItemClickListener(item -> { 69 | switch (item.getItemId()) { 70 | case R.id.log_out: 71 | SharedPreferencesUtils.putToSpfs(getApplicationContext(), "login", false); 72 | Intent intent = new Intent(BrowseActivity.this, LoginActivity.class); 73 | startActivity(intent); 74 | finish(); 75 | break; 76 | case R.id.user: 77 | showUserProfile(); 78 | break; 79 | case R.id.clear_cache: 80 | clearUserCache(); 81 | default: 82 | break; 83 | } 84 | return true; 85 | }); 86 | mPresenter = new BrowsePresenter(this); 87 | if (!ifPermissionGranted()) { 88 | requestPermissions(); 89 | } else { 90 | init(); 91 | } 92 | } 93 | 94 | private void startToolbarAnimation() { 95 | int toolbarHeight=ScreenUtils.dp2px(this,56); 96 | mToolbar.setTranslationY(-toolbarHeight); 97 | userProfileItem.getActionView().setTranslationY(-toolbarHeight); 98 | mToolbar.animate() 99 | .translationY(0) 100 | .setDuration(ANIM_DURATION) 101 | .setStartDelay(300).start(); 102 | userProfileItem.getActionView().animate() 103 | .translationY(0) 104 | .setDuration(ANIM_DURATION) 105 | .setStartDelay(500).start(); 106 | } 107 | 108 | private void clearUserCache() { 109 | new Thread(()->{ 110 | //clear OKHttp cache 111 | FileUtils.deleteFileOrDir(getCacheDir().getAbsolutePath()+ File.separator+ ApiClient.CACHE_DIR); 112 | //clear Glide cache 113 | Glide.get(BrowseActivity.this).clearDiskCache(); 114 | //Glide.get(BrowseActivity.this).clearMemory(); 115 | Toast.makeText(BrowseActivity.this, "Clear Cache success", Toast.LENGTH_SHORT).show(); 116 | }).start(); 117 | } 118 | 119 | private void init() { 120 | isLogin = (boolean) SharedPreferencesUtils.getFromSpfs(getApplicationContext(), "login", false); 121 | mSnackBar = Snackbar.make(mContainer, "Sure to exit?", Snackbar.LENGTH_INDEFINITE) 122 | .setAction("Sure", view -> finish()); 123 | mFragments[0] = BaseFragment.newInstance(R.layout.fragment_layout); 124 | mFragments[1] = BaseFragment.newInstance(R.layout.fragment_layout); 125 | mFragments[2] = BaseFragment.newInstance(R.layout.fragment_layout); 126 | mAdapter = new FragmentPagerAdapter(getSupportFragmentManager()) { 127 | @Override public Fragment getItem(int position) { 128 | return mFragments[position]; 129 | } 130 | 131 | @Override public int getCount() { 132 | return mFragments.length; 133 | } 134 | 135 | @Override public CharSequence getPageTitle(int position) { 136 | return mTitles[position]; 137 | } 138 | }; 139 | for (int i = 0; i < mTitles.length; i++) { 140 | mTabLayout.addTab(mTabLayout.newTab()); 141 | } 142 | //设置左右各自缓存的fragment数量,默认为1,若保持默认,则当fragment切换至3时,1的fragment会调用onDestroyView,界面就销毁了。 143 | mViewPager.setOffscreenPageLimit(2); 144 | mViewPager.setAdapter(mAdapter); 145 | mTabLayout.setTabMode(TabLayout.MODE_FIXED); 146 | mTabLayout.setupWithViewPager(mViewPager); 147 | mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { 148 | @Override 149 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 150 | 151 | } 152 | 153 | @Override public void onPageSelected(int position) { 154 | 155 | } 156 | 157 | @Override public void onPageScrollStateChanged(int state) { 158 | //控制只有当viewpager为静止状态时才能触发SwipeRefreshLayout 159 | mRefreshLayout.setEnabled(state == ViewPager.SCROLL_STATE_IDLE); 160 | } 161 | }); 162 | mRefreshLayout.setOnRefreshListener(()->{ 163 | mPresenter.refresh(mViewPager.getCurrentItem()); 164 | mRefreshLayout.setRefreshing(true); 165 | }); 166 | //设置下拉刷新的出发高度为屏幕高度的1/3 167 | mRefreshLayout.setDistanceToTriggerSync( 168 | ScreenUtils.getScreenWidthAndHeight(getApplicationContext())[1] / 3); 169 | mRefreshLayout.post(()->mRefreshLayout.setRefreshing(true)); 170 | loadData(); 171 | if (!NetUtils.isNetworkOnline(this)) { 172 | Toast.makeText(this, "客官,您没有联网呦~", Toast.LENGTH_SHORT).show(); 173 | } 174 | } 175 | 176 | private void loadData() { 177 | for (int i = 0; i < mFragments.length; i++) { 178 | mPresenter.load(i); 179 | mFragments[i].showLoadingView(); 180 | final int finalI = i; 181 | mFragments[i].setListener(()->mPresenter.loadMore(finalI)); 182 | } 183 | } 184 | 185 | @Override public void loadShots(int position, List shots, boolean success) { 186 | mRefreshLayout.post(runnable); 187 | if (!success) { 188 | mFragments[position].setLoading(false); 189 | Toast.makeText(this, "客官,数据加载出错了呦~", Toast.LENGTH_SHORT).show(); 190 | } else { 191 | mFragments[position].setData(shots); 192 | } 193 | } 194 | 195 | @Override public void refreshShots(int position, List shots, boolean success) { 196 | mRefreshLayout.post(runnable); 197 | if (!success) { 198 | mFragments[position].setLoading(false); 199 | Toast.makeText(this, "客官,数据加载出错了呦~", Toast.LENGTH_SHORT).show(); 200 | return; 201 | } else { 202 | mFragments[position].refreshDate(shots); 203 | } 204 | } 205 | 206 | @Override public void loadMore(int position, List shots, boolean success) { 207 | mRefreshLayout.post(runnable); 208 | mFragments[position].cancleLoadingView(); 209 | if (!success) { 210 | mFragments[position].setLoading(false); 211 | Toast.makeText(this, "客官,加载出错了呦~", Toast.LENGTH_SHORT).show(); 212 | return; 213 | } else { 214 | mFragments[position].loadMoreData(shots); 215 | mRefreshLayout.setRefreshing(false); 216 | } 217 | } 218 | 219 | public boolean ifPermissionGranted() { 220 | if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) 221 | != PackageManager.PERMISSION_GRANTED) { 222 | return false; 223 | } 224 | return true; 225 | } 226 | 227 | @Override public void requestPermissions() { 228 | ActivityCompat.requestPermissions(this, 229 | new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, REQUEST_CODE); 230 | } 231 | 232 | @Override public void showUserProfile() { 233 | if (isLogin) { 234 | Intent intent = new Intent(BrowseActivity.this, PlayerActivity.class); 235 | //start DetailActivity without parsing a User 236 | startActivity(intent); 237 | } else { 238 | Intent intent = new Intent(this, LoginActivity.class); 239 | startActivity(intent); 240 | } 241 | } 242 | 243 | @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, 244 | @NonNull int[] grantResults) { 245 | super.onRequestPermissionsResult(requestCode, permissions, grantResults); 246 | if (requestCode == REQUEST_CODE) { 247 | if (grantResults.length > 0) { 248 | finish(); 249 | } else { 250 | init(); 251 | } 252 | } 253 | } 254 | 255 | @Override public void onBackPressed() { 256 | if (isSnackBarShown) { 257 | mSnackBar.dismiss(); 258 | isSnackBarShown = false; 259 | } else { 260 | mSnackBar.show(); 261 | isSnackBarShown = true; 262 | } 263 | } 264 | 265 | @Override public boolean onCreateOptionsMenu(Menu menu) { 266 | getMenuInflater().inflate(R.menu.menu_browse, menu); 267 | MenuItem item = menu.getItem(1); 268 | userProfileItem=menu.findItem(R.id.user); 269 | userProfileItem.setActionView(R.layout.user_menu_item_view); 270 | userProfileItem.getActionView().setOnClickListener(view -> showUserProfile()); 271 | if (isLogin) { 272 | item.setTitle("Log out"); 273 | } else { 274 | item.setTitle("Sign in"); 275 | } 276 | startToolbarAnimation(); 277 | return true; 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /app/src/main/java/me/mrrobot97/designer/view/DetailActivity.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer.view; 2 | 3 | import android.animation.Animator; 4 | import android.animation.ValueAnimator; 5 | import android.content.DialogInterface; 6 | import android.content.Intent; 7 | import android.graphics.Bitmap; 8 | import android.graphics.drawable.BitmapDrawable; 9 | import android.os.Build; 10 | import android.os.Bundle; 11 | import android.support.v4.app.ActivityCompat; 12 | import android.support.v4.app.ActivityOptionsCompat; 13 | import android.support.v7.app.AlertDialog; 14 | import android.support.v7.graphics.Palette; 15 | import android.support.v7.widget.LinearLayoutManager; 16 | import android.support.v7.widget.RecyclerView; 17 | import android.support.v7.widget.Toolbar; 18 | import android.text.Html; 19 | import android.text.Spannable; 20 | import android.view.LayoutInflater; 21 | import android.view.View; 22 | import android.view.ViewGroup; 23 | import android.view.ViewTreeObserver; 24 | import android.view.animation.AccelerateInterpolator; 25 | import android.view.animation.DecelerateInterpolator; 26 | import android.widget.Button; 27 | import android.widget.EditText; 28 | import android.widget.ImageView; 29 | import android.widget.RelativeLayout; 30 | import android.widget.TextView; 31 | import android.widget.Toast; 32 | import butterknife.BindView; 33 | import butterknife.ButterKnife; 34 | import com.bumptech.glide.Glide; 35 | import com.bumptech.glide.request.animation.GlideAnimation; 36 | import com.bumptech.glide.request.target.SimpleTarget; 37 | import java.util.List; 38 | import me.mrrobot97.designer.R; 39 | import me.mrrobot97.designer.SwipeActivity.SwipeBackActivity; 40 | import me.mrrobot97.designer.Utils.BitmapUtils; 41 | import me.mrrobot97.designer.Utils.FileUtils; 42 | import me.mrrobot97.designer.Utils.ScreenUtils; 43 | import me.mrrobot97.designer.Utils.SharedPreferencesUtils; 44 | import me.mrrobot97.designer.adapter.AttachmentsAdapter; 45 | import me.mrrobot97.designer.contracts.DetailContract; 46 | import me.mrrobot97.designer.customViews.CircleImageView; 47 | import me.mrrobot97.designer.customViews.HoverView; 48 | import me.mrrobot97.designer.model.Attachment; 49 | import me.mrrobot97.designer.model.Comment; 50 | import me.mrrobot97.designer.model.Shot; 51 | import me.mrrobot97.designer.presenter.DetailPresenter; 52 | import me.mrrobot97.designer.retrofit.ApiClient; 53 | import okhttp3.OkHttpClient; 54 | 55 | import static me.mrrobot97.designer.Utils.StringUtils.checkAvailable; 56 | 57 | //// TODO: 16/11/4 DetailActivity界面加载图片太慢,待解决 58 | public class DetailActivity extends SwipeBackActivity implements DetailContract.IDetailView { 59 | @BindView(R.id.avatar) CircleImageView avatar; 60 | @BindView(R.id.shot_title) TextView shotTitle; 61 | @BindView(R.id.author) TextView author; 62 | @BindView(R.id.image_view) ImageView mImageView; 63 | @BindView(R.id.tool_bar) Toolbar mToolbar; 64 | @BindView(R.id.view_cnt) TextView viewCnt; 65 | @BindView(R.id.comments_cnt) TextView commentsCnt; 66 | @BindView(R.id.likes_cnt) TextView likesCnt; 67 | @BindView(R.id.hover_view) HoverView mHoverView; 68 | @BindView(R.id.front_image_view) ImageView frontImageView; 69 | @BindView(R.id.comments_recycler_view) RecyclerView mCommentsRecycler; 70 | @BindView(R.id.share_iv) ImageView mShareIv; 71 | @BindView(R.id.recyclerview) RecyclerView attachmentRecyclerview; 72 | @BindView(R.id.attachment_layout) RelativeLayout attachmentLayout; 73 | @BindView(R.id.profile_layout)RelativeLayout profileLayout; 74 | @BindView(R.id.edit_comment)EditText mEditComment; 75 | @BindView(R.id.bt_send)Button mBtSend; 76 | 77 | public static final int ANIM_DURATION=500; 78 | 79 | private Shot mShot; 80 | private int screenWidth; 81 | private ValueAnimator mAnimator; 82 | private AlertDialog.Builder mBuilder; 83 | private AlertDialog dialog; 84 | private OkHttpClient client; 85 | private String url; 86 | private List mComments; 87 | private DetailContract.IDetailPresenter mPresenter; 88 | private MyAdapter mAdapter = new MyAdapter(); 89 | private AttachmentsAdapter attachAdapter; 90 | private String frontImageUrl; 91 | private boolean isLogin=false; 92 | private String token=null; 93 | 94 | 95 | @Override protected void onCreate(Bundle savedInstanceState) { 96 | super.onCreate(savedInstanceState); 97 | setContentView(R.layout.activity_detail); 98 | ButterKnife.bind(this); 99 | setSupportActionBar(mToolbar); 100 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 101 | getSupportActionBar().setDisplayShowTitleEnabled(false); 102 | mToolbar.setNavigationOnClickListener(view -> finish()); 103 | screenWidth = ScreenUtils.getScreenWidthAndHeight(getApplicationContext())[0]; 104 | isLogin= (boolean) SharedPreferencesUtils.getFromSpfs(this,"login",false); 105 | token= (String) SharedPreferencesUtils.getFromSpfs(this,"access_token",null); 106 | init(); 107 | } 108 | 109 | @Override protected void onNewIntent(Intent intent) { 110 | super.onNewIntent(intent); 111 | init(); 112 | } 113 | 114 | private void prepareAnimator(){ 115 | mAnimator = ValueAnimator.ofFloat(1f, 0f); 116 | mAnimator.setDuration(200); 117 | mAnimator.setInterpolator(new DecelerateInterpolator()); 118 | mAnimator.addUpdateListener(valueAnimator -> { 119 | float alpha = (float) valueAnimator.getAnimatedValue(); 120 | mHoverView.setAlpha(alpha); 121 | frontImageView.setAlpha(alpha); 122 | }); 123 | mAnimator.addListener(new Animator.AnimatorListener() { 124 | @Override public void onAnimationStart(Animator animator) { 125 | 126 | } 127 | 128 | @Override public void onAnimationEnd(Animator animator) { 129 | mHoverView.setVisibility(View.GONE); 130 | frontImageView.setVisibility(View.GONE); 131 | } 132 | 133 | @Override public void onAnimationCancel(Animator animator) { 134 | 135 | } 136 | 137 | @Override public void onAnimationRepeat(Animator animator) { 138 | 139 | } 140 | }); 141 | } 142 | 143 | private void init() { 144 | mShot = (Shot) getIntent().getSerializableExtra("shot"); 145 | mPresenter = new DetailPresenter(this); 146 | client = ApiClient.getClient(); 147 | mCommentsRecycler.setLayoutManager(new LinearLayoutManager(this)); 148 | attachmentRecyclerview.setLayoutManager( 149 | new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)); 150 | mImageView.setMinimumHeight(screenWidth * 3 / 4); 151 | url = mShot.getImages().getHidpi(); 152 | if (!checkAvailable(url)) { 153 | url = mShot.getImages().getNormal(); 154 | }if (!checkAvailable(url)) { 155 | url = mShot.getImages().getTeaser(); 156 | } 157 | mBuilder = new AlertDialog.Builder(this); 158 | mBuilder.setItems(new String[] { "保存图片" }, (DialogInterface dialogInterface, int i) -> { 159 | FileUtils.saveImage(this,frontImageUrl); 160 | }); 161 | dialog = mBuilder.create(); 162 | setListener(); 163 | prepareAnimator(); 164 | setTextInfo(); 165 | changeToolBarColor(); 166 | generateBlurBitmap(); 167 | loadData(); 168 | } 169 | 170 | private void setListener() { 171 | mImageView.setOnClickListener(view -> { 172 | frontImageUrl = url; 173 | showFullScreenView(); 174 | }); 175 | frontImageView.setOnClickListener(view -> mAnimator.start()); 176 | mHoverView.setOnClickListener(view -> mAnimator.start()); 177 | frontImageView.setOnLongClickListener(view -> { 178 | dialog.show(); 179 | return true; 180 | }); 181 | avatar.setOnClickListener(view -> { 182 | Intent intent = new Intent(DetailActivity.this, PlayerActivity.class); 183 | intent.putExtra("author", mShot.getUser()); 184 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 185 | startActivity(intent); 186 | } else { 187 | //5.0及以上系统实现共享元素动画 188 | ActivityOptionsCompat options = 189 | ActivityOptionsCompat.makeSceneTransitionAnimation(DetailActivity.this, avatar, 190 | getString(R.string.transitionAvatar)); 191 | ActivityCompat.startActivity(DetailActivity.this, intent, options.toBundle()); 192 | } 193 | }); 194 | if(!isLogin){ 195 | mEditComment.setEnabled(false); 196 | } 197 | mBtSend.setOnClickListener(view -> { 198 | if(!isLogin){ 199 | Toast.makeText(this, "Please sign in.", Toast.LENGTH_SHORT).show(); 200 | return; 201 | } 202 | String comment=mEditComment.getText().toString(); 203 | if(comment==null||comment.trim().length()==0) { 204 | Toast.makeText(this, "Empty comment.", Toast.LENGTH_SHORT).show(); 205 | return; 206 | } 207 | mEditComment.setText(""); 208 | mPresenter.postComment(mShot.getId(),comment); 209 | }); 210 | } 211 | 212 | private void setTextInfo() { 213 | shotTitle.setText(mShot.getTitle()); 214 | author.setText(mShot.getUser().getUsername()); 215 | viewCnt.setText(mShot.getViews_count() + ""); 216 | commentsCnt.setText(mShot.getComments_count() + ""); 217 | likesCnt.setText(mShot.getLikes_count() + ""); 218 | mShareIv.setOnClickListener(view -> shareShot()); 219 | } 220 | 221 | private void loadData() { 222 | //load avatar 223 | SimpleTarget target = new SimpleTarget() { 224 | @Override 225 | public void onResourceReady(Bitmap resource, GlideAnimation glideAnimation) { 226 | avatar.setBitmap(resource); 227 | } 228 | }; 229 | Glide.with(this).load(mShot.getUser().getAvatar_url()).asBitmap().into(target); 230 | //load attachments 231 | if (mShot.getAttachments_count() > 0) { 232 | mPresenter.loadAttachments(mShot.getId()); 233 | } 234 | //load comments 235 | mPresenter.loadComments(mShot.getId()); 236 | } 237 | 238 | private void changeToolBarColor() { 239 | //使用Palette动态改变Toolbar背景色 240 | SimpleTarget targetPalette=new SimpleTarget() { 241 | public void onResourceReady(Bitmap resource, GlideAnimation glideAnimation) { 242 | Palette.from(resource).generate(palette -> { 243 | int vibrantColor=palette.getVibrantColor(getResources().getColor(R.color.colorPrimaryDark)); 244 | mToolbar.post(()->{ 245 | mToolbar.setBackgroundColor(vibrantColor); 246 | profileLayout.setBackgroundColor(vibrantColor); 247 | if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.LOLLIPOP) 248 | getWindow().setStatusBarColor(vibrantColor); 249 | }); 250 | }); 251 | } 252 | }; 253 | Glide.with(this).load(mShot.getImages().getTeaser()).asBitmap().into(targetPalette); 254 | } 255 | 256 | private void generateBlurBitmap() { 257 | String blurUrl=null; 258 | //复用在BrowseAcrtivity中下载的缩略图 259 | if (screenWidth >= 720) { 260 | blurUrl=mShot.getImages().getNormal(); 261 | if(!checkAvailable(url)) url=mShot.getImages().getTeaser(); 262 | } else { 263 | blurUrl=mShot.getImages().getTeaser(); 264 | } 265 | SimpleTarget target=new SimpleTarget() { 266 | @Override 267 | public void onResourceReady(Bitmap resource, GlideAnimation glideAnimation) { 268 | Bitmap blurImage= BitmapUtils.fastblur(resource,0.4f,8); 269 | BitmapDrawable drawable=new BitmapDrawable(blurImage); 270 | Glide.with(DetailActivity.this).load(url).placeholder(drawable).crossFade().into(mImageView); 271 | Glide.with(DetailActivity.this).load(url).placeholder(drawable).crossFade().into(frontImageView); 272 | } 273 | }; 274 | Glide.with(DetailActivity.this).load(blurUrl).asBitmap().into(target); 275 | } 276 | 277 | void showFullScreenView() { 278 | mHoverView.setVisibility(View.VISIBLE); 279 | frontImageView.setVisibility(View.VISIBLE); 280 | mHoverView.setAlpha(1f); 281 | frontImageView.setAlpha(1f); 282 | Glide.with(DetailActivity.this).load(frontImageUrl).crossFade().into(frontImageView); 283 | } 284 | 285 | private void shareShot() { 286 | Intent intent = new Intent(Intent.ACTION_SEND); 287 | intent.setType("text/plain"); 288 | 289 | intent.putExtra(Intent.EXTRA_SUBJECT, mShot.getTitle()); 290 | intent.putExtra(Intent.EXTRA_TEXT, 291 | "I found an amazing design in dribbble: " + mShot.getHtml_url()); 292 | startActivity(Intent.createChooser(intent, "Share design")); 293 | } 294 | 295 | 296 | @Override public void showComments(List commments) { 297 | mComments = commments; 298 | mAdapter = new MyAdapter(); 299 | mCommentsRecycler.setAdapter(mAdapter); 300 | mCommentsRecycler.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 301 | @Override public boolean onPreDraw() { 302 | mCommentsRecycler.getViewTreeObserver().removeOnPreDrawListener(this); 303 | mCommentsRecycler.setScaleY(0.1f); 304 | mCommentsRecycler.animate() 305 | .scaleY(1f) 306 | .setDuration(ANIM_DURATION) 307 | .setInterpolator(new AccelerateInterpolator()) 308 | .start(); 309 | return true; 310 | } 311 | }); 312 | } 313 | 314 | @Override public void showAttachments(final List attachments) { 315 | attachmentLayout.setVisibility(View.VISIBLE); 316 | attachmentRecyclerview.setVisibility(View.VISIBLE); 317 | attachAdapter = new AttachmentsAdapter(attachments, DetailActivity.this); 318 | //attachAdapter.setListener(position -> { 319 | // Attachment attachment = attachments.get(position); 320 | // frontImageUrl = attachment.getUrl(); 321 | // showFullScreenView(); 322 | //}); 323 | attachAdapter.setListener(position -> { 324 | Attachment attachment = attachments.get(position); 325 | frontImageUrl = attachment.getUrl(); 326 | showFullScreenView(); 327 | }); 328 | attachmentRecyclerview.setAdapter(attachAdapter); 329 | } 330 | 331 | @Override public void showIfCommentSuccess(Comment comment,boolean success) { 332 | if (success){ 333 | mComments.add(comment); 334 | mAdapter.notifyItemInserted(mComments.size()-1); 335 | }else{ 336 | Toast.makeText(this, "Comment failed,Please make that you have permission to comment.", Toast.LENGTH_SHORT).show(); 337 | } 338 | } 339 | 340 | class MyAdapter extends RecyclerView.Adapter { 341 | 342 | @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 343 | View view = LayoutInflater.from(DetailActivity.this) 344 | .inflate(R.layout.comment_item_layout, parent, false); 345 | MyHolder holder = new MyHolder(view); 346 | return holder; 347 | } 348 | 349 | @Override public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) { 350 | Comment comment = mComments.get(position); 351 | SimpleTarget target = new SimpleTarget() { 352 | @Override public void onResourceReady(Bitmap resource, 353 | GlideAnimation glideAnimation) { 354 | ((MyHolder) holder).mAvatar.setBitmap(resource); 355 | } 356 | }; 357 | Glide.with(DetailActivity.this) 358 | .load(comment.getUser().getAvatar_url()) 359 | .asBitmap() 360 | .into(target); 361 | ((MyHolder) holder).mAuthor.setText(comment.getUser().getUsername()); 362 | Spannable sp = (Spannable) Html.fromHtml(comment.getBody()); 363 | ((MyHolder) holder).mContent.setText(sp.toString().trim()); 364 | if (position % 2 != 0) { 365 | ((MyHolder) holder).mRelativeLayout.setBackgroundColor(getColor(R.color.textColorWhite)); 366 | } 367 | } 368 | 369 | @Override public int getItemCount() { 370 | return mComments.size(); 371 | } 372 | 373 | class MyHolder extends RecyclerView.ViewHolder { 374 | @BindView(R.id.comment_background) RelativeLayout mRelativeLayout; 375 | @BindView(R.id.comment_avatar) CircleImageView mAvatar; 376 | @BindView(R.id.comment_content) TextView mContent; 377 | @BindView(R.id.comment_author) TextView mAuthor; 378 | 379 | public MyHolder(View itemView) { 380 | super(itemView); 381 | ButterKnife.bind(this, itemView); 382 | } 383 | } 384 | } 385 | } 386 | -------------------------------------------------------------------------------- /app/src/main/java/me/mrrobot97/designer/view/LoginActivity.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer.view; 2 | 3 | import android.content.Intent; 4 | import android.graphics.Typeface; 5 | import android.os.Bundle; 6 | import android.support.v7.app.AppCompatActivity; 7 | import android.view.View; 8 | import android.webkit.WebChromeClient; 9 | import android.webkit.WebSettings; 10 | import android.webkit.WebView; 11 | import android.webkit.WebViewClient; 12 | import android.widget.Button; 13 | import android.widget.ProgressBar; 14 | import android.widget.RelativeLayout; 15 | import android.widget.TextView; 16 | import android.widget.Toast; 17 | import butterknife.BindView; 18 | import butterknife.ButterKnife; 19 | import java.io.IOException; 20 | import java.util.HashMap; 21 | import java.util.Map; 22 | import me.mrrobot97.designer.R; 23 | import me.mrrobot97.designer.Utils.FileUtils; 24 | import me.mrrobot97.designer.Utils.SharedPreferencesUtils; 25 | import okhttp3.Call; 26 | import okhttp3.Callback; 27 | import okhttp3.FormBody; 28 | import okhttp3.OkHttpClient; 29 | import okhttp3.Request; 30 | import okhttp3.RequestBody; 31 | import okhttp3.Response; 32 | import org.json.JSONException; 33 | import org.json.JSONObject; 34 | 35 | //// TODO: 16/11/1 WebView缓存问题,每次login前手动删除缓存文件,打开APP第一次认证正常,但第二次登录还是会直接跳过,尚未找到原因。 36 | public class LoginActivity extends AppCompatActivity { 37 | @BindView(R.id.bt_login)Button mBtLogin; 38 | @BindView(R.id.bt_non_login)Button mBtNon; 39 | @BindView(R.id.txt_logo)TextView mTxtLogo; 40 | @BindView(R.id.web_view)WebView mWebView; 41 | @BindView(R.id.loading_layout)RelativeLayout loadingLayout; 42 | @BindView(R.id.back_layout)RelativeLayout backLayout; 43 | @BindView(R.id.progressbar)ProgressBar mProgressBar; 44 | @BindView(R.id.webview_layout)RelativeLayout mWebviewLayout; 45 | 46 | private final String AUTH_URL="https://dribbble.com/oauth/authorize"; 47 | private final String TOKEN_URL="https://dribbble.com/oauth/token"; 48 | private final String CLIENT_ID="a655a6b9c5440762a30d30f85142afc6fe34ff3419ee5864130034f613b1791c"; 49 | private final String CLIENT_SECRET="9c5de21b644560851a8823fb47de3c3ce223d9d3f67a27a8c73346ff02caa98a"; 50 | private final String CACHE_DIR="/data/data/me.mrrobot97.designer/app_webview"; 51 | private final String CACHE_FILE="/data/data/me.mrrobot97.designer/cache/org.chromium.android_webview"; 52 | private final String SCOPE="public+write+comment"; 53 | private String code; 54 | private String token; 55 | private OkHttpClient client; 56 | 57 | 58 | @Override protected void onCreate(Bundle savedInstanceState) { 59 | super.onCreate(savedInstanceState); 60 | setContentView(R.layout.activity_login); 61 | ButterKnife.bind(this); 62 | boolean isLogin= 63 | (boolean) SharedPreferencesUtils.getFromSpfs(getApplicationContext(),"login",false); 64 | if(isLogin){ 65 | startBrowseActivity(); 66 | } 67 | init(); 68 | } 69 | 70 | private void startBrowseActivity(){ 71 | Intent intent=new Intent(LoginActivity.this,BrowseActivity.class); 72 | startActivity(intent); 73 | finish(); 74 | } 75 | 76 | private void init() { 77 | Typeface typeface=Typeface.createFromAsset(getAssets(),"fonts/New Walt Disney.ttf"); 78 | mTxtLogo.setTypeface(typeface); 79 | client=new OkHttpClient(); 80 | mBtLogin.setOnClickListener(view -> login()); 81 | mBtNon.setOnClickListener(view -> nonLogin()); 82 | WebSettings settings=mWebView.getSettings(); 83 | settings.setAppCacheEnabled(false); 84 | settings.setCacheMode(WebSettings.LOAD_NO_CACHE); 85 | settings.setJavaScriptEnabled(true); 86 | settings.setDomStorageEnabled(false); 87 | mWebView.setWebViewClient(new WebViewClient(){ 88 | @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { 89 | if(url.contains("code")){ 90 | //重定向网页,参数中包含了code 91 | code=url.substring(url.lastIndexOf("=")+1); 92 | mWebviewLayout.setVisibility(View.GONE); 93 | getToken(); 94 | return false; 95 | } 96 | view.loadUrl(url); 97 | return true; 98 | } 99 | }); 100 | mWebView.setWebChromeClient(new WebChromeClient(){ 101 | @Override public void onProgressChanged(WebView view, int newProgress) { 102 | super.onProgressChanged(view, newProgress); 103 | mProgressBar.setProgress(newProgress); 104 | } 105 | }); 106 | } 107 | 108 | private void nonLogin() { 109 | SharedPreferencesUtils.putToSpfs(getApplicationContext(),"login",false); 110 | startBrowseActivity(); 111 | } 112 | 113 | private void login() { 114 | //两个缓存目录下的文件都要手动删除,否则重新登录的时候会因为缓存而直接进入 115 | //目前每次从新打开APP第一次登录正常,之后的全部直接走缓存了。 116 | FileUtils.deleteFileOrDir(CACHE_DIR); 117 | FileUtils.deleteFileOrDir(CACHE_FILE); 118 | mWebView.clearCache(true); 119 | Map header=new HashMap<>(); 120 | header.put("Cache-Control","no-cache"); 121 | mWebviewLayout.setVisibility(View.VISIBLE); 122 | backLayout.setVisibility(View.INVISIBLE); 123 | mWebView.loadUrl(AUTH_URL+"?client_id="+CLIENT_ID+"&scope="+SCOPE,header); 124 | } 125 | 126 | public void getToken() { 127 | backLayout.setVisibility(View.VISIBLE); 128 | loadingLayout.setVisibility(View.VISIBLE); 129 | RequestBody postParam= new FormBody.Builder().add("client_id",CLIENT_ID).add("client_secret",CLIENT_SECRET).add("code",code).build(); 130 | final Request request=new Request.Builder().url(TOKEN_URL).post(postParam).build(); 131 | client.newCall(request).enqueue(new Callback() { 132 | @Override public void onFailure(Call call, IOException e) { 133 | loadingLayout.setVisibility(View.GONE); 134 | backLayout.setVisibility(View.VISIBLE); 135 | Toast.makeText(LoginActivity.this, "获取认证信息失败,请重新登录", Toast.LENGTH_SHORT).show(); 136 | } 137 | 138 | @Override public void onResponse(Call call, Response response) throws IOException { 139 | try { 140 | loadingLayout.post(()->{ 141 | loadingLayout.setVisibility(View.GONE); 142 | backLayout.setVisibility(View.VISIBLE); 143 | }); 144 | JSONObject object=new JSONObject(response.body().string()); 145 | token=object.getString("access_token"); 146 | SharedPreferencesUtils.putToSpfs(getApplicationContext(),"access_token",token); 147 | SharedPreferencesUtils.putToSpfs(getApplicationContext(),"login",true); 148 | startBrowseActivity(); 149 | } catch (JSONException e) { 150 | e.printStackTrace(); 151 | } 152 | } 153 | }); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /app/src/main/java/me/mrrobot97/designer/view/PlayerActivity.java: -------------------------------------------------------------------------------- 1 | package me.mrrobot97.designer.view; 2 | 3 | import android.animation.Animator; 4 | import android.animation.ValueAnimator; 5 | import android.content.DialogInterface; 6 | import android.content.Intent; 7 | import android.graphics.Bitmap; 8 | import android.net.Uri; 9 | import android.os.Build; 10 | import android.os.Bundle; 11 | import android.support.annotation.RequiresApi; 12 | import android.support.v7.app.AlertDialog; 13 | import android.support.v7.widget.LinearLayoutManager; 14 | import android.support.v7.widget.RecyclerView; 15 | import android.support.v7.widget.Toolbar; 16 | import android.view.View; 17 | import android.view.animation.DecelerateInterpolator; 18 | import android.widget.ImageView; 19 | import android.widget.RelativeLayout; 20 | import android.widget.TextView; 21 | import butterknife.BindView; 22 | import butterknife.ButterKnife; 23 | import com.bumptech.glide.Glide; 24 | import com.bumptech.glide.request.animation.GlideAnimation; 25 | import com.bumptech.glide.request.target.SimpleTarget; 26 | import java.util.List; 27 | import me.mrrobot97.designer.R; 28 | import me.mrrobot97.designer.SwipeActivity.SwipeBackActivity; 29 | import me.mrrobot97.designer.Utils.BitmapUtils; 30 | import me.mrrobot97.designer.Utils.FileUtils; 31 | import me.mrrobot97.designer.Utils.ScreenUtils; 32 | import me.mrrobot97.designer.Utils.StringUtils; 33 | import me.mrrobot97.designer.adapter.ShotsAdapter; 34 | import me.mrrobot97.designer.contracts.PlayerContract; 35 | import me.mrrobot97.designer.customViews.HoverView; 36 | import me.mrrobot97.designer.model.Shot; 37 | import me.mrrobot97.designer.model.User; 38 | import me.mrrobot97.designer.presenter.PlayerPresenter; 39 | 40 | //// TODO: 16/11/5 用户名与所在地址显示位置有问题,待解决 41 | public class PlayerActivity extends SwipeBackActivity implements PlayerContract.IPlayerView, View.OnClickListener { 42 | @BindView(R.id.tool_bar) Toolbar mToolbar; 43 | @BindView(R.id.avatar) ImageView avatar; 44 | @BindView(R.id.name) TextView name; 45 | @BindView(R.id.location) TextView location; 46 | @BindView(R.id.follower_num) TextView followers; 47 | @BindView(R.id.following_num) TextView followings; 48 | @BindView(R.id.recyclerview) RecyclerView mRecyclerView; 49 | @BindView(R.id.twitter_url) TextView twitterUrl; 50 | @BindView(R.id.website_url) TextView websiteUrl; 51 | @BindView(R.id.dribbble_url) TextView dribbbleUrl; 52 | @BindView(R.id.twitter_link) ImageView twitterLink; 53 | @BindView(R.id.browser_link) ImageView browserLink; 54 | @BindView(R.id.dribbble_link) ImageView dribbbleLink; 55 | @BindView(R.id.hover_view) HoverView mHoverView; 56 | @BindView(R.id.front_image_view) ImageView frontImageView; 57 | @BindView(R.id.twitter_view) RelativeLayout twitterView; 58 | @BindView(R.id.website_view) RelativeLayout webView; 59 | @BindView(R.id.dribbble_view) RelativeLayout dribbbleView; 60 | @BindView(R.id.blue_avatar) ImageView blurAvatar; 61 | 62 | private User mUser; 63 | private List shots; 64 | private ShotsAdapter mAdapter; 65 | private PlayerContract.IPlayerPresenter mPresenter; 66 | private ValueAnimator mAnimator; 67 | private String frontImageUrl; 68 | private AlertDialog.Builder mBuilder; 69 | private AlertDialog dialog; 70 | private int screenWidth; 71 | 72 | @Override protected void onCreate(Bundle savedInstanceState) { 73 | super.onCreate(savedInstanceState); 74 | setContentView(R.layout.activity_player); 75 | ButterKnife.bind(this); 76 | 77 | setSupportActionBar(mToolbar); 78 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 79 | getSupportActionBar().setDisplayShowTitleEnabled(false); 80 | mToolbar.setNavigationOnClickListener(view -> finish()); 81 | init(); 82 | } 83 | 84 | private void init() { 85 | mPresenter = new PlayerPresenter(this); 86 | mUser = (User) getIntent().getSerializableExtra("author"); 87 | mRecyclerView.setLayoutManager( 88 | new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)); 89 | mRecyclerView.addItemDecoration(new ShotsAdapter.MyItemDecoration()); 90 | mAnimator = ValueAnimator.ofFloat(1f, 0f); 91 | mAnimator.setDuration(200); 92 | mAnimator.setInterpolator(new DecelerateInterpolator()); 93 | mAnimator.addUpdateListener(valueAnimator -> { 94 | float alpha = (float) valueAnimator.getAnimatedValue(); 95 | mHoverView.setAlpha(alpha); 96 | frontImageView.setAlpha(alpha); 97 | }); 98 | mAnimator.addListener(new Animator.AnimatorListener() { 99 | @Override public void onAnimationStart(Animator animator) { 100 | 101 | } 102 | 103 | @Override public void onAnimationEnd(Animator animator) { 104 | mHoverView.setVisibility(View.GONE); 105 | frontImageView.setVisibility(View.GONE); 106 | } 107 | 108 | @Override public void onAnimationCancel(Animator animator) { 109 | 110 | } 111 | 112 | @Override public void onAnimationRepeat(Animator animator) { 113 | 114 | } 115 | }); 116 | frontImageView.setOnClickListener(view -> mAnimator.start()); 117 | frontImageView.setOnLongClickListener(view -> { 118 | dialog.show(); 119 | return true; 120 | }); 121 | mHoverView.setOnClickListener(view -> mAnimator.start()); 122 | if (mUser != null) { 123 | showPlayerInfo(); 124 | } else { 125 | mPresenter.loadUserProfile(); 126 | } 127 | mBuilder = new AlertDialog.Builder(this); 128 | mBuilder.setItems(new String[] { "保存图片" }, (DialogInterface dialogInterface, int i) -> { 129 | FileUtils.saveImage(this, frontImageUrl); 130 | }); 131 | dialog = mBuilder.create(); 132 | screenWidth = ScreenUtils.getScreenWidthAndHeight(getApplicationContext())[0]; 133 | blurAvatar.setMinimumHeight(screenWidth); 134 | 135 | } 136 | 137 | @Override public void showPlayerInfo() { 138 | generateBlurAvatar(); 139 | Glide.with(this).load(mUser.getAvatar_url()).crossFade().into(avatar); 140 | name.setText(mUser.getName()); 141 | location.setText(mUser.getLocation()); 142 | followers.setText(mUser.getFollowers_count() + ""); 143 | followings.setText(mUser.getFollowings_count() + ""); 144 | mPresenter.loadUserShots(mUser.getId()); 145 | if (checkNotNUll(mUser.getLinks().getTwitter())) { 146 | twitterUrl.setText(mUser.getLinks().getTwitter()); 147 | twitterLink.setOnClickListener(this); 148 | } else { 149 | twitterView.setVisibility(View.GONE); 150 | } 151 | if (checkNotNUll(mUser.getLinks().getWeb())) { 152 | websiteUrl.setText(mUser.getLinks().getWeb()); 153 | browserLink.setOnClickListener(this); 154 | } else { 155 | webView.setVisibility(View.GONE); 156 | } 157 | if (checkNotNUll(mUser.getUsername())) { 158 | dribbbleUrl.setText(mUser.getUsername()); 159 | dribbbleLink.setOnClickListener(this); 160 | } else { 161 | dribbbleView.setVisibility(View.GONE); 162 | } 163 | } 164 | 165 | private void generateBlurAvatar() { 166 | SimpleTarget target=new SimpleTarget() { 167 | @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1) @Override 168 | public void onResourceReady(Bitmap resource, GlideAnimation glideAnimation) { 169 | Bitmap blurImage= BitmapUtils.fastblur(resource,0.4f,8); 170 | runOnUiThread(()->{ 171 | blurAvatar.setImageBitmap(blurImage); 172 | }); 173 | } 174 | }; 175 | Glide.with(this).load(mUser.getAvatar_url()).asBitmap().into(target); 176 | } 177 | 178 | @Override public void showPlayerInfoAsync(User user) { 179 | mUser = user; 180 | showPlayerInfo(); 181 | } 182 | 183 | private boolean checkNotNUll(String str) { 184 | if (str == null) return false; 185 | if (str.trim().length() == 0) return false; 186 | return true; 187 | } 188 | 189 | void jumpToUrl(String url) { 190 | if (url == null) return; 191 | Intent intent = new Intent(); 192 | intent.setAction(Intent.ACTION_VIEW); 193 | intent.setData(Uri.parse(url)); 194 | startActivity(intent); 195 | } 196 | 197 | @Override public void showShots(final List shots) { 198 | this.shots = shots; 199 | mAdapter = new ShotsAdapter(shots, this); 200 | mAdapter.setListener(position -> { 201 | Shot shot = shots.get(position); 202 | frontImageUrl = shot.getImages().getHidpi(); 203 | if (!StringUtils.checkAvailable(frontImageUrl)) { 204 | frontImageUrl = shot.getImages().getNormal(); 205 | } 206 | if (!StringUtils.checkAvailable(frontImageUrl)) { 207 | frontImageUrl = shot.getImages().getTeaser(); 208 | } 209 | Glide.with(PlayerActivity.this).load(frontImageUrl).crossFade().into(frontImageView); 210 | frontImageView.setAlpha(1f); 211 | mHoverView.setAlpha(1f); 212 | mHoverView.setVisibility(View.VISIBLE); 213 | frontImageView.setVisibility(View.VISIBLE); 214 | }); 215 | mRecyclerView.setAdapter(mAdapter); 216 | mAdapter.notifyDataSetChanged(); 217 | } 218 | 219 | @Override public void onClick(View view) { 220 | String url = null; 221 | switch (view.getId()) { 222 | case R.id.twitter_link: 223 | url = mUser.getLinks().getTwitter(); 224 | break; 225 | case R.id.browser_link: 226 | url = mUser.getLinks().getWeb(); 227 | break; 228 | case R.id.dribbble_link: 229 | url = mUser.getHtml_url(); 230 | break; 231 | default: 232 | break; 233 | } 234 | jumpToUrl(url); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrrobot97/Designer/d2e156f3613896b568329f52ea468a029b5b80df/app/src/main/res/drawable/browser.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/comments.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrrobot97/Designer/d2e156f3613896b568329f52ea468a029b5b80df/app/src/main/res/drawable/comments.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/dribbble.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrrobot97/Designer/d2e156f3613896b568329f52ea468a029b5b80df/app/src/main/res/drawable/dribbble.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrrobot97/Designer/d2e156f3613896b568329f52ea468a029b5b80df/app/src/main/res/drawable/group.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrrobot97/Designer/d2e156f3613896b568329f52ea468a029b5b80df/app/src/main/res/drawable/ic_account.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrrobot97/Designer/d2e156f3613896b568329f52ea468a029b5b80df/app/src/main/res/drawable/ic_arrow.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrrobot97/Designer/d2e156f3613896b568329f52ea468a029b5b80df/app/src/main/res/drawable/ic_menu.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrrobot97/Designer/d2e156f3613896b568329f52ea468a029b5b80df/app/src/main/res/drawable/link.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrrobot97/Designer/d2e156f3613896b568329f52ea468a029b5b80df/app/src/main/res/drawable/loading.gif -------------------------------------------------------------------------------- /app/src/main/res/drawable/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrrobot97/Designer/d2e156f3613896b568329f52ea468a029b5b80df/app/src/main/res/drawable/placeholder.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_rectangle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrrobot97/Designer/d2e156f3613896b568329f52ea468a029b5b80df/app/src/main/res/drawable/share.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrrobot97/Designer/d2e156f3613896b568329f52ea468a029b5b80df/app/src/main/res/drawable/twitter.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrrobot97/Designer/d2e156f3613896b568329f52ea468a029b5b80df/app/src/main/res/drawable/user.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_browse.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 17 | 24 | 25 | 26 | 36 | 37 | 38 | 45 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 18 | 27 | 28 | 29 | 34 | 35 | 40 | 41 | 42 | 43 | 50 | 59 |