├── .gitignore ├── README.md ├── build.gradle ├── demo ├── .gitignore ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── theartofdev │ │ └── fastimageloaderdemo │ │ ├── Adapter.java │ │ ├── AppApplication.java │ │ ├── ImgIXFragment.java │ │ ├── MainActivity.java │ │ ├── Specs.java │ │ ├── instagram │ │ ├── Adapter.java │ │ ├── InstagramFragment.java │ │ ├── ItemView.java │ │ └── service │ │ │ ├── Feed.java │ │ │ ├── Image.java │ │ │ ├── Images.java │ │ │ ├── InstagramService.java │ │ │ ├── Item.java │ │ │ └── User.java │ │ └── zoom │ │ ├── ZoomActivity.java │ │ └── ZoomImageView.java │ └── res │ ├── drawable-hdpi │ └── ic_launcher.png │ ├── drawable-xhdpi │ ├── ic_arrow_down_bold_circle_outline_white_24dp.png │ ├── ic_arrow_down_bold_circle_white_24dp.png │ ├── ic_recycle_white_24dp.png │ └── pattern.png │ ├── drawable │ └── pattern_repeat.xml │ ├── layout │ ├── activity_main.xml │ ├── activity_zoom.xml │ ├── fragment_img_ix.xml │ ├── fragment_instagram.xml │ └── item.xml │ ├── menu │ └── menu_main.xml │ └── values │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── fastimageloader ├── .gitignore ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── theartofdev │ └── fastimageloader │ ├── Decoder.java │ ├── DiskCache.java │ ├── Downloader.java │ ├── FastImageLoader.java │ ├── HttpClient.java │ ├── ImageLoadSpec.java │ ├── ImageLoadSpecBuilder.java │ ├── ImageServiceAdapter.java │ ├── LoadState.java │ ├── LoadedFrom.java │ ├── LogAppender.java │ ├── MemoryPool.java │ ├── ReusableBitmap.java │ ├── Target.java │ ├── adapter │ ├── IdentityAdapter.java │ ├── ImgIXAdapter.java │ ├── ThumborAdapter.java │ └── ThumborInlineAdapter.java │ ├── impl │ ├── DecoderImpl.java │ ├── DiskCacheImpl.java │ ├── DownloaderImpl.java │ ├── ImageRequest.java │ ├── LoaderHandler.java │ ├── MemoryPoolImpl.java │ ├── NativeHttpClient.java │ ├── OkHttpClient.java │ └── util │ │ ├── FILLogger.java │ │ └── FILUtils.java │ └── target │ ├── AnimatingTargetDrawable.java │ ├── TargetAvatarImageView.java │ ├── TargetCircleDrawable.java │ ├── TargetDrawable.java │ ├── TargetHelper.java │ ├── TargetImageView.java │ └── TargetImageViewHandler.java ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── license.md └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | .DS_Store 4 | /build 5 | 6 | ## Directory-based project format 7 | .idea/ 8 | 9 | ## File-based project format 10 | *.ipr 11 | *.iml 12 | *.iws 13 | 14 | # Files generated by pro-guard 15 | *.log 16 | 17 | ## Additional for IntelliJ 18 | out/ 19 | 20 | # generated by mpeltonen/sbt-idea plugin 21 | .idea_modules/ 22 | 23 | # generated by JIRA plugin 24 | atlassian-ide-plugin.xml 25 | 26 | # generated by Crashlytics plugin (for Android Studio and Intellij) 27 | com_crashlytics_export_strings.xml -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android Fast Image Loader 2 | New approach to image loading that overcomes Android memory management limitation by leveraging size, density, config, aggressive bitmap reuse and super-fast disk loading, to provide the fastest and most lightweight image handling possible. 3 | 4 | This library requires fine-tuning work, if you are just looking for simple library I would recommend: [Picasso][1], [Glide][2], [Universal Image Loader][3] or [Volley][4]. 5 | 6 | ### Quick Start 7 | ```groovy 8 | compile 'com.theartofdev:fast-image-loader:0.8.+' 9 | ``` 10 | 11 | ```java 12 | // initialize fast image loader to use thumbor image service 13 | FastImageLoader.init(application) 14 | .setDefaultImageServiceAdapter(new ThumborAdapter("http://myThumbor.com/")) 15 | .setDebugIndicator(true); 16 | 17 | // create image loading specification with specific image dimensions 18 | FastImageLoader.buildSpec("ImageSpec") 19 | .setDimensionByResource(R.dimen.imageWidth, R.dimen.imageHeight) 20 | .build(); 21 | ``` 22 | 23 | ```java 24 | // replace ImageView with TargetImageView in the UI 25 | ((TargetImageView)findViewById(R.id.imageView)).loadImage(imageUri, "ImageSpec"); 26 | ``` 27 | 28 | ### Features 29 | 30 | * Smart memory and disk caching. 31 | * Super-fast, asynchronous, disk cache loading. 32 | * Asynchronous and parallel download. 33 | * Low memory footprint and optimizations for memory pressure. 34 | * Image services support ( [Thumbor ][5], [imgIX ][6], etc.) 35 | * Highly customizable specification loading (size/format/density/bitmap config/etc.) 36 | * Alternative specification loading. 37 | * Pre-fetch download images. 38 | * Advanced bitmaps reuse using inBitmap, reusing bitmaps on destroy and invisibility. 39 | * Smart prioritization and canceling of image load requests, more than list item reuse. 40 | * Placeholder, round rendering, download progress, fade-in animation support. 41 | * Extensive extensibility. 42 | * Logging and analytics hooks. 43 | * Debug indicator (loaded from Memory/Disk/Network). 44 | 45 | 46 | ### Leverage Size, Density and Config 47 | There is a little benefit to download 1500x1500 pixel (450 ppi) image that will take **8.5 MB** of memory to show 500x500 preview in a feed where the user scrolls quickly. It will be much better to decrees the image density by half and use 565 bitmap config lowering the memory footprint to **1 MB** , empirically, without loss of quality. 48 | 49 | ### Aggressive bitmap reuse 50 | To limit the amount of memory allocated and GC work the library reused bitmaps as soon as they are no longer visible in the UI. 51 | 52 | * Activity/Fragment images that have not been destroyed but are no longer visible (onStop has been called) are eligible for bitmap reuse. 53 | * When the Activity/Fragment becomes visible again, if the bitmap was reused, the image is reload. 54 | * After initial allocations, bitmap reuse prevents almost all subsequent allocations. 55 | * High disk loading performance make this process seamless. 56 | 57 | 58 | ### High disk cache performance 59 | 60 | * Up to 5-8 times faster load of cached images from disk. 61 | * Will add benchmark comparison… 62 | 63 | 64 | ### License 65 | The MIT License (MIT) 66 | Copyright (c) 2015 Arthur Teplitzki 67 | See [license.md][7] 68 | 69 | [1]: http://square.github.io/picasso/ 70 | [2]: https://github.com/bumptech/glide 71 | [3]: https://github.com/nostra13/Android-Universal-Image-Loader 72 | [4]: https://github.com/mcxiaoke/android-volley 73 | [5]: https://github.com/thumbor/thumbor 74 | [6]: http://www.imgix.com/ 75 | [7]: https://github.com/ArthurHub/Android-Fast-Image-Loader/blob/master/license.md 76 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | } 5 | dependencies { 6 | classpath 'com.android.tools.build:gradle:1.0.0' 7 | } 8 | } 9 | 10 | allprojects { 11 | repositories { 12 | jcenter() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /demo/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 21 5 | buildToolsVersion "21.1.2" 6 | 7 | defaultConfig { 8 | applicationId "com.theartofdev.fastimageloaderdemo" 9 | minSdkVersion 14 10 | targetSdkVersion 21 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | 15 | lintOptions { 16 | abortOnError false 17 | } 18 | } 19 | 20 | dependencies { 21 | compile project(':fastimageloader') 22 | 23 | compile 'com.google.code.gson:gson:2.3.1' 24 | compile 'com.squareup.okhttp:okhttp:2.2.0' 25 | compile 'com.squareup.retrofit:retrofit:1.9.0' 26 | 27 | compile 'com.jpardogo.materialtabstrip:library:1.0.8' 28 | compile 'com.github.chrisbanes.photoview:library:1.2.3' 29 | 30 | compile 'com.android.support:appcompat-v7:21.0.3' 31 | compile "com.android.support:recyclerview-v7:21.0.3" 32 | compile "com.android.support:support-v13:21.0.3" 33 | } 34 | -------------------------------------------------------------------------------- /demo/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /demo/src/main/java/com/theartofdev/fastimageloaderdemo/Adapter.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloaderdemo; 14 | 15 | import android.app.Activity; 16 | import android.support.v7.widget.RecyclerView; 17 | import android.view.LayoutInflater; 18 | import android.view.View; 19 | import android.view.ViewGroup; 20 | import android.widget.TextView; 21 | 22 | import com.theartofdev.fastimageloader.FastImageLoader; 23 | import com.theartofdev.fastimageloader.ImageLoadSpec; 24 | import com.theartofdev.fastimageloader.target.TargetImageView; 25 | import com.theartofdev.fastimageloaderdemo.zoom.ZoomActivity; 26 | 27 | public final class Adapter extends RecyclerView.Adapter { 28 | 29 | private String[] mItems = new String[]{ 30 | "http://assets.imgix.net/examples/clownfish.jpg", 31 | "http://assets.imgix.net/examples/espresso.jpg", 32 | "http://assets.imgix.net/examples/kayaks.png", 33 | "http://assets.imgix.net/examples/leaves.jpg", 34 | "http://assets.imgix.net/examples/puffins.jpg", 35 | "http://assets.imgix.net/examples/redleaf.jpg", 36 | "http://assets.imgix.net/examples/butterfly.jpg", 37 | "http://assets.imgix.net/examples/blueberries.jpg", 38 | "http://assets.imgix.net/examples/octopus.jpg" 39 | }; 40 | 41 | public Adapter() { 42 | if (AppApplication.mPrefetchImages) { 43 | for (String item : mItems) { 44 | FastImageLoader.prefetchImage(item, Specs.IMG_IX_IMAGE); 45 | } 46 | } 47 | } 48 | 49 | @Override 50 | public int getItemCount() { 51 | return mItems.length; 52 | } 53 | 54 | // Create new views (invoked by the layout manager) 55 | @Override 56 | public Adapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 57 | View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false); 58 | return new ViewHolder(v); 59 | } 60 | 61 | @Override 62 | public void onBindViewHolder(ViewHolder holder, int position) { 63 | ImageLoadSpec spec = FastImageLoader.getSpec(Specs.IMG_IX_IMAGE); 64 | holder.mUrlTextView.setText(mItems[position]); 65 | holder.mSpecTextView.setText(spec.getFormat() + ": (" + spec.getWidth() + "," + spec.getHeight() + ") Config: " + spec.getPixelConfig()); 66 | holder.mTargetImageView.loadImage(mItems[position], spec.getKey()); 67 | } 68 | 69 | //region: Inner class: ViewHolder 70 | 71 | /** 72 | * Provide a reference to the views for each data item 73 | * Complex data items may need more than one view per item, and 74 | * you provide access to all the views for a data item in a view holder 75 | */ 76 | static final class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { 77 | 78 | public final TextView mUrlTextView; 79 | 80 | public final TextView mSpecTextView; 81 | 82 | public final TargetImageView mTargetImageView; 83 | 84 | public ViewHolder(View v) { 85 | super(v); 86 | mUrlTextView = (TextView) v.findViewById(R.id.image_url); 87 | mSpecTextView = (TextView) v.findViewById(R.id.image_spec); 88 | mTargetImageView = (TargetImageView) v.findViewById(R.id.image_view); 89 | mTargetImageView.setOnClickListener(this); 90 | } 91 | 92 | @Override 93 | public void onClick(View v) { 94 | Activity activity = (Activity) mTargetImageView.getContext(); 95 | if (activity != null) { 96 | ZoomActivity.startActivity(activity, mTargetImageView.getUrl(), Specs.UNBOUNDED_MAX, Specs.IMG_IX_IMAGE); 97 | } 98 | } 99 | 100 | } 101 | //endregion 102 | } -------------------------------------------------------------------------------- /demo/src/main/java/com/theartofdev/fastimageloaderdemo/AppApplication.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloaderdemo; 14 | 15 | import android.app.Application; 16 | import android.graphics.Bitmap; 17 | import android.preference.PreferenceManager; 18 | import android.util.Log; 19 | 20 | import com.theartofdev.fastimageloader.FastImageLoader; 21 | import com.theartofdev.fastimageloader.adapter.IdentityAdapter; 22 | import com.theartofdev.fastimageloader.adapter.ImgIXAdapter; 23 | 24 | public class AppApplication extends Application { 25 | 26 | public static final int INSTAGRAM_IMAGE_SIZE = 640; 27 | 28 | public static final int INSTAGRAM_AVATAR_SIZE = 150; 29 | 30 | public static boolean mPrefetchImages; 31 | 32 | @Override 33 | public void onCreate() { 34 | super.onCreate(); 35 | 36 | mPrefetchImages = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("prefetch", true); 37 | 38 | FastImageLoader 39 | .init(this) 40 | .setDefaultImageServiceAdapter(new ImgIXAdapter()) 41 | .setWriteLogsToLogcat(true) 42 | .setLogLevel(Log.DEBUG) 43 | .setDebugIndicator(true); 44 | 45 | FastImageLoader.buildSpec(Specs.IMG_IX_UNBOUNDED) 46 | .setUnboundDimension() 47 | .setPixelConfig(Bitmap.Config.RGB_565) 48 | .build(); 49 | 50 | FastImageLoader.buildSpec(Specs.IMG_IX_IMAGE) 51 | .setDimensionByDisplay() 52 | .setHeightByResource(R.dimen.image_height) 53 | .setPixelConfig(Bitmap.Config.RGB_565) 54 | .build(); 55 | 56 | IdentityAdapter identityUriEnhancer = new IdentityAdapter(); 57 | FastImageLoader.buildSpec(Specs.INSTA_AVATAR) 58 | .setDimension(INSTAGRAM_AVATAR_SIZE) 59 | .setImageServiceAdapter(identityUriEnhancer) 60 | .build(); 61 | 62 | FastImageLoader.buildSpec(Specs.INSTA_IMAGE) 63 | .setDimension(INSTAGRAM_IMAGE_SIZE) 64 | .setPixelConfig(Bitmap.Config.RGB_565) 65 | .setImageServiceAdapter(identityUriEnhancer) 66 | .build(); 67 | 68 | FastImageLoader.buildSpec(Specs.UNBOUNDED_MAX) 69 | .setUnboundDimension() 70 | .setMaxDensity() 71 | .build(); 72 | } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /demo/src/main/java/com/theartofdev/fastimageloaderdemo/ImgIXFragment.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloaderdemo; 14 | 15 | import android.os.Bundle; 16 | import android.support.v4.app.Fragment; 17 | import android.support.v7.widget.LinearLayoutManager; 18 | import android.support.v7.widget.RecyclerView; 19 | import android.view.LayoutInflater; 20 | import android.view.View; 21 | import android.view.ViewGroup; 22 | 23 | public class ImgIXFragment extends Fragment { 24 | 25 | private Adapter mAdapter; 26 | 27 | public ImgIXFragment() { 28 | } 29 | 30 | @Override 31 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 32 | View view = inflater.inflate(R.layout.fragment_img_ix, container, false); 33 | 34 | RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.recycler_view); 35 | recyclerView.setLayoutManager(new LinearLayoutManager(container.getContext())); 36 | 37 | mAdapter = new Adapter(); 38 | recyclerView.setAdapter(mAdapter); 39 | 40 | return view; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /demo/src/main/java/com/theartofdev/fastimageloaderdemo/MainActivity.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloaderdemo; 14 | 15 | import android.content.SharedPreferences; 16 | import android.os.Bundle; 17 | import android.preference.PreferenceManager; 18 | import android.support.v4.app.Fragment; 19 | import android.support.v4.app.FragmentManager; 20 | import android.support.v4.app.FragmentPagerAdapter; 21 | import android.support.v4.view.ViewPager; 22 | import android.support.v7.app.ActionBarActivity; 23 | import android.view.Menu; 24 | import android.view.MenuItem; 25 | import android.widget.Toast; 26 | 27 | import com.astuetz.PagerSlidingTabStrip; 28 | import com.theartofdev.fastimageloader.FastImageLoader; 29 | import com.theartofdev.fastimageloaderdemo.instagram.InstagramFragment; 30 | 31 | public class MainActivity extends ActionBarActivity { 32 | 33 | @Override 34 | protected void onCreate(Bundle savedInstanceState) { 35 | super.onCreate(savedInstanceState); 36 | setContentView(R.layout.activity_main); 37 | 38 | // Initialize the ViewPager and set an adapter 39 | ViewPager pager = (ViewPager) findViewById(R.id.pager); 40 | pager.setAdapter(new PagerAdapter(getSupportFragmentManager())); 41 | 42 | // Bind the tabs to the ViewPager 43 | PagerSlidingTabStrip tabs = (PagerSlidingTabStrip) findViewById(R.id.pager_tabs); 44 | tabs.setViewPager(pager); 45 | } 46 | 47 | @Override 48 | public boolean onCreateOptionsMenu(Menu menu) { 49 | getMenuInflater().inflate(R.menu.menu_main, menu); 50 | setPrefetchMenuIcon(menu.findItem(R.id.toggle_prefetch)); 51 | return true; 52 | } 53 | 54 | @Override 55 | public boolean onOptionsItemSelected(MenuItem item) { 56 | if (item.getItemId() == R.id.clear_disk_cache) { 57 | FastImageLoader.clearDiskCache(); 58 | return true; 59 | } else if (item.getItemId() == R.id.toggle_prefetch) { 60 | AppApplication.mPrefetchImages = !AppApplication.mPrefetchImages; 61 | 62 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 63 | prefs.edit().putBoolean("prefetch", AppApplication.mPrefetchImages).apply(); 64 | 65 | setPrefetchMenuIcon(item); 66 | Toast.makeText(this, AppApplication.mPrefetchImages ? R.string.toggle_use_prefetch_on : R.string.toggle_use_prefetch_off, Toast.LENGTH_LONG).show(); 67 | return true; 68 | } 69 | return super.onOptionsItemSelected(item); 70 | } 71 | 72 | private void setPrefetchMenuIcon(MenuItem item) { 73 | if (AppApplication.mPrefetchImages) { 74 | item.setIcon(R.drawable.ic_arrow_down_bold_circle_white_24dp); 75 | } else { 76 | item.setIcon(R.drawable.ic_arrow_down_bold_circle_outline_white_24dp); 77 | } 78 | } 79 | 80 | class PagerAdapter extends FragmentPagerAdapter { 81 | 82 | private final String[] TITLES = {"Instagram", "img IX"}; 83 | 84 | public PagerAdapter(FragmentManager fm) { 85 | super(fm); 86 | } 87 | 88 | @Override 89 | public CharSequence getPageTitle(int position) { 90 | return TITLES[position]; 91 | } 92 | 93 | @Override 94 | public int getCount() { 95 | return TITLES.length; 96 | } 97 | 98 | @Override 99 | public Fragment getItem(int position) { 100 | switch (position) { 101 | case 0: 102 | return new InstagramFragment(); 103 | case 1: 104 | return new ImgIXFragment(); 105 | } 106 | return null; 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /demo/src/main/java/com/theartofdev/fastimageloaderdemo/Specs.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloaderdemo; 14 | 15 | /** 16 | * Consts of image loading spec keys. 17 | */ 18 | public final class Specs { 19 | 20 | private Specs() { 21 | } 22 | 23 | public static final String UNBOUNDED_MAX = "UnboundedMax"; 24 | 25 | public static final String IMG_IX_UNBOUNDED = "ImgIXUnbounded"; 26 | 27 | public static final String IMG_IX_IMAGE = "ImgIXImage"; 28 | 29 | public static final String INSTA_AVATAR = "InstagramAvatar"; 30 | 31 | public static final String INSTA_IMAGE = "InstagramImage"; 32 | } 33 | 34 | -------------------------------------------------------------------------------- /demo/src/main/java/com/theartofdev/fastimageloaderdemo/instagram/Adapter.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloaderdemo.instagram; 14 | 15 | import android.support.v7.widget.RecyclerView; 16 | import android.view.ViewGroup; 17 | 18 | import com.theartofdev.fastimageloader.FastImageLoader; 19 | import com.theartofdev.fastimageloaderdemo.AppApplication; 20 | import com.theartofdev.fastimageloaderdemo.Specs; 21 | import com.theartofdev.fastimageloaderdemo.instagram.service.Feed; 22 | import com.theartofdev.fastimageloaderdemo.instagram.service.InstagramService; 23 | import com.theartofdev.fastimageloaderdemo.instagram.service.Item; 24 | 25 | import retrofit.Callback; 26 | import retrofit.RestAdapter; 27 | import retrofit.RetrofitError; 28 | import retrofit.client.Response; 29 | 30 | public final class Adapter extends RecyclerView.Adapter { 31 | 32 | private InstagramService mService; 33 | 34 | private static Item[] mItems = new Item[0]; 35 | 36 | public Adapter() { 37 | RestAdapter restAdapter = new RestAdapter.Builder() 38 | .setEndpoint("https://api.instagram.com/v1") 39 | .build(); 40 | mService = restAdapter.create(InstagramService.class); 41 | } 42 | 43 | @Override 44 | public int getItemCount() { 45 | return mItems.length; 46 | } 47 | 48 | public void loadData(final Callback callback) { 49 | if (mItems.length < 1) { 50 | mService.getFeed(new Callback() { 51 | @Override 52 | public void success(Feed feed, Response response) { 53 | mItems = feed.data; 54 | if (AppApplication.mPrefetchImages) { 55 | for (Item item : mItems) { 56 | FastImageLoader.prefetchImage(item.user.profile_picture, Specs.INSTA_IMAGE); 57 | FastImageLoader.prefetchImage(item.images.standard_resolution.url, Specs.INSTA_IMAGE); 58 | } 59 | } 60 | Adapter.this.notifyDataSetChanged(); 61 | callback.success(feed, response); 62 | } 63 | 64 | @Override 65 | public void failure(RetrofitError error) { 66 | callback.failure(error); 67 | } 68 | }); 69 | } 70 | } 71 | 72 | // Create new views (invoked by the layout manager) 73 | @Override 74 | public Adapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 75 | return new ViewHolder(new ItemView(parent.getContext())); 76 | } 77 | 78 | @Override 79 | public void onBindViewHolder(ViewHolder holder, int position) { 80 | ((ItemView) holder.itemView).setData(mItems[position]); 81 | } 82 | 83 | //region: Inner class: ViewHolder 84 | 85 | /** 86 | * Provide a reference to the views for each data item 87 | * Complex data items may need more than one view per item, and 88 | * you provide access to all the views for a data item in a view holder 89 | */ 90 | static final class ViewHolder extends RecyclerView.ViewHolder { 91 | 92 | public ViewHolder(ItemView v) { 93 | super(v); 94 | } 95 | } 96 | //endregion 97 | } 98 | 99 | -------------------------------------------------------------------------------- /demo/src/main/java/com/theartofdev/fastimageloaderdemo/instagram/InstagramFragment.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloaderdemo.instagram; 14 | 15 | import android.os.Bundle; 16 | import android.support.v4.app.Fragment; 17 | import android.support.v7.widget.LinearLayoutManager; 18 | import android.support.v7.widget.RecyclerView; 19 | import android.view.LayoutInflater; 20 | import android.view.View; 21 | import android.view.ViewGroup; 22 | import android.widget.Toast; 23 | 24 | import com.theartofdev.fastimageloaderdemo.R; 25 | import com.theartofdev.fastimageloaderdemo.instagram.service.Feed; 26 | 27 | import retrofit.Callback; 28 | import retrofit.RetrofitError; 29 | import retrofit.client.Response; 30 | 31 | public class InstagramFragment extends Fragment { 32 | 33 | private Adapter mAdapter; 34 | 35 | public InstagramFragment() { 36 | } 37 | 38 | @Override 39 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 40 | View view = inflater.inflate(R.layout.fragment_img_ix, container, false); 41 | 42 | RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.recycler_view); 43 | recyclerView.setLayoutManager(new LinearLayoutManager(container.getContext())); 44 | 45 | mAdapter = new Adapter(); 46 | recyclerView.setAdapter(mAdapter); 47 | 48 | loadData(); 49 | 50 | return view; 51 | } 52 | 53 | private void loadData() { 54 | mAdapter.loadData(new Callback() { 55 | @Override 56 | public void success(Feed feed, Response response) { 57 | Toast.makeText(InstagramFragment.this.getActivity(), "Loaded " + feed.data.length + " items", Toast.LENGTH_LONG).show(); 58 | } 59 | 60 | @Override 61 | public void failure(RetrofitError error) { 62 | Toast.makeText(InstagramFragment.this.getActivity(), "Failed to load data: " + error.getMessage(), Toast.LENGTH_LONG).show(); 63 | } 64 | }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /demo/src/main/java/com/theartofdev/fastimageloaderdemo/instagram/ItemView.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloaderdemo.instagram; 14 | 15 | import android.app.Activity; 16 | import android.content.Context; 17 | import android.graphics.Point; 18 | import android.view.Display; 19 | import android.view.View; 20 | import android.view.ViewGroup; 21 | import android.view.WindowManager; 22 | import android.widget.ImageView; 23 | import android.widget.TextView; 24 | 25 | import com.theartofdev.fastimageloader.target.TargetAvatarImageView; 26 | import com.theartofdev.fastimageloader.target.TargetImageView; 27 | import com.theartofdev.fastimageloaderdemo.R; 28 | import com.theartofdev.fastimageloaderdemo.Specs; 29 | import com.theartofdev.fastimageloaderdemo.instagram.service.Item; 30 | import com.theartofdev.fastimageloaderdemo.instagram.service.User; 31 | import com.theartofdev.fastimageloaderdemo.zoom.ZoomActivity; 32 | 33 | public final class ItemView extends ViewGroup implements View.OnClickListener { 34 | 35 | private final TargetAvatarImageView mAvatar; 36 | 37 | private final TargetImageView mImage; 38 | 39 | private final TextView mAuthor; 40 | 41 | public ItemView(Context context) { 42 | super(context); 43 | 44 | mAvatar = new TargetAvatarImageView(context); 45 | int size = getResources().getDimensionPixelSize(R.dimen.avatar_size); 46 | mAvatar.setLayoutParams(new LayoutParams(size, size)); 47 | mAvatar.setRounded(true); 48 | addView(mAvatar); 49 | 50 | Point p = new Point(); 51 | Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); 52 | display.getSize(p); 53 | 54 | mImage = new TargetImageView(context); 55 | mImage.setLayoutParams(new LayoutParams(p.x, p.x)); 56 | mImage.setScaleType(ImageView.ScaleType.FIT_CENTER); 57 | mImage.setPlaceholder(getResources().getDrawable(R.drawable.pattern_repeat)); 58 | mImage.setOnClickListener(this); 59 | addView(mImage); 60 | 61 | mAuthor = new TextView(context); 62 | mAuthor.setSingleLine(); 63 | addView(mAuthor); 64 | } 65 | 66 | public void setData(Item item) { 67 | User user = item.user; 68 | String userName = user.full_name != null ? user.full_name : user.username; 69 | mAvatar.loadAvatar(user.profile_picture, userName, Specs.INSTA_AVATAR); 70 | mImage.loadImage(item.images.standard_resolution.url, Specs.INSTA_IMAGE); 71 | mAuthor.setText(userName); 72 | } 73 | 74 | @Override 75 | public void onClick(View v) { 76 | ZoomActivity.startActivity((Activity) getContext(), mImage.getUrl(), Specs.INSTA_IMAGE, null); 77 | } 78 | 79 | @Override 80 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 81 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 82 | 83 | int m = getResources().getDimensionPixelSize(R.dimen.margin); 84 | 85 | measureChild(mAvatar, widthMeasureSpec, heightMeasureSpec); 86 | measureChild(mImage, widthMeasureSpec, heightMeasureSpec); 87 | measureChild(mAuthor, widthMeasureSpec, heightMeasureSpec); 88 | 89 | mAvatar.layout(2 * m, 2 * m, 2 * m + mAvatar.getMeasuredWidth(), 2 * m + mAvatar.getMeasuredHeight()); 90 | mImage.layout(0, 2 * m + mAvatar.getBottom(), getMeasuredWidth(), 2 * m + mAvatar.getBottom() + mImage.getMeasuredHeight()); 91 | 92 | int left = mAvatar.getRight() + 2 * m; 93 | int top = mAvatar.getTop() + (mAvatar.getHeight() - mAuthor.getMeasuredHeight()) / 2 - m; 94 | mAuthor.layout(left, top, left + mAuthor.getMeasuredWidth(), top + mAuthor.getMeasuredHeight()); 95 | 96 | setMeasuredDimension(getMeasuredWidth(), mImage.getBottom() + 2 * m); 97 | } 98 | 99 | @Override 100 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 101 | } 102 | } -------------------------------------------------------------------------------- /demo/src/main/java/com/theartofdev/fastimageloaderdemo/instagram/service/Feed.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloaderdemo.instagram.service; 14 | 15 | public class Feed { 16 | 17 | public Item[] data; 18 | } 19 | 20 | -------------------------------------------------------------------------------- /demo/src/main/java/com/theartofdev/fastimageloaderdemo/instagram/service/Image.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloaderdemo.instagram.service; 14 | 15 | public class Image { 16 | 17 | public String url; 18 | 19 | public Integer width; 20 | 21 | public Integer height; 22 | } 23 | 24 | -------------------------------------------------------------------------------- /demo/src/main/java/com/theartofdev/fastimageloaderdemo/instagram/service/Images.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloaderdemo.instagram.service; 14 | 15 | public class Images { 16 | 17 | public Image low_resolution; 18 | 19 | public Image thumbnail; 20 | 21 | public Image standard_resolution; 22 | } 23 | 24 | -------------------------------------------------------------------------------- /demo/src/main/java/com/theartofdev/fastimageloaderdemo/instagram/service/InstagramService.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloaderdemo.instagram.service; 14 | 15 | import retrofit.Callback; 16 | import retrofit.http.GET; 17 | 18 | /** 19 | * http://instagram.com/developer/endpoints/ 20 | */ 21 | public interface InstagramService { 22 | 23 | /** 24 | * http://instagram.com/developer/endpoints/users/#get_users_feed 25 | */ 26 | @GET("/users/self/feed?access_token=1670815861.1fb234f.b9690c21d125435a8f722856f8043ea2") 27 | public void getFeed(Callback callback); 28 | 29 | } 30 | 31 | -------------------------------------------------------------------------------- /demo/src/main/java/com/theartofdev/fastimageloaderdemo/instagram/service/Item.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloaderdemo.instagram.service; 14 | 15 | public class Item { 16 | 17 | public String link; 18 | 19 | public String createdTime; 20 | 21 | public Images images; 22 | 23 | public String type; 24 | 25 | public String id; 26 | 27 | public User user; 28 | } 29 | 30 | -------------------------------------------------------------------------------- /demo/src/main/java/com/theartofdev/fastimageloaderdemo/instagram/service/User.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloaderdemo.instagram.service; 14 | 15 | public class User { 16 | 17 | public String id; 18 | 19 | public String username; 20 | 21 | public String full_name; 22 | 23 | public String profile_picture; 24 | } -------------------------------------------------------------------------------- /demo/src/main/java/com/theartofdev/fastimageloaderdemo/zoom/ZoomActivity.java: -------------------------------------------------------------------------------- 1 | package com.theartofdev.fastimageloaderdemo.zoom; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.support.v7.app.ActionBarActivity; 7 | import android.widget.ProgressBar; 8 | 9 | import com.theartofdev.fastimageloaderdemo.R; 10 | 11 | public class ZoomActivity extends ActionBarActivity { 12 | 13 | public static void startActivity(Activity activity, String uri, String specKey, String altSpecKey) { 14 | Intent intent = new Intent(activity, ZoomActivity.class); 15 | intent.putExtra("uri", uri); 16 | intent.putExtra("specKey", specKey); 17 | intent.putExtra("altSpecKey", altSpecKey); 18 | activity.startActivity(intent); 19 | } 20 | 21 | @Override 22 | protected void onCreate(Bundle savedInstanceState) { 23 | super.onCreate(savedInstanceState); 24 | setContentView(R.layout.activity_zoom); 25 | 26 | String uri = getIntent().getStringExtra("uri"); 27 | String specKey = getIntent().getStringExtra("specKey"); 28 | String altSpecKey = getIntent().getStringExtra("altSpecKey"); 29 | 30 | ProgressBar progressBar = (ProgressBar) findViewById(R.id.progressBar); 31 | ZoomImageView zoomImageView = (ZoomImageView) findViewById(R.id.zoom_image); 32 | zoomImageView.loadImage(uri, specKey, altSpecKey, progressBar); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /demo/src/main/java/com/theartofdev/fastimageloaderdemo/zoom/ZoomImageView.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloaderdemo.zoom; 14 | 15 | import android.content.Context; 16 | import android.graphics.Canvas; 17 | import android.util.AttributeSet; 18 | import android.widget.ImageView; 19 | import android.widget.ProgressBar; 20 | 21 | import com.theartofdev.fastimageloader.LoadedFrom; 22 | import com.theartofdev.fastimageloader.ReusableBitmap; 23 | import com.theartofdev.fastimageloader.target.TargetHelper; 24 | import com.theartofdev.fastimageloader.target.TargetImageViewHandler; 25 | 26 | import uk.co.senab.photoview.PhotoView; 27 | 28 | public class ZoomImageView extends PhotoView { 29 | 30 | private ProgressBar mProgressBar; 31 | 32 | /** 33 | * The target image handler to load the image and control its lifecycle. 34 | */ 35 | private TargetImageViewHandler mHandler; 36 | 37 | public ZoomImageView(Context context, AttributeSet attrs) { 38 | super(context, attrs); 39 | mHandler = new ZoomTargetImageViewHandler(this); 40 | } 41 | 42 | public ZoomImageView(Context context, AttributeSet attrs, int defStyle) { 43 | super(context, attrs, defStyle); 44 | mHandler = new ZoomTargetImageViewHandler(this); 45 | } 46 | 47 | /** 48 | * Load the given image into the zoom image view. 49 | */ 50 | public void loadImage(String url, String specKey, String altSpecKey, ProgressBar progressBar) { 51 | mProgressBar = progressBar; 52 | mProgressBar.setVisibility(VISIBLE); 53 | mHandler.loadImage(url, specKey, altSpecKey, false); 54 | } 55 | 56 | /** 57 | * On image view visibility change set show/hide on the image handler to it will update its in-use status. 58 | */ 59 | @Override 60 | protected void onWindowVisibilityChanged(int visibility) { 61 | super.onWindowVisibilityChanged(visibility); 62 | mHandler.onViewVisibilityChanged(visibility); 63 | } 64 | 65 | /** 66 | * Override draw to draw download progress indicator. 67 | */ 68 | @Override 69 | public void onDraw(@SuppressWarnings("NullableProblems") Canvas canvas) { 70 | super.onDraw(canvas); 71 | TargetHelper.drawProgressIndicator(canvas, mHandler.getDownloaded(), mHandler.getContentLength()); 72 | } 73 | 74 | private final class ZoomTargetImageViewHandler extends TargetImageViewHandler { 75 | 76 | /** 77 | * @param imageView The image view to handle. 78 | */ 79 | public ZoomTargetImageViewHandler(ImageView imageView) { 80 | super(imageView); 81 | setInvalidateOnDownloading(true); 82 | } 83 | 84 | @Override 85 | protected void setImage(ReusableBitmap bitmap, LoadedFrom from) { 86 | super.setImage(bitmap, from); 87 | if (bitmap.getSpec().getKey().equals(mSpecKey)) { 88 | mProgressBar.setVisibility(GONE); 89 | } 90 | } 91 | } 92 | } 93 | 94 | -------------------------------------------------------------------------------- /demo/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArthurHub/Android-Fast-Image-Loader/b794be15c2ddaf3157bdb27ef9ec32995f3f5266/demo/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-xhdpi/ic_arrow_down_bold_circle_outline_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArthurHub/Android-Fast-Image-Loader/b794be15c2ddaf3157bdb27ef9ec32995f3f5266/demo/src/main/res/drawable-xhdpi/ic_arrow_down_bold_circle_outline_white_24dp.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-xhdpi/ic_arrow_down_bold_circle_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArthurHub/Android-Fast-Image-Loader/b794be15c2ddaf3157bdb27ef9ec32995f3f5266/demo/src/main/res/drawable-xhdpi/ic_arrow_down_bold_circle_white_24dp.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-xhdpi/ic_recycle_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArthurHub/Android-Fast-Image-Loader/b794be15c2ddaf3157bdb27ef9ec32995f3f5266/demo/src/main/res/drawable-xhdpi/ic_recycle_white_24dp.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-xhdpi/pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArthurHub/Android-Fast-Image-Loader/b794be15c2ddaf3157bdb27ef9ec32995f3f5266/demo/src/main/res/drawable-xhdpi/pattern.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable/pattern_repeat.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | 18 | 22 | 23 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/activity_zoom.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 10 | 11 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/fragment_img_ix.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/fragment_instagram.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/item.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 16 | 17 | 26 | 27 | 32 | 33 | -------------------------------------------------------------------------------- /demo/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | 12 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /demo/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 340dp 5 | 40dp 6 | 4dp 7 | 16dp 8 | 16dp 9 | 10 | 11 | -------------------------------------------------------------------------------- /demo/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Fast Image Loader 5 | Clear Disk Cache 6 | Toggle Use Prefetch 7 | Prefetch enabled, all feed images will be downloaded in the background 8 | Prefetch disabled, images will be downloaded only when explicitly requested 9 | 10 | 11 | -------------------------------------------------------------------------------- /demo/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /fastimageloader/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /fastimageloader/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | ext { 4 | PUBLISH_GROUP_ID = 'com.theartofdev' 5 | PUBLISH_ARTIFACT_ID = 'fast-image-loader' 6 | PUBLISH_VERSION = '0.8.4' 7 | // gradlew clean build generateRelease 8 | } 9 | 10 | android { 11 | compileSdkVersion 21 12 | buildToolsVersion "21.1.2" 13 | 14 | defaultConfig { 15 | minSdkVersion 14 16 | targetSdkVersion 21 17 | versionCode 1 18 | versionName PUBLISH_VERSION 19 | } 20 | 21 | lintOptions { 22 | abortOnError false 23 | } 24 | } 25 | 26 | apply from: 'https://raw.githubusercontent.com/ArthurHub/release-android-library/master/android-release-jar.gradle' 27 | 28 | dependencies { 29 | optional 'com.squareup.okhttp:okhttp:2.2.+' 30 | } -------------------------------------------------------------------------------- /fastimageloader/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/Decoder.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader; 14 | 15 | import com.theartofdev.fastimageloader.impl.ImageRequest; 16 | 17 | import java.io.File; 18 | 19 | /** 20 | * TODO:a add doc 21 | */ 22 | public interface Decoder { 23 | 24 | /** 25 | * Load image from disk file on the current thread and set it in the image request object. 26 | */ 27 | void decode(MemoryPool memoryPool, ImageRequest imageRequest, File file, ImageLoadSpec spec); 28 | } 29 | -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/DiskCache.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader; 14 | 15 | import com.theartofdev.fastimageloader.impl.ImageRequest; 16 | 17 | import java.io.File; 18 | 19 | /** 20 | * TODO:a add doc 21 | */ 22 | public interface DiskCache { 23 | 24 | /** 25 | * Gets the representation of the online uri on the local disk.
26 | * Must be unique mapping from URI to file path. 27 | * 28 | * @param uri The online image uri 29 | * @return The path of the file on the disk 30 | */ 31 | File getCacheFile(String uri, ImageLoadSpec spec); 32 | 33 | /** 34 | * Get disk cached image for the given request.
35 | * If the image is NOT in the cache the callback will be executed immediately.
36 | * If the image is in cache an async operation will load the image from disk and then execute the callback. 37 | * 38 | * @param imageRequest the request to load the image from disk for. 39 | * @param decoder Used to decode images from the disk to bitmap. 40 | * @param memoryPool Used to provide reusable bitmaps for image decoding into. 41 | * @param callback The callback to execute on async requests to the cache 42 | */ 43 | void getAsync(ImageRequest imageRequest, ImageLoadSpec altSpec, Decoder decoder, MemoryPool memoryPool, Callback callback); 44 | 45 | /** 46 | * Image added to disk cache, update the disk cache.
47 | * Called when an image was downloaded and now is part of the disk cache, disk cache will update 48 | * its knowledge of the disk cache size, may trigger cleanup of the cache if limit is reached. 49 | */ 50 | void imageAdded(long size); 51 | 52 | /** 53 | * Clear all the cached files async. 54 | */ 55 | void clear(); 56 | 57 | //region: Inner class: Callbacks 58 | 59 | /** 60 | * Callback for getting cached image. 61 | */ 62 | public static interface Callback { 63 | 64 | /** 65 | * Callback for getting cached image, if not cached will have null. 66 | * 67 | * @param canceled if the request was canceled during execution therefor not loading the image 68 | */ 69 | void loadImageDiskCacheCallback(ImageRequest imageRequest, boolean canceled); 70 | } 71 | //endregion 72 | } 73 | -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/Downloader.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader; 14 | 15 | import com.theartofdev.fastimageloader.impl.ImageRequest; 16 | 17 | /** 18 | * TODO:a add doc 19 | */ 20 | public interface Downloader { 21 | 22 | /** 23 | * Download 24 | * 25 | * @param imageRequest the request to download the image for. 26 | * @param prefetch if the request is prefetch or actually required now. 27 | * @param callback The callback to execute on async requests to the downloader 28 | */ 29 | void downloadAsync(ImageRequest imageRequest, boolean prefetch, Callback callback); 30 | 31 | //region: Inner class: Callback 32 | 33 | /** 34 | * Callback for downloading image. 35 | */ 36 | public static interface Callback { 37 | 38 | /** 39 | * Callback for downloading image.
40 | * If the image was downloaded the download flag will be true, it can be false is request 41 | * was canceled during execution or download has failed. download can be true even if 42 | * the request was canceled if more than 50% was download before cancellation. 43 | * 44 | * @param downloaded if the image was downloaded, maybe false if canceled or failed 45 | * @param canceled if the request was canceled during execution therefor not loading the image 46 | */ 47 | public void loadImageDownloaderCallback(ImageRequest imageRequest, boolean downloaded, boolean canceled); 48 | } 49 | //endregion 50 | } 51 | -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/HttpClient.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader; 14 | 15 | import java.io.InputStream; 16 | 17 | /** 18 | * Define a client to be used to download requested images.
19 | * The client must be thread-safe as a single instance will be used for multiple requests 20 | * on multiple threads. 21 | */ 22 | public interface HttpClient { 23 | 24 | /** 25 | * Execute image download for the given URI.
26 | * Invokes the request immediately, and blocks until the response can be processed or is in error. 27 | * 28 | * @param uri the URI of the image to download. 29 | * @return The response of the execution with the result data 30 | */ 31 | HttpResponse execute(String uri); 32 | 33 | /** 34 | * The response returned from client execution. 35 | */ 36 | public interface HttpResponse { 37 | 38 | /** 39 | * The HTTP status code. 40 | */ 41 | int getCode(); 42 | 43 | /** 44 | * The HTTP status message or null if it is unknown. 45 | */ 46 | String getErrorMessage(); 47 | 48 | /** 49 | * The content-length of the response body. 50 | */ 51 | long getContentLength(); 52 | 53 | /** 54 | * Stream of the HTTP response body. 55 | */ 56 | InputStream getBodyStream(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/ImageLoadSpec.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader; 14 | 15 | import android.graphics.Bitmap; 16 | 17 | /** 18 | * The image loading spec data.
19 | *

20 | * equals and hashCode are used to match image request that can reuse bitmaps by spec, so it 21 | * contains only the config that define unique reusable bitmap. 22 | */ 23 | public final class ImageLoadSpec { 24 | 25 | //region: Fields and Consts 26 | 27 | /** 28 | * the unique key of the spec used for identification and debug 29 | */ 30 | private final String mKey; 31 | 32 | /** 33 | * the width of the image in pixels 34 | */ 35 | private final int mWidth; 36 | 37 | /** 38 | * the height of the image in pixels 39 | */ 40 | private final int mHeight; 41 | 42 | /** 43 | * The format of the image. 44 | */ 45 | private final Format mFormat; 46 | 47 | /** 48 | * the pixel configuration to load the image in (4 bytes per image pixel, 2 bytes, etc.) 49 | */ 50 | private final Bitmap.Config mPixelConfig; 51 | 52 | /** 53 | * The URI enhancer to use for this spec image loading 54 | */ 55 | private final ImageServiceAdapter mImageServiceAdapter; 56 | //endregion 57 | 58 | /** 59 | * Init image loading spec. 60 | * 61 | * @param key the unique key of the spec used for identification and debug 62 | * @param width the width of the image in pixels 63 | * @param height the height of the image in pixels 64 | * @param format The format of the image. 65 | * @param pixelConfig the pixel configuration to load the image in (4 bytes per image pixel, 2 bytes, etc.) 66 | * @param imageServiceAdapter The URI enhancer to use for this spec image loading 67 | */ 68 | ImageLoadSpec(String key, int width, int height, Format format, Bitmap.Config pixelConfig, ImageServiceAdapter imageServiceAdapter) { 69 | mKey = key; 70 | mWidth = width; 71 | mHeight = height; 72 | mFormat = format; 73 | mPixelConfig = pixelConfig; 74 | mImageServiceAdapter = imageServiceAdapter; 75 | } 76 | 77 | /** 78 | * the unique key of the spec used for identification and debug 79 | */ 80 | public String getKey() { 81 | return mKey; 82 | } 83 | 84 | /** 85 | * the width of the image in pixels 86 | */ 87 | public int getWidth() { 88 | return mWidth; 89 | } 90 | 91 | /** 92 | * the height of the image in pixels 93 | */ 94 | public int getHeight() { 95 | return mHeight; 96 | } 97 | 98 | /** 99 | * The format of the image. 100 | */ 101 | public Format getFormat() { 102 | return mFormat; 103 | } 104 | 105 | /** 106 | * the pixel configuration to load the image in (4 bytes per image pixel, 2 bytes, etc.) 107 | */ 108 | public Bitmap.Config getPixelConfig() { 109 | return mPixelConfig; 110 | } 111 | 112 | /** 113 | * The URI enhancer to use for this spec image loading 114 | */ 115 | public ImageServiceAdapter getImageServiceAdapter() { 116 | return mImageServiceAdapter; 117 | } 118 | 119 | /** 120 | * Is the spec define specific width and height for the image. 121 | */ 122 | public boolean isSizeBounded() { 123 | return mWidth > 0 && mHeight > 0; 124 | } 125 | 126 | @Override 127 | public String toString() { 128 | return "ImageLoadSpec{" + 129 | "mKey='" + mKey + '\'' + 130 | ", mWidth=" + mWidth + 131 | ", mHeight=" + mHeight + 132 | ", mFormat=" + mFormat + 133 | ", mPixelConfig=" + mPixelConfig + 134 | ", mImageServiceAdapter=" + mImageServiceAdapter + 135 | '}'; 136 | } 137 | 138 | //region: Inner class: Format 139 | 140 | /** 141 | * The format of the image. 142 | */ 143 | public static enum Format { 144 | UNCHANGE, 145 | JPEG, 146 | PNG, 147 | WEBP, 148 | } 149 | //endregion 150 | } -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/ImageLoadSpecBuilder.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader; 14 | 15 | import android.app.Application; 16 | import android.content.Context; 17 | import android.graphics.Bitmap; 18 | import android.graphics.Point; 19 | import android.view.Display; 20 | import android.view.WindowManager; 21 | 22 | import com.theartofdev.fastimageloader.impl.util.FILUtils; 23 | 24 | /** 25 | * Builder for creating {@link com.theartofdev.fastimageloader.ImageLoadSpec} instances. 26 | *

27 | * Defaults:
28 | * Format - JPEG
29 | * Max Density - 1.5
30 | * Pixel Config - ARGB_8888
31 | */ 32 | public final class ImageLoadSpecBuilder { 33 | 34 | //region: Fields and Consts 35 | 36 | /** 37 | * the unique key of the spec used for identification and debug 38 | */ 39 | private String mKey; 40 | 41 | /** 42 | * The application object 43 | */ 44 | private Application mApplication; 45 | 46 | /** 47 | * the width of the image in pixels 48 | */ 49 | private int mWidth = -1; 50 | 51 | /** 52 | * the height of the image in pixels 53 | */ 54 | private int mHeight = -1; 55 | 56 | /** 57 | * the max pixel per inch deviceDensity to load the image in 58 | */ 59 | private float mMaxDensity = 1.5f; 60 | 61 | /** 62 | * The format of the image. 63 | */ 64 | private ImageLoadSpec.Format mFormat = ImageLoadSpec.Format.JPEG; 65 | 66 | /** 67 | * the pixel configuration to load the image in (4 bytes per image pixel, 2 bytes, etc.) 68 | */ 69 | private Bitmap.Config mPixelConfig = Bitmap.Config.ARGB_8888; 70 | 71 | /** 72 | * The URI enhancer to use for this spec image loading 73 | */ 74 | private ImageServiceAdapter mImageServiceAdapter; 75 | //endregion 76 | 77 | /** 78 | * @param key the unique key of the spec used for identification and debug 79 | * @param application The application object 80 | * @param imageServiceAdapter default URI enhancer to use for this spec image loading 81 | */ 82 | ImageLoadSpecBuilder(String key, Application application, ImageServiceAdapter imageServiceAdapter) { 83 | FILUtils.notNullOrEmpty(key, "key"); 84 | FILUtils.notNull(application, "application"); 85 | FILUtils.notNull(imageServiceAdapter, "imageServiceAdapter"); 86 | 87 | mKey = key; 88 | mApplication = application; 89 | mImageServiceAdapter = imageServiceAdapter; 90 | } 91 | 92 | /** 93 | * Get the display size of the device. 94 | */ 95 | public Point getDisplaySize() { 96 | Point p = new Point(); 97 | Display display = ((WindowManager) mApplication.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); 98 | display.getSize(p); 99 | return p; 100 | } 101 | 102 | /** 103 | * The format of the image to download. 104 | */ 105 | public ImageLoadSpecBuilder setFormat(ImageLoadSpec.Format format) { 106 | mFormat = format; 107 | return this; 108 | } 109 | 110 | /** 111 | * the pixel configuration to load the image in (4 bytes per image pixel, 2 bytes, etc.) 112 | */ 113 | public ImageLoadSpecBuilder setPixelConfig(Bitmap.Config pixelConfig) { 114 | mPixelConfig = pixelConfig; 115 | return this; 116 | } 117 | 118 | /** 119 | * the width and height of the image in pixels to the size of the screen. 120 | */ 121 | public ImageLoadSpecBuilder setDimensionByDisplay() { 122 | Point size = getDisplaySize(); 123 | mWidth = size.x; 124 | mHeight = size.y; 125 | return this; 126 | } 127 | 128 | /** 129 | * the width and height of the image to unbound, will be the size of the downloaded image. 130 | */ 131 | public ImageLoadSpecBuilder setUnboundDimension() { 132 | mWidth = 0; 133 | mHeight = 0; 134 | return this; 135 | } 136 | 137 | /** 138 | * the width and height of the image in pixels to the same value (square). 139 | */ 140 | public ImageLoadSpecBuilder setDimension(int size) { 141 | mWidth = size; 142 | mHeight = size; 143 | return this; 144 | } 145 | 146 | /** 147 | * the width and height of the image in pixels.
148 | * to set one dimension and the second to scale set the second to 0. 149 | */ 150 | public ImageLoadSpecBuilder setDimension(int width, int height) { 151 | mWidth = width; 152 | mHeight = height; 153 | return this; 154 | } 155 | 156 | /** 157 | * the width of the image in pixels.
158 | * to set the height to scale set it to 0. 159 | */ 160 | public ImageLoadSpecBuilder setWidth(int width) { 161 | mWidth = width; 162 | return this; 163 | } 164 | 165 | /** 166 | * the height of the image in pixels.
167 | * to set the width to scale set it to 0. 168 | */ 169 | public ImageLoadSpecBuilder setHeight(int height) { 170 | mHeight = height; 171 | return this; 172 | } 173 | 174 | /** 175 | * the width and height of the image in pixels to the same value (square). 176 | */ 177 | public ImageLoadSpecBuilder setDimensionByResource(int resId) { 178 | mWidth = mHeight = mApplication.getResources().getDimensionPixelSize(resId); 179 | return this; 180 | } 181 | 182 | /** 183 | * the width and height of the image in pixels.
184 | * to set one dimension and the second to scale set the second to 0. 185 | */ 186 | public ImageLoadSpecBuilder setDimensionByResource(int widthResId, int heightResId) { 187 | mWidth = mApplication.getResources().getDimensionPixelSize(widthResId); 188 | mHeight = mApplication.getResources().getDimensionPixelSize(heightResId); 189 | return this; 190 | } 191 | 192 | /** 193 | * the width of the image by reading dimension resource by the given key.
194 | * to set the height to scale set it to 0. 195 | */ 196 | public ImageLoadSpecBuilder setWidthByResource(int resId) { 197 | mWidth = mApplication.getResources().getDimensionPixelSize(resId); 198 | return this; 199 | } 200 | 201 | /** 202 | * the height of the image by reading dimension resource by the given key.
203 | * to set the width to scale set it to 0. 204 | */ 205 | public ImageLoadSpecBuilder setHeightByResource(int resId) { 206 | mHeight = mApplication.getResources().getDimensionPixelSize(resId); 207 | return this; 208 | } 209 | 210 | /** 211 | * set the max pixel per inch deviceDensity to the device deviceDensity 212 | */ 213 | public ImageLoadSpecBuilder setMaxDensity() { 214 | mMaxDensity = 9999; 215 | return this; 216 | } 217 | 218 | /** 219 | * the max pixel per inch deviceDensity to load the image in 220 | * 221 | * @throws IllegalArgumentException if value if < 0.5 222 | */ 223 | public ImageLoadSpecBuilder setMaxDensity(float maxDensity) { 224 | if (maxDensity <= 0.5) 225 | throw new IllegalArgumentException("max density must be > .5"); 226 | mMaxDensity = maxDensity; 227 | return this; 228 | } 229 | 230 | /** 231 | * The URI enhancer to use for this spec image loading 232 | */ 233 | public ImageLoadSpecBuilder setImageServiceAdapter(ImageServiceAdapter imageServiceAdapter) { 234 | mImageServiceAdapter = imageServiceAdapter; 235 | return this; 236 | } 237 | 238 | /** 239 | * Create spec by set parameters. 240 | * 241 | * @throws IllegalArgumentException width or height not set correctly. 242 | */ 243 | public ImageLoadSpec build() { 244 | if (mWidth < 0 || mHeight < 0) 245 | throw new IllegalArgumentException("width and height must be set"); 246 | 247 | float deviceDensity = mApplication.getResources().getDisplayMetrics().density; 248 | float densityAdj = deviceDensity >= mMaxDensity ? mMaxDensity / deviceDensity : 1f; 249 | 250 | ImageLoadSpec spec = new ImageLoadSpec(mKey, (int) (mWidth * densityAdj), (int) (mHeight * densityAdj), mFormat, mPixelConfig, mImageServiceAdapter); 251 | 252 | FastImageLoader.addSpec(spec); 253 | 254 | return spec; 255 | } 256 | } -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/ImageServiceAdapter.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader; 14 | 15 | /** 16 | * Define adapter for specific image service (Thumbor/imgIX/Cloudinary/etc.) used in image loading.
17 | * Used to add to the requested image URI the required image loading specification (format/size/etc). 18 | */ 19 | public interface ImageServiceAdapter { 20 | 21 | /** 22 | * Add to raw image loading URI the required specifications (format/size/etc.) parameters. 23 | * 24 | * @param uri the raw image URI to convert 25 | * @param spec the image loading specification to convert by 26 | * @return URI with loading specification 27 | */ 28 | String convert(String uri, ImageLoadSpec spec); 29 | } 30 | -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/LoadState.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader; 14 | 15 | /** 16 | * The possible states of loading image request. 17 | */ 18 | public enum LoadState { 19 | 20 | /** 21 | * No image is set to load or last image was cleared 22 | */ 23 | UNSET, 24 | 25 | /** 26 | * Image requested to load 27 | */ 28 | LOADING, 29 | 30 | /** 31 | * Image finished loading successfully 32 | */ 33 | LOADED, 34 | 35 | /** 36 | * Image failed to load 37 | */ 38 | FAILED 39 | } 40 | 41 | -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/LoadedFrom.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader; 14 | 15 | /** 16 | * Describes where the image was loaded from. 17 | */ 18 | public enum LoadedFrom { 19 | MEMORY, 20 | DISK, 21 | NETWORK 22 | } 23 | 24 | -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/LogAppender.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader; 14 | 15 | /** 16 | * Appender to use to send logs to, allow client to log this library inner logs into custom framework. 17 | */ 18 | public interface LogAppender { 19 | 20 | /** 21 | * Write given log entry. 22 | * 23 | * @param level the log level ({@link android.util.Log#DEBUG}, {@link android.util.Log#INFO}, etc.) 24 | * @param tag the log tag string 25 | * @param message the log message 26 | * @param error optional: the error logged 27 | */ 28 | void log(int level, String tag, String message, Throwable error); 29 | 30 | /** 31 | * Image load operation complete. 32 | * 33 | * @param url the url of the image 34 | * @param specKey the spec of the image load request 35 | * @param from from where the image was loaded (MEMORY/DISK/NETWORK) 36 | * @param successful was the image load successful 37 | * @param time the time in milliseconds it took from request to finish 38 | */ 39 | void imageLoadOperation(String url, String specKey, LoadedFrom from, boolean successful, long time); 40 | 41 | /** 42 | * Image download operation complete. 43 | * 44 | * @param url the url of the image 45 | * @param specKey the spec of the image load request 46 | * @param responseCode the response code of the download web request 47 | * @param time the time in milliseconds it took to download the image 48 | * @param bytes the number of bytes received if download was successful 49 | * @param error optional: if download failed will contain the error 50 | */ 51 | void imageDownloadOperation(String url, String specKey, int responseCode, long time, long bytes, Throwable error); 52 | } 53 | -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/MemoryPool.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader; 14 | 15 | /** 16 | * TODO:a add doc 17 | */ 18 | public interface MemoryPool { 19 | 20 | /** 21 | * Retrieve an image for the specified {@code url} and {@code spec}.
22 | * If not found for primary spec, use the alternative. 23 | */ 24 | ReusableBitmap get(String url, ImageLoadSpec spec, ImageLoadSpec altSpec); 25 | 26 | /** 27 | * Store an image in the cache for the specified {@code key}. 28 | */ 29 | void set(ReusableBitmap bitmap); 30 | 31 | /** 32 | * TODO:a. doc 33 | */ 34 | ReusableBitmap getUnused(ImageLoadSpec spec); 35 | 36 | /** 37 | * TODO:a. doc 38 | */ 39 | void returnUnused(ReusableBitmap bitmap); 40 | 41 | /** 42 | * Clears the cache/pool. 43 | */ 44 | void clear(); 45 | 46 | /** 47 | * Handle trim memory event to release image caches on memory pressure. 48 | */ 49 | void onTrimMemory(int level); 50 | } 51 | -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/ReusableBitmap.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader; 14 | 15 | import android.graphics.Bitmap; 16 | 17 | import com.theartofdev.fastimageloader.impl.util.FILLogger; 18 | import com.theartofdev.fastimageloader.impl.util.FILUtils; 19 | 20 | import java.util.concurrent.atomic.AtomicInteger; 21 | 22 | /** 23 | * Bitmap that can reuse the allocated memory to load a new image into the bitmap. 24 | */ 25 | public class ReusableBitmap { 26 | 27 | //region: Fields and Consts 28 | 29 | /** 30 | * The actual bitmap 31 | */ 32 | protected Bitmap mBitmap; 33 | 34 | /** 35 | * the spec to load the image by 36 | */ 37 | protected final ImageLoadSpec mSpec; 38 | 39 | /** 40 | * The URL of the image loaded in the bitmap, used to know that loaded image changed 41 | */ 42 | protected String mBitmapUrl; 43 | 44 | /** 45 | * Is the bitmap is currently in use 46 | */ 47 | protected AtomicInteger mInUse = new AtomicInteger(); 48 | 49 | /** 50 | * Is the bitmap is currently in use by loading, not real use 51 | */ 52 | protected boolean mInLoadUse; 53 | 54 | /** 55 | * Is the bitmap is has been released. 56 | */ 57 | protected boolean mClosed; 58 | 59 | /** 60 | * The number of times the bitmap has been recycled 61 | */ 62 | protected int mRecycleCount; 63 | //endregion 64 | 65 | /** 66 | * @param bitmap The actual bitmap 67 | * @param spec the spec to load the image by 68 | */ 69 | public ReusableBitmap(Bitmap bitmap, ImageLoadSpec spec) { 70 | FILUtils.notNull(bitmap, "bitmap"); 71 | FILUtils.notNull(spec, "spec"); 72 | mBitmap = bitmap; 73 | mSpec = spec; 74 | } 75 | 76 | /** 77 | * The actual bitmap instance. 78 | */ 79 | public Bitmap getBitmap() { 80 | return mBitmap; 81 | } 82 | 83 | /** 84 | * the URI of the loaded image in the bitmap.
85 | * Used to know if the target requested image has been changed.
86 | */ 87 | public String getUri() { 88 | return mBitmapUrl; 89 | } 90 | 91 | /** 92 | * the URL of the loaded image in the bitmap. 93 | */ 94 | public void setUrl(String url) { 95 | mRecycleCount++; 96 | mBitmapUrl = url; 97 | } 98 | 99 | /** 100 | * the spec the loaded image was loaded by. 101 | */ 102 | public ImageLoadSpec getSpec() { 103 | return mSpec; 104 | } 105 | 106 | /** 107 | * Is the bitmap is currently in use and cannot be reused. 108 | */ 109 | public boolean isInUse() { 110 | return mInUse.get() > 0 || mInLoadUse; 111 | } 112 | 113 | /** 114 | * Increment the bitmap in use count by 1.
115 | * Affects the {@link #isInUse()} to know if the bitmap can be reused.
116 | * Critical to call this method correctly. 117 | */ 118 | public void incrementInUse() { 119 | mInUse.incrementAndGet(); 120 | mInLoadUse = false; 121 | } 122 | 123 | /** 124 | * Decrement the bitmap in use count by 1.
125 | * Affects the {@link #isInUse()} to know if the bitmap can be reused.
126 | * Critical to call this method correctly. 127 | */ 128 | public void decrementInUse() { 129 | mInUse.decrementAndGet(); 130 | } 131 | 132 | /** 133 | * Is the bitmap is currently in use by loading, not real use. 134 | */ 135 | public void setInLoadUse(boolean inLoadUse) { 136 | mInLoadUse = inLoadUse; 137 | } 138 | 139 | /** 140 | * Release the inner bitmap. 141 | */ 142 | public void close() { 143 | FILLogger.debug("Close recycle bitmap [{}]", this); 144 | mClosed = true; 145 | mBitmapUrl = null; 146 | mBitmap.recycle(); 147 | mBitmap = null; 148 | } 149 | 150 | @Override 151 | public String toString() { 152 | return "RecycleBitmap{" + 153 | "hash=" + hashCode() + 154 | ", mSpec='" + mSpec + '\'' + 155 | ", mInUse=" + mInUse.get() + 156 | ", mInLoadUse=" + mInLoadUse + 157 | ", mRecycleCount=" + mRecycleCount + 158 | ", mClosed=" + mClosed + 159 | '}'; 160 | } 161 | } 162 | 163 | -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/Target.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader; 14 | 15 | /** 16 | * Represents an arbitrary listener for image loading.
17 | * Client will receive the raw instance of {@link com.theartofdev.fastimageloader.ReusableBitmap} and will 18 | * be responsible for calling {@link com.theartofdev.fastimageloader.ReusableBitmap#incrementInUse()} and 19 | * {@link com.theartofdev.fastimageloader.ReusableBitmap#decrementInUse()} correctly. 20 | *

21 | * Instances of this interface will used to determine the image to load by {@link #getUri()} and the 22 | * specification to load the image by {@link #getSpecKey()}.
23 | * Those methods will also be used to cancel image load request if the returned value of 24 | * {@link #getUri()} has been changed or nullified. 25 | *

26 | * Note: Prefer using {@link com.theartofdev.fastimageloader.target.TargetImageViewHandler}, it 27 | * implements most of the required functionality. 28 | */ 29 | public interface Target { 30 | 31 | /** 32 | * The URI source of the image 33 | */ 34 | String getUri(); 35 | 36 | /** 37 | * the spec to load the image by 38 | */ 39 | String getSpecKey(); 40 | 41 | /** 42 | * Callback when image is been downloaded to show progress. 43 | * 44 | * @param downloaded the number of bytes already downloaded 45 | * @param contentLength the total number of bytes to download 46 | */ 47 | void onBitmapDownloading(long downloaded, long contentLength); 48 | 49 | /** 50 | * Callback when an image has been successfully loaded.
51 | * Note: You must not recycle the bitmap. 52 | */ 53 | void onBitmapLoaded(ReusableBitmap bitmap, LoadedFrom from); 54 | 55 | /** 56 | * Callback indicating the image could not be successfully loaded.
57 | */ 58 | void onBitmapFailed(); 59 | } 60 | 61 | -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/adapter/IdentityAdapter.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader.adapter; 14 | 15 | import com.theartofdev.fastimageloader.ImageLoadSpec; 16 | import com.theartofdev.fastimageloader.ImageServiceAdapter; 17 | 18 | /** 19 | * Doesn't change the URI. 20 | */ 21 | public class IdentityAdapter implements ImageServiceAdapter { 22 | 23 | @Override 24 | public String convert(String uri, ImageLoadSpec spec) { 25 | return uri; 26 | } 27 | } -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/adapter/ImgIXAdapter.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader.adapter; 14 | 15 | import com.theartofdev.fastimageloader.ImageLoadSpec; 16 | import com.theartofdev.fastimageloader.ImageServiceAdapter; 17 | 18 | /** 19 | * imgIX image service (http://www.imgix.com) adapter.
20 | */ 21 | public class ImgIXAdapter implements ImageServiceAdapter { 22 | 23 | @Override 24 | public String convert(String uri, ImageLoadSpec spec) { 25 | StringBuilder sb = new StringBuilder(uri); 26 | 27 | int qIdx = uri.indexOf('?'); 28 | sb.append(qIdx > -1 ? '&' : '?'); 29 | 30 | if (spec.getFormat() == ImageLoadSpec.Format.JPEG) 31 | sb.append("auto=jpeg&"); 32 | else if (spec.getFormat() == ImageLoadSpec.Format.PNG) 33 | sb.append("auto=png&"); 34 | else if (spec.getFormat() == ImageLoadSpec.Format.WEBP) 35 | sb.append("auto=webp&"); 36 | 37 | sb.append("fit=crop&"); 38 | 39 | if (spec.getWidth() > 0) { 40 | sb.append("w=").append(spec.getWidth()).append('&'); 41 | } 42 | if (spec.getHeight() > 0) { 43 | sb.append("h=").append(spec.getHeight()).append('&'); 44 | } 45 | 46 | return sb.toString().substring(0, sb.length() - 1); 47 | } 48 | } -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/adapter/ThumborAdapter.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader.adapter; 14 | 15 | import com.theartofdev.fastimageloader.ImageLoadSpec; 16 | import com.theartofdev.fastimageloader.ImageServiceAdapter; 17 | import com.theartofdev.fastimageloader.impl.util.FILUtils; 18 | 19 | /** 20 | * thumbor image service (http://thumbor.org/) adapter.
21 | * Add image load specification into the path of the image URL.
22 | * Using Thumbor service URI to build new URI with the image URI as suffix. 23 | */ 24 | public class ThumborAdapter implements ImageServiceAdapter { 25 | 26 | //region: Fields and Consts 27 | 28 | /** 29 | * the thumbor base URI 30 | */ 31 | private final String mBaseUri; 32 | //endregion 33 | 34 | protected ThumborAdapter() { 35 | mBaseUri = null; 36 | } 37 | 38 | /** 39 | * @param baseUri the thumbor base URI 40 | */ 41 | public ThumborAdapter(String baseUri) { 42 | FILUtils.notNullOrEmpty(baseUri, "baseUri"); 43 | 44 | baseUri = baseUri.trim(); 45 | if (baseUri.endsWith("/")) 46 | baseUri = baseUri.substring(0, baseUri.length() - 2); 47 | mBaseUri = baseUri; 48 | } 49 | 50 | @Override 51 | public String convert(String uri, ImageLoadSpec spec) { 52 | return createUri(mBaseUri, uri, spec); 53 | } 54 | 55 | /** 56 | * Create thumbor URI from thumbor and image parts for the given spec. 57 | */ 58 | protected String createUri(String thumborPart, String imagePort, ImageLoadSpec spec) { 59 | StringBuilder sb = new StringBuilder(); 60 | 61 | sb.append(thumborPart); 62 | sb.append("/unsafe"); 63 | 64 | if (spec.getWidth() > 0 || spec.getHeight() > 0) { 65 | sb.append("/").append(spec.getWidth()).append("x").append(spec.getHeight()); 66 | } 67 | 68 | sb.append("/filters:fill(fff,true)"); 69 | 70 | if (spec.getFormat() == ImageLoadSpec.Format.JPEG) 71 | sb.append(":format(jpeg)"); 72 | else if (spec.getFormat() == ImageLoadSpec.Format.PNG) 73 | sb.append(":format(png)"); 74 | else if (spec.getFormat() == ImageLoadSpec.Format.WEBP) 75 | sb.append(":format(webp)"); 76 | 77 | sb.append("/").append(imagePort); 78 | 79 | return sb.toString(); 80 | } 81 | } -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/adapter/ThumborInlineAdapter.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader.adapter; 14 | 15 | import com.theartofdev.fastimageloader.ImageLoadSpec; 16 | import com.theartofdev.fastimageloader.impl.util.FILUtils; 17 | 18 | /** 19 | * thumbor image service (http://thumbor.org/) adapter.
20 | * Add image load specification into the path of the image URL.
21 | * The image URI is already Thumbor URI, add Thumbor parameters in the middle of the URI. 22 | */ 23 | public class ThumborInlineAdapter extends ThumborAdapter { 24 | 25 | /** 26 | * the path part that split the thumbor URI part from image part. 27 | */ 28 | private final String mPathPartSplit; 29 | 30 | /** 31 | * @param pathPartSplit the path part that split the thumbor URI part from image part. 32 | */ 33 | public ThumborInlineAdapter(String pathPartSplit) { 34 | FILUtils.notNullOrEmpty(pathPartSplit, "pathPartSplit"); 35 | mPathPartSplit = pathPartSplit; 36 | } 37 | 38 | @Override 39 | public String convert(String uri, ImageLoadSpec spec) { 40 | int idx = uri.indexOf(mPathPartSplit); 41 | if (idx > -1) { 42 | String thumborPart = uri.substring(0, idx); 43 | String imagePart = uri.substring(idx + mPathPartSplit.length()); 44 | return createUri(thumborPart, imagePart, spec); 45 | } 46 | return uri; 47 | } 48 | } -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/impl/DecoderImpl.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader.impl; 14 | 15 | import android.graphics.Bitmap; 16 | import android.graphics.BitmapFactory; 17 | 18 | import com.theartofdev.fastimageloader.Decoder; 19 | import com.theartofdev.fastimageloader.ImageLoadSpec; 20 | import com.theartofdev.fastimageloader.MemoryPool; 21 | import com.theartofdev.fastimageloader.ReusableBitmap; 22 | import com.theartofdev.fastimageloader.impl.util.FILLogger; 23 | 24 | import java.io.File; 25 | 26 | /** 27 | * Handler for decoding image object from image File.
28 | */ 29 | public class DecoderImpl implements Decoder { 30 | 31 | //region: Fields and Consts 32 | 33 | /** 34 | * Used to reuse bitmaps on image loading from disk. 35 | */ 36 | private final BitmapFactory.Options[] mOptions = new BitmapFactory.Options[2]; 37 | //endregion 38 | 39 | @Override 40 | public void decode(MemoryPool memoryPool, ImageRequest imageRequest, File file, ImageLoadSpec spec) { 41 | ReusableBitmap poolBitmap = memoryPool.getUnused(spec); 42 | 43 | FILLogger.debug("Decode image from disk... [{}] [{}]", imageRequest, poolBitmap); 44 | ReusableBitmap decodedBitmap = decode(file, spec, poolBitmap); 45 | 46 | if (decodedBitmap != null) { 47 | imageRequest.setBitmap(decodedBitmap); 48 | } 49 | 50 | if (poolBitmap != null && poolBitmap != decodedBitmap) { 51 | memoryPool.returnUnused(poolBitmap); 52 | } 53 | } 54 | 55 | /** 56 | * Load image from disk file on the current thread and set it in the image request object. 57 | */ 58 | protected ReusableBitmap decode(File file, ImageLoadSpec spec, ReusableBitmap poolBitmap) { 59 | 60 | BitmapFactory.Options options = getOptions(); 61 | try { 62 | options.inBitmap = poolBitmap != null ? poolBitmap.getBitmap() : null; 63 | options.inPreferredConfig = spec.getPixelConfig(); 64 | 65 | Bitmap rawBitmap = BitmapFactory.decodeFile(file.getAbsolutePath(), options); 66 | if (rawBitmap != null) { 67 | if (poolBitmap != null && poolBitmap.getBitmap() == rawBitmap) { 68 | // successful load of image into reusable bitmap 69 | return poolBitmap; 70 | } 71 | 72 | FILLogger.debug("Create new reusable bitmap... [{}]", spec); 73 | return new ReusableBitmap(rawBitmap, spec); 74 | } else { 75 | FILLogger.critical("Failed to load image from cache [{}] [{}] [{}]", file, spec, poolBitmap); 76 | } 77 | } catch (Throwable e) { 78 | FILLogger.warn("Failed to load disk cached image [{}] [{}] [{}]", e, file, spec, poolBitmap); 79 | } finally { 80 | returnOptions(options); 81 | } 82 | if (poolBitmap != null) { 83 | FILLogger.warn("Retry image decode without pool bitmap... [{}] [{}]", file, spec); 84 | return decode(file, spec, null); 85 | } 86 | return null; 87 | } 88 | 89 | /** 90 | * Get options to be used for decoding, use existing if possible. 91 | */ 92 | private BitmapFactory.Options getOptions() { 93 | BitmapFactory.Options options = null; 94 | synchronized (mOptions) { 95 | for (int i = 0; i < mOptions.length; i++) { 96 | if (mOptions[i] != null) { 97 | options = mOptions[i]; 98 | mOptions[i] = null; 99 | break; 100 | } 101 | } 102 | } 103 | if (options == null) { 104 | options = new BitmapFactory.Options(); 105 | options.inSampleSize = 1; 106 | options.inMutable = true; 107 | options.inTempStorage = new byte[16 * 1024]; 108 | } 109 | return options; 110 | } 111 | 112 | /** 113 | * Return options to be used for decoding to reuse object. 114 | */ 115 | private void returnOptions(BitmapFactory.Options options) { 116 | if (options != null) { 117 | options.inBitmap = null; 118 | synchronized (mOptions) { 119 | for (int i = 0; i < mOptions.length; i++) { 120 | if (mOptions[i] == null) { 121 | mOptions[i] = options; 122 | break; 123 | } 124 | } 125 | } 126 | } 127 | } 128 | } 129 | 130 | -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/impl/DiskCacheImpl.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader.impl; 14 | 15 | import android.content.Context; 16 | import android.content.SharedPreferences; 17 | import android.preference.PreferenceManager; 18 | import android.text.format.DateUtils; 19 | 20 | import com.theartofdev.fastimageloader.Decoder; 21 | import com.theartofdev.fastimageloader.ImageLoadSpec; 22 | import com.theartofdev.fastimageloader.MemoryPool; 23 | import com.theartofdev.fastimageloader.impl.util.FILLogger; 24 | import com.theartofdev.fastimageloader.impl.util.FILUtils; 25 | 26 | import java.io.File; 27 | import java.text.NumberFormat; 28 | import java.util.Arrays; 29 | import java.util.Comparator; 30 | import java.util.concurrent.LinkedBlockingQueue; 31 | import java.util.concurrent.ThreadPoolExecutor; 32 | import java.util.concurrent.TimeUnit; 33 | 34 | /** 35 | * Disk cache for image handler.
36 | */ 37 | public class DiskCacheImpl implements com.theartofdev.fastimageloader.DiskCache { 38 | 39 | //region: Fields and Consts 40 | 41 | /** 42 | * The key to persist stats last scan data 43 | */ 44 | protected static final String STATS_LAST_SCAN = "DiskImageCache_lastCheck"; 45 | 46 | /** 47 | * The key to persist stats cache size data 48 | */ 49 | protected static final String STATS_CACHE_SIZE = "DiskImageCache_size"; 50 | 51 | /** 52 | * The max size of the cache (50MB) 53 | */ 54 | protected final long mMaxSize; 55 | 56 | /** 57 | * The bound to delete cached data to this size 58 | */ 59 | protected final long mMaxSizeLowerBound; 60 | 61 | /** 62 | * The max time image is cached without use before delete 63 | */ 64 | protected final long mCacheTtl; 65 | 66 | /** 67 | * The interval to execute cache scan even when max size has not reached 68 | */ 69 | protected final long SCAN_INTERVAL = 2 * DateUtils.DAY_IN_MILLIS; 70 | 71 | /** 72 | * the folder that the image cached on disk are located 73 | */ 74 | protected final File mCacheFolder; 75 | 76 | /** 77 | * Application context 78 | */ 79 | protected final Context mContext; 80 | 81 | /** 82 | * Threads service for all read operations. 83 | */ 84 | protected final ThreadPoolExecutor mReadExecutorService; 85 | 86 | /** 87 | * Threads service for scan of cached folder operation. 88 | */ 89 | protected final ThreadPoolExecutor mScanExecutorService; 90 | 91 | /** 92 | * The time of the last cache check 93 | */ 94 | private long mLastCacheScanTime = -1; 95 | 96 | /** 97 | * the current size of the cache 98 | */ 99 | private long mCurrentCacheSize; 100 | //endregion 101 | 102 | /** 103 | * @param context the application object to read config stuff 104 | * @param cacheFolder the folder to keep the cached image data 105 | * @param maxSize the max size of the disk cache in bytes 106 | * @param cacheTtl the max time a cached image remains in cache without use before deletion 107 | */ 108 | public DiskCacheImpl(Context context, File cacheFolder, long maxSize, long cacheTtl) { 109 | FILUtils.notNull(context, "context"); 110 | FILUtils.notNull(cacheFolder, "cacheFolder"); 111 | 112 | mContext = context; 113 | mCacheFolder = cacheFolder; 114 | mMaxSize = maxSize; 115 | mCacheTtl = cacheTtl; 116 | mMaxSizeLowerBound = (long) (mMaxSize * .8); 117 | 118 | //noinspection ResultOfMethodCallIgnored 119 | mCacheFolder.mkdirs(); 120 | 121 | mReadExecutorService = new ThreadPoolExecutor(0, 1, 60, TimeUnit.SECONDS, 122 | new LinkedBlockingQueue(), FILUtils.threadFactory("ImageCacheRead", true)); 123 | 124 | mScanExecutorService = new ThreadPoolExecutor(0, 1, 10, TimeUnit.SECONDS, 125 | new LinkedBlockingQueue(), FILUtils.threadFactory("ImageCacheScan", true)); 126 | } 127 | 128 | @Override 129 | public File getCacheFile(String uri, ImageLoadSpec spec) { 130 | int lastSlash = uri.lastIndexOf('/'); 131 | if (lastSlash == -1) { 132 | return null; 133 | } 134 | String name = FILUtils.format("{}_{}_{}_{}", 135 | Integer.toHexString(uri.substring(0, lastSlash).hashCode()), 136 | Integer.toHexString(uri.substring(lastSlash + 1).hashCode()), 137 | uri.substring(Math.max(lastSlash + 1, uri.length() - 10)), 138 | spec.getKey()); 139 | return new File(FILUtils.pathCombine(mCacheFolder.getAbsolutePath(), name)); 140 | } 141 | 142 | @Override 143 | public void getAsync(final ImageRequest imageRequest, 144 | final ImageLoadSpec altSpec, 145 | final Decoder decoder, 146 | final MemoryPool memoryPool, 147 | final Callback callback) { 148 | 149 | File altFile = null; 150 | boolean exists = imageRequest.getFile().exists(); 151 | if (!exists && altSpec != null) { 152 | // if primary spec file doesn't exist in cache but alternative does, load it 153 | altFile = getCacheFile(imageRequest.getUri(), altSpec); 154 | } 155 | 156 | if (exists || (altFile != null && altFile.exists())) { 157 | // use the primary or the alternative file and spec to decode the image 158 | final File file = exists ? imageRequest.getFile() : altFile; 159 | final ImageLoadSpec spec = exists ? imageRequest.getSpec() : altSpec; 160 | mReadExecutorService.execute(new Runnable() { 161 | @Override 162 | public void run() { 163 | loadImageFromCache(imageRequest, file, spec, decoder, memoryPool, callback); 164 | } 165 | }); 166 | } else { 167 | callback.loadImageDiskCacheCallback(imageRequest, false); 168 | } 169 | } 170 | 171 | @Override 172 | public void imageAdded(long size) { 173 | mCurrentCacheSize += size; 174 | if (mLastCacheScanTime < 1 || mLastCacheScanTime + SCAN_INTERVAL < System.currentTimeMillis() || mCurrentCacheSize > mMaxSize) { 175 | mScanExecutorService.execute(new Runnable() { 176 | @Override 177 | public void run() { 178 | scanCache(); 179 | } 180 | }); 181 | } 182 | } 183 | 184 | @Override 185 | public void clear() { 186 | mReadExecutorService.execute(new Runnable() { 187 | @Override 188 | public void run() { 189 | clearCache(); 190 | } 191 | }); 192 | } 193 | 194 | /** 195 | * Populate the given string builder with report on cache status. 196 | */ 197 | public void report(StringBuilder sb) { 198 | sb.append("Disk Cache: "); 199 | if (mLastCacheScanTime > 0) { 200 | sb.append("Size: ").append(NumberFormat.getInstance().format(mCurrentCacheSize / 1024)).append("K\n"); 201 | sb.append("Since Last Scan: ").append(NumberFormat.getInstance().format((System.currentTimeMillis() - mLastCacheScanTime) / 1000 / 60)).append(" Minutes\n"); 202 | } else { 203 | sb.append("Not scanned"); 204 | } 205 | } 206 | 207 | @Override 208 | public String toString() { 209 | return "ImageDiskCache{" + 210 | "mLastCacheScanTime=" + mLastCacheScanTime + 211 | ", mCurrentCacheSize=" + mCurrentCacheSize + 212 | '}'; 213 | } 214 | 215 | //region: Private methods 216 | 217 | /** 218 | * Load the given cached image file into reusable bitmap, post result on given callback.
219 | * This method is executed on a dedicated separate thread. 220 | */ 221 | protected void loadImageFromCache(ImageRequest imageRequest, 222 | File file, 223 | ImageLoadSpec spec, 224 | Decoder decoder, 225 | MemoryPool memoryPool, 226 | Callback callback) { 227 | 228 | final boolean canceled = !imageRequest.isValid(); 229 | if (!canceled) { 230 | //noinspection ResultOfMethodCallIgnored 231 | file.setLastModified(System.currentTimeMillis()); 232 | decoder.decode(memoryPool, imageRequest, file, spec); 233 | } 234 | callback.loadImageDiskCacheCallback(imageRequest, canceled); 235 | } 236 | 237 | /** 238 | * Iterate over all the cached image files to delete LRU images. 239 | */ 240 | protected void scanCache() { 241 | try { 242 | if (mLastCacheScanTime < 1) { 243 | loadStats(); 244 | } 245 | if (mLastCacheScanTime + SCAN_INTERVAL < System.currentTimeMillis() || mCurrentCacheSize > mMaxSize) { 246 | long startTime = System.currentTimeMillis(); 247 | try { 248 | long totalSize = 0; 249 | long totalSizeFull = 0; 250 | int deleteByTTL = 0; 251 | int deleteByMaxSize = 0; 252 | 253 | // iterate over all cached files, delete stale and calculate current cache size 254 | File[] allImages = mCacheFolder.listFiles(); 255 | for (int i = 0; i < allImages.length; i++) { 256 | long fileSize = allImages[i].length(); 257 | totalSizeFull += fileSize; 258 | if (allImages[i].lastModified() + mCacheTtl < System.currentTimeMillis()) { 259 | if (allImages[i].delete()) { 260 | deleteByTTL++; 261 | allImages[i] = null; 262 | } 263 | } else { 264 | totalSize += fileSize; 265 | } 266 | } 267 | 268 | // if cache max size reached, need to delete LRU images 269 | if (totalSize > mMaxSize) { 270 | 271 | // sort all cached files by last access date 272 | Arrays.sort(allImages, new Comparator() { 273 | @Override 274 | public int compare(File lhs, File rhs) { 275 | long l = lhs != null ? lhs.lastModified() : 0; 276 | long r = rhs != null ? rhs.lastModified() : 0; 277 | return l < r ? -1 : (l == r ? 0 : 1); 278 | } 279 | }); 280 | 281 | // delete cached images until cache size is reduced to 90% of max 282 | for (int i = 0; i < allImages.length && totalSize > mMaxSizeLowerBound; i++) { 283 | if (allImages[i] != null) { 284 | long length = allImages[i].length(); 285 | if (allImages[i].delete()) { 286 | deleteByMaxSize++; 287 | totalSize -= length; 288 | allImages[i] = null; 289 | } 290 | } 291 | } 292 | } 293 | 294 | mLastCacheScanTime = System.currentTimeMillis(); 295 | mCurrentCacheSize = totalSize; 296 | 297 | saveStats(); 298 | 299 | FILLogger.info("Image disk cache scan complete [Before: {} / {}K] [After: {} / {}K] [Delete TTL: {}] [Delete size: {}]", 300 | allImages.length, totalSizeFull / 1024, allImages.length - deleteByTTL - deleteByMaxSize, totalSize / 1024, deleteByTTL, deleteByMaxSize); 301 | } finally { 302 | FILLogger.info("ImageCacheScan [{}]", System.currentTimeMillis() - startTime); 303 | } 304 | } 305 | } catch (Exception e) { 306 | FILLogger.critical("Error in image disk cache scan", e); 307 | } 308 | } 309 | 310 | /** 311 | * Delete all cached images and update the cache scan data. 312 | */ 313 | protected void clearCache() { 314 | String[] list = mCacheFolder.list(); 315 | for (String filePath : list) { 316 | try { 317 | File file = new File(FILUtils.pathCombine(mCacheFolder.getAbsolutePath(), filePath)); 318 | //noinspection ResultOfMethodCallIgnored 319 | file.delete(); 320 | } catch (Exception e) { 321 | FILLogger.warn("Failed to delete disk cached image", e); 322 | } 323 | } 324 | mCurrentCacheSize = 0; 325 | mLastCacheScanTime = System.currentTimeMillis(); 326 | saveStats(); 327 | } 328 | 329 | /** 330 | * Load stats used for cache operation: last cache scan, total cache size.
331 | * The states are persisted so cache scan won't happen unless really required. 332 | */ 333 | protected void loadStats() { 334 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); 335 | mLastCacheScanTime = prefs.getLong(STATS_LAST_SCAN, 0); 336 | mCurrentCacheSize += prefs.getLong(STATS_CACHE_SIZE, 0); 337 | } 338 | 339 | /** 340 | * Save stats used for cache operation: last cache scan, total cache size.
341 | * The states are persisted so cache scan won't happen unless really required. 342 | */ 343 | protected void saveStats() { 344 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); 345 | SharedPreferences.Editor editor = prefs.edit(); 346 | editor.putLong(STATS_LAST_SCAN, mLastCacheScanTime); 347 | editor.putLong(STATS_CACHE_SIZE, mCurrentCacheSize); 348 | editor.apply(); 349 | } 350 | //endregion 351 | } 352 | 353 | -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/impl/DownloaderImpl.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader.impl; 14 | 15 | import com.theartofdev.fastimageloader.HttpClient; 16 | import com.theartofdev.fastimageloader.impl.util.FILLogger; 17 | import com.theartofdev.fastimageloader.impl.util.FILUtils; 18 | 19 | import java.io.File; 20 | import java.io.FileOutputStream; 21 | import java.io.IOException; 22 | import java.io.InputStream; 23 | import java.io.OutputStream; 24 | import java.net.ConnectException; 25 | import java.util.concurrent.Executor; 26 | import java.util.concurrent.LinkedBlockingQueue; 27 | import java.util.concurrent.ThreadPoolExecutor; 28 | import java.util.concurrent.TimeUnit; 29 | 30 | /** 31 | * TODO:a add doc 32 | */ 33 | public final class DownloaderImpl implements com.theartofdev.fastimageloader.Downloader { 34 | 35 | //region: Fields and Consts 36 | 37 | /** 38 | * The HTTP client used to execute download image requests 39 | */ 40 | private final HttpClient mClient; 41 | 42 | /** 43 | * Threads service for download operations. 44 | */ 45 | private final ThreadPoolExecutor mExecutor; 46 | 47 | /** 48 | * Threads service for pre-fetch download operations. 49 | */ 50 | private final ThreadPoolExecutor mPrefetchExecutor; 51 | 52 | /** 53 | * the buffers used to download image 54 | */ 55 | private final byte[][] mBuffers; 56 | 57 | private int mExecutorThreads = 2; 58 | //endregion 59 | 60 | /** 61 | * @param client the OkHttp client to use to download the images. 62 | */ 63 | public DownloaderImpl(HttpClient client) { 64 | FILUtils.notNull(client, "client"); 65 | 66 | mClient = client; 67 | 68 | mBuffers = new byte[mExecutorThreads + 1][]; 69 | 70 | mExecutor = new ThreadPoolExecutor(mExecutorThreads, mExecutorThreads, 30, TimeUnit.SECONDS, 71 | new LinkedBlockingQueue(), FILUtils.threadFactory("ImageDownloader", true)); 72 | mExecutor.allowCoreThreadTimeOut(true); 73 | 74 | mPrefetchExecutor = new ThreadPoolExecutor(1, 1, 30, TimeUnit.SECONDS, 75 | new LinkedBlockingQueue(), FILUtils.threadFactory("ImagePrefetchDownloader", true)); 76 | mPrefetchExecutor.allowCoreThreadTimeOut(true); 77 | } 78 | 79 | @Override 80 | public void downloadAsync(final ImageRequest imageRequest, final boolean prefetch, final Callback callback) { 81 | Executor executor = prefetch ? mPrefetchExecutor : mExecutor; 82 | executor.execute(new Runnable() { 83 | @Override 84 | public void run() { 85 | handleExecutorDownload(imageRequest, prefetch, callback); 86 | 87 | } 88 | }); 89 | } 90 | 91 | //region: Private methods 92 | 93 | /** 94 | * Handle starting download execution on executor worker thread.
95 | * Request can be executed twice, on prefetch and regular executor so it must safeguard 96 | * from handling the same request twice.
97 | * Raise the given callback when download return with flags if the request was downloaded/canceled in 98 | * any combination possible. 99 | */ 100 | protected void handleExecutorDownload(ImageRequest imageRequest, boolean prefetch, Callback callback) { 101 | // mark start download, the first to do this will win (sync between prefetch and load) 102 | if ((prefetch || !imageRequest.isPrefetch()) && imageRequest.startDownload()) { 103 | FILLogger.debug("Start image request download... [{}]", imageRequest); 104 | boolean canceled = downloadByClient(imageRequest); 105 | boolean downloaded = imageRequest.getFileSize() > 0; 106 | callback.loadImageDownloaderCallback(imageRequest, downloaded, canceled); 107 | } else { 108 | FILLogger.debug("Image request download already handled [{}]", imageRequest); 109 | } 110 | } 111 | 112 | /** 113 | * Execute client request to download the file, if valid response is returned use 114 | * {@link #downloadToFile(ImageRequest, com.theartofdev.fastimageloader.HttpClient.HttpResponse)} 115 | * to download the image from response body. 116 | * 117 | * @return true - download was canceled before finishing, false - otherwise. 118 | */ 119 | protected boolean downloadByClient(ImageRequest imageRequest) { 120 | int responseCode = 0; 121 | Exception error = null; 122 | boolean canceled = false; 123 | long start = System.currentTimeMillis(); 124 | try { 125 | canceled = !imageRequest.isValid(); 126 | if (!canceled) { 127 | 128 | // start image download request 129 | HttpClient.HttpResponse httpResponse = mClient.execute(imageRequest.getEnhancedUri()); 130 | 131 | // check handshake 132 | responseCode = httpResponse.getCode(); 133 | if (responseCode < 300) { 134 | canceled = !imageRequest.isValid(); 135 | if (!canceled) { 136 | // download data 137 | canceled = downloadToFile(imageRequest, httpResponse); 138 | } 139 | } else { 140 | error = new ConnectException(httpResponse.getCode() + ": " + httpResponse.getErrorMessage()); 141 | FILLogger.error("Failed to download image... [{}] [{}] [{}]", httpResponse.getCode(), httpResponse.getErrorMessage(), imageRequest); 142 | } 143 | } 144 | } catch (Exception e) { 145 | error = e; 146 | FILLogger.error("Failed to download image [{}]", e, imageRequest); 147 | } 148 | 149 | // if downloaded or error occurred - report operation, don't report cancelled 150 | if (imageRequest.getFileSize() > 0 || error != null) { 151 | FILLogger.operation(imageRequest.getEnhancedUri(), imageRequest.getSpec().getKey(), responseCode, System.currentTimeMillis() - start, imageRequest.getFileSize(), error); 152 | } 153 | 154 | return canceled; 155 | } 156 | 157 | /** 158 | * Download image data from the given web response.
159 | * Download to temp file so if error occurred it won't result in corrupted cached file and handle 160 | * smart cancelling, if request is no longer valid but more than 50% has been downloaded, finish it but 161 | * don't load the image object. 162 | * 163 | * @return true - download was canceled before finishing, false - otherwise. 164 | */ 165 | protected boolean downloadToFile(ImageRequest imageRequest, HttpClient.HttpResponse response) throws IOException { 166 | byte[] buffer = null; 167 | InputStream in = null; 168 | OutputStream out = null; 169 | boolean canceled = false; 170 | File tmpFile = new File(imageRequest.getFile().getAbsolutePath() + "_tmp"); 171 | try { 172 | in = response.getBodyStream(); 173 | out = new FileOutputStream(tmpFile); 174 | 175 | int len = 0; 176 | int size = 0; 177 | buffer = getBuffer(); 178 | 179 | // don't cancel download if passed 50% 180 | long contentLength = response.getContentLength(); 181 | while ((contentLength < 0 || contentLength * .5f < size || imageRequest.isValid()) && (len = in.read(buffer)) != -1) { 182 | size += len; 183 | out.write(buffer, 0, len); 184 | imageRequest.updateDownloading(size, contentLength); 185 | } 186 | 187 | // if we finished download 188 | if (len == -1) { 189 | if (tmpFile.renameTo(imageRequest.getFile())) { 190 | imageRequest.setFileSize(size); 191 | } else { 192 | FILLogger.warn("Failed to rename temp download file to target file"); 193 | } 194 | } else { 195 | canceled = true; 196 | } 197 | } finally { 198 | FILUtils.closeSafe(out); 199 | FILUtils.closeSafe(in); 200 | FILUtils.deleteSafe(tmpFile); 201 | returnBuffer(buffer); 202 | } 203 | return canceled; 204 | } 205 | 206 | /** 207 | * Get buffer to be used for image download, use recycled if possible. 208 | */ 209 | private byte[] getBuffer() { 210 | byte[] buffer = null; 211 | synchronized (mBuffers) { 212 | for (int i = 0; i < mBuffers.length; i++) { 213 | if (mBuffers[i] != null) { 214 | buffer = mBuffers[i]; 215 | mBuffers[i] = null; 216 | break; 217 | } 218 | } 219 | } 220 | if (buffer == null) { 221 | buffer = new byte[2048]; 222 | } 223 | return buffer; 224 | } 225 | 226 | /** 227 | * Return buffer to be used for image download to recycled collection. 228 | */ 229 | private void returnBuffer(byte[] buffer) { 230 | if (buffer != null) { 231 | synchronized (mBuffers) { 232 | for (int i = 0; i < mBuffers.length; i++) { 233 | if (mBuffers[i] == null) { 234 | mBuffers[i] = buffer; 235 | break; 236 | } 237 | } 238 | } 239 | } 240 | } 241 | //endregion 242 | } -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/impl/ImageRequest.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader.impl; 14 | 15 | import android.text.TextUtils; 16 | 17 | import com.theartofdev.fastimageloader.ImageLoadSpec; 18 | import com.theartofdev.fastimageloader.ReusableBitmap; 19 | import com.theartofdev.fastimageloader.Target; 20 | 21 | import java.io.File; 22 | import java.util.ArrayList; 23 | import java.util.Collection; 24 | import java.util.List; 25 | import java.util.concurrent.atomic.AtomicBoolean; 26 | 27 | /** 28 | * Encapsulate 29 | */ 30 | public class ImageRequest { 31 | 32 | //region: Fields and Consts 33 | 34 | /** 35 | * the URL of the requested image as given 36 | */ 37 | private final String mUri; 38 | 39 | /** 40 | * the spec to load the image by 41 | */ 42 | private final ImageLoadSpec mSpec; 43 | 44 | /** 45 | * the file of the image in the disk 46 | */ 47 | private final File mFile; 48 | 49 | /** 50 | * Is the request is prefetch request 51 | */ 52 | private final boolean mPrefetch; 53 | 54 | /** 55 | * the size of the file in the disk cache 56 | */ 57 | private long mFileSize = -1; 58 | 59 | /** 60 | * the loaded image bitmap 61 | */ 62 | private ReusableBitmap mBitmap; 63 | 64 | /** 65 | * the target to load the image into 66 | */ 67 | private List mTargets = new ArrayList<>(3); 68 | 69 | /** 70 | * Is download of the image request started 71 | */ 72 | private AtomicBoolean mDownloadStarted = new AtomicBoolean(false); 73 | //endregion 74 | 75 | /** 76 | * @param uri the URL of the requested image as given 77 | * @param spec the dimension key used to load the image in specific size 78 | * @param file the path of the image in the disk 79 | */ 80 | ImageRequest(String uri, ImageLoadSpec spec, File file) { 81 | mUri = uri; 82 | mSpec = spec; 83 | mFile = file; 84 | mPrefetch = true; 85 | } 86 | 87 | /** 88 | * @param target the target to load the image into 89 | * @param uri the URL of the requested image as given 90 | * @param spec the dimension key used to load the image in specific size 91 | * @param file the path of the image in the disk 92 | */ 93 | ImageRequest(Target target, String uri, ImageLoadSpec spec, File file) { 94 | mTargets.add(target); 95 | mUri = uri; 96 | mSpec = spec; 97 | mFile = file; 98 | mPrefetch = false; 99 | } 100 | 101 | /** 102 | * The unique key of the image request. 103 | */ 104 | public String getUniqueKey() { 105 | return getUriUniqueKey(mSpec, mUri); 106 | } 107 | 108 | /** 109 | * The unique key of the image URI with the given spec. 110 | */ 111 | public static String getUriUniqueKey(ImageLoadSpec spec, String uri) { 112 | return uri + "$" + spec.getKey(); 113 | } 114 | 115 | /** 116 | * the URL of the requested image as given 117 | */ 118 | public String getUri() { 119 | return mUri; 120 | } 121 | 122 | /** 123 | * the URL of the requested image with thumbor parameters 124 | */ 125 | public String getEnhancedUri() { 126 | return mSpec.getImageServiceAdapter().convert(mUri, mSpec); 127 | } 128 | 129 | /** 130 | * the spec to load the image by 131 | */ 132 | public ImageLoadSpec getSpec() { 133 | return mSpec; 134 | } 135 | 136 | /** 137 | * the file of the image in the disk 138 | */ 139 | public File getFile() { 140 | return mFile; 141 | } 142 | 143 | /** 144 | * the size of the file in the disk 145 | */ 146 | public long getFileSize() { 147 | return mFileSize; 148 | } 149 | 150 | /** 151 | * the size of the file in the disk 152 | */ 153 | public void setFileSize(long fileSize) { 154 | mFileSize = fileSize; 155 | } 156 | 157 | /** 158 | * the loaded image bitmap 159 | */ 160 | public ReusableBitmap getBitmap() { 161 | return mBitmap; 162 | } 163 | 164 | /** 165 | * the loaded image bitmap 166 | */ 167 | public void setBitmap(ReusableBitmap bitmap) { 168 | if (mBitmap != null) { 169 | mBitmap.setInLoadUse(false); 170 | } 171 | mBitmap = bitmap; 172 | if (mBitmap != null) { 173 | mBitmap.setInLoadUse(true); 174 | mBitmap.setUrl(mUri); 175 | } 176 | } 177 | 178 | /** 179 | * the target to load the image into 180 | */ 181 | public Collection getValidTargets() { 182 | filterValidTargets(); 183 | return mTargets; 184 | } 185 | 186 | /** 187 | * Is the loading of the requested image is still valid or was it canceled.
188 | */ 189 | public boolean isValid() { 190 | filterValidTargets(); 191 | return mPrefetch || mTargets.size() > 0; 192 | } 193 | 194 | /** 195 | * Is the request is for prefetch and not real target 196 | */ 197 | public boolean isPrefetch() { 198 | return mPrefetch && mTargets.size() == 0; 199 | } 200 | 201 | /** 202 | * Mark request download start, return true if download start set or false if was already set. 203 | */ 204 | public boolean startDownload() { 205 | return mDownloadStarted.compareAndSet(false, true); 206 | } 207 | 208 | /** 209 | * Send update to all current targets on the download progress. 210 | * 211 | * @param downloaded the number of bytes already downloaded 212 | * @param contentLength the total number of bytes to download 213 | */ 214 | public void updateDownloading(int downloaded, long contentLength) { 215 | for (int i = 0; i < mTargets.size(); i++) { 216 | try { 217 | mTargets.get(i).onBitmapDownloading(downloaded, contentLength); 218 | } catch (Exception ignored) { 219 | } 220 | } 221 | } 222 | 223 | /** 224 | * Add another target to the request.
225 | * Check if the request was prefetch that its download has not been started yet.
226 | * If the request was prefetch it will be set to not prefetch 227 | * 228 | * @return true - request was prefetch and the download not started, false - otherwise. 229 | */ 230 | public boolean addTargetAndCheck(Target target) { 231 | mTargets.add(target); 232 | return mPrefetch && !mDownloadStarted.get(); 233 | } 234 | 235 | private void filterValidTargets() { 236 | for (int i = mTargets.size() - 1; i >= 0; i--) { 237 | boolean isValid = TextUtils.equals(mTargets.get(i).getUri(), mUri); 238 | if (!isValid) { 239 | mTargets.remove(i); 240 | } 241 | } 242 | } 243 | 244 | @Override 245 | public String toString() { 246 | return "ImageRequest{" + 247 | "mUri='" + mUri + '\'' + 248 | ", mSpec='" + mSpec + '\'' + 249 | ", mFile='" + mFile + '\'' + 250 | ", mFileSize=" + mFileSize + 251 | ", mBitmap=" + mBitmap + 252 | ", mTargets=" + mTargets.size() + 253 | ", mPrefetch=" + mPrefetch + 254 | ", isValid=" + isValid() + 255 | '}'; 256 | } 257 | } 258 | 259 | -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/impl/LoaderHandler.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader.impl; 14 | 15 | import android.app.Application; 16 | import android.content.ComponentCallbacks2; 17 | import android.content.res.Configuration; 18 | import android.os.Handler; 19 | import android.text.TextUtils; 20 | 21 | import com.theartofdev.fastimageloader.Decoder; 22 | import com.theartofdev.fastimageloader.DiskCache; 23 | import com.theartofdev.fastimageloader.Downloader; 24 | import com.theartofdev.fastimageloader.ImageLoadSpec; 25 | import com.theartofdev.fastimageloader.LoadedFrom; 26 | import com.theartofdev.fastimageloader.MemoryPool; 27 | import com.theartofdev.fastimageloader.ReusableBitmap; 28 | import com.theartofdev.fastimageloader.Target; 29 | import com.theartofdev.fastimageloader.impl.util.FILLogger; 30 | import com.theartofdev.fastimageloader.impl.util.FILUtils; 31 | 32 | import java.io.File; 33 | import java.util.HashMap; 34 | import java.util.Map; 35 | 36 | /** 37 | * Handler for image loading using memory/disk cache and other features. 38 | */ 39 | public final class LoaderHandler implements DiskCacheImpl.Callback, DownloaderImpl.Callback, ComponentCallbacks2 { 40 | 41 | //region: Fields and Consts 42 | 43 | /** 44 | * map of url to image request running it to reuse if same image is requested again 45 | */ 46 | private final Map mLoadingRequests = new HashMap<>(); 47 | 48 | /** 49 | * Memory cache for images loaded 50 | */ 51 | private final MemoryPool mMemoryPool; 52 | 53 | /** 54 | * Disk cache for images loaded 55 | */ 56 | private final DiskCache mDiskCache; 57 | 58 | /** 59 | * Downloader to download images from the web 60 | */ 61 | private final Downloader mDownloader; 62 | 63 | /** 64 | * Used to decode images from the disk to bitmap. 65 | */ 66 | private final Decoder mDecoder; 67 | 68 | /** 69 | * Used to post execution to main thread. 70 | */ 71 | private final Handler mHandler; 72 | 73 | /** 74 | * stats on the number of memory cache hits 75 | */ 76 | private int mMemoryHits; 77 | 78 | /** 79 | * stats on the number of memory cache hits for alternative sepc 80 | */ 81 | private int mMemoryAltHits; 82 | 83 | /** 84 | * stats on the number of disk cache hits 85 | */ 86 | private int mDiskHits; 87 | 88 | /** 89 | * stats on the number of disk cache hits for alternative sepc 90 | */ 91 | private int mDiskAltHits; 92 | 93 | /** 94 | * stats on the number of cache miss, network begin request 95 | */ 96 | private int mNetworkRequests; 97 | 98 | /** 99 | * stats on the number of cache miss, network loaded 100 | */ 101 | private int mNetworkLoads; 102 | //endregion 103 | 104 | /** 105 | * Init. 106 | * 107 | * @param decoder Used to decode images from the disk to bitmap. 108 | */ 109 | public LoaderHandler(Application application, 110 | MemoryPool memoryPool, 111 | DiskCache diskCache, 112 | Downloader downloader, 113 | Decoder decoder) { 114 | FILUtils.notNull(application, "application"); 115 | FILUtils.notNull(memoryPool, "memoryPool"); 116 | FILUtils.notNull(diskCache, "diskCache"); 117 | FILUtils.notNull(downloader, "downloader"); 118 | FILUtils.notNull(decoder, "decoder"); 119 | 120 | mMemoryPool = memoryPool; 121 | mDiskCache = diskCache; 122 | mDownloader = downloader; 123 | mDecoder = decoder; 124 | 125 | mHandler = new Handler(application.getMainLooper()); 126 | 127 | application.registerComponentCallbacks(this); 128 | } 129 | 130 | /** 131 | * Create image report for analyses. 132 | */ 133 | public String getReport() { 134 | StringBuilder sb = new StringBuilder(); 135 | sb.append("Image handler report:"); 136 | sb.append('\n'); 137 | sb.append("Memory Hit: ").append(mMemoryHits).append('\n'); 138 | sb.append("Memory alt Hit: ").append(mMemoryAltHits).append('\n'); 139 | sb.append("Disk Hit: ").append(mDiskHits).append('\n'); 140 | sb.append("Disk alt Hit: ").append(mDiskAltHits).append('\n'); 141 | sb.append("Network Requests: ").append(mNetworkRequests).append('\n'); 142 | sb.append("Network Loaded: ").append(mNetworkLoads).append('\n'); 143 | sb.append('\n'); 144 | //mMemoryPool.report(sb); 145 | sb.append('\n'); 146 | //mDiskCache.report(sb); 147 | return sb.toString(); 148 | } 149 | 150 | /** 151 | * Prefetch image (uri+spec) to be available in disk cache.
152 | * 153 | * @param uri the URI of the image to prefetch 154 | * @param spec the spec to prefetch the image by 155 | */ 156 | public void prefetchImage(String uri, ImageLoadSpec spec) { 157 | try { 158 | String imageKey = ImageRequest.getUriUniqueKey(spec, uri); 159 | ImageRequest request = mLoadingRequests.get(imageKey); 160 | if (request == null) { 161 | File file = mDiskCache.getCacheFile(uri, spec); 162 | if (!file.exists()) { 163 | request = new ImageRequest(uri, spec, file); 164 | mLoadingRequests.put(imageKey, request); 165 | 166 | FILLogger.debug("Add prefetch request... [{}]", request); 167 | mDownloader.downloadAsync(request, true, this); 168 | } 169 | } 170 | } catch (Exception e) { 171 | FILLogger.critical("Error in prefetch image [{}] [{}]", e, uri, spec); 172 | } 173 | } 174 | 175 | /** 176 | * Load image by given URL to the given target.
177 | * Handle transformation on the image, image dimension specification and dimension fallback.
178 | * If the image of the requested dimensions is not found in memory cache we try to find the fallback dimension, if 179 | * found it will be set to the target, and the requested dimension image will be loaded async. 180 | */ 181 | public void loadImage(Target target, ImageLoadSpec spec, ImageLoadSpec altSpec) { 182 | try { 183 | String uri = target.getUri(); 184 | if (!TextUtils.isEmpty(uri)) { 185 | 186 | ReusableBitmap image = mMemoryPool.get(uri, spec, altSpec); 187 | if (image != null) { 188 | mMemoryHits++; 189 | if (image.getSpec() != spec) 190 | mMemoryAltHits++; 191 | target.onBitmapLoaded(image, LoadedFrom.MEMORY); 192 | } 193 | 194 | // not found or loaded alternative spec 195 | if (image == null || image.getSpec() != spec) { 196 | String imageKey = ImageRequest.getUriUniqueKey(spec, uri); 197 | ImageRequest request = mLoadingRequests.get(imageKey); 198 | if (request != null) { 199 | FILLogger.debug("Memory cache miss, image already requested, add target to request... [{}] [{}]", request, target); 200 | if (request.addTargetAndCheck(target)) { 201 | mDownloader.downloadAsync(request, false, this); 202 | } 203 | } else { 204 | // start async process of loading image from disk cache or network 205 | request = new ImageRequest(target, uri, spec, mDiskCache.getCacheFile(uri, spec)); 206 | mLoadingRequests.put(imageKey, request); 207 | 208 | FILLogger.debug("Memory cache miss, start request handling... [{}]", request); 209 | // don't use alternative spec if image was loaded from memory cache 210 | mDiskCache.getAsync(request, image == null ? altSpec : null, mDecoder, mMemoryPool, this); 211 | } 212 | } 213 | } 214 | } catch (Exception e) { 215 | FILLogger.critical("Error in load image [{}]", e, target); 216 | target.onBitmapFailed(); 217 | } 218 | } 219 | 220 | /** 221 | * Clear the disk image cache, deleting all cached images. 222 | */ 223 | public void clearDiskCache() { 224 | mDiskCache.clear(); 225 | } 226 | 227 | //region: Private methods 228 | 229 | @Override 230 | public void loadImageDiskCacheCallback(final ImageRequest imageRequest, final boolean canceled) { 231 | if (FILUtils.isOnMainThread()) { 232 | onLoadImageDiskCacheCallback(imageRequest, canceled); 233 | } else 234 | mHandler.post(new Runnable() { 235 | @Override 236 | public void run() { 237 | onLoadImageDiskCacheCallback(imageRequest, canceled); 238 | } 239 | }); 240 | } 241 | 242 | @Override 243 | public void loadImageDownloaderCallback(final ImageRequest imageRequest, final boolean downloaded, final boolean canceled) { 244 | 245 | // if downloaded and request is still valid - load the image object 246 | if (downloaded && !canceled && !imageRequest.isPrefetch()) { 247 | mDecoder.decode(mMemoryPool, imageRequest, imageRequest.getFile(), imageRequest.getSpec()); 248 | } 249 | 250 | if (FILUtils.isOnMainThread()) { 251 | onLoadImageDownloaderCallback(imageRequest, downloaded, canceled); 252 | } else 253 | mHandler.post(new Runnable() { 254 | @Override 255 | public void run() { 256 | onLoadImageDownloaderCallback(imageRequest, downloaded, canceled); 257 | } 258 | }); 259 | } 260 | 261 | /** 262 | * Callback after the disk cache loaded the image or returned cache miss.
263 | * Hit - set the loaded image on the requesting target.
264 | * Miss - pass the request to image downloader.
265 | */ 266 | private void onLoadImageDiskCacheCallback(ImageRequest imageRequest, boolean canceled) { 267 | try { 268 | boolean loaded = imageRequest.getBitmap() != null; 269 | boolean loadedAlt = loaded && imageRequest.getBitmap().getSpec() != imageRequest.getSpec(); 270 | FILLogger.debug("Get image from disk cache callback... [Loaded: {}, Alt:{}] [Canceled: {}] [{}]", loaded, loadedAlt, canceled, imageRequest); 271 | 272 | if (loaded) { 273 | // if image object was loaded - add it to memory cache 274 | mMemoryPool.set(imageRequest.getBitmap()); 275 | } 276 | 277 | if (imageRequest.isValid()) { 278 | if (loaded) { 279 | // if some image was loaded set it to targets 280 | if (loadedAlt) { 281 | mDiskAltHits++; 282 | } else { 283 | mDiskHits++; 284 | } 285 | for (Target target : imageRequest.getValidTargets()) { 286 | target.onBitmapLoaded(imageRequest.getBitmap(), LoadedFrom.DISK); 287 | } 288 | } 289 | if (loaded && !loadedAlt) { 290 | // if primary loaded we are done 291 | mLoadingRequests.remove(imageRequest.getUniqueKey()); 292 | } else { 293 | // need to download primary 294 | if (canceled) { 295 | // race-condition, canceled request that add valid target (run again) 296 | mDiskCache.getAsync(imageRequest, null, mDecoder, mMemoryPool, this); 297 | } else { 298 | mNetworkRequests++; 299 | mDownloader.downloadAsync(imageRequest, false, this); 300 | } 301 | } 302 | } else { 303 | mLoadingRequests.remove(imageRequest.getUniqueKey()); 304 | } 305 | } catch (Exception e) { 306 | mLoadingRequests.remove(imageRequest.getUniqueKey()); 307 | FILLogger.critical("Error in load image disk callback", e); 308 | } 309 | } 310 | 311 | /** 312 | * Callback after image downloader downloaded the image and loaded from disk, failed or canceled.
313 | * Success - set the loaded image on the requesting target.
314 | * Failed - set failure on the requesting target.
315 | */ 316 | private void onLoadImageDownloaderCallback(ImageRequest imageRequest, boolean downloaded, boolean canceled) { 317 | try { 318 | FILLogger.debug("Load image from network callback... [{}] [Downloaded: {}] [Canceled: {}]", imageRequest, downloaded, canceled); 319 | 320 | // if image was downloaded - notify disk cache 321 | if (downloaded) { 322 | mDiskCache.imageAdded(imageRequest.getFileSize()); 323 | } 324 | 325 | // if image object was loaded - add it to memory cache 326 | if (imageRequest.getBitmap() != null) { 327 | mMemoryPool.set(imageRequest.getBitmap()); 328 | } 329 | 330 | // request are valid if there is target or prefetch, but here we don't care for prefetch 331 | if (imageRequest.isValid() && !imageRequest.isPrefetch()) { 332 | if (imageRequest.getBitmap() != null) { 333 | mNetworkLoads++; 334 | mLoadingRequests.remove(imageRequest.getUniqueKey()); 335 | for (Target target : imageRequest.getValidTargets()) { 336 | target.onBitmapLoaded(imageRequest.getBitmap(), LoadedFrom.NETWORK); 337 | } 338 | } else { 339 | if (canceled) { 340 | // race-condition, canceled request that add valid target (run again) 341 | mDiskCache.getAsync(imageRequest, null, mDecoder, mMemoryPool, this); 342 | } else { 343 | mLoadingRequests.remove(imageRequest.getUniqueKey()); 344 | for (Target target : imageRequest.getValidTargets()) { 345 | target.onBitmapFailed(); 346 | } 347 | } 348 | } 349 | } else { 350 | mLoadingRequests.remove(imageRequest.getUniqueKey()); 351 | imageRequest.setBitmap(null); 352 | } 353 | } catch (Exception e) { 354 | mLoadingRequests.remove(imageRequest.getUniqueKey()); 355 | FILLogger.critical("Error in load image downloader callback", e); 356 | } 357 | } 358 | 359 | @Override 360 | public void onTrimMemory(int level) { 361 | mMemoryPool.onTrimMemory(level); 362 | } 363 | 364 | @Override 365 | public void onConfigurationChanged(Configuration newConfig) { 366 | 367 | } 368 | 369 | @Override 370 | public void onLowMemory() { 371 | mMemoryPool.onTrimMemory(0); 372 | } 373 | //endregion 374 | } -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/impl/MemoryPoolImpl.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader.impl; 14 | 15 | import android.content.ComponentCallbacks2; 16 | 17 | import com.theartofdev.fastimageloader.ImageLoadSpec; 18 | import com.theartofdev.fastimageloader.MemoryPool; 19 | import com.theartofdev.fastimageloader.ReusableBitmap; 20 | import com.theartofdev.fastimageloader.impl.util.FILLogger; 21 | 22 | import java.util.Iterator; 23 | import java.util.LinkedHashMap; 24 | import java.util.LinkedList; 25 | import java.util.Map; 26 | 27 | /** 28 | * Memory cache for image handler.
29 | * Holds the images loaded two caches: large for images larger than 300px (width+height) 30 | * and small for smaller.
31 | * Caches may be evicted when memory pressure is detected. 32 | */ 33 | public class MemoryPoolImpl implements MemoryPool { 34 | 35 | //region: Fields and Consts 36 | 37 | /** 38 | * Cache and pool of reusable bitmaps. 39 | */ 40 | private final Map> mBitmapsCachePool = new LinkedHashMap<>(); 41 | 42 | /** 43 | * stats on the number of cache hit 44 | */ 45 | private int mCacheHit; 46 | 47 | /** 48 | * stats on the number of cache miss 49 | */ 50 | private int mCacheMiss; 51 | 52 | /** 53 | * stats on the number of bitmaps used from recycled instance 54 | */ 55 | private int mReUsed; 56 | 57 | /** 58 | * stats on the number of recycled images returned after failed to use 59 | */ 60 | private int mReturned; 61 | 62 | /** 63 | * stats on the number of recycled images thrown because of limit 64 | */ 65 | private int mThrown; 66 | //endregion 67 | 68 | @Override 69 | public ReusableBitmap get(String url, ImageLoadSpec spec, ImageLoadSpec altSpec) { 70 | synchronized (mBitmapsCachePool) { 71 | ReusableBitmap bitmap = getUnusedBitmapBySpec(url, spec); 72 | if (bitmap == null && altSpec != null) { 73 | bitmap = getUnusedBitmapBySpec(url, altSpec); 74 | } 75 | if (bitmap != null) { 76 | mCacheHit++; 77 | } else { 78 | mCacheMiss++; 79 | } 80 | return bitmap; 81 | } 82 | } 83 | 84 | @Override 85 | public void set(ReusableBitmap bitmap) { 86 | synchronized (mBitmapsCachePool) { 87 | if (bitmap != null) { 88 | LinkedList list = mBitmapsCachePool.get(bitmap.getSpec()); 89 | if (list == null) { 90 | list = new LinkedList<>(); 91 | mBitmapsCachePool.put(bitmap.getSpec(), list); 92 | } 93 | list.addFirst(bitmap); 94 | } 95 | } 96 | } 97 | 98 | @Override 99 | public ReusableBitmap getUnused(ImageLoadSpec spec) { 100 | synchronized (mBitmapsCachePool) { 101 | LinkedList list = mBitmapsCachePool.get(spec); 102 | if (list != null) { 103 | Iterator iter = list.iterator(); 104 | if (spec.isSizeBounded()) { 105 | while (iter.hasNext()) { 106 | ReusableBitmap bitmap = iter.next(); 107 | if (!bitmap.isInUse()) { 108 | iter.remove(); 109 | mReUsed++; 110 | bitmap.setInLoadUse(true); 111 | return bitmap; 112 | } 113 | } 114 | } else { 115 | // don't keep unbounded bitmaps (only 2 to be nice on quick return) 116 | int grace = 2; 117 | while (iter.hasNext()) { 118 | ReusableBitmap bitmap = iter.next(); 119 | if (!bitmap.isInUse() && grace-- < 1) { 120 | mThrown++; 121 | iter.remove(); 122 | bitmap.close(); 123 | } 124 | } 125 | } 126 | } 127 | } 128 | return null; 129 | } 130 | 131 | @Override 132 | public void returnUnused(ReusableBitmap bitmap) { 133 | synchronized (mBitmapsCachePool) { 134 | mReUsed--; 135 | mReturned++; 136 | bitmap.setInLoadUse(false); 137 | LinkedList list = mBitmapsCachePool.get(bitmap.getSpec()); 138 | if (list != null) { 139 | list.addFirst(bitmap); 140 | } else { 141 | mThrown++; 142 | bitmap.close(); 143 | } 144 | } 145 | } 146 | 147 | @Override 148 | public void clear() { 149 | releaseUnUsedBitmaps(0); 150 | } 151 | 152 | /** 153 | * Populate the given string builder with report on cache status. 154 | */ 155 | public void report(StringBuilder sb) { 156 | sb.append("Memory Cache: ").append(mCacheHit + mCacheMiss).append('\n'); 157 | sb.append("Cache Hit: ").append(mCacheHit).append('\n'); 158 | sb.append("Cache Miss: ").append(mCacheMiss).append('\n'); 159 | sb.append("ReUsed: ").append(mReUsed).append('\n'); 160 | sb.append("Returned: ").append(mReturned).append('\n'); 161 | sb.append("Thrown: ").append(mThrown).append('\n'); 162 | 163 | // sb.append("Small: ") 164 | // .append(mSmallCache.items()).append('/') 165 | // .append(mSmallCache.getMaxItems()).append(", (") 166 | // .append(NumberFormat.getInstance().format(mSmallCache.size() / 1024)).append("K/") 167 | // .append(NumberFormat.getInstance().format(mSmallCache.maxSize() / 1024)).append("K)") 168 | // .append('\n'); 169 | 170 | // sb.append("Bitmap Recycler: ").append('\n'); 171 | // sb.append("Added: ").append(mAdded).append('\n'); 172 | // 173 | // sb.append("Returned: ").append(mReturned).append('\n'); 174 | // sb.append("Thrown: ").append(mThrown).append('\n'); 175 | // for (Map.Entry> entry : mReusableBitmaps.entrySet()) { 176 | // long size = 0; 177 | // for (RecycleBitmap bitmap : entry.getValue()) { 178 | // size += bitmap.getBitmap().getByteCount(); 179 | // } 180 | // sb.append(entry.getKey()).append(": ") 181 | // .append(entry.getValue().size()).append(", ") 182 | // .append(NumberFormat.getInstance().format(size / 1024)).append("K\n"); 183 | // } 184 | 185 | } 186 | 187 | @Override 188 | public void onTrimMemory(int level) { 189 | switch (level) { 190 | case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN: 191 | releaseUnUsedBitmaps(3); 192 | break; 193 | case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND: 194 | releaseUnUsedBitmaps(1); 195 | break; 196 | case ComponentCallbacks2.TRIM_MEMORY_MODERATE: 197 | case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE: 198 | case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW: 199 | case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL: 200 | case ComponentCallbacks2.TRIM_MEMORY_COMPLETE: 201 | releaseUnUsedBitmaps(0); 202 | break; 203 | } 204 | } 205 | 206 | @Override 207 | public String toString() { 208 | return "ImageMemoryCache{" + 209 | "mCacheHit=" + mCacheHit + 210 | ", mCacheMiss=" + mCacheMiss + 211 | '}'; 212 | } 213 | 214 | //region: Private methods 215 | 216 | /** 217 | * Get bitmap from cache that is of the given spec and has image loaded of the given URI. 218 | */ 219 | private ReusableBitmap getUnusedBitmapBySpec(String uri, ImageLoadSpec spec) { 220 | LinkedList list = mBitmapsCachePool.get(spec); 221 | if (list != null) { 222 | Iterator iter = list.iterator(); 223 | while (iter.hasNext()) { 224 | ReusableBitmap bitmap = iter.next(); 225 | if (uri.equals(bitmap.getUri())) { 226 | iter.remove(); 227 | list.addLast(bitmap); 228 | return bitmap; 229 | } 230 | } 231 | } 232 | return null; 233 | } 234 | 235 | /** 236 | * Release unused bitmaps that are currently in the pool. 237 | * 238 | * @param graceLevel the number of unused bitmaps NOT to release 239 | */ 240 | private void releaseUnUsedBitmaps(int graceLevel) { 241 | FILLogger.debug("trim image cache to size [{}]", graceLevel); 242 | for (LinkedList list : mBitmapsCachePool.values()) { 243 | int listGrace = graceLevel; 244 | Iterator iter = list.iterator(); 245 | while (iter.hasNext()) { 246 | ReusableBitmap bitmap = iter.next(); 247 | if (!bitmap.isInUse() && listGrace-- < 1) { 248 | mThrown++; 249 | iter.remove(); 250 | bitmap.close(); 251 | } 252 | } 253 | } 254 | } 255 | //endregion 256 | } 257 | 258 | -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/impl/NativeHttpClient.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader.impl; 14 | 15 | import com.theartofdev.fastimageloader.HttpClient; 16 | import com.theartofdev.fastimageloader.impl.util.FILUtils; 17 | 18 | import java.io.IOException; 19 | import java.io.InputStream; 20 | import java.net.HttpURLConnection; 21 | import java.net.URL; 22 | 23 | /** 24 | * {@link com.theartofdev.fastimageloader.HttpClient} using native Android HttpURLConnection. 25 | */ 26 | public class NativeHttpClient implements HttpClient { 27 | 28 | /** 29 | * the maximum time in milliseconds to wait while connecting 30 | */ 31 | private final int mConnectTimeout; 32 | 33 | /** 34 | * the maximum time to wait for an input stream read to complete before giving up 35 | */ 36 | private final int mReadTimeout; 37 | 38 | /** 39 | * Create new instance and set connect and read timeout to 10 and 15 seconds respectively. 40 | */ 41 | public NativeHttpClient() { 42 | this(10000, 15000); 43 | } 44 | 45 | /** 46 | * @param connectTimeout the maximum time in milliseconds to wait while connecting 47 | * @param readTimeout the maximum time to wait for an input stream read to complete before giving up 48 | */ 49 | public NativeHttpClient(int connectTimeout, int readTimeout) { 50 | mConnectTimeout = connectTimeout; 51 | mReadTimeout = readTimeout; 52 | } 53 | 54 | @Override 55 | public HttpClient.HttpResponse execute(String uri) { 56 | try { 57 | URL url = new URL(uri); 58 | HttpURLConnection connection = (HttpURLConnection) url.openConnection(); 59 | connection.setConnectTimeout(mConnectTimeout); 60 | connection.setReadTimeout(mReadTimeout); 61 | connection.connect(); 62 | return new NativeHttpResponse(connection.getResponseCode(), connection); 63 | } catch (IOException e) { 64 | throw new RuntimeException("HTTP execute failed", e); 65 | } 66 | } 67 | 68 | //region: Inner class: OkHttpResponse 69 | 70 | private static final class NativeHttpResponse implements HttpResponse { 71 | 72 | private int mCode; 73 | 74 | private final HttpURLConnection mConnection; 75 | 76 | public NativeHttpResponse(int code, HttpURLConnection connection) { 77 | mCode = code; 78 | mConnection = connection; 79 | } 80 | 81 | @Override 82 | public int getCode() { 83 | return mCode; 84 | } 85 | 86 | @Override 87 | public String getErrorMessage() { 88 | try { 89 | return mConnection.getResponseMessage(); 90 | } catch (IOException e) { 91 | throw new RuntimeException("HTTP execute failed", e); 92 | } 93 | } 94 | 95 | @Override 96 | public long getContentLength() { 97 | return FILUtils.parseLong(mConnection.getHeaderField("content-length"), -1); 98 | } 99 | 100 | @Override 101 | public InputStream getBodyStream() { 102 | try { 103 | return mConnection.getInputStream(); 104 | } catch (IOException e) { 105 | throw new RuntimeException("HTTP execute failed", e); 106 | } 107 | } 108 | } 109 | //endregion 110 | } 111 | 112 | -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/impl/OkHttpClient.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader.impl; 14 | 15 | import com.squareup.okhttp.Request; 16 | import com.squareup.okhttp.Response; 17 | import com.theartofdev.fastimageloader.HttpClient; 18 | import com.theartofdev.fastimageloader.impl.util.FILUtils; 19 | 20 | import java.io.IOException; 21 | import java.io.InputStream; 22 | import java.util.concurrent.TimeUnit; 23 | 24 | /** 25 | * {@link com.theartofdev.fastimageloader.HttpClient} using OK HTTP network library. 26 | */ 27 | public class OkHttpClient implements HttpClient { 28 | 29 | //region: Fields and Consts 30 | 31 | /** 32 | * The actual OK HTTP client used 33 | */ 34 | private com.squareup.okhttp.OkHttpClient mClient; 35 | //endregion 36 | 37 | /** 38 | * Create new OkHttpClient instance and set connect and read timeout to 10 and 15 seconds respectively. 39 | */ 40 | public OkHttpClient() { 41 | this(new com.squareup.okhttp.OkHttpClient()); 42 | mClient.setConnectTimeout(10, TimeUnit.SECONDS); 43 | mClient.setReadTimeout(15, TimeUnit.SECONDS); 44 | } 45 | 46 | /** 47 | * Use the given OK HTTP client for all requests. 48 | * 49 | * @param client The actual OK HTTP client used 50 | */ 51 | public OkHttpClient(com.squareup.okhttp.OkHttpClient client) { 52 | FILUtils.notNull(client, "client"); 53 | mClient = client; 54 | } 55 | 56 | @Override 57 | public HttpResponse execute(String uri) { 58 | try { 59 | Request httpRequest = new Request.Builder().url(uri).build(); 60 | Response httpResponse = mClient.newCall(httpRequest).execute(); 61 | return new OkHttpResponse(httpResponse); 62 | } catch (IOException e) { 63 | throw new RuntimeException("HTTP execute failed", e); 64 | } 65 | } 66 | 67 | //region: Inner class: OkHttpResponse 68 | 69 | private static final class OkHttpResponse implements HttpResponse { 70 | 71 | private final Response mResponse; 72 | 73 | public OkHttpResponse(Response response) { 74 | mResponse = response; 75 | } 76 | 77 | @Override 78 | public int getCode() { 79 | return mResponse.code(); 80 | } 81 | 82 | @Override 83 | public String getErrorMessage() { 84 | return mResponse.message(); 85 | } 86 | 87 | @Override 88 | public long getContentLength() { 89 | return FILUtils.parseLong(mResponse.header("content-length"), -1); 90 | } 91 | 92 | @Override 93 | public InputStream getBodyStream() { 94 | return mResponse.body().byteStream(); 95 | } 96 | } 97 | //endregion 98 | } 99 | 100 | -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/impl/util/FILLogger.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader.impl.util; 14 | 15 | import android.util.Log; 16 | 17 | import com.theartofdev.fastimageloader.LoadedFrom; 18 | import com.theartofdev.fastimageloader.LogAppender; 19 | 20 | /** 21 | * Logger for Fast Image Loader internal use only. 22 | */ 23 | public final class FILLogger { 24 | 25 | /** 26 | * The tag to use for all logs 27 | */ 28 | private static final String TAG = "FastImageLoader"; 29 | 30 | /** 31 | * If to write logs to logcat. 32 | */ 33 | public static boolean mLogcatEnabled = false; 34 | 35 | /** 36 | * Extensibility appender to write the logs to. 37 | */ 38 | public static LogAppender mAppender; 39 | 40 | /** 41 | * The min log level to write logs at, logs below this level are ignored. 42 | */ 43 | public static int mLogLevel = Log.INFO; 44 | 45 | /** 46 | * Image load operation complete. 47 | * 48 | * @param url the url of the image 49 | * @param specKey the spec of the image load request 50 | * @param from from where the image was loaded (MEMORY/DISK/NETWORK) 51 | * @param successful was the image load successful 52 | * @param time the time in milliseconds it took from request to finish 53 | */ 54 | public static void operation(String url, String specKey, LoadedFrom from, boolean successful, long time) { 55 | if (mLogcatEnabled) { 56 | String msg = FILUtils.format("Operation: LoadImage [{}] [{}] [{}] [{}]", from, specKey, successful, time); 57 | Log.println(from == LoadedFrom.MEMORY ? Log.DEBUG : Log.INFO, TAG, msg); 58 | } 59 | if (mAppender != null) 60 | mAppender.imageLoadOperation(url, specKey, from, successful, time); 61 | } 62 | 63 | /** 64 | * Image download operation complete. 65 | * 66 | * @param url the url of the image 67 | * @param specKey the spec of the image load request 68 | * @param responseCode the response code of the download web request 69 | * @param time the time in milliseconds it took to download the image 70 | * @param bytes the number of bytes received if download was successful 71 | * @param error optional: if download failed will contain the error 72 | */ 73 | public static void operation(String url, String specKey, int responseCode, long time, long bytes, Throwable error) { 74 | if (mLogcatEnabled) { 75 | String msg = FILUtils.format("Operation: DownloadImage [{}] [{}] [{}] [{}] [{}]", url, specKey, responseCode, bytes, time); 76 | if (error == null) { 77 | Log.i(TAG, msg); 78 | } else { 79 | Log.e(TAG, msg, error); 80 | } 81 | } 82 | if (mAppender != null) 83 | mAppender.imageDownloadOperation(url, specKey, responseCode, time, bytes, error); 84 | } 85 | 86 | public static void debug(String msg) { 87 | if (mLogLevel <= Log.DEBUG) { 88 | if (mLogcatEnabled) 89 | Log.d(TAG, msg); 90 | if (mAppender != null) 91 | mAppender.log(Log.DEBUG, TAG, msg, null); 92 | } 93 | } 94 | 95 | public static void debug(String msg, Object arg1) { 96 | if (mLogLevel <= Log.DEBUG) { 97 | if (mLogcatEnabled) 98 | Log.d(TAG, FILUtils.format(msg, arg1)); 99 | if (mAppender != null) 100 | mAppender.log(Log.DEBUG, TAG, FILUtils.format(msg, arg1), null); 101 | } 102 | } 103 | 104 | public static void debug(String msg, Object arg1, Object arg2) { 105 | if (mLogLevel <= Log.DEBUG) { 106 | if (mLogcatEnabled) 107 | Log.d(TAG, FILUtils.format(msg, arg1, arg2)); 108 | if (mAppender != null) 109 | mAppender.log(Log.DEBUG, TAG, FILUtils.format(msg, arg1, arg2), null); 110 | } 111 | } 112 | 113 | public static void debug(String msg, Object arg1, Object arg2, Object arg3) { 114 | if (mLogLevel <= Log.DEBUG) { 115 | if (mLogcatEnabled) 116 | Log.d(TAG, FILUtils.format(msg, arg1, arg2, arg3)); 117 | if (mAppender != null) 118 | mAppender.log(Log.DEBUG, TAG, FILUtils.format(msg, arg1, arg2, arg3), null); 119 | } 120 | } 121 | 122 | public static void debug(String msg, Object arg1, Object arg2, Object arg3, Object arg4) { 123 | if (mLogLevel <= Log.DEBUG) { 124 | if (mLogcatEnabled) 125 | Log.d(TAG, FILUtils.format(msg, arg1, arg2, arg3, arg4)); 126 | if (mAppender != null) 127 | mAppender.log(Log.DEBUG, TAG, FILUtils.format(msg, arg1, arg2, arg3, arg4), null); 128 | } 129 | } 130 | 131 | public static void info(String msg) { 132 | if (mLogLevel <= Log.INFO) { 133 | if (mLogcatEnabled) 134 | Log.i(TAG, msg); 135 | if (mAppender != null) 136 | mAppender.log(Log.INFO, TAG, msg, null); 137 | } 138 | } 139 | 140 | public static void info(String msg, Object arg1) { 141 | if (mLogLevel <= Log.INFO) { 142 | if (mLogcatEnabled) 143 | Log.i(TAG, FILUtils.format(msg, arg1)); 144 | if (mAppender != null) 145 | mAppender.log(Log.INFO, TAG, FILUtils.format(msg, arg1), null); 146 | } 147 | } 148 | 149 | public static void info(String msg, Object arg1, Object arg2) { 150 | if (mLogLevel <= Log.INFO) { 151 | if (mLogcatEnabled) 152 | Log.i(TAG, FILUtils.format(msg, arg1, arg2)); 153 | if (mAppender != null) 154 | mAppender.log(Log.INFO, TAG, FILUtils.format(msg, arg1, arg2), null); 155 | } 156 | } 157 | 158 | public static void info(String msg, Object arg1, Object arg2, Object arg3) { 159 | if (mLogLevel <= Log.INFO) { 160 | if (mLogcatEnabled) 161 | Log.i(TAG, FILUtils.format(msg, arg1, arg2, arg3)); 162 | if (mAppender != null) 163 | mAppender.log(Log.INFO, TAG, FILUtils.format(msg, arg1, arg2, arg3), null); 164 | } 165 | } 166 | 167 | public static void info(String msg, Object... args) { 168 | if (mLogLevel <= Log.INFO) { 169 | if (mLogcatEnabled) 170 | Log.i(TAG, FILUtils.format(msg, args)); 171 | if (mAppender != null) 172 | mAppender.log(Log.INFO, TAG, FILUtils.format(msg, args), null); 173 | } 174 | } 175 | 176 | public static void warn(String msg, Object... args) { 177 | if (mLogLevel <= Log.WARN) { 178 | if (mLogcatEnabled) 179 | Log.w(TAG, FILUtils.format(msg, args)); 180 | if (mAppender != null) 181 | mAppender.log(Log.WARN, TAG, FILUtils.format(msg, args), null); 182 | } 183 | } 184 | 185 | public static void warn(String msg, Throwable e, Object... args) { 186 | if (mLogLevel <= Log.WARN) { 187 | if (mLogcatEnabled) 188 | Log.w(TAG, FILUtils.format(msg, args), e); 189 | if (mAppender != null) 190 | mAppender.log(Log.WARN, TAG, FILUtils.format(msg, args), e); 191 | } 192 | } 193 | 194 | public static void error(String msg) { 195 | if (mLogLevel <= Log.ERROR) { 196 | if (mLogcatEnabled) 197 | Log.e(TAG, msg); 198 | if (mAppender != null) 199 | mAppender.log(Log.ERROR, TAG, msg, null); 200 | } 201 | } 202 | 203 | public static void error(String msg, Object... args) { 204 | if (mLogLevel <= Log.ERROR) { 205 | msg = FILUtils.format(msg, args); 206 | if (mLogcatEnabled) 207 | Log.e(TAG, msg); 208 | if (mAppender != null) 209 | mAppender.log(Log.ERROR, TAG, msg, null); 210 | } 211 | } 212 | 213 | public static void error(String msg, Throwable e, Object... args) { 214 | if (mLogLevel <= Log.ERROR) { 215 | msg = FILUtils.format(msg, args); 216 | if (mLogcatEnabled) 217 | Log.e(TAG, msg, e); 218 | if (mAppender != null) 219 | mAppender.log(Log.ERROR, TAG, msg, e); 220 | } 221 | } 222 | 223 | public static void critical(String msg, Object... args) { 224 | if (mLogLevel <= Log.ASSERT) { 225 | msg = FILUtils.format(msg, args); 226 | Exception e = new Exception(msg); 227 | if (mLogcatEnabled) 228 | Log.e(TAG, msg, e); 229 | if (mAppender != null) 230 | mAppender.log(Log.ASSERT, TAG, msg, e); 231 | } 232 | } 233 | 234 | public static void critical(String msg, Throwable e, Object... args) { 235 | if (mLogLevel <= Log.ASSERT) { 236 | msg = FILUtils.format(msg, args); 237 | if (mLogcatEnabled) 238 | Log.e(TAG, msg, e); 239 | if (mAppender != null) 240 | mAppender.log(Log.ASSERT, TAG, msg, e); 241 | } 242 | } 243 | } 244 | 245 | -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/impl/util/FILUtils.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader.impl.util; 14 | 15 | import android.graphics.Rect; 16 | import android.graphics.RectF; 17 | import android.text.TextUtils; 18 | 19 | import java.io.Closeable; 20 | import java.io.File; 21 | import java.io.IOException; 22 | import java.util.Collection; 23 | import java.util.concurrent.ThreadFactory; 24 | 25 | /** 26 | * General utility methods for Fast Image Loader internal use only. 27 | */ 28 | public final class FILUtils { 29 | 30 | /** 31 | * Reuse Rect object 32 | */ 33 | public static final Rect rect = new Rect(); 34 | 35 | /** 36 | * Reuse RectF object 37 | */ 38 | public static final RectF rectF = new RectF(); 39 | 40 | /** 41 | * The ID of the main thread of the application, used to know if currently execution on main thread. 42 | */ 43 | public static long MainThreadId; 44 | 45 | /** 46 | * Validate that the current executing thread is the main Android thread for the app. 47 | * 48 | * @throws RuntimeException current thread is not main thread. 49 | */ 50 | public static void verifyOnMainThread() { 51 | if (!isOnMainThread()) 52 | throw new RuntimeException("Access to this method must be on main thread only"); 53 | } 54 | 55 | /** 56 | * Returns true if the current executing thread is the main Android app thread, false otherwise. 57 | */ 58 | public static boolean isOnMainThread() { 59 | return Thread.currentThread().getId() == MainThreadId; 60 | } 61 | 62 | /** 63 | * Validate given argument isn't null. 64 | * 65 | * @param arg argument to validate 66 | * @param argName name of the argument to show in error message 67 | * @throws IllegalArgumentException 68 | */ 69 | public static void notNull(Object arg, String argName) { 70 | if (arg == null) { 71 | throw new IllegalArgumentException("argument is null: " + argName); 72 | } 73 | } 74 | 75 | /** 76 | * Validate given string argument isn't null or empty string. 77 | * 78 | * @param arg argument to validate 79 | * @param argName name of the argument to show in error message 80 | * @throws IllegalArgumentException 81 | */ 82 | public static void notNullOrEmpty(String arg, String argName) { 83 | if (arg == null || arg.length() < 1) { 84 | throw new IllegalArgumentException("argument is null: " + argName); 85 | } 86 | } 87 | 88 | /** 89 | * Validate given array argument isn't null or empty string. 90 | * 91 | * @param arg argument to validate 92 | * @param argName name of the argument to show in error message 93 | * @throws IllegalArgumentException 94 | */ 95 | public static void notNullOrEmpty(T[] arg, String argName) { 96 | if (arg == null || arg.length < 1) { 97 | throw new IllegalArgumentException("argument is null: " + argName); 98 | } 99 | } 100 | 101 | /** 102 | * Validate given collection argument isn't null or empty string. 103 | * 104 | * @param arg argument to validate 105 | * @param argName name of the argument to show in error message 106 | * @throws IllegalArgumentException 107 | */ 108 | public static void notNullOrEmpty(Collection arg, String argName) { 109 | if (arg == null || arg.size() < 1) { 110 | throw new IllegalArgumentException("argument is null: " + argName); 111 | } 112 | } 113 | 114 | /** 115 | * Safe parse the given string to long value. 116 | */ 117 | public static long parseLong(String value, long defaultValue) { 118 | if (!TextUtils.isEmpty(value)) { 119 | try { 120 | return Integer.parseInt(value); 121 | } catch (Exception ignored) { 122 | } 123 | } 124 | return defaultValue; 125 | } 126 | 127 | /** 128 | * combine two path into a single path with File.separator. 129 | * Handle all cases where the separator already exists. 130 | */ 131 | public static String pathCombine(String path1, String path2) { 132 | if (path2 == null) 133 | return path1; 134 | else if (path1 == null) 135 | return path2; 136 | 137 | path1 = path1.trim(); 138 | path2 = path2.trim(); 139 | if (path1.endsWith(File.separator)) { 140 | if (path2.startsWith(File.separator)) 141 | return path1 + path2.substring(1); 142 | else 143 | return path1 + path2; 144 | } else { 145 | if (path2.startsWith(File.separator)) 146 | return path1 + path2; 147 | else 148 | return path1 + File.separator + path2; 149 | } 150 | } 151 | 152 | /** 153 | * Close the given closeable object (Stream) in a safe way: check if it is null and catch-log 154 | * exception thrown. 155 | * 156 | * @param closeable the closable object to close 157 | */ 158 | public static void closeSafe(Closeable closeable) { 159 | if (closeable != null) { 160 | try { 161 | closeable.close(); 162 | } catch (IOException e) { 163 | FILLogger.warn("Failed to close closable object [{}]", e, closeable); 164 | } 165 | } 166 | } 167 | 168 | /** 169 | * Delete the given file in a safe way: check if it is null and catch-log exception thrown. 170 | * 171 | * @param file the file to delete 172 | */ 173 | public static void deleteSafe(File file) { 174 | if (file != null) { 175 | try { 176 | //noinspection ResultOfMethodCallIgnored 177 | file.delete(); 178 | } catch (Throwable e) { 179 | FILLogger.warn("Failed to delete file [{}]", e, file); 180 | } 181 | } 182 | } 183 | 184 | /** 185 | * Format the given format string by replacing {} placeholders with given arguments. 186 | */ 187 | public static String format(String format, Object arg1) { 188 | return replacePlaceHolder(format, arg1); 189 | } 190 | 191 | /** 192 | * Format the given format string by replacing {} placeholders with given arguments. 193 | */ 194 | public static String format(String format, Object arg1, Object arg2) { 195 | format = replacePlaceHolder(format, arg1); 196 | return replacePlaceHolder(format, arg2); 197 | } 198 | 199 | /** 200 | * Format the given format string by replacing {} placeholders with given arguments. 201 | */ 202 | public static String format(String format, Object arg1, Object arg2, Object arg3) { 203 | format = replacePlaceHolder(format, arg1); 204 | format = replacePlaceHolder(format, arg2); 205 | return replacePlaceHolder(format, arg3); 206 | } 207 | 208 | /** 209 | * Format the given format string by replacing {} placeholders with given arguments. 210 | */ 211 | public static String format(String format, Object arg1, Object arg2, Object arg3, Object arg4) { 212 | format = replacePlaceHolder(format, arg1); 213 | format = replacePlaceHolder(format, arg2); 214 | format = replacePlaceHolder(format, arg3); 215 | return replacePlaceHolder(format, arg4); 216 | } 217 | 218 | /** 219 | * Format the given format string by replacing {} placeholders with given arguments. 220 | */ 221 | public static String format(String format, Object... args) { 222 | for (Object arg : args) { 223 | format = replacePlaceHolder(format, arg); 224 | } 225 | return format; 226 | } 227 | 228 | /** 229 | * Replace the first occurrence of {} placeholder in format string by arg1 toString() value. 230 | */ 231 | private static String replacePlaceHolder(String format, Object arg1) { 232 | int idx = format.indexOf("{}"); 233 | if (idx > -1) { 234 | format = format.substring(0, idx) + arg1 + format.substring(idx + 2); 235 | } 236 | return format; 237 | } 238 | 239 | public static ThreadFactory threadFactory(final String name, final boolean daemon) { 240 | return new ThreadFactory() { 241 | @Override 242 | public Thread newThread(Runnable runnable) { 243 | Thread result = new Thread(runnable, name); 244 | result.setDaemon(daemon); 245 | return result; 246 | } 247 | }; 248 | } 249 | 250 | } -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/target/AnimatingTargetDrawable.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader.target; 14 | 15 | /** 16 | * Used to declare target drawable to support animation. 17 | */ 18 | public interface AnimatingTargetDrawable { 19 | 20 | /** 21 | * Is the drawable is currently animating fade-in of the image 22 | */ 23 | boolean isAnimating(); 24 | } -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/target/TargetAvatarImageView.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader.target; 14 | 15 | import android.content.Context; 16 | import android.graphics.Canvas; 17 | import android.graphics.Color; 18 | import android.graphics.Paint; 19 | import android.text.TextUtils; 20 | import android.util.AttributeSet; 21 | 22 | import com.theartofdev.fastimageloader.LoadState; 23 | import com.theartofdev.fastimageloader.impl.util.FILUtils; 24 | 25 | /** 26 | * TODO:a add doc 27 | */ 28 | public class TargetAvatarImageView extends TargetImageView { 29 | 30 | //region: Fields and Consts 31 | 32 | /** 33 | * Full AuthorName 34 | */ 35 | protected String mName; 36 | 37 | /** 38 | * Acronyms, 2 letters will be written as placeholder 39 | */ 40 | protected String mAcronyms; 41 | 42 | /** 43 | * Calculate background color, by 'AuthorName' hasecode % 30 44 | */ 45 | protected static Paint mBackPaint; 46 | 47 | /** 48 | * WHITE color painter for text 49 | */ 50 | protected static Paint mTextPaint; 51 | //endregion 52 | 53 | public TargetAvatarImageView(Context context) { 54 | super(context); 55 | setRounded(true); 56 | } 57 | 58 | public TargetAvatarImageView(Context context, AttributeSet attrs) { 59 | super(context, attrs); 60 | setRounded(true); 61 | } 62 | 63 | public TargetAvatarImageView(Context context, AttributeSet attrs, int defStyle) { 64 | super(context, attrs, defStyle); 65 | setRounded(true); 66 | } 67 | 68 | /** 69 | * See: {@link #loadAvatar(String, String, String, String)} 70 | */ 71 | public void loadAvatar(String source, String name, String specKey) { 72 | loadAvatar(source, name, specKey, null); 73 | } 74 | 75 | /** 76 | * Load avatar image from the given source, use the given name for placeholder while loading 77 | * or avatar load failed. 78 | * 79 | * @param source the avatar source URL to load from 80 | * @param name the user name to use while avatar is loading or failed 81 | * @param specKey the spec to load the image by 82 | * @param altSpecKey optional: the spec to use for memory cached image in case the primary is not found. 83 | */ 84 | public void loadAvatar(String source, String name, String specKey, String altSpecKey) { 85 | if (!TextUtils.equals(mName, name)) { 86 | mName = TextUtils.isEmpty(name) ? "UU" : name; 87 | mAcronyms = null; 88 | } 89 | loadImage(source, specKey, altSpecKey, false); 90 | } 91 | 92 | //region: Private methods 93 | 94 | @Override 95 | protected void drawPlaceholder(Canvas canvas, LoadState loadState) { 96 | if (!TextUtils.isEmpty(mName)) { 97 | if (mAcronyms == null) { 98 | init(); 99 | } 100 | 101 | FILUtils.rectF.set(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(), getHeight() - getPaddingBottom()); 102 | if (isRounded()) { 103 | canvas.drawRoundRect(FILUtils.rectF, getWidth(), getHeight(), mBackPaint); 104 | } else { 105 | canvas.drawRect(FILUtils.rectF, mBackPaint); 106 | } 107 | 108 | mTextPaint.setTextSize(getWidth() / 1.8f); 109 | int xPos = canvas.getWidth() / 2; 110 | int yPos = (int) ((canvas.getHeight() / 2) - ((mTextPaint.descent() + mTextPaint.ascent()) / 2)); 111 | canvas.drawText(mAcronyms, xPos, yPos, mTextPaint); 112 | } 113 | } 114 | 115 | /** 116 | * Init the user acronym and the paint object required. 117 | */ 118 | protected void init() { 119 | if (mBackPaint == null) { 120 | mBackPaint = new Paint(); 121 | mBackPaint.setColor(Color.LTGRAY); 122 | mBackPaint.setAntiAlias(true); 123 | } 124 | 125 | if (mTextPaint == null) { 126 | mTextPaint = new Paint(); 127 | mTextPaint.setColor(Color.DKGRAY); 128 | mTextPaint.setTextAlign(Paint.Align.CENTER); 129 | } 130 | 131 | int idx = mName.indexOf(' '); 132 | if (idx < 0) { 133 | idx = 0; 134 | while (idx + 1 < mName.length() && Character.isLowerCase(mName.charAt(idx + 1))) { 135 | idx++; 136 | } 137 | } 138 | mAcronyms = idx > -1 && idx + 1 < mName.length() 139 | ? String.format("%c%c", mName.charAt(0), mName.charAt(idx + 1)) 140 | : mName.substring(0, 1); 141 | } 142 | //endregion 143 | } -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/target/TargetCircleDrawable.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader.target; 14 | 15 | import android.graphics.Bitmap; 16 | import android.graphics.BitmapShader; 17 | import android.graphics.Canvas; 18 | import android.graphics.ColorFilter; 19 | import android.graphics.Matrix; 20 | import android.graphics.Paint; 21 | import android.graphics.Rect; 22 | import android.graphics.Shader; 23 | import android.graphics.drawable.Drawable; 24 | import android.os.SystemClock; 25 | 26 | import com.theartofdev.fastimageloader.LoadedFrom; 27 | import com.theartofdev.fastimageloader.impl.util.FILUtils; 28 | 29 | /** 30 | * Drawable used for loaded images with additional capabilities:
31 | * 1. scale the image to the set rectangle.
32 | * 2. render round image
33 | * 3. fade effect for showing the image at start.
34 | * 4. showing indicator if the image was loading from memory/disk/network.
35 | */ 36 | public class TargetCircleDrawable extends Drawable implements AnimatingTargetDrawable { 37 | 38 | //region: Fields and Consts 39 | 40 | private static final float FADE_DURATION = 200f; 41 | 42 | protected final Paint mPaint; 43 | 44 | protected final Matrix mMatrix = new Matrix(); 45 | 46 | protected final LoadedFrom mLoadedFrom; 47 | 48 | protected final float mBitmapWidth; 49 | 50 | protected final float mBitmapHeight; 51 | 52 | protected float mScale = -1; 53 | 54 | protected int mTranslateX = -1; 55 | 56 | protected int mTranslateY = -1; 57 | 58 | /** 59 | * used for fade animation progress 60 | */ 61 | protected long mStartTimeMillis; 62 | //endregion 63 | 64 | /** 65 | * @param bitmap the bitmap to render in the drawable 66 | * @param loadedFrom where the bitmap was loaded from MEMORY/DISK/NETWORK for debug indicator 67 | * @param showFade if to show fade effect starting from now 68 | */ 69 | public TargetCircleDrawable(Bitmap bitmap, LoadedFrom loadedFrom, boolean showFade) { 70 | FILUtils.notNull(bitmap, "bitmap"); 71 | 72 | mLoadedFrom = loadedFrom; 73 | 74 | mBitmapWidth = bitmap.getWidth(); 75 | mBitmapHeight = bitmap.getHeight(); 76 | 77 | mPaint = new Paint(); 78 | mPaint.setAntiAlias(true); 79 | mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)); 80 | 81 | mStartTimeMillis = showFade ? SystemClock.uptimeMillis() : 0; 82 | } 83 | 84 | @Override 85 | public boolean isAnimating() { 86 | return mStartTimeMillis > 0; 87 | } 88 | 89 | @Override 90 | public void setAlpha(int alpha) { 91 | 92 | } 93 | 94 | @Override 95 | public void setColorFilter(ColorFilter cf) { 96 | 97 | } 98 | 99 | @Override 100 | public int getOpacity() { 101 | return 0; 102 | } 103 | 104 | /** 105 | * {@inheritDoc} 106 | *

107 | * On set of bounds update the transform matrix applied on the bitmap to fit into the bounds.
108 | * - Scale to fit the dimensions of the image into the bounded rectangle.
109 | * - Offset the rendered bitmap to center the dimension that is larger\smaller than the bounds. 110 | *

111 | */ 112 | @Override 113 | public void setBounds(int left, int top, int right, int bottom) { 114 | super.setBounds(left, top, right, bottom); 115 | float scale = Math.max((right - left) / mBitmapWidth, (bottom - top) / mBitmapHeight); 116 | int translateX = (int) (((right - left) - mBitmapWidth * scale) / 2); 117 | int translateY = (int) (((bottom - top) - mBitmapHeight * scale) / 2); 118 | 119 | if (Math.abs(scale - mScale) > 0.01 || translateX != mTranslateX || translateY != mTranslateY) { 120 | mScale = scale; 121 | mTranslateX = translateX; 122 | mTranslateY = translateY; 123 | if (mScale != 0 || mTranslateX != 0 || mTranslateY != 0) { 124 | if (mScale != 0) 125 | mMatrix.setScale(mScale, mScale); 126 | if (mTranslateX != 0 || mTranslateY != 0) 127 | mMatrix.postTranslate(mTranslateX, mTranslateY); 128 | mPaint.getShader().setLocalMatrix(mMatrix); 129 | } else { 130 | mPaint.getShader().setLocalMatrix(null); 131 | } 132 | } 133 | } 134 | 135 | /** 136 | * {@inheritDoc} 137 | *

138 | * Additional functionality:
139 | * Draw bitmap with opacity to show fade-in if animating.
140 | * Draw loaded from debug indicator.
141 | *

142 | */ 143 | @Override 144 | public void draw(Canvas canvas) { 145 | float normalized = (SystemClock.uptimeMillis() - mStartTimeMillis) / FADE_DURATION; 146 | if (normalized >= 1f) { 147 | drawBitmap(canvas); 148 | if (mStartTimeMillis > 0) 149 | invalidateSelf(); 150 | mStartTimeMillis = 0; 151 | } else { 152 | mPaint.setAlpha((int) (255 * normalized)); 153 | drawBitmap(canvas); 154 | mPaint.setAlpha(255); 155 | invalidateSelf(); 156 | } 157 | 158 | if (TargetHelper.debugIndicator) { 159 | Rect bounds = getBounds(); 160 | TargetHelper.drawDebugIndicator(canvas, mLoadedFrom, bounds.width(), bounds.height()); 161 | } 162 | } 163 | 164 | /** 165 | * Draw the bitmap on the canvas either rounded or rectangular. 166 | */ 167 | protected void drawBitmap(Canvas canvas) { 168 | Rect bounds = getBounds(); 169 | FILUtils.rectF.set(0, 0, bounds.width(), bounds.height()); 170 | canvas.drawRoundRect(FILUtils.rectF, bounds.width() / 2, bounds.height() / 2, mPaint); 171 | } 172 | } -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/target/TargetDrawable.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader.target; 14 | 15 | import android.graphics.Bitmap; 16 | import android.graphics.Canvas; 17 | import android.graphics.Rect; 18 | import android.graphics.drawable.BitmapDrawable; 19 | import android.os.SystemClock; 20 | 21 | import com.theartofdev.fastimageloader.LoadedFrom; 22 | import com.theartofdev.fastimageloader.impl.util.FILUtils; 23 | 24 | /** 25 | * Drawable used for loaded images with additional capabilities:
26 | * 1. fade effect for showing the image at start.
27 | * 2. showing indicator if the image was loading from memory/disk/network.
28 | */ 29 | public class TargetDrawable extends BitmapDrawable implements AnimatingTargetDrawable { 30 | 31 | //region: Fields and Consts 32 | 33 | private static final float FADE_DURATION = 200f; 34 | 35 | protected final LoadedFrom mLoadedFrom; 36 | 37 | /** 38 | * used for fade animation progress 39 | */ 40 | protected long mStartTimeMillis; 41 | //endregion 42 | 43 | /** 44 | * @param bitmap the bitmap to render in the drawable 45 | * @param loadedFrom where the bitmap was loaded from MEMORY/DISK/NETWORK for debug indicator 46 | * @param showFade if to show fade effect starting from now 47 | */ 48 | public TargetDrawable(Bitmap bitmap, LoadedFrom loadedFrom, boolean showFade) { 49 | super(bitmap); 50 | FILUtils.notNull(bitmap, "bitmap"); 51 | 52 | mLoadedFrom = loadedFrom; 53 | 54 | mStartTimeMillis = showFade ? SystemClock.uptimeMillis() : 0; 55 | } 56 | 57 | @Override 58 | public boolean isAnimating() { 59 | return mStartTimeMillis > 0; 60 | } 61 | 62 | /** 63 | * {@inheritDoc} 64 | *

65 | * Additional functionality:
66 | * Draw bitmap with opacity to show fade-in if animating.
67 | * Draw loaded from debug indicator.
68 | *

69 | */ 70 | @Override 71 | public void draw(Canvas canvas) { 72 | float normalized = (SystemClock.uptimeMillis() - mStartTimeMillis) / FADE_DURATION; 73 | if (normalized >= 1f) { 74 | super.draw(canvas); 75 | if (mStartTimeMillis > 0) 76 | invalidateSelf(); 77 | mStartTimeMillis = 0; 78 | } else { 79 | setAlpha((int) (255 * normalized)); 80 | super.draw(canvas); 81 | setAlpha(255); 82 | invalidateSelf(); 83 | } 84 | 85 | if (TargetHelper.debugIndicator) { 86 | Rect bounds = getBounds(); 87 | TargetHelper.drawDebugIndicator(canvas, mLoadedFrom, bounds.width(), bounds.height()); 88 | } 89 | } 90 | 91 | //region: Inner class: Animated 92 | 93 | //endregion 94 | } -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/target/TargetHelper.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader.target; 14 | 15 | import android.graphics.Canvas; 16 | import android.graphics.Color; 17 | import android.graphics.Paint; 18 | import android.graphics.RectF; 19 | 20 | import com.theartofdev.fastimageloader.LoadedFrom; 21 | 22 | import static android.graphics.Color.GREEN; 23 | import static android.graphics.Color.RED; 24 | import static android.graphics.Color.WHITE; 25 | import static android.graphics.Color.YELLOW; 26 | 27 | /** 28 | * Helper methods for Target Drawable or Image View code. 29 | */ 30 | public final class TargetHelper { 31 | 32 | /** 33 | * Used to paint debug indicator 34 | */ 35 | private static Paint mDebugPaint; 36 | 37 | /** 38 | * Paint used to draw download progress 39 | */ 40 | private static Paint mProgressPaint; 41 | 42 | /** 43 | * If to show indicator if the image was loaded from MEMORY/DISK/NETWORK. 44 | */ 45 | public static boolean debugIndicator; 46 | 47 | /** 48 | * The density of the current 49 | */ 50 | public static float mDensity; 51 | 52 | private TargetHelper() { 53 | } 54 | 55 | /** 56 | * draw indicator on where the image was loaded from.
57 | * Green - memory, Yellow - disk, Red - network. 58 | */ 59 | public static void drawDebugIndicator(Canvas canvas, LoadedFrom loadedFrom, int width, int height) { 60 | if (debugIndicator) { 61 | if (mDebugPaint == null) { 62 | mDebugPaint = new Paint(); 63 | mDebugPaint.setAntiAlias(true); 64 | } 65 | 66 | mDebugPaint.setColor(WHITE); 67 | canvas.drawCircle(width / 2, height / 2, 6 * mDensity, mDebugPaint); 68 | 69 | mDebugPaint.setColor(loadedFrom == LoadedFrom.MEMORY ? GREEN : loadedFrom == LoadedFrom.DISK ? YELLOW : RED); 70 | canvas.drawCircle(width / 2, height / 2, 4 * mDensity, mDebugPaint); 71 | } 72 | } 73 | 74 | /** 75 | * Draw indicator of download progress. 76 | * 77 | * @param downloaded downloaded bytes 78 | * @param contentLength total bytes 79 | */ 80 | public static void drawProgressIndicator(Canvas canvas, long downloaded, long contentLength) { 81 | if (contentLength > 0 && downloaded < contentLength) { 82 | if (mProgressPaint == null) { 83 | mProgressPaint = new Paint(); 84 | mProgressPaint.setAntiAlias(true); 85 | mProgressPaint.setColor(Color.argb(160, 0, 160, 0)); 86 | } 87 | 88 | float s = (float) Math.min(36 * mDensity, Math.min(canvas.getWidth() * .2, canvas.getHeight() * .2)); 89 | int l = canvas.getWidth() / 2; 90 | int t = canvas.getHeight() / 2; 91 | RectF rect = new RectF(l - s / 2, t - s / 2, l + s / 2, t + s / 2); 92 | canvas.drawArc(rect, -90, 360f * downloaded / contentLength, true, mProgressPaint); 93 | } 94 | } 95 | } 96 | 97 | -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/target/TargetImageView.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader.target; 14 | 15 | import android.content.Context; 16 | import android.graphics.Canvas; 17 | import android.graphics.drawable.Drawable; 18 | import android.util.AttributeSet; 19 | import android.widget.ImageView; 20 | 21 | import com.theartofdev.fastimageloader.LoadState; 22 | import com.theartofdev.fastimageloader.impl.util.FILUtils; 23 | 24 | /** 25 | * {@link ImageView} with embedded handling of loading image using {@link com.theartofdev.fastimageloader.FastImageLoader} 26 | * and managing its lifecycle. 27 | */ 28 | public class TargetImageView extends ImageView { 29 | 30 | //region: Fields and Consts 31 | 32 | /** 33 | * The target image handler to load the image and control its lifecycle. 34 | */ 35 | protected TargetImageViewHandler mHandler; 36 | 37 | /** 38 | * The placeholder drawable to draw while the image is not loaded 39 | */ 40 | protected Drawable mPlaceholder; 41 | //endregion 42 | 43 | public TargetImageView(Context context) { 44 | super(context); 45 | mHandler = new TargetImageViewHandler(this); 46 | mHandler.setInvalidateOnDownloading(true); 47 | } 48 | 49 | public TargetImageView(Context context, AttributeSet attrs) { 50 | super(context, attrs); 51 | mHandler = new TargetImageViewHandler(this); 52 | mHandler.setInvalidateOnDownloading(true); 53 | } 54 | 55 | public TargetImageView(Context context, AttributeSet attrs, int defStyle) { 56 | super(context, attrs, defStyle); 57 | mHandler = new TargetImageViewHandler(this); 58 | mHandler.setInvalidateOnDownloading(true); 59 | } 60 | 61 | /** 62 | * The URL source of the image 63 | */ 64 | public String getUrl() { 65 | return mHandler.getUri(); 66 | } 67 | 68 | /** 69 | * the spec to load the image by 70 | */ 71 | public String getSpecKey() { 72 | return mHandler.getSpecKey(); 73 | } 74 | 75 | /** 76 | * Is the image should be rendered rounded 77 | */ 78 | public boolean isRounded() { 79 | return mHandler.isRounded(); 80 | } 81 | 82 | /** 83 | * Is the image should be rendered rounded 84 | */ 85 | public void setRounded(boolean isRounded) { 86 | mHandler.setRounded(isRounded); 87 | } 88 | 89 | /** 90 | * If to show download progress indicator when the requested image is downloading. 91 | */ 92 | public boolean isShowDownloadProgressIndicator() { 93 | return mHandler.isInvalidateOnDownloading(); 94 | } 95 | 96 | /** 97 | * If to show download progress indicator when the requested image is downloading. 98 | */ 99 | public void setShowDownloadProgressIndicator(boolean show) { 100 | mHandler.setInvalidateOnDownloading(show); 101 | } 102 | 103 | /** 104 | * The placeholder drawable to draw while the image is not loaded 105 | */ 106 | public Drawable getPlaceholder() { 107 | return mPlaceholder; 108 | } 109 | 110 | /** 111 | * The placeholder drawable to draw while the image is not loaded 112 | */ 113 | public void setPlaceholder(Drawable placeholder) { 114 | mPlaceholder = placeholder; 115 | } 116 | 117 | @Override 118 | public void setImageDrawable(Drawable drawable) { 119 | super.setImageDrawable(drawable); 120 | if (drawable == null) { 121 | mHandler.clearUsedBitmap(); 122 | } 123 | } 124 | 125 | /** 126 | * See: {@link #loadImage(String, String, String, boolean)}. 127 | */ 128 | public void loadImage(String url, String specKey) { 129 | mHandler.loadImage(url, specKey, null, false); 130 | } 131 | 132 | /** 133 | * See: {@link #loadImage(String, String, String, boolean)}. 134 | */ 135 | public void loadImage(String url, String specKey, String altSpecKey) { 136 | mHandler.loadImage(url, specKey, altSpecKey, false); 137 | } 138 | 139 | /** 140 | * Load image from the given source. 141 | * 142 | * @param url the avatar source URL to load from 143 | * @param specKey the spec to load the image by 144 | * @param altSpecKey optional: the spec to use for memory cached image in case the primary is not found. 145 | * @param force true - force image load even if it is the same source 146 | */ 147 | public void loadImage(String url, String specKey, String altSpecKey, boolean force) { 148 | mHandler.loadImage(url, specKey, altSpecKey, force); 149 | } 150 | 151 | /** 152 | * On image view visibility change set show/hide on the image handler to it will update its in-use status. 153 | */ 154 | @Override 155 | protected void onWindowVisibilityChanged(int visibility) { 156 | super.onWindowVisibilityChanged(visibility); 157 | mHandler.onViewVisibilityChanged(visibility); 158 | } 159 | 160 | /** 161 | * Override draw to draw placeholder before the image if it is not loaded yet or animating fade-in. 162 | */ 163 | @Override 164 | public void onDraw(@SuppressWarnings("NullableProblems") Canvas canvas) { 165 | if (getDrawable() == null || mHandler.isAnimating()) { 166 | drawPlaceholder(canvas, mHandler.getLoadState()); 167 | } 168 | 169 | super.onDraw(canvas); 170 | 171 | if (isShowDownloadProgressIndicator() && mHandler.getContentLength() > 0 && mHandler.getDownloaded() < mHandler.getContentLength()) { 172 | drawProgressIndicator(canvas); 173 | } 174 | } 175 | 176 | /** 177 | * Draw placeholder if the image is loading/animating to show or failed to load. 178 | * 179 | * @param loadState the current load state of the image to draw specific placeholder 180 | */ 181 | protected void drawPlaceholder(Canvas canvas, LoadState loadState) { 182 | if (mPlaceholder != null) { 183 | canvas.getClipBounds(FILUtils.rect); 184 | mPlaceholder.setBounds(FILUtils.rect); 185 | mPlaceholder.draw(canvas); 186 | } 187 | } 188 | 189 | /** 190 | * Draw indicator of download progress. 191 | */ 192 | protected void drawProgressIndicator(Canvas canvas) { 193 | TargetHelper.drawProgressIndicator(canvas, mHandler.getDownloaded(), mHandler.getContentLength()); 194 | } 195 | } -------------------------------------------------------------------------------- /fastimageloader/src/main/java/com/theartofdev/fastimageloader/target/TargetImageViewHandler.java: -------------------------------------------------------------------------------- 1 | // "Therefore those skilled at the unorthodox 2 | // are infinite as heaven and earth, 3 | // inexhaustible as the great rivers. 4 | // When they come to an end, 5 | // they begin again, 6 | // like the days and months; 7 | // they die and are reborn, 8 | // like the four seasons." 9 | // 10 | // - Sun Tsu, 11 | // "The Art of War" 12 | 13 | package com.theartofdev.fastimageloader.target; 14 | 15 | import android.graphics.drawable.Drawable; 16 | import android.text.TextUtils; 17 | import android.view.View; 18 | import android.widget.ImageView; 19 | 20 | import com.theartofdev.fastimageloader.FastImageLoader; 21 | import com.theartofdev.fastimageloader.LoadState; 22 | import com.theartofdev.fastimageloader.LoadedFrom; 23 | import com.theartofdev.fastimageloader.ReusableBitmap; 24 | import com.theartofdev.fastimageloader.Target; 25 | import com.theartofdev.fastimageloader.impl.util.FILLogger; 26 | import com.theartofdev.fastimageloader.impl.util.FILUtils; 27 | 28 | /** 29 | * Handler for loading image as {@link com.theartofdev.fastimageloader.ReusableBitmap} and managing its lifecycle.
30 | * A single instance of the handler should be used for each ImageView.
31 | *

32 | * Use {@link #loadImage(String, String, String, boolean)} to load of image into the 33 | * handler, it will handle cancellation of unfinished requests if a new loading request is given. 34 | *

35 | * The handler attaches itself to ImageView StateChange to update the in-use of the loaded 36 | * bitmap, allowing to reuse bitmaps that are detached from window. This ensures the bitmap is 37 | * reused when the activity/fragment is destroyed.
38 | * On reattach to window if the bitmap was reused the image will be reloaded. 39 | *

40 | * For improved reuse it is advisable to override {@link android.widget.ImageView#onWindowVisibilityChanged(int)} 41 | * method and call {@link #onViewShown()}/{@link #onViewHidden()} on the handler to update in-use state. 42 | *
 43 |  * {@code protected void onWindowVisibilityChanged(int visibility) {
 44 |  *   super.onWindowVisibilityChanged(visibility);
 45 |  *   if (visibility == VISIBLE) {
 46 |  *     mHandler.onViewShown();
 47 |  *   } else {
 48 |  *     mHandler.onViewHidden();
 49 |  *   }
 50 |  * }}
 51 |  * 
52 | */ 53 | public class TargetImageViewHandler implements Target, View.OnAttachStateChangeListener { 54 | 55 | //region: Fields and Consts 56 | 57 | /** 58 | * the image view 59 | */ 60 | protected final ImageView mImageView; 61 | 62 | /** 63 | * The URL source of the image 64 | */ 65 | protected String mUrl; 66 | 67 | /** 68 | * the spec to load the image by 69 | */ 70 | protected String mSpecKey; 71 | 72 | /** 73 | * The loaded image 74 | */ 75 | protected ReusableBitmap mReusableBitmap; 76 | 77 | /** 78 | * Is the recycle bitmap is currently set in use in this image view, so not to set twice 79 | */ 80 | protected boolean mInUse; 81 | 82 | /** 83 | * Is the image should be rendered rounded 84 | */ 85 | protected boolean mRounded; 86 | 87 | /** 88 | * Is the handled image view should be invalidated on bitmap downloading progress event 89 | */ 90 | protected boolean mInvalidateOnDownloading; 91 | 92 | /** 93 | * The current loading states of the image in the handler. 94 | */ 95 | protected LoadState mLoadState = LoadState.UNSET; 96 | 97 | /** 98 | * when the image load request started, measure image load request time 99 | */ 100 | protected long mStartImageLoadTime; 101 | 102 | /** 103 | * the number of bytes already downloaded, if requested image is downloading 104 | */ 105 | protected long mDownloaded; 106 | 107 | /** 108 | * the total number of bytes to download, if requested image is downloading 109 | */ 110 | protected long mContentLength; 111 | //endregion 112 | 113 | /** 114 | * @param imageView The image view to handle. 115 | */ 116 | public TargetImageViewHandler(ImageView imageView) { 117 | FILUtils.notNull(imageView, "imageView"); 118 | 119 | mImageView = imageView; 120 | mImageView.addOnAttachStateChangeListener(this); 121 | } 122 | 123 | /** 124 | * The current loading states of the image in the handler. 125 | */ 126 | public LoadState getLoadState() { 127 | return mLoadState; 128 | } 129 | 130 | /** 131 | * Is the drawable is currently animating fade-in of the image 132 | */ 133 | public boolean isAnimating() { 134 | Drawable drawable = mImageView.getDrawable(); 135 | return drawable != null && 136 | drawable instanceof AnimatingTargetDrawable && 137 | ((AnimatingTargetDrawable) drawable).isAnimating(); 138 | } 139 | 140 | /** 141 | * Is the image should be rendered rounded 142 | */ 143 | public boolean isRounded() { 144 | return mRounded; 145 | } 146 | 147 | /** 148 | * Is the image should be rendered rounded 149 | */ 150 | public void setRounded(boolean isRounded) { 151 | mRounded = isRounded; 152 | } 153 | 154 | /** 155 | * Is the handled image view should be invalidated on bitmap downloading progress event 156 | */ 157 | public boolean isInvalidateOnDownloading() { 158 | return mInvalidateOnDownloading; 159 | } 160 | 161 | /** 162 | * Is the handled image view should be invalidated on bitmap downloading progress event 163 | */ 164 | public void setInvalidateOnDownloading(boolean invalidateOnDownloading) { 165 | mInvalidateOnDownloading = invalidateOnDownloading; 166 | } 167 | 168 | /** 169 | * the number of bytes already downloaded, if requested image is downloading 170 | */ 171 | public long getDownloaded() { 172 | return mDownloaded; 173 | } 174 | 175 | /** 176 | * the total number of bytes to download, if requested image is downloading 177 | */ 178 | public long getContentLength() { 179 | return mContentLength; 180 | } 181 | 182 | @Override 183 | public String getUri() { 184 | return mUrl; 185 | } 186 | 187 | @Override 188 | public String getSpecKey() { 189 | return mSpecKey; 190 | } 191 | 192 | /** 193 | * See {@link #loadImage(String, String, String, boolean)}. 194 | */ 195 | public void loadImage(String url, String specKey) { 196 | loadImage(url, specKey, null, false); 197 | } 198 | 199 | /** 200 | * See {@link #loadImage(String, String, String, boolean)}. 201 | */ 202 | public void loadImage(String url, String specKey, String altSpecKey) { 203 | loadImage(url, specKey, altSpecKey, false); 204 | } 205 | 206 | /** 207 | * Load image from the given source.
208 | * If image of the same source is already requested/loaded the request is ignored unless force is true. 209 | * 210 | * @param url the avatar source URL to load from 211 | * @param specKey the spec to load the image by 212 | * @param altSpecKey optional: the spec to use for memory cached image in case the primary is not found. 213 | * @param force true - force image load even if it is the same source 214 | */ 215 | public void loadImage(String url, String specKey, String altSpecKey, boolean force) { 216 | FILUtils.notNull(specKey, "spec"); 217 | 218 | mDownloaded = 0; 219 | mContentLength = 0; 220 | if (!TextUtils.equals(mUrl, url) || TextUtils.isEmpty(url) || force) { 221 | mStartImageLoadTime = System.currentTimeMillis(); 222 | clearImage(); 223 | 224 | mUrl = url; 225 | mSpecKey = specKey; 226 | 227 | if (!TextUtils.isEmpty(url)) { 228 | mLoadState = LoadState.LOADING; 229 | FastImageLoader.loadImage(this, altSpecKey); 230 | } else { 231 | clearUsedBitmap(); 232 | clearImage(); 233 | } 234 | mImageView.invalidate(); 235 | } 236 | } 237 | 238 | /** 239 | * Clear the currently used bitmap and mark it as not in use. 240 | */ 241 | public void clearUsedBitmap() { 242 | clearUsedBitmap(true); 243 | } 244 | 245 | @Override 246 | public void onBitmapDownloading(long downloaded, long contentLength) { 247 | mDownloaded = downloaded; 248 | mContentLength = contentLength; 249 | if (mInvalidateOnDownloading) { 250 | mImageView.postInvalidate(); 251 | } 252 | } 253 | 254 | @Override 255 | public void onBitmapLoaded(ReusableBitmap bitmap, LoadedFrom from) { 256 | 257 | clearUsedBitmap(false); 258 | 259 | mLoadState = LoadState.LOADED; 260 | 261 | mInUse = true; 262 | mReusableBitmap = bitmap; 263 | mReusableBitmap.incrementInUse(); 264 | 265 | setImage(bitmap, from); 266 | 267 | FILLogger.operation(mUrl, mSpecKey, from, true, System.currentTimeMillis() - mStartImageLoadTime); 268 | } 269 | 270 | @Override 271 | public void onBitmapFailed() { 272 | String url = mUrl; 273 | String specKey = mSpecKey; 274 | mLoadState = LoadState.FAILED; 275 | if (mImageView.getDrawable() == null) { 276 | clearImage(); 277 | mUrl = url; 278 | mSpecKey = specKey; 279 | mImageView.invalidate(); 280 | } 281 | FILLogger.operation(url, specKey, null, false, System.currentTimeMillis() - mStartImageLoadTime); 282 | } 283 | 284 | /** 285 | * Handle Image View visibility change by updating the used bitmap.
286 | * If the Image View is hidden then we decrement the in-use.
287 | * If the Image View is shown we use the existing bitmap or reload the image. 288 | */ 289 | public void onViewVisibilityChanged(int visibility) { 290 | if (visibility == View.VISIBLE) { 291 | onViewShown(); 292 | } else { 293 | onViewHidden(); 294 | } 295 | } 296 | 297 | /** 298 | * On attach of the ImageView to window call {@link #onViewShown()}. 299 | */ 300 | @Override 301 | public void onViewAttachedToWindow(View v) { 302 | onViewShown(); 303 | } 304 | 305 | /** 306 | * On detach of the ImageView from window call {@link #onViewHidden()}. 307 | */ 308 | @Override 309 | public void onViewDetachedFromWindow(View v) { 310 | onViewHidden(); 311 | } 312 | 313 | /** 314 | * On image view shown verify that the set bitmap is still valid for the image view (not reused).
315 | * If valid: set in-use on the bitmap.
316 | * If not valid: execute image load request to re-load the image needed for the image view.
317 | */ 318 | protected void onViewShown() { 319 | if (mReusableBitmap != null && !mInUse) { 320 | if (TextUtils.equals(mReusableBitmap.getUri(), mUrl)) { 321 | mInUse = true; 322 | mReusableBitmap.incrementInUse(); 323 | } else { 324 | FILLogger.info("ImageView attachToWindow uses recycled bitmap, reload... [{}]", mReusableBitmap); 325 | loadImage(mUrl, mSpecKey, null, true); 326 | } 327 | } 328 | } 329 | 330 | /** 331 | * On image view hidden set the used bitmap to not-in-use so it can be reused. 332 | */ 333 | protected void onViewHidden() { 334 | if (mReusableBitmap != null && mInUse) { 335 | mInUse = false; 336 | mReusableBitmap.decrementInUse(); 337 | } 338 | } 339 | 340 | /** 341 | * Release the resource, unregister from state change. 342 | */ 343 | public void close() { 344 | clearUsedBitmap(); 345 | mImageView.removeOnAttachStateChangeListener(this); 346 | } 347 | 348 | /** 349 | * Called to clear the existing image in the handled image view.
350 | * Called on loading or failure. 351 | */ 352 | protected void clearImage() { 353 | mImageView.setImageDrawable(null); 354 | } 355 | 356 | /** 357 | * Called to set the loaded image bitmap in the handled image view. 358 | */ 359 | protected void setImage(ReusableBitmap bitmap, LoadedFrom from) { 360 | boolean showFade = from == LoadedFrom.NETWORK && mImageView.getDrawable() == null; 361 | Drawable drawable = mRounded 362 | ? new TargetCircleDrawable(bitmap.getBitmap(), from, showFade) 363 | : new TargetDrawable(bitmap.getBitmap(), from, showFade); 364 | mImageView.setImageDrawable(drawable); 365 | } 366 | 367 | /** 368 | * Clear the currently used bitmap and mark it as not in use. 369 | */ 370 | protected void clearUsedBitmap(boolean full) { 371 | if (full) { 372 | mUrl = null; 373 | mSpecKey = null; 374 | } 375 | mLoadState = LoadState.UNSET; 376 | if (mReusableBitmap != null) { 377 | if (mInUse) { 378 | mInUse = false; 379 | mReusableBitmap.decrementInUse(); 380 | } 381 | mReusableBitmap = null; 382 | } 383 | } 384 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArthurHub/Android-Fast-Image-Loader/b794be15c2ddaf3157bdb27ef9ec32995f3f5266/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Apr 10 15:27:10 PDT 2013 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Arthur Teplitzki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':demo', ':fastimageloader' 2 | --------------------------------------------------------------------------------