├── .classpath ├── .gitignore ├── .project ├── .settings └── org.eclipse.jdt.core.prefs ├── AndroidManifest.xml ├── README.md ├── libs └── android-support-v4.jar ├── pom.xml ├── proguard-project.txt ├── project.properties ├── res ├── drawable-hdpi │ ├── empty_photo.png │ ├── ic_launcher.png │ ├── news_item_bg.9.png │ └── scrollbar_handle_accelerated_anim2.9.png ├── drawable-ldpi │ └── ic_launcher.png ├── drawable-mdpi │ └── ic_launcher.png ├── drawable-xhdpi │ └── ic_launcher.png ├── drawable-xxhdpi │ └── ic_launcher.png ├── layout │ ├── activity_main.xml │ └── infos_list.xml ├── menu │ └── activity_main.xml ├── values-v11 │ └── styles.xml ├── values-v14 │ └── styles.xml └── values │ ├── attrs.xml │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml └── src └── com ├── dodola ├── model │ ├── DuitangInfo.java │ └── Infos.java └── waterex │ ├── MainActivity.java │ └── StaggeredAdapter.java ├── example └── android │ └── bitmapfun │ └── util │ ├── DiskLruCache.java │ ├── Helper.java │ ├── ImageCache.java │ ├── ImageFetcher.java │ ├── ImageResizer.java │ ├── ImageWorker.java │ ├── RetainFragment.java │ └── Utils.java └── origamilabs └── library └── views ├── FastScroller.java ├── ScrollerCompat.java ├── ScrollerCompatIcs.java └── StaggeredGridView.java /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # files for the dex VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # generated files 12 | bin/ 13 | gen/ 14 | 15 | # Local configuration file (sdk path, etc) 16 | local.properties 17 | 18 | # Eclipse project files 19 | #.classpath 20 | #.project 21 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | WaterFallEx 4 | 5 | 6 | 7 | 8 | 9 | com.android.ide.eclipse.adt.ResourceManagerBuilder 10 | 11 | 12 | 13 | 14 | com.android.ide.eclipse.adt.PreCompilerBuilder 15 | 16 | 17 | 18 | 19 | org.eclipse.jdt.core.javabuilder 20 | 21 | 22 | 23 | 24 | com.android.ide.eclipse.adt.ApkBuilder 25 | 26 | 27 | 28 | 29 | 30 | com.android.ide.eclipse.adt.AndroidNature 31 | org.eclipse.jdt.core.javanature 32 | 33 | 34 | -------------------------------------------------------------------------------- /.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 3 | org.eclipse.jdt.core.compiler.compliance=1.6 4 | org.eclipse.jdt.core.compiler.source=1.6 5 | -------------------------------------------------------------------------------- /AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WaterFallExt 2 | ============ 3 | 4 | 增强版的瀑布流 5 | -------------------------------------------------------------------------------- /libs/android-support-v4.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodola/WaterFallExt/f24dcf8a55867497e94f71bcb572b35d28169dd9/libs/android-support-v4.jar -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | 7 | com.origamilabs.library 8 | StaggeredGridView 9 | 1.0 10 | StaggeredGridView 11 | apklib 12 | 13 | 14 | 1.6 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | com.google.android 28 | android 29 | provided 30 | 4.1.1.4 31 | 32 | 33 | android.support 34 | compatibility-v4 35 | 11 36 | 37 | 38 | 39 | 40 | src 41 | 42 | 43 | 44 | 45 | org.apache.maven.plugins 46 | maven-compiler-plugin 47 | 2.5 48 | 49 | ${java.version} 50 | ${java.version} 51 | 52 | 53 | 54 | 55 | com.jayway.maven.plugins.android.generation2 56 | android-maven-plugin 57 | 3.4.1 58 | 59 | 60 | 16 61 | 62 | 63 | 64 | 65 | 66 | com.jayway.maven.plugins.android.generation2 67 | android-maven-plugin 68 | 3.4.1 69 | true 70 | 71 | ignored 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /proguard-project.txt: -------------------------------------------------------------------------------- 1 | # To enable ProGuard in your project, edit project.properties 2 | # to define the proguard.config property as described in that file. 3 | # 4 | # Add project specific ProGuard rules here. 5 | # By default, the flags in this file are appended to flags specified 6 | # in ${sdk.dir}/tools/proguard/proguard-android.txt 7 | # You can edit the include path and order by changing the ProGuard 8 | # include property in project.properties. 9 | # 10 | # For more details, see 11 | # http://developer.android.com/guide/developing/tools/proguard.html 12 | 13 | # Add any project specific keep options here: 14 | 15 | # If your project uses WebView with JS, uncomment the following 16 | # and specify the fully qualified class name to the JavaScript interface 17 | # class: 18 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 19 | # public *; 20 | #} 21 | -------------------------------------------------------------------------------- /project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system edit 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | # 10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): 11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt 12 | 13 | # Project target. 14 | target=android-17 15 | android.library=false 16 | android.library.reference.1=../StaggeredGridView 17 | -------------------------------------------------------------------------------- /res/drawable-hdpi/empty_photo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodola/WaterFallExt/f24dcf8a55867497e94f71bcb572b35d28169dd9/res/drawable-hdpi/empty_photo.png -------------------------------------------------------------------------------- /res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodola/WaterFallExt/f24dcf8a55867497e94f71bcb572b35d28169dd9/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /res/drawable-hdpi/news_item_bg.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodola/WaterFallExt/f24dcf8a55867497e94f71bcb572b35d28169dd9/res/drawable-hdpi/news_item_bg.9.png -------------------------------------------------------------------------------- /res/drawable-hdpi/scrollbar_handle_accelerated_anim2.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodola/WaterFallExt/f24dcf8a55867497e94f71bcb572b35d28169dd9/res/drawable-hdpi/scrollbar_handle_accelerated_anim2.9.png -------------------------------------------------------------------------------- /res/drawable-ldpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodola/WaterFallExt/f24dcf8a55867497e94f71bcb572b35d28169dd9/res/drawable-ldpi/ic_launcher.png -------------------------------------------------------------------------------- /res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodola/WaterFallExt/f24dcf8a55867497e94f71bcb572b35d28169dd9/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodola/WaterFallExt/f24dcf8a55867497e94f71bcb572b35d28169dd9/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodola/WaterFallExt/f24dcf8a55867497e94f71bcb572b35d28169dd9/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | 17 | -------------------------------------------------------------------------------- /res/layout/infos_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 15 | 16 | 22 | 23 | 30 | 31 | -------------------------------------------------------------------------------- /res/menu/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /res/values-v11/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /res/values-v14/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #FF000000 5 | 6 | -------------------------------------------------------------------------------- /res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 1dp 3 | 4 | 5 | 84dp 6 | 7 | 63dp 8 | 10 | 48dip 11 | 64dip 12 | 13 | 25dip 14 | 15 | 104dp 16 | 17 | 64dp 18 | 19 | 52dp 20 | -------------------------------------------------------------------------------- /res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 瀑布流Ex 5 | Hello world! 6 | Settings 7 | 8 | -------------------------------------------------------------------------------- /res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 14 | 15 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /src/com/dodola/model/DuitangInfo.java: -------------------------------------------------------------------------------- 1 | package com.dodola.model; 2 | 3 | public class DuitangInfo { 4 | 5 | private int height; 6 | private String albid = ""; 7 | private String msg = ""; 8 | private String isrc = ""; 9 | 10 | public int getWidth(){ 11 | return 200; 12 | } 13 | public String getAlbid() { 14 | return albid; 15 | } 16 | 17 | public void setAlbid(String albid) { 18 | this.albid = albid; 19 | } 20 | 21 | public String getMsg() { 22 | return msg; 23 | } 24 | 25 | public void setMsg(String msg) { 26 | this.msg = msg; 27 | } 28 | 29 | public String getIsrc() { 30 | return isrc; 31 | } 32 | 33 | public void setIsrc(String isrc) { 34 | this.isrc = isrc; 35 | } 36 | 37 | public int getHeight() { 38 | return height; 39 | } 40 | 41 | public void setHeight(int height) { 42 | this.height = height; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/com/dodola/model/Infos.java: -------------------------------------------------------------------------------- 1 | package com.dodola.model; 2 | 3 | import java.util.List; 4 | 5 | public class Infos { 6 | private String newsLast = "0"; 7 | private int type = 0; 8 | private List newsInfos; 9 | 10 | public String getNewsLast() { 11 | return newsLast; 12 | } 13 | 14 | public void setNewsLast(String newsLast) { 15 | this.newsLast = newsLast; 16 | } 17 | 18 | public int getType() { 19 | return type; 20 | } 21 | 22 | public void setType(int type) { 23 | this.type = type; 24 | } 25 | 26 | public List getNewsInfos() { 27 | return newsInfos; 28 | } 29 | 30 | public void setNewsInfos(List newsInfos) { 31 | this.newsInfos = newsInfos; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/com/dodola/waterex/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.dodola.waterex; 2 | 3 | import java.io.IOException; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | import org.json.JSONArray; 8 | import org.json.JSONException; 9 | import org.json.JSONObject; 10 | 11 | import android.app.Activity; 12 | import android.content.Context; 13 | import android.os.AsyncTask; 14 | import android.os.Bundle; 15 | import android.os.AsyncTask.Status; 16 | import android.util.Log; 17 | import android.view.Menu; 18 | 19 | import com.dodola.model.DuitangInfo; 20 | import com.dodola.waterex.R; 21 | import com.example.android.bitmapfun.util.Helper; 22 | import com.example.android.bitmapfun.util.ImageFetcher; 23 | import com.origamilabs.library.views.StaggeredGridView; 24 | 25 | /** This will not work so great since the heights of the imageViews are 26 | * calculated on the iamgeLoader callback ruining the offsets. To fix this try 27 | * to get the (intrinsic) image width and height and set the views height 28 | * manually. I will look into a fix once I find extra time. 29 | * 30 | * @author Maurycy Wojtowicz */ 31 | public class MainActivity extends Activity { 32 | private ImageFetcher mImageFetcher; 33 | private StaggeredAdapter mAdapter; 34 | private ContentTask task = new ContentTask(this, 2); 35 | 36 | @Override 37 | protected void onCreate(Bundle savedInstanceState) { 38 | super.onCreate(savedInstanceState); 39 | setContentView(R.layout.activity_main); 40 | mImageFetcher = new ImageFetcher(this, 240); 41 | mImageFetcher.setLoadingImage(R.drawable.empty_photo); 42 | StaggeredGridView gridView = (StaggeredGridView) this.findViewById(R.id.staggeredGridView1); 43 | 44 | int margin = getResources().getDimensionPixelSize(R.dimen.margin); 45 | 46 | gridView.setFastScrollEnabled(true); 47 | 48 | mAdapter = new StaggeredAdapter(MainActivity.this, mImageFetcher); 49 | gridView.setAdapter(mAdapter); 50 | mAdapter.notifyDataSetChanged(); 51 | AddItemToContainer(1, 1); 52 | AddItemToContainer(2, 1); 53 | AddItemToContainer(3, 1); 54 | 55 | } 56 | 57 | private void AddItemToContainer(int pageindex, int type) { 58 | if (task.getStatus() != Status.RUNNING) { 59 | String url = "http://www.duitang.com/album/1733789/masn/p/" + pageindex + "/24/"; 60 | Log.d("MainActivity", "current url:" + url); 61 | ContentTask task = new ContentTask(this, type); 62 | task.execute(url); 63 | 64 | } 65 | } 66 | 67 | @Override 68 | public boolean onCreateOptionsMenu(Menu menu) { 69 | getMenuInflater().inflate(R.menu.activity_main, menu); 70 | return true; 71 | } 72 | 73 | private class ContentTask extends AsyncTask> { 74 | 75 | private Context mContext; 76 | private int mType = 1; 77 | 78 | public ContentTask(Context context, int type) { 79 | super(); 80 | mContext = context; 81 | mType = type; 82 | } 83 | 84 | @Override 85 | protected List doInBackground(String... params) { 86 | try { 87 | return parseNewsJSON(params[0]); 88 | } catch (IOException e) { 89 | e.printStackTrace(); 90 | } 91 | return null; 92 | } 93 | 94 | @Override 95 | protected void onPostExecute(List result) { 96 | if (mType == 1) { 97 | 98 | mAdapter.addItemTop(result); 99 | mAdapter.notifyDataSetChanged(); 100 | 101 | } else if (mType == 2) { 102 | mAdapter.addItemLast(result); 103 | mAdapter.notifyDataSetChanged(); 104 | 105 | } 106 | 107 | } 108 | 109 | @Override 110 | protected void onPreExecute() { 111 | } 112 | 113 | public List parseNewsJSON(String url) throws IOException { 114 | List duitangs = new ArrayList(); 115 | String json = ""; 116 | if (Helper.checkConnection(mContext)) { 117 | try { 118 | json = Helper.getStringFromUrl(url); 119 | 120 | } catch (IOException e) { 121 | Log.e("IOException is : ", e.toString()); 122 | e.printStackTrace(); 123 | return duitangs; 124 | } 125 | } 126 | Log.d("MainActiivty", "json:" + json); 127 | 128 | try { 129 | if (null != json) { 130 | JSONObject newsObject = new JSONObject(json); 131 | JSONObject jsonObject = newsObject.getJSONObject("data"); 132 | JSONArray blogsJson = jsonObject.getJSONArray("blogs"); 133 | 134 | for (int i = 0; i < blogsJson.length(); i++) { 135 | JSONObject newsInfoLeftObject = blogsJson.getJSONObject(i); 136 | DuitangInfo newsInfo1 = new DuitangInfo(); 137 | newsInfo1.setAlbid(newsInfoLeftObject.isNull("albid") ? "" : newsInfoLeftObject.getString("albid")); 138 | newsInfo1.setIsrc(newsInfoLeftObject.isNull("isrc") ? "" : newsInfoLeftObject.getString("isrc")); 139 | newsInfo1.setMsg(newsInfoLeftObject.isNull("msg") ? "" : newsInfoLeftObject.getString("msg")); 140 | newsInfo1.setHeight(newsInfoLeftObject.getInt("iht")); 141 | duitangs.add(newsInfo1); 142 | } 143 | } 144 | } catch (JSONException e) { 145 | e.printStackTrace(); 146 | } 147 | return duitangs; 148 | } 149 | } 150 | 151 | } 152 | -------------------------------------------------------------------------------- /src/com/dodola/waterex/StaggeredAdapter.java: -------------------------------------------------------------------------------- 1 | package com.dodola.waterex; 2 | 3 | import java.util.LinkedList; 4 | import java.util.List; 5 | 6 | import android.content.Context; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.widget.BaseAdapter; 11 | import android.widget.ImageView; 12 | import android.widget.LinearLayout; 13 | import android.widget.TextView; 14 | 15 | import com.dodola.model.DuitangInfo; 16 | import com.dodola.waterex.R; 17 | import com.example.android.bitmapfun.util.ImageFetcher; 18 | 19 | public class StaggeredAdapter extends BaseAdapter { 20 | private LinkedList mInfos; 21 | ImageFetcher mImageFetcher; 22 | 23 | public StaggeredAdapter(Context context, ImageFetcher f) { 24 | mInfos = new LinkedList(); 25 | mImageFetcher = f; 26 | } 27 | 28 | @Override 29 | public View getView(int position, View convertView, ViewGroup parent) { 30 | 31 | ViewHolder holder; 32 | DuitangInfo duitangInfo = mInfos.get(position); 33 | 34 | if (convertView == null) { 35 | LayoutInflater layoutInflator = LayoutInflater.from(parent.getContext()); 36 | convertView = layoutInflator.inflate(R.layout.infos_list, null); 37 | holder = new ViewHolder(); 38 | holder.imageView = (ImageView) convertView.findViewById(R.id.news_pic); 39 | holder.contentView = (TextView) convertView.findViewById(R.id.news_title); 40 | convertView.setTag(holder); 41 | } 42 | holder = (ViewHolder) convertView. getTag(); 43 | 44 | 45 | // float iHeight = ((float) 200 / 183 * duitangInfo.getHeight()); 46 | holder.imageView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, (int) duitangInfo.getHeight())); 47 | 48 | holder.contentView.setText(duitangInfo.getMsg()); 49 | mImageFetcher.loadImage(duitangInfo.getIsrc(), holder.imageView); 50 | return convertView; 51 | } 52 | 53 | class ViewHolder { 54 | ImageView imageView; 55 | TextView contentView; 56 | TextView timeView; 57 | } 58 | 59 | @Override 60 | public int getCount() { 61 | return mInfos.size(); 62 | } 63 | 64 | @Override 65 | public Object getItem(int arg0) { 66 | return mInfos.get(arg0); 67 | } 68 | 69 | @Override 70 | public long getItemId(int arg0) { 71 | return 0; 72 | } 73 | 74 | public void addItemLast(List datas) { 75 | mInfos.addAll(datas); 76 | } 77 | 78 | public void addItemTop(List datas) { 79 | for (DuitangInfo info : datas) { 80 | mInfos.addFirst(info); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/com/example/android/bitmapfun/util/DiskLruCache.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.bitmapfun.util; 18 | 19 | import android.content.Context; 20 | import android.graphics.Bitmap; 21 | import android.graphics.Bitmap.CompressFormat; 22 | import android.graphics.BitmapFactory; 23 | import android.os.Environment; 24 | import android.util.Log; 25 | 26 | import java.io.BufferedOutputStream; 27 | import java.io.File; 28 | import java.io.FileNotFoundException; 29 | import java.io.FileOutputStream; 30 | import java.io.FilenameFilter; 31 | import java.io.IOException; 32 | import java.io.OutputStream; 33 | import java.io.UnsupportedEncodingException; 34 | import java.net.URLEncoder; 35 | import java.util.Collections; 36 | import java.util.LinkedHashMap; 37 | import java.util.Map; 38 | import java.util.Map.Entry; 39 | 40 | import com.dodola.waterex.BuildConfig; 41 | 42 | /** 43 | * A simple disk LRU bitmap cache to illustrate how a disk cache would be used 44 | * for bitmap caching. A much more robust and efficient disk LRU cache solution 45 | * can be found in the ICS source code 46 | * (libcore/luni/src/main/java/libcore/io/DiskLruCache.java) and is preferable 47 | * to this simple implementation. 48 | */ 49 | public class DiskLruCache { 50 | private static final String TAG = "DiskLruCache"; 51 | private static final String CACHE_FILENAME_PREFIX = "cache_"; 52 | private static final int MAX_REMOVALS = 4; 53 | private static final int INITIAL_CAPACITY = 32; 54 | private static final float LOAD_FACTOR = 0.75f; 55 | 56 | private final File mCacheDir; 57 | private int cacheSize = 0; 58 | private int cacheByteSize = 0; 59 | private final int maxCacheItemSize = 64; // 64 item default 60 | private long maxCacheByteSize = 1024 * 1024 * 5; // 5MB default 61 | private CompressFormat mCompressFormat = CompressFormat.JPEG; 62 | private int mCompressQuality = 70; 63 | 64 | private final Map mLinkedHashMap = Collections.synchronizedMap(new LinkedHashMap(INITIAL_CAPACITY, 65 | LOAD_FACTOR, true)); 66 | 67 | /** 68 | * A filename filter to use to identify the cache filenames which have 69 | * CACHE_FILENAME_PREFIX prepended. 70 | */ 71 | private static final FilenameFilter cacheFileFilter = new FilenameFilter() { 72 | @Override 73 | public boolean accept(File dir, String filename) { 74 | return filename.startsWith(CACHE_FILENAME_PREFIX); 75 | } 76 | }; 77 | 78 | /** 79 | * Used to fetch an instance of DiskLruCache. 80 | * 81 | * @param context 82 | * @param cacheDir 83 | * @param maxByteSize 84 | * @return 85 | */ 86 | public static DiskLruCache openCache(Context context, File cacheDir, long maxByteSize) { 87 | if (!cacheDir.exists()) { 88 | cacheDir.mkdir(); 89 | } 90 | 91 | if (cacheDir.isDirectory() && cacheDir.canWrite() && Utils.getUsableSpace(cacheDir) > maxByteSize) { 92 | return new DiskLruCache(cacheDir, maxByteSize); 93 | } 94 | 95 | return null; 96 | } 97 | 98 | /** 99 | * Constructor that should not be called directly, instead use 100 | * {@link DiskLruCache#openCache(Context, File, long)} which runs some extra 101 | * checks before creating a DiskLruCache instance. 102 | * 103 | * @param cacheDir 104 | * @param maxByteSize 105 | */ 106 | private DiskLruCache(File cacheDir, long maxByteSize) { 107 | mCacheDir = cacheDir; 108 | maxCacheByteSize = maxByteSize; 109 | } 110 | 111 | /** 112 | * Add a bitmap to the disk cache. 113 | * 114 | * @param key 115 | * A unique identifier for the bitmap. 116 | * @param data 117 | * The bitmap to store. 118 | */ 119 | public void put(String key, Bitmap data) { 120 | synchronized (mLinkedHashMap) { 121 | if (mLinkedHashMap.get(key) == null) { 122 | try { 123 | final String file = createFilePath(mCacheDir, key); 124 | if (writeBitmapToFile(data, file)) { 125 | put(key, file); 126 | flushCache(); 127 | } 128 | } catch (final FileNotFoundException e) { 129 | Log.e(TAG, "Error in put: " + e.getMessage()); 130 | } catch (final IOException e) { 131 | Log.e(TAG, "Error in put: " + e.getMessage()); 132 | } 133 | } 134 | } 135 | } 136 | 137 | private void put(String key, String file) { 138 | mLinkedHashMap.put(key, file); 139 | cacheSize = mLinkedHashMap.size(); 140 | cacheByteSize += new File(file).length(); 141 | } 142 | 143 | /** 144 | * Flush the cache, removing oldest entries if the total size is over the 145 | * specified cache size. Note that this isn't keeping track of stale files 146 | * in the cache directory that aren't in the HashMap. If the images and keys 147 | * in the disk cache change often then they probably won't ever be removed. 148 | */ 149 | private void flushCache() { 150 | Entry eldestEntry; 151 | File eldestFile; 152 | long eldestFileSize; 153 | int count = 0; 154 | 155 | while (count < MAX_REMOVALS && (cacheSize > maxCacheItemSize || cacheByteSize > maxCacheByteSize)) { 156 | eldestEntry = mLinkedHashMap.entrySet().iterator().next(); 157 | eldestFile = new File(eldestEntry.getValue()); 158 | eldestFileSize = eldestFile.length(); 159 | mLinkedHashMap.remove(eldestEntry.getKey()); 160 | eldestFile.delete(); 161 | cacheSize = mLinkedHashMap.size(); 162 | cacheByteSize -= eldestFileSize; 163 | count++; 164 | if (BuildConfig.DEBUG) { 165 | Log.d(TAG, "flushCache - Removed cache file, " + eldestFile + ", " + eldestFileSize); 166 | } 167 | } 168 | } 169 | 170 | /** 171 | * Get an image from the disk cache. 172 | * 173 | * @param key 174 | * The unique identifier for the bitmap 175 | * @return The bitmap or null if not found 176 | */ 177 | public Bitmap get(String key) { 178 | synchronized (mLinkedHashMap) { 179 | final String file = mLinkedHashMap.get(key); 180 | if (file != null) { 181 | if (BuildConfig.DEBUG) { 182 | Log.d(TAG, "Disk cache hit"); 183 | } 184 | return BitmapFactory.decodeFile(file); 185 | } else { 186 | final String existingFile = createFilePath(mCacheDir, key); 187 | if (new File(existingFile).exists()) { 188 | put(key, existingFile); 189 | if (BuildConfig.DEBUG) { 190 | Log.d(TAG, "Disk cache hit (existing file)"); 191 | } 192 | return BitmapFactory.decodeFile(existingFile); 193 | } 194 | } 195 | return null; 196 | } 197 | } 198 | 199 | /** 200 | * Checks if a specific key exist in the cache. 201 | * 202 | * @param key 203 | * The unique identifier for the bitmap 204 | * @return true if found, false otherwise 205 | */ 206 | public boolean containsKey(String key) { 207 | // See if the key is in our HashMap 208 | if (mLinkedHashMap.containsKey(key)) { 209 | return true; 210 | } 211 | 212 | // Now check if there's an actual file that exists based on the key 213 | final String existingFile = createFilePath(mCacheDir, key); 214 | if (new File(existingFile).exists()) { 215 | // File found, add it to the HashMap for future use 216 | put(key, existingFile); 217 | return true; 218 | } 219 | return false; 220 | } 221 | 222 | /** 223 | * Removes all disk cache entries from this instance cache dir 224 | */ 225 | public void clearCache() { 226 | DiskLruCache.clearCache(mCacheDir); 227 | } 228 | 229 | /** 230 | * Removes all disk cache entries from the application cache directory in 231 | * the uniqueName sub-directory. 232 | * 233 | * @param context 234 | * The context to use 235 | * @param uniqueName 236 | * A unique cache directory name to append to the app cache 237 | * directory 238 | */ 239 | public static void clearCache(Context context, String uniqueName) { 240 | File cacheDir = getDiskCacheDir(context, uniqueName); 241 | clearCache(cacheDir); 242 | } 243 | 244 | /** 245 | * Removes all disk cache entries from the given directory. This should not 246 | * be called directly, call {@link DiskLruCache#clearCache(Context, String)} 247 | * or {@link DiskLruCache#clearCache()} instead. 248 | * 249 | * @param cacheDir 250 | * The directory to remove the cache files from 251 | */ 252 | private static void clearCache(File cacheDir) { 253 | final File[] files = cacheDir.listFiles(cacheFileFilter); 254 | for (int i = 0; i < files.length; i++) { 255 | files[i].delete(); 256 | } 257 | } 258 | 259 | /** 260 | * Get a usable cache directory (external if available, internal otherwise). 261 | * 262 | * @param context 263 | * The context to use 264 | * @param uniqueName 265 | * A unique directory name to append to the cache dir 266 | * @return The cache dir 267 | */ 268 | public static File getDiskCacheDir(Context context, String uniqueName) { 269 | 270 | // Check if media is mounted or storage is built-in, if so, try and use 271 | // external cache dir 272 | // otherwise use internal cache dir 273 | final String cachePath = Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED || !Utils.isExternalStorageRemovable() ? Utils 274 | .getExternalCacheDir(context).getPath() : context.getCacheDir().getPath(); 275 | 276 | return new File(cachePath + File.separator + uniqueName); 277 | } 278 | 279 | /** 280 | * Creates a constant cache file path given a target cache directory and an 281 | * image key. 282 | * 283 | * @param cacheDir 284 | * @param key 285 | * @return 286 | */ 287 | public static String createFilePath(File cacheDir, String key) { 288 | try { 289 | // Use URLEncoder to ensure we have a valid filename, a tad hacky 290 | // but it will do for 291 | // this example 292 | return cacheDir.getAbsolutePath() + File.separator + CACHE_FILENAME_PREFIX + URLEncoder.encode(key.replace("*", ""), "UTF-8"); 293 | } catch (final UnsupportedEncodingException e) { 294 | Log.e(TAG, "createFilePath - " + e); 295 | } 296 | 297 | return null; 298 | } 299 | 300 | /** 301 | * Create a constant cache file path using the current cache directory and 302 | * an image key. 303 | * 304 | * @param key 305 | * @return 306 | */ 307 | public String createFilePath(String key) { 308 | return createFilePath(mCacheDir, key); 309 | } 310 | 311 | /** 312 | * Sets the target compression format and quality for images written to the 313 | * disk cache. 314 | * 315 | * @param compressFormat 316 | * @param quality 317 | */ 318 | public void setCompressParams(CompressFormat compressFormat, int quality) { 319 | mCompressFormat = compressFormat; 320 | mCompressQuality = quality; 321 | } 322 | 323 | /** 324 | * Writes a bitmap to a file. Call 325 | * {@link DiskLruCache#setCompressParams(CompressFormat, int)} first to set 326 | * the target bitmap compression and format. 327 | * 328 | * @param bitmap 329 | * @param file 330 | * @return 331 | */ 332 | private boolean writeBitmapToFile(Bitmap bitmap, String file) throws IOException, FileNotFoundException { 333 | 334 | OutputStream out = null; 335 | try { 336 | out = new BufferedOutputStream(new FileOutputStream(file), Utils.IO_BUFFER_SIZE); 337 | return bitmap.compress(mCompressFormat, mCompressQuality, out); 338 | } finally { 339 | if (out != null) { 340 | out.close(); 341 | } 342 | } 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /src/com/example/android/bitmapfun/util/Helper.java: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodola/WaterFallExt/f24dcf8a55867497e94f71bcb572b35d28169dd9/src/com/example/android/bitmapfun/util/Helper.java -------------------------------------------------------------------------------- /src/com/example/android/bitmapfun/util/ImageCache.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.bitmapfun.util; 18 | 19 | import android.content.Context; 20 | import android.graphics.Bitmap; 21 | import android.graphics.Bitmap.CompressFormat; 22 | import android.support.v4.app.FragmentActivity; 23 | import android.support.v4.util.LruCache; 24 | import android.util.Log; 25 | 26 | 27 | import java.io.File; 28 | 29 | import com.dodola.waterex.BuildConfig; 30 | 31 | 32 | /** 33 | * This class holds our bitmap caches (memory and disk). 34 | */ 35 | public class ImageCache { 36 | private static final String TAG = "ImageCache"; 37 | 38 | // Default memory cache size 39 | private static final int DEFAULT_MEM_CACHE_SIZE = 1024 * 1024 * 5; // 5MB 40 | 41 | // Default disk cache size 42 | private static final int DEFAULT_DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB 43 | 44 | // Compression settings when writing images to disk cache 45 | private static final CompressFormat DEFAULT_COMPRESS_FORMAT = CompressFormat.JPEG; 46 | private static final int DEFAULT_COMPRESS_QUALITY = 70; 47 | 48 | // Constants to easily toggle various caches 49 | private static final boolean DEFAULT_MEM_CACHE_ENABLED = true; 50 | private static final boolean DEFAULT_DISK_CACHE_ENABLED = true; 51 | private static final boolean DEFAULT_CLEAR_DISK_CACHE_ON_START = false; 52 | 53 | private DiskLruCache mDiskCache; 54 | private LruCache mMemoryCache; 55 | 56 | /** 57 | * Creating a new ImageCache object using the specified parameters. 58 | * 59 | * @param context The context to use 60 | * @param cacheParams The cache parameters to use to initialize the cache 61 | */ 62 | public ImageCache(Context context, ImageCacheParams cacheParams) { 63 | init(context, cacheParams); 64 | } 65 | 66 | /** 67 | * Creating a new ImageCache object using the default parameters. 68 | * 69 | * @param context The context to use 70 | * @param uniqueName A unique name that will be appended to the cache directory 71 | */ 72 | public ImageCache(Context context, String uniqueName) { 73 | init(context, new ImageCacheParams(uniqueName)); 74 | } 75 | 76 | /** 77 | * Find and return an existing ImageCache stored in a {@link RetainFragment}, if not found a new 78 | * one is created with defaults and saved to a {@link RetainFragment}. 79 | * 80 | * @param activity The calling {@link FragmentActivity} 81 | * @param uniqueName A unique name to append to the cache directory 82 | * @return An existing retained ImageCache object or a new one if one did not exist. 83 | */ 84 | public static ImageCache findOrCreateCache( 85 | final FragmentActivity activity, final String uniqueName) { 86 | return findOrCreateCache(activity, new ImageCacheParams(uniqueName)); 87 | } 88 | 89 | /** 90 | * Find and return an existing ImageCache stored in a {@link RetainFragment}, if not found a new 91 | * one is created using the supplied params and saved to a {@link RetainFragment}. 92 | * 93 | * @param activity The calling {@link FragmentActivity} 94 | * @param cacheParams The cache parameters to use if creating the ImageCache 95 | * @return An existing retained ImageCache object or a new one if one did not exist 96 | */ 97 | public static ImageCache findOrCreateCache( 98 | final FragmentActivity activity, ImageCacheParams cacheParams) { 99 | 100 | // Search for, or create an instance of the non-UI RetainFragment 101 | final RetainFragment mRetainFragment = RetainFragment.findOrCreateRetainFragment( 102 | activity.getSupportFragmentManager()); 103 | 104 | // See if we already have an ImageCache stored in RetainFragment 105 | ImageCache imageCache = (ImageCache) mRetainFragment.getObject(); 106 | 107 | // No existing ImageCache, create one and store it in RetainFragment 108 | if (imageCache == null) { 109 | imageCache = new ImageCache(activity, cacheParams); 110 | mRetainFragment.setObject(imageCache); 111 | } 112 | 113 | return imageCache; 114 | } 115 | 116 | /** 117 | * Initialize the cache, providing all parameters. 118 | * 119 | * @param context The context to use 120 | * @param cacheParams The cache parameters to initialize the cache 121 | */ 122 | private void init(Context context, ImageCacheParams cacheParams) { 123 | final File diskCacheDir = DiskLruCache.getDiskCacheDir(context, cacheParams.uniqueName); 124 | 125 | // Set up disk cache 126 | if (cacheParams.diskCacheEnabled) { 127 | mDiskCache = DiskLruCache.openCache(context, diskCacheDir, cacheParams.diskCacheSize); 128 | mDiskCache.setCompressParams(cacheParams.compressFormat, cacheParams.compressQuality); 129 | if (cacheParams.clearDiskCacheOnStart) { 130 | mDiskCache.clearCache(); 131 | } 132 | } 133 | 134 | // Set up memory cache 135 | if (cacheParams.memoryCacheEnabled) { 136 | mMemoryCache = new LruCache(cacheParams.memCacheSize) { 137 | /** 138 | * Measure item size in bytes rather than units which is more practical for a bitmap 139 | * cache 140 | */ 141 | @Override 142 | protected int sizeOf(String key, Bitmap bitmap) { 143 | return Utils.getBitmapSize(bitmap); 144 | } 145 | }; 146 | } 147 | } 148 | 149 | public void addBitmapToCache(String data, Bitmap bitmap) { 150 | if (data == null || bitmap == null) { 151 | return; 152 | } 153 | 154 | // Add to memory cache 155 | if (mMemoryCache != null && mMemoryCache.get(data) == null) { 156 | mMemoryCache.put(data, bitmap); 157 | } 158 | 159 | // Add to disk cache 160 | if (mDiskCache != null && !mDiskCache.containsKey(data)) { 161 | mDiskCache.put(data, bitmap); 162 | } 163 | } 164 | 165 | /** 166 | * Get from memory cache. 167 | * 168 | * @param data Unique identifier for which item to get 169 | * @return The bitmap if found in cache, null otherwise 170 | */ 171 | public Bitmap getBitmapFromMemCache(String data) { 172 | if (mMemoryCache != null) { 173 | final Bitmap memBitmap = mMemoryCache.get(data); 174 | if (memBitmap != null) { 175 | if (BuildConfig.DEBUG) { 176 | Log.d(TAG, "Memory cache hit"); 177 | } 178 | return memBitmap; 179 | } 180 | } 181 | return null; 182 | } 183 | 184 | /** 185 | * Get from disk cache. 186 | * 187 | * @param data Unique identifier for which item to get 188 | * @return The bitmap if found in cache, null otherwise 189 | */ 190 | public Bitmap getBitmapFromDiskCache(String data) { 191 | if (mDiskCache != null) { 192 | return mDiskCache.get(data); 193 | } 194 | return null; 195 | } 196 | 197 | public void clearCaches() { 198 | mDiskCache.clearCache(); 199 | mMemoryCache.evictAll(); 200 | } 201 | 202 | /** 203 | * A holder class that contains cache parameters. 204 | */ 205 | public static class ImageCacheParams { 206 | public String uniqueName; 207 | public int memCacheSize = DEFAULT_MEM_CACHE_SIZE; 208 | public int diskCacheSize = DEFAULT_DISK_CACHE_SIZE; 209 | public CompressFormat compressFormat = DEFAULT_COMPRESS_FORMAT; 210 | public int compressQuality = DEFAULT_COMPRESS_QUALITY; 211 | public boolean memoryCacheEnabled = DEFAULT_MEM_CACHE_ENABLED; 212 | public boolean diskCacheEnabled = DEFAULT_DISK_CACHE_ENABLED; 213 | public boolean clearDiskCacheOnStart = DEFAULT_CLEAR_DISK_CACHE_ON_START; 214 | 215 | public ImageCacheParams(String uniqueName) { 216 | this.uniqueName = uniqueName; 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/com/example/android/bitmapfun/util/ImageFetcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.bitmapfun.util; 18 | 19 | import android.content.Context; 20 | import android.graphics.Bitmap; 21 | import android.net.ConnectivityManager; 22 | import android.net.NetworkInfo; 23 | import android.util.Log; 24 | import android.widget.Toast; 25 | 26 | 27 | import java.io.BufferedInputStream; 28 | import java.io.BufferedOutputStream; 29 | import java.io.File; 30 | import java.io.FileOutputStream; 31 | import java.io.IOException; 32 | import java.io.InputStream; 33 | import java.net.HttpURLConnection; 34 | import java.net.URL; 35 | 36 | import com.dodola.waterex.BuildConfig; 37 | 38 | 39 | /** 40 | * A simple subclass of {@link ImageResizer} that fetches and resizes images fetched from a URL. 41 | */ 42 | public class ImageFetcher extends ImageResizer { 43 | private static final String TAG = "ImageFetcher"; 44 | private static final int HTTP_CACHE_SIZE = 10 * 1024 * 1024; // 10MB 45 | public static final String HTTP_CACHE_DIR = "http"; 46 | 47 | /** 48 | * Initialize providing a target image width and height for the processing images. 49 | * 50 | * @param context 51 | * @param imageWidth 52 | * @param imageHeight 53 | */ 54 | public ImageFetcher(Context context, int imageWidth, int imageHeight) { 55 | super(context); 56 | init(context); 57 | } 58 | 59 | /** 60 | * Initialize providing a single target image size (used for both width and height); 61 | * 62 | * @param context 63 | * @param imageSize 64 | */ 65 | public ImageFetcher(Context context, int imageSize) { 66 | super(context); 67 | init(context); 68 | } 69 | 70 | private void init(Context context) { 71 | checkConnection(context); 72 | } 73 | 74 | /** 75 | * Simple network connection check. 76 | * 77 | * @param context 78 | */ 79 | private void checkConnection(Context context) { 80 | final ConnectivityManager cm = 81 | (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 82 | final NetworkInfo networkInfo = cm.getActiveNetworkInfo(); 83 | if (networkInfo == null || !networkInfo.isConnectedOrConnecting()) { 84 | Toast.makeText(context, "No network connection found.", Toast.LENGTH_LONG).show(); 85 | Log.e(TAG, "checkConnection - no connection found"); 86 | } 87 | } 88 | 89 | /** 90 | * The main process method, which will be called by the ImageWorker in the AsyncTask background 91 | * thread. 92 | * 93 | * @param data The data to load the bitmap, in this case, a regular http URL 94 | * @return The downloaded and resized bitmap 95 | */ 96 | private Bitmap processBitmap(String data) { 97 | if (BuildConfig.DEBUG) { 98 | // Log.d(TAG, "processBitmap - " + data); 99 | } 100 | 101 | // Download a bitmap, write it to a file 102 | final File f = downloadBitmap(mContext, data); 103 | 104 | if (f != null) { 105 | // Return a sampled down version 106 | return decodeSampledBitmapFromFile(f.toString()); 107 | } 108 | 109 | return null; 110 | } 111 | 112 | @Override 113 | protected Bitmap processBitmap(Object data) { 114 | return processBitmap(String.valueOf(data)); 115 | } 116 | 117 | /** 118 | * Download a bitmap from a URL, write it to a disk and return the File pointer. This 119 | * implementation uses a simple disk cache. 120 | * 121 | * @param context The context to use 122 | * @param urlString The URL to fetch 123 | * @return A File pointing to the fetched bitmap 124 | */ 125 | public static File downloadBitmap(Context context, String urlString) { 126 | final File cacheDir = DiskLruCache.getDiskCacheDir(context, HTTP_CACHE_DIR); 127 | 128 | final DiskLruCache cache = 129 | DiskLruCache.openCache(context, cacheDir, HTTP_CACHE_SIZE); 130 | 131 | final File cacheFile = new File(cache.createFilePath(urlString)); 132 | 133 | if (cache.containsKey(urlString)) { 134 | if (BuildConfig.DEBUG) { 135 | // Log.d(TAG, "downloadBitmap - found in http cache - " + urlString); 136 | } 137 | return cacheFile; 138 | } 139 | 140 | if (BuildConfig.DEBUG) { 141 | // Log.d(TAG, "downloadBitmap - downloading - " + urlString); 142 | } 143 | 144 | Utils.disableConnectionReuseIfNecessary(); 145 | HttpURLConnection urlConnection = null; 146 | BufferedOutputStream out = null; 147 | 148 | try { 149 | final URL url = new URL(urlString); 150 | urlConnection = (HttpURLConnection) url.openConnection(); 151 | final InputStream in = 152 | new BufferedInputStream(urlConnection.getInputStream(), Utils.IO_BUFFER_SIZE); 153 | out = new BufferedOutputStream(new FileOutputStream(cacheFile), Utils.IO_BUFFER_SIZE); 154 | 155 | int b; 156 | while ((b = in.read()) != -1) { 157 | out.write(b); 158 | } 159 | 160 | return cacheFile; 161 | 162 | } catch (final IOException e) { 163 | Log.e(TAG, "Error in downloadBitmap - " + e); 164 | } finally { 165 | if (urlConnection != null) { 166 | urlConnection.disconnect(); 167 | } 168 | if (out != null) { 169 | try { 170 | out.close(); 171 | } catch (final IOException e) { 172 | Log.e(TAG, "Error in downloadBitmap - " + e); 173 | } 174 | } 175 | } 176 | 177 | return null; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/com/example/android/bitmapfun/util/ImageResizer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.bitmapfun.util; 18 | 19 | import com.dodola.waterex.BuildConfig; 20 | 21 | import android.content.Context; 22 | import android.content.res.Resources; 23 | import android.graphics.Bitmap; 24 | import android.graphics.BitmapFactory; 25 | import android.util.Log; 26 | 27 | /** 28 | * A simple subclass of {@link ImageWorker} that resizes images from resources 29 | * given a target width and height. Useful for when the input images might be 30 | * too large to simply load directly into memory. 31 | */ 32 | public class ImageResizer extends ImageWorker { 33 | private static final String TAG = "ImageWorker"; 34 | 35 | /** 36 | * Initialize providing a single target image size (used for both width and 37 | * height); 38 | * 39 | * @param context 40 | * @param imageWidth 41 | * @param imageHeight 42 | */ 43 | public ImageResizer(Context context) { 44 | super(context); 45 | } 46 | 47 | /** 48 | * The main processing method. This happens in a background task. In this 49 | * case we are just sampling down the bitmap and returning it from a 50 | * resource. 51 | * 52 | * @param resId 53 | * @return 54 | */ 55 | private Bitmap processBitmap(int resId) { 56 | if (BuildConfig.DEBUG) { 57 | Log.d(TAG, "processBitmap - " + resId); 58 | } 59 | return decodeSampledBitmapFromResource(mContext.getResources(), resId); 60 | } 61 | 62 | @Override 63 | protected Bitmap processBitmap(Object data) { 64 | return processBitmap(Integer.parseInt(String.valueOf(data))); 65 | } 66 | 67 | /** 68 | * Decode and sample down a bitmap from resources to the requested width and 69 | * height. 70 | * 71 | * @param res 72 | * The resources object containing the image data 73 | * @param resId 74 | * The resource id of the image data 75 | * @param reqWidth 76 | * The requested width of the resulting bitmap 77 | * @param reqHeight 78 | * The requested height of the resulting bitmap 79 | * @return A bitmap sampled down from the original with the same aspect 80 | * ratio and dimensions that are equal to or greater than the 81 | * requested width and height 82 | */ 83 | public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId) { 84 | 85 | // First decode with inJustDecodeBounds=true to check dimensions 86 | final BitmapFactory.Options options = new BitmapFactory.Options(); 87 | options.inJustDecodeBounds = true; 88 | BitmapFactory.decodeResource(res, resId, options); 89 | 90 | // Calculate inSampleSize 91 | options.inSampleSize = 1; 92 | 93 | // Decode bitmap with inSampleSize set 94 | options.inJustDecodeBounds = false; 95 | return BitmapFactory.decodeResource(res, resId, options); 96 | } 97 | 98 | /** 99 | * Decode and sample down a bitmap from a file to the requested width and 100 | * height. 101 | * 102 | * @param filename 103 | * The full path of the file to decode 104 | * @param reqWidth 105 | * The requested width of the resulting bitmap 106 | * @param reqHeight 107 | * The requested height of the resulting bitmap 108 | * @return A bitmap sampled down from the original with the same aspect 109 | * ratio and dimensions that are equal to or greater than the 110 | * requested width and height 111 | */ 112 | public static synchronized Bitmap decodeSampledBitmapFromFile(String filename) { 113 | 114 | // First decode with inJustDecodeBounds=true to check dimensions 115 | final BitmapFactory.Options options = new BitmapFactory.Options(); 116 | options.inJustDecodeBounds = true; 117 | BitmapFactory.decodeFile(filename, options); 118 | 119 | // Calculate inSampleSize 120 | options.inSampleSize =1; 121 | 122 | // Decode bitmap with inSampleSize set 123 | options.inJustDecodeBounds = false; 124 | return BitmapFactory.decodeFile(filename, options); 125 | } 126 | 127 | /** 128 | * Calculate an inSampleSize for use in a {@link BitmapFactory.Options} 129 | * object when decoding bitmaps using the decode* methods from 130 | * {@link BitmapFactory}. This implementation calculates the closest 131 | * inSampleSize that will result in the final decoded bitmap having a width 132 | * and height equal to or larger than the requested width and height. This 133 | * implementation does not ensure a power of 2 is returned for inSampleSize 134 | * which can be faster when decoding but results in a larger bitmap which 135 | * isn't as useful for caching purposes. 136 | * 137 | * @param options 138 | * An options object with out* params already populated (run 139 | * through a decode* method with inJustDecodeBounds==true 140 | * @param reqWidth 141 | * The requested width of the resulting bitmap 142 | * @param reqHeight 143 | * The requested height of the resulting bitmap 144 | * @return The value to be used for inSampleSize 145 | */ 146 | public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { 147 | // Raw height and width of image 148 | final int height = options.outHeight; 149 | final int width = options.outWidth; 150 | int inSampleSize = 1; 151 | 152 | if (height > reqHeight || width > reqWidth) { 153 | if (width > height) { 154 | inSampleSize = Math.round((float) height / (float) reqHeight); 155 | } else { 156 | inSampleSize = Math.round((float) width / (float) reqWidth); 157 | } 158 | 159 | // This offers some additional logic in case the image has a strange 160 | // aspect ratio. For example, a panorama may have a much larger 161 | // width than height. In these cases the total pixels might still 162 | // end up being too large to fit comfortably in memory, so we should 163 | // be more aggressive with sample down the image (=larger 164 | // inSampleSize). 165 | 166 | final float totalPixels = width * height; 167 | 168 | // Anything more than 2x the requested pixels we'll sample down 169 | // further. 170 | final float totalReqPixelsCap = reqWidth * reqHeight * 2; 171 | 172 | while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) { 173 | inSampleSize++; 174 | } 175 | } 176 | return inSampleSize; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/com/example/android/bitmapfun/util/ImageWorker.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.bitmapfun.util; 18 | 19 | import android.content.Context; 20 | import android.content.res.Resources; 21 | import android.graphics.Bitmap; 22 | import android.graphics.BitmapFactory; 23 | import android.graphics.drawable.BitmapDrawable; 24 | import android.graphics.drawable.ColorDrawable; 25 | import android.graphics.drawable.Drawable; 26 | import android.graphics.drawable.TransitionDrawable; 27 | import android.os.AsyncTask; 28 | import android.util.Log; 29 | import android.widget.ImageView; 30 | 31 | import java.lang.ref.WeakReference; 32 | 33 | import com.dodola.waterex.BuildConfig; 34 | 35 | /** 36 | * This class wraps up completing some arbitrary long running work when loading 37 | * a bitmap to an ImageView. It handles things like using a memory and disk 38 | * cache, running the work in a background thread and setting a placeholder 39 | * image. 40 | */ 41 | public abstract class ImageWorker { 42 | private static final String TAG = "ImageWorker"; 43 | private static final int FADE_IN_TIME = 200; 44 | 45 | private ImageCache mImageCache; 46 | private Bitmap mLoadingBitmap; 47 | private boolean mFadeInBitmap = true; 48 | private boolean mExitTasksEarly = false; 49 | 50 | protected Context mContext; 51 | protected ImageWorkerAdapter mImageWorkerAdapter; 52 | 53 | protected ImageWorker(Context context) { 54 | mContext = context; 55 | } 56 | 57 | /** 58 | * Load an image specified by the data parameter into an ImageView (override 59 | * {@link ImageWorker#processBitmap(Object)} to define the processing 60 | * logic). A memory and disk cache will be used if an {@link ImageCache} has 61 | * been set using {@link ImageWorker#setImageCache(ImageCache)}. If the 62 | * image is found in the memory cache, it is set immediately, otherwise an 63 | * {@link AsyncTask} will be created to asynchronously load the bitmap. 64 | * 65 | * @param data 66 | * The URL of the image to download. 67 | * @param imageView 68 | * The ImageView to bind the downloaded image to. 69 | */ 70 | public void loadImage(Object data, ImageView imageView) { 71 | Bitmap bitmap = null; 72 | 73 | if (mImageCache != null) { 74 | bitmap = mImageCache.getBitmapFromMemCache(String.valueOf(data)); 75 | } 76 | 77 | if (bitmap != null) { 78 | // Bitmap found in memory cache 79 | imageView.setImageBitmap(bitmap); 80 | } else if (cancelPotentialWork(data, imageView)) { 81 | final BitmapWorkerTask task = new BitmapWorkerTask(imageView); 82 | final AsyncDrawable asyncDrawable = new AsyncDrawable(mContext.getResources(), mLoadingBitmap, task); 83 | imageView.setImageDrawable(asyncDrawable); 84 | task.execute(data); 85 | } 86 | } 87 | 88 | /** 89 | * Load an image specified from a set adapter into an ImageView (override 90 | * {@link ImageWorker#processBitmap(Object)} to define the processing 91 | * logic). A memory and disk cache will be used if an {@link ImageCache} has 92 | * been set using {@link ImageWorker#setImageCache(ImageCache)}. If the 93 | * image is found in the memory cache, it is set immediately, otherwise an 94 | * {@link AsyncTask} will be created to asynchronously load the bitmap. 95 | * {@link ImageWorker#setAdapter(ImageWorkerAdapter)} must be called before 96 | * using this method. 97 | * 98 | * @param data 99 | * The URL of the image to download. 100 | * @param imageView 101 | * The ImageView to bind the downloaded image to. 102 | */ 103 | public void loadImage(int num, ImageView imageView) { 104 | if (mImageWorkerAdapter != null) { 105 | loadImage(mImageWorkerAdapter.getItem(num), imageView); 106 | } else { 107 | throw new NullPointerException("Data not set, must call setAdapter() first."); 108 | } 109 | } 110 | 111 | /** 112 | * Set placeholder bitmap that shows when the the background thread is 113 | * running. 114 | * 115 | * @param bitmap 116 | */ 117 | public void setLoadingImage(Bitmap bitmap) { 118 | mLoadingBitmap = bitmap; 119 | } 120 | 121 | /** 122 | * Set placeholder bitmap that shows when the the background thread is 123 | * running. 124 | * 125 | * @param resId 126 | */ 127 | public void setLoadingImage(int resId) { 128 | mLoadingBitmap = BitmapFactory.decodeResource(mContext.getResources(), resId); 129 | } 130 | 131 | /** 132 | * Set the {@link ImageCache} object to use with this ImageWorker. 133 | * 134 | * @param cacheCallback 135 | */ 136 | public void setImageCache(ImageCache cacheCallback) { 137 | mImageCache = cacheCallback; 138 | } 139 | 140 | public ImageCache getImageCache() { 141 | return mImageCache; 142 | } 143 | 144 | /** 145 | * If set to true, the image will fade-in once it has been loaded by the 146 | * background thread. 147 | * 148 | * @param fadeIn 149 | */ 150 | public void setImageFadeIn(boolean fadeIn) { 151 | mFadeInBitmap = fadeIn; 152 | } 153 | 154 | public void setExitTasksEarly(boolean exitTasksEarly) { 155 | mExitTasksEarly = exitTasksEarly; 156 | } 157 | 158 | /** 159 | * Subclasses should override this to define any processing or work that 160 | * must happen to produce the final bitmap. This will be executed in a 161 | * background thread and be long running. For example, you could resize a 162 | * large bitmap here, or pull down an image from the network. 163 | * 164 | * @param data 165 | * The data to identify which image to process, as provided by 166 | * {@link ImageWorker#loadImage(Object, ImageView)} 167 | * @return The processed bitmap 168 | */ 169 | protected abstract Bitmap processBitmap(Object data); 170 | 171 | public static void cancelWork(ImageView imageView) { 172 | final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); 173 | if (bitmapWorkerTask != null) { 174 | bitmapWorkerTask.cancel(true); 175 | if (BuildConfig.DEBUG) { 176 | final Object bitmapData = bitmapWorkerTask.data; 177 | Log.d(TAG, "cancelWork - cancelled work for " + bitmapData); 178 | } 179 | } 180 | } 181 | 182 | /** 183 | * Returns true if the current work has been canceled or if there was no 184 | * work in progress on this image view. Returns false if the work in 185 | * progress deals with the same data. The work is not stopped in that case. 186 | */ 187 | public static boolean cancelPotentialWork(Object data, ImageView imageView) { 188 | final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); 189 | 190 | if (bitmapWorkerTask != null) { 191 | final Object bitmapData = bitmapWorkerTask.data; 192 | if (bitmapData == null || !bitmapData.equals(data)) { 193 | bitmapWorkerTask.cancel(true); 194 | if (BuildConfig.DEBUG) { 195 | Log.d(TAG, "cancelPotentialWork - cancelled work for " + data); 196 | } 197 | } else { 198 | // The same work is already in progress. 199 | return false; 200 | } 201 | } 202 | return true; 203 | } 204 | 205 | /** 206 | * @param imageView 207 | * Any imageView 208 | * @return Retrieve the currently active work task (if any) associated with 209 | * this imageView. null if there is no such task. 210 | */ 211 | private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { 212 | if (imageView != null) { 213 | final Drawable drawable = imageView.getDrawable(); 214 | if (drawable instanceof AsyncDrawable) { 215 | final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; 216 | return asyncDrawable.getBitmapWorkerTask(); 217 | } 218 | } 219 | return null; 220 | } 221 | 222 | /** 223 | * The actual AsyncTask that will asynchronously process the image. 224 | */ 225 | private class BitmapWorkerTask extends AsyncTask { 226 | private Object data; 227 | private final WeakReference imageViewReference; 228 | 229 | public BitmapWorkerTask(ImageView imageView) { 230 | imageViewReference = new WeakReference(imageView); 231 | } 232 | 233 | /** 234 | * Background processing. 235 | */ 236 | @Override 237 | protected Bitmap doInBackground(Object... params) { 238 | data = params[0]; 239 | final String dataString = String.valueOf(data); 240 | Bitmap bitmap = null; 241 | 242 | // If the image cache is available and this task has not been 243 | // cancelled by another 244 | // thread and the ImageView that was originally bound to this task 245 | // is still bound back 246 | // to this task and our "exit early" flag is not set then try and 247 | // fetch the bitmap from 248 | // the cache 249 | if (mImageCache != null && !isCancelled() && getAttachedImageView() != null && !mExitTasksEarly) { 250 | bitmap = mImageCache.getBitmapFromDiskCache(dataString); 251 | } 252 | 253 | // If the bitmap was not found in the cache and this task has not 254 | // been cancelled by 255 | // another thread and the ImageView that was originally bound to 256 | // this task is still 257 | // bound back to this task and our "exit early" flag is not set, 258 | // then call the main 259 | // process method (as implemented by a subclass) 260 | if (bitmap == null && !isCancelled() && getAttachedImageView() != null && !mExitTasksEarly) { 261 | bitmap = processBitmap(params[0]); 262 | } 263 | 264 | // If the bitmap was processed and the image cache is available, 265 | // then add the processed 266 | // bitmap to the cache for future use. Note we don't check if the 267 | // task was cancelled 268 | // here, if it was, and the thread is still running, we may as well 269 | // add the processed 270 | // bitmap to our cache as it might be used again in the future 271 | if (bitmap != null && mImageCache != null) { 272 | mImageCache.addBitmapToCache(dataString, bitmap); 273 | } 274 | 275 | return bitmap; 276 | } 277 | 278 | /** 279 | * Once the image is processed, associates it to the imageView 280 | */ 281 | @Override 282 | protected void onPostExecute(Bitmap bitmap) { 283 | // if cancel was called on this task or the "exit early" flag is set 284 | // then we're done 285 | if (isCancelled() || mExitTasksEarly) { 286 | bitmap = null; 287 | } 288 | 289 | final ImageView imageView = getAttachedImageView(); 290 | if (bitmap != null && imageView != null) { 291 | setImageBitmap(imageView, bitmap); 292 | } 293 | } 294 | 295 | /** 296 | * Returns the ImageView associated with this task as long as the 297 | * ImageView's task still points to this task as well. Returns null 298 | * otherwise. 299 | */ 300 | private ImageView getAttachedImageView() { 301 | final ImageView imageView = imageViewReference.get(); 302 | final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); 303 | 304 | if (this == bitmapWorkerTask) { 305 | return imageView; 306 | } 307 | 308 | return null; 309 | } 310 | } 311 | 312 | /** 313 | * A custom Drawable that will be attached to the imageView while the work 314 | * is in progress. Contains a reference to the actual worker task, so that 315 | * it can be stopped if a new binding is required, and makes sure that only 316 | * the last started worker process can bind its result, independently of the 317 | * finish order. 318 | */ 319 | private static class AsyncDrawable extends BitmapDrawable { 320 | private final WeakReference bitmapWorkerTaskReference; 321 | 322 | public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { 323 | super(res, bitmap); 324 | 325 | bitmapWorkerTaskReference = new WeakReference(bitmapWorkerTask); 326 | } 327 | 328 | public BitmapWorkerTask getBitmapWorkerTask() { 329 | return bitmapWorkerTaskReference.get(); 330 | } 331 | } 332 | 333 | /** 334 | * Called when the processing is complete and the final bitmap should be set 335 | * on the ImageView. 336 | * 337 | * @param imageView 338 | * @param bitmap 339 | */ 340 | private void setImageBitmap(ImageView imageView, Bitmap bitmap) { 341 | if (mFadeInBitmap) { 342 | // Transition drawable with a transparent drwabale and the final 343 | // bitmap 344 | final TransitionDrawable td = new TransitionDrawable(new Drawable[] { new ColorDrawable(android.R.color.transparent), 345 | new BitmapDrawable(mContext.getResources(), bitmap) }); 346 | // Set background to loading bitmap 347 | imageView.setBackgroundDrawable(new BitmapDrawable(mContext.getResources(), mLoadingBitmap)); 348 | 349 | imageView.setImageDrawable(td); 350 | td.startTransition(FADE_IN_TIME); 351 | } else { 352 | imageView.setImageBitmap(bitmap); 353 | } 354 | } 355 | 356 | /** 357 | * Set the simple adapter which holds the backing data. 358 | * 359 | * @param adapter 360 | */ 361 | public void setAdapter(ImageWorkerAdapter adapter) { 362 | mImageWorkerAdapter = adapter; 363 | } 364 | 365 | /** 366 | * Get the current adapter. 367 | * 368 | * @return 369 | */ 370 | public ImageWorkerAdapter getAdapter() { 371 | return mImageWorkerAdapter; 372 | } 373 | 374 | /** 375 | * A very simple adapter for use with ImageWorker class and subclasses. 376 | */ 377 | public static abstract class ImageWorkerAdapter { 378 | public abstract Object getItem(int num); 379 | 380 | public abstract int getSize(); 381 | } 382 | } 383 | -------------------------------------------------------------------------------- /src/com/example/android/bitmapfun/util/RetainFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.bitmapfun.util; 18 | 19 | import android.os.Bundle; 20 | import android.support.v4.app.Fragment; 21 | import android.support.v4.app.FragmentManager; 22 | 23 | /** 24 | * A simple non-UI Fragment that stores a single Object and is retained over configuration changes. 25 | * In this sample it will be used to retain the ImageCache object. 26 | */ 27 | public class RetainFragment extends Fragment { 28 | private static final String TAG = "RetainFragment"; 29 | private Object mObject; 30 | 31 | /** 32 | * Empty constructor as per the Fragment documentation 33 | */ 34 | public RetainFragment() {} 35 | 36 | /** 37 | * Locate an existing instance of this Fragment or if not found, create and 38 | * add it using FragmentManager. 39 | * 40 | * @param fm The FragmentManager manager to use. 41 | * @return The existing instance of the Fragment or the new instance if just 42 | * created. 43 | */ 44 | public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) { 45 | // Check to see if we have retained the worker fragment. 46 | RetainFragment mRetainFragment = (RetainFragment) fm.findFragmentByTag(TAG); 47 | 48 | // If not retained (or first time running), we need to create and add it. 49 | if (mRetainFragment == null) { 50 | mRetainFragment = new RetainFragment(); 51 | fm.beginTransaction().add(mRetainFragment, TAG).commit(); 52 | } 53 | 54 | return mRetainFragment; 55 | } 56 | 57 | @Override 58 | public void onCreate(Bundle savedInstanceState) { 59 | super.onCreate(savedInstanceState); 60 | 61 | // Make sure this Fragment is retained over a configuration change 62 | setRetainInstance(true); 63 | } 64 | 65 | /** 66 | * Store a single object in this Fragment. 67 | * 68 | * @param object The object to store 69 | */ 70 | public void setObject(Object object) { 71 | mObject = object; 72 | } 73 | 74 | /** 75 | * Get the stored object. 76 | * 77 | * @return The stored object 78 | */ 79 | public Object getObject() { 80 | return mObject; 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/com/example/android/bitmapfun/util/Utils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.bitmapfun.util; 18 | 19 | import android.annotation.SuppressLint; 20 | import android.app.ActivityManager; 21 | import android.content.Context; 22 | import android.graphics.Bitmap; 23 | import android.os.Build; 24 | import android.os.Environment; 25 | import android.os.StatFs; 26 | 27 | import java.io.File; 28 | 29 | /** 30 | * Class containing some static utility methods. 31 | */ 32 | public class Utils { 33 | public static final int IO_BUFFER_SIZE = 8 * 1024; 34 | 35 | private Utils() {}; 36 | 37 | /** 38 | * Workaround for bug pre-Froyo, see here for more info: 39 | * http://android-developers.blogspot.com/2011/09/androids-http-clients.html 40 | */ 41 | public static void disableConnectionReuseIfNecessary() { 42 | // HTTP connection reuse which was buggy pre-froyo 43 | if (hasHttpConnectionBug()) { 44 | System.setProperty("http.keepAlive", "false"); 45 | } 46 | } 47 | 48 | /** 49 | * Get the size in bytes of a bitmap. 50 | * @param bitmap 51 | * @return size in bytes 52 | */ 53 | @SuppressLint("NewApi") 54 | public static int getBitmapSize(Bitmap bitmap) { 55 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) { 56 | return bitmap.getByteCount(); 57 | } 58 | // Pre HC-MR1 59 | return bitmap.getRowBytes() * bitmap.getHeight(); 60 | } 61 | 62 | /** 63 | * Check if external storage is built-in or removable. 64 | * 65 | * @return True if external storage is removable (like an SD card), false 66 | * otherwise. 67 | */ 68 | @SuppressLint("NewApi") 69 | public static boolean isExternalStorageRemovable() { 70 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { 71 | return Environment.isExternalStorageRemovable(); 72 | } 73 | return true; 74 | } 75 | 76 | /** 77 | * Get the external app cache directory. 78 | * 79 | * @param context The context to use 80 | * @return The external cache dir 81 | */ 82 | @SuppressLint("NewApi") 83 | public static File getExternalCacheDir(Context context) { 84 | if (hasExternalCacheDir()) { 85 | return context.getExternalCacheDir(); 86 | } 87 | 88 | // Before Froyo we need to construct the external cache dir ourselves 89 | final String cacheDir = "/Android/data/" + context.getPackageName() + "/cache/"; 90 | return new File(Environment.getExternalStorageDirectory().getPath() + cacheDir); 91 | } 92 | 93 | /** 94 | * Check how much usable space is available at a given path. 95 | * 96 | * @param path The path to check 97 | * @return The space available in bytes 98 | */ 99 | @SuppressLint("NewApi") 100 | public static long getUsableSpace(File path) { 101 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { 102 | return path.getUsableSpace(); 103 | } 104 | final StatFs stats = new StatFs(path.getPath()); 105 | return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks(); 106 | } 107 | 108 | /** 109 | * Get the memory class of this device (approx. per-app memory limit) 110 | * 111 | * @param context 112 | * @return 113 | */ 114 | public static int getMemoryClass(Context context) { 115 | return ((ActivityManager) context.getSystemService( 116 | Context.ACTIVITY_SERVICE)).getMemoryClass(); 117 | } 118 | 119 | /** 120 | * Check if OS version has a http URLConnection bug. See here for more information: 121 | * http://android-developers.blogspot.com/2011/09/androids-http-clients.html 122 | * 123 | * @return 124 | */ 125 | public static boolean hasHttpConnectionBug() { 126 | return Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO; 127 | } 128 | 129 | /** 130 | * Check if OS version has built-in external cache dir method. 131 | * 132 | * @return 133 | */ 134 | public static boolean hasExternalCacheDir() { 135 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO; 136 | } 137 | 138 | /** 139 | * Check if ActionBar is available. 140 | * 141 | * @return 142 | */ 143 | public static boolean hasActionBar() { 144 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/com/origamilabs/library/views/FastScroller.java: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodola/WaterFallExt/f24dcf8a55867497e94f71bcb572b35d28169dd9/src/com/origamilabs/library/views/FastScroller.java -------------------------------------------------------------------------------- /src/com/origamilabs/library/views/ScrollerCompat.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.origamilabs.library.views; 18 | 19 | import android.content.Context; 20 | import android.widget.Scroller; 21 | 22 | /** 23 | * Provides access to new {@link android.widget.Scroller Scroller} APIs when available. 24 | * 25 | *

This class provides a platform version-independent mechanism for obeying the 26 | * current device's preferred scroll physics and fling behavior. It offers a subset of 27 | * the APIs from Scroller or OverScroller.

28 | */ 29 | class ScrollerCompat { 30 | Scroller mScroller; 31 | 32 | static class ScrollerCompatImplIcs extends ScrollerCompat { 33 | public ScrollerCompatImplIcs(Context context) { 34 | super(context); 35 | } 36 | 37 | @Override 38 | public float getCurrVelocity() { 39 | return ScrollerCompatIcs.getCurrVelocity(mScroller); 40 | } 41 | } 42 | 43 | public static ScrollerCompat from(Context context) { 44 | if (android.os.Build.VERSION.SDK_INT >= 14) { 45 | return new ScrollerCompatImplIcs(context); 46 | } 47 | return new ScrollerCompat(context); 48 | } 49 | 50 | ScrollerCompat(Context context) { 51 | mScroller = new Scroller(context); 52 | } 53 | 54 | /** 55 | * Returns whether the scroller has finished scrolling. 56 | * 57 | * @return True if the scroller has finished scrolling, false otherwise. 58 | */ 59 | public boolean isFinished() { 60 | return mScroller.isFinished(); 61 | } 62 | 63 | /** 64 | * Returns how long the scroll event will take, in milliseconds. 65 | * 66 | * @return The duration of the scroll in milliseconds. 67 | */ 68 | public int getDuration() { 69 | return mScroller.getDuration(); 70 | } 71 | 72 | /** 73 | * Returns the current X offset in the scroll. 74 | * 75 | * @return The new X offset as an absolute distance from the origin. 76 | */ 77 | public int getCurrX() { 78 | return mScroller.getCurrX(); 79 | } 80 | 81 | /** 82 | * Returns the current Y offset in the scroll. 83 | * 84 | * @return The new Y offset as an absolute distance from the origin. 85 | */ 86 | public int getCurrY() { 87 | return mScroller.getCurrY(); 88 | } 89 | 90 | /** 91 | * Returns the current velocity. 92 | * 93 | * TODO: Approximate a sane result for older platform versions. Right now 94 | * this will return 0 for platforms earlier than ICS. This is acceptable 95 | * at the moment only since it is only used for EdgeEffect, which is also only 96 | * present in ICS+, and ScrollerCompat is not public. 97 | * 98 | * @return The original velocity less the deceleration. Result may be 99 | * negative. 100 | */ 101 | public float getCurrVelocity() { 102 | return 0; 103 | } 104 | 105 | /** 106 | * Call this when you want to know the new location. If it returns true, 107 | * the animation is not yet finished. loc will be altered to provide the 108 | * new location. 109 | */ 110 | public boolean computeScrollOffset() { 111 | return mScroller.computeScrollOffset(); 112 | } 113 | 114 | /** 115 | * Start scrolling by providing a starting point and the distance to travel. 116 | * The scroll will use the default value of 250 milliseconds for the 117 | * duration. 118 | * 119 | * @param startX Starting horizontal scroll offset in pixels. Positive 120 | * numbers will scroll the content to the left. 121 | * @param startY Starting vertical scroll offset in pixels. Positive numbers 122 | * will scroll the content up. 123 | * @param dx Horizontal distance to travel. Positive numbers will scroll the 124 | * content to the left. 125 | * @param dy Vertical distance to travel. Positive numbers will scroll the 126 | * content up. 127 | */ 128 | public void startScroll(int startX, int startY, int dx, int dy) { 129 | mScroller.startScroll(startX, startY, dx, dy); 130 | } 131 | 132 | /** 133 | * Start scrolling by providing a starting point and the distance to travel. 134 | * 135 | * @param startX Starting horizontal scroll offset in pixels. Positive 136 | * numbers will scroll the content to the left. 137 | * @param startY Starting vertical scroll offset in pixels. Positive numbers 138 | * will scroll the content up. 139 | * @param dx Horizontal distance to travel. Positive numbers will scroll the 140 | * content to the left. 141 | * @param dy Vertical distance to travel. Positive numbers will scroll the 142 | * content up. 143 | * @param duration Duration of the scroll in milliseconds. 144 | */ 145 | public void startScroll(int startX, int startY, int dx, int dy, int duration) { 146 | mScroller.startScroll(startX, startY, dx, dy, duration); 147 | } 148 | 149 | /** 150 | * Start scrolling based on a fling gesture. The distance travelled will 151 | * depend on the initial velocity of the fling. 152 | * 153 | * @param startX Starting point of the scroll (X) 154 | * @param startY Starting point of the scroll (Y) 155 | * @param velocityX Initial velocity of the fling (X) measured in pixels per 156 | * second. 157 | * @param velocityY Initial velocity of the fling (Y) measured in pixels per 158 | * second 159 | * @param minX Minimum X value. The scroller will not scroll past this 160 | * point. 161 | * @param maxX Maximum X value. The scroller will not scroll past this 162 | * point. 163 | * @param minY Minimum Y value. The scroller will not scroll past this 164 | * point. 165 | * @param maxY Maximum Y value. The scroller will not scroll past this 166 | * point. 167 | */ 168 | public void fling(int startX, int startY, int velocityX, int velocityY, 169 | int minX, int maxX, int minY, int maxY) { 170 | mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY); 171 | } 172 | 173 | /** 174 | * Stops the animation. Contrary to {@link #forceFinished(boolean)}, 175 | * aborting the animating cause the scroller to move to the final x and y 176 | * position 177 | */ 178 | public void abortAnimation() { 179 | mScroller.abortAnimation(); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/com/origamilabs/library/views/ScrollerCompatIcs.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.origamilabs.library.views; 18 | 19 | import android.annotation.TargetApi; 20 | import android.os.Build; 21 | import android.widget.Scroller; 22 | 23 | /** 24 | * ICS API access for Scroller 25 | */ 26 | class ScrollerCompatIcs { 27 | @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) 28 | public static float getCurrVelocity(Scroller scroller) { 29 | return scroller.getCurrVelocity(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/com/origamilabs/library/views/StaggeredGridView.java: -------------------------------------------------------------------------------- 1 | package com.origamilabs.library.views; 2 | 3 | /* 4 | * Copyright (C) 2012 The Android Open Source Project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * modified by Maurycy Wojtowicz 19 | * 20 | */ 21 | 22 | import java.util.ArrayList; 23 | import java.util.Arrays; 24 | 25 | import com.dodola.waterex.R; 26 | 27 | import android.annotation.TargetApi; 28 | import android.content.Context; 29 | import android.content.res.TypedArray; 30 | import android.database.DataSetObserver; 31 | import android.graphics.Canvas; 32 | import android.graphics.Rect; 33 | import android.graphics.drawable.Drawable; 34 | import android.graphics.drawable.TransitionDrawable; 35 | import android.os.Build; 36 | import android.os.Handler; 37 | import android.os.Parcel; 38 | import android.os.Parcelable; 39 | import android.support.v4.util.SparseArrayCompat; 40 | import android.support.v4.view.MotionEventCompat; 41 | import android.support.v4.view.VelocityTrackerCompat; 42 | import android.support.v4.view.ViewCompat; 43 | import android.support.v4.widget.EdgeEffectCompat; 44 | import android.util.AttributeSet; 45 | import android.util.Log; 46 | import android.util.SparseArray; 47 | import android.view.ContextMenu; 48 | import android.view.ContextMenu.ContextMenuInfo; 49 | import android.view.HapticFeedbackConstants; 50 | import android.view.MotionEvent; 51 | import android.view.SoundEffectConstants; 52 | import android.view.VelocityTracker; 53 | import android.view.View; 54 | import android.view.ViewConfiguration; 55 | import android.view.ViewGroup; 56 | import android.view.accessibility.AccessibilityEvent; 57 | import android.widget.ListAdapter; 58 | 59 | /** ListView and GridView just not complex enough? Try StaggeredGridView! 60 | * 61 | *

62 | * StaggeredGridView presents a multi-column grid with consistent column sizes 63 | * but varying row sizes between the columns. Each successive item from a 64 | * {@link android.widget.ListAdapter ListAdapter} will be arranged from top to 65 | * bottom, left to right. The largest vertical gap is always filled first. 66 | *

67 | * 68 | *

69 | * Item views may span multiple columns as specified by their 70 | * {@link LayoutParams}. The attribute android:layout_span may be 71 | * used when inflating item views from xml. 72 | *

*/ 73 | public class StaggeredGridView extends ViewGroup { 74 | private static final String TAG = "StaggeredGridView"; 75 | 76 | /* 77 | * There are a few things you should know if you're going to make 78 | * modifications to StaggeredGridView. 79 | * 80 | * Like ListView, SGV populates from an adapter and recycles views that fall 81 | * out of the visible boundaries of the grid. A few invariants always hold: 82 | * 83 | * - mFirstPosition is the adapter position of the View returned by 84 | * getChildAt(0). - Any child index can be translated to an adapter position 85 | * by adding mFirstPosition. - Any adapter position can be translated to a 86 | * child index by subtracting mFirstPosition. - Views for items in the range 87 | * [mFirstPosition, mFirstPosition + getChildCount()) are currently attached 88 | * to the grid as children. All other adapter positions do not have active 89 | * views. 90 | * 91 | * This means a few things thanks to the staggered grid's nature. Some views 92 | * may stay attached long after they have scrolled offscreen if removing and 93 | * recycling them would result in breaking one of the invariants above. 94 | * 95 | * LayoutRecords are used to track data about a particular item's layout 96 | * after the associated view has been removed. These let positioning and the 97 | * choice of column for an item remain consistent even though the rules for 98 | * filling content up vs. filling down vary. 99 | * 100 | * Whenever layout parameters for a known LayoutRecord change, other 101 | * LayoutRecords before or after it may need to be invalidated. e.g. if the 102 | * item's height or the number of columns it spans changes, all bets for 103 | * other items in the same direction are off since the cached information no 104 | * longer applies. 105 | */ 106 | 107 | private ListAdapter mAdapter; 108 | 109 | private static final int TOUCH_MODE_IDLE = 0; 110 | private static final int TOUCH_MODE_DRAGGING = 1; 111 | private static final int TOUCH_MODE_FLINGING = 2; 112 | private static final int TOUCH_MODE_DOWN = 3; 113 | private static final int TOUCH_MODE_TAP = 4; 114 | private static final int TOUCH_MODE_DONE_WAITING = 5; 115 | private static final int TOUCH_MODE_REST = 6; 116 | private static final int INVALID_POSITION = -1; 117 | public static final int COLUMN_COUNT_AUTO = -1; 118 | 119 | private int mColCountSetting = 2; 120 | private int mColCount = 2; 121 | private int mMinColWidth = 0; 122 | // private int mItemMargin; 123 | 124 | private int[] mItemTops; 125 | private int[] mItemBottoms; 126 | 127 | private boolean mFastChildLayout; 128 | private boolean mPopulating; 129 | private boolean mInLayout; 130 | private int[] mRestoreOffsets; 131 | 132 | private final RecycleBin mRecycler = new RecycleBin(); 133 | 134 | private final AdapterDataSetObserver mObserver = new AdapterDataSetObserver(); 135 | 136 | private boolean mDataChanged; 137 | private int mItemCount; 138 | private boolean mHasStableIds; 139 | 140 | private int mFirstPosition; 141 | 142 | private int mTouchSlop; 143 | private int mMaximumVelocity; 144 | private int mFlingVelocity; 145 | private float mLastTouchY; 146 | private float mLastTouchX; 147 | private float mTouchRemainderY; 148 | private int mActivePointerId; 149 | private int mMotionPosition; 150 | private int mColWidth; 151 | private long mFirstAdapterId; 152 | private boolean mBeginClick; 153 | private boolean mSmoothScrollbarEnabled = true; 154 | 155 | private FastScroller mFastScroller; 156 | private boolean mFastScrollEnabled = true; 157 | 158 | private int mTouchMode; 159 | private final VelocityTracker mVelocityTracker = VelocityTracker.obtain(); 160 | private final ScrollerCompat mScroller; 161 | 162 | private final EdgeEffectCompat mTopEdge; 163 | private final EdgeEffectCompat mBottomEdge; 164 | 165 | private ArrayList> mColMappings = new ArrayList>(); 166 | 167 | private Runnable mPendingCheckForTap; 168 | 169 | private ContextMenuInfo mContextMenuInfo = null; 170 | private OnScrollListener mOnScrollListener; 171 | 172 | private boolean mAreAllItemsSelectable = true; 173 | int mNextSelectedPosition = INVALID_POSITION; 174 | public static final long INVALID_ROW_ID = Long.MIN_VALUE; 175 | /** The item id of the item to select during the next layout. */ 176 | long mNextSelectedRowId = INVALID_ROW_ID; 177 | 178 | /** The position within the adapter's data set of the currently selected 179 | * item. */ 180 | int mSelectedPosition = INVALID_POSITION; 181 | 182 | /** The item id of the currently selected item. */ 183 | long mSelectedRowId = INVALID_ROW_ID; 184 | // /** Ԫ��֮����ϱ߾� */ 185 | // private int mItemTopMargin; 186 | // /** Ԫ��֮����±߾� */ 187 | // private int mItemBottomMargin; 188 | // /** Ԫ��֮�����߾� */ 189 | // private int mItemLeftMargin; 190 | // /** Ԫ��֮����ұ߾� */ 191 | // private int mItemRightMargin; 192 | 193 | private int mLastScrollState = OnScrollListener.SCROLL_STATE_IDLE; 194 | /** The drawable used to draw the selector */ 195 | Drawable mSelector; 196 | 197 | boolean mDrawSelectorOnTop = false; 198 | 199 | /** Delayed action for touch mode. */ 200 | private Runnable mTouchModeReset; 201 | 202 | /** The selection's left padding */ 203 | int mSelectionLeftPadding = 0; 204 | 205 | /** The selection's top padding */ 206 | int mSelectionTopPadding = 0; 207 | 208 | /** The selection's right padding */ 209 | int mSelectionRightPadding = 0; 210 | 211 | /** The selection's bottom padding */ 212 | int mSelectionBottomPadding = 0; 213 | 214 | /** The select child's view (from the adapter's getView) is enabled. */ 215 | private boolean mIsChildViewEnabled; 216 | 217 | /** Defines the selector's location and dimension at drawing time */ 218 | Rect mSelectorRect = new Rect(); 219 | 220 | /** The current position of the selector in the list. */ 221 | int mSelectorPosition = INVALID_POSITION; 222 | 223 | /** The listener that receives notifications when an item is clicked. */ 224 | OnItemClickListener mOnItemClickListener; 225 | 226 | /** The listener that receives notifications when an item is long clicked. */ 227 | OnItemLongClickListener mOnItemLongClickListener; 228 | 229 | /** The last CheckForLongPress runnable we posted, if any */ 230 | private CheckForLongPress mPendingCheckForLongPress; 231 | 232 | /** Acts upon click */ 233 | private PerformClick mPerformClick; 234 | 235 | /** Rectangle used for hit testing children */ 236 | private Rect mTouchFrame; 237 | /** Regular layout - usually an unsolicited layout from the view system */ 238 | static final int LAYOUT_NORMAL = 0; 239 | 240 | /** Show the first item */ 241 | static final int LAYOUT_FORCE_TOP = 1; 242 | 243 | /** Force the selected item to be on somewhere on the screen */ 244 | static final int LAYOUT_SET_SELECTION = 2; 245 | 246 | /** Show the last item */ 247 | static final int LAYOUT_FORCE_BOTTOM = 3; 248 | 249 | /** Make a mSelectedItem appear in a specific location and build the rest of 250 | * the views from there. The top is specified by mSpecificTop. */ 251 | static final int LAYOUT_SPECIFIC = 4; 252 | 253 | /** Layout to sync as a result of a data change. Restore mSyncPosition to 254 | * have its top at mSpecificTop */ 255 | static final int LAYOUT_SYNC = 5; 256 | 257 | /** Layout as a result of using the navigation keys */ 258 | static final int LAYOUT_MOVE_SELECTION = 6; 259 | 260 | /** Controls how the next layout will happen */ 261 | int mLayoutMode = LAYOUT_NORMAL; 262 | 263 | private static final class LayoutRecord { 264 | public int column; 265 | public long id = -1; 266 | public int height; 267 | public int span; 268 | private int[] mMargins; 269 | 270 | private final void ensureMargins() { 271 | if (mMargins == null) { 272 | // Don't need to confirm length; 273 | // all layoutrecords are purged when column count changes. 274 | mMargins = new int[span * 2]; 275 | } 276 | } 277 | 278 | public final int getMarginAbove(int col) { 279 | if (mMargins == null) { 280 | return 0; 281 | } 282 | return mMargins[col * 2]; 283 | } 284 | 285 | public final int getMarginBelow(int col) { 286 | if (mMargins == null) { 287 | return 0; 288 | } 289 | return mMargins[col * 2 + 1]; 290 | } 291 | 292 | public final void setMarginAbove(int col, int margin) { 293 | if (mMargins == null && margin == 0) { 294 | return; 295 | } 296 | ensureMargins(); 297 | mMargins[col * 2] = margin; 298 | } 299 | 300 | public final void setMarginBelow(int col, int margin) { 301 | if (mMargins == null && margin == 0) { 302 | return; 303 | } 304 | ensureMargins(); 305 | mMargins[col * 2 + 1] = margin; 306 | } 307 | 308 | @Override 309 | public String toString() { 310 | String result = "LayoutRecord{c=" + column + ", id=" + id + " h=" 311 | + height + " s=" + span; 312 | if (mMargins != null) { 313 | result += " margins[above, below]("; 314 | for (int i = 0; i < mMargins.length; i += 2) { 315 | result += "[" + mMargins[i] + ", " + mMargins[i + 1] + "]"; 316 | } 317 | result += ")"; 318 | } 319 | return result + "}"; 320 | } 321 | } 322 | 323 | public interface OnScrollListener { 324 | 325 | /** The view is not scrolling. Note navigating the list using the 326 | * trackball counts as being in the idle state since these transitions 327 | * are not animated. */ 328 | public static int SCROLL_STATE_IDLE = 0; 329 | 330 | /** The user is scrolling using touch, and their finger is still on the 331 | * screen */ 332 | public static int SCROLL_STATE_TOUCH_SCROLL = 1; 333 | 334 | /** The user had previously been scrolling using touch and had performed 335 | * a fling. The animation is now coasting to a stop */ 336 | public static int SCROLL_STATE_FLING = 2; 337 | 338 | /** Callback method to be invoked while the list view or grid view is 339 | * being scrolled. If the view is being scrolled, this method will be 340 | * called before the next frame of the scroll is rendered. In 341 | * particular, it will be called before any calls to 342 | * {@link Adapter#getView(int, View, ViewGroup)}. 343 | * 344 | * @param view 345 | * The view whose scroll state is being reported 346 | * 347 | * @param scrollState 348 | * The current scroll state. One of 349 | * {@link #SCROLL_STATE_IDLE}, 350 | * {@link #SCROLL_STATE_TOUCH_SCROLL} or 351 | * {@link #SCROLL_STATE_IDLE}. */ 352 | public void onScrollStateChanged(StaggeredGridView view, int scrollState); 353 | 354 | /** Callback method to be invoked when the list or grid has been 355 | * scrolled. This will be called after the scroll has completed 356 | * 357 | * @param view 358 | * The view whose scroll state is being reported 359 | * @param firstVisibleItem 360 | * the index of the first visible cell (ignore if 361 | * visibleItemCount == 0) 362 | * @param visibleItemCount 363 | * the number of visible cells 364 | * @param totalItemCount 365 | * the number of items in the list adaptor */ 366 | public void onScroll(StaggeredGridView view, int firstVisibleItem, 367 | int visibleItemCount, int totalItemCount); 368 | } 369 | 370 | private final SparseArrayCompat mLayoutRecords = new SparseArrayCompat(); 371 | 372 | public StaggeredGridView(Context context) { 373 | this(context, null); 374 | } 375 | 376 | public StaggeredGridView(Context context, AttributeSet attrs) { 377 | this(context, attrs, 0); 378 | } 379 | 380 | public StaggeredGridView(Context context, AttributeSet attrs, int defStyle) { 381 | super(context, attrs, defStyle); 382 | 383 | if (attrs != null) { 384 | TypedArray a = getContext().obtainStyledAttributes(attrs, 385 | R.styleable.StaggeredGridView); 386 | mColCount = a.getInteger(R.styleable.StaggeredGridView_numColumns, 387 | 2); 388 | mDrawSelectorOnTop = a.getBoolean( 389 | R.styleable.StaggeredGridView_drawSelectorOnTop, false); 390 | a.recycle(); 391 | } else { 392 | mColCount = 2; 393 | mDrawSelectorOnTop = false; 394 | } 395 | 396 | final ViewConfiguration vc = ViewConfiguration.get(context); 397 | mTouchSlop = vc.getScaledTouchSlop(); 398 | mMaximumVelocity = vc.getScaledMaximumFlingVelocity(); 399 | mFlingVelocity = vc.getScaledMinimumFlingVelocity(); 400 | mScroller = ScrollerCompat.from(context); 401 | 402 | mTopEdge = new EdgeEffectCompat(context); 403 | mBottomEdge = new EdgeEffectCompat(context); 404 | setWillNotDraw(false); 405 | setClipToPadding(false); 406 | this.setFocusableInTouchMode(false); 407 | 408 | if (mSelector == null) { 409 | useDefaultSelector(); 410 | } 411 | } 412 | 413 | public void setFastScrollEnabled(boolean enabled) { 414 | mFastScrollEnabled = enabled; 415 | if (enabled) { 416 | if (mFastScroller == null) { 417 | mFastScroller = new FastScroller(getContext(), this); 418 | } 419 | } else { 420 | if (mFastScroller != null) { 421 | mFastScroller.stop(); 422 | mFastScroller = null; 423 | } 424 | } 425 | } 426 | 427 | @Override 428 | protected int computeVerticalScrollExtent() { 429 | final int count = getChildCount(); 430 | if (count > 0) { 431 | if (mSmoothScrollbarEnabled) { 432 | int extent = count * 100; 433 | 434 | View view = getChildAt(0); 435 | // final int top = view.getTop(); 436 | final int top = getFillChildTop(); 437 | 438 | int height = view.getHeight(); 439 | if (height > 0) { 440 | extent += (top * 100) / height; 441 | } 442 | 443 | view = getChildAt(count - 1); 444 | // final int bottom = view.getBottom(); 445 | final int bottom = getScrollChildBottom(); 446 | height = view.getHeight(); 447 | if (height > 0) { 448 | extent -= ((bottom - getHeight()) * 100) / height; 449 | } 450 | 451 | return extent; 452 | } else { 453 | return 1; 454 | } 455 | } 456 | return 0; 457 | } 458 | 459 | @Override 460 | protected int computeVerticalScrollOffset() { 461 | final int firstPosition = mFirstPosition; 462 | final int childCount = getChildCount(); 463 | if (firstPosition >= 0 && childCount > 0) { 464 | if (mSmoothScrollbarEnabled) { 465 | final View view = getChildAt(0); 466 | // final int top = view.getTop(); 467 | final int top = getFillChildTop(); 468 | int height = view.getHeight(); 469 | if (height > 0) { 470 | return Math.max(firstPosition 471 | * 100 472 | - (top * 100) 473 | / height 474 | + (int) ((float) getScrollY() / getHeight() 475 | * mItemCount * 100), 0); 476 | } 477 | } else { 478 | int index; 479 | final int count = mItemCount; 480 | if (firstPosition == 0) { 481 | index = 0; 482 | } else if (firstPosition + childCount == count) { 483 | index = count; 484 | } else { 485 | index = firstPosition + childCount / 2; 486 | } 487 | int vertOffset = (int) (firstPosition + childCount 488 | * (index / (float) count)); 489 | return vertOffset; 490 | } 491 | } 492 | 493 | return 0; 494 | } 495 | 496 | protected int getScrollChildBottom() { 497 | final int count = getChildCount(); 498 | if (count == 0) 499 | return 0; 500 | return getChildAt(count - 1).getBottom(); 501 | } 502 | 503 | protected int getFillChildTop() { 504 | final int count = getChildCount(); 505 | if (count == 0) 506 | return 0; 507 | return getChildAt(0).getTop(); 508 | } 509 | 510 | @Override 511 | protected int computeVerticalScrollRange() { 512 | int result; 513 | if (mSmoothScrollbarEnabled) { 514 | result = Math.max(mItemCount * 100, 0); 515 | } else { 516 | result = mItemCount; 517 | } 518 | // Log.d(TAG, "computeVerticalScrollRange " + result); 519 | return result; 520 | } 521 | 522 | /** Set a fixed number of columns for this grid. Space will be divided evenly 523 | * among all columns, respecting the item margin between columns. The 524 | * default is 2. (If it were 1, perhaps you should be using a 525 | * {@link android.widget.ListView ListView}.) 526 | * 527 | * @param colCount 528 | * Number of columns to display. 529 | * @see #setMinColumnWidth(int) */ 530 | public void setColumnCount(int colCount) { 531 | if (colCount < 1 && colCount != COLUMN_COUNT_AUTO) { 532 | throw new IllegalArgumentException( 533 | "Column count must be at least 1 - received " + colCount); 534 | } 535 | final boolean needsPopulate = colCount != mColCount; 536 | mColCount = mColCountSetting = colCount; 537 | if (needsPopulate) { 538 | populate(false); 539 | } 540 | } 541 | 542 | public int getColumnCount() { 543 | return mColCount; 544 | } 545 | 546 | /** Set a minimum column width for 547 | * 548 | * @param minColWidth */ 549 | public void setMinColumnWidth(int minColWidth) { 550 | mMinColWidth = minColWidth; 551 | setColumnCount(COLUMN_COUNT_AUTO); 552 | } 553 | 554 | // /** 555 | // * ����Ԫ��֮��ļ�� 556 | // * 557 | // * @param left 558 | // * ���� 559 | // * @param top 560 | // * �ϼ�� 561 | // * @param right 562 | // * �Ҽ�� 563 | // * @param bottom 564 | // * �¼�� 565 | // */ 566 | // public void setItemMargin(int left, int top, int right, int bottom) { 567 | // final boolean needsPopulate = (left != mItemLeftMargin) 568 | // || (top != mItemTopMargin) || (right != mItemRightMargin) 569 | // || (bottom != mItemBottomMargin); 570 | // mItemLeftMargin = left; 571 | // mItemRightMargin = right; 572 | // mItemTopMargin = top; 573 | // mItemBottomMargin = bottom; 574 | // if (needsPopulate) { 575 | // populate(false); 576 | // } 577 | // } 578 | // 579 | // /** 580 | // * ����Ԫ��֮��ļ�� 581 | // * 582 | // * @param margin 583 | // */ 584 | // public void setItemMargin(int margin) { 585 | // setItemMargin(margin, margin, margin, margin); 586 | // } 587 | 588 | /** Return the first adapter position with a view currently attached as a 589 | * child view of this grid. 590 | * 591 | * @return the adapter position represented by the view at getChildAt(0). */ 592 | public int getFirstPosition() { 593 | return mFirstPosition; 594 | } 595 | 596 | @Override 597 | public boolean onInterceptTouchEvent(MotionEvent ev) { 598 | 599 | if (mFastScroller != null) { 600 | boolean intercepted = mFastScroller.onInterceptTouchEvent(ev); 601 | if (intercepted) { 602 | return true; 603 | } 604 | } 605 | 606 | mVelocityTracker.addMovement(ev); 607 | final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; 608 | switch (action) { 609 | case MotionEvent.ACTION_DOWN: 610 | mVelocityTracker.clear(); 611 | mScroller.abortAnimation(); 612 | mLastTouchY = ev.getY(); 613 | mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 614 | mTouchRemainderY = 0; 615 | if (mTouchMode == TOUCH_MODE_FLINGING) { 616 | // Catch! 617 | mTouchMode = TOUCH_MODE_DRAGGING; 618 | return true; 619 | } 620 | break; 621 | 622 | case MotionEvent.ACTION_MOVE: { 623 | final int index = MotionEventCompat.findPointerIndex(ev, 624 | mActivePointerId); 625 | if (index < 0) { 626 | Log.e(TAG, 627 | "onInterceptTouchEvent could not find pointer with id " 628 | + mActivePointerId 629 | + " - did StaggeredGridView receive an inconsistent " 630 | + "event stream?"); 631 | return false; 632 | } 633 | final float y = MotionEventCompat.getY(ev, index); 634 | final float dy = y - mLastTouchY + mTouchRemainderY; 635 | final int deltaY = (int) dy; 636 | mTouchRemainderY = dy - deltaY; 637 | 638 | if (Math.abs(dy) > mTouchSlop) { 639 | mTouchMode = TOUCH_MODE_DRAGGING; 640 | return true; 641 | } 642 | } 643 | } 644 | 645 | return false; 646 | } 647 | 648 | void hideSelector() { 649 | if (this.mSelectorPosition != INVALID_POSITION) { 650 | // TODO: hide selector properly 651 | } 652 | } 653 | 654 | @Override 655 | public boolean onTouchEvent(MotionEvent ev) { 656 | if (mFastScroller != null) { 657 | boolean intercepted = mFastScroller.onTouchEvent(ev); 658 | if (intercepted) { 659 | return true; 660 | } 661 | } 662 | mVelocityTracker.addMovement(ev); 663 | final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; 664 | 665 | int motionPosition = pointToPosition((int) ev.getX(), (int) ev.getY()); 666 | 667 | switch (action) { 668 | case MotionEvent.ACTION_DOWN: 669 | 670 | mVelocityTracker.clear(); 671 | mScroller.abortAnimation(); 672 | mLastTouchY = ev.getY(); 673 | mLastTouchX = ev.getX(); 674 | motionPosition = pointToPosition((int) mLastTouchX, 675 | (int) mLastTouchY); 676 | mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 677 | mTouchRemainderY = 0; 678 | 679 | if (mTouchMode != TOUCH_MODE_FLINGING && !mDataChanged 680 | && motionPosition >= 0 681 | && getAdapter().isEnabled(motionPosition)) { 682 | mTouchMode = TOUCH_MODE_DOWN; 683 | 684 | mBeginClick = true; 685 | 686 | if (mPendingCheckForTap == null) { 687 | mPendingCheckForTap = new CheckForTap(); 688 | } 689 | 690 | postDelayed(mPendingCheckForTap, 691 | ViewConfiguration.getTapTimeout()); 692 | } 693 | 694 | mMotionPosition = motionPosition; 695 | invalidate(); 696 | break; 697 | 698 | case MotionEvent.ACTION_MOVE: { 699 | 700 | final int index = MotionEventCompat.findPointerIndex(ev, 701 | mActivePointerId); 702 | if (index < 0) { 703 | Log.e(TAG, 704 | "onInterceptTouchEvent could not find pointer with id " 705 | + mActivePointerId 706 | + " - did StaggeredGridView receive an inconsistent " 707 | + "event stream?"); 708 | return false; 709 | } 710 | final float y = MotionEventCompat.getY(ev, index); 711 | final float dy = y - mLastTouchY + mTouchRemainderY; 712 | final int deltaY = (int) dy; 713 | mTouchRemainderY = dy - deltaY; 714 | 715 | if (Math.abs(dy) > mTouchSlop) { 716 | mTouchMode = TOUCH_MODE_DRAGGING; 717 | } 718 | 719 | if (mTouchMode == TOUCH_MODE_DRAGGING) { 720 | mLastTouchY = y; 721 | 722 | if (!trackMotionScroll(deltaY, true)) { 723 | // Break fling velocity if we impacted an edge. 724 | mVelocityTracker.clear(); 725 | } 726 | } 727 | 728 | updateSelectorState(); 729 | } 730 | break; 731 | 732 | case MotionEvent.ACTION_CANCEL: 733 | mTouchMode = TOUCH_MODE_IDLE; 734 | updateSelectorState(); 735 | setPressed(false); 736 | View motionView = this.getChildAt(mMotionPosition - mFirstPosition); 737 | if (motionView != null) { 738 | motionView.setPressed(false); 739 | } 740 | final Handler handler = getHandler(); 741 | if (handler != null) { 742 | handler.removeCallbacks(mPendingCheckForLongPress); 743 | } 744 | 745 | if (mTopEdge != null) { 746 | mTopEdge.onRelease(); 747 | mBottomEdge.onRelease(); 748 | } 749 | 750 | mTouchMode = TOUCH_MODE_IDLE; 751 | break; 752 | 753 | case MotionEvent.ACTION_UP: { 754 | mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 755 | final float velocity = VelocityTrackerCompat.getYVelocity( 756 | mVelocityTracker, mActivePointerId); 757 | final int prevTouchMode = mTouchMode; 758 | 759 | if (Math.abs(velocity) > mFlingVelocity) { // TODO 760 | mTouchMode = TOUCH_MODE_FLINGING; 761 | mScroller.fling(0, 0, 0, (int) velocity, 0, 0, 762 | Integer.MIN_VALUE, Integer.MAX_VALUE); 763 | mLastTouchY = 0; 764 | invalidate(); 765 | } else { 766 | mTouchMode = TOUCH_MODE_IDLE; 767 | } 768 | 769 | if (!mDataChanged && mAdapter.isEnabled(motionPosition)) { 770 | // TODO : handle 771 | mTouchMode = TOUCH_MODE_TAP; 772 | } else { 773 | mTouchMode = TOUCH_MODE_REST; 774 | } 775 | 776 | switch (prevTouchMode) { 777 | case TOUCH_MODE_DOWN: 778 | case TOUCH_MODE_TAP: 779 | case TOUCH_MODE_DONE_WAITING: 780 | final View child = getChildAt(motionPosition - mFirstPosition); 781 | final float x = ev.getX(); 782 | final boolean inList = x > getPaddingLeft() 783 | && x < getWidth() - getPaddingRight(); 784 | if (child != null && !child.hasFocusable() && inList) { 785 | if (mTouchMode != TOUCH_MODE_DOWN) { 786 | child.setPressed(false); 787 | } 788 | 789 | if (mPerformClick == null) { 790 | invalidate(); 791 | mPerformClick = new PerformClick(); 792 | } 793 | 794 | final PerformClick performClick = mPerformClick; 795 | performClick.mClickMotionPosition = motionPosition; 796 | performClick.rememberWindowAttachCount(); 797 | 798 | if (mTouchMode == TOUCH_MODE_DOWN 799 | || mTouchMode == TOUCH_MODE_TAP) { 800 | final Handler handlerTouch = getHandler(); 801 | if (handlerTouch != null) { 802 | handlerTouch 803 | .removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ? mPendingCheckForTap 804 | : mPendingCheckForLongPress); 805 | } 806 | mLayoutMode = LAYOUT_NORMAL; 807 | 808 | if (!mDataChanged && mAdapter.isEnabled(motionPosition)) { 809 | mTouchMode = TOUCH_MODE_TAP; 810 | 811 | layoutChildren(mDataChanged); 812 | child.setPressed(true); 813 | positionSelector(mMotionPosition, child); 814 | setPressed(true); 815 | if (mSelector != null) { 816 | Drawable d = mSelector.getCurrent(); 817 | if (d != null 818 | && d instanceof TransitionDrawable) { 819 | ((TransitionDrawable) d).resetTransition(); 820 | } 821 | } 822 | if (mTouchModeReset != null) { 823 | removeCallbacks(mTouchModeReset); 824 | } 825 | mTouchModeReset = new Runnable() { 826 | @Override 827 | public void run() { 828 | mTouchMode = TOUCH_MODE_REST; 829 | child.setPressed(false); 830 | setPressed(false); 831 | if (!mDataChanged) { 832 | performClick.run(); 833 | } 834 | } 835 | }; 836 | postDelayed(mTouchModeReset, 837 | ViewConfiguration.getPressedStateDuration()); 838 | 839 | } else { 840 | mTouchMode = TOUCH_MODE_REST; 841 | } 842 | return true; 843 | } else if (!mDataChanged 844 | && mAdapter.isEnabled(motionPosition)) { 845 | performClick.run(); 846 | } 847 | } 848 | 849 | mTouchMode = TOUCH_MODE_REST; 850 | } 851 | 852 | mBeginClick = false; 853 | 854 | updateSelectorState(); 855 | } 856 | break; 857 | } 858 | return true; 859 | } 860 | 861 | public int getCount() { 862 | return mItemCount; 863 | } 864 | 865 | private int getTopSelectionPixel(int childrenTop, int fadingEdgeLength, 866 | int selectedPosition) { 867 | // first pixel we can draw the selection into 868 | int topSelectionPixel = childrenTop; 869 | if (selectedPosition > 0) { 870 | topSelectionPixel += fadingEdgeLength; 871 | } 872 | return topSelectionPixel; 873 | } 874 | 875 | private int getBottomSelectionPixel(int childrenBottom, 876 | int fadingEdgeLength, int selectedPosition) { 877 | int bottomSelectionPixel = childrenBottom; 878 | if (selectedPosition != mItemCount - 1) { 879 | bottomSelectionPixel -= fadingEdgeLength; 880 | } 881 | return bottomSelectionPixel; 882 | } 883 | 884 | int lookForSelectablePosition(int position, boolean lookDown) { 885 | final ListAdapter adapter = mAdapter; 886 | if (adapter == null || isInTouchMode()) { 887 | return INVALID_POSITION; 888 | } 889 | 890 | final int count = adapter.getCount(); 891 | if (!mAreAllItemsSelectable) { 892 | if (lookDown) { 893 | position = Math.max(0, position); 894 | while (position < count && !adapter.isEnabled(position)) { 895 | position++; 896 | } 897 | } else { 898 | position = Math.min(position, count - 1); 899 | while (position >= 0 && !adapter.isEnabled(position)) { 900 | position--; 901 | } 902 | } 903 | 904 | if (position < 0 || position >= count) { 905 | return INVALID_POSITION; 906 | } 907 | return position; 908 | } else { 909 | if (position < 0 || position >= count) { 910 | return INVALID_POSITION; 911 | } 912 | return position; 913 | } 914 | } 915 | 916 | void setNextSelectedPositionInt(int position) { 917 | mNextSelectedPosition = position; 918 | mNextSelectedRowId = getItemIdAtPosition(position); 919 | } 920 | 921 | public long getItemIdAtPosition(int position) { 922 | ListAdapter adapter = getAdapter(); 923 | return (adapter == null || position < 0) ? INVALID_ROW_ID : adapter 924 | .getItemId(position); 925 | } 926 | 927 | // TODO: 928 | public void setSelection(int position) { 929 | if (mAdapter == null) { 930 | return; 931 | } 932 | 933 | if (!isInTouchMode()) { 934 | position = lookForSelectablePosition(position, true); 935 | if (position >= 0) { 936 | setNextSelectedPositionInt(position); 937 | } 938 | } 939 | Log.d(TAG, "===========position:" + position); 940 | if (position >= 0) { 941 | mLayoutMode = LAYOUT_SPECIFIC; 942 | // mSpecificTop = mItemTops[position]; 943 | requestLayout(); 944 | } 945 | } 946 | 947 | void reportScrollStateChange(int newState) { 948 | if (newState != mLastScrollState) { 949 | if (mOnScrollListener != null) { 950 | mOnScrollListener.onScrollStateChanged(this, newState); 951 | mLastScrollState = newState; 952 | } 953 | } 954 | } 955 | 956 | /** @param deltaY 957 | * Pixels that content should move by 958 | * @return true if the movement completed, false if it was stopped 959 | * prematurely. */ 960 | private boolean trackMotionScroll(int deltaY, boolean allowOverScroll) { 961 | final boolean contentFits = contentFits(); 962 | final int allowOverhang = Math.abs(deltaY); 963 | 964 | final int overScrolledBy; 965 | final int movedBy; 966 | if (!contentFits) { 967 | final int overhang; 968 | final boolean up; 969 | mPopulating = true; 970 | if (deltaY > 0) { 971 | overhang = fillUp(mFirstPosition - 1, allowOverhang); 972 | up = true; 973 | } else { 974 | overhang = fillDown(mFirstPosition + getChildCount(), 975 | allowOverhang); 976 | up = false; 977 | } 978 | movedBy = Math.min(overhang, allowOverhang); 979 | offsetChildren(up ? movedBy : -movedBy); 980 | recycleOffscreenViews(); 981 | mPopulating = false; 982 | overScrolledBy = allowOverhang - overhang; 983 | } else { 984 | overScrolledBy = allowOverhang; 985 | movedBy = 0; 986 | } 987 | 988 | if (allowOverScroll) { 989 | final int overScrollMode = ViewCompat.getOverScrollMode(this); 990 | 991 | if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS 992 | || (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && !contentFits)) { 993 | if (overScrolledBy > 0) { 994 | EdgeEffectCompat edge = deltaY > 0 ? mTopEdge : mBottomEdge; 995 | edge.onPull((float) Math.abs(deltaY) / getHeight()); 996 | invalidate(); 997 | } 998 | } 999 | } 1000 | 1001 | if (mSelectorPosition != INVALID_POSITION) { 1002 | final int childIndex = mSelectorPosition - mFirstPosition; 1003 | if (childIndex >= 0 && childIndex < getChildCount()) { 1004 | positionSelector(INVALID_POSITION, getChildAt(childIndex)); 1005 | } 1006 | } else { 1007 | mSelectorRect.setEmpty(); 1008 | } 1009 | invokeOnItemScrollListener(); 1010 | // ��ʾscrollbar 1011 | if (!awakenScrollBars()) { 1012 | invalidate(); 1013 | } 1014 | return deltaY == 0 || movedBy != 0; 1015 | } 1016 | 1017 | @Override 1018 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 1019 | if (getChildCount() > 0) { 1020 | mDataChanged = true; 1021 | // rememberSyncState();//TODO: 1022 | } 1023 | 1024 | if (mFastScroller != null) { 1025 | mFastScroller.onSizeChanged(w, h, oldw, oldh); 1026 | } 1027 | } 1028 | 1029 | private final boolean contentFits() { 1030 | if (mFirstPosition != 0 || getChildCount() != mItemCount) { 1031 | return false; 1032 | } 1033 | 1034 | int topmost = Integer.MAX_VALUE; 1035 | int bottommost = Integer.MIN_VALUE; 1036 | for (int i = 0; i < mColCount; i++) { 1037 | if (mItemTops[i] < topmost) { 1038 | topmost = mItemTops[i]; 1039 | } 1040 | if (mItemBottoms[i] > bottommost) { 1041 | bottommost = mItemBottoms[i]; 1042 | } 1043 | } 1044 | 1045 | return topmost >= getPaddingTop() 1046 | && bottommost <= getHeight() - getPaddingBottom(); 1047 | } 1048 | 1049 | private void recycleAllViews() { 1050 | for (int i = 0; i < getChildCount(); i++) { 1051 | mRecycler.addScrap(getChildAt(i)); 1052 | } 1053 | 1054 | if (mInLayout) { 1055 | removeAllViewsInLayout(); 1056 | } else { 1057 | removeAllViews(); 1058 | } 1059 | } 1060 | 1061 | /** Important: this method will leave offscreen views attached if they are 1062 | * required to maintain the invariant that child view with index i is always 1063 | * the view corresponding to position mFirstPosition + i. */ 1064 | private void recycleOffscreenViews() { 1065 | final int height = getHeight(); 1066 | final int clearAbove = 0; 1067 | final int clearBelow = height; 1068 | for (int i = getChildCount() - 1; i >= 0; i--) { 1069 | final View child = getChildAt(i); 1070 | if (child.getTop() <= clearBelow) { 1071 | // There may be other offscreen views, but we need to maintain 1072 | // the invariant documented above. 1073 | break; 1074 | } 1075 | 1076 | if (mInLayout) { 1077 | removeViewsInLayout(i, 1); 1078 | } else { 1079 | removeViewAt(i); 1080 | } 1081 | 1082 | mRecycler.addScrap(child); 1083 | } 1084 | 1085 | while (getChildCount() > 0) { 1086 | final View child = getChildAt(0); 1087 | if (child.getBottom() >= clearAbove) { 1088 | // There may be other offscreen views, but we need to maintain 1089 | // the invariant documented above. 1090 | break; 1091 | } 1092 | 1093 | if (mInLayout) { 1094 | removeViewsInLayout(0, 1); 1095 | } else { 1096 | removeViewAt(0); 1097 | } 1098 | 1099 | mRecycler.addScrap(child); 1100 | mFirstPosition++; 1101 | } 1102 | 1103 | final int childCount = getChildCount(); 1104 | if (childCount > 0) { 1105 | // Repair the top and bottom column boundaries from the views we 1106 | // still have 1107 | Arrays.fill(mItemTops, Integer.MAX_VALUE); 1108 | Arrays.fill(mItemBottoms, Integer.MIN_VALUE); 1109 | 1110 | for (int i = 0; i < childCount; i++) { 1111 | final View child = getChildAt(i); 1112 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1113 | final int top = child.getTop();// - mItemTopMargin; 1114 | final int bottom = child.getBottom();// - mItemBottomMargin; 1115 | final LayoutRecord rec = mLayoutRecords.get(mFirstPosition + i); 1116 | 1117 | final int colEnd = lp.column + Math.min(mColCount, lp.span); 1118 | for (int col = lp.column; col < colEnd; col++) { 1119 | final int colTop = top 1120 | - rec.getMarginAbove(col - lp.column); 1121 | final int colBottom = bottom 1122 | + rec.getMarginBelow(col - lp.column); 1123 | if (colTop < mItemTops[col]) { 1124 | mItemTops[col] = colTop; 1125 | } 1126 | if (colBottom > mItemBottoms[col]) { 1127 | mItemBottoms[col] = colBottom; 1128 | } 1129 | } 1130 | } 1131 | 1132 | for (int col = 0; col < mColCount; col++) { 1133 | if (mItemTops[col] == Integer.MAX_VALUE) { 1134 | // If one was untouched, both were. 1135 | mItemTops[col] = 0; 1136 | mItemBottoms[col] = 0; 1137 | } 1138 | } 1139 | } 1140 | } 1141 | 1142 | public void computeScroll() { 1143 | if (mScroller.computeScrollOffset()) { 1144 | final int y = mScroller.getCurrY(); 1145 | final int dy = (int) (y - mLastTouchY); 1146 | mLastTouchY = y; 1147 | final boolean stopped = !trackMotionScroll(dy, false); 1148 | 1149 | if (!stopped && !mScroller.isFinished()) { 1150 | postInvalidate(); 1151 | } else { 1152 | if (stopped) { 1153 | final int overScrollMode = ViewCompat 1154 | .getOverScrollMode(this); 1155 | if (overScrollMode != ViewCompat.OVER_SCROLL_NEVER) { 1156 | final EdgeEffectCompat edge; 1157 | if (dy > 0) { 1158 | edge = mTopEdge; 1159 | } else { 1160 | edge = mBottomEdge; 1161 | } 1162 | edge.onAbsorb(Math.abs((int) mScroller 1163 | .getCurrVelocity())); 1164 | postInvalidate(); 1165 | } 1166 | mScroller.abortAnimation(); 1167 | } 1168 | mTouchMode = TOUCH_MODE_IDLE; 1169 | } 1170 | } 1171 | } 1172 | 1173 | @Override 1174 | protected void dispatchDraw(Canvas canvas) { 1175 | final boolean drawSelectorOnTop = mDrawSelectorOnTop; 1176 | if (!drawSelectorOnTop) { 1177 | drawSelector(canvas); 1178 | } 1179 | 1180 | super.dispatchDraw(canvas); 1181 | 1182 | if (drawSelectorOnTop) { 1183 | drawSelector(canvas); 1184 | } 1185 | } 1186 | 1187 | private void drawSelector(Canvas canvas) { 1188 | if (!mSelectorRect.isEmpty() && mSelector != null && mBeginClick) { 1189 | final Drawable selector = mSelector; 1190 | selector.setBounds(mSelectorRect); 1191 | selector.draw(canvas); 1192 | } 1193 | } 1194 | 1195 | @Override 1196 | public void draw(Canvas canvas) { 1197 | super.draw(canvas); 1198 | 1199 | if (mTopEdge != null) { 1200 | boolean needsInvalidate = false; 1201 | if (!mTopEdge.isFinished()) { 1202 | mTopEdge.draw(canvas); 1203 | needsInvalidate = true; 1204 | } 1205 | if (!mBottomEdge.isFinished()) { 1206 | final int restoreCount = canvas.save(); 1207 | final int width = getWidth(); 1208 | canvas.translate(-width, getHeight()); 1209 | canvas.rotate(180, width, 0); 1210 | mBottomEdge.draw(canvas); 1211 | canvas.restoreToCount(restoreCount); 1212 | needsInvalidate = true; 1213 | } 1214 | 1215 | if (needsInvalidate) { 1216 | invalidate(); 1217 | } 1218 | } 1219 | if (mFastScroller != null) { 1220 | final int scrollY = this.getScrollY(); 1221 | if (scrollY != 0) { 1222 | // Pin to the top/bottom during overscroll 1223 | int restoreCount = canvas.save(); 1224 | canvas.translate(0, (float) scrollY); 1225 | mFastScroller.draw(canvas); 1226 | canvas.restoreToCount(restoreCount); 1227 | } else { 1228 | mFastScroller.draw(canvas); 1229 | } 1230 | } 1231 | // drawSelector(canvas); 1232 | } 1233 | 1234 | public void beginFastChildLayout() { 1235 | mFastChildLayout = true; 1236 | } 1237 | 1238 | public void endFastChildLayout() { 1239 | mFastChildLayout = false; 1240 | populate(false); 1241 | } 1242 | 1243 | @Override 1244 | public void requestLayout() { 1245 | if (!mPopulating && !mFastChildLayout) { 1246 | super.requestLayout(); 1247 | } 1248 | } 1249 | 1250 | @Override 1251 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1252 | 1253 | int widthMode = MeasureSpec.getMode(widthMeasureSpec); 1254 | int heightMode = MeasureSpec.getMode(heightMeasureSpec); 1255 | int widthSize = MeasureSpec.getSize(widthMeasureSpec); 1256 | int heightSize = MeasureSpec.getSize(heightMeasureSpec); 1257 | 1258 | if (widthMode != MeasureSpec.EXACTLY) { 1259 | widthMode = MeasureSpec.EXACTLY; 1260 | } 1261 | if (heightMode != MeasureSpec.EXACTLY) { 1262 | heightMode = MeasureSpec.EXACTLY; 1263 | } 1264 | 1265 | setMeasuredDimension(widthSize, heightSize); 1266 | 1267 | if (mColCountSetting == COLUMN_COUNT_AUTO) { 1268 | final int colCount = widthSize / mMinColWidth; 1269 | if (colCount != mColCount) { 1270 | mColCount = colCount; 1271 | } 1272 | } 1273 | } 1274 | 1275 | @Override 1276 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 1277 | mInLayout = true; 1278 | populate(false); 1279 | mInLayout = false; 1280 | 1281 | final int width = r - l; 1282 | final int height = b - t; 1283 | mTopEdge.setSize(width, height); 1284 | mBottomEdge.setSize(width, height); 1285 | } 1286 | 1287 | private void populate(boolean clearData) { 1288 | 1289 | if (getWidth() == 0 || getHeight() == 0) { 1290 | return; 1291 | } 1292 | 1293 | if (mColCount == COLUMN_COUNT_AUTO) { 1294 | final int colCount = getWidth() / mMinColWidth; 1295 | if (colCount != mColCount) { 1296 | mColCount = colCount; 1297 | } 1298 | } 1299 | 1300 | final int colCount = mColCount; 1301 | 1302 | // setup arraylist for mappings 1303 | if (mColMappings.size() != mColCount) { 1304 | mColMappings.clear(); 1305 | for (int i = 0; i < mColCount; i++) { 1306 | mColMappings.add(new ArrayList()); 1307 | } 1308 | } 1309 | 1310 | if (mItemTops == null || mItemTops.length != colCount) { 1311 | mItemTops = new int[colCount]; 1312 | mItemBottoms = new int[colCount]; 1313 | 1314 | mLayoutRecords.clear(); 1315 | if (mInLayout) { 1316 | removeAllViewsInLayout(); 1317 | } else { 1318 | removeAllViews(); 1319 | } 1320 | } 1321 | 1322 | final int top = getPaddingTop(); 1323 | for (int i = 0; i < colCount; i++) { 1324 | final int offset = top 1325 | + ((mRestoreOffsets != null) ? Math.min(mRestoreOffsets[i], 1326 | 0) : 0); 1327 | mItemTops[i] = (offset == 0) ? mItemTops[i] : offset; 1328 | mItemBottoms[i] = (offset == 0) ? mItemBottoms[i] : offset; 1329 | } 1330 | 1331 | mPopulating = true; 1332 | 1333 | layoutChildren(mDataChanged); 1334 | fillDown(mFirstPosition + getChildCount(), 0); 1335 | fillUp(mFirstPosition - 1, 0); 1336 | mPopulating = false; 1337 | mDataChanged = false; 1338 | 1339 | if (clearData) { 1340 | if (mRestoreOffsets != null) 1341 | Arrays.fill(mRestoreOffsets, 0); 1342 | } 1343 | } 1344 | 1345 | final void offsetChildren(int offset) { 1346 | final int childCount = getChildCount(); 1347 | for (int i = 0; i < childCount; i++) { 1348 | final View child = getChildAt(i); 1349 | child.layout(child.getLeft(), child.getTop() + offset, 1350 | child.getRight(), child.getBottom() + offset); 1351 | } 1352 | 1353 | final int colCount = mColCount; 1354 | for (int i = 0; i < colCount; i++) { 1355 | mItemTops[i] += offset; 1356 | mItemBottoms[i] += offset; 1357 | } 1358 | } 1359 | 1360 | /** Measure and layout all currently visible children. 1361 | * 1362 | * @param queryAdapter 1363 | * true to requery the adapter for view data */ 1364 | final void layoutChildren(boolean queryAdapter) { 1365 | final int paddingLeft = getPaddingLeft(); 1366 | final int paddingRight = getPaddingRight(); 1367 | // final int leftMargin = mItemLeftMargin; 1368 | // final int rightMargin = mItemRightMargin; 1369 | // final int topMargin = mItemTopMargin; 1370 | // final int bottomMargin = mItemBottomMargin; 1371 | 1372 | final int colWidth = (getWidth() - paddingLeft - paddingRight) 1373 | / mColCount; 1374 | mColWidth = colWidth; 1375 | int rebuildLayoutRecordsBefore = -1; 1376 | int rebuildLayoutRecordsAfter = -1; 1377 | 1378 | Arrays.fill(mItemBottoms, Integer.MIN_VALUE); 1379 | 1380 | final int childCount = getChildCount(); 1381 | int amountRemoved = 0; 1382 | 1383 | for (int i = 0; i < childCount; i++) { 1384 | View child = getChildAt(i); 1385 | LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1386 | final int col = lp.column; 1387 | final int position = mFirstPosition + i; 1388 | final boolean needsLayout = queryAdapter 1389 | || child.isLayoutRequested(); 1390 | 1391 | if (queryAdapter) { 1392 | 1393 | View newView = obtainView(position, child); 1394 | if (newView == null) { 1395 | // child has been removed 1396 | removeViewAt(i); 1397 | if (i - 1 >= 0) 1398 | invalidateLayoutRecordsAfterPosition(i - 1); 1399 | amountRemoved++; 1400 | continue; 1401 | } else if (newView != child) { 1402 | removeViewAt(i); 1403 | addView(newView, i); 1404 | child = newView; 1405 | } 1406 | lp = (LayoutParams) child.getLayoutParams(); // Might have 1407 | // changed 1408 | } 1409 | 1410 | final int span = Math.min(mColCount, lp.span); 1411 | final int widthSize = colWidth * span; 1412 | 1413 | if (needsLayout) { 1414 | final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, 1415 | MeasureSpec.EXACTLY); 1416 | 1417 | final int heightSpec; 1418 | if (lp.height == LayoutParams.WRAP_CONTENT) { 1419 | heightSpec = MeasureSpec.makeMeasureSpec(0, 1420 | MeasureSpec.UNSPECIFIED); 1421 | } else { 1422 | heightSpec = MeasureSpec.makeMeasureSpec(lp.height, 1423 | MeasureSpec.EXACTLY); 1424 | } 1425 | 1426 | child.measure(widthSpec, heightSpec); 1427 | } 1428 | 1429 | int childTop = mItemBottoms[col] > Integer.MIN_VALUE ? mItemBottoms[col] 1430 | : child.getTop(); 1431 | 1432 | if (span > 1) { 1433 | int lowest = childTop; 1434 | for (int j = col + 1; j < col + span; j++) { 1435 | final int bottom = mItemBottoms[j];// + bottomMargin; 1436 | if (bottom > lowest) { 1437 | lowest = bottom; 1438 | } 1439 | } 1440 | childTop = lowest; 1441 | } 1442 | 1443 | final int childHeight = child.getMeasuredHeight(); 1444 | final int childBottom = childTop + childHeight; 1445 | final int childLeft = paddingLeft + col * (colWidth); 1446 | final int childRight = childLeft + child.getMeasuredWidth(); 1447 | child.layout(childLeft, childTop, childRight, childBottom); 1448 | 1449 | for (int j = col; j < col + span; j++) { 1450 | mItemBottoms[j] = childBottom; 1451 | } 1452 | 1453 | final LayoutRecord rec = mLayoutRecords.get(position); 1454 | if (rec != null && rec.height != childHeight) { 1455 | // Invalidate our layout records for everything before this. 1456 | rec.height = childHeight; 1457 | rebuildLayoutRecordsBefore = position; 1458 | } 1459 | 1460 | if (rec != null && rec.span != span) { 1461 | // Invalidate our layout records for everything after this. 1462 | rec.span = span; 1463 | rebuildLayoutRecordsAfter = position; 1464 | } 1465 | } 1466 | 1467 | // Update mItemBottoms for any empty columns 1468 | for (int i = 0; i < mColCount; i++) { 1469 | if (mItemBottoms[i] == Integer.MIN_VALUE) { 1470 | mItemBottoms[i] = mItemTops[i]; 1471 | } 1472 | } 1473 | 1474 | if (rebuildLayoutRecordsBefore >= 0 || rebuildLayoutRecordsAfter >= 0) { 1475 | if (rebuildLayoutRecordsBefore >= 0) { 1476 | invalidateLayoutRecordsBeforePosition(rebuildLayoutRecordsBefore); 1477 | } 1478 | if (rebuildLayoutRecordsAfter >= 0) { 1479 | invalidateLayoutRecordsAfterPosition(rebuildLayoutRecordsAfter); 1480 | } 1481 | for (int i = 0; i < (childCount - amountRemoved); i++) { 1482 | final int position = mFirstPosition + i; 1483 | final View child = getChildAt(i); 1484 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1485 | LayoutRecord rec = mLayoutRecords.get(position); 1486 | if (rec == null) { 1487 | rec = new LayoutRecord(); 1488 | mLayoutRecords.put(position, rec); 1489 | } 1490 | rec.column = lp.column; 1491 | rec.height = child.getHeight(); 1492 | rec.id = lp.id; 1493 | rec.span = Math.min(mColCount, lp.span); 1494 | } 1495 | } 1496 | 1497 | if (this.mSelectorPosition != INVALID_POSITION) { 1498 | View child = getChildAt(mMotionPosition - mFirstPosition); 1499 | if (child != null) 1500 | positionSelector(mMotionPosition, child); 1501 | } else if (mTouchMode > TOUCH_MODE_DOWN) { 1502 | View child = getChildAt(mMotionPosition - mFirstPosition); 1503 | if (child != null) 1504 | positionSelector(mMotionPosition, child); 1505 | } else { 1506 | mSelectorRect.setEmpty(); 1507 | } 1508 | invokeOnItemScrollListener(); 1509 | 1510 | } 1511 | 1512 | final void invalidateLayoutRecordsBeforePosition(int position) { 1513 | int endAt = 0; 1514 | while (endAt < mLayoutRecords.size() 1515 | && mLayoutRecords.keyAt(endAt) < position) { 1516 | endAt++; 1517 | } 1518 | mLayoutRecords.removeAtRange(0, endAt); 1519 | } 1520 | 1521 | final void invalidateLayoutRecordsAfterPosition(int position) { 1522 | int beginAt = mLayoutRecords.size() - 1; 1523 | while (beginAt >= 0 && mLayoutRecords.keyAt(beginAt) > position) { 1524 | beginAt--; 1525 | } 1526 | beginAt++; 1527 | mLayoutRecords.removeAtRange(beginAt + 1, mLayoutRecords.size() 1528 | - beginAt); 1529 | } 1530 | 1531 | public void setOnScrollListener(OnScrollListener l) { 1532 | mOnScrollListener = l; 1533 | invokeOnItemScrollListener(); 1534 | } 1535 | 1536 | /** Notify our scroll listener (if there is one) of a change in scroll state */ 1537 | void invokeOnItemScrollListener() { 1538 | if (mFastScroller != null) { 1539 | mFastScroller.onScroll(this, mFirstPosition, getChildCount(), 1540 | mItemCount); 1541 | } 1542 | if (mOnScrollListener != null) { 1543 | mOnScrollListener.onScroll(this, mFirstPosition, getChildCount(), 1544 | mItemCount); 1545 | } 1546 | } 1547 | 1548 | /** Should be called with mPopulating set to true 1549 | * 1550 | * @param fromPosition 1551 | * Position to start filling from 1552 | * @param overhang 1553 | * the number of extra pixels to fill beyond the current top edge 1554 | * @return the max overhang beyond the beginning of the view of any added 1555 | * items at the top */ 1556 | final int fillUp(int fromPosition, int overhang) { 1557 | 1558 | final int paddingLeft = getPaddingLeft(); 1559 | final int paddingRight = getPaddingRight(); 1560 | // final int itemMargin = mItemMargin; 1561 | 1562 | final int colWidth = (getWidth() - paddingLeft - paddingRight) 1563 | / mColCount; 1564 | mColWidth = colWidth; 1565 | final int gridTop = getPaddingTop(); 1566 | final int fillTo = gridTop - overhang; 1567 | int nextCol = getNextColumnUp(); 1568 | int position = fromPosition; 1569 | 1570 | while (nextCol >= 0 && mItemTops[nextCol] > fillTo && position >= 0) { 1571 | // make sure the nextCol is correct. check to see if has been mapped 1572 | // otherwise stick to getNextColumnUp() 1573 | if (!mColMappings.get(nextCol).contains((Integer) position)) { 1574 | for (int i = 0; i < mColMappings.size(); i++) { 1575 | if (mColMappings.get(i).contains((Integer) position)) { 1576 | nextCol = i; 1577 | break; 1578 | } 1579 | } 1580 | } 1581 | 1582 | // displayMapping(); 1583 | 1584 | final View child = obtainView(position, null); 1585 | 1586 | if (child == null) 1587 | continue; 1588 | 1589 | LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1590 | 1591 | if (lp == null) { 1592 | lp = this.generateDefaultLayoutParams(); 1593 | child.setLayoutParams(lp); 1594 | } 1595 | 1596 | if (child.getParent() != this) { 1597 | if (mInLayout) { 1598 | addViewInLayout(child, 0, lp); 1599 | } else { 1600 | addView(child, 0); 1601 | } 1602 | } 1603 | 1604 | final int span = Math.min(mColCount, lp.span); 1605 | final int widthSize = colWidth * span; 1606 | final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, 1607 | MeasureSpec.EXACTLY); 1608 | 1609 | LayoutRecord rec; 1610 | if (span > 1) { 1611 | rec = getNextRecordUp(position, span); 1612 | // nextCol = rec.column; 1613 | } else { 1614 | rec = mLayoutRecords.get(position); 1615 | } 1616 | 1617 | boolean invalidateBefore = false; 1618 | if (rec == null) { 1619 | rec = new LayoutRecord(); 1620 | mLayoutRecords.put(position, rec); 1621 | rec.column = nextCol; 1622 | rec.span = span; 1623 | } else if (span != rec.span) { 1624 | rec.span = span; 1625 | rec.column = nextCol; 1626 | invalidateBefore = true; 1627 | } else { 1628 | // nextCol = rec.column; 1629 | } 1630 | 1631 | if (mHasStableIds) { 1632 | final long id = mAdapter.getItemId(position); 1633 | rec.id = id; 1634 | lp.id = id; 1635 | } 1636 | 1637 | lp.column = nextCol; 1638 | 1639 | final int heightSpec; 1640 | if (lp.height == LayoutParams.WRAP_CONTENT) { 1641 | heightSpec = MeasureSpec.makeMeasureSpec(0, 1642 | MeasureSpec.UNSPECIFIED); 1643 | } else { 1644 | heightSpec = MeasureSpec.makeMeasureSpec(lp.height, 1645 | MeasureSpec.EXACTLY); 1646 | } 1647 | child.measure(widthSpec, heightSpec); 1648 | 1649 | final int childHeight = child.getMeasuredHeight(); 1650 | if (invalidateBefore 1651 | || (childHeight != rec.height && rec.height > 0)) { 1652 | invalidateLayoutRecordsBeforePosition(position); 1653 | } 1654 | rec.height = childHeight; 1655 | 1656 | // int itemTop = mItemTops[nextCol]; 1657 | 1658 | final int startFrom; 1659 | if (span > 1) { 1660 | int highest = mItemTops[nextCol]; 1661 | for (int i = nextCol + 1; i < nextCol + span; i++) { 1662 | final int top = mItemTops[i]; 1663 | if (top < highest) { 1664 | highest = top; 1665 | } 1666 | } 1667 | startFrom = highest; 1668 | } else { 1669 | startFrom = mItemTops[nextCol]; 1670 | } 1671 | 1672 | int childBottom = startFrom; 1673 | int childTop = childBottom - childHeight; 1674 | final int childLeft = paddingLeft + nextCol 1675 | * (colWidth); 1676 | final int childRight = childLeft + child.getMeasuredWidth(); 1677 | 1678 | // if(position == 0){ 1679 | // if(this.getChildCount()>1 && this.mColCount>1){ 1680 | // childTop = this.getChildAt(1).getTop(); 1681 | // childBottom = childTop + childHeight; 1682 | // } 1683 | // } 1684 | 1685 | child.layout(childLeft, childTop, childRight, childBottom); 1686 | 1687 | for (int i = nextCol; i < nextCol + span; i++) { 1688 | mItemTops[i] = childTop - rec.getMarginAbove(i - nextCol); 1689 | } 1690 | 1691 | nextCol = getNextColumnUp(); 1692 | mFirstPosition = position--; 1693 | } 1694 | 1695 | int highestView = getHeight(); 1696 | 1697 | for (int i = 0; i < mColCount; i++) { 1698 | final View child = getFirstChildAtColumn(i); 1699 | if (child == null) { 1700 | highestView = 0; 1701 | break; 1702 | } 1703 | final int top = child.getTop(); 1704 | 1705 | if (top < highestView) { 1706 | highestView = top; 1707 | } 1708 | } 1709 | 1710 | return gridTop - highestView; 1711 | } 1712 | 1713 | private View getFirstChildAtColumn(int column) { 1714 | 1715 | if (this.getChildCount() > column) { 1716 | for (int i = 0; i < this.mColCount; i++) { 1717 | final View child = getChildAt(i); 1718 | final int left = child.getLeft(); 1719 | 1720 | if (child != null) { 1721 | 1722 | int col = 0; 1723 | 1724 | // determine the column by cycling widths 1725 | while (left > col 1726 | * (this.mColWidth) 1727 | + getPaddingLeft()) { 1728 | col++; 1729 | } 1730 | 1731 | if (col == column) { 1732 | return child; 1733 | } 1734 | 1735 | } 1736 | } 1737 | } 1738 | 1739 | return null; 1740 | } 1741 | 1742 | /** Should be called with mPopulating set to true 1743 | * 1744 | * @param fromPosition 1745 | * Position to start filling from 1746 | * @param overhang 1747 | * the number of extra pixels to fill beyond the current bottom 1748 | * edge 1749 | * @return the max overhang beyond the end of the view of any added items at 1750 | * the bottom */ 1751 | final int fillDown(int fromPosition, int overhang) { 1752 | 1753 | final int paddingLeft = getPaddingLeft(); 1754 | final int paddingRight = getPaddingRight(); 1755 | // final int itemMargin = mItemMargin; 1756 | 1757 | final int colWidth = (getWidth() - paddingLeft - paddingRight) 1758 | / mColCount; 1759 | final int gridBottom = getHeight() - getPaddingBottom(); 1760 | final int fillTo = gridBottom + overhang; 1761 | int nextCol = getNextColumnDown(fromPosition); 1762 | int position = fromPosition; 1763 | 1764 | while (nextCol >= 0 && mItemBottoms[nextCol] < fillTo 1765 | && position < mItemCount) { 1766 | 1767 | final View child = obtainView(position, null); 1768 | 1769 | if (child == null) 1770 | continue; 1771 | 1772 | LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1773 | if (lp == null) { 1774 | lp = this.generateDefaultLayoutParams(); 1775 | child.setLayoutParams(lp); 1776 | } 1777 | if (child.getParent() != this) { 1778 | if (mInLayout) { 1779 | addViewInLayout(child, -1, lp); 1780 | } else { 1781 | addView(child); 1782 | } 1783 | } 1784 | 1785 | final int span = Math.min(mColCount, lp.span); 1786 | final int widthSize = colWidth * span; 1787 | final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, 1788 | MeasureSpec.EXACTLY); 1789 | 1790 | LayoutRecord rec; 1791 | if (span > 1) { 1792 | rec = getNextRecordDown(position, span); 1793 | // nextCol = rec.column; 1794 | } else { 1795 | rec = mLayoutRecords.get(position); 1796 | } 1797 | 1798 | boolean invalidateAfter = false; 1799 | if (rec == null) { 1800 | rec = new LayoutRecord(); 1801 | mLayoutRecords.put(position, rec); 1802 | rec.column = nextCol; 1803 | rec.span = span; 1804 | } else if (span != rec.span) { 1805 | rec.span = span; 1806 | rec.column = nextCol; 1807 | invalidateAfter = true; 1808 | } else { 1809 | // nextCol = rec.column; 1810 | } 1811 | 1812 | if (mHasStableIds) { 1813 | final long id = mAdapter.getItemId(position); 1814 | rec.id = id; 1815 | lp.id = id; 1816 | } 1817 | 1818 | lp.column = nextCol; 1819 | 1820 | final int heightSpec; 1821 | if (lp.height == LayoutParams.WRAP_CONTENT) { 1822 | heightSpec = MeasureSpec.makeMeasureSpec(0, 1823 | MeasureSpec.UNSPECIFIED); 1824 | } else { 1825 | heightSpec = MeasureSpec.makeMeasureSpec(lp.height, 1826 | MeasureSpec.EXACTLY); 1827 | } 1828 | child.measure(widthSpec, heightSpec); 1829 | 1830 | final int childHeight = child.getMeasuredHeight(); 1831 | if (invalidateAfter 1832 | || (childHeight != rec.height && rec.height > 0)) { 1833 | invalidateLayoutRecordsAfterPosition(position); 1834 | } 1835 | rec.height = childHeight; 1836 | 1837 | final int startFrom; 1838 | if (span > 1) { 1839 | int lowest = mItemBottoms[nextCol]; 1840 | for (int i = nextCol + 1; i < nextCol + span; i++) { 1841 | final int bottom = mItemBottoms[i]; 1842 | if (bottom > lowest) { 1843 | lowest = bottom; 1844 | } 1845 | } 1846 | startFrom = lowest; 1847 | } else { 1848 | startFrom = mItemBottoms[nextCol]; 1849 | } 1850 | 1851 | final int childTop = startFrom; 1852 | final int childBottom = childTop + childHeight; 1853 | final int childLeft = paddingLeft + nextCol 1854 | * (colWidth); 1855 | final int childRight = childLeft + child.getMeasuredWidth(); 1856 | child.layout(childLeft, childTop, childRight, childBottom); 1857 | 1858 | // add the position to the mapping 1859 | if (!mColMappings.get(nextCol).contains(position)) { 1860 | 1861 | // check to see if the mapping exists in other columns 1862 | // this would happen if list has been updated 1863 | for (ArrayList list : mColMappings) { 1864 | if (list.contains(position)) { 1865 | list.remove((Integer) position); 1866 | } 1867 | } 1868 | 1869 | mColMappings.get(nextCol).add(position); 1870 | 1871 | } 1872 | 1873 | for (int i = nextCol; i < nextCol + span; i++) { 1874 | mItemBottoms[i] = childBottom + rec.getMarginBelow(i - nextCol); 1875 | } 1876 | 1877 | position++; 1878 | nextCol = getNextColumnDown(position); 1879 | } 1880 | 1881 | int lowestView = 0; 1882 | for (int i = 0; i < mColCount; i++) { 1883 | if (mItemBottoms[i] > lowestView) { 1884 | lowestView = mItemBottoms[i]; 1885 | } 1886 | } 1887 | return lowestView - gridBottom; 1888 | } 1889 | 1890 | /** for debug purposes */ 1891 | private void displayMapping() { 1892 | Log.w("DISPLAY", "MAP ****************"); 1893 | StringBuilder sb = new StringBuilder(); 1894 | int col = 0; 1895 | 1896 | for (ArrayList map : this.mColMappings) { 1897 | sb.append("COL" + col + ":"); 1898 | sb.append(' '); 1899 | for (Integer i : map) { 1900 | sb.append(i); 1901 | sb.append(" , "); 1902 | } 1903 | Log.w("DISPLAY", sb.toString()); 1904 | sb = new StringBuilder(); 1905 | col++; 1906 | } 1907 | Log.w("DISPLAY", "MAP END ****************"); 1908 | } 1909 | 1910 | /** @return column that the next view filling upwards should occupy. This is 1911 | * the bottom-most position available for a single-column item. */ 1912 | final int getNextColumnUp() { 1913 | int result = -1; 1914 | int bottomMost = Integer.MIN_VALUE; 1915 | 1916 | final int colCount = mColCount; 1917 | for (int i = colCount - 1; i >= 0; i--) { 1918 | final int top = mItemTops[i]; 1919 | if (top > bottomMost) { 1920 | bottomMost = top; 1921 | result = i; 1922 | } 1923 | } 1924 | return result; 1925 | } 1926 | 1927 | /** Return a LayoutRecord for the given position 1928 | * 1929 | * @param position 1930 | * @param span 1931 | * @return */ 1932 | final LayoutRecord getNextRecordUp(int position, int span) { 1933 | LayoutRecord rec = mLayoutRecords.get(position); 1934 | if (rec == null) { 1935 | rec = new LayoutRecord(); 1936 | rec.span = span; 1937 | mLayoutRecords.put(position, rec); 1938 | } else if (rec.span != span) { 1939 | throw new IllegalStateException( 1940 | "Invalid LayoutRecord! Record had span=" + rec.span 1941 | + " but caller requested span=" + span 1942 | + " for position=" + position); 1943 | } 1944 | int targetCol = -1; 1945 | int bottomMost = Integer.MIN_VALUE; 1946 | 1947 | final int colCount = mColCount; 1948 | for (int i = colCount - span; i >= 0; i--) { 1949 | int top = Integer.MAX_VALUE; 1950 | for (int j = i; j < i + span; j++) { 1951 | final int singleTop = mItemTops[j]; 1952 | if (singleTop < top) { 1953 | top = singleTop; 1954 | } 1955 | } 1956 | if (top > bottomMost) { 1957 | bottomMost = top; 1958 | targetCol = i; 1959 | } 1960 | } 1961 | 1962 | rec.column = targetCol; 1963 | 1964 | for (int i = 0; i < span; i++) { 1965 | rec.setMarginBelow(i, mItemTops[i + targetCol] - bottomMost); 1966 | } 1967 | 1968 | return rec; 1969 | } 1970 | 1971 | /** @return column that the next view filling downwards should occupy. This is 1972 | * the top-most position available. */ 1973 | final int getNextColumnDown(int position) { 1974 | int result = -1; 1975 | int topMost = Integer.MAX_VALUE; 1976 | 1977 | final int colCount = mColCount; 1978 | 1979 | for (int i = 0; i < colCount; i++) { 1980 | final int bottom = mItemBottoms[i]; 1981 | if (bottom < topMost) { 1982 | topMost = bottom; 1983 | result = i; 1984 | } 1985 | } 1986 | 1987 | return result; 1988 | } 1989 | 1990 | final LayoutRecord getNextRecordDown(int position, int span) { 1991 | LayoutRecord rec = mLayoutRecords.get(position); 1992 | if (rec == null) { 1993 | rec = new LayoutRecord(); 1994 | rec.span = span; 1995 | mLayoutRecords.put(position, rec); 1996 | } else if (rec.span != span) { 1997 | throw new IllegalStateException( 1998 | "Invalid LayoutRecord! Record had span=" + rec.span 1999 | + " but caller requested span=" + span 2000 | + " for position=" + position); 2001 | } 2002 | int targetCol = -1; 2003 | int topMost = Integer.MAX_VALUE; 2004 | 2005 | final int colCount = mColCount; 2006 | for (int i = 0; i <= colCount - span; i++) { 2007 | int bottom = Integer.MIN_VALUE; 2008 | for (int j = i; j < i + span; j++) { 2009 | final int singleBottom = mItemBottoms[j]; 2010 | if (singleBottom > bottom) { 2011 | bottom = singleBottom; 2012 | } 2013 | } 2014 | if (bottom < topMost) { 2015 | topMost = bottom; 2016 | targetCol = i; 2017 | } 2018 | } 2019 | 2020 | rec.column = targetCol; 2021 | 2022 | for (int i = 0; i < span; i++) { 2023 | rec.setMarginAbove(i, topMost - mItemBottoms[i + targetCol]); 2024 | } 2025 | 2026 | return rec; 2027 | } 2028 | 2029 | /** Obtain a populated view from the adapter. If optScrap is non-null and is 2030 | * not reused it will be placed in the recycle bin. 2031 | * 2032 | * @param position 2033 | * position to get view for 2034 | * @param optScrap 2035 | * Optional scrap view; will be reused if possible 2036 | * @return A new view, a recycled view from mRecycler, or optScrap */ 2037 | final View obtainView(int position, View optScrap) { 2038 | 2039 | View view = mRecycler.getTransientStateView(position); 2040 | if (view != null) { 2041 | return view; 2042 | } 2043 | 2044 | if (position >= mAdapter.getCount()) { 2045 | view = null; 2046 | return null; 2047 | } 2048 | 2049 | // Reuse optScrap if it's of the right type (and not null) 2050 | final int optType = optScrap != null ? ((LayoutParams) optScrap 2051 | .getLayoutParams()).viewType : -1; 2052 | final int positionViewType = mAdapter.getItemViewType(position); 2053 | final View scrap = optType == positionViewType ? optScrap : mRecycler 2054 | .getScrapView(positionViewType); 2055 | 2056 | view = mAdapter.getView(position, scrap, this); 2057 | 2058 | if (view != scrap && scrap != null) { 2059 | // The adapter didn't use it; put it back. 2060 | mRecycler.addScrap(scrap); 2061 | } 2062 | 2063 | ViewGroup.LayoutParams lp = view.getLayoutParams(); 2064 | 2065 | if (view.getParent() != this) { 2066 | if (lp == null) { 2067 | lp = generateDefaultLayoutParams(); 2068 | } else if (!checkLayoutParams(lp)) { 2069 | lp = generateLayoutParams(lp); 2070 | } 2071 | } 2072 | 2073 | final LayoutParams sglp = (LayoutParams) lp; 2074 | sglp.position = position; 2075 | sglp.viewType = positionViewType; 2076 | 2077 | return view; 2078 | } 2079 | 2080 | public ListAdapter getAdapter() { 2081 | return mAdapter; 2082 | } 2083 | 2084 | public void setAdapter(ListAdapter adapter) { 2085 | if (mAdapter != null) { 2086 | mAdapter.unregisterDataSetObserver(mObserver); 2087 | } 2088 | // TODO: If the new adapter says that there are stable IDs, remove 2089 | // certain layout records 2090 | // and onscreen views if they have changed instead of removing all of 2091 | // the state here. 2092 | clearAllState(); 2093 | mAdapter = adapter; 2094 | mDataChanged = true; 2095 | 2096 | if (adapter != null) { 2097 | adapter.registerDataSetObserver(mObserver); 2098 | mRecycler.setViewTypeCount(adapter.getViewTypeCount()); 2099 | mHasStableIds = adapter.hasStableIds(); 2100 | } else { 2101 | mHasStableIds = false; 2102 | } 2103 | populate(adapter != null); 2104 | } 2105 | 2106 | /** Clear all state because the grid will be used for a completely different 2107 | * set of data. */ 2108 | private void clearAllState() { 2109 | // Clear all layout records and views 2110 | mLayoutRecords.clear(); 2111 | removeAllViews(); 2112 | 2113 | // Reset to the top of the grid 2114 | resetStateForGridTop(); 2115 | 2116 | // Clear recycler because there could be different view types now 2117 | mRecycler.clear(); 2118 | 2119 | mSelectorRect.setEmpty(); 2120 | mSelectorPosition = INVALID_POSITION; 2121 | } 2122 | 2123 | /** Reset all internal state to be at the top of the grid. */ 2124 | private void resetStateForGridTop() { 2125 | // Reset mItemTops and mItemBottoms 2126 | final int colCount = mColCount; 2127 | if (mItemTops == null || mItemTops.length != colCount) { 2128 | mItemTops = new int[colCount]; 2129 | mItemBottoms = new int[colCount]; 2130 | } 2131 | final int top = getPaddingTop(); 2132 | Arrays.fill(mItemTops, top); 2133 | Arrays.fill(mItemBottoms, top); 2134 | 2135 | // Reset the first visible position in the grid to be item 0 2136 | mFirstPosition = 0; 2137 | if (mRestoreOffsets != null) 2138 | Arrays.fill(mRestoreOffsets, 0); 2139 | } 2140 | 2141 | /** Scroll the list so the first visible position in the grid is the first 2142 | * item in the adapter. */ 2143 | public void setSelectionToTop() { 2144 | // Clear out the views (but don't clear out the layout records or 2145 | // recycler because the data 2146 | // has not changed) 2147 | removeAllViews(); 2148 | 2149 | // Reset to top of grid 2150 | resetStateForGridTop(); 2151 | 2152 | // Start populating again 2153 | populate(false); 2154 | } 2155 | 2156 | @Override 2157 | protected LayoutParams generateDefaultLayoutParams() { 2158 | return new LayoutParams(LayoutParams.WRAP_CONTENT); 2159 | } 2160 | 2161 | @Override 2162 | protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { 2163 | return new LayoutParams(lp); 2164 | } 2165 | 2166 | @Override 2167 | protected boolean checkLayoutParams(ViewGroup.LayoutParams lp) { 2168 | return lp instanceof LayoutParams; 2169 | } 2170 | 2171 | @Override 2172 | public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 2173 | return new LayoutParams(getContext(), attrs); 2174 | } 2175 | 2176 | @Override 2177 | public Parcelable onSaveInstanceState() { 2178 | final Parcelable superState = super.onSaveInstanceState(); 2179 | final SavedState ss = new SavedState(superState); 2180 | final int position = mFirstPosition; 2181 | ss.position = mFirstPosition; 2182 | 2183 | if (position >= 0 && mAdapter != null && position < mAdapter.getCount()) { 2184 | ss.firstId = mAdapter.getItemId(position); 2185 | } 2186 | 2187 | if (getChildCount() > 0) { 2188 | 2189 | int topOffsets[] = new int[this.mColCount]; 2190 | 2191 | if (this.mColWidth > 0) 2192 | for (int i = 0; i < mColCount; i++) { 2193 | if (getChildAt(i) != null) { 2194 | final View child = getChildAt(i); 2195 | final int left = child.getLeft(); 2196 | int col = 0; 2197 | Log.w("mColWidth", mColWidth + " " + left); 2198 | 2199 | // determine the column by cycling widths 2200 | while (left > col 2201 | * (this.mColWidth) 2202 | + getPaddingLeft()) { 2203 | col++; 2204 | } 2205 | 2206 | topOffsets[col] = getChildAt(i).getTop() 2207 | 2208 | - getPaddingTop(); 2209 | } 2210 | 2211 | } 2212 | 2213 | ss.topOffsets = topOffsets; 2214 | 2215 | // convert nested arraylist so it can be parcelable 2216 | ArrayList convert = new ArrayList(); 2217 | for (ArrayList cols : mColMappings) { 2218 | convert.add(new ColMap(cols)); 2219 | } 2220 | 2221 | ss.mapping = convert; 2222 | } 2223 | return ss; 2224 | } 2225 | 2226 | @Override 2227 | public void onRestoreInstanceState(Parcelable state) { 2228 | SavedState ss = (SavedState) state; 2229 | super.onRestoreInstanceState(ss.getSuperState()); 2230 | mDataChanged = true; 2231 | mFirstPosition = ss.position; 2232 | mRestoreOffsets = ss.topOffsets; 2233 | 2234 | ArrayList convert = ss.mapping; 2235 | 2236 | if (convert != null) { 2237 | mColMappings.clear(); 2238 | for (ColMap colMap : convert) { 2239 | mColMappings.add(colMap.values); 2240 | } 2241 | } 2242 | 2243 | if (ss.firstId >= 0) { 2244 | this.mFirstAdapterId = ss.firstId; 2245 | mSelectorPosition = INVALID_POSITION; 2246 | } 2247 | 2248 | requestLayout(); 2249 | } 2250 | 2251 | public static class LayoutParams extends ViewGroup.LayoutParams { 2252 | private static final int[] LAYOUT_ATTRS = new int[] { android.R.attr.layout_span }; 2253 | 2254 | private static final int SPAN_INDEX = 0; 2255 | 2256 | /** The number of columns this item should span */ 2257 | public int span = 1; 2258 | 2259 | /** Item position this view represents */ 2260 | int position; 2261 | 2262 | /** Type of this view as reported by the adapter */ 2263 | int viewType; 2264 | 2265 | /** The column this view is occupying */ 2266 | int column; 2267 | 2268 | /** The stable ID of the item this view displays */ 2269 | long id = -1; 2270 | 2271 | public LayoutParams(int height) { 2272 | super(MATCH_PARENT, height); 2273 | 2274 | if (this.height == MATCH_PARENT) { 2275 | Log.w(TAG, 2276 | "Constructing LayoutParams with height FILL_PARENT - " 2277 | + "impossible! Falling back to WRAP_CONTENT"); 2278 | this.height = WRAP_CONTENT; 2279 | } 2280 | } 2281 | 2282 | public LayoutParams(Context c, AttributeSet attrs) { 2283 | super(c, attrs); 2284 | 2285 | if (this.width != MATCH_PARENT) { 2286 | Log.w(TAG, "Inflation setting LayoutParams width to " 2287 | + this.width + " - must be MATCH_PARENT"); 2288 | this.width = MATCH_PARENT; 2289 | } 2290 | if (this.height == MATCH_PARENT) { 2291 | Log.w(TAG, 2292 | "Inflation setting LayoutParams height to MATCH_PARENT - " 2293 | + "impossible! Falling back to WRAP_CONTENT"); 2294 | this.height = WRAP_CONTENT; 2295 | } 2296 | 2297 | TypedArray a = c.obtainStyledAttributes(attrs, LAYOUT_ATTRS); 2298 | span = a.getInteger(SPAN_INDEX, 1); 2299 | a.recycle(); 2300 | } 2301 | 2302 | public LayoutParams(ViewGroup.LayoutParams other) { 2303 | super(other); 2304 | 2305 | if (this.width != MATCH_PARENT) { 2306 | Log.w(TAG, "Constructing LayoutParams with width " + this.width 2307 | + " - must be MATCH_PARENT"); 2308 | this.width = MATCH_PARENT; 2309 | } 2310 | if (this.height == MATCH_PARENT) { 2311 | Log.w(TAG, 2312 | "Constructing LayoutParams with height MATCH_PARENT - " 2313 | + "impossible! Falling back to WRAP_CONTENT"); 2314 | this.height = WRAP_CONTENT; 2315 | } 2316 | } 2317 | } 2318 | 2319 | private class RecycleBin { 2320 | private ArrayList[] mScrapViews; 2321 | private int mViewTypeCount; 2322 | private int mMaxScrap; 2323 | 2324 | private SparseArray mTransientStateViews; 2325 | 2326 | public void setViewTypeCount(int viewTypeCount) { 2327 | if (viewTypeCount < 1) { 2328 | throw new IllegalArgumentException( 2329 | "Must have at least one view type (" + viewTypeCount 2330 | + " types reported)"); 2331 | } 2332 | if (viewTypeCount == mViewTypeCount) { 2333 | return; 2334 | } 2335 | 2336 | @SuppressWarnings("unchecked") 2337 | ArrayList[] scrapViews = new ArrayList[viewTypeCount]; 2338 | 2339 | for (int i = 0; i < viewTypeCount; i++) { 2340 | scrapViews[i] = new ArrayList(); 2341 | } 2342 | mViewTypeCount = viewTypeCount; 2343 | mScrapViews = scrapViews; 2344 | } 2345 | 2346 | public void clear() { 2347 | final int typeCount = mViewTypeCount; 2348 | for (int i = 0; i < typeCount; i++) { 2349 | mScrapViews[i].clear(); 2350 | } 2351 | if (mTransientStateViews != null) { 2352 | mTransientStateViews.clear(); 2353 | } 2354 | } 2355 | 2356 | public void clearTransientViews() { 2357 | if (mTransientStateViews != null) { 2358 | mTransientStateViews.clear(); 2359 | } 2360 | } 2361 | 2362 | public void addScrap(View v) { 2363 | final LayoutParams lp = (LayoutParams) v.getLayoutParams(); 2364 | if (ViewCompat.hasTransientState(v)) { 2365 | if (mTransientStateViews == null) { 2366 | mTransientStateViews = new SparseArray(); 2367 | } 2368 | mTransientStateViews.put(lp.position, v); 2369 | return; 2370 | } 2371 | 2372 | final int childCount = getChildCount(); 2373 | if (childCount > mMaxScrap) { 2374 | mMaxScrap = childCount; 2375 | } 2376 | 2377 | ArrayList scrap = mScrapViews[lp.viewType]; 2378 | if (scrap.size() < mMaxScrap) { 2379 | scrap.add(v); 2380 | } 2381 | } 2382 | 2383 | public View getTransientStateView(int position) { 2384 | if (mTransientStateViews == null) { 2385 | return null; 2386 | } 2387 | 2388 | final View result = mTransientStateViews.get(position); 2389 | if (result != null) { 2390 | mTransientStateViews.remove(position); 2391 | } 2392 | return result; 2393 | } 2394 | 2395 | public View getScrapView(int type) { 2396 | ArrayList scrap = mScrapViews[type]; 2397 | if (scrap.isEmpty()) { 2398 | return null; 2399 | } 2400 | 2401 | final int index = scrap.size() - 1; 2402 | final View result = scrap.get(index); 2403 | scrap.remove(index); 2404 | return result; 2405 | } 2406 | 2407 | } 2408 | 2409 | private class AdapterDataSetObserver extends DataSetObserver { 2410 | @Override 2411 | public void onChanged() { 2412 | 2413 | mDataChanged = true; 2414 | mItemCount = mAdapter.getCount(); 2415 | 2416 | // TODO: Consider matching these back up if we have stable IDs. 2417 | mRecycler.clearTransientViews(); 2418 | 2419 | if (!mHasStableIds) { 2420 | // Clear all layout records and recycle the views 2421 | mLayoutRecords.clear(); 2422 | recycleAllViews(); 2423 | 2424 | // Reset item bottoms to be equal to item tops 2425 | final int colCount = mColCount; 2426 | for (int i = 0; i < colCount; i++) { 2427 | mItemBottoms[i] = mItemTops[i]; 2428 | } 2429 | } 2430 | 2431 | // reset list if position does not exist or id for position has 2432 | // changed 2433 | if (mFirstPosition > mItemCount - 1 2434 | || mAdapter.getItemId(mFirstPosition) != mFirstAdapterId) { 2435 | mFirstPosition = 0; 2436 | Arrays.fill(mItemTops, 0); 2437 | Arrays.fill(mItemBottoms, 0); 2438 | 2439 | if (mRestoreOffsets != null) 2440 | Arrays.fill(mRestoreOffsets, 0); 2441 | } 2442 | 2443 | // TODO: consider repopulating in a deferred runnable instead 2444 | // (so that successive changes may still be batched) 2445 | requestLayout(); 2446 | } 2447 | 2448 | @Override 2449 | public void onInvalidated() { 2450 | } 2451 | } 2452 | 2453 | static class ColMap implements Parcelable { 2454 | private ArrayList values; 2455 | int tempMap[]; 2456 | 2457 | public ColMap(ArrayList values) { 2458 | this.values = values; 2459 | } 2460 | 2461 | private ColMap(Parcel in) { 2462 | in.readIntArray(tempMap); 2463 | values = new ArrayList(); 2464 | for (int index = 0; index < tempMap.length; index++) { 2465 | values.add(tempMap[index]); 2466 | } 2467 | } 2468 | 2469 | @Override 2470 | public void writeToParcel(Parcel out, int flags) { 2471 | tempMap = toIntArray(values); 2472 | out.writeIntArray(tempMap); 2473 | } 2474 | 2475 | public static final Creator CREATOR = new Creator() { 2476 | public ColMap createFromParcel(Parcel source) { 2477 | return new ColMap(source); 2478 | } 2479 | 2480 | public ColMap[] newArray(int size) { 2481 | return new ColMap[size]; 2482 | } 2483 | }; 2484 | 2485 | int[] toIntArray(ArrayList list) { 2486 | int[] ret = new int[list.size()]; 2487 | for (int i = 0; i < ret.length; i++) 2488 | ret[i] = list.get(i); 2489 | return ret; 2490 | } 2491 | 2492 | @Override 2493 | public int describeContents() { 2494 | return 0; 2495 | } 2496 | } 2497 | 2498 | static class SavedState extends BaseSavedState { 2499 | long firstId = -1; 2500 | int position; 2501 | int topOffsets[]; 2502 | ArrayList mapping; 2503 | 2504 | SavedState(Parcelable superState) { 2505 | super(superState); 2506 | } 2507 | 2508 | private SavedState(Parcel in) { 2509 | super(in); 2510 | firstId = in.readLong(); 2511 | position = in.readInt(); 2512 | in.readIntArray(topOffsets); 2513 | in.readTypedList(mapping, ColMap.CREATOR); 2514 | 2515 | } 2516 | 2517 | @Override 2518 | public void writeToParcel(Parcel out, int flags) { 2519 | super.writeToParcel(out, flags); 2520 | out.writeLong(firstId); 2521 | out.writeInt(position); 2522 | out.writeIntArray(topOffsets); 2523 | out.writeTypedList(mapping); 2524 | } 2525 | 2526 | @Override 2527 | public String toString() { 2528 | return "StaggereGridView.SavedState{" 2529 | + Integer.toHexString(System.identityHashCode(this)) 2530 | + " firstId=" + firstId + " position=" + position + "}"; 2531 | } 2532 | 2533 | public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { 2534 | public SavedState createFromParcel(Parcel in) { 2535 | return new SavedState(in); 2536 | } 2537 | 2538 | public SavedState[] newArray(int size) { 2539 | return new SavedState[size]; 2540 | } 2541 | }; 2542 | } 2543 | 2544 | /** A base class for Runnables that will check that their view is still 2545 | * attached to the original window as when the Runnable was created. */ 2546 | private class WindowRunnnable { 2547 | private int mOriginalAttachCount; 2548 | 2549 | public void rememberWindowAttachCount() { 2550 | mOriginalAttachCount = getWindowAttachCount(); 2551 | } 2552 | 2553 | public boolean sameWindow() { 2554 | return hasWindowFocus() 2555 | && getWindowAttachCount() == mOriginalAttachCount; 2556 | } 2557 | } 2558 | 2559 | private void useDefaultSelector() { 2560 | setSelector(getResources().getDrawable( 2561 | android.R.drawable.list_selector_background)); 2562 | } 2563 | 2564 | void positionSelector(int position, View sel) { 2565 | if (position != INVALID_POSITION) { 2566 | mSelectorPosition = position; 2567 | } 2568 | 2569 | final Rect selectorRect = mSelectorRect; 2570 | selectorRect.set(sel.getLeft(), sel.getTop(), sel.getRight(), 2571 | sel.getBottom()); 2572 | if (sel instanceof SelectionBoundsAdjuster) { 2573 | ((SelectionBoundsAdjuster) sel) 2574 | .adjustListItemSelectionBounds(selectorRect); 2575 | } 2576 | 2577 | positionSelector(selectorRect.left, selectorRect.top, 2578 | selectorRect.right, selectorRect.bottom); 2579 | 2580 | final boolean isChildViewEnabled = mIsChildViewEnabled; 2581 | if (sel.isEnabled() != isChildViewEnabled) { 2582 | mIsChildViewEnabled = !isChildViewEnabled; 2583 | if (getSelectedItemPosition() != INVALID_POSITION) { 2584 | refreshDrawableState(); 2585 | } 2586 | } 2587 | } 2588 | 2589 | /** The top-level view of a list item can implement this interface to allow 2590 | * itself to modify the bounds of the selection shown for that item. */ 2591 | public interface SelectionBoundsAdjuster { 2592 | /** Called to allow the list item to adjust the bounds shown for its 2593 | * selection. 2594 | * 2595 | * @param bounds 2596 | * On call, this contains the bounds the list has selected 2597 | * for the item (that is the bounds of the entire view). The 2598 | * values can be modified as desired. */ 2599 | public void adjustListItemSelectionBounds(Rect bounds); 2600 | } 2601 | 2602 | private int getSelectedItemPosition() { 2603 | // TODO: setup mNextSelectedPosition 2604 | return this.mSelectorPosition; 2605 | } 2606 | 2607 | @Override 2608 | protected int[] onCreateDrawableState(int extraSpace) { 2609 | // If the child view is enabled then do the default behavior. 2610 | if (mIsChildViewEnabled) { 2611 | // Common case 2612 | return super.onCreateDrawableState(extraSpace); 2613 | } 2614 | 2615 | // The selector uses this View's drawable state. The selected child view 2616 | // is disabled, so we need to remove the enabled state from the drawable 2617 | // states. 2618 | final int enabledState = ENABLED_STATE_SET[0]; 2619 | 2620 | // If we don't have any extra space, it will return one of the static 2621 | // state arrays, 2622 | // and clearing the enabled state on those arrays is a bad thing! If we 2623 | // specify 2624 | // we need extra space, it will create+copy into a new array that safely 2625 | // mutable. 2626 | int[] state = super.onCreateDrawableState(extraSpace + 1); 2627 | int enabledPos = -1; 2628 | for (int i = state.length - 1; i >= 0; i--) { 2629 | if (state[i] == enabledState) { 2630 | enabledPos = i; 2631 | break; 2632 | } 2633 | } 2634 | 2635 | // Remove the enabled state 2636 | if (enabledPos >= 0) { 2637 | System.arraycopy(state, enabledPos + 1, state, enabledPos, 2638 | state.length - enabledPos - 1); 2639 | } 2640 | 2641 | return state; 2642 | } 2643 | 2644 | private void positionSelector(int l, int t, int r, int b) { 2645 | mSelectorRect.set(l - mSelectionLeftPadding, t - mSelectionTopPadding, 2646 | r + mSelectionRightPadding, b + mSelectionBottomPadding); 2647 | } 2648 | 2649 | final class CheckForTap implements Runnable { 2650 | public void run() { 2651 | if (mTouchMode == TOUCH_MODE_DOWN) { 2652 | 2653 | mTouchMode = TOUCH_MODE_TAP; 2654 | final View child = getChildAt(mMotionPosition - mFirstPosition); 2655 | if (child != null && !child.hasFocusable()) { 2656 | 2657 | if (!mDataChanged) { 2658 | child.setSelected(true); 2659 | child.setPressed(true); 2660 | 2661 | setPressed(true); 2662 | layoutChildren(false); 2663 | positionSelector(mMotionPosition, child); 2664 | refreshDrawableState(); 2665 | 2666 | final int longPressTimeout = ViewConfiguration 2667 | .getLongPressTimeout(); 2668 | final boolean longClickable = isLongClickable(); 2669 | 2670 | if (mSelector != null) { 2671 | Drawable d = mSelector.getCurrent(); 2672 | if (d instanceof TransitionDrawable) { 2673 | if (longClickable) { 2674 | ((TransitionDrawable) d) 2675 | .startTransition(longPressTimeout); 2676 | } else { 2677 | ((TransitionDrawable) d).resetTransition(); 2678 | } 2679 | } 2680 | } 2681 | 2682 | if (longClickable) { 2683 | if (mPendingCheckForLongPress == null) { 2684 | mPendingCheckForLongPress = new CheckForLongPress(); 2685 | } 2686 | mPendingCheckForLongPress 2687 | .rememberWindowAttachCount(); 2688 | postDelayed(mPendingCheckForLongPress, 2689 | longPressTimeout); 2690 | } else { 2691 | mTouchMode = TOUCH_MODE_DONE_WAITING; 2692 | } 2693 | 2694 | postInvalidate(); 2695 | } else { 2696 | mTouchMode = TOUCH_MODE_DONE_WAITING; 2697 | } 2698 | } 2699 | } 2700 | } 2701 | } 2702 | 2703 | private class CheckForLongPress extends WindowRunnnable implements Runnable { 2704 | public void run() { 2705 | final int motionPosition = mMotionPosition; 2706 | final View child = getChildAt(motionPosition - mFirstPosition); 2707 | if (child != null) { 2708 | final int longPressPosition = mMotionPosition; 2709 | final long longPressId = mAdapter.getItemId(mMotionPosition); 2710 | 2711 | boolean handled = false; 2712 | if (sameWindow() && !mDataChanged) { 2713 | handled = performLongPress(child, longPressPosition, 2714 | longPressId); 2715 | } 2716 | if (handled) { 2717 | mTouchMode = TOUCH_MODE_REST; 2718 | setPressed(false); 2719 | child.setPressed(false); 2720 | } else { 2721 | mTouchMode = TOUCH_MODE_DONE_WAITING; 2722 | } 2723 | } 2724 | } 2725 | } 2726 | 2727 | private class PerformClick extends WindowRunnnable implements Runnable { 2728 | int mClickMotionPosition; 2729 | 2730 | public void run() { 2731 | // The data has changed since we posted this action in the event 2732 | // queue, 2733 | // bail out before bad things happen 2734 | if (mDataChanged) 2735 | return; 2736 | 2737 | final ListAdapter adapter = mAdapter; 2738 | final int motionPosition = mClickMotionPosition; 2739 | if (adapter != null && mItemCount > 0 2740 | && motionPosition != INVALID_POSITION 2741 | && motionPosition < adapter.getCount() && sameWindow()) { 2742 | final View view = getChildAt(motionPosition - mFirstPosition); 2743 | // If there is no view, something bad happened (the view 2744 | // scrolled off the 2745 | // screen, etc.) and we should cancel the click 2746 | if (view != null) { 2747 | performItemClick(view, motionPosition, 2748 | adapter.getItemId(motionPosition)); 2749 | } 2750 | } 2751 | } 2752 | } 2753 | 2754 | public boolean performItemClick(View view, int position, long id) { 2755 | if (mOnItemClickListener != null) { 2756 | playSoundEffect(SoundEffectConstants.CLICK); 2757 | if (view != null) { 2758 | view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); 2759 | } 2760 | mOnItemClickListener.onItemClick(this, view, position, id); 2761 | return true; 2762 | } 2763 | 2764 | return false; 2765 | } 2766 | 2767 | boolean performLongPress(final View child, final int longPressPosition, 2768 | final long longPressId) { 2769 | 2770 | // TODO : add check for multiple choice mode.. currently modes are yet 2771 | // to be supported 2772 | 2773 | boolean handled = false; 2774 | if (mOnItemLongClickListener != null) { 2775 | handled = mOnItemLongClickListener.onItemLongClick(this, child, 2776 | longPressPosition, longPressId); 2777 | } 2778 | if (!handled) { 2779 | mContextMenuInfo = createContextMenuInfo(child, longPressPosition, 2780 | longPressId); 2781 | handled = super.showContextMenuForChild(this); 2782 | } 2783 | if (handled) { 2784 | performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); 2785 | } 2786 | return handled; 2787 | } 2788 | 2789 | @Override 2790 | protected ContextMenuInfo getContextMenuInfo() { 2791 | return mContextMenuInfo; 2792 | } 2793 | 2794 | /** Creates the ContextMenuInfo returned from {@link #getContextMenuInfo()}. 2795 | * This methods knows the view, position and ID of the item that received 2796 | * the long press. 2797 | * 2798 | * @param view 2799 | * The view that received the long press. 2800 | * @param position 2801 | * The position of the item that received the long press. 2802 | * @param id 2803 | * The ID of the item that received the long press. 2804 | * @return The extra information that should be returned by 2805 | * {@link #getContextMenuInfo()}. */ 2806 | ContextMenuInfo createContextMenuInfo(View view, int position, long id) { 2807 | return new AdapterContextMenuInfo(view, position, id); 2808 | } 2809 | 2810 | /** Extra menu information provided to the 2811 | * {@link android.view.View.OnCreateContextMenuListener#onCreateContextMenu(ContextMenu, View, ContextMenuInfo) } 2812 | * callback when a context menu is brought up for this AdapterView. */ 2813 | public static class AdapterContextMenuInfo implements 2814 | ContextMenu.ContextMenuInfo { 2815 | 2816 | public AdapterContextMenuInfo(View targetView, int position, long id) { 2817 | this.targetView = targetView; 2818 | this.position = position; 2819 | this.id = id; 2820 | } 2821 | 2822 | /** The child view for which the context menu is being displayed. This 2823 | * will be one of the children of this AdapterView. */ 2824 | public View targetView; 2825 | 2826 | /** The position in the adapter for which the context menu is being 2827 | * displayed. */ 2828 | public int position; 2829 | 2830 | /** The row id of the item for which the context menu is being displayed. */ 2831 | public long id; 2832 | } 2833 | 2834 | /** Returns the selector {@link android.graphics.drawable.Drawable} that is 2835 | * used to draw the selection in the list. 2836 | * 2837 | * @return the drawable used to display the selector */ 2838 | public Drawable getSelector() { 2839 | return mSelector; 2840 | } 2841 | 2842 | /** Set a Drawable that should be used to highlight the currently selected 2843 | * item. 2844 | * 2845 | * @param resID 2846 | * A Drawable resource to use as the selection highlight. 2847 | * 2848 | * @attr ref android.R.styleable#AbsListView_listSelector */ 2849 | public void setSelector(int resID) { 2850 | setSelector(getResources().getDrawable(resID)); 2851 | } 2852 | 2853 | @Override 2854 | public boolean verifyDrawable(Drawable dr) { 2855 | return mSelector == dr || super.verifyDrawable(dr); 2856 | } 2857 | 2858 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 2859 | @Override 2860 | public void jumpDrawablesToCurrentState() { 2861 | super.jumpDrawablesToCurrentState(); 2862 | if (mSelector != null) 2863 | mSelector.jumpToCurrentState(); 2864 | } 2865 | 2866 | public void setSelector(Drawable sel) { 2867 | if (mSelector != null) { 2868 | mSelector.setCallback(null); 2869 | unscheduleDrawable(mSelector); 2870 | } 2871 | 2872 | mSelector = sel; 2873 | 2874 | if (mSelector == null) { 2875 | return; 2876 | } 2877 | 2878 | Rect padding = new Rect(); 2879 | sel.getPadding(padding); 2880 | mSelectionLeftPadding = padding.left; 2881 | mSelectionTopPadding = padding.top; 2882 | mSelectionRightPadding = padding.right; 2883 | mSelectionBottomPadding = padding.bottom; 2884 | sel.setCallback(this); 2885 | updateSelectorState(); 2886 | } 2887 | 2888 | void updateSelectorState() { 2889 | if (mSelector != null) { 2890 | if (shouldShowSelector()) { 2891 | mSelector.setState(getDrawableState()); 2892 | } else { 2893 | mSelector.setState(new int[] { 0 }); 2894 | } 2895 | } 2896 | } 2897 | 2898 | @Override 2899 | protected void drawableStateChanged() { 2900 | super.drawableStateChanged(); 2901 | updateSelectorState(); 2902 | } 2903 | 2904 | /** Indicates whether this view is in a state where the selector should be 2905 | * drawn. This will happen if we have focus but are not in touch mode, or we 2906 | * are in the middle of displaying the pressed state for an item. 2907 | * 2908 | * @return True if the selector should be shown */ 2909 | boolean shouldShowSelector() { 2910 | return ((hasFocus() && !isInTouchMode()) || touchModeDrawsInPressedState()) 2911 | && (mBeginClick); 2912 | } 2913 | 2914 | /** @return True if the current touch mode requires that we draw the selector 2915 | * in the pressed state. */ 2916 | boolean touchModeDrawsInPressedState() { 2917 | // FIXME use isPressed for this 2918 | switch (mTouchMode) { 2919 | case TOUCH_MODE_TAP: 2920 | case TOUCH_MODE_DONE_WAITING: 2921 | return true; 2922 | default: 2923 | return false; 2924 | } 2925 | } 2926 | 2927 | /** Register a callback to be invoked when an item in this AdapterView has 2928 | * been clicked. 2929 | * 2930 | * @param listener 2931 | * The callback that will be invoked. */ 2932 | public void setOnItemClickListener(OnItemClickListener listener) { 2933 | mOnItemClickListener = listener; 2934 | } 2935 | 2936 | /** @return The callback to be invoked with an item in this AdapterView has 2937 | * been clicked, or null id no callback has been set. */ 2938 | public final OnItemClickListener getOnItemClickListener() { 2939 | return mOnItemClickListener; 2940 | } 2941 | 2942 | public interface OnItemClickListener { 2943 | 2944 | /** Callback method to be invoked when an item in this AdapterView has 2945 | * been clicked. 2946 | *

2947 | * Implementers can call getItemAtPosition(position) if they need to 2948 | * access the data associated with the selected item. 2949 | * 2950 | * @param parent 2951 | * The AdapterView where the click happened. 2952 | * @param view 2953 | * The view within the AdapterView that was clicked (this 2954 | * will be a view provided by the adapter) 2955 | * @param position 2956 | * The position of the view in the adapter. 2957 | * @param id 2958 | * The row id of the item that was clicked. */ 2959 | void onItemClick(StaggeredGridView parent, View view, int position, 2960 | long id); 2961 | } 2962 | 2963 | /** Register a callback to be invoked when an item in this AdapterView has 2964 | * been clicked and held 2965 | * 2966 | * @param listener 2967 | * The callback that will run */ 2968 | public void setOnItemLongClickListener(OnItemLongClickListener listener) { 2969 | if (!isLongClickable()) { 2970 | setLongClickable(true); 2971 | } 2972 | mOnItemLongClickListener = listener; 2973 | } 2974 | 2975 | /** @return The callback to be invoked with an item in this AdapterView has 2976 | * been clicked and held, or null id no callback as been set. */ 2977 | public final OnItemLongClickListener getOnItemLongClickListener() { 2978 | return mOnItemLongClickListener; 2979 | } 2980 | 2981 | public interface OnItemLongClickListener { 2982 | /** Callback method to be invoked when an item in this view has been 2983 | * clicked and held. 2984 | * 2985 | * Implementers can call getItemAtPosition(position) if they need to 2986 | * access the data associated with the selected item. 2987 | * 2988 | * @param parent 2989 | * The AbsListView where the click happened 2990 | * @param view 2991 | * The view within the AbsListView that was clicked 2992 | * @param position 2993 | * The position of the view in the list 2994 | * @param id 2995 | * The row id of the item that was clicked 2996 | * 2997 | * @return true if the callback consumed the long click, false otherwise */ 2998 | boolean onItemLongClick(StaggeredGridView parent, View view, 2999 | int position, long id); 3000 | } 3001 | 3002 | /** Maps a point to a position in the list. 3003 | * 3004 | * @param x 3005 | * X in local coordinate 3006 | * @param y 3007 | * Y in local coordinate 3008 | * @return The position of the item which contains the specified point, or 3009 | * {@link #INVALID_POSITION} if the point does not intersect an 3010 | * item. */ 3011 | public int pointToPosition(int x, int y) { 3012 | Rect frame = mTouchFrame; 3013 | if (frame == null) { 3014 | mTouchFrame = new Rect(); 3015 | frame = mTouchFrame; 3016 | } 3017 | 3018 | final int count = getChildCount(); 3019 | for (int i = count - 1; i >= 0; i--) { 3020 | final View child = getChildAt(i); 3021 | if (child.getVisibility() == View.VISIBLE) { 3022 | child.getHitRect(frame); 3023 | if (frame.contains(x, y)) { 3024 | return mFirstPosition + i; 3025 | } 3026 | } 3027 | } 3028 | return INVALID_POSITION; 3029 | } 3030 | 3031 | public boolean isDrawSelectorOnTop() { 3032 | return mDrawSelectorOnTop; 3033 | } 3034 | 3035 | public void setDrawSelectorOnTop(boolean mDrawSelectorOnTop) { 3036 | this.mDrawSelectorOnTop = mDrawSelectorOnTop; 3037 | } 3038 | 3039 | } --------------------------------------------------------------------------------