commit 60d4aa0b205bd74593645fa9c45b4f67b836fb45 Author: Zhanty Date: Tue Jul 2 15:48:57 2019 +0800 Initial commit diff --git a/lib_base/.gitignore b/lib_base/.gitignore new file mode 100644 index 0000000..c06fb85 --- /dev/null +++ b/lib_base/.gitignore @@ -0,0 +1,22 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +*.iml +.idea/ +.gradle +/local.properties +.DS_Store +/build +/captures +*.apk +*.ap_ +*.dex +*.class +bin/ +gen/ +local.properties \ No newline at end of file diff --git a/lib_base/README.md b/lib_base/README.md new file mode 100644 index 0000000..527c57e --- /dev/null +++ b/lib_base/README.md @@ -0,0 +1,20 @@ +# Android Development Base Library + +## 1 Third party Libraries + +- [AndroidX](https://developer.android.com/jetpack/androidx) +- [RxJava(2)](https://github.com/ReactiveX/RxJava) +- [RxAndroid(2)](https://github.com/ReactiveX/RxAndroid) +- [AutoDispose](https://github.com/uber/AutoDispose) +- [Dagger2](https://github.com/google/dagger) +- [Glide](https://github.com/bumptech/glide) +- [OkHttp](https://github.com/square/okhttp) +- [Timber](https://github.com/JakeWharton/timber) +- [WrapperAdapter](https://github.com/Ztiany/WrapperAdapter) +- [MultiTypeAdapter](https://github.com/drakeet/MultiType) +- [AndroidUtilCode](https://github.com/Blankj/AndroidUtilCode) + +## 2 Environment + +- AndroidStudio 3+ +- Java8 with desugar \ No newline at end of file diff --git a/lib_base/build.gradle b/lib_base/build.gradle new file mode 100644 index 0000000..f3326c2 --- /dev/null +++ b/lib_base/build.gradle @@ -0,0 +1,130 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' + +androidExtensions { + experimental = true +} + +android { + compileSdkVersion rootProject.compileSdkVersion + buildToolsVersion rootProject.buildToolsVersion + + defaultConfig { + + minSdkVersion rootProject.minSdkVersion + targetSdkVersion rootProject.targetSdkVersion + versionCode 1 + versionName "1.0" + + javaCompileOptions { + annotationProcessorOptions { + includeCompileClasspath false + } + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + lintOptions { + abortOnError false + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + java.srcDirs += "src/github/java" + res.srcDirs += "src/github/res" + } + } + + project.afterEvaluate { + Task assembleReleaseTask = project.tasks.findByPath("assembleRelease") + if (assembleReleaseTask != null) { + assembleReleaseTask.doLast { + File infile = project.file("build/outputs/aar/") + File outfile = project.file("./release") + project.copy { + from infile + into outfile + } + } + } + } + +} + + +dependencies { + api fileTree(dir: 'libs', include: ['*.jar']) + + //测试 + testImplementation testLibraries.junit + + //AndroidSupport + api androidLibraries.androidCompatV4 + api androidLibraries.androidCompatV7 + api androidLibraries.androidRecyclerView + api androidLibraries.androidDesign + api androidLibraries.androidPrecent + api androidLibraries.constraintLayout + + //AAC + api androidLibraries.lifecycle + api androidLibraries.lifecycleJava8 + api androidLibraries.lifecycleExtensions + api androidLibraries.liveDataReactiveStreams + + //Kotlin + api kotlinLibraries.kotlinStdlib + api kotlinLibraries.kotlinReflect + api kotlinLibraries.kotlinCoroutines + api kotlinLibraries.kotlinAndroidCoroutines + + //RxJava + api thirdLibraries.rxJava + api thirdLibraries.rxAndroid + api thirdLibraries.rxBinding + api thirdLibraries.autoDispose + api thirdLibraries.autoDisposeLifecycleArchcomponents + + /*Dagger2*/ + api thirdLibraries.dagger2 + api thirdLibraries.jsr305 + api thirdLibraries.dagger2Android + api thirdLibraries.dagger2AndroidSupport + kapt thirdLibraries.dagger2Apt + kapt thirdLibraries.dagger2AndroidApt + + //LoadMore + api uiLibraries.wrapperAdapter + + //Adapter + api uiLibraries.multiType + api uiLibraries.multiTypeKotlin + + //Log + api thirdLibraries.timber + + //ImageLoader + api thirdLibraries.glide + api thirdLibraries.glideOkHttp + api thirdLibraries.okHttp + + //Utils + api thirdLibraries.utilcode + api thirdLibraries.jOOR + api thirdLibraries.andPermission + api thirdLibraries.supportOptional + +} diff --git a/lib_base/proguard-rules.pro b/lib_base/proguard-rules.pro new file mode 100644 index 0000000..070a2ee --- /dev/null +++ b/lib_base/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in E:\DeveloperTools\sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/lib_base/src/main/AndroidManifest.xml b/lib_base/src/main/AndroidManifest.xml new file mode 100644 index 0000000..7cde085 --- /dev/null +++ b/lib_base/src/main/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/lib_base/src/main/java/com/android/base/adapter/DataManager.java b/lib_base/src/main/java/com/android/base/adapter/DataManager.java new file mode 100644 index 0000000..f92f4ad --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/DataManager.java @@ -0,0 +1,81 @@ +package com.android.base.adapter; + +import java.util.List; + +/** + *
+ *     注意数据源引用的替换,只有setDataSource方法会把elements替换掉之前的数据源引用,其他方法都是基于现有数据集合做删除与添加操作。
+ * 
+ * + * @author Ztiany + * Date : 2016-09-12 11:33 + * Email: 1169654504@qq.com + */ +public interface DataManager { + + //Add op + void add(T element); + + void addAt(int location, T element); + + void addItems(List elements); + + /** + * 添加元素前会使用equals方法进行比较,荣旧的数据集合中删除相同的数据在添加 + * + * @param elements 元素 + */ + void addItemsChecked(List elements); + + void addItemsAt(int location, List elements); + + //update op + void replace(T oldElement, T newElement); + + void replaceAt(int index, T element); + + /** + * 清除之前集合中的数据,然后把elements添加到之前的集合中,不会使用elements作为数据源 + * + * @param elements 元素 + */ + void replaceAll(List elements); + + /** + * 此方法会使用elements替换掉之前的数据源,而不对之前的数据源做任何操作 + * + * @param newDataSource 新的数据集 + * @param notifyDataSetChanged 是否调用adapter的notifyDataSetChanged方法 + */ + void setDataSource(List newDataSource, boolean notifyDataSetChanged); + + //remove opt + void remove(T element); + + void removeAt(int index); + + void removeItems(List elements); + + //get + T getItem(int position); + + List getItems(); + + int getDataSize(); + + //contains + boolean contains(T element); + + boolean isEmpty(); + + //clear opt + void clear(); + + //Utils + + /** + * @param t element + * @return -1 if not contains this element + */ + int getItemPosition(T t); +} diff --git a/lib_base/src/main/java/com/android/base/adapter/ItemHelper.java b/lib_base/src/main/java/com/android/base/adapter/ItemHelper.java new file mode 100644 index 0000000..57f9d6c --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/ItemHelper.java @@ -0,0 +1,72 @@ +package com.android.base.adapter; + +import android.support.annotation.IdRes; +import android.support.annotation.NonNull; +import android.support.annotation.StringRes; +import android.util.SparseArray; +import android.view.View; +import android.widget.TextView; + + +/** + * @author Ztiany + * Email :1169654504@qq.com + * Date :015-12-29 20:47 + */ +public class ItemHelper { + + private View mItemView; + private SparseArray views; + + public ItemHelper(View itemView) { + mItemView = itemView; + views = new SparseArray<>(); + } + + @SuppressWarnings("unchecked") + public T getView(int viewId) { + View view = views.get(viewId); + if (view == null) { + view = mItemView.findViewById(viewId); + views.put(viewId, view); + } + return (T) view; + } + + public ItemHelper setText(CharSequence str, @IdRes int viewId) { + ((TextView) getView(viewId)).setText(str == null ? "" : str); + return this; + } + + public ItemHelper setText(@StringRes int strId, @IdRes int viewId) { + ((TextView) getView(viewId)).setText(strId); + return this; + } + + public ItemHelper setTag(@NonNull Object object, @IdRes int tagId, @IdRes int viewID) { + View view = getView(viewID); + view.setTag(tagId, object); + return this; + } + + public ItemHelper setTag(@NonNull Object object, @IdRes int viewID) { + View view = getView(viewID); + view.setTag(object); + return this; + } + + public T getTag(@IdRes int tagId, @IdRes int viewID) { + View view = getView(viewID); + @SuppressWarnings("unchecked") + T tag = (T) view.getTag(tagId); + return tag; + } + + public T getTag(@IdRes int viewID) { + View view = getView(viewID); + @SuppressWarnings("unchecked") + T tag = (T) view.getTag(); + return tag; + } + +} diff --git a/lib_base/src/main/java/com/android/base/adapter/list/BaseListAdapter.java b/lib_base/src/main/java/com/android/base/adapter/list/BaseListAdapter.java new file mode 100644 index 0000000..ba792db --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/list/BaseListAdapter.java @@ -0,0 +1,178 @@ +package com.android.base.adapter.list; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; + +import com.android.base.R; +import com.android.base.adapter.DataManager; + +import java.util.List; + +/** + * absListView通用的Adapter,注意:只有setDataSource才能替换原有数据源的引用。 + * + * @param 数据模型 + * @author Ztiany + */ +@SuppressWarnings("unused") +public abstract class BaseListAdapter extends BaseAdapter implements DataManager { + + protected final Context mContext; + private final static int ITEM_ID = R.id.base_item_tag_view_id; + private DataManager mDataManager; + private final LayoutInflater mLayoutInflater; + + public BaseListAdapter(@NonNull Context context) { + this(context, null); + } + + @SuppressWarnings("all") + public BaseListAdapter(Context context, List data) { + this.mContext = context; + mLayoutInflater = LayoutInflater.from(context); + mDataManager = new ListDataManagerImpl<>(data, this); + } + + @Override + public int getCount() { + return getDataSize(); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + @SuppressWarnings("unchecked") + public View getView(int position, View convertView, ViewGroup parent) { + VH viewHolder; + int type = getItemViewType(position); + if (convertView == null) { + viewHolder = onCreateViewHolder(mLayoutInflater, parent, type); + viewHolder.mItemView.setTag(ITEM_ID, viewHolder); + } else { + viewHolder = (VH) convertView.getTag(ITEM_ID); + } + viewHolder.setPosition(position); + viewHolder.setType(type); + T item = getItem(position); + onBindData(viewHolder, item); + return viewHolder.mItemView; + } + + @SuppressWarnings("all") + protected abstract void onBindData(@NonNull VH viewHolder, T item); + + @NonNull + protected abstract VH onCreateViewHolder(@NonNull LayoutInflater layoutInflater, @NonNull ViewGroup parent, int type); + + @Override + public int getItemViewType(int position) { + return super.getItemViewType(position); + } + + @Override + public int getViewTypeCount() { + return super.getViewTypeCount(); + } + + + /////////////////////////////////////////////////////////////////////////// + // DataManager + /////////////////////////////////////////////////////////////////////////// + + @Override + public void add(T elem) { + mDataManager.add(elem); + } + + @Override + public void addAt(int location, T elem) { + mDataManager.addAt(location, elem); + } + + @Override + public void addItems(List elements) { + mDataManager.addItems(elements); + } + + @Override + public void addItemsChecked(List elements) { + mDataManager.addItemsChecked(elements); + } + + @Override + public void addItemsAt(int location, List elements) { + mDataManager.addItemsAt(location, elements); + } + + @Override + public void replace(T oldElem, T newElem) { + mDataManager.replace(oldElem, newElem); + } + + @Override + public void replaceAt(int index, T elem) { + mDataManager.replaceAt(index, elem); + } + + @Override + public void replaceAll(List elements) { + mDataManager.replaceAll(elements); + } + + @Override + public void remove(T elem) { + mDataManager.remove(elem); + } + + @Override + public void removeItems(List elements) { + mDataManager.removeItems(elements); + } + + @Override + public void removeAt(int index) { + mDataManager.removeAt(index); + } + + @Override + public T getItem(int position) { + return mDataManager.getItem(position); + } + + @Override + public final int getDataSize() { + return mDataManager.getDataSize(); + } + + @Override + public boolean contains(T elem) { + return mDataManager.contains(elem); + } + + @Override + public void setDataSource(List elements, boolean notifyDataSetChanged) { + mDataManager.setDataSource(elements, notifyDataSetChanged); + } + + @Override + public int getItemPosition(T t) { + return mDataManager.getItemPosition(t); + } + + @Override + public void clear() { + mDataManager.clear(); + } + + @Override + public List getItems() { + return mDataManager.getItems(); + } +} diff --git a/lib_base/src/main/java/com/android/base/adapter/list/ListDataManagerImpl.java b/lib_base/src/main/java/com/android/base/adapter/list/ListDataManagerImpl.java new file mode 100644 index 0000000..c6efa6a --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/list/ListDataManagerImpl.java @@ -0,0 +1,183 @@ +package com.android.base.adapter.list; + +import android.widget.BaseAdapter; + +import com.android.base.adapter.DataManager; +import com.android.base.utils.common.Checker; + +import java.util.ArrayList; +import java.util.List; + + +final class ListDataManagerImpl implements DataManager { + + private List mData; + private BaseAdapter mBaseAdapter; + + ListDataManagerImpl(List tList, BaseAdapter adapter) { + mData = tList; + mBaseAdapter = adapter; + } + + private void checkData() { + if (mData == null) { + mData = new ArrayList<>(); + } + } + + @Override + public void add(T elem) { + checkData(); + mData.add(elem); + notifyDataSetChanged(); + } + + @Override + public void addAt(int location, T elem) { + checkData(); + mData.add(location, elem); + notifyDataSetChanged(); + } + + @Override + public void addItems(List elements) { + checkData(); + mData.addAll(elements); + notifyDataSetChanged(); + } + + @Override + public void addItemsChecked(List elements) { + if (Checker.isEmpty(elements)) { + return; + } + if (mData == null) { + addItems(elements); + return; + } + for (T element : elements) { + if (element == null) { + continue; + } + mData.remove(element); + } + mData.addAll(elements); + notifyDataSetChanged(); + } + + @Override + public void addItemsAt(int location, List elements) { + checkData(); + mData.addAll(location, elements); + notifyDataSetChanged(); + } + + @Override + public void replace(T oldElem, T newElem) { + if (mData != null && mData.contains(oldElem)) { + replaceAt(mData.indexOf(oldElem), newElem); + } + } + + @Override + public void replaceAt(int index, T elem) { + if (mData != null && mData.size() > index) { + mData.set(index, elem); + notifyDataSetChanged(); + } + } + + @Override + public void replaceAll(List elements) { + if (mData == null) { + mData = new ArrayList<>(); + } + mData.clear(); + if (elements != null) { + mData.addAll(elements); + } + notifyDataSetChanged(); + } + + @Override + public void setDataSource(List newDataSource, boolean notifyDataSetChanged) { + mData = newDataSource; + if (notifyDataSetChanged) { + notifyDataSetChanged(); + } + } + + @Override + public void remove(T elem) { + if (mData != null && mData.contains(elem)) { + mData.remove(elem); + notifyDataSetChanged(); + } + } + + @Override + public void removeItems(List elements) { + if (mData != null && mData.containsAll(elements)) { + mData.removeAll(elements); + notifyDataSetChanged(); + } + } + + @Override + public void removeAt(int index) { + if (mData != null && mData.size() > index) { + mData.remove(index); + notifyDataSetChanged(); + } + } + + @Override + public T getItem(int position) { + if (mData != null && mData.size() > position) { + return mData.get(position); + } + return null; + } + + @Override + public final int getDataSize() { + return mData == null ? 0 : mData.size(); + } + + @Override + public boolean contains(T elem) { + return mData != null && mData.contains(elem); + } + + @Override + public boolean isEmpty() { + return mData == null || mData.size() == 0; + } + + @Override + public void clear() { + if (mData != null && !mData.isEmpty()) { + mData.clear(); + notifyDataSetChanged(); + } + } + + @Override + public int getItemPosition(T t) { + List items = getItems(); + if (items == null) { + return -1; + } + return items.indexOf(t); + } + + @Override + public List getItems() { + return mData; + } + + private void notifyDataSetChanged() { + mBaseAdapter.notifyDataSetChanged(); + } + +} diff --git a/lib_base/src/main/java/com/android/base/adapter/list/SmartViewHolder.java b/lib_base/src/main/java/com/android/base/adapter/list/SmartViewHolder.java new file mode 100644 index 0000000..71c0123 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/list/SmartViewHolder.java @@ -0,0 +1,21 @@ +package com.android.base.adapter.list; + +import android.view.View; + +import com.android.base.adapter.ItemHelper; + +@SuppressWarnings("unused") +public class SmartViewHolder extends ViewHolder { + + protected final ItemHelper mHelper; + + public SmartViewHolder(View itemView) { + super(itemView); + mHelper = new ItemHelper(itemView); + } + + public ItemHelper helper() { + return mHelper; + } + +} diff --git a/lib_base/src/main/java/com/android/base/adapter/list/ViewHolder.java b/lib_base/src/main/java/com/android/base/adapter/list/ViewHolder.java new file mode 100644 index 0000000..ba9fc47 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/list/ViewHolder.java @@ -0,0 +1,35 @@ +package com.android.base.adapter.list; + +import android.view.View; + +public class ViewHolder { + + @SuppressWarnings("all") + protected final View mItemView; + private int mPosition; + private int mType; + + public ViewHolder(View itemView) { + mItemView = itemView; + } + + public int getPosition() { + return mPosition; + } + + void setPosition(int position) { + mPosition = position; + } + + public int getType() { + return mType; + } + + void setType(int type) { + mType = type; + } + + public View getItemView() { + return mItemView; + } +} diff --git a/lib_base/src/main/java/com/android/base/adapter/pager/SimpleViewPagerAdapter.kt b/lib_base/src/main/java/com/android/base/adapter/pager/SimpleViewPagerAdapter.kt new file mode 100644 index 0000000..6794c17 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/pager/SimpleViewPagerAdapter.kt @@ -0,0 +1,33 @@ +package com.android.base.adapter.pager + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import kotlinx.android.extensions.LayoutContainer + +/** + * 如果使用可缩放的 View 作为 pager,可能不适合使用此Adapter + * + * @param 数据 + * @param View Holder类型 + */ +abstract class SimpleViewPagerAdapter(data: List) : ViewPagerAdapter(data) { + + override fun createViewHolder(container: ViewGroup): KtViewHolder { + val layout = provideLayout(container) + val itemView = if (layout is Int) { + LayoutInflater.from(container.context).inflate(layout, container, false) + } else { + layout as View + } + return KtViewHolder(itemView) + } + + /**provide a layout id or a View*/ + abstract fun provideLayout(parent: ViewGroup): Any + +} + +class KtViewHolder(itemView: View) : ViewPagerAdapter.ViewHolder(itemView), LayoutContainer { + override val containerView: View = itemView +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/adapter/pager/ViewPageFragmentAdapter.java b/lib_base/src/main/java/com/android/base/adapter/pager/ViewPageFragmentAdapter.java new file mode 100644 index 0000000..5204619 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/pager/ViewPageFragmentAdapter.java @@ -0,0 +1,47 @@ +package com.android.base.adapter.pager; + +import android.content.Context; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentPagerAdapter; + +import java.util.ArrayList; +import java.util.List; + +@SuppressWarnings("unused") +public class ViewPageFragmentAdapter extends FragmentPagerAdapter { + + private final List mTabs; + private Context mContext; + + public ViewPageFragmentAdapter(FragmentManager fragmentManager, Context context) { + super(fragmentManager); + mContext = context; + mTabs = new ArrayList<>(); + } + + public void setDataSource(List viewPageInfoList) { + mTabs.clear(); + mTabs.addAll(viewPageInfoList); + } + + @Override + public int getCount() { + return mTabs.size(); + } + + @Override + public Fragment getItem(int position) { + ViewPageInfo viewPageInfo = mTabs.get(position); + return Fragment.instantiate(mContext, viewPageInfo.clazz.getName(), viewPageInfo.args); + } + + @Override + public CharSequence getPageTitle(int position) { + return mTabs.get(position).title; + } + + public List getTabs() { + return mTabs; + } +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/adapter/pager/ViewPageInfo.java b/lib_base/src/main/java/com/android/base/adapter/pager/ViewPageInfo.java new file mode 100644 index 0000000..9cac811 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/pager/ViewPageInfo.java @@ -0,0 +1,17 @@ +package com.android.base.adapter.pager; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +@SuppressWarnings("all") +public class ViewPageInfo { + + public final Class clazz; + public final Bundle args; + public final String title; + + public ViewPageInfo(String title, Class clazz, Bundle args) { + this.title = title; + this.clazz = clazz; + this.args = args; + } +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/adapter/pager/ViewPageStateFragmentAdapter.java b/lib_base/src/main/java/com/android/base/adapter/pager/ViewPageStateFragmentAdapter.java new file mode 100644 index 0000000..a6a26cb --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/pager/ViewPageStateFragmentAdapter.java @@ -0,0 +1,48 @@ +package com.android.base.adapter.pager; + +import android.content.Context; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentStatePagerAdapter; + +import java.util.ArrayList; +import java.util.List; + +public class ViewPageStateFragmentAdapter extends FragmentStatePagerAdapter { + + private final List mTabs; + private Context mContext; + + public ViewPageStateFragmentAdapter(FragmentManager fragmentManager, Context context) { + super(fragmentManager); + mContext = context; + mTabs = new ArrayList<>(); + } + + public void setDataSource(List viewPageInfoList) { + mTabs.clear(); + mTabs.addAll(viewPageInfoList); + } + + @Override + public int getCount() { + return mTabs.size(); + } + + @Override + public Fragment getItem(int position) { + ViewPageInfo viewPageInfo = mTabs.get(position); + return Fragment.instantiate(mContext, viewPageInfo.clazz.getName(), viewPageInfo.args); + } + + @Override + public CharSequence getPageTitle(int position) { + return mTabs.get(position).title; + } + + protected List getTabs() { + return mTabs; + } + + +} diff --git a/lib_base/src/main/java/com/android/base/adapter/pager/ViewPagerAdapter.java b/lib_base/src/main/java/com/android/base/adapter/pager/ViewPagerAdapter.java new file mode 100644 index 0000000..0719894 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/pager/ViewPagerAdapter.java @@ -0,0 +1,69 @@ +package com.android.base.adapter.pager; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.View; +import android.view.ViewGroup; + +import com.android.base.R; +import com.android.base.adapter.pager.recycler.RecyclingPagerAdapter; +import com.android.base.utils.common.Checker; + +import java.util.List; + +/** + * 如果使用可缩放的 View 作为 pager,可能不适合使用此Adapter + * + * @param 数据 + * @param View Holder类型 + */ +public abstract class ViewPagerAdapter extends RecyclingPagerAdapter { + + private List mData; + + public ViewPagerAdapter(@Nullable List data) { + mData = data; + } + + @Override + @SuppressWarnings("unchecked") + public View getView(int position, View convertView, ViewGroup container) { + VH viewHolder; + if (convertView == null) { + viewHolder = createViewHolder(container); + } else { + viewHolder = (VH) convertView.getTag(R.id.base_item_tag_view_id); + } + T item = getItem(position); + onBindData(viewHolder, item); + return viewHolder.itemView; + } + + protected abstract VH createViewHolder(@NonNull ViewGroup container); + + protected abstract void onBindData(@NonNull VH viewHolder, @NonNull T item); + + @Override + public int getCount() { + return Checker.isEmpty(mData) ? 0 : mData.size(); + } + + public T getItem(int position) { + if (position < 0 || position >= mData.size()) { + return null; + } + return mData.get(position); + } + + public static class ViewHolder { + + public View itemView; + + public ViewHolder(@NonNull View itemView) { + this.itemView = itemView; + itemView.setTag(R.id.base_item_tag_view_id, this); + } + + } + +} diff --git a/lib_base/src/main/java/com/android/base/adapter/pager/recycler/RecycleBin.java b/lib_base/src/main/java/com/android/base/adapter/pager/recycler/RecycleBin.java new file mode 100644 index 0000000..2b2d01e --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/pager/recycler/RecycleBin.java @@ -0,0 +1,152 @@ +package com.android.base.adapter.pager.recycler; + +import android.os.Build; +import android.util.SparseArray; +import android.view.View; + +/** + * The RecycleBin facilitates reuse of views across layouts. The RecycleBin has two levels of + * storage: ActiveViews and ScrapViews. ActiveViews are those views which were onscreen at the + * start of a layout. By construction, they are displaying current information. At the end of + * layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews are old views that + * could potentially be used by the adapter to avoid allocating views unnecessarily. + *

+ * This class was taken from Android's implementation of {@link android.widget.AbsListView} which + * is copyrighted 2006 The Android Open Source Project. + */ +public class RecycleBin { + /** + * Views that were on screen at the start of layout. This array is populated at the start of + * layout, and at the end of layout all view in activeViews are moved to scrapViews. + * Views in activeViews represent a contiguous range of Views, with position of the first + * view store in mFirstActivePosition. + */ + private View[] activeViews = new View[0]; + private int[] activeViewTypes = new int[0]; + + /** Unsorted views that can be used by the adapter as a convert view. */ + private SparseArray[] scrapViews; + + private int viewTypeCount; + + private SparseArray currentScrapViews; + + public void setViewTypeCount(int viewTypeCount) { + if (viewTypeCount < 1) { + throw new IllegalArgumentException("Can't have a viewTypeCount < 1"); + } + //noinspection unchecked + SparseArray[] scrapViews = new SparseArray[viewTypeCount]; + for (int i = 0; i < viewTypeCount; i++) { + scrapViews[i] = new SparseArray(); + } + this.viewTypeCount = viewTypeCount; + currentScrapViews = scrapViews[0]; + this.scrapViews = scrapViews; + } + + protected boolean shouldRecycleViewType(int viewType) { + return viewType >= 0; + } + + /** @return A view from the ScrapViews collection. These are unordered. */ + View getScrapView(int position, int viewType) { + if (viewTypeCount == 1) { + return retrieveFromScrap(currentScrapViews, position); + } else if (viewType >= 0 && viewType < scrapViews.length) { + return retrieveFromScrap(scrapViews[viewType], position); + } + return null; + } + + /** + * Put a view into the ScrapViews list. These views are unordered. + * + * @param scrap The view to add + */ + void addScrapView(View scrap, int position, int viewType) { + if (viewTypeCount == 1) { + currentScrapViews.put(position, scrap); + } else { + scrapViews[viewType].put(position, scrap); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + scrap.setAccessibilityDelegate(null); + } + } + + /** Move all views remaining in activeViews to scrapViews. */ + void scrapActiveViews() { + final View[] activeViews = this.activeViews; + final int[] activeViewTypes = this.activeViewTypes; + final boolean multipleScraps = viewTypeCount > 1; + + SparseArray scrapViews = currentScrapViews; + final int count = activeViews.length; + for (int i = count - 1; i >= 0; i--) { + final View victim = activeViews[i]; + if (victim != null) { + int whichScrap = activeViewTypes[i]; + + activeViews[i] = null; + activeViewTypes[i] = -1; + + if (!shouldRecycleViewType(whichScrap)) { + continue; + } + + if (multipleScraps) { + scrapViews = this.scrapViews[whichScrap]; + } + scrapViews.put(i, victim); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + victim.setAccessibilityDelegate(null); + } + } + } + + pruneScrapViews(); + } + + /** + * Makes sure that the size of scrapViews does not exceed the size of activeViews. + * (This can happen if an adapter does not recycle its views). + */ + private void pruneScrapViews() { + final int maxViews = activeViews.length; + final int viewTypeCount = this.viewTypeCount; + final SparseArray[] scrapViews = this.scrapViews; + for (int i = 0; i < viewTypeCount; ++i) { + final SparseArray scrapPile = scrapViews[i]; + int size = scrapPile.size(); + final int extras = size - maxViews; + size--; + for (int j = 0; j < extras; j++) { + scrapPile.remove(scrapPile.keyAt(size--)); + } + } + } + + static View retrieveFromScrap(SparseArray scrapViews, int position) { + int size = scrapViews.size(); + if (size > 0) { + // See if we still have a view for this position. + for (int i = 0; i < size; i++) { + int fromPosition = scrapViews.keyAt(i); + View view = scrapViews.get(fromPosition); + if (fromPosition == position) { + scrapViews.remove(fromPosition); + return view; + } + } + int index = size - 1; + View r = scrapViews.valueAt(index); + scrapViews.remove(scrapViews.keyAt(index)); + return r; + } else { + return null; + } + } +} diff --git a/lib_base/src/main/java/com/android/base/adapter/pager/recycler/RecyclingPagerAdapter.java b/lib_base/src/main/java/com/android/base/adapter/pager/recycler/RecyclingPagerAdapter.java new file mode 100644 index 0000000..56f2383 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/pager/recycler/RecyclingPagerAdapter.java @@ -0,0 +1,114 @@ +package com.android.base.adapter.pager.recycler; + +import android.support.v4.view.PagerAdapter; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; + +/** + * A {@link PagerAdapter} which behaves like an {@link android.widget.Adapter} with view types and view recycling. + * + * @see JakeWharton/salvage + */ +public abstract class RecyclingPagerAdapter extends PagerAdapter { + + static final int IGNORE_ITEM_VIEW_TYPE = AdapterView.ITEM_VIEW_TYPE_IGNORE; + + private final RecycleBin recycleBin; + + public RecyclingPagerAdapter() { + this(new RecycleBin()); + } + + RecyclingPagerAdapter(RecycleBin recycleBin) { + this.recycleBin = recycleBin; + recycleBin.setViewTypeCount(getViewTypeCount()); + } + + @Override + public void notifyDataSetChanged() { + recycleBin.scrapActiveViews(); + super.notifyDataSetChanged(); + } + + @Override + public final Object instantiateItem(ViewGroup container, int position) { + int viewType = getItemViewType(position); + View view = null; + if (viewType != IGNORE_ITEM_VIEW_TYPE) { + view = recycleBin.getScrapView(position, viewType); + } + view = getView(position, view, container); + container.addView(view); + return view; + } + + @Override + public final void destroyItem(ViewGroup container, int position, Object object) { + View view = (View) object; + container.removeView(view); + int viewType = getItemViewType(position); + if (viewType != IGNORE_ITEM_VIEW_TYPE) { + recycleBin.addScrapView(view, position, viewType); + } + } + + @Override + public final boolean isViewFromObject(View view, Object object) { + return view == object; + } + + /** + *

+ * Returns the number of types of Views that will be created by + * {@link #getView}. Each type represents a set of views that can be + * converted in {@link #getView}. If the adapter always returns the same + * type of View for all items, this method should return 1. + *

+ *

+ * This method will only be called when when the adapter is set on the + * the {@link AdapterView}. + *

+ * + * @return The number of types of Views that will be created by this adapter + */ + public int getViewTypeCount() { + return 1; + } + + /** + * Get the type of View that will be created by {@link #getView} for the specified item. + * + * @param position The position of the item within the adapter's data set whose view type we + * want. + * @return An integer representing the type of View. Two views should share the same type if one + * can be converted to the other in {@link #getView}. Note: Integers must be in the + * range 0 to {@link #getViewTypeCount} - 1. {@link #IGNORE_ITEM_VIEW_TYPE} can + * also be returned. + * @see #IGNORE_ITEM_VIEW_TYPE + */ + @SuppressWarnings("UnusedParameters") // Argument potentially used by subclasses. + public int getItemViewType(int position) { + return 0; + } + + /** + * Get a View that displays the data at the specified position in the data set. You can either + * create a View manually or inflate it from an XML layout file. When the View is inflated, the + * parent View (GridView, ListView...) will apply default layout parameters unless you use + * {@link android.view.LayoutInflater#inflate(int, ViewGroup, boolean)} + * to specify a root view and to prevent attachment to the root. + * + * @param position The position of the item within the adapter's data set of the item whose view + * we want. + * @param convertView The old view to reuse, if possible. Note: You should check that this view + * is non-null and of an appropriate type before using. If it is not possible to convert + * this view to display the correct data, this method can create a new view. + * Heterogeneous lists can specify their number of view types, so that this View is + * always of the right type (see {@link #getViewTypeCount()} and + * {@link #getItemViewType(int)}). + * @param parent The parent that this view will eventually be attached to + * @return A View corresponding to the data at the specified position. + */ + public abstract View getView(int position, View convertView, ViewGroup container); +} diff --git a/lib_base/src/main/java/com/android/base/adapter/recycler/AdapterDataObserverProxy.java b/lib_base/src/main/java/com/android/base/adapter/recycler/AdapterDataObserverProxy.java new file mode 100644 index 0000000..bbdbf71 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/recycler/AdapterDataObserverProxy.java @@ -0,0 +1,49 @@ +package com.android.base.adapter.recycler; + +import android.support.annotation.Nullable; +import android.support.v7.widget.RecyclerView; + +/** + * @see PagingWithHeader + */ +class AdapterDataObserverProxy extends RecyclerView.AdapterDataObserver { + + private RecyclerView.AdapterDataObserver adapterDataObserver; + private int headerCount; + + AdapterDataObserverProxy(RecyclerView.AdapterDataObserver adapterDataObserver, int headerCount) { + this.adapterDataObserver = adapterDataObserver; + this.headerCount = headerCount; + } + + @Override + public void onChanged() { + adapterDataObserver.onChanged(); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount) { + adapterDataObserver.onItemRangeChanged(positionStart + headerCount, itemCount); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) { + adapterDataObserver.onItemRangeChanged(positionStart + headerCount, itemCount, payload); + } + + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + adapterDataObserver.onItemRangeInserted(positionStart + headerCount, itemCount); + } + + @Override + public void onItemRangeRemoved(int positionStart, int itemCount) { + adapterDataObserver.onItemRangeRemoved(positionStart + headerCount, itemCount); + } + + @Override + public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { + super.onItemRangeMoved(fromPosition + headerCount, toPosition + headerCount, itemCount); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/adapter/recycler/DiffRecyclerAdapter.java b/lib_base/src/main/java/com/android/base/adapter/recycler/DiffRecyclerAdapter.java new file mode 100644 index 0000000..6054520 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/recycler/DiffRecyclerAdapter.java @@ -0,0 +1,255 @@ +package com.android.base.adapter.recycler; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.recyclerview.extensions.AsyncDifferConfig; +import android.support.v7.recyclerview.extensions.AsyncListDiffer; +import android.support.v7.util.AdapterListUpdateCallback; +import android.support.v7.util.DiffUtil; +import android.support.v7.widget.RecyclerView; +import android.view.ViewGroup; + +import com.android.base.adapter.DataManager; +import com.android.base.utils.common.Checker; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; + +/** + * RecyclerView 的适配器,注意: 只有{@link #setDataSource(List, boolean)}才能替换原有数据源的引用。 + * + * @param 当前列表使用的数据类型 + * @author Ztiany + * date : 2015-05-11 22:38 + * email: 1169654504@qq.com + */ +@SuppressWarnings("unused") +public abstract class DiffRecyclerAdapter extends RecyclerView.Adapter implements DataManager { + + @NonNull + protected Context mContext; + + private AsyncListDiffer mAsyncListDiffer; + + private final int mHeaderCount; + + @SuppressWarnings("WeakerAccess") + public DiffRecyclerAdapter(@NonNull Context context, @NonNull DiffUtil.ItemCallback itemCallback, @Nullable Executor executor, int headerCount) { + mContext = context; + mHeaderCount = headerCount; + + AsyncDifferConfig.Builder tBuilder = new AsyncDifferConfig.Builder<>(itemCallback); + if (executor != null) { + tBuilder.setBackgroundThreadExecutor(executor); + } + AsyncDifferConfig differConfig = tBuilder.build(); + + mAsyncListDiffer = new AsyncListDiffer<>(new AdapterListUpdateCallback(this), differConfig); + } + + public DiffRecyclerAdapter(@NonNull Context context, @NonNull DiffUtil.ItemCallback itemCallback, int headerCount) { + this(context, itemCallback, null, headerCount); + } + + public DiffRecyclerAdapter(@NonNull Context context, @NonNull DiffUtil.ItemCallback itemCallback) { + this(context, itemCallback, null, 0); + } + + @Override + public void registerAdapterDataObserver(@NonNull RecyclerView.AdapterDataObserver observer) { + if (mHeaderCount != 0) { + super.registerAdapterDataObserver(new AdapterDataObserverProxy(observer, mHeaderCount)); + } else { + super.registerAdapterDataObserver(observer); + } + } + + @Override + public int getItemCount() { + return getDataSize(); + } + + @NonNull + @Override + public abstract VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType); + + @Override + public void onBindViewHolder(@NonNull VH holder, int position, @NonNull List payloads) { + super.onBindViewHolder(holder, position, payloads); + } + + @Override + public abstract void onBindViewHolder(@NonNull VH viewHolder, int position); + + public void notifyEntryChanged(T t) { + int itemPosition = getItemPosition(t); + if (itemPosition != -1) { + notifyItemChanged(itemPosition); + } + } + + @Override + public void add(T element) { + List newList = newList(); + newList.add(element); + mAsyncListDiffer.submitList(newList); + } + + @Override + public void addAt(int location, T element) { + if (location > getDataSize()) { + location = getDataSize(); + } + List newList = newList(); + newList.add(location, element); + mAsyncListDiffer.submitList(newList); + } + + @Override + public void addItems(List elements) { + if (Checker.isEmpty(elements)) { + return; + } + List newList = newList(); + newList.addAll(elements); + mAsyncListDiffer.submitList(newList); + } + + @Override + public void addItemsChecked(List elements) { + if (Checker.isEmpty(elements)) { + return; + } + + List newList = newList(); + + for (T element : elements) { + if (element == null) { + continue; + } + newList.remove(element); + } + + newList.addAll(elements); + mAsyncListDiffer.submitList(newList); + } + + @Override + public void addItemsAt(int location, List elements) { + if (Checker.isEmpty(elements)) { + return; + } + List newList = newList(); + + if (location > newList.size()) { + location = newList.size(); + } + + newList.addAll(location, elements); + mAsyncListDiffer.submitList(newList); + } + + @Override + public void replace(T oldElement, T newElement) { + if (!contains(oldElement)) { + return; + } + List newList = newList(); + newList.set(newList.indexOf(oldElement), newElement); + mAsyncListDiffer.submitList(newList); + } + + @Override + public void replaceAt(int index, T element) { + if (getDataSize() > index) { + List newList = newList(); + newList.set(index, element); + mAsyncListDiffer.submitList(newList); + } + } + + @Override + public void replaceAll(List elements) { + List newList = new ArrayList<>(elements); + mAsyncListDiffer.submitList(newList); + } + + @Override + public void remove(T element) { + if (contains(element)) { + List newList = newList(); + newList.remove(element); + mAsyncListDiffer.submitList(newList); + } + } + + @Override + public void removeItems(List elements) { + if (Checker.isEmpty(elements) || isEmpty() || !getItems().containsAll(elements)) { + return; + } + List newList = newList(); + newList.removeAll(elements); + mAsyncListDiffer.submitList(newList); + } + + @Override + public void removeAt(int index) { + if (getDataSize() > index) { + List newList = newList(); + newList.remove(index); + mAsyncListDiffer.submitList(newList); + } + } + + @Override + public T getItem(int position) { + return getDataSize() > position ? getItems().get(position) : null; + } + + @Override + public final int getDataSize() { + return getItems() == null ? 0 : getItems().size(); + } + + @Override + public boolean contains(T element) { + return !isEmpty() && getItems().contains(element); + } + + @Override + public void clear() { + mAsyncListDiffer.submitList(null); + } + + @Override + public void setDataSource(List elements, boolean notifyDataSetChanged) { + mAsyncListDiffer.submitList(elements); + } + + @Override + public List getItems() { + return mAsyncListDiffer.getCurrentList(); + } + + @Override + public boolean isEmpty() { + return getDataSize() == 0; + } + + @Override + public int getItemPosition(T t) { + return isEmpty() ? -1 : getItems().indexOf(t); + } + + private List newList() { + if (getItems() == null) { + return new ArrayList<>(); + } else { + return new ArrayList<>(getItems()); + } + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/adapter/recycler/HeaderSize.java b/lib_base/src/main/java/com/android/base/adapter/recycler/HeaderSize.java new file mode 100644 index 0000000..da9af1c --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/recycler/HeaderSize.java @@ -0,0 +1,5 @@ +package com.android.base.adapter.recycler; + +public interface HeaderSize { + int getHeaderSize(); +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/adapter/recycler/ItemViewBinder.kt b/lib_base/src/main/java/com/android/base/adapter/recycler/ItemViewBinder.kt new file mode 100644 index 0000000..3b85ca3 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/recycler/ItemViewBinder.kt @@ -0,0 +1,35 @@ +package com.android.base.adapter.recycler + +import android.support.v7.widget.RecyclerView +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.android.base.kotlin.KtViewHolder + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-09-13 15:33 + */ +abstract class ItemViewBinder : me.drakeet.multitype.ItemViewBinder() { + + protected val dataManager: MultiTypeAdapter + get() = adapter as MultiTypeAdapter + +} + +abstract class SimpleItemViewBinder : ItemViewBinder() { + + override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): KtViewHolder { + val layout = provideLayout(inflater, parent) + val itemView = if (layout is Int) { + inflater.inflate(layout, parent, false) + } else + layout as View + return KtViewHolder(itemView) + } + + /**provide a layout id or a View*/ + abstract fun provideLayout(inflater: LayoutInflater, parent: ViewGroup): Any + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/adapter/recycler/MultiTypeAdapter.java b/lib_base/src/main/java/com/android/base/adapter/recycler/MultiTypeAdapter.java new file mode 100644 index 0000000..d29f4d7 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/recycler/MultiTypeAdapter.java @@ -0,0 +1,166 @@ +package com.android.base.adapter.recycler; + +import android.content.Context; +import android.support.annotation.NonNull; + +import com.android.base.adapter.DataManager; + +import java.util.ArrayList; +import java.util.List; + +import me.drakeet.multitype.TypePool; + +/** + * @see drakeet/MultiTypeAdapter + */ +public class MultiTypeAdapter extends me.drakeet.multitype.MultiTypeAdapter implements DataManager { + + protected final Context mContext; + + private RecyclerDataManagerImpl mRecyclerDataManager; + + public MultiTypeAdapter(Context context) { + super(); + mContext = context; + ArrayList objects = new ArrayList<>(); + mRecyclerDataManager = new RecyclerDataManagerImpl<>(objects, this); + super.setItems(objects); + } + + public MultiTypeAdapter(Context context, @NonNull List items) { + super(); + mContext = context; + ArrayList objects = new ArrayList<>(items); + mRecyclerDataManager = new RecyclerDataManagerImpl<>(objects, this); + super.setItems(objects); + } + + public MultiTypeAdapter(Context context, @NonNull List items, int initialCapacity) { + super(items, initialCapacity); + mContext = context; + ArrayList objects = new ArrayList<>(items); + mRecyclerDataManager = new RecyclerDataManagerImpl<>(objects, this); + super.setItems(objects); + } + + public MultiTypeAdapter(Context context, @NonNull List items, @NonNull TypePool pool) { + super(items, pool); + mContext = context; + ArrayList objects = new ArrayList<>(items); + mRecyclerDataManager = new RecyclerDataManagerImpl<>(objects, this); + super.setItems(objects); + } + + @SuppressWarnings("unused") + public void notifyEntryChanged(Object entry) { + int itemPosition = getItemPosition(entry); + if (itemPosition != -1) { + notifyItemChanged(itemPosition); + } + } + + @Override + public void add(Object elem) { + mRecyclerDataManager.add(elem); + } + + @Override + public void addAt(int location, Object elem) { + mRecyclerDataManager.addAt(location, elem); + } + + @Override + public void addItems(List elements) { + mRecyclerDataManager.addItems(elements); + } + + @Override + public void addItemsChecked(List elements) { + mRecyclerDataManager.addItemsChecked(elements); + } + + @Override + public void addItemsAt(int location, List elements) { + mRecyclerDataManager.addItemsAt(location, elements); + } + + @Override + public void replace(Object oldElem, Object newElem) { + mRecyclerDataManager.replace(oldElem, newElem); + } + + @Override + public void replaceAt(int index, Object elem) { + mRecyclerDataManager.replaceAt(index, elem); + } + + @Override + public void replaceAll(List elements) { + mRecyclerDataManager.replaceAll(elements); + } + + @Override + public void setDataSource(List newDataSource, boolean notifyDataSetChanged) { + super.setItems(newDataSource); + mRecyclerDataManager.setDataSource(newDataSource, notifyDataSetChanged); + } + + @Override + public void remove(Object elem) { + mRecyclerDataManager.remove(elem); + } + + @Override + public void removeAt(int index) { + mRecyclerDataManager.removeAt(index); + } + + @Override + public void removeItems(List elements) { + mRecyclerDataManager.removeItems(elements); + } + + @Override + public Object getItem(int position) { + return mRecyclerDataManager.getItem(position); + } + + @Override + public int getDataSize() { + return mRecyclerDataManager.getDataSize(); + } + + @Override + public boolean contains(Object elem) { + return mRecyclerDataManager.contains(elem); + } + + @Override + public boolean isEmpty() { + return mRecyclerDataManager.isEmpty(); + } + + @Override + public void clear() { + mRecyclerDataManager.clear(); + } + + @NonNull + @Override + public List getItems() { + return mRecyclerDataManager.getItems(); + } + + @Override + public int getItemPosition(Object o) { + return mRecyclerDataManager.getItemPosition(o); + } + + @Override + public void setItems(@NonNull List items) { + ArrayList objects = new ArrayList<>(items); + super.setItems(objects); + mRecyclerDataManager.setDataSource(objects, true); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/adapter/recycler/RecyclerAdapter.java b/lib_base/src/main/java/com/android/base/adapter/recycler/RecyclerAdapter.java new file mode 100644 index 0000000..0bf632f --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/recycler/RecyclerAdapter.java @@ -0,0 +1,164 @@ +package com.android.base.adapter.recycler; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; +import android.view.ViewGroup; + +import com.android.base.adapter.DataManager; + +import java.util.List; + +/** + * RecyclerView 的适配器 + * 注意: 只有setDataSource才能替换原有数据源的引用。 + * + * @param 当前列表使用的数据类型 + * @author Ztiany + * date : 2015-05-11 22:38 + * email: 1169654504@qq.com + */ +public abstract class RecyclerAdapter extends RecyclerView.Adapter implements DataManager { + + private RecyclerDataManagerImpl mDataManager; + + @NonNull + protected Context mContext; + + public RecyclerAdapter(@NonNull Context context, List data) { + mDataManager = new RecyclerDataManagerImpl<>(data, this); + this.mContext = context; + } + + public RecyclerAdapter(@NonNull Context context) { + this(context, null); + } + + @Override + public int getItemCount() { + return getDataSize(); + } + + @NonNull + @Override + public abstract VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType); + + @Override + public void onBindViewHolder(@NonNull VH holder, int position, @NonNull List payloads) { + super.onBindViewHolder(holder, position, payloads); + } + + @Override + public abstract void onBindViewHolder(@NonNull VH viewHolder, int position); + + public void notifyEntryChanged(T t) { + int itemPosition = getItemPosition(t); + if (itemPosition != -1) { + notifyItemChanged(itemPosition); + } + } + + /////////////////////////////////////////////////////////////////////////// + // DataManager + /////////////////////////////////////////////////////////////////////////// + + @Override + public void add(T elem) { + mDataManager.add(elem); + } + + @Override + public void addAt(int location, T elem) { + mDataManager.addAt(location, elem); + } + + @Override + public void addItems(List elements) { + mDataManager.addItems(elements); + } + + @Override + public void addItemsChecked(List elements) { + mDataManager.addItemsChecked(elements); + } + + @Override + public void addItemsAt(int location, List elements) { + mDataManager.addItemsAt(location, elements); + } + + @Override + public void replace(T oldElem, T newElem) { + mDataManager.replace(oldElem, newElem); + } + + @Override + public void replaceAt(int index, T elem) { + mDataManager.replaceAt(index, elem); + } + + @Override + public void replaceAll(List elements) { + mDataManager.replaceAll(elements); + } + + @Override + public void remove(T elem) { + mDataManager.remove(elem); + } + + @Override + public void removeItems(List elements) { + mDataManager.removeItems(elements); + } + + @Override + public void removeAt(int index) { + mDataManager.removeAt(index); + } + + @Override + public T getItem(int position) { + return mDataManager.getItem(position); + } + + @Override + public final int getDataSize() { + return mDataManager.getDataSize(); + } + + @Override + public boolean contains(T elem) { + return mDataManager.contains(elem); + } + + @Override + public void clear() { + mDataManager.clear(); + } + + @Override + public void setDataSource(List elements, boolean notifyDataSetChanged) { + mDataManager.setDataSource(elements, notifyDataSetChanged); + } + + @Override + public List getItems() { + return mDataManager.getItems(); + } + + protected final void setHeaderSize(HeaderSize headerSize) { + mDataManager.setHeaderSize(headerSize); + } + + @Override + public boolean isEmpty() { + return mDataManager.isEmpty(); + } + + @Override + public int getItemPosition(T t) { + return mDataManager.getItemPosition(t); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/adapter/recycler/RecyclerDataManagerImpl.java b/lib_base/src/main/java/com/android/base/adapter/recycler/RecyclerDataManagerImpl.java new file mode 100644 index 0000000..1524222 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/recycler/RecyclerDataManagerImpl.java @@ -0,0 +1,248 @@ +package com.android.base.adapter.recycler; + +import android.support.v7.widget.RecyclerView; + +import com.android.base.adapter.DataManager; +import com.android.base.utils.common.Checker; + +import java.util.ArrayList; +import java.util.List; + + +final class RecyclerDataManagerImpl implements DataManager { + + private List mData; + private RecyclerView.Adapter mAdapter; + private HeaderSize mHeaderSize; + + RecyclerDataManagerImpl(List tList, RecyclerView.Adapter adapter) { + mData = tList; + mAdapter = adapter; + } + + @Override + public boolean isEmpty() { + return getDataSize() == 0; + } + + @Override + public void add(T element) { + addAt(getDataSize(), element); + } + + @Override + public void addAt(int location, T element) { + if (mData != null) { + if (mData.isEmpty()) { + mData.add(element); + notifyItemInserted(getHeaderSize()); + } else { + int size = mData.size(); + int lastIndex = (location >= size ? size : location) + getHeaderSize(); + mData.add(location, element); + notifyItemInserted(lastIndex); + } + } else { + mData = new ArrayList<>(); + mData.add(element); + notifyDataSetChanged(); + } + } + + @Override + public void addItems(List elements) { + addItemsAt(getDataSize(), elements); + } + + @Override + public void addItemsChecked(List elements) { + if (Checker.isEmpty(elements)) { + return; + } + if (mData == null) { + addItems(elements); + return; + } + boolean hasRemovedElements = false; + for (T element : elements) { + if (element == null) { + continue; + } + if (mData.contains(element)) { + mData.remove(element); + if (!hasRemovedElements) { + hasRemovedElements = true; + } + } + } + if (hasRemovedElements) { + mData.addAll(elements); + notifyDataSetChanged(); + } else { + addItems(elements); + } + } + + @Override + public void addItemsAt(int location, List elements) { + if (Checker.isEmpty(elements)) { + return; + } + if (this.mData != null) { + + if (mData.isEmpty()) { + mData.addAll(elements); + notifyItemRangeInserted(getHeaderSize(), elements.size()); + } else { + int size = mData.size(); + int lastIndex = (location >= size ? size : location) + getHeaderSize(); + this.mData.addAll(location, elements); + notifyItemRangeInserted(lastIndex, elements.size()); + } + + } else { + this.mData = new ArrayList<>(elements.size()); + mData.addAll(elements); + notifyDataSetChanged(); + } + } + + @Override + public void replace(T oldElem, T newElem) { + if (mData != null && mData.contains(newElem)) { + replaceAt(mData.indexOf(oldElem), newElem); + } + } + + @Override + public void replaceAt(int index, T element) { + if (getDataSize() > index) { + mData.set(index, element); + notifyItemChanged(index + getHeaderSize()); + } + } + + @Override + public void replaceAll(List elements) { + if (mData == null) { + mData = new ArrayList<>(); + } + mData.clear(); + if (elements != null) { + mData.addAll(elements); + } + notifyDataSetChanged(); + } + + @Override + public void setDataSource(List newDataSource, boolean notifyDataSetChanged) { + mData = newDataSource; + if (notifyDataSetChanged) { + notifyDataSetChanged(); + } + } + + @Override + public void remove(T element) { + if (mData == null || mData.isEmpty()) { + return; + } + if (mData.contains(element)) { + int index = mData.indexOf(element) + getHeaderSize(); + mData.remove(element); + notifyItemRemoved(index); + } + } + + @Override + public void removeAt(int index) { + if (getDataSize() > index) { + mData.remove(index); + notifyItemRemoved(index + getHeaderSize()); + } + } + + @Override + public void removeItems(List elements) { + if (!Checker.isEmpty(elements) && mData != null && mData.containsAll(elements)) { + mData.removeAll(elements); + notifyDataSetChanged(); + } + } + + @Override + public T getItem(int position) { + position = position - getHeaderSize(); + if (position >= 0 && getDataSize() > position) { + return mData.get(position); + } + return null; + } + + @Override + public final int getDataSize() { + return mData == null ? 0 : mData.size(); + } + + @Override + public boolean contains(T element) { + return mData != null && mData.contains(element); + } + + @Override + public void clear() { + if (mData != null) { + mData.clear(); + notifyDataSetChanged(); + } + } + + @Override + public int getItemPosition(T t) { + List items = getItems(); + if (items == null) { + return -1; + } + return items.indexOf(t); + } + + @Override + public List getItems() { + return mData; + } + + /////////////////////////////////////////////////////////////////////////// + // Size + /////////////////////////////////////////////////////////////////////////// + private int getHeaderSize() { + return mHeaderSize == null ? 0 : mHeaderSize.getHeaderSize(); + } + + /////////////////////////////////////////////////////////////////////////// + // Adapter Call + /////////////////////////////////////////////////////////////////////////// + private void notifyItemChanged(int position) { + mAdapter.notifyItemChanged(position); + } + + private void notifyDataSetChanged() { + mAdapter.notifyDataSetChanged(); + } + + private void notifyItemInserted(int position) { + mAdapter.notifyItemInserted(position); + } + + private void notifyItemRangeInserted(int position, int size) { + mAdapter.notifyItemRangeInserted(position, size); + } + + private void notifyItemRemoved(int index) { + mAdapter.notifyItemRemoved(index); + } + + void setHeaderSize(HeaderSize headerSize) { + mHeaderSize = headerSize; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/adapter/recycler/SimpleRecyclerAdapter.kt b/lib_base/src/main/java/com/android/base/adapter/recycler/SimpleRecyclerAdapter.kt new file mode 100644 index 0000000..ed14837 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/recycler/SimpleRecyclerAdapter.kt @@ -0,0 +1,59 @@ +package com.android.base.adapter.recycler + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import kotlin.reflect.KClass +import kotlin.reflect.full.primaryConstructor + +/** + * A simple way to build a simple list. If you want to build a multi type list, perhaps you need to use [MultiTypeAdapter]. + * + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-01-15 11:41 + */ +abstract class SimpleRecyclerAdapter(context: Context, data: List? = null) : RecyclerAdapter(context, data) { + + private var mLayoutInflater: LayoutInflater = LayoutInflater.from(mContext) + + final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH { + val layout = provideLayout(parent, viewType) + val itemView = if (layout is Int) { + mLayoutInflater.inflate(layout, parent, false) + } else + layout as View + return provideViewHolder(itemView) + } + + @Suppress("UNCHECKED_CAST") + open fun provideViewHolder(itemView: View): VH { + return (this::class.supertypes[0].arguments[1].type?.classifier as? KClass)?.primaryConstructor?.call(itemView) + ?: throw IllegalArgumentException("need primaryConstructor, and arguments is (View)") + } + + /**provide a layout id or a View*/ + abstract fun provideLayout(parent: ViewGroup, viewType: Int): Any + + override fun getItemViewType(position: Int): Int { + return TYPE_ITEM + } + + override fun onBindViewHolder(viewHolder: VH, position: Int) { + if (viewHolder.itemViewType == TYPE_ITEM) { + bind(viewHolder, getItem(position)) + } else { + bindOtherTypes(viewHolder, position) + } + } + + protected abstract fun bind(viewHolder: VH, item: T) + + protected open fun bindOtherTypes(viewHolder: ViewHolder, position: Int) {} + + companion object { + protected const val TYPE_ITEM = 0 + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/adapter/recycler/SmartViewHolder.java b/lib_base/src/main/java/com/android/base/adapter/recycler/SmartViewHolder.java new file mode 100644 index 0000000..b22d6d5 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/recycler/SmartViewHolder.java @@ -0,0 +1,22 @@ +package com.android.base.adapter.recycler; + +import android.view.View; + +import com.android.base.adapter.ItemHelper; + + +public class SmartViewHolder extends ViewHolder { + + @SuppressWarnings("all") + protected final ItemHelper mHelper; + + public SmartViewHolder(View itemView) { + super(itemView); + mHelper = new ItemHelper(itemView); + } + + public ItemHelper helper() { + return mHelper; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/adapter/recycler/ViewHolder.java b/lib_base/src/main/java/com/android/base/adapter/recycler/ViewHolder.java new file mode 100644 index 0000000..67f135f --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/recycler/ViewHolder.java @@ -0,0 +1,28 @@ +package com.android.base.adapter.recycler; + +import android.content.Context; +import android.support.annotation.IdRes; +import android.support.v7.widget.RecyclerView; +import android.view.View; + + +public class ViewHolder extends RecyclerView.ViewHolder { + + public ViewHolder(View itemView) { + super(itemView); + } + + protected Context getContext() { + return itemView.getContext(); + } + + public V findView(@IdRes int viewId) { + return itemView.findViewById(viewId); + } + + public ViewHolder setItemClickListener(View.OnClickListener onClickListener) { + itemView.setOnClickListener(onClickListener); + return this; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/ApplicationDelegate.java b/lib_base/src/main/java/com/android/base/app/ApplicationDelegate.java new file mode 100644 index 0000000..67d09a9 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/ApplicationDelegate.java @@ -0,0 +1,116 @@ +package com.android.base.app; + +import android.app.Application; +import android.content.Context; +import android.content.IntentFilter; +import android.content.res.Configuration; +import android.net.ConnectivityManager; + +import com.android.base.receiver.NetStateReceiver; +import com.android.base.utils.BaseUtils; +import com.blankj.utilcode.util.AppUtils; +import com.blankj.utilcode.util.Utils; + +import java.util.concurrent.atomic.AtomicBoolean; + +import io.reactivex.Flowable; +import io.reactivex.processors.BehaviorProcessor; +import timber.log.Timber; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-10-12 18:19 + */ +@SuppressWarnings("WeakerAccess,unused") +public final class ApplicationDelegate { + + private Application mApplication; + + private CrashHandler mCrashHandler; + private BehaviorProcessor mAppStatus; + + private AtomicBoolean mOnCreateCalled = new AtomicBoolean(false); + private AtomicBoolean mOnAttachBaseCalled = new AtomicBoolean(false); + + ApplicationDelegate() { + } + + public void attachBaseContext(@SuppressWarnings("unused") Context base) { + if (!mOnAttachBaseCalled.compareAndSet(false, true)) { + throw new IllegalStateException("Can only be called once"); + } + //no op + } + + public void onCreate(Application application) { + if (!mOnCreateCalled.compareAndSet(false, true)) { + throw new IllegalStateException("Can only be called once"); + } + mApplication = application; + //工具类初始化 + BaseUtils.init(application); + //异常日志记录 + mCrashHandler = CrashHandler.register(application); + //网络状态 + listenNetworkState(); + //App前台后台 + listenActivityLifecycleCallbacks(); + } + + public void onTerminate() { + //no op + } + + public void onConfigurationChanged(Configuration newConfig) { + //no op + } + + public void onTrimMemory(int level) { + //no op + } + + public void onLowMemory() { + //no op + } + + private void listenNetworkState() { + NetStateReceiver netStateReceiver = new NetStateReceiver(); + mApplication.registerReceiver(netStateReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); + } + + private void listenActivityLifecycleCallbacks() { + mAppStatus = BehaviorProcessor.create(); + AppUtils.registerAppStatusChangedListener(this, new Utils.OnAppStatusChangedListener() { + @Override + public void onForeground() { + Timber.d("app进入前台"); + mAppStatus.onNext(true); + } + + @Override + public void onBackground() { + Timber.d("app进入后台"); + mAppStatus.onNext(false); + } + }); + } + + /** + * 获取可观察的 app 生命周期 + */ + Flowable appAppState() { + return mAppStatus; + } + + void setCrashProcessor(BaseKit.CrashProcessor crashProcessor) { + if (mCrashHandler != null) { + mCrashHandler.setCrashProcessor(crashProcessor); + } + } + + Application getApplication() { + return mApplication; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/BaseAppContext.java b/lib_base/src/main/java/com/android/base/app/BaseAppContext.java new file mode 100644 index 0000000..72a3d3c --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/BaseAppContext.java @@ -0,0 +1,46 @@ +package com.android.base.app; + +import android.app.Application; +import android.content.Context; +import android.content.res.Configuration; + + +public class BaseAppContext extends Application { + + @Override + protected void attachBaseContext(Context base) { + super.attachBaseContext(base); + BaseKit.get().getApplicationDelegate().attachBaseContext(base); + } + + @Override + public void onCreate() { + super.onCreate(); + BaseKit.get().getApplicationDelegate().onCreate(this); + } + + @Override + public void onLowMemory() { + super.onLowMemory(); + BaseKit.get().getApplicationDelegate().onLowMemory(); + } + + @Override + public void onTrimMemory(int level) { + super.onTrimMemory(level); + BaseKit.get().getApplicationDelegate().onTrimMemory(level); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + BaseKit.get().getApplicationDelegate().onConfigurationChanged(newConfig); + } + + @Override + public void onTerminate() { + super.onTerminate(); + BaseKit.get().getApplicationDelegate().onTerminate(); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/BaseKit.java b/lib_base/src/main/java/com/android/base/app/BaseKit.java new file mode 100644 index 0000000..39219c9 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/BaseKit.java @@ -0,0 +1,208 @@ +package com.android.base.app; + +import android.app.Activity; +import android.app.Application; +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; + +import com.android.base.app.dagger.Injectable; +import com.android.base.app.fragment.FragmentConfig; +import com.android.base.app.fragment.LoadingViewFactory; +import com.android.base.app.ui.PageNumber; +import com.android.base.app.ui.RefreshLoadViewFactory; +import com.android.base.app.ui.RefreshViewFactory; +import com.android.base.interfaces.adapter.ActivityLifecycleCallbacksAdapter; +import com.android.base.receiver.NetworkState; +import com.blankj.utilcode.util.ActivityUtils; +import com.blankj.utilcode.util.AppUtils; + +import dagger.android.AndroidInjection; +import dagger.android.support.AndroidSupportInjection; +import io.reactivex.Flowable; + +/** + * 基础库工具 + * + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-04-16 17:12 + */ +@UiThread +public final class BaseKit { + + private static final BaseKit ONLY_BASE = new BaseKit(); + + private BaseKit() { + mApplicationDelegate = new ApplicationDelegate(); + } + + public static BaseKit get() { + return ONLY_BASE; + } + + /** + * LoadingView + */ + private LoadingViewFactory mLoadingViewFactory; + + /** + * Application lifecycle delegate + */ + private ApplicationDelegate mApplicationDelegate; + + /** + * 错误类型检查 + */ + private ErrorClassifier mErrorClassifier; + + /** + * 获取 Application 代理 + */ + @SuppressWarnings("WeakerAccess") + public ApplicationDelegate getApplicationDelegate() { + return mApplicationDelegate; + } + + public BaseKit registerLoadingFactory(LoadingViewFactory loadingViewFactory) { + if (mLoadingViewFactory != null) { + throw new UnsupportedOperationException("LoadingViewFactory had already set"); + } + mLoadingViewFactory = loadingViewFactory; + return this; + } + + public LoadingViewFactory getLoadingViewFactory() { + if (mLoadingViewFactory == null) { + throw new NullPointerException("you have not set the LoadingViewFactory by AndroidBase"); + } + return mLoadingViewFactory; + } + + public Flowable networkState() { + return NetworkState.observableState(); + } + + + public interface CrashProcessor { + void uncaughtException(Thread thread, Throwable ex); + } + + public BaseKit setCrashProcessor(CrashProcessor crashProcessor) { + mApplicationDelegate.setCrashProcessor(crashProcessor); + return this; + } + + public BaseKit setDefaultPageStart(int pageStart) { + PageNumber.setDefaultPageStart(pageStart); + return this; + } + + public BaseKit setDefaultPageSize(int defaultPageSize) { + PageNumber.setDefaultPageSize(defaultPageSize); + return this; + } + + /** + * 给 {@link com.android.base.app.fragment.Fragments } 设置一个默认的容器 id,在使用 其相关方法而没有传入特定的容器 id 时,则使用默认的容器 id。 + * + * @param defaultContainerId 容器id + */ + public BaseKit setDefaultFragmentContainerId(int defaultContainerId) { + FragmentConfig.setDefaultContainerId(defaultContainerId); + return this; + } + + public BaseKit enableAutoInject() { + Application.ActivityLifecycleCallbacks activityLifecycleCallbacks = new ActivityLifecycleCallbacksAdapter() { + + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) { + if (activity instanceof Injectable) { + if (((Injectable) activity).enableInject()) { + AndroidInjection.inject(activity); + if (activity instanceof FragmentActivity) { + handedFragmentInject((FragmentActivity) activity); + } + } + } + } + + private void handedFragmentInject(FragmentActivity activity) { + activity.getSupportFragmentManager().registerFragmentLifecycleCallbacks(new FragmentManager.FragmentLifecycleCallbacks() { + @Override + public void onFragmentAttached(@NonNull FragmentManager fm, @NonNull Fragment f, @NonNull Context context) { + if (f instanceof Injectable) { + if (((Injectable) f).enableInject()) { + AndroidSupportInjection.inject(f); + } + } + } + }, true); + } + }; + mApplicationDelegate.getApplication().registerActivityLifecycleCallbacks(activityLifecycleCallbacks); + return this; + } + + /** + * 获取可观察的 app 生命周期 + */ + public Flowable appState() { + return mApplicationDelegate.appAppState(); + } + + /** + * 获取当前resume的Activity + * + * @return activity + */ + @Nullable + public Activity getTopActivity() { + return ActivityUtils.getTopActivity(); + } + + /** + * App是否在前台运行 + * + * @return true 表示App在前台运行 + */ + public boolean isForeground() { + return AppUtils.isAppForeground(); + } + + public interface ErrorClassifier { + boolean isNetworkError(Throwable throwable); + + boolean isServerError(Throwable throwable); + } + + @SuppressWarnings("all") + public BaseKit setErrorClassifier(ErrorClassifier errorClassifier) { + if (mErrorClassifier != null) { + throw new UnsupportedOperationException("ErrorClassifier had already set"); + } + mErrorClassifier = errorClassifier; + return this; + } + + public ErrorClassifier errorClassifier() { + return mErrorClassifier; + } + + public BaseKit registerRefreshLoadViewFactory(RefreshLoadViewFactory.Factory factory) { + RefreshLoadViewFactory.registerFactory(factory); + return this; + } + + public BaseKit registerRefreshViewFactory(RefreshViewFactory.Factory factory) { + RefreshViewFactory.registerFactory(factory); + return this; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/CrashHandler.java b/lib_base/src/main/java/com/android/base/app/CrashHandler.java new file mode 100644 index 0000000..1509ee0 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/CrashHandler.java @@ -0,0 +1,113 @@ +package com.android.base.app; + +import android.annotation.SuppressLint; +import android.app.Application; +import android.content.Context; +import android.os.Build; +import android.os.Process; + +import com.blankj.utilcode.util.AppUtils; + +import java.io.File; +import java.io.PrintStream; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; + +import timber.log.Timber; + +/** + * 全局异常处理 + */ +final class CrashHandler implements Thread.UncaughtExceptionHandler { + + private Context mContext; + private BaseKit.CrashProcessor mCrashProcessor; + + public static CrashHandler register(Application application) { + CrashHandler crashHandler = new CrashHandler(application); + Thread.setDefaultUncaughtExceptionHandler(crashHandler); + return crashHandler; + } + + void setCrashProcessor(BaseKit.CrashProcessor crashProcessor) { + mCrashProcessor = crashProcessor; + } + + private CrashHandler(Context context) { + this.mContext = context; + } + + @Override + public void uncaughtException(Thread thread, Throwable ex) { + + if (mCrashProcessor != null) { + mCrashProcessor.uncaughtException(thread, ex); + } else { + // 收集异常信息,写入到sd卡 + restoreCrash(thread, ex); + //退出 + killProcess(); + } + } + + private void restoreCrash(@SuppressWarnings("unused") Thread thread, Throwable ex) { + ex.printStackTrace(System.err); + // 收集异常信息,写入到sd卡 + File dir = new File(mContext.getExternalFilesDir(null) + File.separator + "crash"); + + if (!dir.exists()) { + boolean mkdirs = dir.mkdirs(); + if (!mkdirs) { + Timber.e("CrashHandler create dir fail"); + return; + } + } + + try { + @SuppressLint("SimpleDateFormat") + DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String name = dateFormat.format(new Date(System.currentTimeMillis())) + ".log"; + File fileName = new File(dir, name); + if (!fileName.exists()) { + @SuppressWarnings("unused") + boolean newFile = fileName.createNewFile(); + } + + PrintStream err = new PrintStream(fileName); + + err.println("--------------------------------AppInfo--------------------------------"); + err.println("AndroidVersion: " + AppUtils.getAppVersionName()); + err.println(); + err.println(); + err.println("--------------------------------SystemInfo:--------------------------------"); + err.println("Product: " + android.os.Build.PRODUCT); + err.println("CPU_ABI: " + android.os.Build.CPU_ABI); + err.println("TAGS: " + android.os.Build.TAGS); + err.println("VERSION_CODES.BASE:" + android.os.Build.VERSION_CODES.BASE); + err.println("MODEL: " + android.os.Build.MODEL); + err.println("SDK: " + Build.VERSION.SDK_INT); + err.println("VERSION.RELEASE: " + android.os.Build.VERSION.RELEASE); + err.println("DEVICE: " + android.os.Build.DEVICE); + err.println("DISPLAY: " + android.os.Build.DISPLAY); + err.println("BRAND: " + android.os.Build.BRAND); + err.println("BOARD: " + android.os.Build.BOARD); + err.println("FINGERPRINT: " + android.os.Build.FINGERPRINT); + err.println("ID: " + android.os.Build.ID); + err.println("MANUFACTURER: " + android.os.Build.MANUFACTURER); + err.println("USER: " + android.os.Build.USER); + err.println(); + err.println(); + err.println("--------------------------------CrashContent--------------------------------"); + ex.printStackTrace(err); + err.println(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void killProcess() { + Process.killProcess(Process.myPid()); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/aac/AutoClearedValue.kt b/lib_base/src/main/java/com/android/base/app/aac/AutoClearedValue.kt new file mode 100644 index 0000000..2f29cd1 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/aac/AutoClearedValue.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.base.app.aac + +import android.arch.lifecycle.DefaultLifecycleObserver +import android.arch.lifecycle.Lifecycle +import android.arch.lifecycle.LifecycleOwner +import android.support.v4.app.Fragment +import android.support.v4.app.FragmentActivity +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +class AutoClearedValue( + lifecycle: Lifecycle, + private val _event: Lifecycle.Event, + private val onCleared: (() -> Unit)? +) : ReadWriteProperty { + + private var _value: T? = null + + init { + lifecycle.addObserver(object : DefaultLifecycleObserver { + + override fun onPause(owner: LifecycleOwner) { + clearValue(Lifecycle.Event.ON_PAUSE) + } + + override fun onStop(owner: LifecycleOwner) { + clearValue(Lifecycle.Event.ON_STOP) + } + + override fun onDestroy(owner: LifecycleOwner) { + clearValue(Lifecycle.Event.ON_DESTROY) + } + + }) + } + + override fun getValue(thisRef: Fragment, property: KProperty<*>): T { + return _value + ?: throw IllegalStateException("should never call auto-cleared-value get when it might not be available") + } + + override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) { + _value = value + } + + private fun clearValue(event: Lifecycle.Event) { + if (_event == event) { + _value = null + onCleared?.invoke() + } + } + +} + +fun Fragment.autoCleared( + event: Lifecycle.Event = Lifecycle.Event.ON_DESTROY, + onCleared: (() -> Unit)? = null +) = AutoClearedValue(this.lifecycle, event, onCleared) + +fun FragmentActivity.autoCleared( + event: Lifecycle.Event = Lifecycle.Event.ON_DESTROY, + onCleared: (() -> Unit)? = null +) = AutoClearedValue(this.lifecycle, event, onCleared) \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/aac/AutoDisposeLiveData.kt b/lib_base/src/main/java/com/android/base/app/aac/AutoDisposeLiveData.kt new file mode 100644 index 0000000..db50540 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/aac/AutoDisposeLiveData.kt @@ -0,0 +1,225 @@ +package com.android.base.app.aac + +import android.arch.lifecycle.LiveData +import android.arch.lifecycle.MutableLiveData +import com.android.base.data.Resource +import com.android.base.rx.subscribeIgnoreError +import com.github.dmstocking.optional.java.util.Optional +import com.uber.autodispose.* + + +fun ObservableSubscribeProxy.toLiveData(): LiveData { + val liveData = MutableLiveData() + this.subscribeIgnoreError { + liveData.postValue(it) + } + return liveData +} + +fun FlowableSubscribeProxy.toLiveData(): LiveData { + val liveData = MutableLiveData() + this.subscribeIgnoreError { + liveData.postValue(it) + } + return liveData +} + +fun SingleSubscribeProxy.toLiveData(): LiveData { + val liveData = MutableLiveData() + this.subscribeIgnoreError { + liveData.postValue(it) + } + return liveData +} + +fun MaybeSubscribeProxy.toLiveData(): LiveData { + val liveData = MutableLiveData() + this.subscribeIgnoreError { + liveData.postValue(it) + } + return liveData +} + +//----------------------------------------------------------------------------------------- + +fun ObservableSubscribeProxy.subscribeWithLiveData(liveData: MutableLiveData>) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + liveData.postValue(Resource.success(it)) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +fun ObservableSubscribeProxy.subscribeWithLiveData(liveData: MutableLiveData>, map: (T) -> R) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + liveData.postValue(Resource.success(map(it))) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +fun ObservableSubscribeProxy>.subscribeOptionalWithLiveData(liveData: MutableLiveData>) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + liveData.postValue(Resource.success(it.orElse(null))) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +fun ObservableSubscribeProxy>.subscribeOptionalWithLiveData(liveData: MutableLiveData>, map: (T?) -> R?) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + val value = map(it.orElse(null)) + liveData.postValue(Resource.success(value)) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +fun FlowableSubscribeProxy.subscribeWithLiveData(liveData: MutableLiveData>) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + liveData.postValue(Resource.success(it)) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +fun FlowableSubscribeProxy.subscribeWithLiveData(liveData: MutableLiveData>, map: (T) -> R) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + liveData.postValue(Resource.success(map(it))) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +fun FlowableSubscribeProxy>.subscribeOptionalWithLiveData(liveData: MutableLiveData>) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + liveData.postValue(Resource.success(it.orElse(null))) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +fun FlowableSubscribeProxy>.subscribeOptionalWithLiveData(liveData: MutableLiveData>, map: (T?) -> R?) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + val value = map(it.orElse(null)) + liveData.postValue(Resource.success(value)) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +fun CompletableSubscribeProxy.subscribeWithLiveData(liveData: MutableLiveData>) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + liveData.postValue(Resource.success()) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +//----------------------------------------------------------------------------------------- + +fun ObservableSubscribeProxy.toResourceLiveData(): LiveData> { + val mutableLiveData = MutableLiveData>() + mutableLiveData.value = Resource.loading() + subscribe( + { + mutableLiveData.postValue(Resource.success(it)) + }, + { + mutableLiveData.postValue(Resource.error(it)) + } + ) + return mutableLiveData +} + +fun ObservableSubscribeProxy>.optionalToResourceLiveData(): LiveData> { + val mutableLiveData = MutableLiveData>() + mutableLiveData.value = Resource.loading() + subscribe( + { + mutableLiveData.postValue(Resource.success(it.orElse(null))) + }, + { + mutableLiveData.postValue(Resource.error(it)) + } + ) + return mutableLiveData +} + +fun FlowableSubscribeProxy.toResourceLiveData(): LiveData> { + val mutableLiveData = MutableLiveData>() + mutableLiveData.value = Resource.loading() + subscribe( + { + mutableLiveData.postValue(Resource.success(it)) + }, + { + mutableLiveData.postValue(Resource.error(it)) + } + ) + return mutableLiveData +} + +fun FlowableSubscribeProxy>.optionalToResourceLiveData(): LiveData> { + val mutableLiveData = MutableLiveData>() + mutableLiveData.value = Resource.loading() + subscribe( + { + mutableLiveData.postValue(Resource.success(it.orElse(null))) + }, + { + mutableLiveData.postValue(Resource.error(it)) + } + ) + return mutableLiveData +} + +fun CompletableSubscribeProxy.toResourceLiveData(): LiveData> { + val mutableLiveData = MutableLiveData>() + mutableLiveData.value = Resource.loading() + subscribe( + { + mutableLiveData.postValue(Resource.success()) + }, + { + mutableLiveData.postValue(Resource.error(it)) + } + ) + return mutableLiveData +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/aac/ResourceLiveData.kt b/lib_base/src/main/java/com/android/base/app/aac/ResourceLiveData.kt new file mode 100644 index 0000000..d2c21fb --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/aac/ResourceLiveData.kt @@ -0,0 +1,227 @@ +package com.android.base.app.aac + +import android.arch.lifecycle.LiveData +import android.arch.lifecycle.MutableLiveData +import com.android.base.data.Resource +import com.android.base.rx.subscribeIgnoreError +import com.github.dmstocking.optional.java.util.Optional +import io.reactivex.* + + +//----------------------------------------------------------------------------------------- + +fun Observable.subscribeWithLiveData(liveData: MutableLiveData>) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + liveData.postValue(Resource.success(it)) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +fun Observable.subscribeWithLiveData(liveData: MutableLiveData>, map: (T) -> R) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + liveData.postValue(Resource.success(map(it))) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +fun Observable>.subscribeOptionalWithLiveData(liveData: MutableLiveData>) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + liveData.postValue(Resource.success(it.orElse(null))) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +fun Observable>.subscribeOptionalWithLiveData(liveData: MutableLiveData>, map: (T?) -> R?) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + val value = map(it.orElse(null)) + liveData.postValue(Resource.success(value)) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +fun Flowable.subscribeWithLiveData(liveData: MutableLiveData>) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + liveData.postValue(Resource.success(it)) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +fun Flowable.subscribeWithLiveData(liveData: MutableLiveData>, map: (T) -> R) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + liveData.postValue(Resource.success(map(it))) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +fun Flowable>.subscribeOptionalWithLiveData(liveData: MutableLiveData>) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + liveData.postValue(Resource.success(it.orElse(null))) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +fun Flowable>.subscribeOptionalWithLiveData(liveData: MutableLiveData>, map: (T?) -> R?) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + val value = map(it.orElse(null)) + liveData.postValue(Resource.success(value)) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +fun Completable.subscribeWithLiveData(liveData: MutableLiveData>) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + liveData.postValue(Resource.success()) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +//----------------------------------------------------------------------------------------- + +fun Observable.toResourceLiveData(): LiveData> { + val mutableLiveData = MutableLiveData>() + mutableLiveData.value = Resource.loading() + subscribe( + { + mutableLiveData.postValue(Resource.success(it)) + }, + { + mutableLiveData.postValue(Resource.error(it)) + } + ) + return mutableLiveData +} + +fun Observable>.optionalToResourceLiveData(): LiveData> { + val mutableLiveData = MutableLiveData>() + mutableLiveData.value = Resource.loading() + subscribe( + { + mutableLiveData.postValue(Resource.success(it.orElse(null))) + }, + { + mutableLiveData.postValue(Resource.error(it)) + } + ) + return mutableLiveData +} + +fun Flowable.toResourceLiveData(): LiveData> { + val mutableLiveData = MutableLiveData>() + mutableLiveData.value = Resource.loading() + subscribe( + { + mutableLiveData.postValue(Resource.success(it)) + }, + { + mutableLiveData.postValue(Resource.error(it)) + } + ) + return mutableLiveData +} + +fun Flowable>.optionalToResourceLiveData(): LiveData> { + val mutableLiveData = MutableLiveData>() + mutableLiveData.value = Resource.loading() + subscribe( + { + mutableLiveData.postValue(Resource.success(it.orElse(null))) + }, + { + mutableLiveData.postValue(Resource.error(it)) + } + ) + return mutableLiveData +} + +fun Completable.toResourceLiveData(): LiveData> { + val mutableLiveData = MutableLiveData>() + mutableLiveData.value = Resource.loading() + subscribe( + { + mutableLiveData.postValue(Resource.success()) + }, + { + mutableLiveData.postValue(Resource.error(it)) + } + ) + return mutableLiveData +} + +//----------------------------------------------------------------------------------------- + +fun Observable.toLiveData(): LiveData { + val liveData = MutableLiveData() + this.subscribeIgnoreError { + liveData.postValue(it) + } + return liveData +} + +fun Flowable.toLiveData(): LiveData { + val liveData = MutableLiveData() + this.subscribeIgnoreError { + liveData.postValue(it) + } + return liveData +} + +fun Single.toLiveData(): LiveData { + val liveData = MutableLiveData() + this.subscribeIgnoreError { + liveData.postValue(it) + } + return liveData +} + +fun Maybe.toLiveData(): LiveData { + val liveData = MutableLiveData() + this.subscribeIgnoreError { + liveData.postValue(it) + } + return liveData +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/aac/SingleLiveData.kt b/lib_base/src/main/java/com/android/base/app/aac/SingleLiveData.kt new file mode 100644 index 0000000..b256889 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/aac/SingleLiveData.kt @@ -0,0 +1,36 @@ +package com.android.base.app.aac + +import android.arch.lifecycle.LifecycleOwner +import android.arch.lifecycle.MediatorLiveData +import android.arch.lifecycle.Observer + +/** https://github.com/Shopify/livedata-ktx */ +class SingleLiveData : MediatorLiveData() { + + private var _version = 0 + private val version: Int get() = _version + + override fun observe(owner: LifecycleOwner, observer: Observer) { + val observerVersion = version + super.observe(owner, Observer { + if (observerVersion < version) { + observer.onChanged(it) + } + }) + } + + override fun observeForever(observer: Observer) { + val observeSinceVersion = version + super.observeForever { + if (version > observeSinceVersion) { + observer.onChanged(it) + } + } + } + + override fun setValue(value: T?) { + _version++ + super.setValue(value) + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/aac/SingleLiveEvent.java b/lib_base/src/main/java/com/android/base/app/aac/SingleLiveEvent.java new file mode 100644 index 0000000..753aca3 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/aac/SingleLiveEvent.java @@ -0,0 +1,61 @@ +package com.android.base.app.aac; + +import android.arch.lifecycle.LifecycleOwner; +import android.arch.lifecycle.MutableLiveData; +import android.arch.lifecycle.Observer; +import android.support.annotation.MainThread; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; + +import java.util.concurrent.atomic.AtomicBoolean; + + +/** + * A lifecycle-aware observable that sends only new updates after subscription, used for events like + * navigation and Snackbar messages. + *

+ * This avoids a common problem with events: on configuration change (like rotation) an update + * can be emitted if the observer is active. This LiveData only calls the observable if there's an + * explicit call to setValue() or call(). + *

+ * Note that only one observer is going to be notified of changes. + * + * @see SingleLiveEvent + */ +public class SingleLiveEvent extends MutableLiveData { + + private static final String TAG = "SingleLiveEvent"; + + private final AtomicBoolean mPending = new AtomicBoolean(false); + + @MainThread + public void observe(@NonNull LifecycleOwner owner, @NonNull final Observer observer) { + + if (hasActiveObservers()) { + Log.w(TAG, "Multiple observers registered but only one will be notified of changes."); + } + + // Observe the internal MutableLiveData + super.observe(owner, t -> { + if (mPending.compareAndSet(true, false)) { + observer.onChanged(t); + } + }); + } + + @MainThread + public void setValue(@Nullable T t) { + mPending.set(true); + super.setValue(t); + } + + /** + * Used for cases where T is Void, to make calls cleaner. + */ + @MainThread + public void call() { + setValue(null); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/activity/ActivityDelegate.java b/lib_base/src/main/java/com/android/base/app/activity/ActivityDelegate.java new file mode 100644 index 0000000..77d5c20 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/activity/ActivityDelegate.java @@ -0,0 +1,68 @@ +package com.android.base.app.activity; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + + +/** + * Activity生命周期代理 + * + * @author Ztiany + * Date : 2016-05-06 15:04 + * Email: 1169654504@qq.com + */ +@SuppressWarnings("unused") +public interface ActivityDelegate { + + default void onAttachedToActivity(@NonNull T activity) { + } + + default void onDetachedFromActivity() { + } + + default void onCreateBeforeSetContentView(@Nullable Bundle savedInstanceState) { + } + + default void onCreateAfterSetContentView(@Nullable Bundle savedInstanceState) { + } + + default void onSaveInstanceState(Bundle savedInstanceState) { + } + + default void onStart() { + } + + default void onResume() { + } + + default void onPause() { + } + + default void onStop() { + } + + default void onDestroy() { + } + + default void onActivityResult(int requestCode, int resultCode, Intent data) { + } + + default void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + } + + default void onRestoreInstanceState(Bundle savedInstanceState) { + } + + default void onRestart() { + } + + default void onPostCreate(@Nullable Bundle savedInstanceState) { + } + + default void onResumeFragments() { + } + +} diff --git a/lib_base/src/main/java/com/android/base/app/activity/ActivityDelegateOwner.java b/lib_base/src/main/java/com/android/base/app/activity/ActivityDelegateOwner.java new file mode 100644 index 0000000..245ab97 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/activity/ActivityDelegateOwner.java @@ -0,0 +1,19 @@ +package com.android.base.app.activity; + +import android.support.annotation.UiThread; + +import com.github.dmstocking.optional.java.util.function.Predicate; + +@UiThread +@SuppressWarnings("unused") +public interface ActivityDelegateOwner { + + void addDelegate(ActivityDelegate fragmentDelegate); + + boolean removeDelegate(ActivityDelegate fragmentDelegate); + + ActivityDelegate findDelegate(Predicate predicate); + + ActivityStatus getStatus(); + +} diff --git a/lib_base/src/main/java/com/android/base/app/activity/ActivityDelegates.java b/lib_base/src/main/java/com/android/base/app/activity/ActivityDelegates.java new file mode 100644 index 0000000..f27e187 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/activity/ActivityDelegates.java @@ -0,0 +1,137 @@ +package com.android.base.app.activity; + +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; +import android.support.v7.app.AppCompatActivity; + +import com.github.dmstocking.optional.java.util.function.Predicate; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2016-12-20 11:43 + */ +@UiThread +final class ActivityDelegates { + + private final List mDelegates; + private AppCompatActivity mBaseActivity; + + ActivityDelegates(AppCompatActivity baseActivity) { + mDelegates = new ArrayList<>(4); + mBaseActivity = baseActivity; + } + + void callOnCreateBeforeSetContentView(@Nullable Bundle savedInstanceState) { + for (ActivityDelegate activityDelegate : mDelegates) { + activityDelegate.onCreateBeforeSetContentView(savedInstanceState); + } + } + + void callOnCreateAfterSetContentView(@Nullable Bundle savedInstanceState) { + for (ActivityDelegate activityDelegate : mDelegates) { + activityDelegate.onCreateAfterSetContentView(savedInstanceState); + } + } + + void callOnRestoreInstanceState(Bundle savedInstanceState) { + for (ActivityDelegate activityDelegate : mDelegates) { + activityDelegate.onRestoreInstanceState(savedInstanceState); + } + } + + void callOnPostCreate(@Nullable Bundle savedInstanceState) { + for (ActivityDelegate activityDelegate : mDelegates) { + activityDelegate.onPostCreate(savedInstanceState); + } + } + + void callOnSaveInstanceState(Bundle outState) { + for (ActivityDelegate activityDelegate : mDelegates) { + activityDelegate.onSaveInstanceState(outState); + } + } + + void callOnDestroy() { + for (ActivityDelegate activityDelegate : mDelegates) { + activityDelegate.onDestroy(); + } + } + + void callOnStop() { + for (ActivityDelegate activityDelegate : mDelegates) { + activityDelegate.onStop(); + } + } + + void callOnPause() { + for (ActivityDelegate activityDelegate : mDelegates) { + activityDelegate.onPause(); + } + } + + void callOnResume() { + for (ActivityDelegate activityDelegate : mDelegates) { + activityDelegate.onResume(); + } + } + + void callOnStart() { + for (ActivityDelegate activityDelegate : mDelegates) { + activityDelegate.onStart(); + } + } + + void callOnRestart() { + for (ActivityDelegate activityDelegate : mDelegates) { + activityDelegate.onRestart(); + } + } + + void callOnActivityResult(int requestCode, int resultCode, Intent data) { + for (ActivityDelegate activityDelegate : mDelegates) { + activityDelegate.onActivityResult(requestCode, resultCode, data); + } + } + + void callOnRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + for (ActivityDelegate activityDelegate : mDelegates) { + activityDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } + + void callOnResumeFragments() { + for (ActivityDelegate activityDelegate : mDelegates) { + activityDelegate.onResumeFragments(); + } + } + + @SuppressWarnings("unchecked") + void addActivityDelegate(ActivityDelegate activityDelegate) { + mDelegates.add(activityDelegate); + activityDelegate.onAttachedToActivity(mBaseActivity); + } + + boolean removeActivityDelegate(ActivityDelegate activityDelegate) { + boolean remove = mDelegates.remove(activityDelegate); + if (remove) { + activityDelegate.onDetachedFromActivity(); + } + return remove; + } + + ActivityDelegate findDelegate(Predicate predicate) { + for (ActivityDelegate delegate : mDelegates) { + if (predicate.test(delegate)) { + return delegate; + } + } + return null; + } + +} diff --git a/lib_base/src/main/java/com/android/base/app/activity/ActivityStatus.kt b/lib_base/src/main/java/com/android/base/app/activity/ActivityStatus.kt new file mode 100644 index 0000000..e0fd32a --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/activity/ActivityStatus.kt @@ -0,0 +1,11 @@ +package com.android.base.app.activity + +enum class ActivityStatus { + INITIALIZED, + CREATE, + START, + RESUME, + PAUSE, + STOP, + DESTROY +} diff --git a/lib_base/src/main/java/com/android/base/app/activity/BackHandlerHelper.java b/lib_base/src/main/java/com/android/base/app/activity/BackHandlerHelper.java new file mode 100644 index 0000000..a4d98d3 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/activity/BackHandlerHelper.java @@ -0,0 +1,71 @@ +package com.android.base.app.activity; + +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; + +import java.util.List; + +/** + *

+ *        1.Fragment需要自己处理BackPress事件,如果不处理,就交给子Fragment处理。都不处理则由Activity处理
+ *        2.BackPress的传递由低层往深层传递,同一层级的中外层中的 Fragment优先处理。
+ *        3.在Fragment中嵌套使用Fragment时,请使用getSupportChildFragmentManager
+ * 
+ */ +public class BackHandlerHelper { + + /** + * 将back事件分发给 FragmentManager 中管理的子Fragment,如果该 FragmentManager 中的所有Fragment都 + * 没有处理back事件,则尝试 FragmentManager.popBackStack() + * + * @return 如果处理了back键则返回 true + * @see #handleBackPress(Fragment) + * @see #handleBackPress(FragmentActivity) + */ + public static boolean handleBackPress(FragmentManager fragmentManager) { + List fragments = fragmentManager.getFragments(); + for (int i = fragments.size() - 1; i >= 0; i--) { + Fragment child = fragments.get(i); + if (isFragmentBackHandled(child)) { + return true; + } + } + return false; + } + + /** + * 将back事件分发给Fragment中的子Fragment, + * 该方法调用了 {@link #handleBackPress(FragmentManager)} + * + * @return 如果处理了back键则返回 true + */ + public static boolean handleBackPress(Fragment fragment) { + return handleBackPress(fragment.getChildFragmentManager()); + } + + /** + * 将back事件分发给Activity中的子Fragment, + * 该方法调用了 {@link #handleBackPress(FragmentManager)} + * + * @return 如果处理了back键则返回 true + */ + public static boolean handleBackPress(FragmentActivity fragmentActivity) { + return handleBackPress(fragmentActivity.getSupportFragmentManager()); + } + + /** + * 判断Fragment是否处理了Back键 + * + * @return 如果处理了back键则返回 true + */ + @SuppressWarnings("WeakerAccess") + public static boolean isFragmentBackHandled(Fragment fragment) { + return fragment != null + && fragment.isVisible() + && fragment.getUserVisibleHint() //getUserVisibleHint默认情况下为true,在ViewPager中会被使用到。 + && fragment instanceof OnBackPressListener + && ((OnBackPressListener) fragment).onBackPressed(); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/activity/BaseActivity.java b/lib_base/src/main/java/com/android/base/app/activity/BaseActivity.java new file mode 100644 index 0000000..b7597ac --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/activity/BaseActivity.java @@ -0,0 +1,222 @@ +package com.android.base.app.activity; + +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; +import android.support.v7.app.AppCompatActivity; +import android.view.View; + +import com.android.base.utils.android.compat.AndroidVersion; +import com.github.dmstocking.optional.java.util.function.Predicate; + +import timber.log.Timber; + +/** + *
+ *          1,封装通用流程。
+ *          2,onBackPressed 事件分发,优先交给 Fragment 处理。
+ *          3,提供 RxJava 的生命周期绑定。
+ *  
+ * + * @author Ztiany + * Date : 2016-05-04 15:40 + * Email: 1169654504@qq.com + */ +public abstract class BaseActivity extends AppCompatActivity implements ActivityDelegateOwner { + + private final ActivityDelegates mActivityDelegates = new ActivityDelegates(this); + + private ActivityStatus mActivityStatus = ActivityStatus.INITIALIZED; + + private String tag() { + return this.getClass().getSimpleName(); + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + Timber.tag(tag()).d("---->onCreate before call super"); + + initialize(savedInstanceState); + mActivityDelegates.callOnCreateBeforeSetContentView(savedInstanceState); + + super.onCreate(savedInstanceState); + Timber.tag(tag()).d("---->onCreate after call super " + "bundle = " + savedInstanceState); + + Object layout = layout(); + if (layout instanceof View) { + setContentView((View) layout); + } else if (layout instanceof Integer) { + setContentView((Integer) layout); + } else if (layout == null) { + Timber.d("layout() return null layout"); + } else { + throw new IllegalArgumentException("layout() return type no support, layout = " + layout); + } + + mActivityStatus = ActivityStatus.CREATE; + mActivityDelegates.callOnCreateAfterSetContentView(savedInstanceState); + + setupView(savedInstanceState); + } + + @Override + protected void onRestart() { + Timber.tag(tag()).d("---->onRestart before call super"); + super.onRestart(); + Timber.tag(tag()).d("---->onRestart after call super "); + mActivityDelegates.callOnRestart(); + } + + @Override + protected void onStart() { + Timber.tag(tag()).d("---->onStart before call super"); + super.onStart(); + Timber.tag(tag()).d("---->onStart after call super"); + mActivityStatus = ActivityStatus.START; + mActivityDelegates.callOnStart(); + } + + @Override + protected void onResume() { + Timber.tag(tag()).d("---->onResume before call super"); + super.onResume(); + Timber.tag(tag()).d("---->onResume after call super"); + mActivityStatus = ActivityStatus.RESUME; + mActivityDelegates.callOnResume(); + } + + @Override + protected void onPause() { + Timber.tag(tag()).d("---->onPause before call super"); + mActivityStatus = ActivityStatus.PAUSE; + mActivityDelegates.callOnPause(); + super.onPause(); + Timber.tag(tag()).d("---->onPause after call super "); + } + + @Override + protected void onStop() { + Timber.tag(tag()).d("---->onStop before call super"); + mActivityStatus = ActivityStatus.STOP; + mActivityDelegates.callOnStop(); + super.onStop(); + Timber.tag(tag()).d("---->onStop after call super"); + } + + @Override + protected void onDestroy() { + Timber.tag(tag()).d("---->onDestroy before call super"); + mActivityStatus = ActivityStatus.DESTROY; + mActivityDelegates.callOnDestroy(); + super.onDestroy(); + Timber.tag(tag()).d("---->onDestroy after call super"); + } + + @Override + protected void onPostCreate(@Nullable Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + mActivityDelegates.callOnPostCreate(savedInstanceState); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + mActivityDelegates.callOnSaveInstanceState(outState); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + mActivityDelegates.callOnRestoreInstanceState(savedInstanceState); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + mActivityDelegates.callOnActivityResult(requestCode, resultCode, data); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + mActivityDelegates.callOnRequestPermissionsResult(requestCode, permissions, grantResults); + } + + @Override + protected void onResumeFragments() { + super.onResumeFragments(); + mActivityDelegates.callOnResumeFragments(); + } + + /////////////////////////////////////////////////////////////////////////// + // interface impl + /////////////////////////////////////////////////////////////////////////// + @UiThread + @Override + public final void addDelegate(@NonNull ActivityDelegate activityDelegate) { + mActivityDelegates.addActivityDelegate(activityDelegate); + } + + @SuppressWarnings("unused") + @UiThread + @Override + public final boolean removeDelegate(@NonNull ActivityDelegate activityDelegate) { + return mActivityDelegates.removeActivityDelegate(activityDelegate); + } + + @Override + public ActivityDelegate findDelegate(Predicate predicate) { + return mActivityDelegates.findDelegate(predicate); + } + + @Override + public ActivityStatus getStatus() { + return mActivityStatus; + } + + /** + * Before call super.onCreate and setContentView + * + * @param savedInstanceState state + */ + protected void initialize(@Nullable Bundle savedInstanceState) { + } + + /** + * provide a layoutId (int) or layout (View) + * + * @return layoutId + */ + @Nullable + protected abstract Object layout(); + + /** + * after setContentView + */ + protected abstract void setupView(@Nullable Bundle savedInstanceState); + + @Override + public void onBackPressed() { + if (BackHandlerHelper.handleBackPress(this)) { + Timber.d("onBackPressed() called but child fragment handle it"); + } else { + superOnBackPressed(); + } + } + + protected void superOnBackPressed() { + super.onBackPressed(); + } + + @Override + public boolean isDestroyed() { + if (AndroidVersion.atLeast(17)) { + return super.isDestroyed(); + } else { + return getStatus() == ActivityStatus.DESTROY; + } + } + +} diff --git a/lib_base/src/main/java/com/android/base/app/activity/OnBackPressListener.java b/lib_base/src/main/java/com/android/base/app/activity/OnBackPressListener.java new file mode 100644 index 0000000..7c3b426 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/activity/OnBackPressListener.java @@ -0,0 +1,13 @@ +package com.android.base.app.activity; + +/** + * Activity的返回键监听 + */ +public interface OnBackPressListener { + + /** + * @return true 表示Fragment处理back press,false表示由Activity处理 + */ + boolean onBackPressed(); + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/dagger/ActivityScope.java b/lib_base/src/main/java/com/android/base/app/dagger/ActivityScope.java new file mode 100644 index 0000000..9038e1d --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/dagger/ActivityScope.java @@ -0,0 +1,14 @@ +package com.android.base.app.dagger; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Scope; + + +@Scope +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ActivityScope { +} diff --git a/lib_base/src/main/java/com/android/base/app/dagger/ContextType.java b/lib_base/src/main/java/com/android/base/app/dagger/ContextType.java new file mode 100644 index 0000000..370f713 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/dagger/ContextType.java @@ -0,0 +1,22 @@ +package com.android.base.app.dagger; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import javax.inject.Qualifier; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + + +@Qualifier +@Documented +@Retention(RUNTIME) +public @interface ContextType { + + String ACTIVITY = "Activity"; + String CONTEXT = "Context"; + String APPLICATION = "Application"; + + String value() default APPLICATION; + +} diff --git a/lib_base/src/main/java/com/android/base/app/dagger/FragmentScope.java b/lib_base/src/main/java/com/android/base/app/dagger/FragmentScope.java new file mode 100644 index 0000000..57d2313 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/dagger/FragmentScope.java @@ -0,0 +1,14 @@ +package com.android.base.app.dagger; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Scope; + + +@Scope +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface FragmentScope { +} diff --git a/lib_base/src/main/java/com/android/base/app/dagger/HasViewInjector.java b/lib_base/src/main/java/com/android/base/app/dagger/HasViewInjector.java new file mode 100644 index 0000000..69919e1 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/dagger/HasViewInjector.java @@ -0,0 +1,18 @@ +package com.android.base.app.dagger; + +import android.view.View; + +import java.util.Map; + +import javax.inject.Provider; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2017-06-21 14:58 + */ +public interface HasViewInjector { + + Map, Provider> viewInjectors(); + +} diff --git a/lib_base/src/main/java/com/android/base/app/dagger/Injectable.java b/lib_base/src/main/java/com/android/base/app/dagger/Injectable.java new file mode 100644 index 0000000..146a7b7 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/dagger/Injectable.java @@ -0,0 +1,31 @@ +package com.android.base.app.dagger; + +/** + * 当 Activity 实现此接口时,如果需要为内部的 Fragment 提供注入容器,需要实现 HasSupportFragmentInjector,具体如下面代码: + * + *
{@code
+ *
+ *       class InjectableActivity implements Injectable, HasSupportFragmentInjector{
+ *              @Inject
+ *              DispatchingAndroidInjector fragmentInjector;
+ *
+ *              public AndroidInjector supportFragmentInjector() {
+ *                         return fragmentInjector;
+ *              }
+ *       }
+ * }
+ *
+ * 标记接口,用于标记此类需要被注入依赖。
+ *
+ * @author Ztiany
+ * Email: ztiany3@gmail.com
+ * Date : 2018-12-11 12:38
+ */
+public interface Injectable {
+
+    default boolean enableInject() {
+        return true;
+    }
+
+}
+
diff --git a/lib_base/src/main/java/com/android/base/app/dagger/ViewComponent.java b/lib_base/src/main/java/com/android/base/app/dagger/ViewComponent.java
new file mode 100644
index 0000000..6ad4058
--- /dev/null
+++ b/lib_base/src/main/java/com/android/base/app/dagger/ViewComponent.java
@@ -0,0 +1,14 @@
+package com.android.base.app.dagger;
+
+import android.view.View;
+
+import dagger.MembersInjector;
+
+/**
+ * how to use it? refer https://github.com/Ztiany/Programming-Notes-Code/blob/master/Android/Dagger2AndroidInjection-v2.19/README.md。
+ *
+ * @param 
+ */
+@SuppressWarnings("WeakerAccess")
+public interface ViewComponent extends MembersInjector {
+}
diff --git a/lib_base/src/main/java/com/android/base/app/dagger/ViewComponentBuilder.java b/lib_base/src/main/java/com/android/base/app/dagger/ViewComponentBuilder.java
new file mode 100644
index 0000000..0a0896e
--- /dev/null
+++ b/lib_base/src/main/java/com/android/base/app/dagger/ViewComponentBuilder.java
@@ -0,0 +1,15 @@
+package com.android.base.app.dagger;
+
+import android.view.View;
+
+import dagger.BindsInstance;
+
+
+public interface ViewComponentBuilder> {
+
+    @BindsInstance
+    ViewComponentBuilder bindInstance(T t);
+
+    C build();
+
+}
\ No newline at end of file
diff --git a/lib_base/src/main/java/com/android/base/app/dagger/ViewInjection.java b/lib_base/src/main/java/com/android/base/app/dagger/ViewInjection.java
new file mode 100644
index 0000000..d9de77e
--- /dev/null
+++ b/lib_base/src/main/java/com/android/base/app/dagger/ViewInjection.java
@@ -0,0 +1,60 @@
+package com.android.base.app.dagger;
+
+import android.app.Application;
+import android.content.Context;
+import android.util.Log;
+import android.view.View;
+
+import java.util.Map;
+
+import javax.inject.Provider;
+
+/**
+ * @author Ztiany
+ * Email: ztiany3@gmail.com
+ * Date : 2017-06-21 14:55
+ */
+@SuppressWarnings("unused")
+public class ViewInjection {
+
+    private static final String TAG = ViewInjection.class.getSimpleName();
+
+    @SuppressWarnings("unchecked")
+    public static void inject(View view) {
+
+        if (null == view) {
+            throw new NullPointerException();
+        }
+
+        HasViewInjector hasViewInjector = findHasViewInjectors(view);
+
+        Log.d(TAG, String.format("An injector for %s was found in %s", view.getClass().getCanonicalName(), hasViewInjector.getClass().getCanonicalName()));
+
+        Map, Provider> viewInjectors = hasViewInjector.viewInjectors();
+        Provider provider = viewInjectors.get(view.getClass());
+
+        if (provider != null) {
+            ViewComponentBuilder viewComponentBuilder = provider.get();
+            viewComponentBuilder
+                    .bindInstance(view)
+                    .build()
+                    .injectMembers(view);
+        } else {
+            throw new NullPointerException("ViewInjection  fail ");
+        }
+
+    }
+
+    private static HasViewInjector findHasViewInjectors(View view) {
+        Context context = view.getContext();
+        if (context instanceof HasViewInjector) {
+            return (HasViewInjector) context;
+        }
+        Application application = (Application) context.getApplicationContext();
+        if (application instanceof HasViewInjector) {
+            return (HasViewInjector) application;
+        }
+        throw new IllegalArgumentException(String.format("No injector was found for %s", view.getClass().getCanonicalName()));
+    }
+
+}
diff --git a/lib_base/src/main/java/com/android/base/app/dagger/ViewKey.java b/lib_base/src/main/java/com/android/base/app/dagger/ViewKey.java
new file mode 100644
index 0000000..e29e78e
--- /dev/null
+++ b/lib_base/src/main/java/com/android/base/app/dagger/ViewKey.java
@@ -0,0 +1,18 @@
+package com.android.base.app.dagger;
+
+import android.view.View;
+
+import java.lang.annotation.Target;
+
+import dagger.MapKey;
+import dagger.internal.Beta;
+
+import static java.lang.annotation.ElementType.METHOD;
+
+@Beta
+@MapKey
+@Target(METHOD)
+@SuppressWarnings("unused")
+public @interface ViewKey {
+    Class value();
+}
diff --git a/lib_base/src/main/java/com/android/base/app/dagger/ViewModelFactory.java b/lib_base/src/main/java/com/android/base/app/dagger/ViewModelFactory.java
new file mode 100644
index 0000000..87823c0
--- /dev/null
+++ b/lib_base/src/main/java/com/android/base/app/dagger/ViewModelFactory.java
@@ -0,0 +1,45 @@
+package com.android.base.app.dagger;
+
+import android.arch.lifecycle.ViewModel;
+import android.arch.lifecycle.ViewModelProvider;
+import android.support.annotation.NonNull;
+
+import java.util.Map;
+
+import javax.inject.Inject;
+import javax.inject.Provider;
+
+/**
+ * @author Ztiany
+ * Email: ztiany3@gmail.com
+ * Date : 2018-11-05 14:31
+ */
+public class ViewModelFactory implements ViewModelProvider.Factory {
+
+    private final Map, Provider> mCreatorMap;
+
+    @Inject
+    ViewModelFactory(Map, Provider> classProviderMap) {
+        mCreatorMap = classProviderMap;
+    }
+
+    @NonNull
+    @Override
+    @SuppressWarnings("unchecked")
+    public  T create(@NonNull Class modelClass) {
+        Provider viewModelProvider = mCreatorMap.get(modelClass);
+        if (viewModelProvider == null) {
+            for (Map.Entry, Provider> entry : mCreatorMap.entrySet()) {
+                if (modelClass.isAssignableFrom(entry.getKey())) {
+                    viewModelProvider = entry.getValue();
+                    break;
+                }
+            }
+        }
+        if (viewModelProvider != null) {
+            return (T) viewModelProvider.get();
+        }
+        throw new NullPointerException("can not find provider for " + modelClass);
+    }
+
+}
diff --git a/lib_base/src/main/java/com/android/base/app/dagger/ViewModelKey.java b/lib_base/src/main/java/com/android/base/app/dagger/ViewModelKey.java
new file mode 100644
index 0000000..fef7306
--- /dev/null
+++ b/lib_base/src/main/java/com/android/base/app/dagger/ViewModelKey.java
@@ -0,0 +1,18 @@
+package com.android.base.app.dagger;
+
+import android.arch.lifecycle.ViewModel;
+
+import dagger.MapKey;
+import kotlin.annotation.AnnotationTarget;
+import kotlin.annotation.MustBeDocumented;
+import kotlin.annotation.Retention;
+import kotlin.annotation.Target;
+
+@MustBeDocumented
+@Target(allowedTargets = AnnotationTarget.FUNCTION)
+@Retention()
+@MapKey
+public @interface ViewModelKey {
+
+    Class value();
+}
\ No newline at end of file
diff --git a/lib_base/src/main/java/com/android/base/app/dagger/ViewModelModule.java b/lib_base/src/main/java/com/android/base/app/dagger/ViewModelModule.java
new file mode 100644
index 0000000..e4ea530
--- /dev/null
+++ b/lib_base/src/main/java/com/android/base/app/dagger/ViewModelModule.java
@@ -0,0 +1,22 @@
+package com.android.base.app.dagger;
+
+import android.arch.lifecycle.ViewModelProvider;
+
+import dagger.Binds;
+import dagger.Module;
+
+/**
+ * 使用 ViewModelModule 情况下,所有的 ViewModule 都由 Activity 界别容器提供,因此 Fragment 级容器无法为其 ViewModule 提供依赖。这是仅有的局限性。
+ *
+ * @author Ztiany
+ * Email: ztiany3@gmail.com
+ * Date : 2018-11-05 16:20
+ */
+@Module
+public abstract class ViewModelModule {
+
+    @Binds
+    @ActivityScope
+    abstract ViewModelProvider.Factory provideViewModelFactory(ViewModelFactory viewModelFactory);
+
+}
diff --git a/lib_base/src/main/java/com/android/base/app/dagger/ViewScope.java b/lib_base/src/main/java/com/android/base/app/dagger/ViewScope.java
new file mode 100644
index 0000000..922cb40
--- /dev/null
+++ b/lib_base/src/main/java/com/android/base/app/dagger/ViewScope.java
@@ -0,0 +1,19 @@
+package com.android.base.app.dagger;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+
+import javax.inject.Scope;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * @author Ztiany
+ *         Email: ztiany3@gmail.com
+ *         Date : 2017-05-23 09:59
+ */
+@Scope
+@Documented
+@Retention(RUNTIME)
+public @interface ViewScope {
+}
diff --git a/lib_base/src/main/java/com/android/base/app/fragment/BaseDialogFragment.java b/lib_base/src/main/java/com/android/base/app/fragment/BaseDialogFragment.java
new file mode 100644
index 0000000..b4bc83e
--- /dev/null
+++ b/lib_base/src/main/java/com/android/base/app/fragment/BaseDialogFragment.java
@@ -0,0 +1,278 @@
+package com.android.base.app.fragment;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.StringRes;
+import android.support.annotation.UiThread;
+import android.support.v7.app.AppCompatDialogFragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.base.app.BaseKit;
+import com.android.base.app.activity.BackHandlerHelper;
+import com.android.base.app.activity.OnBackPressListener;
+import com.android.base.app.ui.LoadingView;
+import com.github.dmstocking.optional.java.util.function.Predicate;
+
+import timber.log.Timber;
+
+/**
+ * 提供:
+ * 
+ *     1. RxJava 生命周期绑定。
+ *     2. 返回键监听。
+ *     3. 显示 LoadingDialog 和 Message。
+ *     4. 可以添加生命周期代理。
+ * 
+ * + * @author Ztiany + * date : 2016-03-19 23:09 + * email: 1169654504@qq.com + */ +public class BaseDialogFragment extends AppCompatDialogFragment implements LoadingView, OnBackPressListener, FragmentDelegateOwner { + + private LoadingView mLoadingViewImpl; + + private View mLayoutView; + + /* just for cache*/ + private View mCachedView; + + private final FragmentDelegates mFragmentDelegates = new FragmentDelegates(this); + + private String tag() { + return this.getClass().getSimpleName(); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + Timber.tag(tag()).d("onAttach() called with: context = [" + context + "]"); + mFragmentDelegates.onAttach(context); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Timber.tag(tag()).d("-->onCreate savedInstanceState = " + savedInstanceState); + mFragmentDelegates.onCreate(savedInstanceState); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + if (mCachedView == null) { + Object layout = provideLayout(); + if (layout == null) { + return null; + } + if (layout instanceof Integer) { + return mCachedView = inflater.inflate((Integer) layout, container, false); + } + if (layout instanceof View) { + return mCachedView = (View) layout; + } + throw new IllegalArgumentException("Here you should provide a layout id or a View"); + } + return mCachedView; + } + + /** + * 使用此方法提供的布局,将只会被缓存起来,即此方法将只会被调用一次。 + * + * @return provide a layout id or a View + */ + @Nullable + @SuppressWarnings("unused") + protected Object provideLayout() { + return null; + } + + @Override + public final void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + Timber.tag(tag()).d("-->onViewCreated savedInstanceState = " + savedInstanceState); + if (mLayoutView != view) { + mLayoutView = view; + internalOnViewPrepared(view, savedInstanceState); + onViewPrepared(view, savedInstanceState); + } + mFragmentDelegates.onViewCreated(view, savedInstanceState); + } + + void internalOnViewPrepared(@NonNull View view, @Nullable Bundle savedInstanceState) { + } + + /** + * View is prepared, If {@link android.support.v4.app.Fragment#onCreateView(LayoutInflater, ViewGroup, Bundle)} return same layout, it will be called once + * + * @param view view of fragment + */ + protected void onViewPrepared(@NonNull View view, @Nullable Bundle savedInstanceState) { + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + Timber.tag(tag()).d("-->onActivityCreated savedInstanceState = " + savedInstanceState); + mFragmentDelegates.onActivityCreated(savedInstanceState); + } + + @Override + public void onStart() { + super.onStart(); + Timber.tag(tag()).d("-->onStart"); + mFragmentDelegates.onStart(); + } + + @Override + public void onResume() { + super.onResume(); + Timber.tag(tag()).d("-->onResume"); + mFragmentDelegates.onResume(); + } + + @Override + public void onPause() { + Timber.tag(tag()).d("-->onPause"); + mFragmentDelegates.onPause(); + super.onPause(); + } + + @Override + public void onStop() { + Timber.tag(tag()).d("-->onStop"); + mFragmentDelegates.onStop(); + super.onStop(); + } + + @Override + public void onDestroyView() { + Timber.tag(tag()).d("-->onDestroyView"); + mFragmentDelegates.onDestroyView(); + super.onDestroyView(); + } + + @Override + public void onDestroy() { + Timber.tag(tag()).d("-->onDestroy"); + mFragmentDelegates.onDestroy(); + super.onDestroy(); + dismissLoadingDialog(); + } + + @Override + public void onDetach() { + Timber.tag(tag()).d("-->onDetach"); + mFragmentDelegates.onDetach(); + super.onDetach(); + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + mFragmentDelegates.onSaveInstanceState(outState); + super.onSaveInstanceState(outState); + } + + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + super.setUserVisibleHint(isVisibleToUser); + Timber.tag(tag()).d("-->setUserVisibleHint ==" + isVisibleToUser); + mFragmentDelegates.setUserVisibleHint(isVisibleToUser); + } + + @Override + public void onHiddenChanged(boolean hidden) { + super.onHiddenChanged(hidden); + Timber.tag(tag()).d("-->onHiddenChanged = " + hidden); + mFragmentDelegates.onHiddenChanged(hidden); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + mFragmentDelegates.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + mFragmentDelegates.onActivityResult(requestCode, resultCode, data); + } + + @Override + @UiThread + public final void addDelegate(FragmentDelegate fragmentDelegate) { + mFragmentDelegates.addDelegate(fragmentDelegate); + } + + @Override + @UiThread + public final boolean removeDelegate(FragmentDelegate fragmentDelegate) { + return mFragmentDelegates.removeDelegate(fragmentDelegate); + } + + @Override + public FragmentDelegate findDelegate(Predicate predicate) { + return mFragmentDelegates.findDelegate(predicate); + } + + @Override + public boolean onBackPressed() { + return handleBackPress() || BackHandlerHelper.handleBackPress(this); + } + + /** + * Fragment需要自己处理BackPress事件,如果不处理,就交给子Fragment处理。都不处理则由Activity处理 + */ + protected boolean handleBackPress() { + return false; + } + + private LoadingView getLoadingViewImpl() { + if (mLoadingViewImpl == null) { + mLoadingViewImpl = BaseKit.get().getLoadingViewFactory().createLoadingDelegate(getContext()); + } + return mLoadingViewImpl; + } + + @Override + public void showLoadingDialog() { + getLoadingViewImpl().showLoadingDialog(true); + } + + @Override + public void showLoadingDialog(boolean cancelable) { + getLoadingViewImpl().showLoadingDialog(cancelable); + } + + @Override + public void showLoadingDialog(CharSequence message, boolean cancelable) { + getLoadingViewImpl().showLoadingDialog(message, cancelable); + } + + @Override + public void showLoadingDialog(@StringRes int messageId, boolean cancelable) { + getLoadingViewImpl().showLoadingDialog(messageId, cancelable); + } + + @Override + public void dismissLoadingDialog() { + getLoadingViewImpl().dismissLoadingDialog(); + } + + @Override + public void showMessage(CharSequence message) { + getLoadingViewImpl().showMessage(message); + } + + @Override + public void showMessage(@StringRes int messageId) { + getLoadingViewImpl().showMessage(messageId); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/fragment/BaseFragment.java b/lib_base/src/main/java/com/android/base/app/fragment/BaseFragment.java new file mode 100644 index 0000000..d54170b --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/BaseFragment.java @@ -0,0 +1,289 @@ +package com.android.base.app.fragment; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.StringRes; +import android.support.annotation.UiThread; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; + +import com.android.base.app.BaseKit; +import com.android.base.app.activity.BackHandlerHelper; +import com.android.base.app.activity.OnBackPressListener; +import com.android.base.app.ui.LoadingView; +import com.github.dmstocking.optional.java.util.function.Predicate; + +import timber.log.Timber; + +/** + * 提供: + *
+ *     1. RxJava 生命周期绑定。
+ *     2. 返回键监听。
+ *     3. 显示 LoadingDialog 和 Message。
+ *     4. 可以添加生命周期代理。
+ * 
+ * + * @author Ztiany + * date : 2016-03-19 23:09 + * email: 1169654504@qq.com + */ +public class BaseFragment extends Fragment implements LoadingView, OnBackPressListener, FragmentDelegateOwner { + + private LoadingView mLoadingViewImpl; + + private View mLayoutView; + + /* just for cache*/ + private View mCachedView; + + private final FragmentDelegates mFragmentDelegates = new FragmentDelegates(this); + + private String tag() { + return this.getClass().getSimpleName(); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + Timber.tag(tag()).d("onAttach() called with: context = [" + context + "]"); + mFragmentDelegates.onAttach(context); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Timber.tag(tag()).d("-->onCreate savedInstanceState = " + savedInstanceState); + mFragmentDelegates.onCreate(savedInstanceState); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + if (mCachedView == null) { + Object layout = provideLayout(); + if (layout == null) { + return null; + } + if (layout instanceof Integer) { + return mCachedView = inflater.inflate((Integer) layout, container, false); + } + if (layout instanceof View) { + return mCachedView = (View) layout; + } + throw new IllegalArgumentException("Here you should provide a layout id or a View"); + } + + Timber.tag(tag()).d("mCachedView.parent: " + mCachedView.getParent()); + + if (mCachedView.getParent() != null) { + ViewParent parent = mCachedView.getParent(); + if (parent instanceof ViewGroup) { + ((ViewGroup) parent).removeView(mCachedView); + } + } + + return mCachedView; + } + + /** + * 使用此方法提供的布局,将只会被缓存起来,即此方法将只会被调用一次。 + * + * @return provide a layout id or a View + */ + @Nullable + @SuppressWarnings("unused") + protected Object provideLayout() { + return null; + } + + @Override + public final void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + Timber.tag(tag()).d("-->onViewCreated savedInstanceState = " + savedInstanceState); + if (mLayoutView != view) { + mLayoutView = view; + internalOnViewPrepared(view, savedInstanceState); + onViewPrepared(view, savedInstanceState); + } + mFragmentDelegates.onViewCreated(view, savedInstanceState); + } + + void internalOnViewPrepared(@NonNull View view, @Nullable Bundle savedInstanceState) { + } + + /** + * View is prepared, If {@link android.support.v4.app.Fragment#onCreateView(LayoutInflater, ViewGroup, Bundle)} return same layout, it will be called once + * + * @param view view of fragment + */ + protected void onViewPrepared(@NonNull View view, @Nullable Bundle savedInstanceState) { + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + Timber.tag(tag()).d("-->onActivityCreated savedInstanceState = " + savedInstanceState); + mFragmentDelegates.onActivityCreated(savedInstanceState); + } + + @Override + public void onStart() { + super.onStart(); + Timber.tag(tag()).d("-->onStart"); + mFragmentDelegates.onStart(); + } + + @Override + public void onResume() { + super.onResume(); + Timber.tag(tag()).d("-->onResume"); + mFragmentDelegates.onResume(); + } + + @Override + public void onPause() { + Timber.tag(tag()).d("-->onPause"); + mFragmentDelegates.onPause(); + super.onPause(); + } + + @Override + public void onStop() { + Timber.tag(tag()).d("-->onStop"); + mFragmentDelegates.onStop(); + super.onStop(); + } + + @Override + public void onDestroyView() { + Timber.tag(tag()).d("-->onDestroyView"); + mFragmentDelegates.onDestroyView(); + super.onDestroyView(); + } + + @Override + public void onDestroy() { + Timber.tag(tag()).d("-->onDestroy"); + mFragmentDelegates.onDestroy(); + super.onDestroy(); + dismissLoadingDialog(); + } + + @Override + public void onDetach() { + Timber.tag(tag()).d("-->onDetach"); + mFragmentDelegates.onDetach(); + super.onDetach(); + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + mFragmentDelegates.onSaveInstanceState(outState); + super.onSaveInstanceState(outState); + } + + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + super.setUserVisibleHint(isVisibleToUser); + Timber.tag(tag()).d("-->setUserVisibleHint ==" + isVisibleToUser); + mFragmentDelegates.setUserVisibleHint(isVisibleToUser); + } + + @Override + public void onHiddenChanged(boolean hidden) { + super.onHiddenChanged(hidden); + Timber.tag(tag()).d("-->onHiddenChanged = " + hidden); + mFragmentDelegates.onHiddenChanged(hidden); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + mFragmentDelegates.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + mFragmentDelegates.onActivityResult(requestCode, resultCode, data); + } + + @Override + @UiThread + public final void addDelegate(FragmentDelegate fragmentDelegate) { + mFragmentDelegates.addDelegate(fragmentDelegate); + } + + @Override + @UiThread + public final boolean removeDelegate(FragmentDelegate fragmentDelegate) { + return mFragmentDelegates.removeDelegate(fragmentDelegate); + } + + @Override + public FragmentDelegate findDelegate(Predicate predicate) { + return mFragmentDelegates.findDelegate(predicate); + } + + @Override + public boolean onBackPressed() { + return handleBackPress() || BackHandlerHelper.handleBackPress(this); + } + + /** + * Fragment需要自己处理BackPress事件,如果不处理,就交给子Fragment处理。都不处理则由Activity处理 + */ + protected boolean handleBackPress() { + return false; + } + + private LoadingView getLoadingViewImpl() { + if (mLoadingViewImpl == null) { + mLoadingViewImpl = BaseKit.get().getLoadingViewFactory().createLoadingDelegate(getContext()); + } + return mLoadingViewImpl; + } + + @Override + public void showLoadingDialog() { + getLoadingViewImpl().showLoadingDialog(true); + } + + @Override + public void showLoadingDialog(boolean cancelable) { + getLoadingViewImpl().showLoadingDialog(cancelable); + } + + @Override + public void showLoadingDialog(CharSequence message, boolean cancelable) { + getLoadingViewImpl().showLoadingDialog(message, cancelable); + } + + @Override + public void showLoadingDialog(@StringRes int messageId, boolean cancelable) { + getLoadingViewImpl().showLoadingDialog(messageId, cancelable); + } + + @Override + public void dismissLoadingDialog() { + getLoadingViewImpl().dismissLoadingDialog(); + } + + @Override + public void showMessage(CharSequence message) { + getLoadingViewImpl().showMessage(message); + } + + @Override + public void showMessage(@StringRes int messageId) { + getLoadingViewImpl().showMessage(messageId); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/fragment/BaseListFragment.java b/lib_base/src/main/java/com/android/base/app/fragment/BaseListFragment.java new file mode 100644 index 0000000..1658b14 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/BaseListFragment.java @@ -0,0 +1,150 @@ +package com.android.base.app.fragment; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.widget.RecyclerView; + +import com.android.base.adapter.DataManager; +import com.android.base.app.ui.AutoPageNumber; +import com.android.base.app.ui.PageNumber; +import com.android.base.app.ui.RefreshListLayout; +import com.ztiany.loadmore.adapter.ILoadMore; +import com.ztiany.loadmore.adapter.OnLoadMoreListener; +import com.ztiany.loadmore.adapter.WrapperAdapter; + +import java.util.List; + +/** + * 通用的RecyclerView列表界面:支持下拉刷新和加载更多。 + * + * @param 当前列表使用的数据类型 + * @author Ztiany + * date : 2016-03-19 23:09 + * email: 1169654504@qq.com + */ +public abstract class BaseListFragment extends BaseStateFragment implements RefreshListLayout { + + /** + * 加载更多 + */ + private ILoadMore mLoadMore; + + /** + * 列表数据管理 + */ + private DataManager mDataManager; + + /** + * 分页页码 + */ + private PageNumber mPageNumber; + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + if (mDataManager == null) { + throw new NullPointerException("you need set DataManager"); + } + } + + protected final void setDataManager(@NonNull DataManager dataManager) { + mDataManager = dataManager; + } + + /** + * Default PageNumber is {@link AutoPageNumber} + * + * @param recyclerAdapter adapter + * @return recycler adapter wrapper + */ + protected final RecyclerView.Adapter setupLoadMore(@NonNull RecyclerView.Adapter recyclerAdapter) { + if (mDataManager == null) { + throw new IllegalStateException("you should setup a DataManager before call this method"); + } + return setupLoadMore(recyclerAdapter, new AutoPageNumber(this, mDataManager)); + } + + protected final RecyclerView.Adapter setupLoadMore(@NonNull RecyclerView.Adapter recyclerAdapter, @NonNull PageNumber pageNumber) { + mPageNumber = pageNumber; + + WrapperAdapter wrap = WrapperAdapter.wrap(recyclerAdapter); + mLoadMore = wrap; + mLoadMore.setOnLoadMoreListener(new OnLoadMoreListener() { + @Override + public void onLoadMore() { + BaseListFragment.this.onLoadMore(); + } + + @Override + public boolean canLoadMore() { + return !isRefreshing(); + } + }); + return wrap; + } + + @Override + protected void onRefresh() { + onStartLoad(); + } + + protected void onLoadMore() { + onStartLoad(); + } + + @Override + final boolean canRefresh() { + return !isLoadingMore(); + } + + /** + * call by {@link #onRefresh()} or {@link #onLoadMore()}, you can get current loading type from {@link #isRefreshing()} or {@link #isLoadingMore()}. + */ + protected void onStartLoad() { + } + + @Override + public void replaceData(List data) { + mDataManager.replaceAll(data); + } + + @Override + public void addData(List data) { + mDataManager.addItems(data); + } + + protected final ILoadMore getLoadMoreController() { + return mLoadMore; + } + + @Override + public PageNumber getPager() { + return mPageNumber; + } + + @Override + public boolean isEmpty() { + return mDataManager.isEmpty(); + } + + @Override + public boolean isLoadingMore() { + return mLoadMore != null && mLoadMore.isLoadingMore(); + } + + @Override + public void loadMoreCompleted(boolean hasMore) { + if (mLoadMore != null) { + mLoadMore.loadCompleted(hasMore); + } + } + + @Override + public void loadMoreFailed() { + if (mLoadMore != null) { + mLoadMore.loadFail(); + } + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/fragment/BaseListV2Fragment.kt b/lib_base/src/main/java/com/android/base/app/fragment/BaseListV2Fragment.kt new file mode 100644 index 0000000..aecb072 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/BaseListV2Fragment.kt @@ -0,0 +1,102 @@ +package com.android.base.app.fragment + +import android.os.Bundle +import android.view.View +import com.android.base.adapter.DataManager +import com.android.base.app.ui.AutoPageNumber +import com.android.base.app.ui.RefreshListLayout +import com.android.base.app.ui.StateLayoutConfig + +/** + * 区别于 [BaseListFragment] 只能支持 RecyclerView。[BaseListFragment] 采用包装 [android.support.v7.widget.RecyclerView.Adapter] 的方式, + * 在底部添加 load more view 的 item,来实现加载更多。BaseListV2Fragment 没有采用此种方式,所以你使用的刷新视图应该是支持这下来刷新和加载更多功能的。 + * + * 在调用BaseListV2Fragment [onActivityCreated] 之前,你应该设置好 [dataManager]。 + * + *@author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-03-26 15:06 + */ +abstract class BaseListV2Fragment : BaseFragment(), RefreshListLayout { + + companion object { + protected const val CONTENT = StateLayoutConfig.CONTENT + protected const val LOADING = StateLayoutConfig.LOADING + protected const val ERROR = StateLayoutConfig.ERROR + protected const val EMPTY = StateLayoutConfig.EMPTY + protected const val NET_ERROR = StateLayoutConfig.NET_ERROR + protected const val SERVER_ERROR = StateLayoutConfig.SERVER_ERROR + } + + private lateinit var stateLayout: RefreshLoadMoreStateLayoutImpl + protected open lateinit var dataManager: DataManager + + internal override fun internalOnViewPrepared(view: View, savedInstanceState: Bundle?) { + stateLayout = RefreshLoadMoreStateLayoutImpl.init(view) + stateLayout.refreshView.setRefreshHandler { + onRefresh() + } + stateLayout.refreshView.setLoadMoreHandler { + onLoadMore() + } + stateLayout.setStateRetryListener(this::onRetry) + } + + protected open fun onRetry(state: Int) { + if (!isRefreshing) { + autoRefresh() + } + } + + protected open fun onRefresh() = onStartLoad() + + protected open fun onLoadMore() = onStartLoad() + + /** + * call by [onRefresh] or [onLoadMore], you can get current loading type from [isRefreshing] or [isLoadingMore]. + */ + protected open fun onStartLoad() {} + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + if (::dataManager.isInitialized) { + throw NullPointerException("you need set DataManager") + } + } + + override fun onDestroyView() { + super.onDestroyView() + refreshCompleted() + } + + override fun replaceData(data: MutableList?) { + dataManager.replaceAll(data) + } + + override fun addData(data: MutableList?) { + dataManager.addItems(data) + } + + fun setRefreshEnable(enable: Boolean) = stateLayout.refreshView.setRefreshEnable(enable) + + fun setLoadMoreEnable(enable: Boolean) = stateLayout.refreshView.setLoadMoreEnable(enable) + + override fun getPager() = AutoPageNumber(this, dataManager) + override fun isEmpty() = dataManager.isEmpty + override fun loadMoreCompleted(hasMore: Boolean) = stateLayout.refreshView.loadMoreCompleted(hasMore) + override fun loadMoreFailed() = stateLayout.refreshView.loadMoreFailed() + override fun isRefreshing() = stateLayout.refreshView.isRefreshing + override fun showContentLayout() = stateLayout.showContentLayout() + override fun showLoadingLayout() = stateLayout.showLoadingLayout() + override fun refreshCompleted() = stateLayout.refreshView.refreshCompleted() + override fun showEmptyLayout() = stateLayout.showEmptyLayout() + override fun showErrorLayout() = stateLayout.showErrorLayout() + override fun showRequesting() = stateLayout.showRequesting() + override fun showBlank() = stateLayout.showBlank() + override fun getStateLayoutConfig() = stateLayout.stateLayoutConfig + override fun autoRefresh() = stateLayout.refreshView.autoRefresh() + override fun showNetErrorLayout() = stateLayout.showNetErrorLayout() + override fun showServerErrorLayout() = stateLayout.showServerErrorLayout() + override fun isLoadingMore() = stateLayout.refreshView.isLoadingMore + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/fragment/BaseStateDialogFragment.java b/lib_base/src/main/java/com/android/base/app/fragment/BaseStateDialogFragment.java new file mode 100644 index 0000000..136719d --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/BaseStateDialogFragment.java @@ -0,0 +1,145 @@ +package com.android.base.app.fragment; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.View; + +import com.android.base.app.ui.RefreshStateLayout; +import com.android.base.app.ui.RefreshView; +import com.android.base.app.ui.StateLayoutConfig; + +/** + * @author Ztiany + * date : 2016-03-19 23:09 + * email: 1169654504@qq.com + * @see BaseStateFragment + */ +@SuppressWarnings("unused") +public abstract class BaseStateDialogFragment extends BaseDialogFragment implements RefreshStateLayout { + + private RefreshableStateLayoutImpl mStateLayout; + + protected static final int CONTENT = StateLayoutConfig.CONTENT; + protected static final int LOADING = StateLayoutConfig.LOADING; + protected static final int ERROR = StateLayoutConfig.ERROR; + protected static final int EMPTY = StateLayoutConfig.EMPTY; + protected static final int NET_ERROR = StateLayoutConfig.NET_ERROR; + protected static final int SERVER_ERROR = StateLayoutConfig.SERVER_ERROR; + + @Override + void internalOnViewPrepared(@NonNull View view, @Nullable Bundle savedInstanceState) { + mStateLayout = RefreshableStateLayoutImpl.init(view); + mStateLayout.setRefreshHandler(new RefreshView.RefreshHandler() { + @Override + public void onRefresh() { + BaseStateDialogFragment.this.onRefresh(); + } + + @Override + public boolean canRefresh() { + return BaseStateDialogFragment.this.canRefresh(); + } + }); + + mStateLayout.setStateRetryListenerUnchecked(this::onRetry); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + refreshCompleted(); + } + + boolean canRefresh() { + return true; + } + + final RefreshView getRefreshView() { + return mStateLayout.getRefreshView(); + } + + protected void onRetry(int state) { + if (getRefreshView() != null) { + if (!getRefreshView().isRefreshing()) { + autoRefresh(); + } + } else { + onRefresh(); + } + } + + public final void setRefreshEnable(boolean enable) { + if (getRefreshView() != null) { + getRefreshView().setRefreshEnable(enable); + } + } + + protected void onRefresh() { + } + + @Override + public final StateLayoutConfig getStateLayoutConfig() { + return mStateLayout.getStateLayoutConfig(); + } + + private RefreshStateLayout getStateLayout() { + return mStateLayout; + } + + @Override + public final boolean isRefreshing() { + return mStateLayout.isRefreshing(); + } + + @Override + public void refreshCompleted() { + getStateLayout().refreshCompleted(); + } + + @Override + public void autoRefresh() { + getStateLayout().autoRefresh(); + } + + @Override + public void showContentLayout() { + getStateLayout().showContentLayout(); + } + + @Override + public void showLoadingLayout() { + getStateLayout().showLoadingLayout(); + } + + @Override + public void showEmptyLayout() { + getStateLayout().showEmptyLayout(); + } + + @Override + public void showErrorLayout() { + getStateLayout().showErrorLayout(); + } + + @Override + public void showRequesting() { + getStateLayout().showRequesting(); + } + + @Override + public void showBlank() { + getStateLayout().showBlank(); + } + + @Override + public void showNetErrorLayout() { + getStateLayout().showNetErrorLayout(); + } + + @Override + public void showServerErrorLayout() { + getStateLayout().showServerErrorLayout(); + } + +} diff --git a/lib_base/src/main/java/com/android/base/app/fragment/BaseStateFragment.java b/lib_base/src/main/java/com/android/base/app/fragment/BaseStateFragment.java new file mode 100644 index 0000000..e9b2885 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/BaseStateFragment.java @@ -0,0 +1,152 @@ +package com.android.base.app.fragment; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.View; + +import com.android.base.app.ui.RefreshStateLayout; +import com.android.base.app.ui.RefreshView; +import com.android.base.app.ui.StateLayoutConfig; + +/** + *
+ *          1: 支持显示{CONTENT、LOADING、ERROR、EMPTY}四种布局、支持下拉刷新
+ *          2: 使用的布局中必须有一个id = R.id.base_status_layout的Layout,切改Layout实现了StateLayout
+ *          3: RefreshView(下拉刷新)的id必须设置为 :R.id.refresh_layout,没有添加则表示不需要下拉刷新功能
+ *          4: 默认所有重试和下拉刷新都会调用{@link #onRefresh()},子类可以修改该行为
+ * 
+ * + * @author Ztiany + * date : 2016-03-19 23:09 + * email: 1169654504@qq.com + */ +@SuppressWarnings("unused") +public abstract class BaseStateFragment extends BaseFragment implements RefreshStateLayout { + + private RefreshableStateLayoutImpl mStateLayout; + + protected static final int CONTENT = StateLayoutConfig.CONTENT; + protected static final int LOADING = StateLayoutConfig.LOADING; + protected static final int ERROR = StateLayoutConfig.ERROR; + protected static final int EMPTY = StateLayoutConfig.EMPTY; + protected static final int NET_ERROR = StateLayoutConfig.NET_ERROR; + protected static final int SERVER_ERROR = StateLayoutConfig.SERVER_ERROR; + + @Override + void internalOnViewPrepared(@NonNull View view, @Nullable Bundle savedInstanceState) { + mStateLayout = RefreshableStateLayoutImpl.init(view); + mStateLayout.setRefreshHandler(new RefreshView.RefreshHandler() { + @Override + public void onRefresh() { + BaseStateFragment.this.onRefresh(); + } + + @Override + public boolean canRefresh() { + return BaseStateFragment.this.canRefresh(); + } + }); + + mStateLayout.setStateRetryListenerUnchecked(this::onRetry); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + refreshCompleted(); + } + + boolean canRefresh() { + return true; + } + + final RefreshView getRefreshView() { + return mStateLayout.getRefreshView(); + } + + protected void onRetry(int state) { + if (getRefreshView() != null) { + if (!getRefreshView().isRefreshing()) { + autoRefresh(); + } + } else { + onRefresh(); + } + } + + protected void onRefresh() { + } + + public final void setRefreshEnable(boolean enable) { + if (getRefreshView() != null) { + getRefreshView().setRefreshEnable(enable); + } + } + + @Override + @NonNull + public final StateLayoutConfig getStateLayoutConfig() { + return mStateLayout.getStateLayoutConfig(); + } + + private RefreshStateLayout getStateLayout() { + return mStateLayout; + } + + @Override + public final boolean isRefreshing() { + return mStateLayout.isRefreshing(); + } + + @Override + public void refreshCompleted() { + getStateLayout().refreshCompleted(); + } + + @Override + public void autoRefresh() { + getStateLayout().autoRefresh(); + } + + @Override + public void showContentLayout() { + getStateLayout().showContentLayout(); + } + + @Override + public void showLoadingLayout() { + getStateLayout().showLoadingLayout(); + } + + @Override + public void showEmptyLayout() { + getStateLayout().showEmptyLayout(); + } + + @Override + public void showErrorLayout() { + getStateLayout().showErrorLayout(); + } + + @Override + public void showRequesting() { + getStateLayout().showRequesting(); + } + + @Override + public void showBlank() { + getStateLayout().showBlank(); + } + + @Override + public void showNetErrorLayout() { + getStateLayout().showNetErrorLayout(); + } + + @Override + public void showServerErrorLayout() { + getStateLayout().showServerErrorLayout(); + } + +} diff --git a/lib_base/src/main/java/com/android/base/app/fragment/DelegateEx.kt b/lib_base/src/main/java/com/android/base/app/fragment/DelegateEx.kt new file mode 100644 index 0000000..043e42d --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/DelegateEx.kt @@ -0,0 +1,26 @@ +package com.android.base.app.fragment + +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +/** + * 懒加载代理 + * + *@author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-03-08 12:50 + */ +class LazyLoad(private val onPrepared: (() -> Unit)) : ReadOnlyProperty { + + private lateinit var lazyDelegate: LazyDelegate + + override fun getValue(thisRef: BaseFragment, property: KProperty<*>): LazyDelegate { + if (!::lazyDelegate.isInitialized) { + lazyDelegate = LazyDelegate.attach(thisRef) { + onPrepared.invoke() + } + } + return lazyDelegate + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/fragment/FragmentConfig.java b/lib_base/src/main/java/com/android/base/app/fragment/FragmentConfig.java new file mode 100644 index 0000000..5ccac3c --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/FragmentConfig.java @@ -0,0 +1,24 @@ +package com.android.base.app.fragment; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-03-05 15:25 + */ +public class FragmentConfig { + + private static final int INVALIDATE_ID = -1; + private static int sDefaultContainerId = INVALIDATE_ID; + + public static void setDefaultContainerId(int defaultContainerId) { + sDefaultContainerId = defaultContainerId; + } + + public static int defaultContainerId() { + if (sDefaultContainerId == INVALIDATE_ID) { + throw new IllegalStateException("sDefaultContainerId has not set"); + } + return sDefaultContainerId; + } + +} diff --git a/lib_base/src/main/java/com/android/base/app/fragment/FragmentDelegate.java b/lib_base/src/main/java/com/android/base/app/fragment/FragmentDelegate.java new file mode 100644 index 0000000..e4d00ae --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/FragmentDelegate.java @@ -0,0 +1,73 @@ +package com.android.base.app.fragment; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.view.View; + +public interface FragmentDelegate { + + /** + * 当该Delegate被添加到Fragment中 + */ + default void onAttachToFragment(T fragment) { + } + + /** + * 调用此方法时,清除Fragment的引用 + */ + default void onDetachFromFragment() { + } + + default void onAttach(Context context) { + } + + default void onCreate(@Nullable Bundle savedInstanceState) { + } + + default void onActivityCreated(@Nullable Bundle savedInstanceState) { + } + + default void onSaveInstanceState(Bundle savedInstanceState) { + } + + default void onViewCreated(@NonNull View view, Bundle savedInstanceState) { + } + + default void onStart() { + } + + default void onResume() { + } + + default void onPause() { + } + + default void onStop() { + } + + default void onDestroy() { + } + + default void onDestroyView() { + } + + default void onDetach() { + } + + default void setUserVisibleHint(boolean isVisibleToUser) { + } + + default void onHiddenChanged(boolean hidden) { + } + + default void onActivityResult(int requestCode, int resultCode, Intent data) { + } + + default void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + } + +} diff --git a/lib_base/src/main/java/com/android/base/app/fragment/FragmentDelegateOwner.java b/lib_base/src/main/java/com/android/base/app/fragment/FragmentDelegateOwner.java new file mode 100644 index 0000000..0aca2a8 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/FragmentDelegateOwner.java @@ -0,0 +1,16 @@ +package com.android.base.app.fragment; + +import android.support.annotation.UiThread; + +import com.github.dmstocking.optional.java.util.function.Predicate; + +@UiThread +public interface FragmentDelegateOwner { + + void addDelegate(FragmentDelegate fragmentDelegate); + + boolean removeDelegate(FragmentDelegate fragmentDelegate); + + FragmentDelegate findDelegate(Predicate predicate); + +} diff --git a/lib_base/src/main/java/com/android/base/app/fragment/FragmentDelegates.java b/lib_base/src/main/java/com/android/base/app/fragment/FragmentDelegates.java new file mode 100644 index 0000000..9de2fd1 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/FragmentDelegates.java @@ -0,0 +1,166 @@ +package com.android.base.app.fragment; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; +import android.support.v4.app.Fragment; +import android.view.View; + +import com.github.dmstocking.optional.java.util.function.Predicate; + +import java.util.ArrayList; +import java.util.List; + + +@UiThread +final class FragmentDelegates implements FragmentDelegate, FragmentDelegateOwner { + + private final Fragment mDelegateOwner; + private List mDelegates = new ArrayList<>(4); + + FragmentDelegates(T delegateOwner) { + mDelegateOwner = delegateOwner; + } + + @Override + public void onAttach(Context context) { + for (FragmentDelegate fragmentDelegate : mDelegates) { + fragmentDelegate.onAttach(context); + } + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + for (FragmentDelegate fragmentDelegate : mDelegates) { + fragmentDelegate.onCreate(savedInstanceState); + } + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + for (FragmentDelegate fragmentDelegate : mDelegates) { + fragmentDelegate.onViewCreated(view, savedInstanceState); + } + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + for (FragmentDelegate fragmentDelegate : mDelegates) { + fragmentDelegate.onActivityCreated(savedInstanceState); + } + } + + @Override + public void onStart() { + for (FragmentDelegate fragmentDelegate : mDelegates) { + fragmentDelegate.onStart(); + } + } + + @Override + public void onResume() { + for (FragmentDelegate fragmentDelegate : mDelegates) { + fragmentDelegate.onResume(); + } + } + + @Override + public void onPause() { + for (FragmentDelegate fragmentDelegate : mDelegates) { + fragmentDelegate.onPause(); + } + } + + @Override + public void onStop() { + for (FragmentDelegate fragmentDelegate : mDelegates) { + fragmentDelegate.onStop(); + } + } + + @Override + public void onDestroy() { + for (FragmentDelegate fragmentDelegate : mDelegates) { + fragmentDelegate.onDestroy(); + } + } + + @Override + public void onDestroyView() { + for (FragmentDelegate fragmentDelegate : mDelegates) { + fragmentDelegate.onDestroyView(); + } + } + + @Override + public void onDetach() { + for (FragmentDelegate fragmentDelegate : mDelegates) { + fragmentDelegate.onDetach(); + } + } + + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + for (FragmentDelegate fragmentDelegate : mDelegates) { + fragmentDelegate.setUserVisibleHint(isVisibleToUser); + } + } + + @Override + public void onSaveInstanceState(Bundle savedInstanceState) { + for (FragmentDelegate fragmentDelegate : mDelegates) { + fragmentDelegate.onSaveInstanceState(savedInstanceState); + } + } + + @Override + public void onHiddenChanged(boolean hidden) { + for (FragmentDelegate fragmentDelegate : mDelegates) { + fragmentDelegate.onHiddenChanged(hidden); + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + for (FragmentDelegate fragmentDelegate : mDelegates) { + fragmentDelegate.onActivityResult(requestCode, resultCode, data); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + for (FragmentDelegate fragmentDelegate : mDelegates) { + fragmentDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } + + @Override + @SuppressWarnings("unchecked") + public void addDelegate(FragmentDelegate fragmentDelegate) { + mDelegates.add(fragmentDelegate); + fragmentDelegate.onAttachToFragment(mDelegateOwner); + } + + @Override + public boolean removeDelegate(FragmentDelegate fragmentDelegate) { + boolean remove = mDelegates.remove(fragmentDelegate); + if (remove) { + fragmentDelegate.onDetachFromFragment(); + } + return remove; + } + + @Override + public FragmentDelegate findDelegate(Predicate predicate) { + for (FragmentDelegate delegate : mDelegates) { + if (predicate.test(delegate)) { + return delegate; + } + } + return null; + } + +} diff --git a/lib_base/src/main/java/com/android/base/app/fragment/FragmentInfo.java b/lib_base/src/main/java/com/android/base/app/fragment/FragmentInfo.java new file mode 100644 index 0000000..935f581 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/FragmentInfo.java @@ -0,0 +1,132 @@ +package com.android.base.app.fragment; + +import android.content.Context; +import android.os.Bundle; +import android.support.v4.app.Fragment; + +import java.lang.ref.WeakReference; + +@SuppressWarnings("WeakerAccess,unused") +public class FragmentInfo { + + private final int mPageId; + private final String mTag; + private final Class mClazz; + private final int mTitleId; + private final Bundle mArguments; + private final boolean mIsToStack; + private final String mStackName; + private WeakReference mFragment; + + private FragmentInfo(int pageId, String tag, Class clazz, int titleId, Bundle arguments, boolean toStack, String stackName) { + mPageId = pageId; + mTag = tag; + mClazz = clazz; + mTitleId = titleId; + mArguments = arguments; + mIsToStack = toStack; + mStackName = stackName; + } + + public Fragment getInstance() { + return mFragment == null ? null : mFragment.get(); + } + + public void setInstance(Fragment fragment) { + mFragment = new WeakReference<>(fragment); + } + + public Fragment newFragment(Context context) { + return Fragment.instantiate(context, mClazz.getName(), mArguments); + } + + public boolean isToStack() { + return mIsToStack; + } + + public String getStackName() { + return mStackName; + } + + public Bundle getArguments() { + return mArguments; + } + + public int getTitleId() { + return mTitleId; + } + + public int getPageId() { + return mPageId; + } + + public String getTag() { + return mTag; + } + + public Class getClazz() { + return mClazz; + } + + public static PageBuilder builder() { + return new PageBuilder(); + } + + + public static class PageBuilder { + + private int mPagerId; + private String mTag; + private Class mClazz; + private int mTitleId; + private Bundle mArguments; + private boolean mIsToStack; + private String mStackName; + + public FragmentInfo build() { + return new FragmentInfo(mPagerId, mTag, mClazz, mTitleId, mArguments, mIsToStack, mStackName); + } + + public PageBuilder pagerId(int pagerId) { + mPagerId = pagerId; + return this; + } + + public PageBuilder tag(String tag) { + this.mTag = tag; + return this; + } + + public PageBuilder clazz(Class clazz) { + mClazz = clazz; + return this; + } + + public PageBuilder titleId(int titleId) { + mTitleId = titleId; + return this; + } + + public PageBuilder arguments(Bundle arguments) { + mArguments = arguments; + return this; + } + + public PageBuilder toStack(boolean toStack) { + this.mIsToStack = toStack; + return this; + } + + /** + * 如果需要加入到Stack,建议加上StackName。 + * + * @param stackName StackName + * @return PageBuilder + */ + public PageBuilder stackName(String stackName) { + mStackName = stackName; + return this; + } + } + +} diff --git a/lib_base/src/main/java/com/android/base/app/fragment/Fragments.kt b/lib_base/src/main/java/com/android/base/app/fragment/Fragments.kt new file mode 100644 index 0000000..d0c8b8f --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/Fragments.kt @@ -0,0 +1,463 @@ +@file:JvmName("Fragments") + +package com.android.base.app.fragment + + +import android.support.annotation.NonNull +import android.support.v4.app.Fragment +import android.support.v4.app.FragmentActivity +import android.support.v4.app.FragmentManager +import android.support.v4.app.FragmentTransaction +import android.view.View +import com.android.base.app.activity.ActivityDelegate +import com.android.base.app.activity.ActivityDelegateOwner +import com.android.base.app.activity.ActivityStatus +import com.android.base.kotlin.javaClassName +import kotlin.reflect.KClass + +@JvmOverloads +fun Fragment.exitFragment(immediate: Boolean = false) { + activity.exitFragment(immediate) +} + +@JvmOverloads +fun FragmentActivity?.exitFragment(immediate: Boolean = false) { + if (this == null) { + return + } + val supportFragmentManager = this.supportFragmentManager + val backStackEntryCount = supportFragmentManager.backStackEntryCount + if (backStackEntryCount > 0) { + if (immediate) { + supportFragmentManager.popBackStackImmediate() + } else { + supportFragmentManager.popBackStack() + } + } else { + this.supportFinishAfterTransition() + } +} + +/** + * @param clazz the interface container must implemented + * @param Type + * @return the interface context must implemented + */ +fun Fragment.requireContainerImplement(clazz: Class): T? { + if (clazz.isInstance(parentFragment)) { + return clazz.cast(parentFragment) + } + return if (clazz.isInstance(activity)) { + clazz.cast(activity) + } else { + throw RuntimeException("use this Fragment:$this, Activity or Fragment must impl interface :$clazz") + } +} + +/** + * @param clazz the interface context must implemented + * @param Type + * @return the interface context must implemented + */ +fun Fragment.requireContextImplement(clazz: Class): T? { + return if (!clazz.isInstance(activity)) { + throw RuntimeException("use this Fragment:$this, Activity must impl interface :$clazz") + } else { + clazz.cast(activity) + } +} + +/** + * @param clazz the interface parent must implemented + * @param Type + * @return the interface context must implemented + */ +fun Fragment.requireParentImplement(clazz: Class): T? { + return if (!clazz.isInstance(parentFragment)) { + throw RuntimeException("use this Fragment:$this, ParentFragment must impl interface :$clazz") + } else { + clazz.cast(parentFragment) + } +} + +/** 使用 [clazz] 的全限定类名作为 tag 查找 Fragment */ +fun FragmentManager.findFragmentByTag(clazz: KClass): T? { + @Suppress("UNCHECKED_CAST") + return findFragmentByTag(clazz.java.name) as? T +} + +fun FragmentManager.popBackTo(flag: String, immediate: Boolean = false) { + if (immediate) { + popBackStackImmediate(flag, FragmentManager.POP_BACK_STACK_INCLUSIVE) + } else { + popBackStack(flag, FragmentManager.POP_BACK_STACK_INCLUSIVE) + } +} + +fun FragmentManager.clearBackStack(immediate: Boolean = false) { + if (immediate) { + this.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + } else { + this.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + } +} + +private fun FragmentManager.isFragmentInStack(clazz: Class): Boolean { + val backStackEntryCount = backStackEntryCount + if (backStackEntryCount == 0) { + return false + } + for (i in 0 until backStackEntryCount) { + if (clazz.name == getBackStackEntryAt(i).name) { + return true + } + } + return false +} + +inline fun FragmentManager.inTransaction(func: EnhanceFragmentTransaction.() -> Unit) { + val fragmentTransaction = beginTransaction() + EnhanceFragmentTransaction(this, fragmentTransaction).func() + fragmentTransaction.commit() +} + +inline fun FragmentActivity.inFragmentTransaction(func: EnhanceFragmentTransaction.() -> Unit) { + val transaction = supportFragmentManager.beginTransaction() + EnhanceFragmentTransaction(supportFragmentManager, transaction).func() + transaction.commit() +} + +fun T.inSafelyFragmentTransaction( + func: EnhanceFragmentTransaction.() -> Unit +): Boolean where T : FragmentActivity, T : ActivityDelegateOwner { + + var delegate = findDelegate { + it is SafelyFragmentTransactionActivityDelegate + }as? SafelyFragmentTransactionActivityDelegate + + if (delegate == null) { + delegate = SafelyFragmentTransactionActivityDelegate() + addDelegate(delegate) + } + + val transaction = supportFragmentManager.beginTransaction() + + EnhanceFragmentTransaction(supportFragmentManager, transaction).func() + + return delegate.safeCommit(this, transaction) +} + +inline fun Fragment.inChildFragmentTransaction(func: EnhanceFragmentTransaction.() -> Unit) { + val transaction = childFragmentManager.beginTransaction() + EnhanceFragmentTransaction(childFragmentManager, transaction).func() + transaction.commit() +} + +fun T.inSafelyChildFragmentTransaction( + func: EnhanceFragmentTransaction.() -> Unit +): Boolean where T : Fragment, T : FragmentDelegateOwner { + + var delegate: SafelyFragmentTransactionFragmentDelegate? = findDelegate { + it is SafelyFragmentTransactionFragmentDelegate + } as? SafelyFragmentTransactionFragmentDelegate + + if (delegate == null) { + delegate = SafelyFragmentTransactionFragmentDelegate() + addDelegate(delegate) + } + + val transaction = childFragmentManager.beginTransaction() + + EnhanceFragmentTransaction(childFragmentManager, transaction).func() + + return delegate.safeCommit(this, transaction) +} + +private class SafelyFragmentTransactionActivityDelegate : ActivityDelegate { + + private val mPendingTransactions = mutableListOf() + + fun safeCommit(@NonNull activityDelegateOwner: ActivityDelegateOwner, @NonNull transaction: FragmentTransaction): Boolean { + val status = activityDelegateOwner.status + val isCommitterResumed = (status == ActivityStatus.CREATE || status == ActivityStatus.START || status == ActivityStatus.RESUME) + + return if (isCommitterResumed) { + transaction.commit() + false + } else { + mPendingTransactions.add(transaction) + true + } + } + + override fun onResumeFragments() { + if (mPendingTransactions.isNotEmpty()) { + mPendingTransactions.forEach { it.commit() } + mPendingTransactions.clear() + } + } + +} + +private class SafelyFragmentTransactionFragmentDelegate : FragmentDelegate { + + private val mPendingTransactions = mutableListOf() + + fun safeCommit(@NonNull fragment: Fragment, @NonNull transaction: FragmentTransaction): Boolean { + return if (fragment.isResumed) { + transaction.commit() + false + } else { + mPendingTransactions.add(transaction) + true + } + } + + override fun onResume() { + if (!mPendingTransactions.isEmpty()) { + mPendingTransactions.forEach { it.commit() } + mPendingTransactions.clear() + } + } + +} + +class EnhanceFragmentTransaction constructor( + private val fragmentManager: FragmentManager, + private val fragmentTransaction: FragmentTransaction +) : FragmentTransaction() { + + //------------------------------------------------------------------------------------------------ + // extra functions + //------------------------------------------------------------------------------------------------ + + /** + * 把 [fragment] 添加到回退栈中,并 hide 其他 fragment, + * 如果 [containerId]==0,则使用 [com.android.base.app.BaseKit.setDefaultFragmentContainerId] 中配置的 id, + * 如果 [tag] ==null 则使用 fragment 对应 class 的全限定类名。 + */ + fun addWithStack(containerId: Int = 0, fragment: Fragment, tag: String? = null, transition: Boolean = true): EnhanceFragmentTransaction { + //hide top + hideTopFragment() + //set add to stack + val nonnullTag = (tag ?: fragment.javaClassName()) + addToBackStack(nonnullTag) + //add + fragmentTransaction.add(containerId.confirmId(), fragment, nonnullTag) + if (transition) { + //set a transition + setTransitionOpen() + } + return this + } + + /** + * replace 方式把 [fragment] 添加到回退栈中, + * 如果 [containerId]==0,则使用 [com.android.base.app.BaseKit.setDefaultFragmentContainerId] 中配置的 id, + * 如果 [tag] ==null 则使用 fragment 对应 class 的全限定类名。 + */ + fun replaceWithStack(containerId: Int = 0, fragment: Fragment, tag: String? = null, transition: Boolean = true): EnhanceFragmentTransaction { + //set add to stack + val nonnullTag = (tag ?: fragment.javaClassName()) + addToBackStack(nonnullTag) + //add + fragmentTransaction.replace(containerId.confirmId(), fragment, nonnullTag) + //set a transition + if (transition) { + setTransitionOpen() + } + return this + } + + private fun Int.confirmId(): Int { + return if (this == 0) { + FragmentConfig.defaultContainerId() + } else { + this + } + } + + /** + * 添加 [fragment], + * 默认使用 [com.android.base.app.BaseKit.setDefaultFragmentContainerId] 中配置的 id, + * 如果 [tag] 为null,则使用 [fragment] 的全限定类名* + */ + fun addWithDefaultContainer(fragment: Fragment, tag: String? = null): FragmentTransaction { + val nonnullTag = (tag ?: fragment.javaClassName()) + return fragmentTransaction.add(FragmentConfig.defaultContainerId(), fragment, nonnullTag) + } + + /** + * 替换为 [fragment], + * id 使用 [com.android.base.app.BaseKit.setDefaultFragmentContainerId] 中配置的 id, + * 如果 [tag] 为null,则使用 [fragment] 的全限定类名 + */ + fun replaceWithDefaultContainer(fragment: Fragment, tag: String? = null, transition: Boolean = true): FragmentTransaction { + val nonnullTag = (tag ?: fragment.javaClassName()) + if (transition) { + setTransitionOpen() + } + return fragmentTransaction.replace(FragmentConfig.defaultContainerId(), fragment, nonnullTag) + } + + /**隐藏所有的 fragment */ + private fun hideOtherFragments() { + for (fragment in fragmentManager.fragments) { + if (fragment != null && fragment.isVisible) { + fragmentTransaction.hide(fragment) + } + } + } + + /**隐藏第一个可见的 fragment */ + private fun hideTopFragment() { + fragmentManager.fragments.lastOrNull { it.isVisible }?.let { + fragmentTransaction.hide(it) + } + } + + fun setTransitionOpen(): FragmentTransaction { + return fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) + } + + fun setTransitionClose(): FragmentTransaction { + return fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE) + } + + fun setTransitionFade(): FragmentTransaction { + return fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) + } + + //------------------------------------------------------------------------------------------------ + // original functions + //------------------------------------------------------------------------------------------------ + override fun setBreadCrumbShortTitle(res: Int): FragmentTransaction { + return fragmentTransaction.setBreadCrumbShortTitle(res) + } + + override fun setBreadCrumbShortTitle(text: CharSequence?): FragmentTransaction { + return fragmentTransaction.setBreadCrumbShortTitle(text) + } + + override fun setPrimaryNavigationFragment(fragment: Fragment?): FragmentTransaction { + return fragmentTransaction.setPrimaryNavigationFragment(fragment) + } + + override fun runOnCommit(runnable: Runnable): FragmentTransaction { + return fragmentTransaction.runOnCommit(runnable) + } + + override fun add(fragment: Fragment, tag: String?): FragmentTransaction { + return fragmentTransaction.add(fragment, tag) + } + + override fun add(containerViewId: Int, fragment: Fragment): FragmentTransaction { + return fragmentTransaction.add(containerViewId, fragment) + } + + override fun add(containerViewId: Int, fragment: Fragment, tag: String?): FragmentTransaction { + return fragmentTransaction.add(containerViewId, fragment, tag) + } + + override fun hide(fragment: Fragment): FragmentTransaction { + return fragmentTransaction.hide(fragment) + } + + override fun replace(containerViewId: Int, fragment: Fragment): FragmentTransaction { + return fragmentTransaction.replace(containerViewId, fragment) + } + + override fun replace(containerViewId: Int, fragment: Fragment, tag: String?): FragmentTransaction { + return fragmentTransaction.replace(containerViewId, fragment, tag) + } + + override fun detach(fragment: Fragment): FragmentTransaction { + return fragmentTransaction.detach(fragment) + } + + @Deprecated("") + override fun setAllowOptimization(allowOptimization: Boolean): FragmentTransaction { + return fragmentTransaction.setAllowOptimization(allowOptimization) + } + + override fun setCustomAnimations(enter: Int, exit: Int): FragmentTransaction { + return fragmentTransaction.setCustomAnimations(enter, exit) + } + + override fun setCustomAnimations(enter: Int, exit: Int, popEnter: Int, popExit: Int): FragmentTransaction { + return fragmentTransaction.setCustomAnimations(enter, exit, popEnter, popExit) + } + + override fun addToBackStack(name: String?): FragmentTransaction { + return fragmentTransaction.addToBackStack(name) + } + + override fun disallowAddToBackStack(): FragmentTransaction { + return fragmentTransaction.disallowAddToBackStack() + } + + override fun setTransitionStyle(styleRes: Int): FragmentTransaction { + return fragmentTransaction.setTransitionStyle(styleRes) + } + + override fun setTransition(transit: Int): FragmentTransaction { + return fragmentTransaction.setTransition(transit) + } + + override fun attach(fragment: Fragment): FragmentTransaction { + return fragmentTransaction.attach(fragment) + } + + override fun show(fragment: Fragment): FragmentTransaction { + return fragmentTransaction.show(fragment) + } + + override fun isEmpty(): Boolean { + return fragmentTransaction.isEmpty + } + + override fun remove(fragment: Fragment): FragmentTransaction { + return fragmentTransaction.remove(fragment) + } + + override fun isAddToBackStackAllowed(): Boolean { + return fragmentTransaction.isAddToBackStackAllowed + } + + override fun addSharedElement(sharedElement: View, name: String): FragmentTransaction { + return fragmentTransaction.addSharedElement(sharedElement, name) + } + + override fun setBreadCrumbTitle(res: Int): FragmentTransaction { + return fragmentTransaction.setBreadCrumbTitle(res) + } + + override fun setBreadCrumbTitle(text: CharSequence?): FragmentTransaction { + return fragmentTransaction.setBreadCrumbTitle(text) + } + + override fun setReorderingAllowed(reorderingAllowed: Boolean): FragmentTransaction { + return fragmentTransaction.setReorderingAllowed(reorderingAllowed) + } + + @Deprecated("commit will be called automatically") + override fun commit(): Int { + throw UnsupportedOperationException("commit will be called automatically") + } + + @Deprecated("commitAllowingStateLoss will be called automatically") + override fun commitAllowingStateLoss(): Int { + throw UnsupportedOperationException("commitAllowingStateLoss will be called automatically") + } + + @Deprecated("commitNow will be called automatically", ReplaceWith("throw UnsupportedOperationException(\"commitNow will be called automatically\")")) + override fun commitNow() { + throw UnsupportedOperationException("commitNow will be called automatically") + } + + @Deprecated("commitNowAllowingStateLoss will be called automatically", ReplaceWith("throw UnsupportedOperationException(\"commitNowAllowingStateLoss will be called automatically\")")) + override fun commitNowAllowingStateLoss() { + throw UnsupportedOperationException("commitNowAllowingStateLoss will be called automatically") + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/fragment/LazyDelegate.java b/lib_base/src/main/java/com/android/base/app/fragment/LazyDelegate.java new file mode 100644 index 0000000..2b6af63 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/LazyDelegate.java @@ -0,0 +1,117 @@ +package com.android.base.app.fragment; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.view.View; + +/** + *
+ *    在ViewPager中实现懒加载的Fragment
+ *          changed--1: Android Support 24 把setUserVisibleHint方法放到了Attach之前调用了,所以请在在构造代码块中设置LazyDelegate
+ * 
+ * + * @author Ztiany + * Date : Date : 2016-05-06 15:02 + * Email: 1169654504@qq.com + */ +public class LazyDelegate implements FragmentDelegate { + + /** + * View是否准备好,如果不需要绑定view数据,只是加载网络数据,那么该字段可以去掉 + */ + private boolean mIsViewPrepared; + /** + * 滑动过来后,View是否可见 + */ + private boolean mIsViewVisible; + + private onPreparedListener mOnPreparedListener; + + private LazyDelegate() { + } + + public static LazyDelegate attach(FragmentDelegateOwner delegateFragment, final onPreparedListener onPreparedListener) { + LazyDelegate delegate = new LazyDelegate(); + delegate.mOnPreparedListener = onPreparedListener; + delegateFragment.addDelegate(delegate); + return delegate; + } + + /** + * 在这里实现Fragment数据的缓加载. + * + * @param isVisibleToUser true表用户可见,false表不可见 + */ + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + if (isVisibleToUser) { + mIsViewVisible = true; + onVisible(); + } else { + mIsViewVisible = false; + onInvisible(); + } + } + + @Override + public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { + mIsViewPrepared = true; + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + lazyLoad(); + } + + /** + * 滑动过来后,界面可见时执行 + */ + @SuppressWarnings("all") + protected void onVisible() { + lazyLoad(); + } + + /** + * 滑动过来后,界面不可见时执行 + */ + @SuppressWarnings("all") + protected void onInvisible() { + } + + private void lazyLoad() { + if (mIsViewPrepared && mIsViewVisible) { + notifyLazyLoad(); + } + } + + /** + * 懒加载数据,并在此绑定View数据 + */ + private void notifyLazyLoad() { + if (mOnPreparedListener != null) { + mOnPreparedListener.onPrepared(); + } + } + + public interface onPreparedListener { + void onPrepared(); + } + + public static abstract class SimpleLazyLoadListener implements onPreparedListener { + + private boolean mIsCalled; + + @Override + public final void onPrepared() { + if (!mIsCalled) { + onFirstLoad(); + mIsCalled = true; + } + } + + protected abstract void onFirstLoad(); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/fragment/LoadingViewFactory.java b/lib_base/src/main/java/com/android/base/app/fragment/LoadingViewFactory.java new file mode 100644 index 0000000..77295b4 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/LoadingViewFactory.java @@ -0,0 +1,11 @@ +package com.android.base.app.fragment; + +import android.content.Context; + +import com.android.base.app.ui.LoadingView; + +public interface LoadingViewFactory { + + LoadingView createLoadingDelegate(Context context); + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/fragment/RefreshLoadMoreStateLayoutImpl.kt b/lib_base/src/main/java/com/android/base/app/fragment/RefreshLoadMoreStateLayoutImpl.kt new file mode 100644 index 0000000..0ba52b2 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/RefreshLoadMoreStateLayoutImpl.kt @@ -0,0 +1,68 @@ +package com.android.base.app.fragment + +import android.graphics.drawable.Drawable +import android.view.View +import com.android.base.app.ui.CommonId +import com.android.base.app.ui.* + +internal class RefreshLoadMoreStateLayoutImpl private constructor(private val mLayout: View) : StateLayout, StateLayoutConfig { + + companion object { + fun init(view: View): RefreshLoadMoreStateLayoutImpl { + return RefreshLoadMoreStateLayoutImpl(view) + } + } + + private var mMultiStateView: StateLayout = mLayout.findViewById(CommonId.STATE_ID) as StateLayout + private var mRefreshView: RefreshLoadMoreView + + val refreshView: RefreshLoadMoreView + get() = mRefreshView + + init { + val refreshLayout = mLayout.findViewById(CommonId.REFRESH_ID) + mRefreshView = RefreshLoadViewFactory.createRefreshView(refreshLayout) + } + + override fun showLoadingLayout() = mMultiStateView.showLoadingLayout() + override fun showContentLayout() = mMultiStateView.showContentLayout() + override fun showEmptyLayout() = mMultiStateView.showEmptyLayout() + override fun showErrorLayout() = mMultiStateView.showErrorLayout() + override fun showRequesting() = mMultiStateView.showRequesting() + override fun showBlank() = mMultiStateView.showBlank() + override fun showNetErrorLayout() = mMultiStateView.showNetErrorLayout() + override fun showServerErrorLayout() = mMultiStateView.showServerErrorLayout() + + override fun getStateLayoutConfig(): StateLayoutConfig = mMultiStateView.stateLayoutConfig + + override fun setStateMessage(state: Int, message: CharSequence?): StateLayoutConfig { + mMultiStateView.stateLayoutConfig.setStateMessage(state, message) + return mMultiStateView.stateLayoutConfig + } + + override fun setStateIcon(state: Int, drawable: Drawable?): StateLayoutConfig { + mMultiStateView.stateLayoutConfig.setStateIcon(state, drawable) + return mMultiStateView.stateLayoutConfig + } + + override fun setStateIcon(state: Int, drawableId: Int): StateLayoutConfig { + mMultiStateView.stateLayoutConfig.setStateIcon(state, drawableId) + return mMultiStateView.stateLayoutConfig + } + + override fun setStateAction(state: Int, actionText: CharSequence?): StateLayoutConfig { + mMultiStateView.stateLayoutConfig.setStateAction(state, actionText) + return mMultiStateView.stateLayoutConfig + } + + override fun setStateRetryListener(retryActionListener: OnRetryActionListener?): StateLayoutConfig { + mMultiStateView.stateLayoutConfig.setStateRetryListener(retryActionListener) + return mMultiStateView.stateLayoutConfig + } + + override fun disableOperationWhenRequesting(disable: Boolean): StateLayoutConfig { + mMultiStateView.stateLayoutConfig.disableOperationWhenRequesting(disable) + return mMultiStateView.stateLayoutConfig + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/fragment/RefreshableStateLayoutImpl.java b/lib_base/src/main/java/com/android/base/app/fragment/RefreshableStateLayoutImpl.java new file mode 100644 index 0000000..638f62d --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/RefreshableStateLayoutImpl.java @@ -0,0 +1,189 @@ +package com.android.base.app.fragment; + +import android.graphics.drawable.Drawable; +import android.support.annotation.DrawableRes; +import android.view.View; + +import com.android.base.app.ui.OnRetryActionListener; +import com.android.base.app.ui.RefreshStateLayout; +import com.android.base.app.ui.RefreshView; +import com.android.base.app.ui.RefreshViewFactory; +import com.android.base.app.ui.StateLayout; +import com.android.base.app.ui.StateLayoutConfig; + +import static com.android.base.app.ui.CommonId.REFRESH_ID; +import static com.android.base.app.ui.CommonId.STATE_ID; + +/** + * @author Ztiany + */ +final class RefreshableStateLayoutImpl implements RefreshStateLayout, StateLayoutConfig { + + private StateLayout mMultiStateView; + private RefreshView mRefreshView; + private RefreshView.RefreshHandler mRefreshHandler; + + static RefreshableStateLayoutImpl init(View layoutView) { + return new RefreshableStateLayoutImpl(layoutView); + } + + private RefreshableStateLayoutImpl(View layoutView) { + setupBaseUiLogic(layoutView); + setupRefreshLogic(layoutView); + } + + RefreshView getRefreshView() { + return mRefreshView; + } + + void setRefreshHandler(RefreshView.RefreshHandler refreshHandler) { + mRefreshHandler = refreshHandler; + } + + void setStateRetryListenerUnchecked(OnRetryActionListener retryActionListener) { + if (mMultiStateView != null) { + setStateRetryListener(retryActionListener); + } + } + + @SuppressWarnings("all") + private void setupBaseUiLogic(View layoutView) { + mMultiStateView = (StateLayout) layoutView.findViewById(STATE_ID); + } + + private void setupRefreshLogic(View layoutView) { + View refreshLayout = layoutView.findViewById(REFRESH_ID); + if (refreshLayout == null) { + return; + } + mRefreshView = RefreshViewFactory.createRefreshView(refreshLayout); + mRefreshView.setRefreshHandler(new RefreshView.RefreshHandler() { + @Override + public boolean canRefresh() { + return mRefreshHandler.canRefresh(); + } + + @Override + public void onRefresh() { + mRefreshHandler.onRefresh(); + } + }); + } + + @Override + public final void autoRefresh() { + if (mRefreshView != null) { + mRefreshView.autoRefresh(); + } + } + + @Override + public void showLoadingLayout() { + checkMultiStateView().showLoadingLayout(); + } + + @Override + public void showContentLayout() { + refreshCompleted(); + checkMultiStateView().showContentLayout(); + } + + @Override + public void showEmptyLayout() { + refreshCompleted(); + checkMultiStateView().showEmptyLayout(); + } + + @Override + public void showErrorLayout() { + refreshCompleted(); + checkMultiStateView().showErrorLayout(); + } + + @Override + public void showRequesting() { + checkMultiStateView().showRequesting(); + } + + @Override + public void showBlank() { + checkMultiStateView().showBlank(); + } + + @Override + public void showNetErrorLayout() { + refreshCompleted(); + checkMultiStateView().showNetErrorLayout(); + } + + @Override + public void showServerErrorLayout() { + refreshCompleted(); + checkMultiStateView().showNetErrorLayout(); + } + + @Override + public StateLayoutConfig getStateLayoutConfig() { + checkMultiStateView(); + return this; + } + + @Override + public void refreshCompleted() { + if (mRefreshView != null) { + mRefreshView.refreshCompleted(); + } + } + + @Override + public boolean isRefreshing() { + if (mRefreshView != null) { + return mRefreshView.isRefreshing(); + } + return false; + } + + @Override + public StateLayoutConfig setStateMessage(@RetryableState int state, CharSequence message) { + checkMultiStateView().getStateLayoutConfig().setStateMessage(state, message); + return this; + } + + @Override + public StateLayoutConfig setStateIcon(@RetryableState int state, Drawable drawable) { + checkMultiStateView().getStateLayoutConfig().setStateIcon(state, drawable); + return this; + } + + @Override + public StateLayoutConfig setStateIcon(@RetryableState int state, @DrawableRes int drawableId) { + checkMultiStateView().getStateLayoutConfig().setStateIcon(state, drawableId); + return this; + } + + @Override + public StateLayoutConfig setStateAction(@RetryableState int state, CharSequence actionText) { + checkMultiStateView().getStateLayoutConfig().setStateAction(state, actionText); + return this; + } + + @Override + public StateLayoutConfig setStateRetryListener(OnRetryActionListener retryActionListener) { + checkMultiStateView().getStateLayoutConfig().setStateRetryListener(retryActionListener); + return this; + } + + @Override + public StateLayoutConfig disableOperationWhenRequesting(boolean disable) { + checkMultiStateView().getStateLayoutConfig().disableOperationWhenRequesting(disable); + return this; + } + + private StateLayout checkMultiStateView() { + if (mMultiStateView == null) { + throw new IllegalStateException("Calling this function requires defining a view that implements StateLayout in the Layout"); + } + return mMultiStateView; + } + +} diff --git a/lib_base/src/main/java/com/android/base/app/fragment/TabManager.java b/lib_base/src/main/java/com/android/base/app/fragment/TabManager.java new file mode 100644 index 0000000..fee2ea4 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/TabManager.java @@ -0,0 +1,207 @@ +package com.android.base.app.fragment; + +import android.content.Context; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public abstract class TabManager { + + @SuppressWarnings("WeakerAccess") public static final int ATTACH_DETACH = 1; + @SuppressWarnings("WeakerAccess") public static final int SHOW_HIDE = 2; + + private final FragmentManager mFragmentManager; + private final int mContainerId; + private final Tabs mMainTabs; + private final Context mContext; + + private FragmentInfo mCurrentFragmentInfo; + + private static final String CURRENT_ID_KET = "main_tab_id"; + private static final int NONE = -1; + private int mCurrentId = NONE; + private final int mOperationType; + + public TabManager(Context context, FragmentManager fragmentManager, Tabs tabs, int containerId) { + this(context, fragmentManager, tabs, containerId, ATTACH_DETACH); + } + + /** + * @param operationType {@link #ATTACH_DETACH} or {@link #SHOW_HIDE} + */ + @SuppressWarnings("WeakerAccess") + public TabManager(Context context, FragmentManager fragmentManager, Tabs tabs, int containerId, int operationType) { + if (operationType != ATTACH_DETACH && operationType != SHOW_HIDE) { + throw new IllegalArgumentException("the operationType must be ATTACH_DETACH or SHOW_HIDE"); + } + mMainTabs = tabs; + mContainerId = containerId; + mContext = context; + mFragmentManager = fragmentManager; + mOperationType = operationType; + } + + public final void setup(Bundle bundle) { + int pageId = mMainTabs.homePage().getPageId(); + if (bundle == null) { + switchPage(pageId); + } else { + mCurrentId = bundle.getInt(CURRENT_ID_KET, pageId); + restoreState(); + } + } + + private void restoreState() { + List pages = mMainTabs.getPages(); + for (FragmentInfo page : pages) { + page.setInstance(mFragmentManager.findFragmentByTag(page.getTag())); + if (mCurrentId == page.getPageId()) { + mCurrentFragmentInfo = page; + } + } + if (mCurrentId == NONE) { + doChangeTab(mMainTabs.homePage().getPageId()); + } + } + + private void switchPage(int pageId) { + if (mCurrentId == pageId) { + return; + } + FragmentTransaction ft = null; + if (mCurrentFragmentInfo != null) { + Fragment fragment = mCurrentFragmentInfo.getInstance(); + if (fragment != null) { + ft = mFragmentManager.beginTransaction(); + hideOrDetach(ft, fragment); + } + } + if (ft != null) { + ft.commit(); + } + doChangeTab(pageId); + } + + private void hideOrDetach(FragmentTransaction ft, Fragment fragment) { + if (mOperationType == SHOW_HIDE) { + ft.hide(fragment); + } else { + ft.detach(fragment); + } + } + + private void showOrAttach(FragmentTransaction fragmentTransaction, Fragment fragment) { + if (mOperationType == SHOW_HIDE) { + fragmentTransaction.show(fragment); + } else { + fragmentTransaction.attach(fragment); + } + } + + private void doChangeTab(int fragmentId) { + FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction(); + FragmentInfo fragmentInfo = mMainTabs.getFragmentInfo(fragmentId); + Fragment fragment = fragmentInfo.getInstance(); + if (fragment != null) { + showOrAttach(fragmentTransaction, fragment); + } else { + Fragment newFragment = fragmentInfo.newFragment(mContext); + fragmentInfo.setInstance(newFragment); + onFragmentCreated(fragmentId, newFragment); + fragmentTransaction.add(mContainerId, newFragment, fragmentInfo.getTag()); + } + mCurrentFragmentInfo = fragmentInfo; + mCurrentId = fragmentId; + fragmentTransaction.commit(); + } + + @SuppressWarnings("WeakerAccess") + public void selectTabByPosition(int position) { + switchPage(mMainTabs.getIdByPosition(position)); + } + + public void selectTabById(int tabId) { + selectTabByPosition(mMainTabs.getPositionById(tabId)); + } + + @SuppressWarnings("unused") + public int getCurrentPosition() { + return mMainTabs.getPositionById(mCurrentId); + } + + @SuppressWarnings("WeakerAccess,unused") + protected void onFragmentCreated(int id, Fragment newFragment) { + } + + public void onSaveInstanceState(Bundle bundle) { + bundle.putInt(CURRENT_ID_KET, mCurrentId); + } + + public static abstract class Tabs { + + private final List mPages; + + protected Tabs() { + mPages = new ArrayList<>(); + } + + protected void add(FragmentInfo page) { + mPages.add(page); + } + + FragmentInfo homePage() { + return mPages.get(0); + } + + public int size() { + return mPages.size(); + } + + FragmentInfo getFragmentInfo(int id) { + for (FragmentInfo page : mPages) { + if (page.getPageId() == id) { + return page; + } + } + throw new IllegalArgumentException("MainPages not has this pageId :" + id); + } + + /** + * @param clazz Fragment对应的clazz + * @return pagerId ,没有则返回-1 + */ + @SuppressWarnings("unused") + int getIdByClazz(Class clazz) { + for (FragmentInfo page : mPages) { + if (page.getClazz() == clazz) { + return page.getPageId(); + } + } + return -1; + } + + List getPages() { + return Collections.unmodifiableList(mPages); + } + + private int getPositionById(int tabId) { + int size = mPages.size(); + for (int i = 0; i < size; i++) { + if (mPages.get(i).getPageId() == tabId) { + return i; + } + } + return -1; + } + + private int getIdByPosition(int position) { + return mPages.get(position).getPageId(); + } + } + +} diff --git a/lib_base/src/main/java/com/android/base/app/mvp/AbstractPresenter.kt b/lib_base/src/main/java/com/android/base/app/mvp/AbstractPresenter.kt new file mode 100644 index 0000000..edc66f6 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/mvp/AbstractPresenter.kt @@ -0,0 +1,44 @@ +package com.android.base.app.mvp + +import android.support.annotation.CallSuper +import java.lang.ref.WeakReference + +abstract class AbstractPresenter : IPresenter { + + private var _view: WeakReference? = null + + protected val view: V? + get() = if (_view != null) { + _view?.get() + } else null + + protected val isViewAttached: Boolean + get() = _view != null && _view?.get() != null + + final override fun bindView(view: V?) { + if (view == null) { + throw NullPointerException("Presenter bindView --> view is null") + } + if (_view != null) { + throw UnsupportedOperationException("Presenter bindView --> the view already bind") + } + _view = WeakReference(view) + } + + override fun onPostStart() {} + + override fun onPause() { + } + + override fun onResume() { + } + + @CallSuper + override fun onDestroy() { + if (_view != null) { + _view?.clear() + _view = null + } + } + +} diff --git a/lib_base/src/main/java/com/android/base/app/mvp/IBaseView.kt b/lib_base/src/main/java/com/android/base/app/mvp/IBaseView.kt new file mode 100644 index 0000000..5553dc7 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/mvp/IBaseView.kt @@ -0,0 +1,3 @@ +package com.android.base.app.mvp + +interface IBaseView diff --git a/lib_base/src/main/java/com/android/base/app/mvp/IPresenter.kt b/lib_base/src/main/java/com/android/base/app/mvp/IPresenter.kt new file mode 100644 index 0000000..1c0009c --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/mvp/IPresenter.kt @@ -0,0 +1,13 @@ +package com.android.base.app.mvp + + +interface IPresenter : Lifecycle { + + /** + * bind a view + * + * @param view V + */ + fun bindView(view: V?) + +} diff --git a/lib_base/src/main/java/com/android/base/app/mvp/Lifecycle.kt b/lib_base/src/main/java/com/android/base/app/mvp/Lifecycle.kt new file mode 100644 index 0000000..fa8ecc8 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/mvp/Lifecycle.kt @@ -0,0 +1,31 @@ +package com.android.base.app.mvp + + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-04-18 16:23 + */ +interface Lifecycle { + + /** + * start the Lifecycle , initialize something, will be called only once + */ + fun onStart() + + /** + * will be called when view is ready. + */ + fun onPostStart() + + fun onResume() + + fun onPause() + + /** + * destroy the Lifecycle and release resource, will be called only once + */ + fun onDestroy() + +} + diff --git a/lib_base/src/main/java/com/android/base/app/mvp/PresenterBinder.kt b/lib_base/src/main/java/com/android/base/app/mvp/PresenterBinder.kt new file mode 100644 index 0000000..12214ce --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/mvp/PresenterBinder.kt @@ -0,0 +1,66 @@ +package com.android.base.app.mvp + +import android.os.Bundle +import android.support.v4.app.Fragment +import android.view.View +import com.android.base.app.fragment.FragmentDelegate +import com.android.base.app.fragment.FragmentDelegateOwner + + +class PresenterBinder constructor(private val lifecycle: Lifecycle) : FragmentDelegate { + + private var isCalled: Boolean = false + private var host: Fragment? = null + + override fun onAttachToFragment(fragment: Fragment) { + host = fragment + } + + override fun onDetachFromFragment() { + host = null + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + if (!isCalled) { + lifecycle.onStart() + val activity = host?.activity + activity?.findViewById(android.R.id.content)?.post { lifecycle.onPostStart() } + isCalled = true + } + } + + override fun onResume() { + lifecycle.onResume() + } + + override fun onPause() { + lifecycle.onPause() + } + + override fun onDestroy() { + lifecycle.onDestroy() + } + + companion object { + + /** + * @param v The MVP of the V + * @param p The MVP of the P + * @param The MVP of the V + */ + fun bind(fragmentDelegateOwner: FragmentDelegateOwner, v: V, p: IPresenter): PresenterBinder { + p.bindView(v) + val lifecycleDelegate = PresenterBinder(p) + fragmentDelegateOwner.addDelegate(lifecycleDelegate) + return lifecycleDelegate + } + } + +} + +fun FragmentDelegateOwner.bindPresenter(v: V, p: IPresenter): PresenterBinder { + p.bindView(v) + val lifecycleDelegate = PresenterBinder(p) + addDelegate(lifecycleDelegate) + return lifecycleDelegate +} diff --git a/lib_base/src/main/java/com/android/base/app/mvp/RxPresenter.kt b/lib_base/src/main/java/com/android/base/app/mvp/RxPresenter.kt new file mode 100644 index 0000000..d7b4ac8 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/mvp/RxPresenter.kt @@ -0,0 +1,65 @@ +package com.android.base.app.mvp + +import android.support.annotation.CallSuper +import com.android.base.rx.LifecycleScopeProviderEx +import com.uber.autodispose.lifecycle.CorrespondingEventsFunction +import com.uber.autodispose.lifecycle.LifecycleEndedException +import com.uber.autodispose.lifecycle.LifecycleScopes +import io.reactivex.CompletableSource +import io.reactivex.Observable +import io.reactivex.subjects.BehaviorSubject + + +/** + * work with RxJava + * + * @author Ztiany + * Date : 2016-10-19 12:17 + * Email: 1169654504@qq.com + */ +abstract class RxPresenter : AbstractPresenter(), LifecycleScopeProviderEx { + + private val lifecycleSubject = BehaviorSubject.create() + + enum class LifecycleEvent { + START, + DESTROY + } + + @CallSuper + override fun onStart() { + lifecycleSubject.onNext(LifecycleEvent.START) + } + + @CallSuper + override fun onDestroy() { + lifecycleSubject.onNext(LifecycleEvent.DESTROY) + super@RxPresenter.onDestroy() + } + + final override fun lifecycle(): Observable { + return lifecycleSubject + } + + final override fun correspondingEvents(): CorrespondingEventsFunction { + return LIFECYCLE_CORRESPONDING_EVENTS + } + + final override fun peekLifecycle(): LifecycleEvent? { + return lifecycleSubject.value + } + + final override fun requestScope(): CompletableSource { + return LifecycleScopes.resolveScopeFromLifecycle(this) + } + + companion object { + internal val LIFECYCLE_CORRESPONDING_EVENTS: CorrespondingEventsFunction = CorrespondingEventsFunction { + return@CorrespondingEventsFunction when (it) { + LifecycleEvent.START -> LifecycleEvent.DESTROY + else -> throw LifecycleEndedException("Cannot bind to LifecycleEvent when outside of it.") + } + } + } + +} diff --git a/lib_base/src/main/java/com/android/base/app/mvvm/ArchViewModel.kt b/lib_base/src/main/java/com/android/base/app/mvvm/ArchViewModel.kt new file mode 100644 index 0000000..d483faa --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/mvvm/ArchViewModel.kt @@ -0,0 +1,60 @@ +package com.android.base.app.mvvm + +import android.arch.lifecycle.ViewModel +import android.support.annotation.CallSuper +import com.android.base.rx.LifecycleScopeProviderEx +import com.uber.autodispose.lifecycle.CorrespondingEventsFunction +import com.uber.autodispose.lifecycle.LifecycleEndedException +import com.uber.autodispose.lifecycle.LifecycleScopes +import io.reactivex.CompletableSource +import io.reactivex.Observable +import io.reactivex.subjects.BehaviorSubject + +/** + * ArchViewModel work with Rx + * + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-04-18 16:25 + */ +abstract class ArchViewModel : ViewModel(), LifecycleScopeProviderEx { + + private val archLifecycleSubject = BehaviorSubject.createDefault(ViewModelEvent.CREATED) + + enum class ViewModelEvent { + CREATED, CLEARED + } + + @CallSuper + override fun onCleared() { + archLifecycleSubject.onNext(ViewModelEvent.CLEARED) + super.onCleared() + } + + final override fun correspondingEvents(): CorrespondingEventsFunction { + return CORRESPONDING_EVENTS + } + + final override fun lifecycle(): Observable { + return archLifecycleSubject.hide() + } + + final override fun peekLifecycle(): ViewModelEvent? { + return archLifecycleSubject.value + } + + final override fun requestScope(): CompletableSource { + return LifecycleScopes.resolveScopeFromLifecycle(this) + } + + companion object { + private val CORRESPONDING_EVENTS = CorrespondingEventsFunction { event -> + when (event) { + ViewModelEvent.CREATED -> ViewModelEvent.CLEARED + else -> throw LifecycleEndedException( + "Cannot bind to ViewModel lifecycle after onCleared.") + } + } + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/mvvm/ViewModelEx.kt b/lib_base/src/main/java/com/android/base/app/mvvm/ViewModelEx.kt new file mode 100644 index 0000000..2079d2f --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/mvvm/ViewModelEx.kt @@ -0,0 +1,32 @@ +package com.android.base.app.mvvm + +import android.arch.lifecycle.ViewModel +import android.arch.lifecycle.ViewModelProvider +import android.arch.lifecycle.ViewModelProviders +import android.support.v4.app.Fragment +import android.support.v4.app.FragmentActivity + +inline fun Fragment.getViewModel(factory: ViewModelProvider.Factory? = null): VM { + return if (factory == null) { + ViewModelProviders.of(this)[VM::class.java] + } else { + ViewModelProviders.of(this, factory)[VM::class.java] + } +} + +inline fun Fragment.getViewModelFromActivity(factory: ViewModelProvider.Factory? = null): VM { + val activity = this.activity ?: throw IllegalStateException("fragment is not attach to activity") + return if (factory == null) { + ViewModelProviders.of(activity)[VM::class.java] + } else { + ViewModelProviders.of(activity, factory)[VM::class.java] + } +} + +inline fun FragmentActivity.getViewModel(factory: ViewModelProvider.Factory? = null): VM { + return if (factory == null) { + ViewModelProviders.of(this)[VM::class.java] + } else { + ViewModelProviders.of(this, factory)[VM::class.java] + } +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/ui/AutoPageNumber.java b/lib_base/src/main/java/com/android/base/app/ui/AutoPageNumber.java new file mode 100644 index 0000000..fab9309 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/ui/AutoPageNumber.java @@ -0,0 +1,39 @@ +package com.android.base.app.ui; + +import com.android.base.adapter.DataManager; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-04-27 14:04 + */ +public class AutoPageNumber extends PageNumber { + + private final DataManager mDataManager; + private final RefreshListLayout mRefreshListLayout; + + public AutoPageNumber(RefreshListLayout refreshListLayout, DataManager dataManager) { + mRefreshListLayout = refreshListLayout; + mDataManager = dataManager; + } + + @Override + public int getCurrentPage() { + return calcPageNumber(mDataManager.getDataSize()); + } + + @Override + public int getLoadingPage() { + if (mRefreshListLayout.isRefreshing()) { + return getPageStart(); + } else { + return getCurrentPage() + 1; + } + } + + @Override + public int getItemCount() { + return mDataManager.getDataSize(); + } + +} diff --git a/lib_base/src/main/java/com/android/base/app/ui/CommonId.java b/lib_base/src/main/java/com/android/base/app/ui/CommonId.java new file mode 100644 index 0000000..706b161 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/ui/CommonId.java @@ -0,0 +1,22 @@ +package com.android.base.app.ui; + + +import com.android.base.R; + +public class CommonId { + + private CommonId() { + throw new UnsupportedOperationException(); + } + + public static final int REFRESH_ID = R.id.base_refresh_layout; + public static final int STATE_ID = R.id.base_state_layout; + public static final int LIST_ID = R.id.base_list_layout; + + public static final int RETRY_TV_ID = R.id.base_retry_tv; + public static final int RETRY_IV_ID = R.id.base_retry_icon; + public static final int RETRY_BTN_ID = R.id.base_retry_btn; + + public static final int TOOLBAR_ID = R.id.common_toolbar; + +} diff --git a/lib_base/src/main/java/com/android/base/app/ui/LoadingView.java b/lib_base/src/main/java/com/android/base/app/ui/LoadingView.java new file mode 100644 index 0000000..078d760 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/ui/LoadingView.java @@ -0,0 +1,28 @@ +package com.android.base.app.ui; + +import android.support.annotation.StringRes; + +/** + * 显示通用的 LoadingDialog 和 Message + * + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2016-12-02 15:12 + */ +public interface LoadingView { + + void showLoadingDialog(); + + void showLoadingDialog(boolean cancelable); + + void showLoadingDialog(CharSequence message, boolean cancelable); + + void showLoadingDialog(@StringRes int messageId, boolean cancelable); + + void dismissLoadingDialog(); + + void showMessage(CharSequence message); + + void showMessage(@StringRes int messageId); + +} diff --git a/lib_base/src/main/java/com/android/base/app/ui/OnRetryActionListener.java b/lib_base/src/main/java/com/android/base/app/ui/OnRetryActionListener.java new file mode 100644 index 0000000..9bfc47f --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/ui/OnRetryActionListener.java @@ -0,0 +1,9 @@ +package com.android.base.app.ui; + +import static com.android.base.app.ui.StateLayoutConfig.*; + +public interface OnRetryActionListener { + + void onRetry(@RetryableState int state); + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/ui/PageNumber.java b/lib_base/src/main/java/com/android/base/app/ui/PageNumber.java new file mode 100644 index 0000000..b771698 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/ui/PageNumber.java @@ -0,0 +1,99 @@ +package com.android.base.app.ui; + +/** + * @author Ztiany + * @version 1.0 + */ +public abstract class PageNumber { + + private static int PAGE_START = 1; + private static int PAGE_SIZE = 20; + + private int mPageStart; + private int mPageSize; + + private Object mPageToken; + + @SuppressWarnings("WeakerAccess,unused") + public PageNumber() { + this(PAGE_START, PAGE_SIZE); + } + + @SuppressWarnings("WeakerAccess") + public PageNumber(int pageStart, int pageSize) { + mPageStart = pageStart; + mPageSize = pageSize; + } + + @SuppressWarnings("unused") + public void setPageToken(Object pageToken) { + mPageToken = pageToken; + } + + @SuppressWarnings("unchecked,unused") + public T getPageToken() { + return (T) mPageToken; + } + + @SuppressWarnings("unused") + public int getPageSize() { + return mPageSize; + } + + @SuppressWarnings("WeakerAccess") + public int getPageStart() { + return mPageStart; + } + + public boolean hasMore(int size) { + return size >= mPageSize; + } + + /** + * 根据page size计算当前的页码 + */ + int calcPageNumber(int dataSize) { + /* s=1 s=0 + 19/20 = 0 1 0 + 21/20 = 1 2 1 + 54/20 = 2 3 2 + 64/20 = 3 4 3 + */ + int pageNumber; + int pageSize = mPageSize; + int pageStart = mPageStart; + if (pageStart == 0) { + pageNumber = (dataSize / pageSize) - 1; + pageNumber = pageNumber < 0 ? 0 : pageNumber; + } else if (pageStart == 1) { + pageNumber = (dataSize / pageSize); + pageNumber = pageNumber < 1 ? 1 : pageNumber; + } else { + throw new RuntimeException("pageStart must be 0 or 1"); + } + return pageNumber; + } + + @SuppressWarnings("unused") + public void changePageSetting(int pageStart, int pageSize) { + mPageStart = pageStart; + mPageSize = pageSize; + } + + public static void setDefaultPageStart(int pageSize) { + PAGE_START = pageSize; + } + + public static void setDefaultPageSize(int pageSize) { + PAGE_SIZE = pageSize; + } + + public abstract int getCurrentPage(); + + @SuppressWarnings("unused") + public abstract int getLoadingPage(); + + @SuppressWarnings("unused") + public abstract int getItemCount(); + +} diff --git a/lib_base/src/main/java/com/android/base/app/ui/RefreshListLayout.java b/lib_base/src/main/java/com/android/base/app/ui/RefreshListLayout.java new file mode 100644 index 0000000..27dec34 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/ui/RefreshListLayout.java @@ -0,0 +1,31 @@ +package com.android.base.app.ui; + +import java.util.List; + +/** + * 列表视图行为 + * + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-03-29 22:16 + */ +public interface RefreshListLayout extends RefreshStateLayout { + + void replaceData(List data); + + void addData(List data); + + PageNumber getPager(); + + boolean isEmpty(); + + void loadMoreCompleted(boolean hasMore); + + void loadMoreFailed(); + + boolean isLoadingMore(); + + @Override + boolean isRefreshing(); + +} diff --git a/lib_base/src/main/java/com/android/base/app/ui/RefreshLoadMoreView.java b/lib_base/src/main/java/com/android/base/app/ui/RefreshLoadMoreView.java new file mode 100644 index 0000000..260692c --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/ui/RefreshLoadMoreView.java @@ -0,0 +1,40 @@ +package com.android.base.app.ui; + +/** + *
对下拉刷新的抽象 + *
Email: 1169654504@qq.com + * + * @author Ztiany + * @version 1.0 + */ +public interface RefreshLoadMoreView { + + void autoRefresh(); + + void refreshCompleted(); + + void loadMoreCompleted(boolean hasMore); + + void loadMoreFailed(); + + void setRefreshHandler(RefreshHandler refreshHandler); + + void setLoadMoreHandler(LoadMoreHandler loadMoreHandler); + + boolean isRefreshing(); + + boolean isLoadingMore(); + + void setRefreshEnable(boolean enable); + + void setLoadMoreEnable(boolean enable); + + interface RefreshHandler { + void onRefresh(); + } + + interface LoadMoreHandler { + void onRefresh(); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/ui/RefreshLoadViewFactory.java b/lib_base/src/main/java/com/android/base/app/ui/RefreshLoadViewFactory.java new file mode 100644 index 0000000..c7ba4f8 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/ui/RefreshLoadViewFactory.java @@ -0,0 +1,31 @@ +package com.android.base.app.ui; + +import android.view.View; + +/** + * RefreshLoadMoreView Factory + * + * @author Ztiany + * Email: 1169654504@qq.com + * @version 1.0 + */ +public class RefreshLoadViewFactory { + + private static Factory sFactory; + + public static RefreshLoadMoreView createRefreshView(View view) { + if (sFactory != null) { + return sFactory.createRefreshView(view); + } + throw new IllegalArgumentException("RefreshLoadViewFactory does not support create RefreshLoadMoreView . the view :" + view); + } + + public static void registerFactory(Factory factory) { + sFactory = factory; + } + + public interface Factory { + RefreshLoadMoreView createRefreshView(View view); + } + +} diff --git a/lib_base/src/main/java/com/android/base/app/ui/RefreshStateLayout.java b/lib_base/src/main/java/com/android/base/app/ui/RefreshStateLayout.java new file mode 100644 index 0000000..7383475 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/ui/RefreshStateLayout.java @@ -0,0 +1,11 @@ +package com.android.base.app.ui; + +public interface RefreshStateLayout extends StateLayout{ + + void autoRefresh(); + + void refreshCompleted(); + + boolean isRefreshing(); + +} diff --git a/lib_base/src/main/java/com/android/base/app/ui/RefreshView.java b/lib_base/src/main/java/com/android/base/app/ui/RefreshView.java new file mode 100644 index 0000000..68af62f --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/ui/RefreshView.java @@ -0,0 +1,32 @@ +package com.android.base.app.ui; + +/** + *
对下拉刷新的抽象 + *
Email: 1169654504@qq.com + * + * @author Ztiany + * @version 1.0 + */ +public interface RefreshView { + + void autoRefresh(); + + void refreshCompleted(); + + void setRefreshHandler(RefreshHandler refreshHandler); + + boolean isRefreshing(); + + void setRefreshEnable(boolean enable); + + abstract class RefreshHandler { + + public boolean canRefresh() { + return true; + } + + public abstract void onRefresh(); + + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/ui/RefreshViewFactory.java b/lib_base/src/main/java/com/android/base/app/ui/RefreshViewFactory.java new file mode 100644 index 0000000..f7009d0 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/ui/RefreshViewFactory.java @@ -0,0 +1,35 @@ +package com.android.base.app.ui; + +import android.support.v4.widget.SwipeRefreshLayout; +import android.view.View; + +/** + * RefreshView Factory + * + * @author Ztiany + * Email: 1169654504@qq.com + * @version 1.0 + */ +public class RefreshViewFactory { + + private static Factory sFactory; + + public static RefreshView createRefreshView(View view) { + + if (sFactory != null) { + return sFactory.createRefreshView(view); + } + if (view instanceof SwipeRefreshLayout) { + return new SwipeRefreshView((SwipeRefreshLayout) view); + } + throw new IllegalArgumentException("RefreshViewFactory does not support create RefreshView . the view :" + view); + } + + public static void registerFactory(Factory factory) { + sFactory = factory; + } + + public interface Factory { + RefreshView createRefreshView(View view); + } +} diff --git a/lib_base/src/main/java/com/android/base/app/ui/StateLayout.java b/lib_base/src/main/java/com/android/base/app/ui/StateLayout.java new file mode 100644 index 0000000..44f7fb3 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/ui/StateLayout.java @@ -0,0 +1,27 @@ +package com.android.base.app.ui; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-07-08 14:52 + */ +public interface StateLayout { + + void showContentLayout(); + + void showLoadingLayout(); + + void showEmptyLayout(); + + void showErrorLayout(); + + void showRequesting(); + + void showBlank(); + + void showNetErrorLayout(); + + void showServerErrorLayout(); + + StateLayoutConfig getStateLayoutConfig(); +} diff --git a/lib_base/src/main/java/com/android/base/app/ui/StateLayoutConfig.java b/lib_base/src/main/java/com/android/base/app/ui/StateLayoutConfig.java new file mode 100644 index 0000000..052f34a --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/ui/StateLayoutConfig.java @@ -0,0 +1,63 @@ +package com.android.base.app.ui; + +import android.graphics.drawable.Drawable; +import android.support.annotation.DrawableRes; +import android.support.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-04-20 23:32 + */ +public interface StateLayoutConfig { + + int CONTENT = 0x01; + int LOADING = 0x02; + int ERROR = 0x03; + int EMPTY = 0x04; + int NET_ERROR = 0x05; + int BLANK = 0x06; + int REQUESTING = 0x07; + int SERVER_ERROR = 0x08; + + @IntDef({ + EMPTY, + ERROR, + NET_ERROR, + }) + @Retention(RetentionPolicy.SOURCE) + @interface RetryableState { + + } + + @IntDef({ + EMPTY, + ERROR, + CONTENT, + LOADING, + NET_ERROR, + BLANK, + REQUESTING, + SERVER_ERROR, + }) + @Retention(RetentionPolicy.SOURCE) + @interface ViewState { + + } + + StateLayoutConfig setStateMessage(@RetryableState int state, CharSequence message); + + StateLayoutConfig setStateIcon(@RetryableState int state, Drawable drawable); + + StateLayoutConfig setStateIcon(@RetryableState int state, @DrawableRes int drawableId); + + StateLayoutConfig setStateAction(@RetryableState int state, CharSequence actionText); + + StateLayoutConfig setStateRetryListener(OnRetryActionListener retryActionListener); + + StateLayoutConfig disableOperationWhenRequesting(boolean disable); + +} diff --git a/lib_base/src/main/java/com/android/base/app/ui/SwipeRefreshView.java b/lib_base/src/main/java/com/android/base/app/ui/SwipeRefreshView.java new file mode 100644 index 0000000..5650b6c --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/ui/SwipeRefreshView.java @@ -0,0 +1,51 @@ +package com.android.base.app.ui; + +import android.support.v4.widget.SwipeRefreshLayout; + +class SwipeRefreshView implements RefreshView { + + private SwipeRefreshLayout mSwipeRefreshLayout; + private RefreshHandler mRefreshHandler; + + SwipeRefreshView(SwipeRefreshLayout swipeRefreshLayout) { + mSwipeRefreshLayout = swipeRefreshLayout; + } + + /** + * SwipeRefreshLayout + */ + @Override + public void autoRefresh() { + mSwipeRefreshLayout.setRefreshing(true); + doRefresh(); + } + + @Override + public void refreshCompleted() { + mSwipeRefreshLayout.setRefreshing(false); + } + + @Override + public void setRefreshHandler(final RefreshHandler refreshHandler) { + mRefreshHandler = refreshHandler; + mSwipeRefreshLayout.setOnRefreshListener(this::doRefresh); + } + + private void doRefresh() { + if (mRefreshHandler.canRefresh()) { + mRefreshHandler.onRefresh(); + } else { + mSwipeRefreshLayout.post(() -> mSwipeRefreshLayout.setRefreshing(false)); + } + } + + @Override + public boolean isRefreshing() { + return mSwipeRefreshLayout.isRefreshing(); + } + + @Override + public void setRefreshEnable(boolean enable) { + mSwipeRefreshLayout.setEnabled(enable); + } +} diff --git a/lib_base/src/main/java/com/android/base/app/ui/UIEx.kt b/lib_base/src/main/java/com/android/base/app/ui/UIEx.kt new file mode 100644 index 0000000..d13e9e0 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/ui/UIEx.kt @@ -0,0 +1,153 @@ +@file:JvmName("UIKit") + +package com.android.base.app.ui + +import com.android.base.app.BaseKit +import com.android.base.utils.common.Checker + +fun RefreshListLayout.processListResultWithStatus(list: List?, onEmpty: (() -> Unit)? = null) { + if (isLoadingMore) { + if (!Checker.isEmpty(list)) { + addData(list) + } + } else { + replaceData(list) + refreshCompleted() + } + + if (pager != null) { + loadMoreCompleted(list != null && pager.hasMore(list.size)) + } + + if (isEmpty) { + if (onEmpty == null) { + showEmptyLayout() + } else { + onEmpty() + } + } else { + showContentLayout() + } +} + +fun RefreshListLayout.processListResultWithoutStatus(list: List?, onEmpty: (() -> Unit)? = null) { + if (isLoadingMore) { + if (!Checker.isEmpty(list)) { + addData(list) + } + } else { + replaceData(list) + refreshCompleted() + } + + if (pager != null) { + loadMoreCompleted(list != null && pager.hasMore(list.size)) + } + + if (onEmpty != null && isEmpty) { + onEmpty() + } + +} + +fun RefreshListLayout.submitListResultWithStatus(list: List?, hasMore: Boolean, onEmpty: (() -> Unit)? = null) { + if (isRefreshing) { + refreshCompleted() + } + + replaceData(list) + loadMoreCompleted(hasMore) + + if (isEmpty) { + if (onEmpty == null) { + showEmptyLayout() + } else { + onEmpty() + } + } else { + showContentLayout() + } +} + +fun RefreshListLayout.submitListResultWithoutStatus(list: List?, hasMore: Boolean, onEmpty: (() -> Unit)? = null) { + if (isRefreshing) { + refreshCompleted() + } + + replaceData(list) + loadMoreCompleted(hasMore) + + if (onEmpty != null && isEmpty) { + onEmpty() + } +} + +fun RefreshListLayout<*>.processListErrorWithStatus(throwable: Throwable) { + if (isRefreshing) { + refreshCompleted() + } + if (isLoadingMore) { + loadMoreFailed() + } + if (isEmpty) { + val errorTypeClassifier = BaseKit.get().errorClassifier() + if (errorTypeClassifier != null) { + when { + errorTypeClassifier.isNetworkError(throwable) -> showNetErrorLayout() + errorTypeClassifier.isServerError(throwable) -> showServerErrorLayout() + else -> showErrorLayout() + } + } else { + showErrorLayout() + } + } else { + showContentLayout() + } +} + +fun RefreshListLayout<*>.processListErrorWithoutStatus() { + if (isRefreshing) { + refreshCompleted() + } + if (isLoadingMore) { + loadMoreFailed() + } +} + +fun RefreshListLayout<*>.showLoadingIfEmpty() { + if (isEmpty) { + if (isRefreshing) { + showBlank() + } else { + showLoadingLayout() + } + } +} + +fun RefreshStateLayout.processResultWithStatus(t: T?, onResult: ((T) -> Unit)) { + if (isRefreshing) { + refreshCompleted() + } + if (t == null || (t is Collection<*> && t.isEmpty()) || (t is Map<*, *> && t.isEmpty())) { + showEmptyLayout() + } else { + onResult.invoke(t) + showContentLayout() + } +} + +fun RefreshStateLayout.processErrorWithStatus(throwable: Throwable) { + if (isRefreshing) { + refreshCompleted() + } + val errorTypeClassifier = BaseKit.get().errorClassifier() + if (errorTypeClassifier != null) { + when { + errorTypeClassifier.isNetworkError(throwable) -> showNetErrorLayout() + errorTypeClassifier.isServerError(throwable) -> showServerErrorLayout() + else -> showErrorLayout() + } + } else { + showErrorLayout() + } +} diff --git a/lib_base/src/main/java/com/android/base/concurrent/GoUtil.java b/lib_base/src/main/java/com/android/base/concurrent/GoUtil.java new file mode 100644 index 0000000..1fa539c --- /dev/null +++ b/lib_base/src/main/java/com/android/base/concurrent/GoUtil.java @@ -0,0 +1,106 @@ +package com.android.base.concurrent; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.util.Log; + +import java.io.File; +import java.io.FileFilter; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.regex.Pattern; + +/** + * @author MaTianyu + * @see
android-lite-go + */ +class GoUtil { + + private static final String TAG = GoUtil.class.getSimpleName(); + + private static final String PATH_CPU = "/sys/devices/system/cpu/"; + private static final String CPU_FILTER = "cpu[0-9]+"; + private static int CPU_CORES = 0; + + public static String formatDate(long millis) { + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + return format.format(new Date(millis)); + } + + /** + * Get available processors. + */ + public static int getProcessorsCount() { + return Runtime.getRuntime().availableProcessors(); + } + + /** + * Gets the number of cores available in this device, across all processors. + * Requires: Ability to peruse the filesystem at "/sys/devices/system/cpu" + * + * @return The number of cores, or available processors if failed to get result + */ + public static int getCoresNumbers() { + if (CPU_CORES > 0) { + return CPU_CORES; + } + //Private Class to display only CPU devices in the directory listing + class CpuFilter implements FileFilter { + @Override + public boolean accept(File pathname) { + //Check if filename is "cpu", followed by a single digit number + if (Pattern.matches(CPU_FILTER, pathname.getName())) { + return true; + } + return false; + } + } + try { + //Get directory containing CPU info + File dir = new File(PATH_CPU); + //Filter to only list the devices we care about + File[] files = dir.listFiles(new CpuFilter()); + //Return the number of cores (virtual CPU devices) + CPU_CORES = files.length; + } catch (Exception e) { + e.printStackTrace(); + } + if (CPU_CORES < 1) { + CPU_CORES = Runtime.getRuntime().availableProcessors(); + } + if (CPU_CORES < 1) { + CPU_CORES = 1; + } + Log.i(TAG, "CPU cores: " + CPU_CORES); + return CPU_CORES; + } + + public static AlertDialog.Builder dialogBuilder(Context context, String title, String msg) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + if (msg != null) { + builder.setMessage(msg); + } + if (title != null) { + builder.setTitle(title); + } + return builder; + } + + + public static Dialog showTips(Context context, String title, String des) { + return showTips(context, title, des, null, null); + } + + public static Dialog showTips(Context context, String title, String des, String btn, + DialogInterface.OnDismissListener dismissListener) { + AlertDialog.Builder builder = dialogBuilder(context, title, des); + builder.setCancelable(true); + builder.setPositiveButton(btn, null); + Dialog dialog = builder.show(); + dialog.setCanceledOnTouchOutside(true); + dialog.setOnDismissListener(dismissListener); + return dialog; + } +} diff --git a/lib_base/src/main/java/com/android/base/concurrent/JobExecutor.java b/lib_base/src/main/java/com/android/base/concurrent/JobExecutor.java new file mode 100644 index 0000000..eea4f92 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/concurrent/JobExecutor.java @@ -0,0 +1,37 @@ +package com.android.base.concurrent; + +import java.util.concurrent.Executor; + + +/** + * Smart执行器 + * + * @see android-lite-go + */ +public class JobExecutor { + + private static SmartExecutor smartExecutor; + + public static void setSchedulePolicy(SchedulePolicy policy) { + if (smartExecutor != null) { + smartExecutor.setSchedulePolicy(policy); + } + } + + public static void setOverloadPolicy(OverloadPolicy policy) { + if (smartExecutor != null) { + smartExecutor.setOverloadPolicy(policy); + } + } + + static { + smartExecutor = new SmartExecutor(); + smartExecutor.setSchedulePolicy(SchedulePolicy.LastInFirstRun); + smartExecutor.setOverloadPolicy(OverloadPolicy.DiscardOldTaskInQueue); + } + + public static Executor getJobExecutor() { + return smartExecutor; + } + +} diff --git a/lib_base/src/main/java/com/android/base/concurrent/OverloadPolicy.java b/lib_base/src/main/java/com/android/base/concurrent/OverloadPolicy.java new file mode 100644 index 0000000..39fd1cd --- /dev/null +++ b/lib_base/src/main/java/com/android/base/concurrent/OverloadPolicy.java @@ -0,0 +1,31 @@ +/* + * Copyright 2016 litesuits.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.base.concurrent; + + +/** + * Policy of thread-pool-executor overload. + * + * @author MaTianyu + * @date 2015-04-23 + */ +public enum OverloadPolicy { + DiscardNewTaskInQueue, + DiscardOldTaskInQueue, + DiscardCurrentTask, + CallerRuns, + ThrowExecption +} diff --git a/lib_base/src/main/java/com/android/base/concurrent/PriorityRunnable.java b/lib_base/src/main/java/com/android/base/concurrent/PriorityRunnable.java new file mode 100644 index 0000000..bb26008 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/concurrent/PriorityRunnable.java @@ -0,0 +1,37 @@ +/* + * Copyright 2016 litesuits.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.base.concurrent; + +/** + * @author MaTianyu + * @date 2015-04-23 + */ +public abstract class PriorityRunnable implements Runnable { + + int priority; + + protected PriorityRunnable(int priority) { + this.priority = priority; + } + + public int getPriority() { + return priority; + } + + public void setPriority(int priority) { + this.priority = priority; + } +} diff --git a/lib_base/src/main/java/com/android/base/concurrent/SchedulePolicy.java b/lib_base/src/main/java/com/android/base/concurrent/SchedulePolicy.java new file mode 100644 index 0000000..16db5ef --- /dev/null +++ b/lib_base/src/main/java/com/android/base/concurrent/SchedulePolicy.java @@ -0,0 +1,25 @@ +/* + * Copyright 2016 litesuits.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.base.concurrent; + +/** + * @author MaTianyu + * @date 2015-04-23 + */ +public enum SchedulePolicy { + LastInFirstRun, + FirstInFistRun +} diff --git a/lib_base/src/main/java/com/android/base/concurrent/SmartExecutor.java b/lib_base/src/main/java/com/android/base/concurrent/SmartExecutor.java new file mode 100644 index 0000000..d5e54a0 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/concurrent/SmartExecutor.java @@ -0,0 +1,416 @@ +/* + * Copyright 2016 litesuits.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.base.concurrent; + +import android.util.Log; + +import java.util.LinkedList; +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.concurrent.RunnableFuture; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A smart thread pool executor, about {@link SmartExecutor}: + * + *
    + *
  • keep {@link #coreSize} tasks concurrent, and put them in {@link #runningList}, + * maximum number of running-tasks at the same time is {@link #coreSize}.
  • + *
  • when {@link #runningList} is full, put new task in {@link #waitingList} waiting for execution, + * maximum of waiting-tasks number is {@link #queueSize}.
  • + *
  • when {@link #waitingList} is full, new task is performed by {@link OverloadPolicy}.
  • + *
  • when running task is completed, take it out from {@link #runningList}.
  • + *
  • schedule next by {@link SchedulePolicy}, take next task out from {@link #waitingList} to execute, + * and so on until {@link #waitingList} is empty.
  • + * + *
+ * + * @author MaTianyu + * @date 2015-04-23 + */ + class SmartExecutor implements Executor { + private static final String TAG = SmartExecutor.class.getSimpleName(); + /** + * debug mode turn + */ + private boolean debug = false; + + private static final int CPU_CORE = GoUtil.getCoresNumbers(); + private static final int DEFAULT_CACHE_SENCOND = 5; + private static ThreadPoolExecutor threadPool; + private int coreSize = CPU_CORE; + private int queueSize = coreSize * 32; + private final Object lock = new Object(); + private LinkedList runningList = new LinkedList(); + private LinkedList waitingList = new LinkedList(); + private SchedulePolicy schedulePolicy = SchedulePolicy.FirstInFistRun; + private OverloadPolicy overloadPolicy = OverloadPolicy.DiscardOldTaskInQueue; + + + public SmartExecutor() { + initThreadPool(); + } + + public SmartExecutor(int coreSize, int queueSize) { + this.coreSize = coreSize; + this.queueSize = queueSize; + initThreadPool(); + } + + protected synchronized void initThreadPool() { + if (debug) { + Log.v(TAG, "SmartExecutor core-queue size: " + coreSize + " - " + queueSize + + " running-wait task: " + runningList.size() + " - " + waitingList.size()); + } + if (threadPool == null) { + threadPool = createDefaultThreadPool(); + } + } + + public static ThreadPoolExecutor createDefaultThreadPool() { + // 控制最多4个keep在pool中 + int corePoolSize = Math.min(4, CPU_CORE); + return new ThreadPoolExecutor( + corePoolSize, + Integer.MAX_VALUE, + DEFAULT_CACHE_SENCOND, TimeUnit.SECONDS, + new SynchronousQueue(), + new ThreadFactory() { + static final String NAME = "lite-"; + AtomicInteger IDS = new AtomicInteger(1); + + @Override + public Thread newThread(Runnable r) { + return new Thread(r, NAME + IDS.getAndIncrement()); + } + }, + new ThreadPoolExecutor.DiscardPolicy()); + } + + /** + * turn on or turn off debug mode + */ + public void setDebug(boolean debug) { + this.debug = debug; + } + + public boolean isDebug() { + return debug; + } + + public static void setThreadPool(ThreadPoolExecutor threadPool) { + SmartExecutor.threadPool = threadPool; + } + + public static ThreadPoolExecutor getThreadPool() { + return threadPool; + } + + public boolean cancelWaitingTask(Runnable command) { + boolean removed = false; + synchronized (lock) { + int size = waitingList.size(); + if (size > 0) { + for (int i = size - 1; i >= 0; i--) { + if (waitingList.get(i).getRealRunnable() == command) { + waitingList.remove(i); + removed = true; + } + } + } + } + return removed; + } + + interface WrappedRunnable extends Runnable { + Runnable getRealRunnable(); + } + + protected RunnableFuture newTaskFor(Runnable runnable, T value) { + return new FutureTask(runnable, value); + } + + protected RunnableFuture newTaskFor(Callable callable) { + return new FutureTask(callable); + } + + /** + * submit runnable + */ + public Future submit(Runnable task) { + RunnableFuture ftask = newTaskFor(task, null); + execute(ftask); + return ftask; + } + + /** + * Creates a {@code FutureTask} that will, upon running, execute the + * given {@code Runnable}, and arrange that {@code get} will return the + * given result on successful completion. + * + * @param task the runnable task + * @param result the result to return on successful completion. If + * you don't need a particular result, consider using + * constructions of the form: + * {@code Future f = new FutureTask(runnable, null)} + * @throws NullPointerException if the runnable is null + */ + public Future submit(Runnable task, T result) { + RunnableFuture ftask = newTaskFor(task, result); + execute(ftask); + return ftask; + } + + /** + * submit callable + */ + public Future submit(Callable task) { + RunnableFuture ftask = newTaskFor(task); + execute(ftask); + return ftask; + } + + + /** + * submit RunnableFuture task + */ + public void submit(RunnableFuture task) { + execute(task); + } + + + /** + * When {@link #execute(Runnable)} is called, {@link SmartExecutor} perform actions: + *
    + *
  1. if fewer than {@link #coreSize} tasks are running, post new task in {@link #runningList} and execute it immediately.
  2. + *
  3. if more than {@link #coreSize} tasks are running, and fewer than {@link #queueSize} tasks are waiting, put task in {@link #waitingList}.
  4. + *
  5. if more than {@link #queueSize} tasks are waiting ,schedule new task by {@link OverloadPolicy}
  6. + *
  7. if running task is completed, schedule next task by {@link SchedulePolicy} until {@link #waitingList} is empty.
  8. + *
+ */ + @Override + public void execute(final Runnable command) { + if (command == null) { + return; + } + + WrappedRunnable scheduler = new WrappedRunnable() { + @Override + public Runnable getRealRunnable() { + return command; + } + + public Runnable realRunnable; + + @Override + public void run() { + try { + command.run(); + } finally { + scheduleNext(this); + } + } + }; + + boolean callerRun = false; + synchronized (lock) { + //if (debug) { + // Log.v(TAG, "SmartExecutor core-queue size: " + coreSize + " - " + queueSize + // + " running-wait task: " + runningList.size() + " - " + waitingList.size()); + //} + if (runningList.size() < coreSize) { + runningList.add(scheduler); + threadPool.execute(scheduler); + //Log.v(TAG, "SmartExecutor task execute"); + } else if (waitingList.size() < queueSize) { + waitingList.addLast(scheduler); + //Log.v(TAG, "SmartExecutor task waiting"); + } else { + //if (debug) { + // Log.w(TAG, "SmartExecutor overload , policy is: " + overloadPolicy); + //} + switch (overloadPolicy) { + case DiscardNewTaskInQueue: + waitingList.pollLast(); + waitingList.addLast(scheduler); + break; + case DiscardOldTaskInQueue: + waitingList.pollFirst(); + waitingList.addLast(scheduler); + break; + case CallerRuns: + callerRun = true; + break; + case DiscardCurrentTask: + break; + case ThrowExecption: + throw new RuntimeException("Task rejected from lite smart executor. " + command.toString()); + default: + break; + } + } + //printThreadPoolInfo(); + } + if (callerRun) { + if (debug) { + Log.i(TAG, "SmartExecutor task running in caller thread"); + } + command.run(); + } + } + + private void scheduleNext(WrappedRunnable scheduler) { + synchronized (lock) { + boolean suc = runningList.remove(scheduler); + //if (debug) { + // Log.v(TAG, "Thread " + Thread.currentThread().getName() + // + " is completed. remove prior: " + suc + ", try schedule next.."); + //} + if (!suc) { + runningList.clear(); + Log.e(TAG, + "SmartExecutor scheduler remove failed, so clear all(running list) to avoid unpreditable error : " + scheduler); + } + if (waitingList.size() > 0) { + WrappedRunnable waitingRun; + switch (schedulePolicy) { + case LastInFirstRun: + waitingRun = waitingList.pollLast(); + break; + case FirstInFistRun: + waitingRun = waitingList.pollFirst(); + break; + default: + waitingRun = waitingList.pollLast(); + break; + } + if (waitingRun != null) { + runningList.add(waitingRun); + threadPool.execute(waitingRun); + Log.v(TAG, "Thread " + Thread.currentThread().getName() + " execute next task.."); + } else { + Log.e(TAG, + "SmartExecutor get a NULL task from waiting queue: " + Thread.currentThread().getName()); + } + } else { + if (debug) { + Log.v(TAG, "SmartExecutor: all tasks is completed. current thread: " + + Thread.currentThread().getName()); + //printThreadPoolInfo(); + } + } + } + } + + public void printThreadPoolInfo() { + if (debug) { + Log.i(TAG, "___________________________"); + Log.i(TAG, "state (shutdown - terminating - terminated): " + threadPool.isShutdown() + + " - " + threadPool.isTerminating() + " - " + threadPool.isTerminated()); + Log.i(TAG, "pool size (core - max): " + threadPool.getCorePoolSize() + + " - " + threadPool.getMaximumPoolSize()); + Log.i(TAG, "task (active - complete - total): " + threadPool.getActiveCount() + + " - " + threadPool.getCompletedTaskCount() + " - " + threadPool.getTaskCount()); + Log + .i(TAG, "waitingList size : " + threadPool.getQueue().size() + " , " + threadPool.getQueue()); + } + } + + public int getCoreSize() { + return coreSize; + } + + public int getRunningSize() { + return runningList.size(); + } + + public int getWaitingSize() { + return waitingList.size(); + } + + /** + * Set maximum number of concurrent tasks at the same time. + * Recommended core size is CPU count. + * + * @param coreSize number of concurrent tasks at the same time + * @return this + */ + public SmartExecutor setCoreSize(int coreSize) { + if (coreSize <= 0) { + throw new NullPointerException("coreSize can not <= 0 !"); + } + this.coreSize = coreSize; + if (debug) { + Log.v(TAG, "SmartExecutor core-queue size: " + coreSize + " - " + queueSize + + " running-wait task: " + runningList.size() + " - " + waitingList.size()); + } + return this; + } + + public int getQueueSize() { + return queueSize; + } + + /** + * Adjust maximum number of waiting queue size by yourself or based on phone performance. + * For example: CPU count * 32; + * + * @param queueSize waiting queue size + * @return this + */ + public SmartExecutor setQueueSize(int queueSize) { + if (queueSize < 0) { + throw new NullPointerException("queueSize can not < 0 !"); + } + + this.queueSize = queueSize; + if (debug) { + Log.v(TAG, "SmartExecutor core-queue size: " + coreSize + " - " + queueSize + + " running-wait task: " + runningList.size() + " - " + waitingList.size()); + } + return this; + } + + + public OverloadPolicy getOverloadPolicy() { + return overloadPolicy; + } + + public void setOverloadPolicy(OverloadPolicy overloadPolicy) { + if (overloadPolicy == null) { + throw new NullPointerException("OverloadPolicy can not be null !"); + } + this.overloadPolicy = overloadPolicy; + } + + public SchedulePolicy getSchedulePolicy() { + return schedulePolicy; + } + + public void setSchedulePolicy(SchedulePolicy schedulePolicy) { + if (schedulePolicy == null) { + throw new NullPointerException("SchedulePolicy can not be null !"); + } + this.schedulePolicy = schedulePolicy; + } + +} diff --git a/lib_base/src/main/java/com/android/base/data/Resource.java b/lib_base/src/main/java/com/android/base/data/Resource.java new file mode 100644 index 0000000..f0be229 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/data/Resource.java @@ -0,0 +1,119 @@ +package com.android.base.data; + + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-05-15 16:23 + */ +public class Resource { + + private final Throwable mError; + private final Status mStatus; + private final T mData; //data or default data + private final T mDefaultData; //data or default data + + private Resource(Throwable error, T data, T defaultData, Status status) { + mError = error; + mStatus = status; + mData = data; + mDefaultData = defaultData; + } + + public boolean isSuccess() { + return mStatus == Status.SUCCESS; + } + + public boolean isNoChange() { + return mStatus == Status.NOT_CHANGED; + } + + public boolean isLoading() { + return mStatus == Status.LOADING; + } + + public final boolean isError() { + return mStatus == Status.ERROR; + } + + public final boolean hasData() { + return mData != null; + } + + public static Resource success() { + return new Resource<>(null, null, null, Status.SUCCESS); + } + + public static Resource success(@Nullable T t) { + return new Resource<>(null, t, null, Status.SUCCESS); + } + + public static Resource error(@NonNull Throwable error) { + return error(error, null); + } + + public static Resource error(@NonNull Throwable error, T defaultValue) { + return new Resource<>(error, null, defaultValue, Status.ERROR); + } + + public static Resource loading() { + return loading(null); + } + + public static Resource loading(T defaultValue) { + return new Resource<>(null, null, defaultValue, Status.LOADING); + } + + /** + * 如果数据源(比如 Repository)缓存了上一次请求的数据,然后对其当前请求返回的数据,发现数据是一样的,可以使用此状态表示 + * + * @return Resource + */ + public static Resource noChange() { + return new Resource<>(null, null, null, Status.NOT_CHANGED); + } + + @NonNull + public T data() { + if (isError() || isLoading() || isNoChange()) { + throw new UnsupportedOperationException("This method can only be called when the state success"); + } + if (mData == null) { + throw new NullPointerException("Data is null"); + } + return mData; + } + + @Nullable + public T orElse(@Nullable T elseData) { + if (mData == null) { + return elseData; + } + return mData; + } + + @Nullable + public T defaultData() { + return mDefaultData; + } + + public Throwable error() { + return mError; + } + + @NonNull + @Override + public String toString() { + return "Resource{" + + "mError=" + mError + + ", mStatus=" + mStatus + + ", mData=" + mData + + ", mDefaultData=" + mDefaultData + + '}'; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/data/Resource.kt b/lib_base/src/main/java/com/android/base/data/Resource.kt new file mode 100644 index 0000000..2ac50d9 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/data/Resource.kt @@ -0,0 +1,45 @@ +package com.android.base.data + +import android.arch.lifecycle.LiveData + + +inline fun Resource.onLoading(onLoading: () -> Unit): Resource { + if (this.isLoading) { + onLoading() + } + return this +} + +inline fun Resource.onError(onError: (error: Throwable) -> Unit): Resource { + if (this.isError) { + onError(error()) + } + return this +} + +inline fun Resource.onNoChange(onNoChange: () -> Unit): Resource { + if (this.isNoChange) { + onNoChange() + } + return this +} + +/**success*/ +inline fun Resource.onSuccess(onSuccess: (data: T?) -> Unit): Resource { + if (this.isSuccess) { + onSuccess(this.orElse(null)) + } + return this +} + +/**success with data*/ +inline fun Resource.onSuccessWithData(onSuccess: (data: T) -> Unit): Resource { + if (this.isSuccess && this.hasData()) { + onSuccess(this.data()) + } + return this +} + +fun LiveData>.resourceData(): T? { + return if (value?.isSuccess == true) value?.orElse(null) else null +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/data/Status.java b/lib_base/src/main/java/com/android/base/data/Status.java new file mode 100644 index 0000000..9add2c2 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/data/Status.java @@ -0,0 +1,15 @@ +package com.android.base.data; + +/** + * 用于表示各种状态 + * + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-02-18 11:49 + */ +public enum Status { + LOADING, + ERROR, + SUCCESS, + NOT_CHANGED, +} diff --git a/lib_base/src/main/java/com/android/base/imageloader/DisplayConfig.java b/lib_base/src/main/java/com/android/base/imageloader/DisplayConfig.java new file mode 100644 index 0000000..8c8fe3c --- /dev/null +++ b/lib_base/src/main/java/com/android/base/imageloader/DisplayConfig.java @@ -0,0 +1,137 @@ +package com.android.base.imageloader; + + +import android.graphics.drawable.Drawable; + +public class DisplayConfig { + + private boolean mCacheMemory = true; + private boolean mCacheDisk = true; + private int mTransform; + private int mScaleType; + + /*placeholder*/ + static final int NO_PLACE_HOLDER = -1; + private int mErrorPlaceholder = NO_PLACE_HOLDER; + private int mLoadingPlaceholder = NO_PLACE_HOLDER; + private Drawable mErrorDrawable = null; + private Drawable mLoadingDrawable = null; + + /*scale type*/ + public static final int SCALE_NONE = 0; + public static final int SCALE_CENTER_CROP = 1; + public static final int SCALE_FIT_CENTER = 2; + /*transform*/ + public static final int TRANSFORM_NONE = 1; + public static final int TRANSFORM_CIRCLE = 2; + public static final int TRANSFORM_ROUNDED_CORNERS = 3; + private int mRoundedCornersRadius; + /*animation*/ + public static final int ANIM_NONE = 1; + + private DisplayConfig() { + + } + + public static DisplayConfig create() { + return new DisplayConfig(); + } + + public DisplayConfig setErrorPlaceholder(int errorPlaceholder) { + mErrorPlaceholder = errorPlaceholder; + mErrorDrawable = null; + return this; + } + + public DisplayConfig setLoadingPlaceholder(int loadingPlaceholder) { + mLoadingPlaceholder = loadingPlaceholder; + mErrorDrawable = null; + return this; + } + + public Drawable getErrorDrawable() { + return mErrorDrawable; + } + + public Drawable getLoadingDrawable() { + return mLoadingDrawable; + } + + /** + * @param scaleType {@link #SCALE_CENTER_CROP} or{@link #SCALE_FIT_CENTER} + * @return DisplayConfig + */ + public DisplayConfig scaleType(int scaleType) { + mScaleType = scaleType; + return this; + } + + public DisplayConfig cacheMemory(boolean cacheMemory) { + mCacheMemory = cacheMemory; + return this; + } + + public DisplayConfig setCacheDisk(boolean cacheDisk) { + mCacheDisk = cacheDisk; + return this; + } + + public DisplayConfig setErrorDrawable(Drawable errorDrawable) { + mErrorDrawable = errorDrawable; + mErrorPlaceholder = NO_PLACE_HOLDER; + return this; + } + + public DisplayConfig setLoadingDrawable(Drawable loadingDrawable) { + mLoadingPlaceholder = NO_PLACE_HOLDER; + mLoadingDrawable = loadingDrawable; + return this; + } + + /** + * @param transform {@link #TRANSFORM_ROUNDED_CORNERS} or{@link #TRANSFORM_CIRCLE} + * @return DisplayConfig + */ + public DisplayConfig setTransform(int transform) { + mTransform = transform; + return this; + } + + public DisplayConfig setRoundedCornersRadius(int roundedCornersRadius) { + mRoundedCornersRadius = roundedCornersRadius; + return this; + } + + /////////////////////////////////////////////////////////////////////////// + // Getter + /////////////////////////////////////////////////////////////////////////// + + int getScaleType() { + return mScaleType; + } + + boolean isCacheMemory() { + return mCacheMemory; + } + + boolean isCacheDisk() { + return mCacheDisk; + } + + int getTransform() { + return mTransform; + } + + int getErrorPlaceholder() { + return mErrorPlaceholder; + } + + int getLoadingPlaceholder() { + return mLoadingPlaceholder; + } + + int getRoundedCornersRadius() { + return mRoundedCornersRadius; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/imageloader/GlideImageLoader.java b/lib_base/src/main/java/com/android/base/imageloader/GlideImageLoader.java new file mode 100644 index 0000000..df71392 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/imageloader/GlideImageLoader.java @@ -0,0 +1,389 @@ +package com.android.base.imageloader; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.view.View; +import android.widget.ImageView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.RequestBuilder; +import com.bumptech.glide.RequestManager; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.load.resource.bitmap.RoundedCorners; +import com.bumptech.glide.request.RequestOptions; +import com.bumptech.glide.request.target.ImageViewTarget; +import com.bumptech.glide.request.target.SimpleTarget; +import com.bumptech.glide.request.transition.Transition; + +import java.util.concurrent.ExecutionException; + +class GlideImageLoader implements ImageLoader { + + private boolean canFragmentLoadImage(Fragment fragment) { + return fragment.isResumed() || fragment.isAdded() || fragment.isVisible(); + } + + @Override + public void display(Fragment fragment, ImageView imageView, final String url, final LoadListener loadListener) { + if (canFragmentLoadImage(fragment)) { + Glide.with(fragment).load(url).into(new InnerImageTarget(imageView, url, loadListener)); + } + } + + @Override + public void display(Fragment fragment, ImageView imageView, String url, DisplayConfig displayConfig, LoadListener loadListener) { + if (!canFragmentLoadImage(fragment)) { + return; + } + RequestBuilder requestBuilder = Glide.with(fragment).load(url); + if (displayConfig != null) { + RequestOptions requestOptions = buildRequestOptions(displayConfig); + requestBuilder.apply(requestOptions); + } + requestBuilder.into(new InnerImageTarget(imageView, url, loadListener)); + } + + @Override + public void display(ImageView imageView, String url, LoadListener loadListener) { + Glide.with(imageView.getContext()).load(url).into(new InnerImageTarget(imageView, url, loadListener)); + } + + @Override + public void display(ImageView imageView, String url, DisplayConfig config, LoadListener loadListener) { + RequestBuilder requestBuilder = Glide.with(imageView.getContext()).load(url); + if (config != null) { + RequestOptions requestOptions = buildRequestOptions(config); + requestBuilder.apply(requestOptions); + } + requestBuilder.into(new InnerImageTarget(imageView, url, loadListener)); + } + + + @Override + public void removeAllListener(String url) { + ProgressManager.getInstance().removeListener(url); + } + + @Override + public void addListener(@NonNull String url, @NonNull ProgressListener progressListener) { + ProgressManager.getInstance().addLoadListener(url, progressListener); + } + + @Override + public void setListener(String url, ProgressListener progressListener) { + ProgressManager.getInstance().setListener(url, progressListener); + } + + private class InnerImageTarget extends ImageViewTarget { + + private final LoadListener mLoadListener; + + InnerImageTarget(ImageView view, @SuppressWarnings("unused") String url, LoadListener loadListener) { + super(view); + mLoadListener = loadListener; + } + + @Override + protected void setResource(@Nullable Drawable resource) { + getView().setImageDrawable(resource); + } + + @Override + public void onResourceReady(Drawable resource, @Nullable Transition transition) { + super.onResourceReady(resource, transition); + mLoadListener.onLoadSuccess(resource); + } + + @Override + public void onLoadStarted(@Nullable Drawable placeholder) { + mLoadListener.onLoadStart(); + super.onLoadStarted(placeholder); + } + + @Override + public void onLoadFailed(@Nullable Drawable errorDrawable) { + super.onLoadFailed(errorDrawable); + mLoadListener.onLoadFail(); + } + } + + /////////////////////////////////////////////////////////////////////////// + // Progress end + /////////////////////////////////////////////////////////////////////////// + + @Override + public void display(Fragment fragment, ImageView imageView, String url) { + if (canFragmentLoadImage(fragment)) { + Glide.with(fragment).load(url).into(imageView); + } + } + + @Override + public void display(Fragment fragment, ImageView imageView, String url, DisplayConfig displayConfig) { + display(imageView, Glide.with(fragment).load(url), displayConfig); + } + + @Override + public void display(Fragment fragment, ImageView imageView, Source source) { + display(fragment, imageView, source, null); + } + + @Override + public void display(Fragment fragment, ImageView imageView, Source source, DisplayConfig displayConfig) { + if (!canFragmentLoadImage(fragment)) { + return; + } + RequestManager requestManager = Glide.with(fragment); + RequestBuilder drawableTypeRequest = setToRequest(requestManager, source); + display(imageView, drawableTypeRequest, displayConfig); + } + + @Override + public void display(ImageView imageView, String url) { + display(imageView, Glide.with(imageView.getContext()).load(url), null); + } + + @Override + public void display(ImageView imageView, String url, DisplayConfig config) { + display(imageView, Glide.with(imageView.getContext()).load(url), config); + } + + @Override + public void display(ImageView imageView, Source source) { + RequestBuilder drawableTypeRequest = setToRequest(Glide.with(imageView.getContext()), source); + display(imageView, drawableTypeRequest, null); + } + + @Override + public void display(ImageView imageView, Source source, DisplayConfig config) { + RequestBuilder drawableTypeRequest = setToRequest(Glide.with(imageView.getContext()), source); + display(imageView, drawableTypeRequest, config); + } + + private void display(ImageView imageView, RequestBuilder request, DisplayConfig displayConfig) { + if (displayConfig != null) { + RequestOptions requestOptions = buildRequestOptions(displayConfig); + request.apply(requestOptions); + } + request.into(imageView); + } + + /////////////////////////////////////////////////////////////////////////// + // pause and resume + /////////////////////////////////////////////////////////////////////////// + @Override + public void pause(Fragment fragment) { + Glide.with(fragment).pauseRequests(); + } + + @Override + public void resume(Fragment fragment) { + Glide.with(fragment).resumeRequests(); + } + + @Override + public void pause(Context context) { + Glide.with(context).pauseRequests(); + } + + @Override + public void resume(Context context) { + Glide.with(context).resumeRequests(); + } + + + /////////////////////////////////////////////////////////////////////////// + //Load Bitmap + /////////////////////////////////////////////////////////////////////////// + + @Override + public void preload(Context context, Source source) { + RequestManager requestManager = Glide.with(context); + setToRequest(requestManager, source).preload(); + } + + @Override + public void preload(Context context, Source source, int width, int height) { + RequestManager requestManager = Glide.with(context); + setToRequest(requestManager, source).preload(width, height); + } + + @Override + public void loadBitmap(Context context, Source source, boolean cache, final LoadListener bitmapLoadListener) { + loadBitmap(context, source, cache, 0, 0, bitmapLoadListener); + } + + @Override + public void loadBitmap(Fragment fragment, Source source, boolean cache, LoadListener bitmapLoadListener) { + loadBitmap(fragment, source, cache, 0, 0, bitmapLoadListener); + } + + @Override + public void loadBitmap(Fragment fragment, Source source, boolean cache, int width, int height, LoadListener bitmapLoadListener) { + loadBitmapInternal(Glide.with(fragment), source, cache, width, height, bitmapLoadListener); + } + + @Override + public Bitmap loadBitmap(Context context, Source source, boolean cache, int width, int height) { + RequestBuilder requestBuilder = Glide.with(context).asBitmap(); + requestBuilder = setToRequest(requestBuilder, source); + if (width > 0 && height > 0) { + try { + return requestBuilder.submit(width, height).get(); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + } + } else { + try { + return requestBuilder.submit().get(); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + } + } + return null; + } + + @Override + public void loadBitmap(Context context, Source source, boolean cache, int width, int height, LoadListener bitmapLoadListener) { + loadBitmapInternal(Glide.with(context), source, cache, width, height, bitmapLoadListener); + } + + private void loadBitmapInternal(RequestManager requestManager, Source source, boolean cache, int width, int height, LoadListener bitmapLoadListener) { + RequestOptions requestOptions = new RequestOptions(); + if (cache) { + requestOptions.skipMemoryCache(false); + requestOptions.diskCacheStrategy(DiskCacheStrategy.ALL); + } else { + requestOptions.skipMemoryCache(true); + requestOptions.diskCacheStrategy(DiskCacheStrategy.NONE); + } + if (width > 0 && height > 0) { + requestOptions.override(width, height); + } + + RequestBuilder requestBuilder = requestManager.asBitmap().apply(requestOptions); + requestBuilder = setToRequest(requestBuilder, source); + requestBuilder.into(new SimpleTarget() { + @Override + public void onResourceReady(Bitmap resource, Transition transition) { + bitmapLoadListener.onLoadSuccess(resource); + } + + @Override + public void onLoadFailed(@Nullable Drawable errorDrawable) { + bitmapLoadListener.onLoadFail(); + } + + @Override + public void onLoadStarted(@Nullable Drawable placeholder) { + super.onLoadStarted(placeholder); + bitmapLoadListener.onLoadStart(); + } + }); + } + + @Override + public void clear(Context context) { + Glide.get(context).clearDiskCache(); + } + + @Override + public void clear(View view) { + Context context = view.getContext(); + if (context != null) { + Glide.with(context).clear(view); + } + } + + @Override + public void clear(Fragment fragment, View view) { + Glide.with(fragment).clear(view); + } + + /////////////////////////////////////////////////////////////////////////// + // Build request + /////////////////////////////////////////////////////////////////////////// + + private RequestOptions buildRequestOptions(DisplayConfig displayConfig) { + RequestOptions requestOptions = new RequestOptions(); + /*DiskCache*/ + if (displayConfig.isCacheDisk()) { + requestOptions.diskCacheStrategy(DiskCacheStrategy.ALL); + } else { + requestOptions.diskCacheStrategy(DiskCacheStrategy.NONE); + } + + /*MemoryCache*/ + if (displayConfig.isCacheMemory()) { + requestOptions.skipMemoryCache(false); + } else { + requestOptions.skipMemoryCache(true); + } + + /*SCALE_TYPE*/ + if (displayConfig.getScaleType() == DisplayConfig.SCALE_CENTER_CROP) { + requestOptions.centerCrop(); + } else if (displayConfig.getScaleType() == DisplayConfig.SCALE_FIT_CENTER) { + requestOptions.fitCenter(); + } + + /*transform*/ + if (displayConfig.getTransform() == DisplayConfig.TRANSFORM_CIRCLE) { + requestOptions.circleCrop(); + } else if (displayConfig.getTransform() == DisplayConfig.TRANSFORM_ROUNDED_CORNERS) { + requestOptions.transform(new RoundedCorners(displayConfig.getRoundedCornersRadius())); + } else if (displayConfig.getTransform() == DisplayConfig.TRANSFORM_NONE) { + requestOptions.dontTransform();//不做渐入渐出的转换 + } + + /*Placeholder*/ + if (displayConfig.getErrorPlaceholder() != DisplayConfig.NO_PLACE_HOLDER) { + requestOptions.error(displayConfig.getErrorPlaceholder()); + } + if (displayConfig.getErrorDrawable() != null) { + requestOptions.error(displayConfig.getErrorDrawable()); + } + + if (displayConfig.getLoadingPlaceholder() != DisplayConfig.NO_PLACE_HOLDER) { + requestOptions.placeholder(displayConfig.getLoadingPlaceholder()); + } + + if (displayConfig.getLoadingDrawable() != null) { + requestOptions.placeholder(displayConfig.getLoadingDrawable()); + } + return requestOptions; + } + + private RequestBuilder setToRequest(RequestManager requestManager, Source source) { + if (source.mFile != null) { + return requestManager.load(source.mFile); + } else if (source.mUrl != null) { + return requestManager.load(source.mUrl); + } else if (source.mResource != 0) { + return requestManager.load(source.mResource); + } else if (source.mUri != null) { + return requestManager.load(source.mUri); + } else { + throw new IllegalArgumentException("UnSupport source"); + } + } + + private RequestBuilder setToRequest(RequestBuilder requestBuilder, Source source) { + if (source.mFile != null) { + return requestBuilder.load(source.mFile); + } else if (source.mUrl != null) { + return requestBuilder.load(source.mUrl); + } else if (source.mResource != 0) { + return requestBuilder.load(source.mResource); + } else if (source.mUri != null) { + return requestBuilder.load(source.mUri); + } else { + throw new IllegalArgumentException("UnSupport source"); + } + } + +} diff --git a/lib_base/src/main/java/com/android/base/imageloader/ImageLoader.java b/lib_base/src/main/java/com/android/base/imageloader/ImageLoader.java new file mode 100644 index 0000000..ccd0c27 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/imageloader/ImageLoader.java @@ -0,0 +1,113 @@ +package com.android.base.imageloader; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; +import android.support.annotation.WorkerThread; +import android.support.v4.app.Fragment; +import android.view.View; +import android.widget.ImageView; + +// @formatter:off +public interface ImageLoader { + + void display(Fragment fragment, ImageView imageView, String url, LoadListener loadListener); + + void display(Fragment fragment, ImageView imageView, String url, DisplayConfig displayConfig, LoadListener loadListener); + + void display(ImageView imageView, String url, LoadListener loadListener); + + void display(ImageView imageView, String url, DisplayConfig config, LoadListener loadListener); + + void display(Fragment fragment, ImageView imageView, String url); + + void display(Fragment fragment, ImageView imageView, String url, DisplayConfig displayConfig); + + void display(Fragment fragment, ImageView imageView, Source source); + + void display(Fragment fragment, ImageView imageView, Source source, DisplayConfig displayConfig); + + void display(ImageView imageView, String url); + + void display(ImageView imageView, String url, DisplayConfig config); + + void display(ImageView imageView, Source source); + + void display(ImageView imageView, Source source, DisplayConfig config); + + + /////////////////////////////////////////////////////////////////////////// + // pause and resume + /////////////////////////////////////////////////////////////////////////// + void pause(Fragment fragment); + + void resume(Fragment fragment); + + void pause(Context context); + + void resume(Context context); + + /////////////////////////////////////////////////////////////////////////// + // preload + /////////////////////////////////////////////////////////////////////////// + void preload(Context context, Source source); + + void preload(Context context, Source source, int width, int height); + + /////////////////////////////////////////////////////////////////////////// + // LoadBitmap + /////////////////////////////////////////////////////////////////////////// + void loadBitmap(Context context, Source source, boolean cache, LoadListener bitmapLoadListener); + + void loadBitmap(Fragment fragment, Source source, boolean cache, LoadListener bitmapLoadListener); + + void loadBitmap(Context context, Source source, boolean cache, int width, int height, LoadListener bitmapLoadListener); + + void loadBitmap(Fragment fragment, Source source, boolean cache, int width, int height, LoadListener bitmapLoadListener); + + @WorkerThread + Bitmap loadBitmap(Context context, Source source, boolean cache, int width, int height); + + /////////////////////////////////////////////////////////////////////////// + // clear + /////////////////////////////////////////////////////////////////////////// + @WorkerThread + void clear(Context context); + + void clear(View view); + + void clear(Fragment fragment, View view); + + /////////////////////////////////////////////////////////////////////////// + // progress + /////////////////////////////////////////////////////////////////////////// + + /** + * 移除对应 URL 的所有监听器 + * + * @param url URL + */ + @UiThread + void removeAllListener(String url); + + /** + * 添加一个对应 URL 的监听器,针对相同的 URL 可以有多个 ProgressListener,但相同的对象可以不会被重复添加 + * + * @param url URL + * @param progressListener 监听器 + */ + @UiThread + void addListener(@NonNull String url, @NonNull ProgressListener progressListener); + + /** + * 添加一个对应 URL 的监听器,针对相同的 URL 只会有一个 ProgressListener,已经添加的 ProgressListener 会被新的替换,与{@link #addListener(String, ProgressListener)}是独立的 + * + * @param url URL + * @param progressListener 监听器,如果 progressListener 为 null,则表示移除 + */ + void setListener(String url, @Nullable ProgressListener progressListener); + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/imageloader/ImageLoaderFactory.java b/lib_base/src/main/java/com/android/base/imageloader/ImageLoaderFactory.java new file mode 100644 index 0000000..6017437 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/imageloader/ImageLoaderFactory.java @@ -0,0 +1,21 @@ +package com.android.base.imageloader; + +/** + *
+ *
+ * 
+ * + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-03-27 18:09 + */ + +public class ImageLoaderFactory { + + private static final GlideImageLoader IMAGE_LOADER = new GlideImageLoader(); + + public static ImageLoader getImageLoader() { + return IMAGE_LOADER; + } + +} diff --git a/lib_base/src/main/java/com/android/base/imageloader/LoadListener.java b/lib_base/src/main/java/com/android/base/imageloader/LoadListener.java new file mode 100644 index 0000000..059c164 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/imageloader/LoadListener.java @@ -0,0 +1,14 @@ +package com.android.base.imageloader; + +public interface LoadListener { + + default void onLoadStart() { + } + + default void onLoadSuccess(T resource) { + } + + default void onLoadFail() { + } + +} diff --git a/lib_base/src/main/java/com/android/base/imageloader/LoadListenerAdapter.java b/lib_base/src/main/java/com/android/base/imageloader/LoadListenerAdapter.java new file mode 100644 index 0000000..4d9604c --- /dev/null +++ b/lib_base/src/main/java/com/android/base/imageloader/LoadListenerAdapter.java @@ -0,0 +1,21 @@ +package com.android.base.imageloader; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-09-22 13:14 + */ +public abstract class LoadListenerAdapter implements LoadListener { + + @Override + public void onLoadSuccess(T resource) { + } + + @Override + public void onLoadFail() { + } + + @Override + public void onLoadStart() { + } +} diff --git a/lib_base/src/main/java/com/android/base/imageloader/ProgressGlideModule.java b/lib_base/src/main/java/com/android/base/imageloader/ProgressGlideModule.java new file mode 100644 index 0000000..04e20cc --- /dev/null +++ b/lib_base/src/main/java/com/android/base/imageloader/ProgressGlideModule.java @@ -0,0 +1,38 @@ +package com.android.base.imageloader; + +import android.content.Context; +import android.support.annotation.CallSuper; +import android.support.annotation.NonNull; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.Registry; +import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader; +import com.bumptech.glide.load.model.GlideUrl; +import com.bumptech.glide.module.AppGlideModule; + +import java.io.InputStream; + +import okhttp3.OkHttpClient; + +public class ProgressGlideModule extends AppGlideModule { + + @Override + @CallSuper + public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { + //配置glide网络加载框架 + ProgressManager.getInstance().setRefreshTime(getRefreshTime()); + OkHttpClient.Builder builder = ProgressManager.getInstance().withProgress(new OkHttpClient.Builder()); + registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory(builder.build())); + } + + @Override + public boolean isManifestParsingEnabled() { + //不使用清单配置的方式,减少初始化时间 + return false; + } + + protected int getRefreshTime() { + return 200; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/imageloader/ProgressInfo.java b/lib_base/src/main/java/com/android/base/imageloader/ProgressInfo.java new file mode 100644 index 0000000..a3b8009 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/imageloader/ProgressInfo.java @@ -0,0 +1,102 @@ +package com.android.base.imageloader; + + +import android.support.annotation.NonNull; + +public class ProgressInfo { + + //当前下载的总长度 + private long currentBytes; + //数据总长度 + private long contentLength; + //本次调用距离上一次被调用所间隔的毫秒值 + private long intervalTime; + //本次调用距离上一次被调用的间隔时间内下载的 byte 长度 + private long eachBytes; + //请求的 ID + private long id; + //进度是否完成 + private boolean finish; + + ProgressInfo(long id) { + this.id = id; + } + + void setCurrentBytes(long currentBytes) { + this.currentBytes = currentBytes; + } + + void setContentLength(long contentLength) { + this.contentLength = contentLength; + } + + void setIntervalTime(long intervalTime) { + this.intervalTime = intervalTime; + } + + void setEachBytes(long eachBytes) { + this.eachBytes = eachBytes; + } + + void setFinish(boolean finish) { + this.finish = finish; + } + + public long getCurrentBytes() { + return currentBytes; + } + + public long getContentLength() { + return contentLength; + } + + public long getIntervalTime() { + return intervalTime; + } + + public long getEachBytes() { + return eachBytes; + } + + public long getId() { + return id; + } + + public boolean isFinished() { + return finish; + } + + /** + * 获取下载比例(0 - 1) + */ + public float getProgress() { + if (getCurrentBytes() <= 0 || getContentLength() <= 0) { + return 0; + } + return ((1F * getCurrentBytes()) / getContentLength()); + } + + /** + * 获取上传或下载网络速度,单位为byte/s + */ + public long getSpeed() { + if (getEachBytes() <= 0 || getIntervalTime() <= 0) { + return 0; + } + return getEachBytes() * 1000 / getIntervalTime(); + } + + @NonNull + @Override + public String toString() { + return "ProgressInfo{" + + "id=" + id + + ", currentBytes=" + currentBytes + + ", contentLength=" + contentLength + + ", eachBytes=" + eachBytes + + ", intervalTime=" + intervalTime + + ", finish=" + finish + + '}'; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/imageloader/ProgressListener.java b/lib_base/src/main/java/com/android/base/imageloader/ProgressListener.java new file mode 100644 index 0000000..cb867be --- /dev/null +++ b/lib_base/src/main/java/com/android/base/imageloader/ProgressListener.java @@ -0,0 +1,13 @@ +package com.android.base.imageloader; + +/** + * @author Ztiany + * Date : 2018-08-20 10:40 + */ +public interface ProgressListener { + + void onProgress(String url, ProgressInfo progressInfo); + + default void onError(long id, String url, Throwable throwable){} + +} diff --git a/lib_base/src/main/java/com/android/base/imageloader/ProgressManager.java b/lib_base/src/main/java/com/android/base/imageloader/ProgressManager.java new file mode 100644 index 0000000..54d30c8 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/imageloader/ProgressManager.java @@ -0,0 +1,201 @@ +package com.android.base.imageloader; + +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.NonNull; +import android.support.annotation.UiThread; + +import com.android.base.utils.common.Checker; +import com.android.base.utils.common.StringChecker; + +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import okhttp3.Response; + +final class ProgressManager { + + private static volatile ProgressManager mProgressManager; + private static final int DEFAULT_REFRESH_TIME = 200; + + private final Handler mHandler = new Handler(Looper.getMainLooper()); + private final Map>> mMultiResponseListeners; + private final Map> mExclusiveResponseListeners; + private final ResponseProgressInterceptor mInterceptor; + private int mRefreshTime = DEFAULT_REFRESH_TIME;//进度刷新时间(单位ms),避免高频率调用 + + static ProgressManager getInstance() { + if (mProgressManager == null) { + synchronized (ProgressManager.class) { + if (mProgressManager == null) { + mProgressManager = new ProgressManager(); + } + } + } + return mProgressManager; + } + + private ProgressManager() { + mMultiResponseListeners = new HashMap<>(); + mExclusiveResponseListeners = new HashMap<>(); + this.mInterceptor = new ResponseProgressInterceptor(); + } + + OkHttpClient.Builder withProgress(OkHttpClient.Builder builder) { + return builder.addNetworkInterceptor(mInterceptor); + } + + @UiThread + private void notifyResponseProgress(String url, ProgressInfo progressInfo) { + //multi + List> weakReferences = mMultiResponseListeners.get(url); + if (!Checker.isEmpty(weakReferences)) { + for (WeakReference weakReference : weakReferences) { + ProgressListener progressListener = weakReference.get(); + if (progressListener != null) { + progressListener.onProgress(url, progressInfo); + } + } + } + //Exclusive + WeakReference listenerWeakReference = mExclusiveResponseListeners.get(url); + if (listenerWeakReference != null) { + ProgressListener progressListener = listenerWeakReference.get(); + if (progressListener != null) { + progressListener.onProgress(url, progressInfo); + } + } + } + + @UiThread + private void notifyResponseError(String url, long id, Exception e) { + //multi + List> weakReferences = mMultiResponseListeners.get(url); + if (!Checker.isEmpty(weakReferences)) { + for (WeakReference weakReference : weakReferences) { + ProgressListener progressListener = weakReference.get(); + if (progressListener != null) { + progressListener.onError(id, url, e); + } + } + } + //Exclusive + WeakReference listenerWeakReference = mExclusiveResponseListeners.get(url); + if (listenerWeakReference != null) { + ProgressListener progressListener = listenerWeakReference.get(); + if (progressListener != null) { + progressListener.onError(id, url, e); + } + } + } + + /** + * 设置每次被调用的间隔时间,单位毫秒 + */ + void setRefreshTime(int refreshTime) { + if (refreshTime < 0) { + throw new IllegalArgumentException("the refreshTime must be >= 0"); + } + mRefreshTime = refreshTime; + } + + /////////////////////////////////////////////////////////////////////////// + //listener + /////////////////////////////////////////////////////////////////////////// + @UiThread + void addLoadListener(String url, ProgressListener listener) { + //check + if (StringChecker.isEmpty(url)) { + throw new NullPointerException("url cannot be null"); + } + if (listener == null) { + throw new NullPointerException("ProgressListener cannot be null"); + } + + List> progressListeners = mMultiResponseListeners.get(url); + //make list if need + if (progressListeners == null) { + progressListeners = new ArrayList<>(); + mMultiResponseListeners.put(url, progressListeners); + } + //add direct + if (progressListeners.isEmpty()) { + progressListeners.add(new WeakReference<>(listener)); + } else { + //Prevent duplication + boolean containers = false; + ProgressListener reference; + for (WeakReference progressListener : progressListeners) { + reference = progressListener.get(); + if (reference == listener) { + containers = true; + break; + } + } + if (!containers) { + progressListeners.add(new WeakReference<>(listener)); + } + } + } + + @UiThread + @SuppressWarnings("WeakerAccess") + public void setListener(String url, ProgressListener progressListener) { + if (StringChecker.isEmpty(url)) { + throw new NullPointerException("url cannot be null"); + } + if (progressListener == null) { + mExclusiveResponseListeners.remove(url); + } else { + mExclusiveResponseListeners.put(url, new WeakReference<>(progressListener)); + } + } + + @UiThread + void removeListener(String url) { + mMultiResponseListeners.remove(url); + } + + + /////////////////////////////////////////////////////////////////////////// + //ResponseProgressInterceptor + /////////////////////////////////////////////////////////////////////////// + private class ResponseProgressInterceptor implements Interceptor { + + @NonNull + @Override + public Response intercept(@NonNull Chain chain) throws IOException { + return wrapResponseBody(chain.proceed(chain.request())); + } + + private Response wrapResponseBody(Response response) { + if (response == null || response.body() == null) { + return response; + } + final String key = response.request().url().toString(); + ProgressResponseBody progressResponseBody = new ProgressResponseBody(response.body(), mRefreshTime) { + @Override + void onProgress(ProgressInfo progressInfo) { + runOnUIThread(() -> notifyResponseProgress(key, progressInfo)); + } + + @Override + protected void onError(long id, Exception e) { + runOnUIThread(() -> notifyResponseError(key, id, e)); + } + }; + return response.newBuilder().body(progressResponseBody).build(); + } + } + + private void runOnUIThread(Runnable runnable) { + mHandler.post(runnable); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/imageloader/ProgressResponseBody.java b/lib_base/src/main/java/com/android/base/imageloader/ProgressResponseBody.java new file mode 100644 index 0000000..932de64 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/imageloader/ProgressResponseBody.java @@ -0,0 +1,97 @@ +package com.android.base.imageloader; + +import android.os.SystemClock; +import android.support.annotation.NonNull; + +import java.io.IOException; + +import okhttp3.MediaType; +import okhttp3.ResponseBody; +import okio.Buffer; +import okio.BufferedSource; +import okio.ForwardingSource; +import okio.Okio; +import okio.Source; + +abstract class ProgressResponseBody extends ResponseBody { + + private final int mRefreshTime; + private final ResponseBody mDelegate; + private BufferedSource mBufferedSource; + private final ProgressInfo mProgressInfo; + + ProgressResponseBody(ResponseBody responseBody, int refreshTime) { + this.mDelegate = responseBody; + this.mRefreshTime = refreshTime; + this.mProgressInfo = new ProgressInfo(System.currentTimeMillis()); + } + + @Override + public MediaType contentType() { + return mDelegate.contentType(); + } + + @Override + public long contentLength() { + return mDelegate.contentLength(); + } + + @NonNull + @Override + public BufferedSource source() { + if (mBufferedSource == null) { + mBufferedSource = Okio.buffer(source(mDelegate.source())); + } + return mBufferedSource; + } + + private Source source(Source source) { + return new ForwardingSource(source) { + private long mContentLength; + private long totalBytesRead = 0L; + private long lastRefreshTime = 0L; //最后一次刷新的时间 + + @Override + public long read(@NonNull Buffer sink, long byteCount) throws IOException { + long bytesRead; + try { + bytesRead = super.read(sink, byteCount); + } catch (IOException e) { + e.printStackTrace(); + onError(mProgressInfo.getId(), e); + throw e; + } + + if (mContentLength == 0) { + mContentLength = contentLength(); + mProgressInfo.setContentLength(mContentLength); + } + + // read() returns the number of bytes read, or -1 if this source is exhausted. + totalBytesRead += bytesRead != -1 ? bytesRead : 0; + long curTime = SystemClock.elapsedRealtime(); + long intervalTime = curTime - lastRefreshTime; + + if (intervalTime >= mRefreshTime || bytesRead == -1 || totalBytesRead == mContentLength) { + + boolean finish = bytesRead == -1 && totalBytesRead == mContentLength; + + mProgressInfo.setCurrentBytes(totalBytesRead); + mProgressInfo.setIntervalTime(intervalTime); + mProgressInfo.setFinish(finish); + mProgressInfo.setEachBytes(bytesRead); + + onProgress(mProgressInfo); + + lastRefreshTime = curTime; + } + return bytesRead; + } + }; + } + + protected abstract void onError(long id, Exception e); + + abstract void onProgress(ProgressInfo progressInfo); + +} diff --git a/lib_base/src/main/java/com/android/base/imageloader/Source.java b/lib_base/src/main/java/com/android/base/imageloader/Source.java new file mode 100644 index 0000000..02a7c64 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/imageloader/Source.java @@ -0,0 +1,43 @@ +package com.android.base.imageloader; + +import android.net.Uri; + +import java.io.File; + + +public class Source { + + String mUrl; + File mFile; + int mResource; + Uri mUri; + + public static Source create(String url) { + Source source = new Source(); + source.mUrl = url; + return source; + } + + public static Source createWithPath(String path) { + return create(new File(path)); + } + + public static Source createWithUri(Uri uri) { + Source source = new Source(); + source.mUri = uri; + return source; + } + + public static Source create(File file) { + Source source = new Source(); + source.mFile = file; + return source; + } + + public static Source create(int resource) { + Source source = new Source(); + source.mResource = resource; + return source; + } + +} diff --git a/lib_base/src/main/java/com/android/base/interfaces/OnItemClickListener.java b/lib_base/src/main/java/com/android/base/interfaces/OnItemClickListener.java new file mode 100644 index 0000000..1ba71b8 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/interfaces/OnItemClickListener.java @@ -0,0 +1,24 @@ +package com.android.base.interfaces; + +import android.view.View; + +import timber.log.Timber; + + +public abstract class OnItemClickListener implements View.OnClickListener { + + @Override + @SuppressWarnings("unchecked") + public final void onClick(View v) { + Object tag = v.getTag(); + if (tag == null) { + Timber.w("OnItemClickListener tag is null , view = " + v); + return; + } + onClick(v, (T) tag); + } + + public abstract void onClick(View view, T t); + + +} diff --git a/lib_base/src/main/java/com/android/base/interfaces/OnItemLongClickListener.java b/lib_base/src/main/java/com/android/base/interfaces/OnItemLongClickListener.java new file mode 100644 index 0000000..d1fb782 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/interfaces/OnItemLongClickListener.java @@ -0,0 +1,25 @@ +package com.android.base.interfaces; + +import android.view.View; + +import timber.log.Timber; + + + +public abstract class OnItemLongClickListener implements View.OnLongClickListener { + + @Override + @SuppressWarnings("unchecked") + public final boolean onLongClick(View v) { + Object tag = v.getTag(); + if (tag == null) { + Timber.w("OnItemLongClickListener tag is null , view = " + v); + return false; + } + return onClick(v, (T) tag); + } + + public abstract boolean onClick(View view, T t); + + +} diff --git a/lib_base/src/main/java/com/android/base/interfaces/adapter/ActivityLifecycleCallbacksAdapter.java b/lib_base/src/main/java/com/android/base/interfaces/adapter/ActivityLifecycleCallbacksAdapter.java new file mode 100644 index 0000000..436fcd8 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/interfaces/adapter/ActivityLifecycleCallbacksAdapter.java @@ -0,0 +1,38 @@ +package com.android.base.interfaces.adapter; + +import android.app.Activity; +import android.app.Application; +import android.os.Bundle; + + +public interface ActivityLifecycleCallbacksAdapter extends Application.ActivityLifecycleCallbacks { + + @Override + default void onActivityCreated(Activity activity, Bundle savedInstanceState) { + } + + @Override + default void onActivityStarted(Activity activity) { + } + + @Override + default void onActivityResumed(Activity activity) { + } + + @Override + default void onActivityPaused(Activity activity) { + } + + @Override + default void onActivityStopped(Activity activity) { + } + + @Override + default void onActivitySaveInstanceState(Activity activity, Bundle outState) { + } + + @Override + default void onActivityDestroyed(Activity activity) { + } + +} diff --git a/lib_base/src/main/java/com/android/base/interfaces/adapter/DrawerListenerAdapter.java b/lib_base/src/main/java/com/android/base/interfaces/adapter/DrawerListenerAdapter.java new file mode 100644 index 0000000..f54d556 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/interfaces/adapter/DrawerListenerAdapter.java @@ -0,0 +1,25 @@ +package com.android.base.interfaces.adapter; + +import android.support.v4.widget.DrawerLayout; +import android.view.View; + + +public interface DrawerListenerAdapter extends DrawerLayout.DrawerListener { + + @Override + default void onDrawerSlide(View drawerView, float slideOffset) { + } + + @Override + default void onDrawerOpened(View drawerView) { + } + + @Override + default void onDrawerClosed(View drawerView) { + } + + @Override + default void onDrawerStateChanged(int newState) { + } + +} diff --git a/lib_base/src/main/java/com/android/base/interfaces/adapter/OnPageChangeListenerAdapter.java b/lib_base/src/main/java/com/android/base/interfaces/adapter/OnPageChangeListenerAdapter.java new file mode 100644 index 0000000..b7f81af --- /dev/null +++ b/lib_base/src/main/java/com/android/base/interfaces/adapter/OnPageChangeListenerAdapter.java @@ -0,0 +1,24 @@ +package com.android.base.interfaces.adapter; + +import android.support.v4.view.ViewPager; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2017-12-12 17:35 + */ +public interface OnPageChangeListenerAdapter extends ViewPager.OnPageChangeListener { + + @Override + default void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + } + + @Override + default void onPageSelected(int position) { + } + + @Override + default void onPageScrollStateChanged(int state) { + } + +} diff --git a/lib_base/src/main/java/com/android/base/interfaces/adapter/OnSeekBarChangeListenerAdapter.java b/lib_base/src/main/java/com/android/base/interfaces/adapter/OnSeekBarChangeListenerAdapter.java new file mode 100644 index 0000000..8f985df --- /dev/null +++ b/lib_base/src/main/java/com/android/base/interfaces/adapter/OnSeekBarChangeListenerAdapter.java @@ -0,0 +1,24 @@ +package com.android.base.interfaces.adapter; + +import android.widget.SeekBar; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-04-18 16:29 + */ +public interface OnSeekBarChangeListenerAdapter extends SeekBar.OnSeekBarChangeListener { + + @Override + default void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + } + + @Override + default void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + default void onStopTrackingTouch(SeekBar seekBar) { + } + +} diff --git a/lib_base/src/main/java/com/android/base/interfaces/adapter/OnTabSelectedListenerAdapter.java b/lib_base/src/main/java/com/android/base/interfaces/adapter/OnTabSelectedListenerAdapter.java new file mode 100644 index 0000000..c386e7a --- /dev/null +++ b/lib_base/src/main/java/com/android/base/interfaces/adapter/OnTabSelectedListenerAdapter.java @@ -0,0 +1,19 @@ +package com.android.base.interfaces.adapter; + +import android.support.design.widget.TabLayout; + +public interface OnTabSelectedListenerAdapter extends TabLayout.OnTabSelectedListener { + + @Override + default void onTabSelected(TabLayout.Tab tab) { + } + + @Override + default void onTabUnselected(TabLayout.Tab tab) { + } + + @Override + default void onTabReselected(TabLayout.Tab tab) { + } + +} diff --git a/lib_base/src/main/java/com/android/base/interfaces/adapter/TextWatcherAdapter.java b/lib_base/src/main/java/com/android/base/interfaces/adapter/TextWatcherAdapter.java new file mode 100644 index 0000000..a27d7c5 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/interfaces/adapter/TextWatcherAdapter.java @@ -0,0 +1,25 @@ +package com.android.base.interfaces.adapter; + +import android.text.Editable; +import android.text.TextWatcher; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-07-05 18:01 + */ +public interface TextWatcherAdapter extends TextWatcher { + + @Override + default void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + default void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + default void afterTextChanged(Editable s) { + } + +} diff --git a/lib_base/src/main/java/com/android/base/interfaces/adapter/TransitionListenerAdapter.java b/lib_base/src/main/java/com/android/base/interfaces/adapter/TransitionListenerAdapter.java new file mode 100644 index 0000000..7d4bafb --- /dev/null +++ b/lib_base/src/main/java/com/android/base/interfaces/adapter/TransitionListenerAdapter.java @@ -0,0 +1,31 @@ +package com.android.base.interfaces.adapter; + +import android.os.Build; +import android.support.annotation.RequiresApi; +import android.transition.Transition; + + +@RequiresApi(api = Build.VERSION_CODES.KITKAT) +public interface TransitionListenerAdapter extends Transition.TransitionListener { + + @Override + default void onTransitionStart(Transition transition) { + } + + @Override + default void onTransitionEnd(Transition transition) { + } + + @Override + default void onTransitionCancel(Transition transition) { + } + + @Override + default void onTransitionPause(Transition transition) { + } + + @Override + default void onTransitionResume(Transition transition) { + } + +} diff --git a/lib_base/src/main/java/com/android/base/kotlin/AttrStyleEx.kt b/lib_base/src/main/java/com/android/base/kotlin/AttrStyleEx.kt new file mode 100644 index 0000000..424905b --- /dev/null +++ b/lib_base/src/main/java/com/android/base/kotlin/AttrStyleEx.kt @@ -0,0 +1,38 @@ +package com.android.base.kotlin + +import android.content.Context +import android.content.ContextWrapper +import android.content.res.TypedArray +import android.support.annotation.AttrRes +import android.support.annotation.StyleRes +import android.util.TypedValue +import android.view.ContextThemeWrapper +import android.view.View + +/** 属性相关扩展:https://github.com/Kotlin/anko/issues/16*/ +val View.contextThemeWrapper: ContextThemeWrapper + get() = context.contextThemeWrapper + +val Context.contextThemeWrapper: ContextThemeWrapper + get() = when (this) { + is ContextThemeWrapper -> this + is ContextWrapper -> baseContext.contextThemeWrapper + else -> throw IllegalStateException("Context is not an Activity, can't get theme: $this") + } + +@StyleRes +fun View.attrStyle(@AttrRes attrColor: Int): Int = contextThemeWrapper.attrStyle(attrColor) +@StyleRes +private fun ContextThemeWrapper.attrStyle(@AttrRes attrRes: Int): Int = + attr(attrRes) { + it.getResourceId(0, 0) + } + +private fun ContextThemeWrapper.attr(@AttrRes attrRes: Int, block: (TypedArray)->R): R { + val typedValue = TypedValue() + if (!theme.resolveAttribute(attrRes, typedValue, true)) throw IllegalArgumentException("$attrRes is not resolvable") + val a = obtainStyledAttributes(typedValue.data, intArrayOf(attrRes)) + val result = block(a) + a.recycle() + return result +} diff --git a/lib_base/src/main/java/com/android/base/kotlin/ContextEx.kt b/lib_base/src/main/java/com/android/base/kotlin/ContextEx.kt new file mode 100644 index 0000000..7efe49b --- /dev/null +++ b/lib_base/src/main/java/com/android/base/kotlin/ContextEx.kt @@ -0,0 +1,15 @@ +package com.android.base.kotlin + +import android.content.Context +import android.graphics.drawable.Drawable +import android.support.annotation.ColorRes +import android.support.annotation.DrawableRes +import android.support.v4.content.ContextCompat + +fun Context.colorFromId(@ColorRes id: Int): Int { + return ContextCompat.getColor(this, id) +} + +fun Context.drawableFromId(@DrawableRes id: Int): Drawable? { + return ContextCompat.getDrawable(this, id) +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/kotlin/DialogEx.kt b/lib_base/src/main/java/com/android/base/kotlin/DialogEx.kt new file mode 100644 index 0000000..353d45b --- /dev/null +++ b/lib_base/src/main/java/com/android/base/kotlin/DialogEx.kt @@ -0,0 +1,13 @@ +package com.android.base.kotlin + +import android.app.Dialog + +/** + *@author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-03-04 10:46 + */ +fun Dialog.noCancelable(): Dialog { + this.setCancelable(false) + return this +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/kotlin/FragmentEx.kt b/lib_base/src/main/java/com/android/base/kotlin/FragmentEx.kt new file mode 100644 index 0000000..ae21e7f --- /dev/null +++ b/lib_base/src/main/java/com/android/base/kotlin/FragmentEx.kt @@ -0,0 +1,18 @@ +package com.android.base.kotlin + +import android.graphics.drawable.Drawable +import android.support.annotation.ColorRes +import android.support.annotation.DrawableRes +import android.support.v4.app.Fragment +import android.support.v4.content.ContextCompat + + +fun Fragment.colorFromId(@ColorRes colorRes: Int): Int { + val safeContext = context ?: return 0 + return ContextCompat.getColor(safeContext, colorRes) +} + +fun Fragment.drawableFromId(@DrawableRes colorRes: Int): Drawable? { + val safeContext = context ?: return null + return ContextCompat.getDrawable(safeContext, colorRes) +} diff --git a/lib_base/src/main/java/com/android/base/kotlin/KtViewHolder.kt b/lib_base/src/main/java/com/android/base/kotlin/KtViewHolder.kt new file mode 100644 index 0000000..2637902 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/kotlin/KtViewHolder.kt @@ -0,0 +1,15 @@ +package com.android.base.kotlin + +import android.view.View +import com.android.base.adapter.recycler.ViewHolder +import kotlinx.android.extensions.CacheImplementation +import kotlinx.android.extensions.ContainerOptions +import kotlinx.android.extensions.LayoutContainer + +/** + *@author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-12 17:07 + */ +@ContainerOptions(cache = CacheImplementation.SPARSE_ARRAY) +open class KtViewHolder(override val containerView: View) : ViewHolder(containerView), LayoutContainer diff --git a/lib_base/src/main/java/com/android/base/kotlin/LangEx.kt b/lib_base/src/main/java/com/android/base/kotlin/LangEx.kt new file mode 100644 index 0000000..fb97338 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/kotlin/LangEx.kt @@ -0,0 +1,58 @@ +package com.android.base.kotlin + +/** + * 参考: + * - [Boolean扩展](https://blog.kotliner.cn/2017/06/19/interesting-booleanext/) + */ +sealed class Ext constructor(val boolean: Boolean) + +/** 如果该对象不是null,则执行action */ +fun T?.ifNonNull(action: T.() -> E): Ext { + if (this != null) { + return WithData(action()) + } + return Otherwise +} + +/** 如果该对象是null,则执行action */ +fun T?.ifNull(action: () -> E) { + if (this == null) { + action() + } +} + +inline fun Boolean.yes(block: () -> T): Ext = when { + this -> { + WithData(block()) + } + else -> Otherwise +} + +inline fun Boolean.no(block: () -> T) = when { + this -> Otherwise + else -> { + WithData(block()) + } +} + +object Otherwise : Ext(true) + +class WithData(val data: T) : Ext(false) + +/**除此以外*/ +inline infix fun Ext.otherwise(block: () -> T): T { + return when (this) { + is Otherwise -> block() + is WithData -> this.data + } +} + +inline operator fun Boolean.invoke(block: () -> T) = yes(block) + +fun Any?.javaClassName(): String { + return if (this == null) { + "" + } else { + this::class.java.name + } +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/kotlin/LiveDataEx.kt b/lib_base/src/main/java/com/android/base/kotlin/LiveDataEx.kt new file mode 100644 index 0000000..610a2d0 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/kotlin/LiveDataEx.kt @@ -0,0 +1,23 @@ +package com.android.base.kotlin + +import android.arch.lifecycle.MutableLiveData + +fun MutableLiveData.init(t: T): MutableLiveData { + this.postValue(t) + return this +} + +fun MutableLiveData.touchOff() { + this.postValue(this.value) +} + +fun MutableLiveData>.append(list: List) { + val value = this.value + if (value == null) { + this.postValue(list) + } else { + val mutableList = value.toMutableList() + mutableList.addAll(list) + this.postValue(mutableList) + } +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/kotlin/MenuEx.kt b/lib_base/src/main/java/com/android/base/kotlin/MenuEx.kt new file mode 100644 index 0000000..022a49a --- /dev/null +++ b/lib_base/src/main/java/com/android/base/kotlin/MenuEx.kt @@ -0,0 +1,16 @@ +package com.android.base.kotlin + +import android.view.MenuItem + +fun MenuItem.alwaysShow(): MenuItem { + setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS) + return this +} + +fun MenuItem.setSimpleClickListener(onClick: (MenuItem) -> Unit): MenuItem { + setOnMenuItemClickListener { + onClick(it) + true + } + return this +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/kotlin/RecyclerViewEx.kt b/lib_base/src/main/java/com/android/base/kotlin/RecyclerViewEx.kt new file mode 100644 index 0000000..fcfcfb1 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/kotlin/RecyclerViewEx.kt @@ -0,0 +1,29 @@ +package com.android.base.kotlin + +import android.support.v7.widget.GridLayoutManager +import android.support.v7.widget.LinearLayoutManager +import android.support.v7.widget.RecyclerView + +fun RecyclerView.verticalLinearLayoutManager(): LinearLayoutManager { + val linearLayoutManager = LinearLayoutManager(context) + layoutManager = linearLayoutManager + return linearLayoutManager +} + +fun RecyclerView.horizontalLinearlLayoutManager(): LinearLayoutManager { + val linearLayoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) + layoutManager = linearLayoutManager + return linearLayoutManager +} + +fun RecyclerView.verticalLinearLayoutManager(span: Int): GridLayoutManager { + val gridLayoutManager = GridLayoutManager(context, span) + layoutManager = gridLayoutManager + return gridLayoutManager +} + +fun RecyclerView.horizontalLinearlLayoutManager(span: Int): GridLayoutManager { + val gridLayoutManager = GridLayoutManager(context, span, GridLayoutManager.HORIZONTAL, false) + layoutManager = gridLayoutManager + return gridLayoutManager +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/kotlin/ResourceEx.kt b/lib_base/src/main/java/com/android/base/kotlin/ResourceEx.kt new file mode 100644 index 0000000..114457e --- /dev/null +++ b/lib_base/src/main/java/com/android/base/kotlin/ResourceEx.kt @@ -0,0 +1,21 @@ +package com.android.base.kotlin + +import android.content.res.TypedArray + +inline fun T.use(block: (T) -> R): R { + var recycled = false + try { + return block(this) + } catch (e: Exception) { + recycled = true + try { + this?.recycle() + } catch (exception: Exception) { + } + throw e + } finally { + if (!recycled) { + this?.recycle() + } + } +} diff --git a/lib_base/src/main/java/com/android/base/kotlin/SizeEx.kt b/lib_base/src/main/java/com/android/base/kotlin/SizeEx.kt new file mode 100644 index 0000000..b99dc04 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/kotlin/SizeEx.kt @@ -0,0 +1,27 @@ +package com.android.base.kotlin + +import android.content.Context +import android.support.v4.app.Fragment +import android.support.v7.widget.RecyclerView +import android.view.View +import com.android.base.utils.android.UnitConverter + +fun Context.dip(value: Int): Int = UnitConverter.dpToPx(value) +fun Context.dip(value: Float): Float = UnitConverter.dpToPx(value) +fun Context.sp(value: Int): Int = UnitConverter.spToPx(value) +fun Context.sp(value: Float): Float = UnitConverter.spToPx(value) + +fun Fragment.dip(value: Int): Int = UnitConverter.dpToPx(value) +fun Fragment.dip(value: Float): Float = UnitConverter.dpToPx(value) +fun Fragment.sp(value: Int): Int = UnitConverter.spToPx(value) +fun Fragment.sp(value: Float): Float = UnitConverter.spToPx(value) + +fun View.dip(value: Int): Int = context.dip(value) +fun View.dip(value: Float): Float = context.dip(value) +fun View.sp(value: Int): Int = context.sp(value) +fun View.sp(value: Float): Float = context.sp(value) + +fun RecyclerView.ViewHolder.dip(value: Int): Int = itemView.dip(value) +fun RecyclerView.ViewHolder.dip(value: Float): Float = itemView.dip(value) +fun RecyclerView.ViewHolder.sp(value: Int): Int = itemView.dip(value) +fun RecyclerView.ViewHolder.sp(value: Float): Float = itemView.dip(value) \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/kotlin/TextViewEx.kt b/lib_base/src/main/java/com/android/base/kotlin/TextViewEx.kt new file mode 100644 index 0000000..8587bc5 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/kotlin/TextViewEx.kt @@ -0,0 +1,121 @@ +package com.android.base.kotlin + +import android.graphics.drawable.Drawable +import android.support.annotation.DrawableRes +import android.text.Editable +import android.text.TextWatcher +import android.widget.Button +import android.widget.EditText +import android.widget.TextView +import com.android.base.interfaces.adapter.TextWatcherAdapter + +inline fun TextView.textWatcher(init: KTextWatcher.() -> Unit) = addTextChangedListener(KTextWatcher().apply(init)) + +class KTextWatcher : TextWatcher { + + val TextView.isEmpty + get() = text.isEmpty() + + val TextView.isNotEmpty + get() = text.isNotEmpty() + + val TextView.isBlank + get() = text.isBlank() + + val TextView.isNotBlank + get() = text.isNotBlank() + + private var _beforeTextChanged: ((CharSequence?, Int, Int, Int) -> Unit)? = null + private var _onTextChanged: ((CharSequence?, Int, Int, Int) -> Unit)? = null + private var _afterTextChanged: ((Editable?) -> Unit)? = null + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + _beforeTextChanged?.invoke(s, start, count, after) + } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + _onTextChanged?.invoke(s, start, before, count) + } + + override fun afterTextChanged(s: Editable?) { + _afterTextChanged?.invoke(s) + } + + fun beforeTextChanged(listener: (CharSequence?, Int, Int, Int) -> Unit) { + _beforeTextChanged = listener + } + + fun onTextChanged(listener: (CharSequence?, Int, Int, Int) -> Unit) { + _onTextChanged = listener + } + + fun afterTextChanged(listener: (Editable?) -> Unit) { + _afterTextChanged = listener + } + +} + +fun TextView.topDrawable(@DrawableRes id: Int) { + this.setCompoundDrawablesWithIntrinsicBounds(0, id, 0, 0) +} + +fun TextView.leftDrawable(@DrawableRes id: Int) { + this.setCompoundDrawablesWithIntrinsicBounds(id, 0, 0, 0) +} + +fun TextView.rightDrawable(@DrawableRes id: Int) { + this.setCompoundDrawablesWithIntrinsicBounds(0, 0, id, 0) +} + +fun TextView.bottomDrawable(@DrawableRes id: Int) { + this.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, id) +} + +fun TextView.topDrawable(drawable: Drawable, retain: Boolean = false) { + if (retain) { + val compoundDrawables = this.compoundDrawables + this.setCompoundDrawablesWithIntrinsicBounds(compoundDrawables[0], drawable, compoundDrawables[2], compoundDrawables[3]) + } else { + this.setCompoundDrawablesWithIntrinsicBounds(null, drawable, null, null) + } +} + +fun TextView.leftDrawable(drawable: Drawable, retain: Boolean = false) { + if (retain) { + val compoundDrawables = this.compoundDrawables + this.setCompoundDrawablesWithIntrinsicBounds(drawable, compoundDrawables[1], compoundDrawables[2], compoundDrawables[3]) + } else { + this.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null) + } +} + +fun TextView.rightDrawable(drawable: Drawable, retain: Boolean = false) { + if (retain) { + val compoundDrawables = this.compoundDrawables + this.setCompoundDrawablesWithIntrinsicBounds(compoundDrawables[0], compoundDrawables[1], drawable, compoundDrawables[3]) + } else { + this.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null) + } +} + +fun TextView.bottomDrawable(drawable: Drawable, retain: Boolean = false) { + if (retain) { + val compoundDrawables = this.compoundDrawables + this.setCompoundDrawablesWithIntrinsicBounds(compoundDrawables[0], compoundDrawables[1], compoundDrawables[2], drawable) + } else { + this.setCompoundDrawablesWithIntrinsicBounds(null, null, null, drawable) + } +} + +fun TextView.clearComponentDrawable() { + this.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null) +} + +fun Button.enable(et: EditText, checker: (s: CharSequence?) -> Boolean) { + val btn = this + et.addTextChangedListener(object : TextWatcherAdapter { + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + btn.isEnabled = checker(s) + } + }) +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/kotlin/UtilsEx.kt b/lib_base/src/main/java/com/android/base/kotlin/UtilsEx.kt new file mode 100644 index 0000000..c086ae5 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/kotlin/UtilsEx.kt @@ -0,0 +1,30 @@ +package com.android.base.kotlin + +import com.android.base.utils.android.compat.AndroidVersion + + +inline fun ifSDKAbove(sdkVersion: Int, block: () -> Unit) { + if (AndroidVersion.above(sdkVersion)) { + block() + } +} + +inline fun ifSDKAt(sdkVersion: Int, block: () -> Unit) { + if (AndroidVersion.at(sdkVersion)) { + block() + } +} + +inline fun ifSDKAtLeast(sdkVersion: Int, block: () -> Unit) { + if (AndroidVersion.atLeast(sdkVersion)) { + block() + } +} + +inline fun ignoreCrash(code: () -> Unit) { + try { + code() + } catch (e: Exception) { + e.printStackTrace() + } +} diff --git a/lib_base/src/main/java/com/android/base/kotlin/ViewEx.kt b/lib_base/src/main/java/com/android/base/kotlin/ViewEx.kt new file mode 100644 index 0000000..9b9ca68 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/kotlin/ViewEx.kt @@ -0,0 +1,148 @@ +package com.android.base.kotlin + +import android.graphics.drawable.Drawable +import android.support.annotation.ColorRes +import android.support.annotation.DrawableRes +import android.support.v4.content.ContextCompat +import android.support.v4.view.ViewCompat +import android.view.View +import android.view.ViewGroup +import android.view.ViewTreeObserver +import com.android.base.rx.subscribeIgnoreError +import com.android.base.utils.android.ViewUtils +import com.android.base.utils.android.compat.AndroidVersion.atLeast +import com.jakewharton.rxbinding2.view.RxView +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import java.util.concurrent.TimeUnit + +fun View.visibleOrGone(visible: Boolean) { + if (visible) { + this.visibility = View.VISIBLE + } else { + this.visibility = View.GONE + } +} + +fun View.visibleOrInvisible(visible: Boolean) { + if (visible) { + this.visibility = View.VISIBLE + } else { + this.visibility = View.INVISIBLE + } +} + +fun View.visible() { + this.visibility = View.VISIBLE +} + +fun View.invisible() { + this.visibility = View.INVISIBLE +} + +fun View.gone() { + this.visibility = View.GONE +} + +fun View.realContext() = ViewUtils.getRealContext(this) + +inline fun View.doOnLayoutAvailable(crossinline block: () -> Unit) { + //如果 view 已经通过至少一个布局,则返回true,因为它最后一次附加到窗口或从窗口分离。 + ViewCompat.isLaidOut(this).yes { + block() + }.otherwise { + addOnLayoutChangeListener(object : View.OnLayoutChangeListener { + override fun onLayoutChange(v: View?, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) { + removeOnLayoutChangeListener(this) + block() + } + }) + } +} + +inline fun T.onGlobalLayoutOnce(crossinline action: T.() -> Unit) { + val t: T = this + t.viewTreeObserver + .addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { + override fun onGlobalLayout() { + action.invoke(t) + if (atLeast(16)) { + viewTreeObserver.removeOnGlobalLayoutListener(this) + } else { + @Suppress("DEPRECATION") + viewTreeObserver.removeGlobalOnLayoutListener(this) + } + } + }) +} + +fun View.paddingAll(padding: Int) { + this.setPadding(padding, padding, padding, padding) +} + +fun View.setPaddingLeft(padding: Int) { + this.setPadding(padding, paddingTop, paddingRight, paddingBottom) +} + +fun View.setPaddRight(padding: Int) { + this.setPadding(paddingLeft, paddingTop, padding, paddingBottom) +} + +fun View.setPaddingTop(padding: Int) { + this.setPadding(paddingLeft, padding, paddingRight, paddingBottom) +} + +fun View.setPaddingBottom(padding: Int) { + this.setPadding(paddingLeft, paddingTop, paddingRight, padding) +} + +fun View.colorFromId(@ColorRes colorRes: Int): Int { + val safeContext = context ?: return 0 + return ContextCompat.getColor(safeContext, colorRes) +} + +fun View.drawableFromId(@DrawableRes colorRes: Int): Drawable? { + val safeContext = context ?: return null + return ContextCompat.getDrawable(safeContext, colorRes) +} + +fun View.setWidth(width: Int) { + val params = layoutParams ?: ViewGroup.LayoutParams(0, 0) + params.width = width + layoutParams = params +} + +fun View.setHeight(height: Int) { + val params = layoutParams ?: ViewGroup.LayoutParams(0, 0) + params.height = height + layoutParams = params +} + +fun View.setSize(width: Int, height: Int) { + val params = layoutParams ?: ViewGroup.LayoutParams(0, 0) + params.width = width + params.height = height + layoutParams = params +} + +fun View.onDebouncedClick(onClick: (View) -> Unit) { + onClickObservable(300) + .subscribeIgnoreError { onClick(this) } +} + +fun View.onDebouncedClick(milliseconds: Long, onClick: (View) -> Unit) { + onClickObservable(milliseconds) + .subscribeIgnoreError { onClick(this) } +} + +fun View.onClickObservable(): Observable { + return onClickObservable(300) +} + +fun View.onClickObservable(milliseconds: Long): Observable { + return RxView.clicks(this) + .debounce(milliseconds, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) +} + +inline val ViewGroup.views get() = (0 until childCount).map { getChildAt(it) } \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/permission/AndPermissionFragment.java b/lib_base/src/main/java/com/android/base/permission/AndPermissionFragment.java new file mode 100644 index 0000000..2344e39 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/permission/AndPermissionFragment.java @@ -0,0 +1,78 @@ +package com.android.base.permission; + +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-04-16 17:48 + */ +public class AndPermissionFragment extends Fragment { + + static AndPermissionFragment newInstance() { + return new AndPermissionFragment(); + } + + private final Handler mHandler = new Handler(); + private final Runnable mRunnable = this::startChecked; + + private AndPermissionRequester mRequester; + private boolean mIsActivityReady = false; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mIsActivityReady = true; + } + + @Override + public void onResume() { + super.onResume(); + mIsActivityReady = true; + } + + @Override + public void onPause() { + super.onPause(); + mHandler.removeCallbacks(mRunnable); + mIsActivityReady = false; + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (mRequester != null) { + mRequester.onActivityResult(requestCode, resultCode, data); + } + } + + void setRequester(AndPermissionRequester requester) { + mRequester = requester; + } + + void startAsk() { + startChecked(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + mHandler.removeCallbacks(mRunnable); + } + + private void startChecked() { + if (mIsActivityReady) { + if (mRequester != null) { + mRequester.onAutoPermissionFragmentReady(this); + } + mHandler.removeCallbacks(mRunnable); + } else { + mHandler.post(mRunnable); + } + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/permission/AndPermissionRequester.java b/lib_base/src/main/java/com/android/base/permission/AndPermissionRequester.java new file mode 100644 index 0000000..c34b529 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/permission/AndPermissionRequester.java @@ -0,0 +1,205 @@ +package com.android.base.permission; + +import android.content.Intent; +import android.net.Uri; +import android.provider.Settings; +import android.support.annotation.NonNull; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; + +import com.github.dmstocking.optional.java.util.function.Consumer; +import com.yanzhenjie.permission.AndPermission; +import com.yanzhenjie.permission.runtime.PermissionRequest; + +import java.util.List; + + +/** + *
+ *      1: 使用该类申请权限,当所有的权限都通过时回调权限获取成功,否则回调权限获取失败。
+ *      2:不要同时调用requestPermission方法多次!!!以保证一个完整的流程。
+ * 获取权限流程,以申请相机权限为例:
+ *          1先检查自身是否有相机权限
+ *          2如果有我们的app已经有了相机权限,则可以直接使用相机相关功能了
+ *          3如果没有权限我们就需要请求权限了,但是还需要处理不再询问的设置
+ *              3.1如果shouldShowRequestPermissionRationale返回false,则说明接下来的对话框不包含”不再询问“选择框,我们可以直接申请权限
+ *              3.2如果shouldShowRequestPermissionRationale返回true,我们最好先弹出一个对话框来说明我们需要权限来做什么,让用户来选择是否继续授予权限,如果用户允许继续授予权限则继续申请权限
+ *          4不管权限是否授予给我们的App,都可以在onRequestPermissionsResult的回调中获取结果,我们再问一次
+ * 
+ * + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-01-11 15:09 + */ +public class AndPermissionRequester { + + private final FragmentActivity mActivity; + private boolean mAskAgain = true; + private boolean mShowReason = true; + private boolean mShowTips = false; + private IPermissionUIProvider mPermissionUIProvider; + private Consumer> mOnGranted; + private Consumer> mOnDenied; + private static final int REQUEST_PERMISSION_FOR_SETTING = 999; + private List mDeniedPermission; + private String[] mPerms; + + private AndPermissionRequester(FragmentActivity activity) { + if (activity == null) { + throw new NullPointerException("activity is null"); + } + mActivity = activity; + } + + public static AndPermissionRequester with(Fragment fragment) { + return new AndPermissionRequester(fragment.getActivity()); + } + + public static AndPermissionRequester with(FragmentActivity activity) { + return new AndPermissionRequester(activity); + } + + public AndPermissionRequester permission(String... permissions) { + if (permissions == null || permissions.length == 0) { + throw new IllegalArgumentException(); + } + mPerms = permissions; + return this; + } + + public AndPermissionRequester askAgain(boolean askAgain) { + mAskAgain = askAgain; + return this; + } + + public AndPermissionRequester showReason(boolean showReason) { + mShowReason = showReason; + return this; + } + + public AndPermissionRequester showDeniedTips(boolean showTips) { + mShowTips = showTips; + return this; + } + + public AndPermissionRequester customUI(@NonNull IPermissionUIProvider uiProvider) { + mPermissionUIProvider = uiProvider; + return this; + } + + public AndPermissionRequester onGranted(@NonNull Consumer> onGranted) { + this.mOnGranted = onGranted; + return this; + } + + public AndPermissionRequester onDenied(@NonNull Consumer> onDenied) { + this.mOnDenied = onDenied; + return this; + } + + public void request() { + if (mPerms != null) { + doPermissionRequest(); + } else { + throw new IllegalStateException("no permission set"); + } + } + + private void doPermissionRequest() { + PermissionRequest permissionRequest = AndPermission.with(mActivity).runtime().permission(mPerms); + + if (mShowReason) { + permissionRequest = permissionRequest.rationale((context, data, executor) -> + getPermissionUIProvider().showPermissionRationaleDialog( + mActivity, + data.toArray(new String[0]), + (dialog, which) -> executor.execute(), + (dialog, which) -> executor.cancel())); + } + + permissionRequest + .onGranted(data -> { + if (mOnGranted != null) { + mOnGranted.accept(data); + } + }) + .onDenied(permissions -> { + if (mAskAgain) { + doAskAgain(permissions); + } else { + if (mOnDenied != null) { + mOnDenied.accept(permissions); + } + } + }) + .start(); + } + + /** + * 询问是否去设置界面 + */ + private void doAskAgain(List permissions) { + getPermissionUIProvider().showAskAgainDialog(mActivity, permissions.toArray(new String[0]), + (dialog, which) -> openSettings(permissions),/*去设置界面*/ + (dialog, which) -> { + if (mOnDenied != null) { + mOnDenied.accept(permissions);/*通知权限被拒绝*/ + } + }); + } + + private void openSettings(List permissions) { + FragmentManager supportFragmentManager = mActivity.getSupportFragmentManager(); + AndPermissionFragment fragment = (AndPermissionFragment) supportFragmentManager.findFragmentByTag(AndPermissionFragment.class.getName()); + if (fragment == null) { + fragment = AndPermissionFragment.newInstance(); + fragment.setRequester(this); + supportFragmentManager + .beginTransaction() + .add(fragment, AndPermissionFragment.class.getName()) + .commitNowAllowingStateLoss(); + } else { + fragment.setRequester(this); + } + + mDeniedPermission = permissions; + fragment.startAsk(); + } + + void onAutoPermissionFragmentReady(AndPermissionFragment autoPermissionFragment) { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + Uri uri = Uri.fromParts("package", mActivity.getPackageName(), null); + intent.setData(uri); + try { + autoPermissionFragment.startActivityForResult(intent, REQUEST_PERMISSION_FOR_SETTING, null); + } catch (Exception ignore) { + } + } + + @SuppressWarnings("unused") + void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == REQUEST_PERMISSION_FOR_SETTING) {//申请权限 + if (!AndUtils.hasPermission(mActivity, mPerms)) {//Setting界面回来之后,没有授予权限 + if (mOnDenied != null) { + mOnDenied.accept(mDeniedPermission); + } + if (mShowTips) { + getPermissionUIProvider().showPermissionDeniedTip(mActivity, mDeniedPermission.toArray(new String[0])); + } + } else { + if (mOnGranted != null) { + mOnGranted.accept(mDeniedPermission);//所有权限被获取 + } + } + } + } + + private IPermissionUIProvider getPermissionUIProvider() { + if (mPermissionUIProvider == null) { + mPermissionUIProvider = PermissionUIProviderFactory.getPermissionUIProvider(); + } + return mPermissionUIProvider; + } + +} diff --git a/lib_base/src/main/java/com/android/base/permission/AndUtils.java b/lib_base/src/main/java/com/android/base/permission/AndUtils.java new file mode 100644 index 0000000..f69666a --- /dev/null +++ b/lib_base/src/main/java/com/android/base/permission/AndUtils.java @@ -0,0 +1,21 @@ +package com.android.base.permission; + +import android.content.Context; +import android.support.annotation.NonNull; + +import com.yanzhenjie.permission.checker.DoubleChecker; + +/** + * @author Ztiany + * Date : 2018-09-07 11:38 + */ +class AndUtils { + + /** + * direct check permissions + */ + static boolean hasPermission(@NonNull Context context, @NonNull String... permissions) { + return new DoubleChecker().hasPermission(context, permissions); + } + +} diff --git a/lib_base/src/main/java/com/android/base/permission/AutoPermissionFragment.java b/lib_base/src/main/java/com/android/base/permission/AutoPermissionFragment.java new file mode 100644 index 0000000..53a662c --- /dev/null +++ b/lib_base/src/main/java/com/android/base/permission/AutoPermissionFragment.java @@ -0,0 +1,95 @@ +package com.android.base.permission; + +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-04-16 17:48 + */ +public class AutoPermissionFragment extends Fragment { + + static AutoPermissionFragment newInstance() { + return new AutoPermissionFragment(); + } + + private final Handler mHandler = new Handler(); + private final Runnable mRunnable = this::startChecked; + + private AutoPermissionFragmentCallback mRequester; + private boolean mIsActivityReady = false; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mIsActivityReady = true; + } + + @Override + public void onResume() { + super.onResume(); + mIsActivityReady = true; + } + + @Override + public void onPause() { + super.onPause(); + mHandler.removeCallbacks(mRunnable); + mIsActivityReady = false; + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (mRequester != null) { + mRequester.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + mRequester = null; + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (mRequester != null) { + mRequester.onActivityResult(requestCode, resultCode, data); + } + } + + void startRequest() { + startChecked(); + } + + private void startChecked() { + if (mIsActivityReady) { + if (mRequester != null) { + mRequester.onReady(); + } + mHandler.removeCallbacks(mRunnable); + } else { + mHandler.post(mRunnable); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + mHandler.removeCallbacks(mRunnable); + } + + void setRequester(AutoPermissionFragmentCallback requester) { + mRequester = requester; + } + + interface AutoPermissionFragmentCallback{ + void onReady(); + + void onActivityResult(int requestCode, int resultCode, Intent data); + + void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults); + } + +} diff --git a/lib_base/src/main/java/com/android/base/permission/AutoPermissionRequester.java b/lib_base/src/main/java/com/android/base/permission/AutoPermissionRequester.java new file mode 100644 index 0000000..bb5548a --- /dev/null +++ b/lib_base/src/main/java/com/android/base/permission/AutoPermissionRequester.java @@ -0,0 +1,145 @@ +package com.android.base.permission; + +import android.content.Intent; +import android.support.annotation.NonNull; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; + +import com.android.base.utils.android.ActFragWrapper; + +import static com.android.base.permission.PermissionCode.PERMISSION_REQUESTER_CODE; + +/** + *
+ *      1: 使用该类申请权限,当所有的权限都通过时回调权限获取成功,否则回调权限获取失败。
+ *      2:不要同时调用requestPermission方法多次!!!以保证一个完整的流程。
+ * 获取权限流程,以申请相机权限为例:
+ *          1先检查自身是否有相机权限
+ *          2如果有我们的app已经有了相机权限,则可以直接使用相机相关功能了
+ *          3如果没有权限我们就需要请求权限了,但是还需要处理不再询问的设置
+ *              3.1如果shouldShowRequestPermissionRationale返回false,则说明接下来的对话框不包含”不再询问“选择框,我们可以直接申请权限
+ *              3.2如果shouldShowRequestPermissionRationale返回true,我们最好先弹出一个对话框来说明我们需要权限来做什么,让用户来选择是否继续授予权限,如果用户允许继续授予权限则继续申请权限
+ *          4不管权限是否授予给我们的App,都可以在onRequestPermissionsResult的回调中获取结果,我们再问一次
+ * 
+ * + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-01-11 15:09 + */ +public class AutoPermissionRequester { + + private final FragmentActivity mActivity; + private String[] mPerms; + private boolean mAskAgain = true; + + private IPermissionUIProvider mPermissionUIProvider; + private PermissionCallback mPermissionCallback; + private PermissionRequester mPermissionRequester; + + private OnAllPermissionGrantedListener mOnAllPermissionGrantedListener; + private OnPermissionDeniedListener mOnPermissionDeniedListener; + + private AutoPermissionFragment.AutoPermissionFragmentCallback mAutoPermissionFragmentCallback; + + private AutoPermissionRequester(FragmentActivity activity) { + mActivity = activity; + if (mActivity == null) { + throw new NullPointerException(); + } + } + + public static AutoPermissionRequester with(Fragment fragment) { + return new AutoPermissionRequester(fragment.getActivity()); + } + + public static AutoPermissionRequester with(FragmentActivity activity) { + return new AutoPermissionRequester(activity); + } + + public AutoPermissionRequester permission(String... permissions) { + if (permissions == null || permissions.length == 0) { + throw new IllegalArgumentException(); + } + mPerms = permissions; + return this; + } + + public AutoPermissionRequester askAgain(boolean askAgain) { + mAskAgain = askAgain; + return this; + } + + public AutoPermissionRequester customUI(IPermissionUIProvider uiProvider) { + mPermissionUIProvider = uiProvider; + return this; + } + + public AutoPermissionRequester onGranted(OnAllPermissionGrantedListener listener) { + mOnAllPermissionGrantedListener = listener; + return this; + } + + public AutoPermissionRequester onDenied(OnPermissionDeniedListener listener) { + mOnPermissionDeniedListener = listener; + return this; + } + + public void request() { + mPermissionCallback = new PermissionCallback(mOnPermissionDeniedListener, mOnAllPermissionGrantedListener); + startRequest(); + } + + private void startRequest() { + FragmentManager supportFragmentManager = mActivity.getSupportFragmentManager(); + AutoPermissionFragment fragment = (AutoPermissionFragment) supportFragmentManager.findFragmentByTag(AutoPermissionFragment.class.getName()); + + if (fragment == null) { + fragment = AutoPermissionFragment.newInstance(); + + mPermissionRequester = new PermissionRequester(ActFragWrapper.create(fragment), mPermissionCallback, mAskAgain, mPermissionUIProvider); + fragment.setRequester(getCallback()); + + supportFragmentManager.beginTransaction() + .add(fragment, AutoPermissionFragment.class.getName()) + .commitNowAllowingStateLoss(); + + } else { + fragment.setRequester(getCallback()); + mPermissionRequester = new PermissionRequester(ActFragWrapper.create(fragment), mPermissionCallback, mAskAgain, mPermissionUIProvider); + } + + fragment.startRequest(); + } + + private AutoPermissionFragment.AutoPermissionFragmentCallback getCallback() { + if (mAutoPermissionFragmentCallback == null) { + return mAutoPermissionFragmentCallback = new AutoPermissionFragment.AutoPermissionFragmentCallback() { + @Override + public void onReady() { + if (mPermissionRequester != null) { + mPermissionRequester.requestPermission(PERMISSION_REQUESTER_CODE, mPerms); + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (PERMISSION_REQUESTER_CODE != requestCode) { + if (mPermissionRequester != null) { + mPermissionRequester.onActivityResult(requestCode, resultCode, data); + } + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (mPermissionRequester != null) { + mPermissionRequester.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } + }; + } + return mAutoPermissionFragmentCallback; + } + +} diff --git a/lib_base/src/main/java/com/android/base/permission/DefaultPermissionUIProvider.java b/lib_base/src/main/java/com/android/base/permission/DefaultPermissionUIProvider.java new file mode 100644 index 0000000..6ddd97f --- /dev/null +++ b/lib_base/src/main/java/com/android/base/permission/DefaultPermissionUIProvider.java @@ -0,0 +1,99 @@ +package com.android.base.permission; + +import android.content.Context; +import android.content.DialogInterface; +import android.graphics.Color; +import android.support.annotation.NonNull; +import android.support.v7.app.AlertDialog; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.ForegroundColorSpan; + +import com.android.base.R; +import com.blankj.utilcode.util.AppUtils; +import com.blankj.utilcode.util.ToastUtils; +import com.yanzhenjie.permission.Permission; + +import java.util.Arrays; +import java.util.List; + +class DefaultPermissionUIProvider implements IPermissionUIProvider { + + private static final String COLOR_STRING = "#FF4081"; + + @Override + public void showPermissionRationaleDialog(Context context, final String[] permission, + final DialogInterface.OnClickListener onContinueListener, DialogInterface.OnClickListener onCancelListener) { + + AlertDialog dialog = new AlertDialog.Builder(context) + .setMessage(DefaultPermissionResourceProvider.createPermissionRationaleText(context, permission)) + .setCancelable(false) + .setPositiveButton(R.string.Base_Confirm, onContinueListener) + .setNegativeButton(R.string.Base_Cancel, onCancelListener) + .create(); + + dialog.show(); + } + + @Override + public void showAskAgainDialog(Context context, final String[] permission, + DialogInterface.OnClickListener onToSetPermissionListener, + DialogInterface.OnClickListener onCancelListener) { + + AlertDialog dialog = new AlertDialog.Builder(context) + .setMessage(DefaultPermissionResourceProvider.createPermissionAskAgainText(context, permission)) + .setCancelable(false) + .setPositiveButton(R.string.Base_to_set_permission, onToSetPermissionListener) + .setNegativeButton(R.string.Base_Cancel, onCancelListener) + .create(); + dialog.show(); + } + + @Override + public void showPermissionDeniedTip(Context contexts, String[] permission) { + ToastUtils.showShort(DefaultPermissionResourceProvider.createPermissionDeniedTip(contexts, permission)); + } + + private static final class DefaultPermissionResourceProvider { + + static CharSequence createPermissionRationaleText(Context context, @NonNull String[] perms) { + String permissionText = createPermissionText(context, Arrays.asList(perms)); + return tintText(context.getString(R.string.Base_request_permission_rationale, permissionText), permissionText); + } + + static CharSequence createPermissionAskAgainText(Context context, @NonNull String[] permission) { + String permissionText = createPermissionText(context, Arrays.asList(permission)); + String appName = AppUtils.getAppName(); + String content = context.getString(R.string.Base_permission_denied_ask_again_rationale, appName, permissionText); + return tintText(content, permissionText); + } + + static CharSequence createPermissionDeniedTip(Context context, String[] permission) { + String permissionText = createPermissionText(context, Arrays.asList(permission)); + return tintText(context.getString( + R.string.Base_permission_denied, permissionText), permissionText); + } + + private static CharSequence tintText(String content, String perms) { + SpannableStringBuilder ssb = new SpannableStringBuilder(content); + int indexPerm = content.indexOf(perms); + ssb.setSpan(new ForegroundColorSpan(Color.parseColor(COLOR_STRING)), indexPerm, indexPerm + perms.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + return ssb; + } + + private static String createPermissionText(Context context, @NonNull List perms) { + List permList = Permission.transformText(context, perms); + String characterSeg = context.getString(R.string.Base_character_seg); + StringBuilder sb = new StringBuilder(); + int size = permList.size(); + for (int i = 0; i < size; i++) { + sb.append(permList.get(i)); + if (i < size - 1) { + sb.append(characterSeg); + } + } + return sb.toString(); + } + } + +} diff --git a/lib_base/src/main/java/com/android/base/permission/EasyPermissions.java b/lib_base/src/main/java/com/android/base/permission/EasyPermissions.java new file mode 100644 index 0000000..7455382 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/permission/EasyPermissions.java @@ -0,0 +1,219 @@ +/* + * Copyright Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.base.permission; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.provider.Settings; +import android.support.v4.app.ActivityCompat; +import android.support.v4.app.Fragment; +import android.support.v4.content.ContextCompat; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import timber.log.Timber; + +/** + *
+ *   See:
+ *          https://github.com/googlesamples/easypermissions
+ *          http://droidyue.com/blog/2016/01/17/understanding-marshmallow-runtime-permission/index.html
+ *          http://jijiaxin89.com/2015/08/30/Android-s-Runtime-Permission/
+ *
+ * Utility to request and check System permissions for apps targeting Android M (API >= 23).
+ *
+ * 
+ */ +final class EasyPermissions { + + interface PermissionCaller { + + void onPortionPermissionsGranted(boolean allGranted, int requestCode, List perms); + + void onPermissionsDenied(int requestCode, List perms); + + /** + * @return PermissionCaller must is Fragment(app and support) or Activity + */ + Object getRequester(); + + IPermissionUIProvider getPermissionUIProvider(); + } + + static private Context getContext(PermissionCaller permissionCaller) { + Object object = permissionCaller.getRequester(); + if (object instanceof Activity) { + return (Activity) object; + } else if (object instanceof Fragment) { + return ((Fragment) object).getActivity(); + } else if (object instanceof android.app.Fragment) { + return ((android.app.Fragment) object).getActivity(); + } else { + throw new RuntimeException("PermissionCaller getRequester mu"); + } + } + + /** + * 是否有权限 + */ + static boolean hasPermissions(Context context, String... perms) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + Timber.w("hasPermissions: API version < M, returning true by default"); + return true; + } + for (String perm : perms) { + boolean hasPerm = (ContextCompat.checkSelfPermission(context, perm) == + PackageManager.PERMISSION_GRANTED); + if (!hasPerm) { + return false; + } + } + return true; + } + + + /** + * 请求权限 + */ + static void requestPermissions(final PermissionCaller permissionCaller, final int requestCode, final String... perms) { + + if (hasPermissions(getContext(permissionCaller), perms)) { + permissionCaller.onPortionPermissionsGranted(true, requestCode, Arrays.asList(perms)); + return; + } + + final String[] filterPerms = filter(getContext(permissionCaller), perms);//过滤已有权限 + + //判断是否有不再询问的复选框 + boolean shouldShowRationale = false; + for (String filterPerm : filterPerms) { + shouldShowRationale = shouldShowRationale || shouldShowRequestPermissionRationale(permissionCaller, filterPerm); + } + + if (shouldShowRationale) { + permissionCaller.getPermissionUIProvider().showPermissionRationaleDialog( + getContext(permissionCaller), filterPerms, + (dialog, which) -> executePermissionsRequest(permissionCaller, filterPerms, requestCode), + (dialog, which) -> permissionCaller.onPermissionsDenied(requestCode, Arrays.asList(filterPerms))); + return; + } + //没有不再询问的复选框,直接申请 + executePermissionsRequest(permissionCaller, filterPerms, requestCode); + } + + /** + * 过滤掉已有的权限 + */ + @TargetApi(23) + static String[] filter(Context context, String[] perms) { + List permList = new ArrayList<>(); + for (String perm : perms) { + boolean hasPerm = (ContextCompat.checkSelfPermission(context, perm) == + PackageManager.PERMISSION_GRANTED); + if (!hasPerm) { + permList.add(perm); + } + } + return permList.toArray(new String[0]); + } + + /** + * 处理申请权限后的结果 + */ + static void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults, PermissionCaller permissionCaller) { + // Make a collection of granted and denied permissions from the request. + ArrayList granted = new ArrayList<>(); + ArrayList denied = new ArrayList<>(); + for (int i = 0; i < permissions.length; i++) { + String perm = permissions[i]; + if (grantResults[i] == PackageManager.PERMISSION_GRANTED) { + granted.add(perm); + } else { + denied.add(perm); + } + } + // Report denied permissions, if any. + if (!denied.isEmpty()) { + permissionCaller.onPermissionsDenied(requestCode, denied); + } + // Report granted permissions, if any. + if (!granted.isEmpty()) { + boolean allGranted = denied.isEmpty(); + permissionCaller.onPortionPermissionsGranted(allGranted, requestCode, granted); + } + } + + /** + * 这个方法需要在onPermissionDenial后调用,如果此时shouldShowRationale为false,表示用户已经选择了不再询问切拒绝了授予权限,需要到应用详情界面设置权限 + */ + static boolean checkDeniedPermissionsNeverAskAgain(final PermissionCaller permissionCaller, List deniedPerms) { + boolean shouldShowRationale = false; + for (String perm : deniedPerms) { + shouldShowRationale = shouldShowRequestPermissionRationale(permissionCaller, perm); + } + return !shouldShowRationale; + } + + /** + * 获取一个到详情界面的intent,去设置权限 + * + * @param context 上下文 + * @return Intent + */ + static Intent getIntentForPermission(Context context) { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + Uri uri = Uri.fromParts("package", context.getPackageName(), null); + intent.setData(uri); + return intent; + } + + /** + * 帮我们判断接下来的对话框是否包含”不再询问“选择框。 + * + * @return true表示有 + */ + @TargetApi(23) + private static boolean shouldShowRequestPermissionRationale(PermissionCaller permissionCaller, String perm) { + if (permissionCaller.getRequester() instanceof Activity) { + return ActivityCompat.shouldShowRequestPermissionRationale((Activity) permissionCaller.getRequester(), perm); + } else if (permissionCaller.getRequester() instanceof Fragment) { + return ((Fragment) permissionCaller.getRequester()).shouldShowRequestPermissionRationale(perm); + } else + return permissionCaller.getRequester() instanceof android.app.Fragment + && ((android.app.Fragment) permissionCaller.getRequester()).shouldShowRequestPermissionRationale(perm); + } + + /** + * 执行申请权限 + */ + @TargetApi(23) + private static void executePermissionsRequest(PermissionCaller permissionCaller, String[] perms, int requestCode) { + if (permissionCaller.getRequester() instanceof Activity) { + ActivityCompat.requestPermissions((Activity) permissionCaller.getRequester(), perms, requestCode); + } else if (permissionCaller.getRequester() instanceof Fragment) { + ((Fragment) permissionCaller.getRequester()).requestPermissions(perms, requestCode); + } else if (permissionCaller.getRequester() instanceof android.app.Fragment) { + ((android.app.Fragment) permissionCaller.getRequester()).requestPermissions(perms, requestCode); + } + } +} diff --git a/lib_base/src/main/java/com/android/base/permission/IPermissionUIProvider.java b/lib_base/src/main/java/com/android/base/permission/IPermissionUIProvider.java new file mode 100644 index 0000000..a92ff3d --- /dev/null +++ b/lib_base/src/main/java/com/android/base/permission/IPermissionUIProvider.java @@ -0,0 +1,37 @@ +package com.android.base.permission; + +import android.content.Context; +import android.content.DialogInterface; + + +public interface IPermissionUIProvider { + + /** + * 显示需要获取权限的原因,询问是否继续 + * + * @param context 上下文 + * @param permission 需要的权限 + * @param onContinueListener 继续 + * @param onCancelListener 取消 + */ + void showPermissionRationaleDialog(Context context, final String[] permission, final DialogInterface.OnClickListener onContinueListener, DialogInterface.OnClickListener onCancelListener); + + /** + * 拒绝权限后,询问是否去设置界面授予应用权限 + * + * @param context 上下文 + * @param permission 需要的权限 + * @param onContinueListener 继续 + * @param onCancelListener 取消 + */ + void showAskAgainDialog(Context context, final String[] permission, DialogInterface.OnClickListener onContinueListener, DialogInterface.OnClickListener onCancelListener); + + /** + * 权限被拒绝后,展示一个提示消息,比如 toast + * + * @param contexts 上下文 + * @param permission 被拒绝的权限 + */ + void showPermissionDeniedTip(Context contexts, String[] permission); + +} diff --git a/lib_base/src/main/java/com/android/base/permission/OnAllPermissionGrantedListener.java b/lib_base/src/main/java/com/android/base/permission/OnAllPermissionGrantedListener.java new file mode 100644 index 0000000..41e4bfc --- /dev/null +++ b/lib_base/src/main/java/com/android/base/permission/OnAllPermissionGrantedListener.java @@ -0,0 +1,10 @@ +package com.android.base.permission; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-07-02 13:51 + */ +public interface OnAllPermissionGrantedListener { + void onAllPermissionGranted(); +} diff --git a/lib_base/src/main/java/com/android/base/permission/OnPermissionDeniedListener.java b/lib_base/src/main/java/com/android/base/permission/OnPermissionDeniedListener.java new file mode 100644 index 0000000..2fea808 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/permission/OnPermissionDeniedListener.java @@ -0,0 +1,12 @@ +package com.android.base.permission; + +import java.util.List; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-07-02 13:50 + */ +public interface OnPermissionDeniedListener { + void onPermissionDenied(List permissions); +} diff --git a/lib_base/src/main/java/com/android/base/permission/Permission.java b/lib_base/src/main/java/com/android/base/permission/Permission.java new file mode 100644 index 0000000..8b90ffd --- /dev/null +++ b/lib_base/src/main/java/com/android/base/permission/Permission.java @@ -0,0 +1,209 @@ +package com.android.base.permission; + +import android.content.Context; +import android.os.Build; + +import com.android.base.R; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * copy from https://github.com/yanzhenjie/AndPermission + */ +public final class Permission { + + + public static final String READ_CALENDAR = "android.permission.READ_CALENDAR"; + public static final String WRITE_CALENDAR = "android.permission.WRITE_CALENDAR"; + + public static final String CAMERA = "android.permission.CAMERA"; + + public static final String READ_CONTACTS = "android.permission.READ_CONTACTS"; + public static final String WRITE_CONTACTS = "android.permission.WRITE_CONTACTS"; + public static final String GET_ACCOUNTS = "android.permission.GET_ACCOUNTS"; + + public static final String ACCESS_FINE_LOCATION = "android.permission.ACCESS_FINE_LOCATION"; + public static final String ACCESS_COARSE_LOCATION = "android.permission.ACCESS_COARSE_LOCATION"; + + public static final String RECORD_AUDIO = "android.permission.RECORD_AUDIO"; + + public static final String READ_PHONE_STATE = "android.permission.READ_PHONE_STATE"; + public static final String CALL_PHONE = "android.permission.CALL_PHONE"; + public static final String READ_CALL_LOG = "android.permission.READ_CALL_LOG"; + public static final String WRITE_CALL_LOG = "android.permission.WRITE_CALL_LOG"; + public static final String ADD_VOICEMAIL = "com.android.voicemail.permission.ADD_VOICEMAIL"; + static final String ADD_VOICEMAIL_MANIFEST = "android.permission.ADD_VOICEMAIL"; + public static final String USE_SIP = "android.permission.USE_SIP"; + public static final String PROCESS_OUTGOING_CALLS = "android.permission.PROCESS_OUTGOING_CALLS"; + public static final String READ_PHONE_NUMBERS = "android.permission.READ_PHONE_NUMBERS"; + public static final String ANSWER_PHONE_CALLS = "android.permission.ANSWER_PHONE_CALLS"; + + public static final String BODY_SENSORS = "android.permission.BODY_SENSORS"; + + public static final String SEND_SMS = "android.permission.SEND_SMS"; + public static final String RECEIVE_SMS = "android.permission.RECEIVE_SMS"; + public static final String READ_SMS = "android.permission.READ_SMS"; + public static final String RECEIVE_WAP_PUSH = "android.permission.RECEIVE_WAP_PUSH"; + public static final String RECEIVE_MMS = "android.permission.RECEIVE_MMS"; + + public static final String READ_EXTERNAL_STORAGE = "android.permission.READ_EXTERNAL_STORAGE"; + public static final String WRITE_EXTERNAL_STORAGE = "android.permission.WRITE_EXTERNAL_STORAGE"; + + public static final class Group { + + public static final String[] CALENDAR = new String[]{Permission.READ_CALENDAR, Permission.WRITE_CALENDAR}; + + public static final String[] CAMERA = new String[]{Permission.CAMERA}; + + public static final String[] CONTACTS = new String[]{Permission.READ_CONTACTS, Permission.WRITE_CONTACTS, + Permission.GET_ACCOUNTS}; + + public static final String[] LOCATION = new String[]{Permission.ACCESS_FINE_LOCATION, + Permission.ACCESS_COARSE_LOCATION}; + + public static final String[] MICROPHONE = new String[]{Permission.RECORD_AUDIO}; + + public static final String[] PHONE; + + static { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + PHONE = new String[]{Permission.READ_PHONE_STATE, Permission.CALL_PHONE, Permission.READ_CALL_LOG, + Permission.WRITE_CALL_LOG, Permission.ADD_VOICEMAIL, Permission.USE_SIP, + Permission.PROCESS_OUTGOING_CALLS, Permission.READ_PHONE_NUMBERS, Permission.ANSWER_PHONE_CALLS}; + } else { + PHONE = new String[]{Permission.READ_PHONE_STATE, Permission.CALL_PHONE, Permission.READ_CALL_LOG, + Permission.WRITE_CALL_LOG, Permission.ADD_VOICEMAIL, Permission.USE_SIP, + Permission.PROCESS_OUTGOING_CALLS}; + } + } + + public static final String[] SENSORS = new String[]{Permission.BODY_SENSORS}; + + public static final String[] SMS = new String[]{Permission.SEND_SMS, Permission.RECEIVE_SMS, + Permission.READ_SMS, Permission.RECEIVE_WAP_PUSH, Permission.RECEIVE_MMS}; + + public static final String[] STORAGE = new String[]{Permission.READ_EXTERNAL_STORAGE, + Permission.WRITE_EXTERNAL_STORAGE}; + } + + /** + * Turn permissions into text. + */ + public static List transformText(Context context, String... permissions) { + return transformText(context, Arrays.asList(permissions)); + } + + /** + * Turn permissions into text. + */ + public static List transformText(Context context, String[]... groups) { + List permissionList = new ArrayList<>(); + for (String[] group : groups) { + permissionList.addAll(Arrays.asList(group)); + } + return transformText(context, permissionList); + } + + /** + * Turn permissions into text. + */ + public static List transformText(Context context, List permissions) { + List textList = new ArrayList<>(); + for (String permission : permissions) { + switch (permission) { + case Permission.READ_CALENDAR: + case Permission.WRITE_CALENDAR: { + String message = context.getString(R.string.Base_permission_name_calendar); + if (!textList.contains(message)) { + textList.add(message); + } + break; + } + + case Permission.CAMERA: { + String message = context.getString(R.string.Base_permission_name_camera); + if (!textList.contains(message)) { + textList.add(message); + } + break; + } + case Permission.READ_CONTACTS: + case Permission.WRITE_CONTACTS: { + String message = context.getString(R.string.Base_permission_name_contacts); + if (!textList.contains(message)) { + textList.add(message); + } + break; + } + case Permission.GET_ACCOUNTS: { + String message = context.getString(R.string.Base_permission_name_accounts); + if (!textList.contains(message)) { + textList.add(message); + } + break; + } + case Permission.ACCESS_FINE_LOCATION: + case Permission.ACCESS_COARSE_LOCATION: { + String message = context.getString(R.string.Base_permission_name_location); + if (!textList.contains(message)) { + textList.add(message); + } + break; + } + case Permission.RECORD_AUDIO: { + String message = context.getString(R.string.Base_permission_name_microphone); + if (!textList.contains(message)) { + textList.add(message); + } + break; + } + case Permission.READ_PHONE_STATE: + case Permission.CALL_PHONE: + case Permission.READ_CALL_LOG: + case Permission.WRITE_CALL_LOG: + case Permission.ADD_VOICEMAIL: + case Permission.ADD_VOICEMAIL_MANIFEST: + case Permission.USE_SIP: + case Permission.PROCESS_OUTGOING_CALLS: + case Permission.READ_PHONE_NUMBERS: + case Permission.ANSWER_PHONE_CALLS: { + String message = context.getString(R.string.Base_permission_name_phone); + if (!textList.contains(message)) { + textList.add(message); + } + break; + } + case Permission.BODY_SENSORS: { + String message = context.getString(R.string.Base_permission_name_sensors); + if (!textList.contains(message)) { + textList.add(message); + } + break; + } + case Permission.SEND_SMS: + case Permission.RECEIVE_SMS: + case Permission.READ_SMS: + case Permission.RECEIVE_WAP_PUSH: + case Permission.RECEIVE_MMS: { + String message = context.getString(R.string.Base_permission_name_sms); + if (!textList.contains(message)) { + textList.add(message); + } + break; + } + case Permission.READ_EXTERNAL_STORAGE: + case Permission.WRITE_EXTERNAL_STORAGE: { + String message = context.getString(R.string.Base_permission_name_storage); + if (!textList.contains(message)) { + textList.add(message); + } + break; + } + } + } + return textList; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/permission/PermissionCallback.java b/lib_base/src/main/java/com/android/base/permission/PermissionCallback.java new file mode 100644 index 0000000..5d7f5e0 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/permission/PermissionCallback.java @@ -0,0 +1,27 @@ +package com.android.base.permission; // Callback + +import java.util.List; + +class PermissionCallback { + + private final OnPermissionDeniedListener mOnPermissionDeniedListener; + private final OnAllPermissionGrantedListener mOnAllPermissionGrantedListener; + + PermissionCallback(OnPermissionDeniedListener onPermissionDeniedListener, OnAllPermissionGrantedListener onAllPermissionGrantedListener) { + mOnPermissionDeniedListener = onPermissionDeniedListener; + mOnAllPermissionGrantedListener = onAllPermissionGrantedListener; + } + + void onPermissionDenied(List strings) { + if (mOnPermissionDeniedListener != null) { + mOnPermissionDeniedListener.onPermissionDenied(strings); + } + } + + void onAllPermissionGranted() { + if (mOnAllPermissionGrantedListener != null) { + mOnAllPermissionGrantedListener.onAllPermissionGranted(); + } + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/permission/PermissionCode.java b/lib_base/src/main/java/com/android/base/permission/PermissionCode.java new file mode 100644 index 0000000..305bad7 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/permission/PermissionCode.java @@ -0,0 +1,8 @@ +package com.android.base.permission; + +final class PermissionCode { + + static final int REQUEST_PERMISSION_FOR_SETTING = 999; + static final int PERMISSION_REQUESTER_CODE = 15086; + +} diff --git a/lib_base/src/main/java/com/android/base/permission/PermissionRequester.java b/lib_base/src/main/java/com/android/base/permission/PermissionRequester.java new file mode 100644 index 0000000..ce6b49c --- /dev/null +++ b/lib_base/src/main/java/com/android/base/permission/PermissionRequester.java @@ -0,0 +1,73 @@ +package com.android.base.permission; + +import android.content.Intent; +import android.support.annotation.NonNull; + +import com.android.base.utils.android.ActFragWrapper; + +import java.util.Arrays; + +import static com.android.base.permission.PermissionCode.REQUEST_PERMISSION_FOR_SETTING; + +/** + *
+ *      1:使用{@link #requestPermission(int, String...)}来申请权限,当所有的权限都通过时回调权限获取成功,否则回调权限获取失败。
+ *      2:不要同时调用requestPermission方法多次!!!以保证一个完整的流程。
+ * 获取权限流程,以申请相机权限为例:
+ *          1先检查自身是否有相机权限
+ *          2如果有我们的app已经有了相机权限,则可以直接使用相机相关功能了
+ *          3如果没有权限我们就需要请求权限了,但是还需要处理不再询问的设置
+ *              3.1如果shouldShowRequestPermissionRationale返回false,则说明接下来的对话框不包含”不再询问“选择框,我们可以直接申请权限
+ *              3.2如果shouldShowRequestPermissionRationale返回true,我们最好先弹出一个对话框来说明我们需要权限来做什么,让用户来选择是否继续授予权限,如果用户允许继续授予权限则继续申请权限
+ *          4不管权限是否授予给我们的App,都可以在onRequestPermissionsResult的回调中获取结果,我们再问一次
+ * 
+ * + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-01-11 15:09 + */ +class PermissionRequester { + + private final ActFragWrapper mActFragWrapper; + private String[] mPerms; + private PermissionCallback mPermissionCallback; + private EasyPermissions.PermissionCaller mPermissionCaller; + private final boolean mAskAgain; + private final IPermissionUIProvider mPermissionUIProvider; + + PermissionRequester(ActFragWrapper actFragWrapper, PermissionCallback permissionCallback, boolean askAgain, IPermissionUIProvider permissionUIProvider) { + mPermissionCallback = permissionCallback; + mActFragWrapper = actFragWrapper; + mAskAgain = askAgain; + mPermissionUIProvider = permissionUIProvider; + } + + private EasyPermissions.PermissionCaller getPermissionCaller() { + if (mPermissionCaller == null) { + mPermissionCaller = new PermissionRequesterImpl(mPermissionCallback, mActFragWrapper, mAskAgain, mPermissionUIProvider); + } + return mPermissionCaller; + } + + void requestPermission(int requestCode, String... perms) { + mPerms = perms; + EasyPermissions.requestPermissions(getPermissionCaller(), requestCode, mPerms); + } + + void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, getPermissionCaller()); + } + + void onActivityResult(int requestCode, @SuppressWarnings("unused") int resultCode, @SuppressWarnings("unused") Intent data) { + if (requestCode == REQUEST_PERMISSION_FOR_SETTING) {//申请权限 + if (!EasyPermissions.hasPermissions(mActFragWrapper.getContext(), mPerms)) {//Setting界面回来之后,没有授予权限 + String[] filter = EasyPermissions.filter(mActFragWrapper.getContext(), mPerms); + mPermissionCallback.onPermissionDenied(Arrays.asList(filter));//权限被拒绝 + mPermissionCaller.getPermissionUIProvider().showPermissionDeniedTip(mActFragWrapper.getContext(), filter); + } else { + mPermissionCallback.onAllPermissionGranted();//所有权限被获取 + } + } + } + +} diff --git a/lib_base/src/main/java/com/android/base/permission/PermissionRequesterImpl.java b/lib_base/src/main/java/com/android/base/permission/PermissionRequesterImpl.java new file mode 100644 index 0000000..161a7c5 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/permission/PermissionRequesterImpl.java @@ -0,0 +1,80 @@ +package com.android.base.permission; + +import android.content.Intent; + +import com.android.base.utils.android.ActFragWrapper; + +import java.util.List; + +import static com.android.base.permission.PermissionCode.REQUEST_PERMISSION_FOR_SETTING; + + +class PermissionRequesterImpl implements EasyPermissions.PermissionCaller { + + private final boolean mShouldAskAgain; + private ActFragWrapper mContextWrapper; + private PermissionCallback mPermissionCallback; + private IPermissionUIProvider mIPermissionUIProvider; + + PermissionRequesterImpl(PermissionCallback permissionCallback, ActFragWrapper contextWrapper, boolean shouldAskAgain, IPermissionUIProvider iPermissionUIProvider) { + mPermissionCallback = permissionCallback; + mContextWrapper = contextWrapper; + mShouldAskAgain = shouldAskAgain; + mIPermissionUIProvider = iPermissionUIProvider; + } + + @Override + public Object getRequester() { + if (mContextWrapper.getFragment() != null) { + return mContextWrapper.getFragment(); + } + return mContextWrapper.getContext(); + } + + @Override + public IPermissionUIProvider getPermissionUIProvider() { + if (mIPermissionUIProvider == null) { + return PermissionUIProviderFactory.getPermissionUIProvider(); + } + return mIPermissionUIProvider; + } + + /** + * 只获取到部分权限则不管,会在onPermissionsDenied和onPermissionAllGranted中处理 + */ + @Override + public void onPortionPermissionsGranted(boolean allGranted, int requestCode, List perms) { + // do nothing + if (allGranted) { + mPermissionCallback.onAllPermissionGranted(); + } + } + + @Override + public void onPermissionsDenied(final int requestCode, final List perms) { + if (!mShouldAskAgain) { + notifyPermissionDenied(perms); + return; + } + //ask again + boolean again = EasyPermissions.checkDeniedPermissionsNeverAskAgain(this, perms); + if (again) { + getPermissionUIProvider() + .showAskAgainDialog(mContextWrapper.getContext(), perms.toArray(new String[0]), + (dialog, which) -> { + Intent intentForPermission = EasyPermissions.getIntentForPermission(mContextWrapper.getContext()); + mContextWrapper.startActivityForResult(intentForPermission, REQUEST_PERMISSION_FOR_SETTING, null); + }, + (dialog, which) -> notifyPermissionDenied(perms)); + } else { + notifyPermissionDenied(perms); + } + } + + private void notifyPermissionDenied(List perms) { + mPermissionCallback.onPermissionDenied(perms); + getPermissionUIProvider().showPermissionDeniedTip(mContextWrapper.getContext(), perms.toArray(new String[0])); + } + + +} diff --git a/lib_base/src/main/java/com/android/base/permission/PermissionUIProviderFactory.java b/lib_base/src/main/java/com/android/base/permission/PermissionUIProviderFactory.java new file mode 100644 index 0000000..82a56a9 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/permission/PermissionUIProviderFactory.java @@ -0,0 +1,19 @@ +package com.android.base.permission; + + +public class PermissionUIProviderFactory { + + private static IPermissionUIProvider sIPermissionUIProvider; + + static IPermissionUIProvider getPermissionUIProvider() { + if (sIPermissionUIProvider == null) { + sIPermissionUIProvider = new DefaultPermissionUIProvider(); + } + return sIPermissionUIProvider; + } + + public static void registerPermissionUIProvider(IPermissionUIProvider iPermissionUIProvider) { + sIPermissionUIProvider = iPermissionUIProvider; + } + +} diff --git a/lib_base/src/main/java/com/android/base/receiver/NetStateReceiver.java b/lib_base/src/main/java/com/android/base/receiver/NetStateReceiver.java new file mode 100644 index 0000000..0fced23 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/receiver/NetStateReceiver.java @@ -0,0 +1,130 @@ +package com.android.base.receiver; + +import android.annotation.SuppressLint; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkInfo; +import android.net.NetworkInfo.State; + +import com.android.base.utils.android.compat.AndroidVersion; +import com.blankj.utilcode.util.NetworkUtils; + +import timber.log.Timber; + +import static com.android.base.receiver.NetworkState.STATE_GPRS; +import static com.android.base.receiver.NetworkState.STATE_NONE; +import static com.android.base.receiver.NetworkState.STATE_WIFI; + + +/** + * 网络监听,接下来会使用eventBus来分发网络状态,对网络状态感兴趣的可以对网络状态进行订阅,需要权限: + *
{@code
+ *
+ *   
+ *          
+ *               
+ *               
+ *               
+ *          
+ *      
+ * }
+ *
+ * 
+ * + * @author Ztiany + * email 1169654504@qq.com + * date 2015-12-08 14:50 + * @see Android:检测网络状态&监听网络变化 + */ +public class NetStateReceiver extends BroadcastReceiver { + + private static NetworkState mStatus = null; + + private State state_wifi = null; + private State state_gprs = null; + + @Override + public void onReceive(Context context, Intent intent) { + NetworkState tempStatus; + + // 获得网络连接服务 + ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + + getState(connManager); + + if (null != state_wifi && State.CONNECTED == state_wifi) { // 判断是否正在使用WIFI网络 + tempStatus = STATE_WIFI; + Timber.d("mStatus=" + mStatus + "改变后的网络为WIFI"); + } else if (null != state_gprs && State.CONNECTED == state_gprs) { // 判断是否正在使用GPRS网络 + tempStatus = STATE_GPRS; + Timber.d("mStatus=" + mStatus + "改变后的网络为GPRS"); + } else { + tempStatus = STATE_NONE; + Timber.d("mStatus=" + mStatus + "改变后的网络为无连接"); + } + + if (mStatus != tempStatus) { + Timber.d("mStatus与改变后的网络不同,网络真的改变了"); + //在此处 通知网络更新 + NetworkState.notify(tempStatus); + } else { + Timber.d("mStatus与改变后的网络相同,不处理"); + } + + mStatus = tempStatus; + + state_wifi = null; + state_gprs = null; + } + + @SuppressLint("MissingPermission") + private void getState(ConnectivityManager connManager) { + + if (AndroidVersion.atLeast(23)) { + + //获取所有网络连接的信息 + Network[] networks = connManager.getAllNetworks(); + NetworkInfo networkInfo; + for (Network network : networks) { + //获取ConnectivityManager对象对应的NetworkInfo对象 + networkInfo = connManager.getNetworkInfo(network); + if (networkInfo == null) { + continue; + } + if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { + state_wifi = networkInfo.getState(); + } else if (networkInfo.getType() == ConnectivityManager.TYPE_MOBILE) { + state_gprs = networkInfo.getState(); + } + if (state_wifi != null && state_gprs != null) { + break; + } + } + + if (!NetworkUtils.isConnected()) { + Timber.d("api 23 after getState check isConnected = false"); + state_wifi = null; + state_gprs = null; + } + + Timber.d("api 23 after getState state_wifi = " + state_wifi + " state_gprs = " + state_gprs); + } else { + + try { + state_wifi = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI).getState(); // 获取网络连接状态 + } catch (Exception e) { + Timber.d("测试机没有WIFI模块"); + } + try { + state_gprs = connManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE).getState(); // 获取网络连接状态 + } catch (Exception e) { + Timber.d("测试机没有GPRS模块"); + } + Timber.d("api 23 before getState state_wifi = " + state_wifi + " state_gprs = " + state_gprs); + } + } + +} diff --git a/lib_base/src/main/java/com/android/base/receiver/NetworkState.java b/lib_base/src/main/java/com/android/base/receiver/NetworkState.java new file mode 100644 index 0000000..66e57ad --- /dev/null +++ b/lib_base/src/main/java/com/android/base/receiver/NetworkState.java @@ -0,0 +1,27 @@ +package com.android.base.receiver; + +import io.reactivex.Flowable; +import io.reactivex.processors.BehaviorProcessor; + +public enum NetworkState { + + STATE_WIFI, + STATE_GPRS, + STATE_NONE; + + private static final BehaviorProcessor PROCESSOR = BehaviorProcessor.create(); + + public static Flowable observableState() { + return PROCESSOR; + } + + public boolean isConnected() { + return this != STATE_NONE; + } + + static void notify(NetworkState networkState) { + PROCESSOR.onNext(networkState); + } + + +} diff --git a/lib_base/src/main/java/com/android/base/rx/AutoDispose.kt b/lib_base/src/main/java/com/android/base/rx/AutoDispose.kt new file mode 100644 index 0000000..eb3e92d --- /dev/null +++ b/lib_base/src/main/java/com/android/base/rx/AutoDispose.kt @@ -0,0 +1,103 @@ +@file:JvmName("AutoDisposeUtils") + +package com.android.base.rx + +import android.arch.lifecycle.Lifecycle +import android.arch.lifecycle.LifecycleOwner +import com.uber.autodispose.* +import com.uber.autodispose.AutoDispose.autoDisposable +import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider +import io.reactivex.* +import io.reactivex.disposables.Disposable +import timber.log.Timber + +fun Flowable.bindLifecycle(lifecycleOwner: LifecycleOwner): FlowableSubscribeProxy { + return this.`as`(autoDisposable(AndroidLifecycleScopeProvider.from(lifecycleOwner))) +} + +fun Flowable.bindLifecycle(lifecycleOwner: LifecycleOwner, event: Lifecycle.Event): FlowableSubscribeProxy { + return this.`as`(autoDisposable(AndroidLifecycleScopeProvider.from(lifecycleOwner, event))) +} + +fun Observable.bindLifecycle(lifecycleOwner: LifecycleOwner): ObservableSubscribeProxy { + return this.`as`(autoDisposable(AndroidLifecycleScopeProvider.from(lifecycleOwner))) +} + +fun Observable.bindLifecycle(lifecycleOwner: LifecycleOwner, event: Lifecycle.Event): ObservableSubscribeProxy { + return this.`as`(autoDisposable(AndroidLifecycleScopeProvider.from(lifecycleOwner, event))) +} + +fun Completable.bindLifecycle(lifecycleOwner: LifecycleOwner): CompletableSubscribeProxy { + return this.`as`(autoDisposable(AndroidLifecycleScopeProvider.from(lifecycleOwner))) +} + +fun Completable.bindLifecycle(lifecycleOwner: LifecycleOwner, event: Lifecycle.Event): CompletableSubscribeProxy { + return this.`as`(autoDisposable(AndroidLifecycleScopeProvider.from(lifecycleOwner, event))) +} + +fun Maybe.bindLifecycle(lifecycleOwner: LifecycleOwner): MaybeSubscribeProxy { + return this.`as`(autoDisposable(AndroidLifecycleScopeProvider.from(lifecycleOwner))) +} + +fun Maybe.bindLifecycle(lifecycleOwner: LifecycleOwner, event: Lifecycle.Event): MaybeSubscribeProxy { + return this.`as`(autoDisposable(AndroidLifecycleScopeProvider.from(lifecycleOwner, event))) +} + +fun Single.bindLifecycle(lifecycleOwner: LifecycleOwner): SingleSubscribeProxy { + return this.`as`(autoDisposable(AndroidLifecycleScopeProvider.from(lifecycleOwner))) +} + +fun Single.bindLifecycle(lifecycleOwner: LifecycleOwner, event: Lifecycle.Event): SingleSubscribeProxy { + return this.`as`(autoDisposable(AndroidLifecycleScopeProvider.from(lifecycleOwner, event))) +} + +fun ObservableSubscribeProxy.subscribed(): Disposable = this.subscribe(RxKit.logResultHandler(), RxKit.logErrorHandler()) +fun FlowableSubscribeProxy.subscribed(): Disposable = this.subscribe(RxKit.logResultHandler(), RxKit.logErrorHandler()) +fun SingleSubscribeProxy.subscribed(): Disposable = this.subscribe(RxKit.logResultHandler(), RxKit.logErrorHandler()) +fun MaybeSubscribeProxy.subscribed(): Disposable = this.subscribe(RxKit.logResultHandler(), RxKit.logErrorHandler()) +fun CompletableSubscribeProxy.subscribed(): Disposable = this.subscribe(RxKit.logCompletedHandler(), RxKit.logErrorHandler()) + +fun ObservableSubscribeProxy.subscribeIgnoreError(action: (T) -> Unit): Disposable = this.subscribe( + { + action(it) + }, + { + Timber.e(it, "Kotlin Extends ignoreError: ") + } +) + +fun FlowableSubscribeProxy.subscribeIgnoreError(action: (T) -> Unit): Disposable = this.subscribe( + { + action(it) + }, + { + Timber.e(it, "Kotlin Extends ignoreError: ") + } +) + +fun SingleSubscribeProxy.subscribeIgnoreError(action: (T) -> Unit): Disposable = this.subscribe( + { + action(it) + }, + { + Timber.e(it, "Kotlin Extends ignoreError: ") + } +) + +fun MaybeSubscribeProxy.subscribeIgnoreError(action: (T) -> Unit): Disposable = this.subscribe( + { + action(it) + }, + { + Timber.e(it, "Kotlin Extends ignoreError: ") + } +) + +fun CompletableSubscribeProxy.subscribeIgnoreError(action: () -> Unit): Disposable = this.subscribe( + { + action() + }, + { + Timber.e(it, "Kotlin Extends ignoreError: ") + } +) \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/rx/FlowableRetryDelay.java b/lib_base/src/main/java/com/android/base/rx/FlowableRetryDelay.java new file mode 100644 index 0000000..ea96c3e --- /dev/null +++ b/lib_base/src/main/java/com/android/base/rx/FlowableRetryDelay.java @@ -0,0 +1,48 @@ +package com.android.base.rx; + +import android.support.annotation.Nullable; + +import org.reactivestreams.Publisher; + +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import io.reactivex.Flowable; +import io.reactivex.functions.Function; +import timber.log.Timber; + +public class FlowableRetryDelay implements Function, Publisher> { + + private final int mMaxRetries; + private final long mRetryDelayMillis; + @Nullable + private RetryChecker mRetryChecker; + private int mRetryCount = 0; + + @SuppressWarnings("unused") + public FlowableRetryDelay(final int maxRetries, final long retryDelayMillis) { + this(maxRetries, retryDelayMillis, null); + } + + public FlowableRetryDelay(final int maxRetries, final long retryDelayMillis, @Nullable RetryChecker retryChecker) { + mMaxRetries = maxRetries; + mRetryDelayMillis = retryDelayMillis; + mRetryChecker = retryChecker != null ? retryChecker : throwable -> true; + } + + @Override + public Publisher apply(Flowable throwableFlowable) { + return throwableFlowable.flatMap((Function>) throwable -> { + if (mRetryChecker != null && !mRetryChecker.verify(throwable)) { + return Flowable.error(throwable); + } + mRetryCount++; + Timber.i(new Date() + " 自动重试" + (mRetryCount + 1) + "次,在" + Thread.currentThread() + "线程"); + if (mRetryCount <= mMaxRetries) { + return Flowable.timer(mRetryDelayMillis, TimeUnit.MILLISECONDS); + } + return Flowable.error(throwable); + }); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/rx/LifecycleScopeProviderEx.kt b/lib_base/src/main/java/com/android/base/rx/LifecycleScopeProviderEx.kt new file mode 100644 index 0000000..8a7b65f --- /dev/null +++ b/lib_base/src/main/java/com/android/base/rx/LifecycleScopeProviderEx.kt @@ -0,0 +1,80 @@ +package com.android.base.rx + +import android.arch.lifecycle.Lifecycle +import android.arch.lifecycle.LifecycleOwner +import com.uber.autodispose.* +import com.uber.autodispose.lifecycle.LifecycleScopeProvider +import io.reactivex.* + +/** + *@author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-05-10 14:41 + */ +interface LifecycleScopeProviderEx : LifecycleScopeProvider { + + fun Flowable.autoDispose(): FlowableSubscribeProxy { + return this.`as`(AutoDispose.autoDisposable(this@LifecycleScopeProviderEx)) + } + + fun Observable.autoDispose(): ObservableSubscribeProxy { + return this.`as`(AutoDispose.autoDisposable(this@LifecycleScopeProviderEx)) + } + + fun Completable.autoDispose(): CompletableSubscribeProxy { + return this.`as`(AutoDispose.autoDisposable(this@LifecycleScopeProviderEx)) + } + + fun Maybe.autoDispose(): MaybeSubscribeProxy { + return this.`as`(AutoDispose.autoDisposable(this@LifecycleScopeProviderEx)) + } + + fun Single.autoDispose(): SingleSubscribeProxy { + return this.`as`(AutoDispose.autoDisposable(this@LifecycleScopeProviderEx)) + } + +} + +interface LifecycleOwnerEx : LifecycleOwner { + + fun Flowable.autoDispose(): FlowableSubscribeProxy { + return this.bindLifecycle(this@LifecycleOwnerEx) + } + + fun Observable.autoDispose(): ObservableSubscribeProxy { + return this.bindLifecycle(this@LifecycleOwnerEx) + } + + fun Completable.autoDispose(): CompletableSubscribeProxy { + return this.bindLifecycle(this@LifecycleOwnerEx) + } + + fun Maybe.autoDispose(): MaybeSubscribeProxy { + return this.bindLifecycle(this@LifecycleOwnerEx) + } + + fun Single.autoDispose(): SingleSubscribeProxy { + return this.bindLifecycle(this@LifecycleOwnerEx) + } + + fun Flowable.autoDispose(event: Lifecycle.Event): FlowableSubscribeProxy { + return this.bindLifecycle(this@LifecycleOwnerEx, event) + } + + fun Observable.autoDispose(event: Lifecycle.Event): ObservableSubscribeProxy { + return this.bindLifecycle(this@LifecycleOwnerEx, event) + } + + fun Completable.autoDispose(event: Lifecycle.Event): CompletableSubscribeProxy { + return this.bindLifecycle(this@LifecycleOwnerEx, event) + } + + fun Maybe.autoDispose(event: Lifecycle.Event): MaybeSubscribeProxy { + return this.bindLifecycle(this@LifecycleOwnerEx, event) + } + + fun Single.autoDispose(event: Lifecycle.Event): SingleSubscribeProxy { + return this.bindLifecycle(this@LifecycleOwnerEx, event) + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/rx/ObservableRetryDelay.java b/lib_base/src/main/java/com/android/base/rx/ObservableRetryDelay.java new file mode 100644 index 0000000..8bdac37 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/rx/ObservableRetryDelay.java @@ -0,0 +1,47 @@ +package com.android.base.rx; + +import android.support.annotation.Nullable; + +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import io.reactivex.Observable; +import io.reactivex.ObservableSource; +import io.reactivex.functions.Function; +import timber.log.Timber; + +public class ObservableRetryDelay implements Function, ObservableSource> { + + private final int mMaxRetries; + private final long mRetryDelayMillis; + @Nullable + private RetryChecker mRetryChecker; + private int mRetryCount = 0; + + @SuppressWarnings("unused") + public ObservableRetryDelay(final int maxRetries, final long retryDelayMillis) { + this(maxRetries, retryDelayMillis, null); + } + + public ObservableRetryDelay(final int maxRetries, final long retryDelayMillis, @Nullable RetryChecker retryChecker) { + mMaxRetries = maxRetries; + mRetryDelayMillis = retryDelayMillis; + mRetryChecker = retryChecker != null ? retryChecker : throwable -> true; + } + + @Override + public ObservableSource apply(Observable throwableObservable) { + return throwableObservable.flatMap((Function>) throwable -> { + if (mRetryChecker != null && !mRetryChecker.verify(throwable)) { + return Observable.error(throwable); + } + mRetryCount++; + Timber.i(new Date() + " 自动重试" + (mRetryCount + 1) + "次,在" + Thread.currentThread() + "线程"); + if (mRetryCount <= mMaxRetries) { + return Observable.timer(mRetryDelayMillis, TimeUnit.MILLISECONDS); + } + return Observable.error(throwable); + }); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/rx/RetryChecker.java b/lib_base/src/main/java/com/android/base/rx/RetryChecker.java new file mode 100644 index 0000000..545ed51 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/rx/RetryChecker.java @@ -0,0 +1,7 @@ +package com.android.base.rx; + +public interface RetryChecker { + + boolean verify(Throwable throwable); + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/rx/RxBus.java b/lib_base/src/main/java/com/android/base/rx/RxBus.java new file mode 100644 index 0000000..663ddb4 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/rx/RxBus.java @@ -0,0 +1,83 @@ +package com.android.base.rx; + +import android.support.annotation.NonNull; + +import java.util.UUID; + +import io.reactivex.Flowable; +import io.reactivex.processors.PublishProcessor; + + +/** + * 不要跨大范围的使用 RxBus,比较推荐的模式为针对特定模块定义特定的事件分发类: + *
+ *     {@code
+ *      public class FooEvents{
+ *
+ *                  public Observable subscribeFooEvents(){
+ *                             return RxBus.getDefault().toObservable(FooEvents.class);
+ *                  }
+ *
+ *                  public void sendFooEvent(FooEvents event){
+ *                            RxBus.getDefault().send(event);
+ *                  }
+ *              }
+ *     }
+ * 
+ */ +public class RxBus { + + private static final RxBus BUS = new RxBus(); + + public static RxBus getDefault() { + return BUS; + } + + public static RxBus newInstance() { + return new RxBus(); + } + + //All other Publisher and Subject methods are thread-safe by design. + private static final PublishProcessor mBus = PublishProcessor.create(); + + private final String COMMON_EVENT_IDENTIFY; + + private RxBus() { + COMMON_EVENT_IDENTIFY = UUID.randomUUID().toString(); + } + + public boolean hasObservers() { + return mBus.hasSubscribers(); + } + + public void send(@NonNull final Object event) { + mBus.onNext(new ObjectHolder(COMMON_EVENT_IDENTIFY, event)); + } + + public Flowable toObservable(Class tClass) { + return toObservable(COMMON_EVENT_IDENTIFY, tClass); + } + + public void send(@NonNull String identify, @NonNull final Object event) { + mBus.onNext(new ObjectHolder(identify, event)); + } + + public Flowable toObservable(@NonNull final String identify, final Class tClass) { + return mBus.ofType(ObjectHolder.class) + .filter(objectHolder -> objectHolder.identify.equals(identify) && tClass == objectHolder.event.getClass()) + .map(objectHolder -> tClass.cast(objectHolder.event)); + } + + private static class ObjectHolder { + + private final String identify; + private final Object event; + + ObjectHolder(String identify, Object event) { + this.identify = identify; + this.event = event; + } + + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/rx/RxEx.kt b/lib_base/src/main/java/com/android/base/rx/RxEx.kt new file mode 100644 index 0000000..9ede2b5 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/rx/RxEx.kt @@ -0,0 +1,116 @@ +package com.android.base.rx + +import io.reactivex.* +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import timber.log.Timber + +fun Single.observeOnUI(): Single = this.observeOn(AndroidSchedulers.mainThread()) +fun Maybe.observeOnUI(): Maybe = this.observeOn(AndroidSchedulers.mainThread()) +fun Observable.observeOnUI(): Observable = this.observeOn(AndroidSchedulers.mainThread()) +fun Flowable.observeOnUI(): Flowable = this.observeOn(AndroidSchedulers.mainThread()) +fun Completable.observeOnUI(): Completable = this.observeOn(AndroidSchedulers.mainThread()) + +fun Observable.subscribeIgnoreError(action: (T) -> Unit): Disposable = this.subscribe( + { + action(it) + }, + { + Timber.e(it, "Kotlin Extends ignoreError: ") + } +) + +fun Flowable.subscribeIgnoreError(action: (T) -> Unit): Disposable = this.subscribe( + { + action(it) + }, + { + Timber.e(it, "Kotlin Extends ignoreError: ") + } +) + +fun Single.subscribeIgnoreError(action: (T) -> Unit): Disposable = this.subscribe( + { + action(it) + }, + { + Timber.e(it, "Kotlin Extends ignoreError: ") + } +) + +fun Maybe.subscribeIgnoreError(action: (T) -> Unit): Disposable = this.subscribe( + { + action(it) + }, + { + Timber.e(it, "Kotlin Extends ignoreError: ") + } +) + +fun Completable.subscribeIgnoreError(action: () -> Unit): Disposable = this.subscribe( + { + action() + }, + { + Timber.e(it, "Kotlin Extends ignoreError: ") + } +) + +inline fun RxBus.toObservable(): Flowable = this.toObservable(A::class.java) +inline fun RxBus.toObservable(tag: String): Flowable = this.toObservable(tag, A::class.java) + +fun Observable.io2UI(): Observable = this.compose(RxKit.io2UI()) +fun Observable.newThread2UI(): Observable = this.compose(RxKit.newThread2UI()) +fun Observable.computation2UI(): Observable = this.compose(RxKit.computation2UI()) + +fun Flowable.io2UI(): Flowable = this.compose(RxKit.io2UI()) +fun Flowable.newThread2UI(): Flowable = this.compose(RxKit.newThread2UI()) +fun Flowable.computation2UI(): Flowable = this.compose(RxKit.computation2UI()) + +fun Single.io2UI(): Single = this.compose(RxKit.io2UI()) +fun Single.newThread2UI(): Single = this.compose(RxKit.newThread2UI()) +fun Single.computation2UI(): Single = this.compose(RxKit.computation2UI()) + +fun Maybe.io2UI(): Maybe = this.compose(RxKit.io2UI()) +fun Maybe.newThread2UI(): Maybe = this.compose(RxKit.newThread2UI()) +fun Maybe.computation2UI(): Maybe = this.compose(RxKit.computation2UI()) + +fun Completable.io2UI(): Completable = this.compose(RxKit.io2UI()) +fun Completable.newThread2UI(): Completable = this.compose(RxKit.newThread2UI()) +fun Completable.computation2UI(): Completable = this.compose(RxKit.computation2UI()) + +fun Observable.subscribed(): Disposable = this.subscribe(RxKit.logResultHandler(), RxKit.logErrorHandler()) +fun Flowable.subscribed(): Disposable = this.subscribe(RxKit.logResultHandler(), RxKit.logErrorHandler()) +fun Single.subscribed(): Disposable = this.subscribe(RxKit.logResultHandler(), RxKit.logErrorHandler()) +fun Maybe.subscribed(): Disposable = this.subscribe(RxKit.logResultHandler(), RxKit.logErrorHandler()) +fun Completable.subscribed(): Disposable = this.subscribe(RxKit.logCompletedHandler(), RxKit.logErrorHandler()) + + +fun Flowable.retryWhen(maxRetries: Int, retryDelayMillis: Long, retryChecker: RetryChecker? = null): Flowable { + return this.retryWhen(FlowableRetryDelay(maxRetries, retryDelayMillis, retryChecker)) +} + +fun Observable.retryWhen(maxRetries: Int, retryDelayMillis: Long, retryChecker: RetryChecker? = null): Observable { + return this.retryWhen(ObservableRetryDelay(maxRetries, retryDelayMillis, retryChecker)) +} + +fun Completable.retryWhen(maxRetries: Int, retryDelayMillis: Long, retryChecker: RetryChecker? = null): Completable { + return this.retryWhen(FlowableRetryDelay(maxRetries, retryDelayMillis, retryChecker)) +} + +fun Single.retryWhen(maxRetries: Int, retryDelayMillis: Long, retryChecker: RetryChecker? = null): Single { + return this.retryWhen(FlowableRetryDelay(maxRetries, retryDelayMillis, retryChecker)) +} + +fun Maybe.retryWhen(maxRetries: Int, retryDelayMillis: Long, retryChecker: RetryChecker? = null): Maybe { + return this.retryWhen(FlowableRetryDelay(maxRetries, retryDelayMillis, retryChecker)) +} + +operator fun CompositeDisposable?.plusAssign(disposable: Disposable) { + this?.add(disposable) +} + +fun Disposable.addTo(compositeDisposable: CompositeDisposable?) { + compositeDisposable?.add(this) +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/rx/RxKit.java b/lib_base/src/main/java/com/android/base/rx/RxKit.java new file mode 100644 index 0000000..c1db177 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/rx/RxKit.java @@ -0,0 +1,119 @@ +package com.android.base.rx; + +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Action; +import io.reactivex.functions.Consumer; +import timber.log.Timber; + +/** + * @author Ztiany + * date : 2016-03-19 23:09 + * email: 1169654504@qq.com + */ +public class RxKit { + + /** + *
+     *     {@code private CompositeSubscription mCompositeSubscription;
+     *
+     *       protected void addSubscription(Subscription subscription)
+     *                     CompositeSubscription newCompositeSubIfUnsubscribed =
+     *                     RxUtils .getNewCompositeSubIfUnsubscribed(mCompositeSubscription);
+     *                     mCompositeSubscription = newCompositeSubIfUnsubscribed;
+     *                     newCompositeSubIfUnsubscribed.add(subscription);
+     *
+     *        private void unsubscribe() {
+     *                    RxUtils.unsubscribeIfNotNull(mCompositeSubscription);
+     *        }
+     * }
+     * 
+ */ + public static void unsubscribeIfNotNull(Disposable disposable) { + if (disposable != null && !disposable.isDisposed()) { + disposable.dispose(); + } + } + + public static CompositeDisposable getNewCompositeSubIfUnsubscribed(CompositeDisposable disposable) { + if (disposable == null || disposable.isDisposed()) { + return new CompositeDisposable(); + } + return disposable; + } + + private static final Action LOG_ACTION0 = () -> Timber.d("RxUtils LOG_ACTION0 call() called"); + + private static final Consumer LOG_ACTION1 = obj -> { + if (obj instanceof Throwable) { + Timber.e((Throwable) obj, "RxUtils PRINT_ACTION1 call() called with: error"); + } else { + Timber.d("RxUtils ERROR_ACTION call() called with: result = [" + obj + "]"); + } + }; + + @SuppressWarnings("unchecked") + public static Consumer logErrorHandler() { + return (Consumer) LOG_ACTION1; + } + + public static Action logCompletedHandler() { + return LOG_ACTION0; + } + + @SuppressWarnings("unchecked") + public static Consumer logResultHandler() { + return (Consumer) LOG_ACTION1; + } + + private static final Subscriber LOG_SUBSCRIBER = new Subscriber() { + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + Timber.d("onSubscribe() called with: s = [" + s + "]"); + } + + @Override + public void onNext(Object o) { + Timber.d("onNext() called with: o = [" + o + "]"); + } + + @Override + public void onError(Throwable t) { + Timber.d("onError() called with: t = [" + t + "]"); + } + + @Override + public void onComplete() { + Timber.d("onComplete() called"); + } + }; + + @SuppressWarnings("unchecked") + public static Subscriber logSubscriber() { + return (Subscriber) LOG_SUBSCRIBER; + } + + private static final ThreadTransformer IO_2_UI_SCHEDULERS_TRANSFORMER = ThreadTransformer.newInstance(ThreadTransformer.IO_UI); + private static final ThreadTransformer COMPUTATION_2_UI_SCHEDULERS_TRANSFORMER = ThreadTransformer.newInstance(ThreadTransformer.COMPUTATION_UI); + private static final ThreadTransformer NEW_THREAD_2_UI_SCHEDULERS_TRANSFORMER = ThreadTransformer.newInstance(ThreadTransformer.NEW_THREAD_UI); + + @SuppressWarnings("unchecked") + public static ThreadTransformer io2UI() { + return IO_2_UI_SCHEDULERS_TRANSFORMER; + } + + @SuppressWarnings("unchecked") + public static ThreadTransformer computation2UI() { + return COMPUTATION_2_UI_SCHEDULERS_TRANSFORMER; + } + + @SuppressWarnings("unchecked") + public static ThreadTransformer newThread2UI() { + return NEW_THREAD_2_UI_SCHEDULERS_TRANSFORMER; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/rx/SchedulerProvider.kt b/lib_base/src/main/java/com/android/base/rx/SchedulerProvider.kt new file mode 100644 index 0000000..5070884 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/rx/SchedulerProvider.kt @@ -0,0 +1,46 @@ +@file:JvmName("SchedulerProviders") +package com.android.base.rx + +import io.reactivex.Scheduler +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers + + +/** + * Allow providing different types of [Scheduler]s. + */ +interface SchedulerProvider { + + fun computation(): Scheduler + + fun io(): Scheduler + + fun ui(): Scheduler + + fun database(): Scheduler + +} + +fun newDefaultSchedulerProvider(): SchedulerProvider { + return DefaultSchedulerProvider() +} + +private class DefaultSchedulerProvider : SchedulerProvider { + + override fun computation(): Scheduler { + return Schedulers.computation() + } + + override fun io(): Scheduler { + return Schedulers.io() + } + + override fun ui(): Scheduler { + return AndroidSchedulers.mainThread() + } + + override fun database(): Scheduler { + return Schedulers.single() + } + +} diff --git a/lib_base/src/main/java/com/android/base/rx/ThreadTransformer.java b/lib_base/src/main/java/com/android/base/rx/ThreadTransformer.java new file mode 100644 index 0000000..a83f93c --- /dev/null +++ b/lib_base/src/main/java/com/android/base/rx/ThreadTransformer.java @@ -0,0 +1,85 @@ +package com.android.base.rx; + +import org.reactivestreams.Publisher; + +import io.reactivex.Completable; +import io.reactivex.CompletableSource; +import io.reactivex.CompletableTransformer; +import io.reactivex.Flowable; +import io.reactivex.FlowableTransformer; +import io.reactivex.Maybe; +import io.reactivex.MaybeSource; +import io.reactivex.MaybeTransformer; +import io.reactivex.Observable; +import io.reactivex.ObservableSource; +import io.reactivex.ObservableTransformer; +import io.reactivex.Scheduler; +import io.reactivex.Single; +import io.reactivex.SingleSource; +import io.reactivex.SingleTransformer; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-08-10 00:03 + */ +public final class ThreadTransformer implements ObservableTransformer, + FlowableTransformer, + SingleTransformer, + MaybeTransformer, + CompletableTransformer { + + static ThreadTransformer newInstance(int type) { + return new ThreadTransformer<>(type); + } + + static final int IO_UI = 1; + static final int COMPUTATION_UI = 2; + static final int NEW_THREAD_UI = 3; + + private final int mType; + + private ThreadTransformer(int type) { + mType = type; + if (!(mType == IO_UI || mType == COMPUTATION_UI || mType == NEW_THREAD_UI)) { + throw new IllegalArgumentException("type error: " + type); + } + } + + @Override + public CompletableSource apply(Completable upstream) { + return upstream.subscribeOn(getScheduler()).observeOn(AndroidSchedulers.mainThread()); + } + + @Override + public Publisher apply(Flowable upstream) { + return upstream.subscribeOn(getScheduler()).observeOn(AndroidSchedulers.mainThread()); + } + + @Override + public MaybeSource apply(Maybe upstream) { + return upstream.subscribeOn(getScheduler()).observeOn(AndroidSchedulers.mainThread()); + } + + @Override + public ObservableSource apply(Observable upstream) { + return upstream.subscribeOn(getScheduler()).observeOn(AndroidSchedulers.mainThread()); + } + + @Override + public SingleSource apply(Single upstream) { + return upstream.subscribeOn(getScheduler()).observeOn(AndroidSchedulers.mainThread()); + } + + private Scheduler getScheduler() { + if (mType == IO_UI) { + return Schedulers.io(); + } else if (mType == COMPUTATION_UI) { + return Schedulers.computation(); + } else { + return Schedulers.newThread(); + } + } +} diff --git a/lib_base/src/main/java/com/android/base/utils/BaseUtils.java b/lib_base/src/main/java/com/android/base/utils/BaseUtils.java new file mode 100644 index 0000000..0a0650f --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/BaseUtils.java @@ -0,0 +1,45 @@ +package com.android.base.utils; + +import android.content.Context; +import android.content.res.AssetManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.util.DisplayMetrics; + +import com.blankj.utilcode.util.Utils; + +/** + * 依赖 Content 的其他工具类都由 BaseUtils 提供 + */ +public class BaseUtils { + + public static void init(Context context) { + Utils.init(context); + } + + public static Context getAppContext() { + return Utils.getApp(); + } + + public static Resources getResources() { + return BaseUtils.getAppContext().getResources(); + } + + public static Resources.Theme getTheme() { + return BaseUtils.getAppContext().getTheme(); + } + + public static AssetManager getAssets() { + return BaseUtils.getAppContext().getAssets(); + } + + @SuppressWarnings("unused") + public static Configuration getConfiguration() { + return BaseUtils.getResources().getConfiguration(); + } + + public static DisplayMetrics getDisplayMetrics() { + return BaseUtils.getResources().getDisplayMetrics(); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/utils/android/ActFragWrapper.java b/lib_base/src/main/java/com/android/base/utils/android/ActFragWrapper.java new file mode 100644 index 0000000..72bce69 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/ActFragWrapper.java @@ -0,0 +1,68 @@ +package com.android.base.utils.android; + +import android.app.Activity; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; + +/** + * 包含了Activity和Fragment的相关操作 + */ +public class ActFragWrapper { + + private Fragment mFragment; + private Activity mActivity; + + public static ActFragWrapper create(Activity activity) { + ActFragWrapper context = new ActFragWrapper(); + context.mActivity = activity; + return context; + } + + public Context getContext() { + if (mActivity != null) { + return mActivity; + } else { + return mFragment.getActivity(); + } + } + + public Fragment getFragment() { + return mFragment; + } + + public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options) { + if (mActivity != null) { + mActivity.startActivityForResult(intent, requestCode, options); + } else { + mFragment.startActivityForResult(intent, requestCode, options); + } + } + + public static ActFragWrapper create(Fragment fragment) { + ActFragWrapper context = new ActFragWrapper(); + context.mFragment = fragment; + return context; + } + + public void startService(Intent intent) { + if (mActivity != null) { + mActivity.startService(intent); + } else { + FragmentActivity activity = mFragment.getActivity(); + activity.startService(intent); + } + } + + public void stopService(Class payPalServiceClass) { + if (mActivity != null) { + mActivity.stopService(new Intent(mActivity, payPalServiceClass)); + } else { + mFragment.getActivity().stopService(new Intent(mFragment.getActivity(), payPalServiceClass)); + } + } +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/utils/android/ClipboardUtils.java b/lib_base/src/main/java/com/android/base/utils/android/ClipboardUtils.java new file mode 100644 index 0000000..efc7c50 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/ClipboardUtils.java @@ -0,0 +1,88 @@ +package com.android.base.utils.android; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; + +import com.android.base.utils.BaseUtils; + +public class ClipboardUtils { + + private ClipboardUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * 复制文本到剪贴板 + * + * @param text 文本 + */ + public static void copyText(CharSequence text) { + ClipboardManager clipboard = (ClipboardManager) BaseUtils.getAppContext().getSystemService(Context.CLIPBOARD_SERVICE); + clipboard.setPrimaryClip(ClipData.newPlainText("text", text)); + } + + /** + * 获取剪贴板的文本 + * + * @return 剪贴板的文本 + */ + public static CharSequence getText() { + ClipboardManager clipboard = (ClipboardManager) BaseUtils.getAppContext().getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = clipboard.getPrimaryClip(); + if (clip != null && clip.getItemCount() > 0) { + return clip.getItemAt(0).coerceToText(BaseUtils.getAppContext()); + } + return null; + } + + /** + * 复制uri到剪贴板 + * + * @param uri uri + */ + public static void copyUri(Uri uri) { + ClipboardManager clipboard = (ClipboardManager) BaseUtils.getAppContext().getSystemService(Context.CLIPBOARD_SERVICE); + clipboard.setPrimaryClip(ClipData.newUri(BaseUtils.getAppContext().getContentResolver(), "uri", uri)); + } + + /** + * 获取剪贴板的uri + * + * @return 剪贴板的uri + */ + public static Uri getUri() { + ClipboardManager clipboard = (ClipboardManager) BaseUtils.getAppContext().getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = clipboard.getPrimaryClip(); + if (clip != null && clip.getItemCount() > 0) { + return clip.getItemAt(0).getUri(); + } + return null; + } + + /** + * 复制意图到剪贴板 + * + * @param intent 意图 + */ + public static void copyIntent(Intent intent) { + ClipboardManager clipboard = (ClipboardManager) BaseUtils.getAppContext().getSystemService(Context.CLIPBOARD_SERVICE); + clipboard.setPrimaryClip(ClipData.newIntent("intent", intent)); + } + + /** + * 获取剪贴板的意图 + * + * @return 剪贴板的意图 + */ + public static Intent getIntent() { + ClipboardManager clipboard = (ClipboardManager) BaseUtils.getAppContext().getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = clipboard.getPrimaryClip(); + if (clip != null && clip.getItemCount() > 0) { + return clip.getItemAt(0).getIntent(); + } + return null; + } +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/DebugUtils.java b/lib_base/src/main/java/com/android/base/utils/android/DebugUtils.java new file mode 100644 index 0000000..3e3a909 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/DebugUtils.java @@ -0,0 +1,32 @@ +package com.android.base.utils.android; + +import android.os.StrictMode; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-10-12 18:32 + */ +public class DebugUtils { + + private DebugUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * 开启严苛模式 + */ + public static void startStrictMode() { + StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() + .detectAll() + .penaltyLog() + .build()); + + StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() + .detectAll() + .penaltyLog() + .penaltyDeathOnNetwork() + .build()); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/utils/android/DevicesUtils.java b/lib_base/src/main/java/com/android/base/utils/android/DevicesUtils.java new file mode 100644 index 0000000..b4e2c26 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/DevicesUtils.java @@ -0,0 +1,127 @@ +package com.android.base.utils.android; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.os.Build; +import android.support.annotation.RequiresPermission; +import android.util.Log; + +import com.android.base.utils.android.compat.AndroidVersion; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.UUID; + +import timber.log.Timber; + +public class DevicesUtils { + + private DevicesUtils() { + throw new UnsupportedOperationException("no need instantiation"); + } + + public static String getModel() { + String model = Build.MODEL; + if (model != null) { + model = model.trim().replaceAll("\\s*", ""); + } else { + model = ""; + } + return model; + } + + /** + * for getDeviceId,DevicesId 形式: + *
+     *      00000000-1082-436b-ffff-ffffc2e337a1
+     *      ffffffff-a987-897a-0000-000000de11f7
+     * 
+ */ + private static final String M_SZ_DEV_ID_SHORT = "35" + + Build.BOARD.length() % 10 + Build.BRAND.length() % 10 + + Build.CPU_ABI.length() % 10 + Build.DEVICE.length() % 10 + + Build.DISPLAY.length() % 10 + Build.HOST.length() % 10 + + Build.ID.length() % 10 + Build.MANUFACTURER.length() % 10 + + Build.MODEL.length() % 10 + Build.PRODUCT.length() % 10 + + Build.TAGS.length() % 10 + Build.TYPE.length() % 10 + + Build.USER.length() % 10; //13 位 + + /** + * 参考:http://blog.csdn.net/nugongahou110/article/details/47003257 + * 参考:https://stackoverflow.com/questions/2785485/is-there-a-unique-android-device-id/2853253#2853253 + * 参考:https://www.jianshu.com/p/b6f4b0aca6b0 + * + * @return device id + */ + @RequiresPermission(value = Manifest.permission.READ_PHONE_STATE) + public static String getDeviceId() { + String serial = null; + try { + if (AndroidVersion.atLeast(26)) { + //add in api 26, need Permission.READ_PHONE_STATE + serial = Build.getSerial(); + } else { + serial = Build.class.getField("SERIAL").get(null).toString(); + } + } catch (Exception e) { + Timber.e(e,"getDeviceId"); + } + if (serial == null) { + //serial 需要一个初始化 + serial = "serial"; // 随便一个初始化 + } + //使用硬件信息拼凑出来的15位号码 + return new UUID(M_SZ_DEV_ID_SHORT.hashCode(), serial.hashCode()).toString(); + } + + + @SuppressLint("ObsoleteSdkInt") + public static String printSystemInfo() { + Date date = new Date(System.currentTimeMillis()); + @SuppressLint("SimpleDateFormat") SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String time = dateFormat.format(date); + StringBuilder sb = new StringBuilder(); + sb.append("_______ 系统信息 ").append(time).append(" ______________"); + sb.append("\nID :").append(Build.ID); + sb.append("\nBRAND :").append(Build.BRAND); + sb.append("\nMODEL :").append(Build.MODEL); + sb.append("\nRELEASE :").append(Build.VERSION.RELEASE); + sb.append("\nSDK :").append(Build.VERSION.SDK); + + sb.append("\n_______ OTHER _______"); + sb.append("\nBOARD :").append(Build.BOARD); + sb.append("\nPRODUCT :").append(Build.PRODUCT); + sb.append("\nDEVICE :").append(Build.DEVICE); + sb.append("\nFINGERPRINT :").append(Build.FINGERPRINT); + sb.append("\nHOST :").append(Build.HOST); + sb.append("\nTAGS :").append(Build.TAGS); + sb.append("\nTYPE :").append(Build.TYPE); + sb.append("\nTIME :").append(Build.TIME); + sb.append("\nINCREMENTAL :").append(Build.VERSION.INCREMENTAL); + + sb.append("\n_______ CUPCAKE-3 _______"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CUPCAKE) { + sb.append("\nDISPLAY :").append(Build.DISPLAY); + } + + sb.append("\n_______ DONUT-4 _______"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.DONUT) { + sb.append("\nSDK_INT :").append(Build.VERSION.SDK_INT); + sb.append("\nMANUFACTURER :").append(Build.MANUFACTURER); + sb.append("\nBOOTLOADER :").append(Build.BOOTLOADER); + sb.append("\nCPU_ABI :").append(Build.CPU_ABI); + sb.append("\nCPU_ABI2 :").append(Build.CPU_ABI2); + sb.append("\nHARDWARE :").append(Build.HARDWARE); + sb.append("\nUNKNOWN :").append(Build.UNKNOWN); + sb.append("\nCODENAME :").append(Build.VERSION.CODENAME); + } + + sb.append("\n_______ GINGERBREAD-9 _______"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { + sb.append("\nSERIAL :").append(Build.SERIAL); + } + Log.i("DEVICES", sb.toString()); + return sb.toString(); + } + +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/DirectoryUtils.java b/lib_base/src/main/java/com/android/base/utils/android/DirectoryUtils.java new file mode 100644 index 0000000..8eae586 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/DirectoryUtils.java @@ -0,0 +1,69 @@ +package com.android.base.utils.android; + +import android.os.Environment; +import android.support.annotation.NonNull; + +import com.android.base.utils.BaseUtils; + +import java.io.File; + +import timber.log.Timber; + +public class DirectoryUtils { + + private DirectoryUtils() { + } + + /** + * 获取SD卡上私有的外部存储 + * + * @return /storage/emulated/0/Android/data/包名/cache/ + */ + public static File getAppExternalCacheStorage() { + String state = Environment.getExternalStorageState(); + if (Environment.MEDIA_MOUNTED.equals(state)) { + return BaseUtils.getAppContext().getExternalCacheDir(); + } else { + return BaseUtils.getAppContext().getCacheDir(); + } + } + + /** + * 获取SD卡上外部存储 + * + * @return /storage/emulated/0/ + */ + public static File getExternalStorage() { + String state = Environment.getExternalStorageState(); + if (Environment.MEDIA_MOUNTED.equals(state)) { + return Environment.getExternalStorageDirectory(); + } else { + return BaseUtils.getAppContext().getCacheDir(); + } + } + + /** + * 获取公共的外部存储目录 + * + * @param type {@link Environment#DIRECTORY_DOWNLOADS}, + * {@link Environment#DIRECTORY_DCIM}, ect + * @return DIRECTORY_DCIM = /storage/sdcard0/DCIM , + * DIRECTORY_DOWNLOADS = /storage/sdcard0/Download ...ect + */ + public static File getExternalStoragePublicDirectory(@NonNull String type) { + String state = Environment.getExternalStorageState(); + File dir; + if (Environment.MEDIA_MOUNTED.equals(state)) { + dir = Environment.getExternalStoragePublicDirectory(type); + } else { + dir = new File(BaseUtils.getAppContext().getCacheDir(), type); + } + if (dir != null && !dir.exists()) { + boolean mkdirs = dir.mkdirs(); + Timber.d("getExternalStoragePublicDirectory type = " + type + " mkdirs = " + mkdirs); + } + return dir; + } + + +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/ResourceUtils.java b/lib_base/src/main/java/com/android/base/utils/android/ResourceUtils.java new file mode 100644 index 0000000..6e0ce78 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/ResourceUtils.java @@ -0,0 +1,82 @@ +package com.android.base.utils.android; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.support.annotation.ArrayRes; +import android.support.annotation.AttrRes; +import android.support.annotation.NonNull; +import android.support.annotation.StringRes; + +import com.android.base.utils.BaseUtils; + + +public class ResourceUtils { + + private ResourceUtils() { + throw new UnsupportedOperationException("no need instantiation"); + } + + /** + * @param name 资源的名称,如 ic_launcher 或者 com.example.android/drawable/ic_launcher(这是,下面两个参数可以省略) + * @param defType 资源的类型,如 drawable + * @param defPackage 包名 + * @return 资源id + */ + public static int getResource(String name, String defType, String defPackage) { + return BaseUtils.getResources().getIdentifier(name, defType, defPackage); + } + + public static CharSequence getText(@StringRes int id) { + return BaseUtils.getResources().getText(id); + } + + public static String getString(@StringRes int id) { + return BaseUtils.getResources().getString(id); + } + + public static String getString(@StringRes int id, Object... formatArgs) { + return BaseUtils.getResources().getString(id, formatArgs); + } + + public static String[] getStringArray(@ArrayRes int id) { + return BaseUtils.getResources().getStringArray(id); + } + + + public static int[] getIntArray(@ArrayRes int id) { + return BaseUtils.getResources().getIntArray(id); + } + + + public static Uri createUriByResource(int id) { + return Uri.parse("android.resource://" + BaseUtils.getAppContext().getPackageName() + "/" + id); + } + + public static Uri createUriByAssets(String path) { + return Uri.parse("file:///android_asset/" + path); + } + + public static int getStyledColor(@NonNull Context context, @AttrRes int attr) { + TypedArray a = context.obtainStyledAttributes(null, new int[]{attr}); + try { + return a.getColor(0, 0x000000); + } finally { + a.recycle(); + } + } + + public static Drawable getStyledDrawable(@NonNull Context context, @AttrRes int attr) { + TypedArray a = context.obtainStyledAttributes(null, new int[]{attr}); + try { + return a.getDrawable(0); + } finally { + a.recycle(); + } + } + + public static int getDimensPixelSize(int dimenId) { + return BaseUtils.getResources().getDimensionPixelSize(dimenId); + } +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/SoftKeyboardUtils.java b/lib_base/src/main/java/com/android/base/utils/android/SoftKeyboardUtils.java new file mode 100644 index 0000000..af6bdf0 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/SoftKeyboardUtils.java @@ -0,0 +1,85 @@ +package com.android.base.utils.android; + +import android.app.Activity; +import android.content.Context; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.TextView; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class SoftKeyboardUtils { + + private SoftKeyboardUtils() { + throw new UnsupportedOperationException("no need instantiation"); + } + + public static void toggleSoftInput(Context context) { + InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS); + } + + public static boolean showSoftInput(View view) { + InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + return imm.showSoftInput(view, InputMethodManager.SHOW_FORCED); + } + + public static boolean showSoftInput(Activity activity) { + View view = activity.getCurrentFocus(); + if (view != null) { + InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService( + Context.INPUT_METHOD_SERVICE); + return imm.showSoftInput(view, InputMethodManager.SHOW_FORCED); + } + return false; + } + + public static boolean hideSoftInput(View view) { + InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + return imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + + public static boolean hideSoftInput(Activity activity) { + if (activity.getCurrentFocus() != null) { + InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); + return imm.hideSoftInputFromWindow(activity.getCurrentFocus().getWindowToken(), 0); + } + return false; + } + + public static boolean isActive(Context context) { + InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + return imm.isActive(); + } + + @SuppressWarnings("all") + public static void showErrorImmediately(String error, TextView editText) { + editText.setError(error); + try { + Field mEditor = TextView.class.getDeclaredField("mEditor"); + if (mEditor != null) { + mEditor.setAccessible(true); + Object editor = mEditor.get(editText); + if (editor != null) { + Method showError = editor.getClass().getDeclaredMethod("showError"); + if (showError != null) { + showError.setAccessible(true); + showError.invoke(editor); + } + } + } + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + + +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/SpCache.java b/lib_base/src/main/java/com/android/base/utils/android/SpCache.java new file mode 100644 index 0000000..5219e5a --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/SpCache.java @@ -0,0 +1,209 @@ +package com.android.base.utils.android; + +import android.content.Context; +import android.content.SharedPreferences; +import android.text.TextUtils; +import android.util.Log; + +import com.android.base.utils.BaseUtils; + +import java.lang.reflect.Method; + +import timber.log.Timber; + +/** + * modified from hongyangAndroid/SpCache + */ +public class SpCache { + + private static final String TAG = SpCache.class.getSimpleName(); + + private final SharedPreferences mSharedPreferences; + private final boolean mUseApply; + + public SpCache(String prefFileName) { + this(BaseUtils.getAppContext(), prefFileName, true); + } + + public SpCache(String prefFileName, boolean useApply) { + this(BaseUtils.getAppContext(), prefFileName, useApply); + } + + public SpCache(Context context, String prefFileName, boolean useApply) { + if (TextUtils.isEmpty(prefFileName)) { + throw new NullPointerException("SpCache get fileName = null"); + } + mSharedPreferences = context.getSharedPreferences(prefFileName, Context.MODE_PRIVATE); + mUseApply = useApply; + } + + //put + public SpCache putInt(String key, int val) { + return put(key, val); + } + + public SpCache putLong(String key, long val) { + return put(key, val); + } + + public SpCache putString(String key, String val) { + return put(key, val); + } + + public SpCache putBoolean(String key, boolean val) { + return put(key, val); + } + + public SpCache putFloat(String key, float val) { + return put(key, val); + } + + //get + public int getInt(String key, int defaultVal) { + return (int) (get(key, defaultVal)); + } + + public long getLong(String key, long defaultVal) { + return (long) (get(key, defaultVal)); + } + + public String getString(String key, String defaultVal) { + return (String) (get(key, defaultVal)); + } + + public boolean getBoolean(String key, boolean defaultVal) { + return (boolean) (get(key, defaultVal)); + } + + public float getFloat(String key, float defaultVal) { + return (float) (get(key, defaultVal)); + } + + //contains + public boolean contains(String key) { + return getSharedPreferences().contains(key); + } + + //remove + public SpCache remove(String key) { + return _remove(key); + } + + private SpCache _remove(String key) { + SharedPreferences.Editor editor = getSharedPreferences().edit(); + editor.remove(key); + SharedPreferencesCompat.apply(editor, mUseApply); + return this; + } + + //clear + public SpCache clear() { + return _clear(); + } + + private SpCache _clear() { + SharedPreferences.Editor editor = getSharedPreferences().edit(); + editor.clear(); + SharedPreferencesCompat.apply(editor, mUseApply); + return this; + } + + private SpCache put(String key, T t) { + SharedPreferences.Editor editor = getSharedPreferences().edit(); + if (t instanceof String) { + editor.putString(key, (String) t); + } else if (t instanceof Integer) { + editor.putInt(key, (Integer) t); + } else if (t instanceof Boolean) { + editor.putBoolean(key, (Boolean) t); + } else if (t instanceof Float) { + editor.putFloat(key, (Float) t); + } else if (t instanceof Long) { + editor.putLong(key, (Long) t); + } else { + Timber.d("you may be put a invalid object :" + t); + editor.putString(key, t.toString()); + } + SharedPreferencesCompat.apply(editor, mUseApply); + return this; + } + + + private Object readDisk(String key, Object defaultObject) { + Log.e("TAG", "readDisk"); + SharedPreferences sp = getSharedPreferences(); + + if (defaultObject instanceof String) { + return sp.getString(key, (String) defaultObject); + } else if (defaultObject instanceof Integer) { + return sp.getInt(key, (Integer) defaultObject); + } else if (defaultObject instanceof Boolean) { + return sp.getBoolean(key, (Boolean) defaultObject); + } else if (defaultObject instanceof Float) { + return sp.getFloat(key, (Float) defaultObject); + } else if (defaultObject instanceof Long) { + return sp.getLong(key, (Long) defaultObject); + } + Log.e(TAG, "you can not read object , which class is " + defaultObject.getClass().getSimpleName()); + return null; + + } + + private Object get(String key, Object defaultVal) { + return readDisk(key, defaultVal); + } + + /** + * 创建一个解决SharedPreferencesCompat.apply方法的一个兼容类 + * + * @author zhy + */ + private static class SharedPreferencesCompat { + private static final Method sApplyMethod = findApplyMethod(); + + /** + * 反射查找apply的方法 + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + private static Method findApplyMethod() { + try { + Class clz = SharedPreferences.Editor.class; + return clz.getMethod("apply"); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } + return null; + } + + /** + * 如果找到则使用apply执行,否则使用commit + */ + public static void apply(final SharedPreferences.Editor editor, boolean useApply) { + if (useApply) { + try { + if (sApplyMethod != null) { + sApplyMethod.invoke(editor); + } + } catch (Exception e) { + e.printStackTrace(); + editor.commit(); + } + } else { + editor.commit(); + } + } + } + + private SharedPreferences getSharedPreferences() { + return mSharedPreferences; + } + + public void registerOnSharedPreferenceChangeListener(SharedPreferences.OnSharedPreferenceChangeListener listener) { + getSharedPreferences().registerOnSharedPreferenceChangeListener(listener); + } + + public void unregisterOnSharedPreferenceChangeListener(SharedPreferences.OnSharedPreferenceChangeListener listener) { + getSharedPreferences().unregisterOnSharedPreferenceChangeListener(listener); + } + +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/TintUtils.java b/lib_base/src/main/java/com/android/base/utils/android/TintUtils.java new file mode 100644 index 0000000..75e1c89 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/TintUtils.java @@ -0,0 +1,74 @@ +package com.android.base.utils.android; + +import android.content.res.ColorStateList; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.drawable.Drawable; +import android.support.v4.graphics.drawable.DrawableCompat; + +/** + * usage: + *
+ *     {@code
+ *
+ *              int[][] states = new int[][]{
+ *                  new int[]{android.R.attr.state_selected}, // pressed
+ *                  new int[]{-android.R.attr.state_selected}  // unpressed
+ *             };
+ *
+ *              int[] colors = new int[]{
+ *                  Color.BLACK,
+ *                  Color.GRAY
+ *              };
+ *
+ *              ColorStateList colorStateList = new ColorStateList(states, colors);
+ *              Drawable aaa = ContextCompat.getDrawable(getContext(), R.drawable.aaa);
+ *              Drawable bbb = ContextCompat.getDrawable(getContext(), R.drawable.bbb);
+ *              Drawable aaaTint = TintUtils.tint(aaa, colorStateList);
+ *              Drawable bbbTint = TintUtils.tint(bbb, colorStateList);
+ *              aaaTv.setCompoundDrawablesWithIntrinsicBounds(null, aaaTint, null, null);
+ *              bbbTv.setCompoundDrawablesWithIntrinsicBounds(null, bbbTint, null, null);
+ * 
+ */ +public class TintUtils { + + private TintUtils() { + } + + public static Drawable tint(Drawable originDrawable, int color) { + return tint(originDrawable, ColorStateList.valueOf(color)); + } + + public static Drawable tint(Drawable originDrawable, int color, PorterDuff.Mode tintMode) { + return tint(originDrawable, ColorStateList.valueOf(color), tintMode); + } + + public static Drawable tint(Drawable originDrawable, ColorStateList colorStateList) { + return tint(originDrawable, colorStateList, null); + } + + public static Drawable tint(Drawable originDrawable, ColorStateList colorStateList, PorterDuff.Mode tintMode) { + Drawable tintDrawable = DrawableCompat.wrap(originDrawable); + if (tintMode != null) { + DrawableCompat.setTintMode(tintDrawable, tintMode); + } + DrawableCompat.setTintList(tintDrawable, colorStateList); + return tintDrawable; + } + + public static Bitmap tintBitmap(Bitmap inBitmap, int tintColor) { + if (inBitmap == null) { + return null; + } + Bitmap outBitmap = Bitmap.createBitmap(inBitmap.getWidth(), inBitmap.getHeight(), inBitmap.getConfig()); + Canvas canvas = new Canvas(outBitmap); + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setColorFilter(new PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_IN)); + canvas.drawBitmap(inBitmap, 0, 0, paint); + return outBitmap; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/utils/android/UnitConverter.java b/lib_base/src/main/java/com/android/base/utils/android/UnitConverter.java new file mode 100644 index 0000000..1d53afd --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/UnitConverter.java @@ -0,0 +1,72 @@ +package com.android.base.utils.android; + + +import android.util.DisplayMetrics; +import android.util.TypedValue; + +import com.android.base.utils.BaseUtils; + +public class UnitConverter { + + private UnitConverter() { + throw new UnsupportedOperationException("no need instantiation"); + } + + public static float dpToPx(float dp) { + return dp * BaseUtils.getDisplayMetrics().density; + } + + public static int dpToPx(int dp) { + return (int) (dp * BaseUtils.getDisplayMetrics().density + 0.5f); + } + + public static float pxToDp(float px) { + return px / BaseUtils.getDisplayMetrics().density; + } + + public static int pxToDp(int px) { + return (int) (px / BaseUtils.getDisplayMetrics().density + 0.5f); + } + + public static float spToPx(float sp) { + return sp * BaseUtils.getDisplayMetrics().scaledDensity; + } + + public static int spToPx(int sp) { + return (int) (sp * BaseUtils.getDisplayMetrics().scaledDensity + 0.5f); + } + + public static float pxToSp(float px) { + return px / BaseUtils.getDisplayMetrics().scaledDensity; + } + + public static int pxToSp(int px) { + return (int) (px / BaseUtils.getDisplayMetrics().scaledDensity + 0.5f); + } + + /** + * 各种单位转换,该方法存在于{@link TypedValue} 中 + * + * @param unit 单位 + * @param value 值 + * @return 转换结果 + */ + public static float applyDimension(int unit, float value) { + DisplayMetrics metrics = BaseUtils.getDisplayMetrics(); + switch (unit) { + case TypedValue.COMPLEX_UNIT_PX: + return value; + case TypedValue.COMPLEX_UNIT_DIP: + return value * metrics.density; + case TypedValue.COMPLEX_UNIT_SP: + return value * metrics.scaledDensity; + case TypedValue.COMPLEX_UNIT_PT: + return value * metrics.xdpi * (1.0f / 72); + case TypedValue.COMPLEX_UNIT_IN: + return value * metrics.xdpi; + case TypedValue.COMPLEX_UNIT_MM: + return value * metrics.xdpi * (1.0f / 25.4f); + } + return 0; + } +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/utils/android/VideoUtils.java b/lib_base/src/main/java/com/android/base/utils/android/VideoUtils.java new file mode 100644 index 0000000..acac226 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/VideoUtils.java @@ -0,0 +1,24 @@ +package com.android.base.utils.android; + +import android.content.Context; +import android.graphics.Bitmap; +import android.media.MediaMetadataRetriever; +import android.net.Uri; +import android.support.annotation.WorkerThread; + +public class VideoUtils { + + private VideoUtils() { + } + + @WorkerThread + public static Bitmap createVideoThumbnail(final Context context, final Uri uri) { + Bitmap bitmap; + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + retriever.setDataSource(context, uri); + bitmap = retriever.getFrameAtTime(); + retriever.release(); + return bitmap; + } + +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/ViewUtils.java b/lib_base/src/main/java/com/android/base/utils/android/ViewUtils.java new file mode 100644 index 0000000..c5d15e4 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/ViewUtils.java @@ -0,0 +1,207 @@ +package com.android.base.utils.android; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Picture; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.support.annotation.IdRes; +import android.support.annotation.Nullable; +import android.support.v4.app.FragmentActivity; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.WebView; +import android.widget.TextView; + +public class ViewUtils { + + private ViewUtils() { + } + + private static final int ACTION_VISIBLE = 0x01; + private static final int ACTION_GONE = 0x02; + private static final int ACTION_INVISIBLE = 0x03; + private static final int ACTION_DISABLE = 0x04; + private static final int ACTION_ENABLE = 0x05; + + public static boolean measureWithMaxSize(View view) { + ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); + if (layoutParams == null || (layoutParams.width == ViewGroup.LayoutParams.MATCH_PARENT && layoutParams.height == ViewGroup.LayoutParams.MATCH_PARENT)) { + return false; + } + int size = 1 << 30 - 1;//即后30位 + int measureSpec = View.MeasureSpec.makeMeasureSpec(size, View.MeasureSpec.AT_MOST); + view.measure(measureSpec, measureSpec); + return true; + } + + public static boolean measureWithScreenSize(View view) { + ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); + if (layoutParams == null || (layoutParams.width == ViewGroup.LayoutParams.MATCH_PARENT && layoutParams.height == ViewGroup.LayoutParams.MATCH_PARENT)) { + return false; + } + view.measure( + View.MeasureSpec.makeMeasureSpec(WindowUtils.getScreenWidth(), View.MeasureSpec.AT_MOST), + View.MeasureSpec.makeMeasureSpec(WindowUtils.getScreenHeight(), View.MeasureSpec.AT_MOST)); + return true; + } + + public static boolean measureWithSize(View view, int width, int height) { + ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); + if (layoutParams == null || (layoutParams.width == ViewGroup.LayoutParams.MATCH_PARENT && layoutParams.height == ViewGroup.LayoutParams.MATCH_PARENT)) { + return false; + } + view.measure( + View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.AT_MOST), + View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.AT_MOST)); + return true; + } + + + public static void disable(View view1, View view2) { + view1.setEnabled(false); + view2.setEnabled(false); + } + + public static void disable(View view1, View view2, View view3) { + view1.setEnabled(false); + view2.setEnabled(false); + view3.setEnabled(false); + } + + public static void disable(View view1, View view2, View view3, View... views) { + view1.setEnabled(false); + view2.setEnabled(false); + view3.setEnabled(false); + doAction(ACTION_DISABLE, views); + } + + public static void enable(View view1, View view2) { + view1.setEnabled(true); + view2.setEnabled(true); + } + + public static void enable(View view1, View view2, View view3) { + view1.setEnabled(true); + view2.setEnabled(true); + view3.setEnabled(true); + } + + public static void enable(View view1, View view2, View view3, View... views) { + view1.setEnabled(true); + view2.setEnabled(true); + view3.setEnabled(true); + doAction(ACTION_ENABLE, views); + } + + + public static void gone(View view1, View view2) { + view1.setVisibility(View.GONE); + view2.setVisibility(View.GONE); + } + + public static void gone(View view1, View view2, View view3) { + view1.setVisibility(View.GONE); + view2.setVisibility(View.GONE); + view3.setVisibility(View.GONE); + } + + public static void gone(View view1, View view2, View view3, View... views) { + view1.setVisibility(View.GONE); + view2.setVisibility(View.GONE); + view3.setVisibility(View.GONE); + doAction(ACTION_GONE, views); + } + + public static void visible(View view1, View view2) { + view1.setVisibility(View.VISIBLE); + view2.setVisibility(View.VISIBLE); + } + + public static void visible(View view1, View view2, View view3) { + view1.setVisibility(View.VISIBLE); + view2.setVisibility(View.VISIBLE); + view3.setVisibility(View.VISIBLE); + } + + public static void visible(View view1, View view2, View view3, View... views) { + view1.setVisibility(View.VISIBLE); + view2.setVisibility(View.VISIBLE); + view3.setVisibility(View.VISIBLE); + doAction(ACTION_VISIBLE, views); + } + + public static void invisible(View view1, View view2) { + view1.setVisibility(View.INVISIBLE); + view2.setVisibility(View.INVISIBLE); + } + + public static void invisible(View view1, View view2, View view3) { + view1.setVisibility(View.INVISIBLE); + view2.setVisibility(View.INVISIBLE); + view3.setVisibility(View.INVISIBLE); + } + + public static void invisible(View view1, View view2, View view3, View... views) { + view1.setVisibility(View.INVISIBLE); + view2.setVisibility(View.INVISIBLE); + view3.setVisibility(View.INVISIBLE); + doAction(ACTION_INVISIBLE, views); + } + + private static void doAction(int action, View... views) { + for (View view : views) { + if (action == ACTION_GONE) { + view.setVisibility(View.GONE); + } else if (action == ACTION_INVISIBLE) { + view.setVisibility(View.INVISIBLE); + } else if (action == ACTION_VISIBLE) { + view.setVisibility(View.VISIBLE); + } else if (action == ACTION_ENABLE) { + view.setEnabled(true); + } else if (action == ACTION_DISABLE) { + view.setEnabled(false); + } + } + } + + public static void setBackgroundDrawable(View view, Drawable drawable) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + view.setBackground(drawable); + } else { + view.setBackgroundDrawable(drawable); + } + } + + public static V find(View view, @IdRes int viewId) { + @SuppressWarnings("unchecked") + V v = (V) view.findViewById(viewId); + return v; + } + + public static Bitmap captureBitmapFromWebView(WebView webView) { + Picture snapShot = webView.capturePicture(); + Bitmap bmp = Bitmap.createBitmap(snapShot.getWidth(), snapShot.getHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bmp); + snapShot.draw(canvas); + return bmp; + } + + public static void clearTextDrawable(TextView textView) { + textView.setCompoundDrawables(null, null, null, null); + } + + @Nullable + public static FragmentActivity getRealContext(View view) { + Context context = view.getContext(); + while (context instanceof android.content.ContextWrapper) { + if (context instanceof FragmentActivity) { + return (FragmentActivity) context; + } + context = ((android.content.ContextWrapper) context).getBaseContext(); + } + return null; + } + +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/WebViewUtils.java b/lib_base/src/main/java/com/android/base/utils/android/WebViewUtils.java new file mode 100644 index 0000000..984d5d9 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/WebViewUtils.java @@ -0,0 +1,123 @@ +package com.android.base.utils.android; + +import android.content.Context; +import android.os.Build; +import android.support.annotation.RequiresApi; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.CookieManager; +import android.webkit.CookieSyncManager; +import android.webkit.WebView; + +/** + *
+ * 关于内存泄漏:最好的方式还是开启独立的进程
+ *
+ * 关于Cookie:
+ *
+ *          之前同步 cookie 需要用到 CookieSyncManager 类,现在这个类已经被抛弃了。
+ *          现在WebView已经可以在需要的时候自动同步 cookie 了,
+ *          所以不再需要创建 CookieSyncManager 类的对象来进行强制性的同步 cookie 了。
+ *          现在只需要获得 CookieManager 的对象将 cookie 设置进去就可以了。
+ *
+ *          从服务器的返回头中取出 cookie 根据Http请求的客户端不同,获取 cookie 的方式也不同。比如以HttpURLCollection
+ *
+ *          
+ *               String cookieStr = conn.getHeaderField("Set-Cookie");
+ *          
+ *
+ *          步骤:
+ *                   1:利用HttpClient进行api请求登录,然后获取cookie
+ *                   2:调用syncCookie分发把cookie写入到WebCookie的数据库中
+ * 
+ */ +public class WebViewUtils { + + private static final String TAG = WebViewUtils.class.getSimpleName(); + + private WebViewUtils() { + throw new UnsupportedOperationException(); + } + + public static void destroy(WebView webView) { + try { + if (webView == null) { + return; + } + webView.setWebChromeClient(null); + webView.setWebViewClient(null); + webView.onPause(); + if (webView.getParent() != null) { + ((ViewGroup) webView.getParent()).removeAllViews(); + } + webView.setVisibility(View.GONE);//解决崩溃问题 Receiver not Register + webView.removeAllViews(); + if (webView.getHandler() != null) { + webView.getHandler().removeCallbacks(null); + } + webView.pauseTimers(); + webView.destroy(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 将cookie设置到WebView, 客户端通过以下代码设置cookie,如果两次设置相同,会覆盖上一次的 + * + * @param url 要加载的 url + * @param cookie 要同步的 cookie + */ + public static void syncCookie(String url, String cookie, Context context) { + /* + 注意: + 1,同步 cookie 要在 WebView 加载 url 之前,否则 WebView 无法获得相应的 cookie,也就无法通过验证。 + 2,cookie应该被及时更新,否则很可能导致WebView拿的是旧的session id和服务器进行通信。 + 3,CookieManager会将这个Cookie存入该应用程序data/data/package_name/app_WebView/Cookies.db + */ + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + CookieSyncManager.createInstance(context); + } + CookieManager cookieManager = CookieManager.getInstance(); + cookieManager.setAcceptCookie(true); + cookieManager.setCookie(url, cookie);//如果没有特殊需求,这里只需要将session id以"key=value"形式作为cookie即可 + CookieSyncManager.getInstance().sync(); + } + + /** + * 获取指定 url 的cookie + *
+     * 打开网页,WebView从数据库中读取该cookie值,放到http请求的头部,传递到服务器
+     * 
+ */ + public static String getCookie(String url) { + CookieManager cookieManager = CookieManager.getInstance(); + return cookieManager.getCookie(url); + } + + public static void clearCookie(Context context) { + // 这个两个在 API level 21 被抛弃 + CookieSyncManager.createInstance(context); + CookieManager.getInstance().removeSessionCookie(); + CookieManager.getInstance().removeAllCookie(); + CookieSyncManager.getInstance().sync(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + clearCookie21(); + } + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + private static void clearCookie21() { + // 推荐使用这两个, level 21 新加的 + // 移除所有过期 cookie + CookieManager.getInstance().removeSessionCookies(aBoolean -> { + Log.d(TAG, "clearCookie21 removeSessionCookies:" + aBoolean); + }); + // 移除所有的 cookie + CookieManager.getInstance().removeAllCookies(aBoolean -> { + Log.d(TAG, "clearCookie21 removeAllCookies:" + aBoolean); + }); + } + +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/WindowUtils.java b/lib_base/src/main/java/com/android/base/utils/android/WindowUtils.java new file mode 100644 index 0000000..48f182f --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/WindowUtils.java @@ -0,0 +1,157 @@ +package com.android.base.utils.android; + +import android.app.Activity; +import android.app.KeyguardManager; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.util.DisplayMetrics; +import android.view.Surface; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; + +import com.android.base.utils.BaseUtils; +import com.android.base.utils.android.compat.SystemBarCompat; + +/** + * 窗口工具箱 + */ +public final class WindowUtils { + + /** + * Don't let anyone instantiate this class. + */ + private WindowUtils() { + throw new Error("Do not need instantiate!"); + } + + /** + * 获取当前窗口的旋转角度 + */ + public static int getDisplayRotation(Activity activity) { + switch (activity.getWindowManager().getDefaultDisplay().getRotation()) { + case Surface.ROTATION_0: + return 0; + case Surface.ROTATION_90: + return 90; + case Surface.ROTATION_180: + return 180; + case Surface.ROTATION_270: + return 270; + default: + return 0; + } + } + + /** + * 当前是否是横屏 + */ + public static boolean isLandscape(Context context) { + return context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; + } + + /** + * 当前是否是竖屏 + */ + public static boolean isPortrait(Context context) { + return context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT; + } + + public static int getScreenWidth() { + WindowManager wm = (WindowManager) BaseUtils.getAppContext().getSystemService(Context.WINDOW_SERVICE); + DisplayMetrics outMetrics = new DisplayMetrics(); + if (wm != null) { + wm.getDefaultDisplay().getMetrics(outMetrics); + } + return outMetrics.widthPixels; + } + + public static int getScreenHeight() { + WindowManager wm = (WindowManager) BaseUtils.getAppContext().getSystemService(Context.WINDOW_SERVICE); + DisplayMetrics outMetrics = new DisplayMetrics(); + if (wm != null) { + wm.getDefaultDisplay().getMetrics(outMetrics); + } + return outMetrics.heightPixels; + } + + /** + * FullScreen: + *
+     *          4.0 以及之前设置全屏:
+     *              1:使用全屏的主题
+     *              2:getWindow().addFlag(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+     * 
+ */ + public static void setFullScreen(AppCompatActivity appCompatActivity, boolean fullScreen) { + Window win = appCompatActivity.getWindow(); + WindowManager.LayoutParams winParams = win.getAttributes(); + final int bits = WindowManager.LayoutParams.FLAG_FULLSCREEN; + if (fullScreen) { + winParams.flags |= bits; + } else { + winParams.flags &= ~bits; + } + win.setAttributes(winParams); + ActionBar supportActionBar = appCompatActivity.getSupportActionBar(); + if (supportActionBar != null) { + if (fullScreen) { + supportActionBar.hide(); + } else { + supportActionBar.show(); + } + } + } + + /** + * 获取当前屏幕截图,包含状态栏 + * + * @param activity activity + * @return Bitmap + */ + public static Bitmap captureWithStatusBar(Activity activity) { + View view = activity.getWindow().getDecorView(); + view.setDrawingCacheEnabled(true); + view.buildDrawingCache(); + Bitmap bmp = view.getDrawingCache(); + DisplayMetrics dm = new DisplayMetrics(); + activity.getWindowManager().getDefaultDisplay().getMetrics(dm); + Bitmap ret = Bitmap.createBitmap(bmp, 0, 0, dm.widthPixels, dm.heightPixels); + view.destroyDrawingCache(); + return ret; + } + + /** + * 获取当前屏幕截图,不包含状态栏 + * + * @param activity activity + * @return Bitmap + */ + public static Bitmap captureWithoutStatusBar(Activity activity) { + View view = activity.getWindow().getDecorView(); + view.setDrawingCacheEnabled(true); + view.buildDrawingCache(); + Bitmap bmp = view.getDrawingCache(); + int statusBarHeight = SystemBarCompat.getStatusBarHeight(activity); + DisplayMetrics dm = new DisplayMetrics(); + activity.getWindowManager().getDefaultDisplay().getMetrics(dm); + Bitmap ret = Bitmap.createBitmap(bmp, 0, statusBarHeight, dm.widthPixels, dm.heightPixels - statusBarHeight); + view.destroyDrawingCache(); + return ret; + } + + /** + * 判断是否锁屏 + * + * @return {@code true}: 是
{@code false}: 否 + */ + public static boolean isScreenLock() { + KeyguardManager km = (KeyguardManager) BaseUtils.getAppContext().getSystemService(Context.KEYGUARD_SERVICE); + return km != null && km.inKeyguardRestrictedInputMode(); + } + + +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/XAppUtils.java b/lib_base/src/main/java/com/android/base/utils/android/XAppUtils.java new file mode 100644 index 0000000..bd3cd0b --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/XAppUtils.java @@ -0,0 +1,129 @@ +package com.android.base.utils.android; + +import android.app.ActivityManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.RequiresApi; +import android.support.v4.content.FileProvider; +import android.webkit.MimeTypeMap; + +import com.android.base.utils.common.FileUtils; +import com.android.base.utils.common.StringChecker; + +import java.io.File; +import java.util.List; + +import timber.log.Timber; + +/** + * 对 {@link com.blankj.utilcode.util.AppUtils} 的补充 + */ +public class XAppUtils { + + private XAppUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * 安装App,支持 Android 6.0 FileProvider。 + * + * @param context caller + * @param file 文件 + * @param authority FileProvider authorities, default is {@code PackageName + ".fileProvider"} + */ + public static boolean installApp(Context context, File file, @Nullable String authority) { + if (file == null || !file.exists()) { + return false; + } + try { + if (Build.VERSION.SDK_INT <= 23) { + context.startActivity(getInstallAppIntent23(file)); + } else { + Intent intent = getInstallAppIntent24(context, file, StringChecker.isEmpty(authority) ? (context.getPackageName() + ".fileProvider") : authority); + context.startActivity(intent); + } + Timber.d("installApp open activity successfully"); + return true; + } catch (Exception e) { + Timber.e(e, "installApp"); + } + return false; + } + + /** + * 获取安装App(支持7.0)的意图 + * + * @param file 文件 + * @return intent + */ + @NonNull + @RequiresApi(24) + private static Intent getInstallAppIntent24(Context context, File file, String authority) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + Uri contentUri = FileProvider.getUriForFile(context, authority, file); + intent.setDataAndType(contentUri, MimeTypeMap.getSingleton().getMimeTypeFromExtension(FileUtils.getFileExtension(file))); + + // 然后全部授权 + List resolveLists = context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); + for (ResolveInfo resolveInfo : resolveLists) { + String packageName = resolveInfo.activityInfo.packageName; + context.grantUriPermission(packageName, contentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + } + + return intent; + } + + /** + * 获取安装App(支持6.0)的意图 + * + * @param file 文件 + * @return intent + */ + private static Intent getInstallAppIntent23(File file) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + String type; + if (Build.VERSION.SDK_INT < 23) { + type = "application/vnd.android.package-archive"; + } else { + type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(FileUtils.getFileExtension(file)); + } + intent.setDataAndType(Uri.fromFile(file), type); + return intent; + } + + /** + * 用来判断服务是否运行 + * + * @param context 上下文 + * @param serviceName 判断的服务名字 + * @return true 在运行 false 不在运行 + */ + public static boolean isServiceRunning(Context context, String serviceName) { + boolean isRunning = false; + ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + if (activityManager == null) { + return false; + } + List serviceList = activityManager.getRunningServices(100); + if (serviceList.isEmpty()) { + return false; + } + for (int i = 0; i < serviceList.size(); i++) { + if (serviceName.equals(serviceList.get(i).service.getClassName())) { + isRunning = true; + break; + } + } + return isRunning; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/utils/android/XIntentUtils.java b/lib_base/src/main/java/com/android/base/utils/android/XIntentUtils.java new file mode 100644 index 0000000..95c1186 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/XIntentUtils.java @@ -0,0 +1,150 @@ +package com.android.base.utils.android; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.provider.CalendarContract; +import android.provider.Settings; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.util.Log; + +import java.util.Arrays; +import java.util.Calendar; +import java.util.List; + +import static android.content.ContentValues.TAG; + +/** + * 对 {@link com.blankj.utilcode.util.IntentUtils} 的补充 + */ +public class XIntentUtils { + + public static final String GOOGLE_PLAY_PACKAGE_NAME = "com.android.vending"; + public static final String WANDOUJIA_PACKAGE_NAME = "com.wandoujia.phoenix2"; + public static final String TENCENT_PACKAGE_NAME = "com.tencent.android.qqdownloader"; + + /** + * 打开应用市场 + * + * @param context 上下文 + * @return 是否成功 + */ + public static boolean openMarket(Context context) { + Intent intent = new Intent(Intent.ACTION_VIEW); + //跳转到应用市场,非GooglePlay市场一般情况也实现了这个接口 + intent.setData(Uri.parse("market://details?id=" + context.getPackageName())); + //存在手机里没安装应用市场的情况,跳转会包异常,做一个接收判断 + if (intent.resolveActivity(context.getPackageManager()) != null) { //可以接收 + context.startActivity(intent); + return true; + } else { + return false; + } + } + + /** + * 优先打开规定的应用市场 + * + * @param context 上下文 + * @param specificMarkets 你规定的能打开app的包名,优先打开规定的包名 + * @return true表示打开成功 + */ + public static boolean openSpecificMarket(Context context, String... specificMarkets) { + String appId = context.getPackageName(); + Intent rateIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + appId)); + boolean marketFound = false; + // find all applications able to handle our rateIntent + final List otherApps = context.getPackageManager().queryIntentActivities(rateIntent, 0); + List specifiedPackage = Arrays.asList(specificMarkets); + for (ResolveInfo otherApp : otherApps) { + // look for Google Play application + if (specifiedPackage.contains(otherApp.activityInfo.applicationInfo.packageName)) { + ActivityInfo otherAppActivity = otherApp.activityInfo; + ComponentName componentName = new ComponentName(otherAppActivity.applicationInfo.packageName, otherAppActivity.name); + // make sure it does NOT open in the stack of your activity + rateIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // task reparenting if needed + rateIntent.addFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); + // if the Google Play was already open in a search result + // this make sure it still go to the app page you requested + rateIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + // this make sure only the Google Play app is allowed to + // intercept the intent + rateIntent.setComponent(componentName); + context.startActivity(rateIntent); + marketFound = true; + break; + } + } + return marketFound || openMarket(context); + } + + /** + * @param email email email必须放到数组中 + * @param subject 主题 + * @param text 发送的内容 + */ + public static void sendEmail(Context context, String[] email, String subject, String text) { + Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND); + emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + emailIntent.setType("toastMessage/rfc822"); + emailIntent.putExtra(android.content.Intent.EXTRA_EMAIL, email); + emailIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, subject); + emailIntent.putExtra(android.content.Intent.EXTRA_TEXT, text); + if (emailIntent.resolveActivity(context.getPackageManager()) != null) { + context.startActivity(emailIntent); + } + } + + /** + * 参考:https://developer.android.com/guide/topics/providers/calendar-provider.html?hl=zh-cn#intent-insert + * + * @param context 上下文 + * @param beginTime 开始时间 + * @param endTime 结束时间 + * @param title 标题 + * @param description 描述 + * @param eventLocation 位置 + * @param emails extra 字段提供以逗号分隔的受邀者电子邮件地址列表。 + * @return 是否添加成功 + */ + public static boolean insertEvent(Context context, Calendar beginTime, Calendar endTime, String title, String description, String eventLocation, @Nullable String emails) { + emails = TextUtils.isEmpty(emails) ? "" : emails; + Intent intent = new Intent(Intent.ACTION_INSERT) + .setData(CalendarContract.Events.CONTENT_URI) + .putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, beginTime.getTimeInMillis()) + .putExtra(CalendarContract.EXTRA_EVENT_END_TIME, endTime.getTimeInMillis()) + .putExtra(CalendarContract.Events.TITLE, title) + .putExtra(CalendarContract.Events.DESCRIPTION, description) + .putExtra(CalendarContract.Events.EVENT_LOCATION, eventLocation) + .putExtra(CalendarContract.Events.AVAILABILITY, CalendarContract.Events.AVAILABILITY_BUSY) + .putExtra(Intent.EXTRA_EMAIL, emails); + ComponentName componentName = intent.resolveActivity(context.getPackageManager()); + if (componentName != null) { + context.startActivity(intent); + return true; + } else { + Log.d(TAG, "insertEvent() fail"); + return false; + } + } + + /** + * 打开网络设置界面 + */ + public static boolean networkSettings(Context context) { + try { + Intent intent = new Intent(Settings.ACTION_WIRELESS_SETTINGS); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + return true; + } catch (Exception ignore) { + return false; + } + } + +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/anim/ActivityAnimUtils.java b/lib_base/src/main/java/com/android/base/utils/android/anim/ActivityAnimUtils.java new file mode 100644 index 0000000..f5fc128 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/anim/ActivityAnimUtils.java @@ -0,0 +1,72 @@ +package com.android.base.utils.android.anim; + +import android.app.Activity; +import android.app.ActivityOptions; +import android.content.Intent; +import android.support.annotation.AnimRes; +import android.support.v4.app.ActivityCompat; +import android.support.v4.app.ActivityOptionsCompat; +import android.support.v7.app.AppCompatActivity; +import android.transition.Explode; +import android.transition.Fade; +import android.transition.Slide; +import android.view.View; +import android.view.Window; + +import com.android.base.utils.android.compat.AndroidVersion; + +public class ActivityAnimUtils { + + private ActivityAnimUtils() { + throw new UnsupportedOperationException("no need instantiation"); + } + + public static void startActivity(Activity activity, Intent intent, View share) { + if (AndroidVersion.atLeast(21)) { + ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(activity, share, share.getTransitionName()); + activity.startActivity(intent, options.toBundle()); + } else {//小于5.0,使用makeScaleUpAnimation + ActivityOptionsCompat options = ActivityOptionsCompat.makeScaleUpAnimation(share, share.getWidth() / 2, share.getHeight() / 2, 0, 0); + ActivityCompat.startActivity(activity, intent, options.toBundle()); + } + } + + /////////////////////////////////////////////////////////////////////////// + // Enter And Exit Transition + /////////////////////////////////////////////////////////////////////////// + + public static final int TYPE_EXPLODE = 1; + public static final int TYPE_SLIDE = 2; + public static final int TYPE_FADE = 3; + + /** + * @param activity context + * @param type {@link #TYPE_EXPLODE},{@link #TYPE_SLIDE},{@link #TYPE_FADE} + */ + private static void setTransition(Activity activity, int type) { + if (AndroidVersion.atLeast(21)) { + Window window = activity.getWindow(); + window.requestFeature(Window.FEATURE_CONTENT_TRANSITIONS); + switch (type) { + case TYPE_EXPLODE: + window.setEnterTransition(new Explode()); + window.setExitTransition(new Explode()); + break; + case TYPE_SLIDE: + window.setEnterTransition(new Slide()); + window.setExitTransition(new Slide()); + break; + case TYPE_FADE: + window.setEnterTransition(new Fade()); + window.setExitTransition(new Fade()); + break; + } + } + } + + public static void finishWithAnimation(AppCompatActivity activity, @AnimRes int enterAnim, @AnimRes int exitAnim) { + activity.supportFinishAfterTransition(); + activity.overridePendingTransition(enterAnim, exitAnim); + } + +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/compat/AndroidVersion.java b/lib_base/src/main/java/com/android/base/utils/android/compat/AndroidVersion.java new file mode 100644 index 0000000..b096bdd --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/compat/AndroidVersion.java @@ -0,0 +1,37 @@ +package com.android.base.utils.android.compat; + +import android.os.Build; + +/** + * 版本判断 + */ +public class AndroidVersion { + + private AndroidVersion() { + throw new UnsupportedOperationException(); + } + + /** + * @param sdkVersion 要求的版本 + * @return true when the caller API version is > level + */ + public static boolean above(int sdkVersion) { + return Build.VERSION.SDK_INT > sdkVersion; + } + + /** + * @param sdkVersion 要求的版本 + * @return true when the caller API version >= level + */ + public static boolean atLeast(int sdkVersion) { + return Build.VERSION.SDK_INT >= sdkVersion; + } + + /** + * 当前系统版本 == level + */ + public static boolean at(int sdkVersion) { + return Build.VERSION.SDK_INT == sdkVersion; + } + +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/compat/SystemBarCompat.java b/lib_base/src/main/java/com/android/base/utils/android/compat/SystemBarCompat.java new file mode 100644 index 0000000..19c4468 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/compat/SystemBarCompat.java @@ -0,0 +1,363 @@ +package com.android.base.utils.android.compat; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Color; +import android.graphics.Point; +import android.os.Build; +import android.support.annotation.ColorInt; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.Display; +import android.view.Gravity; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.widget.FrameLayout; + +import timber.log.Timber; + +/** + * SystemBar工具类 + * + *

+ * other useful utils: + *

  • https://github.com/Zackratos/UltimateBar
  • + *
  • https://github.com/niorgai/StatusBarCompat
  • + *
  • https://github.com/laobie/StatusBarUtil
  • + *
  • https://github.com/msdx/status-bar-compat
  • + *

    + * + * @author Ztiany + * Date : 2016-03-16 21:52 + */ +public class SystemBarCompat { + + private SystemBarCompat() { + throw new UnsupportedOperationException(); + } + + private static final String STATUS_BAR_HEIGHT_RES_NAME = "status_bar_height"; + private static final String NAV_BAR_HEIGHT_RES_NAME = "navigation_bar_height"; + + /////////////////////////////////////////////////////////////////////////// + // Kitkat + /////////////////////////////////////////////////////////////////////////// + + @SuppressWarnings("WeakerAccess,unused") + public static void setTranslucentStatusOn19(Activity activity) { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { + setTranslucentSystemUi(activity, true, false); + } + } + + @SuppressWarnings("WeakerAccess,unused") + public static void setTranslucentNavigationOn19(Activity activity) { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { + setTranslucentSystemUi(activity, false, true); + } + } + + @SuppressWarnings("WeakerAccess,unused") + public static void setTranslucentOn19(Activity activity) { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { + setTranslucentSystemUi(activity, true, true); + } + } + + @SuppressWarnings("WeakerAccess,unused") + public static View setStatusBarColorOn19(Activity activity, @ColorInt int color) { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { + ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView(); + return setupStatusBarView(activity, decorView, color); + } + return null; + } + + /////////////////////////////////////////////////////////////////////////// + // After L + /////////////////////////////////////////////////////////////////////////// + + @SuppressWarnings("WeakerAccess,unused") + public static void setTranslucentStatusAfter19(Activity activity) { + if (!AndroidVersion.above(20)) { + return; + } + setTranslucentSystemUi(activity, true, false); + } + + @SuppressWarnings("WeakerAccess,unused") + public static void setTranslucentNavigationAfter19(Activity activity) { + if (!AndroidVersion.above(20)) { + return; + } + setTranslucentSystemUi(activity, false, true); + } + + @SuppressWarnings("WeakerAccess,unused") + public static void setTranslucentAfter19(Activity activity) { + if (!AndroidVersion.above(20)) { + return; + } + setTranslucentSystemUi(activity, true, true); + } + + public static void setupStatusBarColorAfter19(Activity activity, @ColorInt int color) { + if (!AndroidVersion.above(20)) { + return; + } + Window window = activity.getWindow(); + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + activity.getWindow().setStatusBarColor(color); + } + + public static void setupNavigationBarColorAfter19(Activity activity, @ColorInt int color) { + if (!AndroidVersion.above(20)) { + return; + } + Window window = activity.getWindow(); + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + window.setNavigationBarColor(color); + } + + /////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////// + + @TargetApi(Build.VERSION_CODES.KITKAT) + public static void setTranslucentSystemUi(Activity activity, boolean status, boolean navigation) { + Window win = activity.getWindow(); + setTranslucentSystemUi(win, status, navigation); + } + + public static void setStatusBarColor(Activity activity, @ColorInt int color) { + setStatusBarColorOn19(activity, color); + setupStatusBarColorAfter19(activity, color); + } + + public static void setTranslucentSystemUi(Window win, boolean status, boolean navigation) { + if (!AndroidVersion.atLeast(19)) { + return; + } + WindowManager.LayoutParams winParams = win.getAttributes(); + int bits = WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS; + if (status) { + winParams.flags |= bits; + } else { + winParams.flags &= ~bits; + } + bits = WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION; + if (navigation) { + winParams.flags |= bits; + } else { + winParams.flags &= ~bits; + } + win.setAttributes(winParams); + } + + /** + * 适用于4.4,在 rootView 中添加一个与 StatusBar 高度一样的 View,用于对状态栏着色 + * + * @param context 上下文 + * @param rootView 用于添加着色View的根View + * @param color 着色 + * @return 被添加的View + */ + @SuppressWarnings("WeakerAccess,unused") + public static View setupStatusBarView(Context context, ViewGroup rootView, @ColorInt int color) { + View mStatusBarTintView = new View(context); + mStatusBarTintView.setBackgroundColor(color); + FrameLayout.LayoutParams mStatusBarParams = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, getStatusBarHeight(context)); + mStatusBarParams.gravity = Gravity.TOP; + mStatusBarTintView.setLayoutParams(mStatusBarParams); + rootView.addView(mStatusBarTintView, 0); + return mStatusBarTintView; + } + + /** + * 获取状态栏高度 + */ + @SuppressWarnings("WeakerAccess,unused") + public static int getStatusBarHeight(Context context) { + int result = 0; + int resourceId = context.getResources().getIdentifier(STATUS_BAR_HEIGHT_RES_NAME, "dimen", "android"); + if (resourceId > 0) { + result = context.getResources().getDimensionPixelSize(resourceId); + } + return result; + } + + /** + * 获取NavigationBar高度 + * + * @param context 上下文 + */ + @SuppressWarnings("WeakerAccess,unused") + public static int getNavigationBarHeight(Context context) { + int navigationBarHeight = 0; + Resources rs = context.getResources(); + int id = rs.getIdentifier(NAV_BAR_HEIGHT_RES_NAME, "dimen", "android"); + if (id > 0 && hasNavigationBar(context)) { + navigationBarHeight = rs.getDimensionPixelSize(id); + } + return navigationBarHeight; + } + + /** + * 获取是否存在 NavigationBar + * + * @see detect-soft-navigation-bar-availability-in-android-device-progmatically, Android APP适配全面屏手机的技术要点 + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public static boolean hasNavigationBar(Context context) { + if (!hasSoftKeys(context)) { + Timber.d("hasSoftKeys = false"); + return false; + } + + Timber.d("hasSoftKeys = true"); + + WindowManager systemService; + if (context instanceof Activity) { + systemService = ((Activity) context).getWindowManager(); + } else { + systemService = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + } + + Point realSize = new Point(); + Point screenSize = new Point(); + boolean hasNavBar = false; + DisplayMetrics metrics = new DisplayMetrics(); + + systemService.getDefaultDisplay().getRealMetrics(metrics); + realSize.x = metrics.widthPixels; + realSize.y = metrics.heightPixels; + systemService.getDefaultDisplay().getSize(screenSize); + + if (realSize.y != screenSize.y) { + Timber.d("realSize.y = %d screenSize.y = %d", realSize.y, screenSize.y); + int difference = realSize.y - screenSize.y; + int navBarHeight = 0; + Resources resources = context.getResources(); + int resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android"); + if (resourceId > 0) { + navBarHeight = resources.getDimensionPixelSize(resourceId); + } + Timber.d("navBarHeight = %d ", navBarHeight); + if (navBarHeight != 0) { + if (difference >= navBarHeight) { + hasNavBar = true; + } + } + + } + Timber.d("hasNavigationBar = " + hasNavBar); + return hasNavBar; + } + + /** + * 判断有没有导航栏,参考:https://github.com/roughike/BottomBar/blob/master/bottom-bar/src/main/java/com/roughike/bottombar/NavbarUtils.java + * + * @param context 上下文 + * @return true表示有 + */ + @SuppressLint("ObsoleteSdkInt") + private static boolean hasSoftKeys(Context context) { + + boolean hasSoftwareKeys = true; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + Display d = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); + + DisplayMetrics realDisplayMetrics = new DisplayMetrics(); + d.getRealMetrics(realDisplayMetrics); + + int realHeight = realDisplayMetrics.heightPixels; + int realWidth = realDisplayMetrics.widthPixels; + + DisplayMetrics displayMetrics = new DisplayMetrics(); + d.getMetrics(displayMetrics); + + int displayHeight = displayMetrics.heightPixels; + int displayWidth = displayMetrics.widthPixels; + + hasSoftwareKeys = (realWidth - displayWidth) > 0 || (realHeight - displayHeight) > 0; + + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + boolean hasMenuKey = ViewConfiguration.get(context).hasPermanentMenuKey(); + boolean hasBackKey = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK); + hasSoftwareKeys = !hasMenuKey && !hasBackKey; + } + return hasSoftwareKeys; + } + + /** + * 获取ActionBar高度 + * + * @param activity activity + * @return ActionBar高度 + */ + @SuppressWarnings("WeakerAccess,unused") + public static int getActionBarHeight(Activity activity) { + TypedValue tv = new TypedValue(); + if (activity.getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) { + return TypedValue.complexToDimensionPixelSize(tv.data, activity.getResources().getDisplayMetrics()); + } + return 0; + } + + /////////////////////////////////////////////////////////////////////////// + // View Flags + /////////////////////////////////////////////////////////////////////////// + + @SuppressWarnings("WeakerAccess,unused") + public static void setTranslucentStatusViaViewFlags(Activity activity) { + setTranslucentSystemUiViaViewFlags(activity, true, false); + } + + @SuppressWarnings("WeakerAccess,unused") + public static void setTranslucentNavigationViaViewFlags(Activity activity) { + setTranslucentSystemUiViaViewFlags(activity, false, true); + } + + @SuppressWarnings("WeakerAccess,unused") + public static void setTranslucentSystemUiViaViewFlags(Activity activity) { + setTranslucentSystemUiViaViewFlags(activity, true, true); + } + + private static void setTranslucentSystemUiViaViewFlags(Activity activity, boolean status, boolean navigation) { + Window window = activity.getWindow(); + if (AndroidVersion.atLeast(21)) { + if (status && !navigation) { + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + window.getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + } + if (navigation && !status) { + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + window.getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); + } + if (navigation && status) { + window.clearFlags( + WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + window.getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); + } + setupStatusBarColorAfter19(activity, Color.TRANSPARENT); + setupNavigationBarColorAfter19(activity, Color.TRANSPARENT); + } else if (AndroidVersion.at(19)) { + setTranslucentSystemUi(window, status, navigation); + } + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/utils/android/orientation/AbsOrientationProvider.java b/lib_base/src/main/java/com/android/base/utils/android/orientation/AbsOrientationProvider.java new file mode 100644 index 0000000..28b5a91 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/orientation/AbsOrientationProvider.java @@ -0,0 +1,108 @@ +package com.android.base.utils.android.orientation; + +import android.content.Context; +import android.hardware.SensorManager; +import android.view.Surface; +import android.view.WindowManager; + +abstract class AbsOrientationProvider implements OrientationProvider { + + + private Listener mListener; + private int[] worldAxisForDeviceAxisXy; + private final WindowManager mWindowManager; + SensorManager mSensorManager; + private boolean mIsInvertAxle; + private float[] mAdjustedRotationMatrix = new float[9]; + private float[] mOrientation = new float[3]; + + + AbsOrientationProvider(Context context) { + mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + worldAxisForDeviceAxisXy = new int[2]; + mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + } + + + @Override + public final void startListening(Listener listener) { + if (listener == null) { + return; + } + if (mListener == listener) { + return; + } + if (mListener != null) { + mListener = null; + stopListening(); + } + mListener = listener; + onStartListening(); + } + + protected abstract void onStartListening(); + + + @Override + public final void stopListening() { + mListener = null; + onStopListening(); + } + + protected abstract void onStopListening(); + + + void determineOrientation(float[] rotationMatrix) { + setWorldAxisForDeviceAxis(); + SensorManager.remapCoordinateSystem(rotationMatrix, worldAxisForDeviceAxisXy[0], + worldAxisForDeviceAxisXy[1], mAdjustedRotationMatrix); + // Transform rotation matrix into azimuth/pitch/roll + SensorManager.getOrientation(mAdjustedRotationMatrix, mOrientation); + // Convert radians to degrees + // Convert radians to degrees + float pitch = (float) Math.toDegrees(mOrientation[1]);//pitch z轴与水平面的夹角 范围在-90 到 90° ,手机的任何一面垂直于水平面即为0°,z轴指向下方是为角度为正直 + float roll = (float) Math.toDegrees(mOrientation[2]);//roll 绕y轴 + float azimuth = (float) Math.toDegrees(mOrientation[0]);//roll 绕y轴 + if (mIsInvertAxle) { + pitch = -pitch; + roll = -roll; + azimuth = -azimuth; + } + notifyOrientation(azimuth, pitch, roll); + } + + + private void setWorldAxisForDeviceAxis() { + switch (mWindowManager.getDefaultDisplay().getRotation()) { + case Surface.ROTATION_0: + default: + worldAxisForDeviceAxisXy[0] = SensorManager.AXIS_X; + worldAxisForDeviceAxisXy[1] = SensorManager.AXIS_Z; + break; + case Surface.ROTATION_90: + worldAxisForDeviceAxisXy[0] = SensorManager.AXIS_Z; + worldAxisForDeviceAxisXy[1] = SensorManager.AXIS_MINUS_X; + break; + case Surface.ROTATION_180: + worldAxisForDeviceAxisXy[0] = SensorManager.AXIS_MINUS_X; + worldAxisForDeviceAxisXy[1] = SensorManager.AXIS_MINUS_Z; + break; + case Surface.ROTATION_270: + worldAxisForDeviceAxisXy[0] = SensorManager.AXIS_MINUS_Z; + worldAxisForDeviceAxisXy[1] = SensorManager.AXIS_X; + break; + } + } + + private void notifyOrientation(float azimuth, float pitch, float roll) { + if (mListener != null) { + mListener.onOrientationChanged(azimuth, pitch, roll); + } + } + + + @Override + public void invertAxle(boolean isInvertAxle) { + mIsInvertAxle = isInvertAxle; + } +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/orientation/AccelerometerMagneticOrientationProvider.java b/lib_base/src/main/java/com/android/base/utils/android/orientation/AccelerometerMagneticOrientationProvider.java new file mode 100644 index 0000000..a6a5e34 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/orientation/AccelerometerMagneticOrientationProvider.java @@ -0,0 +1,88 @@ +package com.android.base.utils.android.orientation; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; + +import com.android.base.utils.android.orientation.filter.MeanFilterSmoothing; + + +class AccelerometerMagneticOrientationProvider extends AbsOrientationProvider implements SensorEventListener { + + + private float[] mAccelerationValues = new float[3]; + private float[] mMagneticValues = new float[3]; + + private float[] mRotationMatrix = new float[9]; + private final Sensor mAccelerometerSensor; + private final Sensor mMagneticFieldSensor; + + private MeanFilterSmoothing mAccelerometerFilter; + private MeanFilterSmoothing mMagneticFieldFilter; + + + AccelerometerMagneticOrientationProvider(Context context) { + super(context); + mAccelerometerSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + mMagneticFieldSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); + + mAccelerometerFilter = new MeanFilterSmoothing(); + mMagneticFieldFilter = new MeanFilterSmoothing(); + + mAccelerometerFilter.setTimeConstant(0.1F); + mMagneticFieldFilter.setTimeConstant(0.1F); + } + + + @Override + protected void onStartListening() { + mSensorManager.registerListener(this, mAccelerometerSensor, SensorManager.SENSOR_DELAY_UI); + mSensorManager.registerListener(this, mMagneticFieldSensor, SensorManager.SENSOR_DELAY_UI); + } + + @Override + protected void onStopListening() { + mSensorManager.unregisterListener(this); + mSensorManager.unregisterListener(this, mAccelerometerSensor); + mSensorManager.unregisterListener(this, mMagneticFieldSensor); + } + + private boolean generateRotationMatrix() { + if (mAccelerationValues != null && mMagneticValues != null) { + boolean rotationMatrixGenerated; + rotationMatrixGenerated = + SensorManager.getRotationMatrix(mRotationMatrix, null, + mAccelerationValues, + mMagneticValues); + return rotationMatrixGenerated; + } + return false; + } + + + @Override + public void onSensorChanged(SensorEvent event) { + if (event.sensor == mAccelerometerSensor) { + + System.arraycopy(event.values, 0, mAccelerationValues, 0, + event.values.length); + mAccelerationValues = mAccelerometerFilter + .addSamples(mAccelerationValues); + } else if (event.sensor == mMagneticFieldSensor) { + + System.arraycopy(event.values, 0, mMagneticValues, 0, + event.values.length); + mMagneticValues = mMagneticFieldFilter.addSamples(mMagneticValues); + } + if (generateRotationMatrix()) { + determineOrientation(mRotationMatrix); + } + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + + } +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/utils/android/orientation/Orientation.java b/lib_base/src/main/java/com/android/base/utils/android/orientation/Orientation.java new file mode 100644 index 0000000..491f808 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/orientation/Orientation.java @@ -0,0 +1,58 @@ +package com.android.base.utils.android.orientation; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorManager; + +import java.util.List; + + +/** + * 手机旋转方向提供,参考这个项目 + */ +public class Orientation { + + public static OrientationProvider newInstance(Context context) { + if (hasRotationVector(context)) { + return new RotationVectorOrientationProvider(context); + } else if (hasAccelerometer(context) && hasMagnetometer(context)) { + return new AccelerometerMagneticOrientationProvider(context); + } else { + return new OrientationProvider() { + @Override + public void startListening(Listener listener) { + + } + + @Override + public void invertAxle(boolean isInvert) { + + } + + @Override + public void stopListening() { + + } + }; + } + } + + + private static boolean hasMagnetometer(Context context) { + SensorManager mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + List listSensors = mSensorManager.getSensorList(Sensor.TYPE_MAGNETIC_FIELD); + return !listSensors.isEmpty(); + } + + private static boolean hasAccelerometer(Context context) { + SensorManager mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + List listSensors = mSensorManager.getSensorList(Sensor.TYPE_ACCELEROMETER); + return !listSensors.isEmpty(); + } + + private static boolean hasRotationVector(Context context) { + SensorManager mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + List listSensors = mSensorManager.getSensorList(Sensor.TYPE_ROTATION_VECTOR); + return !listSensors.isEmpty(); + } +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/orientation/OrientationProvider.java b/lib_base/src/main/java/com/android/base/utils/android/orientation/OrientationProvider.java new file mode 100644 index 0000000..32c90f5 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/orientation/OrientationProvider.java @@ -0,0 +1,16 @@ +package com.android.base.utils.android.orientation; + +public interface OrientationProvider { + + void startListening(Listener listener); + + void invertAxle(boolean isInvert); + + void stopListening(); + + interface Listener { + void onOrientationChanged(float azimuth, float pitch, float roll); + } + + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/utils/android/orientation/RotationVectorOrientationProvider.java b/lib_base/src/main/java/com/android/base/utils/android/orientation/RotationVectorOrientationProvider.java new file mode 100644 index 0000000..fba330a --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/orientation/RotationVectorOrientationProvider.java @@ -0,0 +1,48 @@ +package com.android.base.utils.android.orientation; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; + +class RotationVectorOrientationProvider extends AbsOrientationProvider implements SensorEventListener { + + private final Sensor mSensor; + private int mLastAccuracy; + private float[] Q = new float[9]; + + RotationVectorOrientationProvider(Context context) { + super(context); + mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR); + } + + @Override + protected void onStartListening() { + mSensorManager.registerListener(RotationVectorOrientationProvider.this, mSensor, SensorManager.SENSOR_DELAY_UI); + } + + @Override + protected void onStopListening() { + mSensorManager.unregisterListener(this); + } + + @Override + public void onSensorChanged(SensorEvent event) { + if (event.sensor == mSensor) { + if (mLastAccuracy != SensorManager.SENSOR_STATUS_UNRELIABLE) { + SensorManager.getRotationMatrixFromVector(Q, event.values); + determineOrientation(Q); + } + } + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + if (sensor == mSensor) { + if (mLastAccuracy != accuracy) { + mLastAccuracy = accuracy; + } + } + } +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/orientation/filter/MeanFilterSmoothing.java b/lib_base/src/main/java/com/android/base/utils/android/orientation/filter/MeanFilterSmoothing.java new file mode 100644 index 0000000..edb0c1d --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/orientation/filter/MeanFilterSmoothing.java @@ -0,0 +1,143 @@ +package com.android.base.utils.android.orientation.filter; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +/* + * Acceleration Explorer + * Copyright (C) 2013-2015, Kaleb Kircher - Kircher Engineering, LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * Implements a mean filter designed to smooth the data points based on a time + * constant in units of seconds. The mean filter will average the samples that + * occur over a period defined by the time constant... the number of samples + * that are averaged is known as the filter window. The approach allows the + * filter window to be defined over a period of time, instead of a fixed number + * of samples. This is important on Android devices that are equipped with + * different hardware sensors that output samples at different frequencies and + * also allow the developer to generally specify the output frequency. Defining + * the filter window in terms of the time constant allows the mean filter to + * applied to all sensor outputs with the same relative filter window, + * regardless of sensor frequency. + * + * @author Kaleb + * @version %I%, %G% + */ +public class MeanFilterSmoothing { + private float timeConstant = 1; + private float startTime = 0; + private float timestamp = 0; + private float hz = 0; + + private int count = 0; + // The size of the mean filters rolling window. + private int filterWindow = 20; + + private boolean dataInit; + + private ArrayList> dataLists; + + /** + * Initialize a new MeanFilter object. + */ + public MeanFilterSmoothing() { + dataLists = new ArrayList<>(); + dataInit = false; + } + + public void setTimeConstant(float timeConstant) { + this.timeConstant = timeConstant; + } + + public void reset() { + startTime = 0; + timestamp = 0; + count = 0; + hz = 0; + } + + /** + * Filter the data. + * + * @param data iterator contains input the data. + * @return the filtered output data. + */ + public float[] addSamples(float[] data) { + // Initialize the start time. + if (startTime == 0) { + startTime = System.nanoTime(); + } + + timestamp = System.nanoTime(); + + // Find the sample period (between updates) and convert from + // nanoseconds to seconds. Note that the sensor delivery rates can + // individually vary by a relatively large time frame, so we use an + // averaging technique with the number of sensor updates to + // determine the delivery rate. + hz = (count++ / ((timestamp - startTime) / 1000000000.0f)); + + filterWindow = (int) (hz * timeConstant); + + for (int i = 0; i < data.length; i++) { + // Initialize the data structures for the data set. + if (!dataInit) { + dataLists.add(new LinkedList()); + } + + dataLists.get(i).addLast(data[i]); + + if (dataLists.get(i).size() > filterWindow) { + dataLists.get(i).removeFirst(); + } + } + + dataInit = true; + + float[] means = new float[dataLists.size()]; + + for (int i = 0; i < dataLists.size(); i++) { + means[i] = getMean(dataLists.get(i)); + } + + return means; + } + + /** + * Get the mean of the data set. + * + * @param data the data set. + * @return the mean of the data set. + */ + private float getMean(List data) { + float m = 0; + float count = 0; + + for (int i = 0; i < data.size(); i++) { + m += data.get(i).floatValue(); + count++; + } + + if (count != 0) { + m = m / count; + } + + return m; + } + +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/text/HtmlBuilder.java b/lib_base/src/main/java/com/android/base/utils/android/text/HtmlBuilder.java new file mode 100644 index 0000000..0be23a0 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/text/HtmlBuilder.java @@ -0,0 +1,582 @@ +/* + * Copyright (C) 2017 Jared Rummler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.base.utils.android.text; + +import android.os.Build; +import android.support.annotation.RequiresApi; +import android.text.Html; +import android.text.Spanned; +import android.widget.TextView; + +import java.util.Iterator; +import java.util.LinkedList; + +/** + *

    Build a valid HTML string for a {@link TextView}.

    + * + *

    Example usage:

    + * + *
    + * 
    + * HtmlBuilder html = new HtmlBuilder()
    + * .p()
    + * .b()
    + * .font().color("red").face("sans-serif-condensed").text("Hello").close()
    + * .close()
    + * .close();
    + *
    + * // html.toString():
    + * // <p><b><font color="red" face="sans-serif-condensed">Hello</font></b></p>
    + *
    + * yourEditText.setText(html.toSpan());
    + * 
    + * 
    + * + *

    HTML Tags Supported by {@link TextView}:

    + * + *
      + *
    • <a href="...">
    • + *
    • <b>
    • + *
    • <big>
    • + *
    • <blockquote>
    • + *
    • <br>
    • + *
    • <cite>
    • + *
    • <dfn>
    • + *
    • <div align="...">
    • + *
    • <em>
    • + *
    • <font color="..." face="...">
    • + *
    • <h1>
    • + *
    • <h2>
    • + *
    • <h3>
    • + *
    • <h4>
    • + *
    • <h5>
    • + *
    • <h6>
    • + *
    • <i>
    • + *
    • <img src="...">
    • + *
    • <p>
    • + *
    • <small>
    • + *
    • <strike>
    • + *
    • <strong>
    • + *
    • <sub>
    • + *
    • <sup>
    • + *
    • <tt>
    • + *
    • <u>
    • + *
    • <ul> (Android 7.0+)
    • + *
    • <li> (Android 7.0+)
    • + *
    + * + * @see HtmlBuilder + */ +public class HtmlBuilder { + + private final StringBuilder html = new StringBuilder(); + + private final LinkedList tags = new LinkedList<>(); + + public HtmlBuilder open(String element, String data) { + tags.add(element); + html.append('<'); + html.append(element); + if (data != null) { + html.append(' ').append(data); + } + html.append('>'); + return this; + } + + public HtmlBuilder open(String element) { + return open(element, null); + } + + public HtmlBuilder close(String element) { + html.append("'); + for (Iterator iterator = tags.iterator(); iterator.hasNext(); ) { + if (iterator.next().equals(element)) { + iterator.remove(); + break; + } + } + return this; + } + + public HtmlBuilder close() { + if (tags.isEmpty()) { + return this; + } + html.append("'); + return this; + } + + public HtmlBuilder close(char element) { + return close(String.valueOf(element)); + } + + public HtmlBuilder append(boolean b) { + html.append(b); + return this; + } + + public HtmlBuilder append(char c) { + html.append(c); + return this; + } + + public HtmlBuilder append(int i) { + html.append(i); + return this; + } + + public HtmlBuilder append(long l) { + html.append(l); + return this; + } + + public HtmlBuilder append(float f) { + html.append(f); + return this; + } + + public HtmlBuilder append(double d) { + html.append(d); + return this; + } + + public HtmlBuilder append(Object obj) { + html.append(obj); + return this; + } + + public HtmlBuilder append(String str) { + html.append(str); + return this; + } + + public HtmlBuilder append(StringBuffer sb) { + html.append(sb); + return this; + } + + public HtmlBuilder append(char[] chars) { + html.append(chars); + return this; + } + + public HtmlBuilder append(char[] str, int offset, int len) { + html.append(str, offset, len); + return this; + } + + public HtmlBuilder append(CharSequence csq) { + html.append(csq); + return this; + } + + public HtmlBuilder append(CharSequence csq, int start, int end) { + html.append(csq, start, end); + return this; + } + + public HtmlBuilder append(Tag tag) { + html.append(tag.toString()); + return this; + } + + public HtmlBuilder a(String href, String text) { + return append(String.format("%s", href, text)); + } + + public HtmlBuilder b() { + return open("b"); + } + + public HtmlBuilder b(String text) { + html.append("").append(text).append(""); + return this; + } + + public HtmlBuilder big() { + return open("big"); + } + + public HtmlBuilder big(String text) { + html.append("").append(text).append(""); + return this; + } + + public HtmlBuilder blockquote() { + return open("blockquote"); + } + + public HtmlBuilder blockquote(String text) { + html.append("
    ").append(text).append("
    "); + return this; + } + + public HtmlBuilder br() { + html.append("
    "); + return this; + } + + public HtmlBuilder cite() { + return open("cite"); + } + + public HtmlBuilder cite(String text) { + html.append("").append(text).append(""); + return this; + } + + public HtmlBuilder dfn() { + return open("dfn"); + } + + public HtmlBuilder dfn(String text) { + html.append("").append(text).append(""); + return this; + } + + public HtmlBuilder div() { + return open("div"); + } + + public HtmlBuilder div(String align) { + html.append(String.format("
    ", align)); + tags.add("div"); + return this; + } + + public HtmlBuilder em() { + return open("em"); + } + + public HtmlBuilder em(String text) { + html.append("").append(text).append(""); + return this; + } + + public Font font() { + return new Font(this); + } + + public HtmlBuilder font(int color, String text) { + return font().color(color).text(text).close(); + } + + public HtmlBuilder font(String face, String text) { + return font().face(face).text(text).close(); + } + + public HtmlBuilder h1() { + return open("h1"); + } + + public HtmlBuilder h1(String text) { + html.append("

    ").append(text).append("

    "); + return this; + } + + public HtmlBuilder h2() { + return open("h2"); + } + + public HtmlBuilder h2(String text) { + html.append("

    ").append(text).append("

    "); + return this; + } + + public HtmlBuilder h3() { + return open("h3"); + } + + public HtmlBuilder h3(String text) { + html.append("

    ").append(text).append("

    "); + return this; + } + + public HtmlBuilder h4() { + return open("h4"); + } + + public HtmlBuilder h4(String text) { + html.append("

    ").append(text).append("

    "); + return this; + } + + public HtmlBuilder h5() { + return open("h5"); + } + + public HtmlBuilder h5(String text) { + html.append("
    ").append(text).append("
    "); + return this; + } + + public HtmlBuilder h6() { + return open("h6"); + } + + public HtmlBuilder h6(String text) { + html.append("
    ").append(text).append("
    "); + return this; + } + + public HtmlBuilder i() { + return open("i"); + } + + public HtmlBuilder i(String text) { + html.append("").append(text).append(""); + return this; + } + + public Img img() { + return new Img(this); + } + + public HtmlBuilder img(String src) { + return img().src(src).close(); + } + + public HtmlBuilder p() { + return open("p"); + } + + public HtmlBuilder p(String text) { + html.append("

    ").append(text).append("

    "); + return this; + } + + public HtmlBuilder small() { + return open("small"); + } + + public HtmlBuilder small(String text) { + html.append("").append(text).append(""); + return this; + } + + public HtmlBuilder strike() { + return open("strike"); + } + + public HtmlBuilder strike(String text) { + html.append("").append(text).append(""); + return this; + } + + public HtmlBuilder strong() { + return open("strong"); + } + + public HtmlBuilder strong(String text) { + html.append("").append(text).append(""); + return this; + } + + public HtmlBuilder sub() { + return open("sub"); + } + + public HtmlBuilder sub(String text) { + html.append("").append(text).append(""); + return this; + } + + public HtmlBuilder sup() { + return open("sup"); + } + + public HtmlBuilder sup(String text) { + html.append("").append(text).append(""); + return this; + } + + public HtmlBuilder tt() { + return open("tt"); + } + + public HtmlBuilder tt(String text) { + html.append("").append(text).append(""); + return this; + } + + public HtmlBuilder u() { + return open("u"); + } + + public HtmlBuilder u(String text) { + html.append("").append(text).append(""); + return this; + } + + @RequiresApi(Build.VERSION_CODES.N) + public HtmlBuilder ul() { + return open("ul"); + } + + @RequiresApi(Build.VERSION_CODES.N) + public HtmlBuilder li() { + return open("li"); + } + + @RequiresApi(Build.VERSION_CODES.N) + public HtmlBuilder li(String text) { + html.append("
  • ").append(text).append("
  • "); + return this; + } + + @RequiresApi(Build.VERSION_CODES.N) + public Spanned build(int flags) { + return Html.fromHtml(html.toString(), flags); + } + + public Spanned build() { + //noinspection deprecation + return Html.fromHtml(html.toString()); + } + + @Override + public String toString() { + return html.toString(); + } + + public static class Tag { + + final HtmlBuilder builder; + final String element; + String separator = ""; + + public Tag(HtmlBuilder builder, String element) { + this.builder = builder; + this.element = element; + open(); + } + + protected void open() { + builder.append('<').append(element).append(' '); + } + + public HtmlBuilder close() { + return builder.append("'); + } + + @Override + public String toString() { + return builder.toString(); + } + + } + + public static class Font extends Tag { + + public Font() { + this(new HtmlBuilder()); + } + + public Font(HtmlBuilder builder) { + super(builder, "font"); + } + + public Font size(int size) { + builder.append(separator).append("size=\"").append(size).append('\"'); + separator = " "; + return this; + } + + public Font size(String size) { + builder.append(separator).append("size=\"").append(size).append('\"'); + separator = " "; + return this; + } + + public Font color(int color) { + return color(String.format("#%06X", (0xFFFFFF & color))); + } + + public Font color(String color) { + builder.append(separator).append("color=\"").append(color).append('\"'); + separator = " "; + return this; + } + + public Font face(String face) { + builder.append(separator).append("face=\"").append(face).append('\"'); + separator = " "; + return this; + } + + public Font text(String text) { + builder.append('>').append(text); + return this; + } + + } + + public static class Img extends Tag { + + public Img() { + this(new HtmlBuilder()); + } + + public Img(HtmlBuilder builder) { + super(builder, "img"); + } + + public Img src(String src) { + builder.append(separator).append("src=\"").append(src).append('\"'); + separator = " "; + return this; + } + + public Img alt(String alt) { + builder.append(separator).append("alt=\"").append(alt).append('\"'); + separator = " "; + return this; + } + + public Img height(String height) { + builder.append(separator).append("height=\"").append(height).append('\"'); + separator = " "; + return this; + } + + public Img height(int height) { + builder.append(separator).append("height=\"").append(height).append('\"'); + separator = " "; + return this; + } + + public Img width(String width) { + builder.append(separator).append("width=\"").append(width).append('\"'); + separator = " "; + return this; + } + + public Img width(int width) { + builder.append(separator).append("width=\"").append(width).append('\"'); + separator = " "; + return this; + } + + @Override + public HtmlBuilder close() { + return builder.append('>'); + } + + } + +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/text/RoundedBackgroundSpan.java b/lib_base/src/main/java/com/android/base/utils/android/text/RoundedBackgroundSpan.java new file mode 100644 index 0000000..79856aa --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/text/RoundedBackgroundSpan.java @@ -0,0 +1,42 @@ +package com.android.base.utils.android.text; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.support.annotation.NonNull; +import android.text.style.ReplacementSpan; + +/** + * 圆角背景Span + */ +public class RoundedBackgroundSpan extends ReplacementSpan { + + private int mPadding; + private int mBackgroundColor; + private int mTextColor; + private int mCorner; + + public RoundedBackgroundSpan(int backgroundColor, int textColor, int padding, int corner) { + super(); + mBackgroundColor = backgroundColor; + mTextColor = textColor; + mCorner = corner; + mPadding = padding; + } + + @Override + public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) { + return (int) (mPadding + paint.measureText(text.subSequence(start, end).toString()) + mPadding); + } + + @Override + public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) { + float width = paint.measureText(text.subSequence(start, end).toString()); + RectF rect = new RectF(x, top, x + width + (2 * mPadding), bottom); + paint.setColor(mBackgroundColor); + canvas.drawRoundRect(rect, mCorner, mCorner, paint); + paint.setColor(mTextColor); + canvas.drawText(text, start, end, x + mPadding, y, paint); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/utils/common/Checker.java b/lib_base/src/main/java/com/android/base/utils/common/Checker.java new file mode 100644 index 0000000..e2f0429 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/common/Checker.java @@ -0,0 +1,59 @@ +package com.android.base.utils.common; + +import java.util.Collection; +import java.util.Map; + +/** + * 对象检查工具 + */ +public class Checker { + + private Checker() { + + } + + public static boolean isEmpty(Collection data) { + return data == null || data.size() == 0; + } + + public static boolean notEmpty(Collection data) { + return !isEmpty(data); + } + + public static boolean isNull(Object o) { + return o == null; + } + + public static boolean isEmpty(Map map) { + return map == null || map.size() == 0; + } + + public static boolean notEmpty(Map map) { + return !isEmpty(map); + } + + public static boolean isEmpty(T[] t) { + return t == null || t.length == 0; + } + + public static boolean notEmpty(T[] t) { + return !isEmpty(t); + } + + public static T requireNonNull(T obj) { + if (obj == null) + throw new NullPointerException(); + return obj; + } + + public static T requireNonNull(T obj, String message) { + if (obj == null) + throw new NullPointerException(message); + return obj; + } + + public static boolean nonNull(Object obj) { + return obj != null; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/utils/common/CloseUtils.java b/lib_base/src/main/java/com/android/base/utils/common/CloseUtils.java new file mode 100644 index 0000000..3515087 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/common/CloseUtils.java @@ -0,0 +1,62 @@ +package com.android.base.utils.common; + +import java.io.Closeable; +import java.io.IOException; + + +public class CloseUtils { + + private CloseUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * 安静关闭IO + */ + public static void closeIO(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + /** + * 关闭IO + */ + public static void closeIOQuietly(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (IOException ignored) { + } + } + } + + /** + * 关闭IO + * + * @param closeables closeable + */ + public static void closeIO(Closeable... closeables) { + if (closeables == null) return; + for (Closeable closeable : closeables) { + closeIO(closeable); + } + } + + /** + * 安静关闭IO + * + * @param closeables closeable + */ + public static void closeIOQuietly(Closeable... closeables) { + if (closeables == null) return; + for (Closeable closeable : closeables) { + closeIOQuietly(closeable); + } + } + +} diff --git a/lib_base/src/main/java/com/android/base/utils/common/CollectionUtils.java b/lib_base/src/main/java/com/android/base/utils/common/CollectionUtils.java new file mode 100644 index 0000000..1332c74 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/common/CollectionUtils.java @@ -0,0 +1,31 @@ +package com.android.base.utils.common; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + + +public class CollectionUtils { + + private CollectionUtils() { + throw new UnsupportedOperationException(); + } + + public static ArrayList toArrayList(List list) { + if (list == null) { + return new ArrayList<>(0); + } + if (list instanceof ArrayList) { + return (ArrayList) list; + } + return new ArrayList<>(list); + } + + public static List emptyIfNull(List list) { + if (list == null) { + return Collections.emptyList(); + } + return list; + } + +} diff --git a/lib_base/src/main/java/com/android/base/utils/common/FileUtils.java b/lib_base/src/main/java/com/android/base/utils/common/FileUtils.java new file mode 100644 index 0000000..2096e20 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/common/FileUtils.java @@ -0,0 +1,90 @@ +package com.android.base.utils.common; + +import android.util.Log; + +import java.io.File; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2016-11-30 16:29 + */ +public class FileUtils { + + private static final String TAG = FileUtils.class.getSimpleName(); + + private FileUtils() { + throw new UnsupportedOperationException("no need instantiation"); + } + + public static boolean makeFilePath(File file) { + if (file == null) { + return false; + } + File parent = file.getParentFile(); + return parent.exists() || makeDir(parent); + } + + private static boolean makeDir(File file) { + return file != null && (file.exists() || file.mkdirs()); + } + + public static long sizeOfFile(File file) { + if (file == null || !file.exists()) { + return 0; + } + if (file.isDirectory()) { + long length = 0; + for (File subFile : file.listFiles()) { + length += sizeOfFile(subFile); + } + return length; + } else { + return file.length(); + } + } + + public static void deleteFile(File file) { + if (file.isDirectory()) { + for (File subFile : file.listFiles()) { + deleteFile(subFile); + } + } else { + boolean delete = file.delete(); + Log.d(TAG, "delete:" + delete); + } + } + + public static boolean isFileExists(File file) { + return file != null && file.isFile() && file.exists(); + } + + /** + * 获取全路径中的文件拓展名 + * + * @param file 文件 + * @return 文件拓展名 + */ + public static String getFileExtension(File file) { + if (file == null) return null; + return getFileExtension(file.getPath()); + } + + /** + * 获取全路径中的文件拓展名 + * + * @param filePath 文件路径 + * @return 文件拓展名 + */ + public static String getFileExtension(String filePath) { + if (StringChecker.isEmpty(filePath)) { + return filePath; + } + int lastPoi = filePath.lastIndexOf('.'); + int lastSep = filePath.lastIndexOf(File.separator); + if (lastPoi == -1 || lastSep >= lastPoi) { + return ""; + } + return filePath.substring(lastPoi + 1); + } +} diff --git a/lib_base/src/main/java/com/android/base/utils/common/StringChecker.java b/lib_base/src/main/java/com/android/base/utils/common/StringChecker.java new file mode 100644 index 0000000..eba3232 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/common/StringChecker.java @@ -0,0 +1,126 @@ +package com.android.base.utils.common; + +import java.util.regex.Pattern; + +public class StringChecker { + + private StringChecker() { + throw new UnsupportedOperationException("no need instantiation"); + } + + private static final String CHINA_PHONE_REG = "^1\\d{10}$"; + private static final String ID_CARD_REG = "[1-9]\\d{13,16}[a-zA-Z0-9]{1}"; + private static final String EMAIL_REG = "\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*"; + private static final String DIGIT_REG = "\\-?[1-9]\\d+\""; + private static final String URL_REG = "(https?://(w{3}\\.)?)?\\w+\\.\\w+(\\.[a-zA-Z]+)*(:\\d{1,5})?(/\\w*)*(\\??(.+=.*)?(&.+=.*)?)?"; + private static final String CHINESE_REGEX = "^[\u4E00-\u9FA5]+$"; + private static final String DECIMALS_REG = "\\-?[1-9]\\d+(\\.\\d+)?"; + private static final String LETTERS_REG = ".*[a-zA-Z]++.*"; + private static final String DIGITAL_REG = ".*[0-9]++.*"; + private static final String DIGITAL_LETTER_ONLY_REG = "^[A-Za-z0-9]+$"; + + /** + * 验证中国的手机号 + * + * @return 验证成功返回 true,验证失败返回 false + */ + public static boolean isChinaPhoneNumber(String mobile) { + return !isEmpty(mobile) && Pattern.matches(CHINA_PHONE_REG, mobile); + } + + public static boolean containsLetter(String text) { + return !isEmpty(text) && Pattern.matches(LETTERS_REG, text); + } + + public static boolean containsDigital(String text) { + return !isEmpty(text) && Pattern.matches(DIGITAL_REG, text); + } + + public static boolean containsDigtalLetterOnly(String text) { + return !isEmpty(text) && Pattern.matches(DIGITAL_LETTER_ONLY_REG, text); + } + + /** + * 验证身份证号码 + * + * @param idCard 居民身份证号码15位或18位,最后一位可能是数字或字母 + * @return 验证成功返回true,验证失败返回false + */ + public static boolean isIdCard(String idCard) { + return !isEmpty(idCard) && Pattern.matches(ID_CARD_REG, idCard); + } + + /** + * 验证Email + * + * @param email email地址,格式:zhangsan@sina.com,zhangsan@xxx.com.cn,xxx代表邮件服务商 + * @return 验证成功返回true,验证失败返回false + */ + public static boolean isEmail(String email) { + return !isEmpty(email) && Pattern.matches(EMAIL_REG, email); + } + + /** + * 验证整数(正整数和负整数) + * + * @param digit 一位或多位0-9之间的整数 + * @return 验证成功返回true,验证失败返回false + */ + public static boolean isDigit(String digit) { + return !isEmpty(digit) && Pattern.matches(DIGIT_REG, digit); + } + + /** + * 验证整数和浮点数(正负整数和正负浮点数) + * + * @param decimals 一位或多位0-9之间的浮点数,如:1.23,233.30 + * @return 验证成功返回true,验证失败返回false + */ + public static boolean isDecimals(String decimals) { + return !isEmpty(decimals) && Pattern.matches(DECIMALS_REG, decimals); + } + + /** + * 验证中文 + * + * @param chinese 中文字符 + * @return 验证成功返回true,验证失败返回false + */ + public static boolean isChinese(String chinese) { + return !isEmpty(chinese) && Pattern.matches(CHINESE_REGEX, chinese); + } + + /** + * 验证URL地址 + * + * @param url url + * @return 验证成功返回true,验证失败返回false + */ + public static boolean isURL(String url) { + return !isEmpty(url) && Pattern.matches(URL_REG, url); + } + + /** + * 获取字符串的字符个数 + */ + public static int getCharLength(String string) { + if (isEmpty(string)) { + return 0; + } + return string.trim().toCharArray().length; + } + + public static boolean isCharLength(String string, int length) { + return null != string && ((isEmpty(string) && length == 0) || (string.trim().toCharArray().length == length)); + } + + public static boolean isLengthIn(String string, int min, int max) { + int length = string == null ? 0 : string.length(); + return length <= max && length >= min; + } + + public static boolean isEmpty(CharSequence str) { + return str == null || str.toString().trim().length() == 0; + } + +} diff --git a/lib_base/src/main/java/com/android/base/utils/security/AESCipherStrategy.java b/lib_base/src/main/java/com/android/base/utils/security/AESCipherStrategy.java new file mode 100644 index 0000000..9cfa9c7 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/security/AESCipherStrategy.java @@ -0,0 +1,42 @@ +package com.android.base.utils.security; + +import com.android.base.utils.security.util.AESUtils; + +import java.io.UnsupportedEncodingException; + +/** + * Reference: https://github.com/zhengxiaopeng/Rocko-Android-Demos + */ +public class AESCipherStrategy extends CipherStrategy { + + private String key; + + public AESCipherStrategy(String key) { + this.key = key; + } + + @Override + public String encrypt(String content) { + byte[] encryptByte = new byte[0]; + try { + encryptByte = AESUtils.encryptData(content.getBytes(CHARSET), key); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + return encodeConvert(encryptByte); + } + + @Override + public String decrypt(String encryptContent) { + byte[] encrypByte = decodeConvert(encryptContent); + byte[] decryptByte = AESUtils.decryptData(encrypByte, key); + String result = ""; + try { + result = new String(decryptByte, CHARSET); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + + return result; + } +} diff --git a/lib_base/src/main/java/com/android/base/utils/security/CipherStrategy.java b/lib_base/src/main/java/com/android/base/utils/security/CipherStrategy.java new file mode 100644 index 0000000..77774a0 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/security/CipherStrategy.java @@ -0,0 +1,43 @@ +package com.android.base.utils.security; + + +import android.util.Base64; + +/** + * Reference: https://github.com/zhengxiaopeng/Rocko-Android-Demos + */ +abstract class CipherStrategy { + + final static String CHARSET = "UTF-8"; + + /** + * 将加密内容的 Base64 编码转换为二进制内容 + */ + protected byte[] decodeConvert(String str) { + return Base64.decode(str, Base64.DEFAULT); + } + + /** + * 对加密后的二进制结果转换为 Base64 编码 + */ + protected String encodeConvert(byte[] bytes) { + return new String(Base64.encode(bytes, Base64.DEFAULT)); + } + + /** + * 对字符串进行加密 + * + * @param content 需要加密的字符串 + * @return + */ + public abstract String encrypt(String content); + + /** + * 对字符串进行解密 + * + * @param encryptContent 加密内容的 Base64 编码 + * @return + */ + public abstract String decrypt(String encryptContent); + +} diff --git a/lib_base/src/main/java/com/android/base/utils/security/DESCipherStrategy.java b/lib_base/src/main/java/com/android/base/utils/security/DESCipherStrategy.java new file mode 100644 index 0000000..10e48a3 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/security/DESCipherStrategy.java @@ -0,0 +1,47 @@ +package com.android.base.utils.security; + + +import com.android.base.utils.security.util.DESUtils; + +import java.io.UnsupportedEncodingException; + +/** + * Reference: https://github.com/zhengxiaopeng/Rocko-Android-Demos + */ +public class DESCipherStrategy extends CipherStrategy { + + private String key;// 解密密码 + + /** + * @param key 加解密的 key + */ + public DESCipherStrategy(String key) { + this.key = key; + } + + @Override + public String encrypt(String content) { + byte[] encryptByte = null; + try { + encryptByte = DESUtils.encrypt(content.getBytes(CHARSET), key); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + return encodeConvert(encryptByte); + } + + @Override + public String decrypt(String encryptContent) { + byte[] encrypByte = decodeConvert(encryptContent); + byte[] decryptByte = DESUtils.decrypt(encrypByte, key); + String result = ""; + try { + result = new String(decryptByte, CHARSET); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + + return result; + } + +} diff --git a/lib_base/src/main/java/com/android/base/utils/security/RSACipherStrategy.java b/lib_base/src/main/java/com/android/base/utils/security/RSACipherStrategy.java new file mode 100644 index 0000000..99490b6 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/security/RSACipherStrategy.java @@ -0,0 +1,70 @@ +package com.android.base.utils.security; + +import com.android.base.utils.security.util.RSAUtils; + +import java.io.InputStream; +import java.security.PrivateKey; +import java.security.PublicKey; + +/** + * Reference: https://github.com/zhengxiaopeng/Rocko-Android-Demos + */ +public class RSACipherStrategy extends CipherStrategy { + + private PublicKey mPublicKey; + private PrivateKey mPrivateKey; + + public void initPublicKey(String publicKeyContentStr) { + try { + mPublicKey = RSAUtils.loadPublicKey(publicKeyContentStr); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void initPublicKey(InputStream publicKeyIs) { + try { + mPublicKey = RSAUtils.loadPublicKey(publicKeyIs); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void initPrivateKey(String privateKeyContentStr) { + try { + mPrivateKey = RSAUtils.loadPrivateKey(privateKeyContentStr); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void initPrivateKey(InputStream privateIs) { + try { + mPrivateKey = RSAUtils.loadPrivateKey(privateIs); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public String encrypt(String content) { + if (mPublicKey == null) { + throw new NullPointerException("PublicKey is null, please initialize it first"); + } + byte[] encryptByte = RSAUtils.encryptData(content.getBytes(), mPublicKey); + + return encodeConvert(encryptByte); + } + + @Override + public String decrypt(String encryptContent) { + if (mPrivateKey == null) { + throw new NullPointerException("PrivateKey is null, please initialize it first"); + } + byte[] encryptByte = decodeConvert(encryptContent); + byte[] decryptByte = RSAUtils.decryptData(encryptByte, mPrivateKey); + + return new String(decryptByte); + } + +} diff --git a/lib_base/src/main/java/com/android/base/utils/security/SimpleRsa.java b/lib_base/src/main/java/com/android/base/utils/security/SimpleRsa.java new file mode 100644 index 0000000..c8c43c1 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/security/SimpleRsa.java @@ -0,0 +1,87 @@ +package com.android.base.utils.security; + +import android.util.Base64; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; + +import javax.crypto.Cipher; + + +public class SimpleRsa { + + private static final String ALGORITHM = "RSA"; + private static final String TRANSFORMATION = "RSA/ECB/PKCS1Padding"; + + /** + * 得到公钥 + * + * @param algorithm 算法 + * @param bysKey key + * @return 公钥 + */ + private static PublicKey getPublicKeyFromX509(String algorithm, String bysKey) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException { + byte[] decodedKey = Base64.decode(bysKey, Base64.DEFAULT); + X509EncodedKeySpec x509 = new X509EncodedKeySpec(decodedKey); + KeyFactory keyFactory = KeyFactory.getInstance(algorithm, "BC"); + return keyFactory.generatePublic(x509); + } + + /** + * 使用公钥加密 + * + * @param content 加密字符串 + * @return 加密后的字符串 + */ + public static String encryptByPublic(String content, String RSA_PUBLIC_KEY) { + try { + PublicKey pubKey = getPublicKeyFromX509(ALGORITHM, RSA_PUBLIC_KEY); + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + cipher.init(Cipher.ENCRYPT_MODE, pubKey); + byte plaintext[] = content.getBytes("UTF-8"); + byte[] output = cipher.doFinal(plaintext); + return new String(Base64.encode(output, Base64.DEFAULT)); + } catch (Exception e) { + return null; + } + } + + /** + * 使用公钥解密 + * + * @param content 密文 + * @return 解密后的字符串 + */ + public static String decryptByPublic(String content, String RSA_PUBLIC_KEY) { + try { + PublicKey pubkey = getPublicKeyFromX509(ALGORITHM, RSA_PUBLIC_KEY); + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + cipher.init(Cipher.DECRYPT_MODE, pubkey); + InputStream ins = new ByteArrayInputStream(Base64.decode(content, Base64.DEFAULT)); + ByteArrayOutputStream writer = new ByteArrayOutputStream(); + byte[] buf = new byte[128]; + int bufl; + while ((bufl = ins.read(buf)) != -1) { + byte[] block; + if (buf.length == bufl) { + block = buf; + } else { + block = new byte[bufl]; + System.arraycopy(buf, 0, block, 0, bufl); + } + writer.write(cipher.doFinal(block)); + } + return new String(writer.toByteArray(), "utf-8"); + } catch (Exception e) { + return null; + } + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/utils/security/util/AESUtils.java b/lib_base/src/main/java/com/android/base/utils/security/util/AESUtils.java new file mode 100644 index 0000000..c75275d --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/security/util/AESUtils.java @@ -0,0 +1,177 @@ +/* + * Copyright 2015 Rocko (http://rocko.xyz) . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.base.utils.security.util; + +import java.io.UnsupportedEncodingException; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; + +/** + * AES 对称加密 + */ +public class AESUtils { + /** + * 算法/模式/填充 * + */ + private static final String CipherMode = "AES"; + + /** + * 创建密钥 + * + * @param password 例如:"0123456701234567" 128位 16*8
    + * 所有密钥长度不能超过16字符中文占两个。192 24; 256 32 + * @return SecretKeySpec 实例 + */ + private static SecretKeySpec generateAESKey(String password) { + byte[] data = null; + StringBuilder sb = new StringBuilder(); + sb.append(password); + while (sb.length() < 16) + sb.append("0"); + if (sb.length() > 16) + sb.setLength(16); + try { + data = sb.toString().getBytes("UTF-8"); + return new SecretKeySpec(data, "AES"); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + /** + * 加密字节数据 + * + * @param content 需要加密的字节数组 + * @param password 密钥 128 <16个字节 192 <24,256 <32个字节 + * @return 加密完后的字节数组 + */ + public static byte[] encryptData(byte[] content, String password) { + try { + SecretKeySpec key = generateAESKey(password); + Cipher cipher = Cipher.getInstance(CipherMode); + cipher.init(Cipher.ENCRYPT_MODE, key); + byte[] result = cipher.doFinal(content); + return result; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + /** + * 加密(结果为16进制字符串), UTF-8 编码 + * + * @param content 要加密的字符串 + * @param password 密钥 + * @return 加密后的16进制字符串 + */ + public static String encryptData(String content, String password) { + byte[] data = null; + try { + data = content.getBytes("UTF-8"); + } catch (Exception e) { + e.printStackTrace(); + } + data = encryptData(data, password); + String result = byte2hex(data); + return result; + } + + /** + * 解密字节数组 UTF-8 + * + * @param content + * @param password + * @return + */ + public static byte[] decryptData(byte[] content, String password) { + try { + SecretKeySpec key = generateAESKey(password); + Cipher cipher = Cipher.getInstance(CipherMode); + cipher.init(Cipher.DECRYPT_MODE, key); + return cipher.doFinal(content); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + /** + * 解密16进制的字符串为字符串 * + */ + public static String decryptData(String content, String password) { + byte[] data = null; + try { + data = hex2byte(content); + } catch (Exception e) { + e.printStackTrace(); + } + data = decryptData(data, password); + if (data == null) + return null; + String result = null; + try { + result = new String(data, "UTF-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + return result; + } + + /** + * 字节数组转成16进制字符串 + * + * @param b + * @return 16进制字符串 + */ + public static String byte2hex(byte[] b) { // 一个字节的数, + StringBuffer sb = new StringBuffer(b.length * 2); + String tmp = ""; + for (int n = 0; n < b.length; n++) { + // 整数转成十六进制表示 + tmp = (Integer.toHexString(b[n] & 0XFF)); + if (tmp.length() == 1) { + sb.append("0"); + } + sb.append(tmp); + } + return sb.toString().toUpperCase(); // 转成大写 + } + + /** + * 将hex字符串转换成字节数组 * + * + * @param inputString 16进制的字符串 + * @return 字节数组 + */ + public static byte[] hex2byte(String inputString) { + if (inputString == null || inputString.length() < 2) { + return new byte[0]; + } + inputString = inputString.toLowerCase(); + int l = inputString.length() / 2; + byte[] result = new byte[l]; + for (int i = 0; i < l; ++i) { + String tmp = inputString.substring(2 * i, 2 * i + 2); + result[i] = (byte) (Integer.parseInt(tmp, 16) & 0xFF); + } + return result; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/utils/security/util/DESUtils.java b/lib_base/src/main/java/com/android/base/utils/security/util/DESUtils.java new file mode 100644 index 0000000..dd06852 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/security/util/DESUtils.java @@ -0,0 +1,75 @@ +/* + * Copyright 2015 Rocko (http://rocko.xyz) . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.base.utils.security.util; + +import java.security.SecureRandom; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.DESKeySpec; + +/** + * Created by Administrator on 2015/11/11. + */ +public class DESUtils { + + /** + * 加密 + * + * @param bytesContent 待加密内容 + * @param key 加密的密钥 + * @return + */ + public static byte[] encrypt(byte[] bytesContent, String key) { + try { + SecureRandom random = new SecureRandom(); + DESKeySpec desKey = new DESKeySpec(key.getBytes()); + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES"); + SecretKey securekey = keyFactory.generateSecret(desKey); + Cipher cipher = Cipher.getInstance("DES"); + cipher.init(Cipher.ENCRYPT_MODE, securekey, random); + byte[] result = cipher.doFinal(bytesContent); + return result; + } catch (Throwable e) { + e.printStackTrace(); + } + return null; + } + + /** + * 解密 + * + * @param content 待解密内容 + * @param key 解密的密钥 + * @return + */ + public static byte[] decrypt(byte[] content, String key) { + try { + SecureRandom random = new SecureRandom(); + DESKeySpec desKey = new DESKeySpec(key.getBytes()); + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES"); + SecretKey securekey = keyFactory.generateSecret(desKey); + Cipher cipher = Cipher.getInstance("DES"); + cipher.init(Cipher.DECRYPT_MODE, securekey, random); + return cipher.doFinal(content); + } catch (Throwable e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/lib_base/src/main/java/com/android/base/utils/security/util/MD5Util.java b/lib_base/src/main/java/com/android/base/utils/security/util/MD5Util.java new file mode 100644 index 0000000..fad4e41 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/security/util/MD5Util.java @@ -0,0 +1,113 @@ +package com.android.base.utils.security.util; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class MD5Util { + + private final static String[] strDigits = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"}; + + private static String byteToArrayString(byte bByte) { + int iRet = bByte; + if (iRet < 0) { + iRet += 256; + } + int iD1 = iRet / 16; + int iD2 = iRet % 16; + return strDigits[iD1] + strDigits[iD2]; + } + + private static String byteToString(byte[] bByte) { + StringBuilder sBuffer = new StringBuilder(); + for (byte aBByte : bByte) { + sBuffer.append(byteToArrayString(aBByte)); + } + return sBuffer.toString(); + } + + /** + * 32 位 MD5加密 + * + * @param str 待加密的字符串 + * @return result + */ + public static String encrypt(String str) { + String result = null; + try { + result = str; + MessageDigest md = MessageDigest.getInstance("MD5"); + result = byteToString(md.digest(str.getBytes())); + } catch (NoSuchAlgorithmException ex) { + ex.printStackTrace(); + } + return result; + } + + + public static String encrypt(String algorithm, String str) { + try { + MessageDigest md = MessageDigest.getInstance(algorithm); + md.update(str.getBytes()); + StringBuilder sb = new StringBuilder(); + byte[] bytes = md.digest(); + for (int i = 0; i < bytes.length; i++) { + int b = bytes[i] & 0xFF; + if (b < 0x10) { + sb.append('0'); + } + sb.append(Integer.toHexString(b)); + } + return sb.toString(); + } catch (Exception e) { + return ""; + } + } + + public static String hashKeyForDisk(String key) { + String cacheKey; + try { + final MessageDigest mDigest = MessageDigest.getInstance("MD5"); + mDigest.update(key.getBytes()); + cacheKey = bytesToHexString(mDigest.digest()); + } catch (NoSuchAlgorithmException e) { + cacheKey = String.valueOf(key.hashCode()); + } + return cacheKey; + } + + private static String bytesToHexString(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < bytes.length; i++) { + String hex = Integer.toHexString(0xFF & bytes[i]); + if (hex.length() == 1) { + sb.append('0'); + } + sb.append(hex); + } + return sb.toString(); + } + + + public static String getMd5Value(String secret) { + try { + MessageDigest bmd5 = MessageDigest.getInstance("MD5"); + bmd5.update(secret.getBytes()); + int i; + StringBuffer buf = new StringBuffer(); + byte[] b = bmd5.digest(); + for (int offset = 0; offset < b.length; offset++) { + i = b[offset]; + if (i < 0) + i += 256; + if (i < 16) + buf.append("0"); + buf.append(Integer.toHexString(i)); + } + return buf.toString(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + return ""; + } + +} diff --git a/lib_base/src/main/java/com/android/base/utils/security/util/RSAUtils.java b/lib_base/src/main/java/com/android/base/utils/security/util/RSAUtils.java new file mode 100644 index 0000000..8b72374 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/security/util/RSAUtils.java @@ -0,0 +1,288 @@ +/* + * Copyright 2015 Rocko (http://rocko.xyz) . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.base.utils.security.util; + +import android.util.Base64; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.RSAPublicKeySpec; +import java.security.spec.X509EncodedKeySpec; + +import javax.crypto.Cipher; + + +/** + * @author Mr.Zheng + * @date 2014年8月22日 下午1:44:23 + */ +public final class RSAUtils { + private final static String KEY_PAIR = "RSA"; + private final static String CIPHER = "RSA/ECB/PKCS1Padding"; + + /** + * 随机生成RSA密钥对(默认密钥长度为1024) + * + * @return + */ + public static KeyPair generateRSAKeyPair() { + return generateRSAKeyPair(1024); + } + + /** + * 随机生成RSA密钥对 + * + * @param keyLength 密钥长度,范围:512~2048
    + * 一般1024 + * @return + */ + public static KeyPair generateRSAKeyPair(int keyLength) { + try { + KeyPairGenerator kpg = KeyPairGenerator.getInstance(KEY_PAIR); + kpg.initialize(keyLength); + return kpg.genKeyPair(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + return null; + } + } + + /** + * 用公钥加密
    + * 每次加密的字节数,不能超过密钥的长度值减去11 + * + * @param data 需加密数据的byte数据 + * @param publicKey 公钥 + * @return 加密后的byte型数据 + */ + public static byte[] encryptData(byte[] data, PublicKey publicKey) { + try { + Cipher cipher = Cipher.getInstance(CIPHER); + // 编码前设定编码方式及密钥 + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + // 传入编码数据并返回编码结果 + return cipher.doFinal(data); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + /** + * 用私钥解密 + * + * @param encryptedData 经过encryptedData()加密返回的byte数据 + * @param privateKey 私钥 + * @return + */ + public static byte[] decryptData(byte[] encryptedData, PrivateKey privateKey) { + try { + Cipher cipher = Cipher.getInstance(CIPHER); + cipher.init(Cipher.DECRYPT_MODE, privateKey); + return cipher.doFinal(encryptedData); + } catch (Exception e) { + return null; + } + } + + /** + * 通过公钥byte[](publicKey.getEncoded())将公钥还原,适用于RSA算法 + * + * @param keyBytes + * @return + * @throws NoSuchAlgorithmException + * @throws InvalidKeySpecException + */ + public static PublicKey getPublicKey(byte[] keyBytes) throws NoSuchAlgorithmException, + InvalidKeySpecException { + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_PAIR); + PublicKey publicKey = keyFactory.generatePublic(keySpec); + return publicKey; + } + + /** + * 通过私钥byte[]将公钥还原,适用于RSA算法 + * + * @param keyBytes + * @return + * @throws NoSuchAlgorithmException + * @throws InvalidKeySpecException + */ + public static PrivateKey getPrivateKey(byte[] keyBytes) throws NoSuchAlgorithmException, + InvalidKeySpecException { + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_PAIR); + PrivateKey privateKey = keyFactory.generatePrivate(keySpec); + return privateKey; + } + + /** + * 使用N、e值还原公钥 + * + * @param modulus + * @param publicExponent + * @return + * @throws NoSuchAlgorithmException + * @throws InvalidKeySpecException + */ + public static PublicKey getPublicKey(String modulus, String publicExponent) + throws NoSuchAlgorithmException, InvalidKeySpecException { + BigInteger bigIntModulus = new BigInteger(modulus); + BigInteger bigIntPrivateExponent = new BigInteger(publicExponent); + RSAPublicKeySpec keySpec = new RSAPublicKeySpec(bigIntModulus, bigIntPrivateExponent); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_PAIR); + PublicKey publicKey = keyFactory.generatePublic(keySpec); + return publicKey; + } + + /** + * 使用N、d值还原私钥 + * + * @param modulus + * @param privateExponent + * @return + * @throws NoSuchAlgorithmException + * @throws InvalidKeySpecException + */ + public static PrivateKey getPrivateKey(String modulus, String privateExponent) + throws NoSuchAlgorithmException, InvalidKeySpecException { + BigInteger bigIntModulus = new BigInteger(modulus); + BigInteger bigIntPrivateExponent = new BigInteger(privateExponent); + RSAPublicKeySpec keySpec = new RSAPublicKeySpec(bigIntModulus, bigIntPrivateExponent); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_PAIR); + PrivateKey privateKey = keyFactory.generatePrivate(keySpec); + return privateKey; + } + + /** + * 从字符串中加载公钥 + * + * @param publicKeyStr 公钥数据字符串 + * @throws Exception 加载公钥时产生的异常 + */ + public static PublicKey loadPublicKey(String publicKeyStr) throws Exception { + try { + byte[] buffer = Base64.decode(publicKeyStr, Base64.DEFAULT); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_PAIR, "BC"); + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(buffer); + return keyFactory.generatePublic(keySpec); + } catch (NoSuchAlgorithmException e) { + throw new Exception("无此算法"); + } catch (InvalidKeySpecException e) { + e.printStackTrace(); + throw new Exception("公钥非法" + e.getLocalizedMessage()); + } catch (NullPointerException e) { + throw new Exception("公钥数据为空"); + } + } + + /** + * 从字符串中加载私钥
    + * 加载时使用的是PKCS8EncodedKeySpec(PKCS#8编码的Key指令)。 + * + * @param privateKeyStr + * @return + * @throws Exception + */ + public static PrivateKey loadPrivateKey(String privateKeyStr) throws Exception { + try { + byte[] buffer = Base64.decode(privateKeyStr, Base64.DEFAULT); + // X509EncodedKeySpec keySpec = new X509EncodedKeySpec(buffer); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(buffer); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_PAIR); + return keyFactory.generatePrivate(keySpec); + } catch (NoSuchAlgorithmException e) { + throw new Exception("无此算法"); + } catch (InvalidKeySpecException e) { + throw new Exception("私钥非法"); + } catch (NullPointerException e) { + throw new Exception("私钥数据为空"); + } + } + + /** + * 从文件中输入流中加载公钥 + * + * @param in 公钥输入流 + * @throws Exception 加载公钥时产生的异常 + */ + public static PublicKey loadPublicKey(InputStream in) throws Exception { + try { + return loadPublicKey(readKey(in)); + } catch (IOException e) { + throw new Exception("公钥数据流读取错误"); + } catch (NullPointerException e) { + throw new Exception("公钥输入流为空"); + } + } + + /** + * 从文件中加载私钥 + * + * @param + * @return 是否成功 + * @throws Exception + */ + public static PrivateKey loadPrivateKey(InputStream in) throws Exception { + try { + return loadPrivateKey(readKey(in)); + } catch (IOException e) { + throw new Exception("私钥数据读取错误"); + } catch (NullPointerException e) { + throw new Exception("私钥输入流为空"); + } + } + + /** + * 读取密钥信息 + * -------------------- + * CONTENT + * -------------------- + * + * @param in + * @return + * @throws IOException + */ + private static String readKey(InputStream in) throws IOException { + BufferedReader br = new BufferedReader(new InputStreamReader(in)); + String readLine = null; + StringBuilder sb = new StringBuilder(); + while ((readLine = br.readLine()) != null) { + if (readLine.charAt(0) == '-') { + continue; + } else { + sb.append(readLine); + sb.append('\r'); + } + } + + return sb.toString(); + } + +} diff --git a/lib_base/src/main/java/com/android/base/widget/MultiStateView.java b/lib_base/src/main/java/com/android/base/widget/MultiStateView.java new file mode 100644 index 0000000..2898281 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/MultiStateView.java @@ -0,0 +1,381 @@ +package com.android.base.widget; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import com.android.base.R; +import com.android.base.app.ui.StateLayoutConfig; +import com.android.base.app.ui.StateLayoutConfig.ViewState; + +import static com.android.base.app.ui.StateLayoutConfig.CONTENT; +import static com.android.base.app.ui.StateLayoutConfig.EMPTY; +import static com.android.base.app.ui.StateLayoutConfig.ERROR; +import static com.android.base.app.ui.StateLayoutConfig.LOADING; +import static com.android.base.app.ui.StateLayoutConfig.NET_ERROR; +import static com.android.base.app.ui.StateLayoutConfig.REQUESTING; +import static com.android.base.app.ui.StateLayoutConfig.SERVER_ERROR; + +/** + * View that contains 7 different states: Content, Error/NetError/ServerError, Empty, and Loading/Request. + */ +public class MultiStateView extends FrameLayout { + + private LayoutInflater mInflater; + private SparseArray mChildren; + private View mContentView; + private boolean mDisableOperationWhenRequesting = true; + + @Nullable + private StateListener mListener; + @ViewState + private int mViewState = CONTENT; + + public MultiStateView(Context context) { + this(context, null); + } + + public MultiStateView(Context context, AttributeSet attrs) { + super(context, attrs); + init(attrs, 0); + } + + public MultiStateView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(attrs, defStyle); + } + + private void init(AttributeSet attrs, int defStyle) { + mChildren = new SparseArray<>(); + mInflater = LayoutInflater.from(getContext()); + + TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.MultiStateView, defStyle, 0); + + int loadingViewResId = a.getResourceId(R.styleable.MultiStateView_msv_loadingView, -1); + int requestingViewResId = a.getResourceId(R.styleable.MultiStateView_msv_requestingView, -1); + int emptyViewResId = a.getResourceId(R.styleable.MultiStateView_msv_emptyView, -1); + int errorViewResId = a.getResourceId(R.styleable.MultiStateView_msv_errorView, -1); + int netErrorViewResId = a.getResourceId(R.styleable.MultiStateView_msv_net_errorView, -1); + int serverErrorViewResId = a.getResourceId(R.styleable.MultiStateView_msv_net_errorView, -1); + + mChildren.put(LOADING, new ViewHolder(loadingViewResId)); + mChildren.put(REQUESTING, new ViewHolder(requestingViewResId)); + mChildren.put(EMPTY, new ViewHolder(emptyViewResId)); + mChildren.put(ERROR, new ViewHolder(errorViewResId)); + mChildren.put(NET_ERROR, new ViewHolder(netErrorViewResId)); + mChildren.put(SERVER_ERROR, new ViewHolder(serverErrorViewResId)); + + mDisableOperationWhenRequesting = a.getBoolean(R.styleable.MultiStateView_msv_disable_when_requesting, true); + + ensureInitState(a.getInt(R.styleable.MultiStateView_msv_viewState, CONTENT)); + + a.recycle(); + } + + private void ensureInitState(int viewState) { + /* + + + + + + + + */ + switch (viewState) { + case 2: + mViewState = LOADING; + break; + case 3: + mViewState = EMPTY; + break; + case 4: + mViewState = ERROR; + break; + case 5: + mViewState = NET_ERROR; + break; + case 6: + mViewState = SERVER_ERROR; + break; + case 7: + mViewState = REQUESTING; + break; + case 1: + default: + mViewState = CONTENT; + break; + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (mContentView == null) { + throw new IllegalArgumentException("Content view is not defined"); + } + setView(); + } + + /* All of the addView methods have been overridden so that it can obtain the content view via XML + It is NOT recommended to add views into MultiStateView via the addView methods, but rather use + any of the setViewForState methods to set views for their given ContentViewState accordingly */ + @Override + public void addView(View child) { + if (isValidContentView(child)) { + mContentView = child; + } + super.addView(child); + } + + @Override + public void addView(View child, int index) { + if (isValidContentView(child)) { + mContentView = child; + } + super.addView(child, index); + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + if (isValidContentView(child)) { + mContentView = child; + } + super.addView(child, index, params); + } + + @Override + public void addView(View child, ViewGroup.LayoutParams params) { + if (isValidContentView(child)) { + mContentView = child; + } + super.addView(child, params); + } + + @Override + public void addView(View child, int width, int height) { + if (isValidContentView(child)) { + mContentView = child; + } + super.addView(child, width, height); + } + + @Override + protected boolean addViewInLayout(View child, int index, ViewGroup.LayoutParams params) { + if (isValidContentView(child)) { + mContentView = child; + } + return super.addViewInLayout(child, index, params); + } + + @Override + protected boolean addViewInLayout(View child, int index, ViewGroup.LayoutParams params, boolean preventRequestLayout) { + if (isValidContentView(child)) { + mContentView = child; + } + return super.addViewInLayout(child, index, params, preventRequestLayout); + } + + /** + * Returns the {@link View} associated with the {@link ViewState} + * + * @param state The {@link ViewState} with to return the view for + * @return The {@link View} associated with the {@link ViewState}, null if no view is present + */ + @Nullable + @SuppressWarnings("unused") + public View getView(@ViewState int state) { + if (state == StateLayoutConfig.BLANK) { + return null; + } + return ensureStateView(state); + } + + private View ensureStateView(@ViewState int state) { + ViewHolder viewHolder = mChildren.get(state); + if (viewHolder == null) { + throw new NullPointerException("the ViewHolder is null, state = " + state); + } + + if (viewHolder.mView != null) { + return viewHolder.mView; + } + + int viewLayoutId = viewHolder.mViewLayoutId; + + if (viewLayoutId > 0) { + View newView = mInflater.inflate(viewLayoutId, this, false); + newView.setTag(R.id.base_tag_multi_state_view, state); + addView(newView, newView.getLayoutParams()); + if (mListener != null) { + mListener.onStateInflated(state, newView); + } + if (mViewState != state) { + newView.setVisibility(GONE); + } + viewHolder.mView = newView; + return newView; + } else { + throw new IllegalStateException("the view layout id is invalidate, layout id = " + viewLayoutId + " state = " + state); + } + } + + /** + * Returns the current {@link ViewState} + * + * @return ContentViewState + */ + @ViewState + @SuppressWarnings("unused") + public int getViewState() { + return mViewState; + } + + /** + * Sets the current {@link ViewState} + * + * @param state The {@link ViewState} to set {@link MultiStateView} to + */ + public void setViewState(@ViewState int state) { + if (state != mViewState) { + mViewState = state; + setView(); + if (mListener != null) { + mListener.onStateChanged(mViewState); + } + } + } + + /** + * Shows the {@link View} based on the {@link ViewState} + */ + private void setView() { + if (mViewState == StateLayoutConfig.BLANK) { + int size = mChildren.size(); + View view; + for (int i = 0; i < size; i++) { + view = mChildren.valueAt(i).mView; + if (view != null) { + view.setVisibility(View.GONE); + } + } + return; + } + + View curStateView = ensureStateView(mViewState); + int size = mChildren.size(); + for (int i = 0; i < size; i++) { + ViewHolder viewHolder = mChildren.valueAt(i); + if (viewHolder.mView == null) { + continue; + } + if (viewHolder.mView != curStateView) { + if (mViewState == REQUESTING && viewHolder.mView != mContentView) { + viewHolder.mView.setVisibility(GONE); + } else { + viewHolder.mView.setVisibility(GONE); + } + } + } + curStateView.setVisibility(VISIBLE); + + if (mViewState == REQUESTING) { + curStateView.setOnTouchListener(mDisableOperationWhenRequesting ? NO_ACTION_TOUCH_LISTENER : null); + } + + } + + /** + * Checks if the given {@link View} is valid for the Content View + * + * @param view The {@link View} to check + */ + private boolean isValidContentView(View view) { + if (mContentView != null && mContentView != view) { + return false; + } + Object tag = view.getTag(R.id.base_tag_multi_state_view); + if (tag == null) { + mChildren.put(CONTENT, new ViewHolder(view, 0)); + return true; + } + if (tag instanceof Integer) { + int viewTag = (Integer) tag; + if (viewTag != LOADING && viewTag != EMPTY && viewTag != ERROR && viewTag != NET_ERROR && viewTag != SERVER_ERROR && viewTag != REQUESTING) { + mChildren.put(CONTENT, new ViewHolder(view, 0)); + return true; + } else { + return false; + } + } + mChildren.put(CONTENT, new ViewHolder(view, 0)); + return true; + } + + public void setDisableOperationWhenRequesting(boolean disableOperationWhenRequesting) { + mDisableOperationWhenRequesting = disableOperationWhenRequesting; + ViewHolder viewHolder = mChildren.get(LOADING); + if (viewHolder == null || viewHolder.mView == null) { + return; + } + if (disableOperationWhenRequesting) { + if (mViewState == REQUESTING) { + viewHolder.mView.setOnTouchListener(NO_ACTION_TOUCH_LISTENER); + } + } else { + viewHolder.mView.setOnTouchListener(null); + } + } + + /** + * Sets the {@link StateListener} for the view + * + * @param listener The {@link StateListener} that will receive callbacks + */ + public void setStateListener(StateListener listener) { + mListener = listener; + } + + public interface StateListener { + /** + * Callback for when the {@link ViewState} has changed + * + * @param viewState The {@link ViewState} that was switched to + */ + void onStateChanged(@ViewState int viewState); + + /** + * Callback for when a {@link ViewState} has been inflated + * + * @param viewState The {@link ViewState} that was inflated + * @param view The {@link View} that was inflated + */ + void onStateInflated(@ViewState int viewState, @NonNull View view); + } + + @SuppressLint("ClickableViewAccessibility") + private static final OnTouchListener NO_ACTION_TOUCH_LISTENER = (v, event) -> true; + + private class ViewHolder { + private View mView; + private int mViewLayoutId; + + public ViewHolder(int viewLayoutId) { + this(null, viewLayoutId); + } + + public ViewHolder(View view, int viewLayoutId) { + mView = view; + mViewLayoutId = viewLayoutId; + } + } + +} diff --git a/lib_base/src/main/java/com/android/base/widget/ScrollChildSwipeRefreshLayout.java b/lib_base/src/main/java/com/android/base/widget/ScrollChildSwipeRefreshLayout.java new file mode 100644 index 0000000..8ca7795 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/ScrollChildSwipeRefreshLayout.java @@ -0,0 +1,87 @@ +package com.android.base.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.support.v4.view.ViewCompat; +import android.support.v4.widget.SwipeRefreshLayout; +import android.util.AttributeSet; +import android.view.View; + +import com.android.base.R; + + +/** + * Extends {@link SwipeRefreshLayout} to support non-direct descendant scrolling views. + *

    + * {@link SwipeRefreshLayout} works as expected when a scroll view is a direct child: it triggers + * the refresh only when the view is on top. This class adds a way (@link #setScrollUpChild} to + * define which view controls this behavior. + */ +public class ScrollChildSwipeRefreshLayout extends SwipeRefreshLayout { + + private View mScrollUpChild; + private int mTargetId; + private boolean mRestoreRefreshStatus; + + public ScrollChildSwipeRefreshLayout(Context context) { + this(context, null); + } + + public ScrollChildSwipeRefreshLayout(Context context, AttributeSet attrs) { + super(context, attrs); + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ScrollChildSwipeRefreshLayout); + mTargetId = typedArray.getResourceId(R.styleable.ScrollChildSwipeRefreshLayout_srl_target_id, 0); + int colorSchemeArrayId = typedArray.getResourceId(R.styleable.ScrollChildSwipeRefreshLayout_srl_color_scheme, 0); + mRestoreRefreshStatus = typedArray.getBoolean(R.styleable.ScrollChildSwipeRefreshLayout_srl_restore_refresh_status, true); + typedArray.recycle(); + if (colorSchemeArrayId != 0) { + setColors(colorSchemeArrayId); + } + } + + private void setColors(int colorSchemeArrayId) { + TypedArray colorsTypeArray = getResources().obtainTypedArray(colorSchemeArrayId); + int indexCount = colorsTypeArray.length(); + if (indexCount != 0) { + int colors[] = new int[indexCount]; + for (int i = 0; i < indexCount; i++) { + colors[i] = colorsTypeArray.getColor(i, Color.BLACK); + } + setColorSchemeColors(colors); + } + colorsTypeArray.recycle(); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + if (mTargetId != 0) { + mScrollUpChild = findViewById(mTargetId); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (mRestoreRefreshStatus && isRefreshing()) { + //show animation + setRefreshing(false); + setRefreshing(true); + } + } + + @Override + public boolean canChildScrollUp() { + if (mScrollUpChild != null) { + return ViewCompat.canScrollVertically(mScrollUpChild, -1); + } + return super.canChildScrollUp(); + } + + @SuppressWarnings("unused") + public void setScrollUpChild(View view) { + mScrollUpChild = view; + } + +} diff --git a/lib_base/src/main/java/com/android/base/widget/SimpleLayout.java b/lib_base/src/main/java/com/android/base/widget/SimpleLayout.java new file mode 100644 index 0000000..a6fd29c --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/SimpleLayout.java @@ -0,0 +1,89 @@ +package com.android.base.widget; + +import android.content.Context; +import android.os.Build; +import android.support.annotation.RequiresApi; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; + +/** + * a SimpleLayout look like FrameLayout + */ +public class SimpleLayout extends ViewGroup { + + public SimpleLayout(Context context) { + super(context); + } + + public SimpleLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SimpleLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public SimpleLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + int maxHeight = 0; + int maxWidth = 0; + int childState = 0; + + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); + final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); + maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); + maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); + childState = combineMeasuredStates(childState, child.getMeasuredState()); + } + } + + maxWidth += getPaddingLeft() + getPaddingRight(); + maxHeight += getPaddingTop() + getPaddingBottom(); + + maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); + maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); + + setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), + resolveSizeAndState(maxHeight, heightMeasureSpec, + childState << MEASURED_HEIGHT_STATE_SHIFT)); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + final MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams(); + child.layout(getPaddingLeft() + params.leftMargin, + getPaddingTop() + params.topMargin, + getPaddingRight() + child.getMeasuredWidth() + params.rightMargin, + getPaddingBottom() + child.getMeasuredHeight() + params.bottomMargin); + } + } + + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new MarginLayoutParams(getContext(), attrs); + } + + @Override + protected LayoutParams generateDefaultLayoutParams() { + return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + } + + @Override + protected LayoutParams generateLayoutParams(LayoutParams p) { + return new MarginLayoutParams(p); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/widget/SimpleMultiStateView.java b/lib_base/src/main/java/com/android/base/widget/SimpleMultiStateView.java new file mode 100644 index 0000000..3f47d7e --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/SimpleMultiStateView.java @@ -0,0 +1,145 @@ +package com.android.base.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.view.View; + +import com.android.base.R; +import com.android.base.app.ui.StateLayout; +import com.android.base.app.ui.StateLayoutConfig; +import com.android.base.utils.common.StringChecker; + +import timber.log.Timber; + +import static com.android.base.app.ui.StateLayoutConfig.CONTENT; +import static com.android.base.app.ui.StateLayoutConfig.EMPTY; +import static com.android.base.app.ui.StateLayoutConfig.ERROR; +import static com.android.base.app.ui.StateLayoutConfig.LOADING; +import static com.android.base.app.ui.StateLayoutConfig.ViewState; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-04-21 10:21 + */ +public class SimpleMultiStateView extends MultiStateView implements StateLayout { + + private StateActionProcessor mStateActionProcessor; + private StateListener mStateListener; + + public SimpleMultiStateView(Context context) { + this(context, null); + } + + public SimpleMultiStateView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SimpleMultiStateView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + setListener(); + + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SimpleMultiStateView, defStyle, defStyle); + + initProcessor(typedArray); + + mStateActionProcessor.onInitialize(this); + mStateActionProcessor.onParseAttrs(typedArray); + + typedArray.recycle(); + } + + private void initProcessor(TypedArray typedArray) { + String processorPath = typedArray.getString(R.styleable.SimpleMultiStateView_msv_state_processor); + + if (!StringChecker.isEmpty(processorPath)) { + try { + Class processorClass = Class.forName(processorPath); + mStateActionProcessor = (StateActionProcessor) processorClass.newInstance(); + } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) { + e.printStackTrace(); + Timber.e("initProcessor() called can not instance processor: " + processorPath); + } + } + + if (mStateActionProcessor == null) { + mStateActionProcessor = new StateActionProcessor(); + } + } + + private void setListener() { + super.setStateListener(new StateListener() { + @Override + public void onStateChanged(@StateLayoutConfig.ViewState int viewState) { + if (mStateListener != null) { + mStateListener.onStateChanged(viewState); + } + } + + @Override + public void onStateInflated(@StateLayoutConfig.ViewState int viewState, @NonNull android.view.View view) { + if (mStateListener != null) { + mStateListener.onStateInflated(viewState, view); + } + processStateInflated(viewState, view); + } + }); + } + + private void processStateInflated(@ViewState int viewState, @NonNull View view) { + mStateActionProcessor.processStateInflated(viewState, view); + } + + @Override + public void setStateListener(StateListener listener) { + mStateListener = listener; + } + + @Override + public void showContentLayout() { + setViewState(CONTENT); + } + + @Override + public void showLoadingLayout() { + setViewState(LOADING); + } + + @Override + public void showEmptyLayout() { + setViewState(EMPTY); + } + + @Override + public void showErrorLayout() { + setViewState(ERROR); + } + + @Override + public void showRequesting() { + setViewState(StateLayoutConfig.REQUESTING); + } + + @Override + public void showBlank() { + setViewState(StateLayoutConfig.BLANK); + } + + @Override + public void showNetErrorLayout() { + setViewState(StateLayoutConfig.NET_ERROR); + } + + @Override + public void showServerErrorLayout() { + setViewState(StateLayoutConfig.SERVER_ERROR); + } + + @Override + public StateLayoutConfig getStateLayoutConfig() { + return mStateActionProcessor.getStateLayoutConfigImpl(); + } + +} diff --git a/lib_base/src/main/java/com/android/base/widget/StateActionProcessor.java b/lib_base/src/main/java/com/android/base/widget/StateActionProcessor.java new file mode 100644 index 0000000..1001b67 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/StateActionProcessor.java @@ -0,0 +1,197 @@ +package com.android.base.widget; + +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.support.annotation.DrawableRes; +import android.support.annotation.NonNull; +import android.support.v4.content.ContextCompat; +import android.text.TextUtils; +import android.view.View; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.base.R; +import com.android.base.app.ui.CommonId; +import com.android.base.app.ui.OnRetryActionListener; +import com.android.base.app.ui.StateLayoutConfig; + +import static com.android.base.app.ui.StateLayoutConfig.EMPTY; +import static com.android.base.app.ui.StateLayoutConfig.ERROR; +import static com.android.base.app.ui.StateLayoutConfig.NET_ERROR; +import static com.android.base.app.ui.StateLayoutConfig.SERVER_ERROR; + + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-05-17 17:08 + */ +public class StateActionProcessor implements StateProcessor { + + private OnRetryActionListener mOnRetryActionListener; + + private ViewInfo mEmptyViewInfo; + private ViewInfo mErrorViewInfo; + private ViewInfo mNetErrorViewInfo; + private ViewInfo mServerErrorViewInfo; + + private SimpleMultiStateView mSimpleMultiStateView; + + @Override + public void onInitialize(SimpleMultiStateView simpleMultiStateView) { + mSimpleMultiStateView = simpleMultiStateView; + } + + @Override + public void onParseAttrs(TypedArray typedArray) { + mErrorViewInfo = new ViewInfo(ERROR); + mErrorViewInfo.mDrawable = typedArray.getDrawable(R.styleable.SimpleMultiStateView_msv_errorImg); + mErrorViewInfo.mMessage = typedArray.getText(R.styleable.SimpleMultiStateView_msv_errorText); + mErrorViewInfo.mActionText = typedArray.getText(R.styleable.SimpleMultiStateView_msv_errorAction); + + mEmptyViewInfo = new ViewInfo(EMPTY); + mEmptyViewInfo.mDrawable = typedArray.getDrawable(R.styleable.SimpleMultiStateView_msv_emptyImg); + mEmptyViewInfo.mMessage = typedArray.getText(R.styleable.SimpleMultiStateView_msv_emptyText); + mEmptyViewInfo.mActionText = typedArray.getText(R.styleable.SimpleMultiStateView_msv_emptyAction); + + mNetErrorViewInfo = new ViewInfo(NET_ERROR); + mNetErrorViewInfo.mDrawable = typedArray.getDrawable(R.styleable.SimpleMultiStateView_msv_net_errorImg); + mNetErrorViewInfo.mMessage = typedArray.getText(R.styleable.SimpleMultiStateView_msv_net_errorText); + mNetErrorViewInfo.mActionText = typedArray.getText(R.styleable.SimpleMultiStateView_msv_net_errorAction); + + mServerErrorViewInfo = new ViewInfo(SERVER_ERROR); + mServerErrorViewInfo.mDrawable = typedArray.getDrawable(R.styleable.SimpleMultiStateView_msv_server_errorImg); + mServerErrorViewInfo.mMessage = typedArray.getText(R.styleable.SimpleMultiStateView_msv_server_errorText); + mServerErrorViewInfo.mActionText = typedArray.getText(R.styleable.SimpleMultiStateView_msv_server_errorAction); + } + + @Override + public void processStateInflated(@StateLayoutConfig.ViewState int viewState, @NonNull View view) { + if (viewState == ERROR) { + mErrorViewInfo.setStateView(view); + } else if (viewState == EMPTY) { + mEmptyViewInfo.setStateView(view); + } else if (viewState == NET_ERROR) { + mNetErrorViewInfo.setStateView(view); + } else if (viewState == SERVER_ERROR) { + mServerErrorViewInfo.setStateView(view); + } + } + + private StateActionProcessor.ViewInfo getViewInfoForState(@StateLayoutConfig.ViewState int viewState) { + if (viewState == EMPTY) { + return mEmptyViewInfo; + } else if (viewState == ERROR) { + return mErrorViewInfo; + } else if (viewState == NET_ERROR) { + return mNetErrorViewInfo; + } else if (viewState == SERVER_ERROR) { + return mServerErrorViewInfo; + } + throw new IllegalArgumentException("no viewInfo in this state"); + } + + @Override + public StateLayoutConfig getStateLayoutConfigImpl() { + return mStateLayoutConfig; + } + + class ViewInfo { + + private final int mState; + private Drawable mDrawable; + private CharSequence mMessage; + private CharSequence mActionText; + private View mStateView; + private TextView mMessageTv; + private ImageView mIconTv; + private Button mActionBtn; + + private ViewInfo(int state) { + mState = state; + } + + void setStateView(View stateView) { + mStateView = stateView; + mIconTv = mStateView.findViewById(CommonId.RETRY_IV_ID); + mMessageTv = mStateView.findViewById(CommonId.RETRY_TV_ID); + mActionBtn = mStateView.findViewById(CommonId.RETRY_BTN_ID); + mActionBtn.setOnClickListener(v -> { + if (mOnRetryActionListener != null) { + mOnRetryActionListener.onRetry(mState); + } + }); + setActionText(mActionText); + setMessage(mMessage); + setDrawable(mDrawable); + } + + void setDrawable(Drawable drawable) { + mDrawable = drawable; + if (mIconTv != null) { + mIconTv.setImageDrawable(drawable); + } + } + + void setMessage(CharSequence message) { + mMessage = message; + if (mMessageTv != null) { + mMessageTv.setText(mMessage); + } + } + + void setActionText(CharSequence actionText) { + mActionText = actionText; + if (mActionBtn == null) { + return; + } + mActionBtn.setText(mActionText); + if (TextUtils.isEmpty(mActionText)) { + mActionBtn.setVisibility(View.GONE); + } else { + mActionBtn.setVisibility(View.VISIBLE); + } + } + } + + private StateLayoutConfig mStateLayoutConfig = new StateLayoutConfig() { + + @Override + public StateLayoutConfig setStateMessage(@ViewState int state, CharSequence message) { + getViewInfoForState(state).setMessage(message); + return this; + } + + @Override + public StateLayoutConfig setStateIcon(@ViewState int state, Drawable drawable) { + getViewInfoForState(state).setDrawable(drawable); + return this; + } + + @Override + public StateLayoutConfig setStateIcon(@ViewState int state, @DrawableRes int drawableId) { + getViewInfoForState(state).setDrawable(ContextCompat.getDrawable(mSimpleMultiStateView.getContext(), drawableId)); + return this; + } + + @Override + public StateLayoutConfig setStateAction(@ViewState int state, CharSequence actionText) { + getViewInfoForState(state).setActionText(actionText); + return this; + } + + @Override + public StateLayoutConfig setStateRetryListener(OnRetryActionListener retryActionListener) { + mOnRetryActionListener = retryActionListener; + return this; + } + + @Override + public StateLayoutConfig disableOperationWhenRequesting(boolean disable) { + mSimpleMultiStateView.setDisableOperationWhenRequesting(disable); + return this; + } + }; + +} diff --git a/lib_base/src/main/java/com/android/base/widget/StateProcessor.java b/lib_base/src/main/java/com/android/base/widget/StateProcessor.java new file mode 100644 index 0000000..f094a73 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/StateProcessor.java @@ -0,0 +1,24 @@ +package com.android.base.widget; + +import android.content.res.TypedArray; +import android.support.annotation.NonNull; +import android.view.View; + +import com.android.base.app.ui.StateLayoutConfig; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-05-17 17:25 + */ +interface StateProcessor { + + void onInitialize(SimpleMultiStateView simpleMultiStateView); + + void onParseAttrs(TypedArray typedArray); + + void processStateInflated(@StateLayoutConfig.ViewState int viewState, @NonNull View view); + + StateLayoutConfig getStateLayoutConfigImpl(); + +} diff --git a/lib_base/src/main/java/com/android/base/widget/pulltozoom/NestedScrollView.java b/lib_base/src/main/java/com/android/base/widget/pulltozoom/NestedScrollView.java new file mode 100644 index 0000000..21dde69 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/pulltozoom/NestedScrollView.java @@ -0,0 +1,1937 @@ +package com.android.base.widget.pulltozoom; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.v4.view.AccessibilityDelegateCompat; +import android.support.v4.view.InputDeviceCompat; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.NestedScrollingChild; +import android.support.v4.view.NestedScrollingChildHelper; +import android.support.v4.view.NestedScrollingParent; +import android.support.v4.view.NestedScrollingParentHelper; +import android.support.v4.view.ScrollingView; +import android.support.v4.view.VelocityTrackerCompat; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.accessibility.AccessibilityEventCompat; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.support.v4.view.accessibility.AccessibilityRecordCompat; +import android.support.v4.widget.EdgeEffectCompat; +import android.support.v4.widget.ScrollerCompat; +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; +import android.view.FocusFinder; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; +import android.view.animation.AnimationUtils; +import android.widget.FrameLayout; +import android.widget.ScrollView; + +import java.util.List; + +class NestedScrollView extends FrameLayout implements NestedScrollingParent, NestedScrollingChild, ScrollingView { + + static final int ANIMATED_SCROLL_GAP = 250; + static final float MAX_SCROLL_FACTOR = 0.5f; + + private static final String TAG = NestedScrollView.class.getSimpleName(); + + /** + * Interface definition for a callback to be invoked when the scroll + * X or Y positions of a view change. + *

    + *

    This version of the interface works on all versions of Android, back to API v4.

    + * + * @see #setOnScrollChangeListener(OnScrollChangeListener) + */ + public interface OnScrollChangeListener { + /** + * Called when the scroll position of a view changes. + * + * @param v The view whose scroll position has changed. + * @param scrollX Current horizontal scroll origin. + * @param scrollY Current vertical scroll origin. + * @param oldScrollX Previous horizontal scroll origin. + * @param oldScrollY Previous vertical scroll origin. + */ + void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY); + } + + private long mLastScroll; + + private final Rect mTempRect = new Rect(); + private ScrollerCompat mScroller; + private EdgeEffectCompat mEdgeGlowTop; + private EdgeEffectCompat mEdgeGlowBottom; + + /** + * Position of the last motion event. + */ + private int mLastMotionY; + + /** + * True when the layout has changed but the traversal has not come through yet. + * Ideally the view hierarchy would keep track of this for us. + */ + private boolean mIsLayoutDirty = true; + private boolean mIsLaidOut = false; + + /** + * The child to give focus to in the event that a child has requested focus while the + * layout is dirty. This prevents the scroll from being wrong if the child has not been + * laid out before requesting focus. + */ + private View mChildToScrollTo = null; + + /** + * True if the subscribeUser is currently dragging this ScrollView around. This is + * not the same as 'is being flinged', which can be checked by + * mScroller.isFinished() (flinging begins when the subscribeUser lifts his finger). + */ + private boolean mIsBeingDragged = false; + + /** + * Determines speed during touch scrolling + */ + private VelocityTracker mVelocityTracker; + + /** + * When set to true, the scroll view measure its child to make it fill the currently + * visible area. + */ + private boolean mFillViewport; + + /** + * Whether arrow scrolling is animated. + */ + private boolean mSmoothScrollingEnabled = true; + + private int mTouchSlop; + private int mMinimumVelocity; + private int mMaximumVelocity; + + /** + * ID of the active pointer. This is used to retain consistency during + * drags/flings if multiple pointers are used. + */ + private int mActivePointerId = INVALID_POINTER; + + /** + * Used during scrolling to retrieve the new offset within the window. + */ + private final int[] mScrollOffset = new int[2]; + private final int[] mScrollConsumed = new int[2]; + private int mNestedYOffset; + + /** + * Sentinel value for no current active pointer. + * Used by {@link #mActivePointerId}. + */ + private static final int INVALID_POINTER = -1; + + private SavedState mSavedState; + + private static final AccessibilityDelegate ACCESSIBILITY_DELEGATE = new AccessibilityDelegate(); + + private static final int[] SCROLLVIEW_STYLEABLE = new int[]{ + android.R.attr.fillViewport + }; + + private final NestedScrollingParentHelper mParentHelper; + private final NestedScrollingChildHelper mChildHelper; + + private float mVerticalScrollFactor; + + private OnScrollChangeListener mOnScrollChangeListener; + + public NestedScrollView(Context context) { + this(context, null); + } + + public NestedScrollView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public NestedScrollView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initScrollView(); + + final TypedArray a = context.obtainStyledAttributes( + attrs, SCROLLVIEW_STYLEABLE, defStyleAttr, 0); + + setFillViewport(a.getBoolean(0, false)); + + a.recycle(); + + mParentHelper = new NestedScrollingParentHelper(this); + mChildHelper = new NestedScrollingChildHelper(this); + + // ...because why else would you be using this widget? + setNestedScrollingEnabled(true); + + ViewCompat.setAccessibilityDelegate(this, ACCESSIBILITY_DELEGATE); + } + + // NestedScrollingChild + + @Override + public void setNestedScrollingEnabled(boolean enabled) { + mChildHelper.setNestedScrollingEnabled(enabled); + } + + @Override + public boolean isNestedScrollingEnabled() { + return mChildHelper.isNestedScrollingEnabled(); + } + + @Override + public boolean startNestedScroll(int axes) { + return mChildHelper.startNestedScroll(axes); + } + + @Override + public void stopNestedScroll() { + mChildHelper.stopNestedScroll(); + } + + @Override + public boolean hasNestedScrollingParent() { + return mChildHelper.hasNestedScrollingParent(); + } + + @Override + public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, + int dyUnconsumed, int[] offsetInWindow) { + return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, + offsetInWindow); + } + + @Override + public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { + return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); + } + + @Override + public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { + return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); + } + + @Override + public boolean dispatchNestedPreFling(float velocityX, float velocityY) { + return mChildHelper.dispatchNestedPreFling(velocityX, velocityY); + } + + // NestedScrollingParent + + @Override + public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { + return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; + } + + @Override + public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) { + mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes); + startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); + } + + @Override + public void onStopNestedScroll(View target) { + mParentHelper.onStopNestedScroll(target); + stopNestedScroll(); + } + + @Override + public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, + int dyUnconsumed) { + final int oldScrollY = getScrollY(); + scrollBy(0, dyUnconsumed); + final int myConsumed = getScrollY() - oldScrollY; + final int myUnconsumed = dyUnconsumed - myConsumed; + dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null); + } + + @Override + public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { + dispatchNestedPreScroll(dx, dy, consumed, null); + } + + @Override + public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { + if (!consumed) { + flingWithNestedDispatch((int) velocityY); + return true; + } + return false; + } + + @Override + public boolean onNestedPreFling(View target, float velocityX, float velocityY) { + return dispatchNestedPreFling(velocityX, velocityY); + } + + @Override + public int getNestedScrollAxes() { + return mParentHelper.getNestedScrollAxes(); + } + + // ScrollView import + + public boolean shouldDelayChildPressedState() { + return true; + } + + @Override + protected float getTopFadingEdgeStrength() { + if (getChildCount() == 0) { + return 0.0f; + } + + final int length = getVerticalFadingEdgeLength(); + final int scrollY = getScrollY(); + if (scrollY < length) { + return scrollY / (float) length; + } + + return 1.0f; + } + + @Override + protected float getBottomFadingEdgeStrength() { + if (getChildCount() == 0) { + return 0.0f; + } + + final int length = getVerticalFadingEdgeLength(); + final int bottomEdge = getHeight() - getPaddingBottom(); + final int span = getChildAt(0).getBottom() - getScrollY() - bottomEdge; + if (span < length) { + return span / (float) length; + } + + return 1.0f; + } + + /** + * @return The maximum amount this scroll view will scroll in response to + * an arrow event. + */ + public int getMaxScrollAmount() { + return (int) (MAX_SCROLL_FACTOR * getHeight()); + } + + private void initScrollView() { + mScroller = ScrollerCompat.create(getContext(), null); + setFocusable(true); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + setWillNotDraw(false); + final ViewConfiguration configuration = ViewConfiguration.get(getContext()); + mTouchSlop = configuration.getScaledTouchSlop(); + mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + } + + @Override + public void addView(View child) { + if (getChildCount() > 0) { + throw new IllegalStateException("ScrollView can host only one direct child"); + } + + super.addView(child); + } + + @Override + public void addView(View child, int index) { + if (getChildCount() > 0) { + throw new IllegalStateException("ScrollView can host only one direct child"); + } + + super.addView(child, index); + } + + @Override + public void addView(View child, ViewGroup.LayoutParams params) { + if (getChildCount() > 0) { + throw new IllegalStateException("ScrollView can host only one direct child"); + } + + super.addView(child, params); + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + if (getChildCount() > 0) { + throw new IllegalStateException("ScrollView can host only one direct child"); + } + + super.addView(child, index, params); + } + + /** + * Register a callback to be invoked when the scroll X or Y positions of + * this view change. + *

    This version of the method works on all versions of Android, back to API v4.

    + * + * @param l The listener to notify when the scroll X or Y position changes. + * @see View#getScrollX() + * @see View#getScrollY() + */ + public void setOnScrollChangeListener(OnScrollChangeListener l) { + mOnScrollChangeListener = l; + } + + /** + * @return Returns true this ScrollView can be scrolled + */ + private boolean canScroll() { + View child = getChildAt(0); + if (child != null) { + int childHeight = child.getHeight(); + return getHeight() < childHeight + getPaddingTop() + getPaddingBottom(); + } + return false; + } + + /** + * Indicates whether this ScrollView's content is stretched to fill the viewport. + * + * @return True if the content fills the viewport, false otherwise. + * @attr name android:fillViewport + */ + public boolean isFillViewport() { + return mFillViewport; + } + + /** + * Set whether this ScrollView should stretch its content height to fill the viewport or not. + * + * @param fillViewport True to stretch the content's height to the viewport's + * boundaries, false otherwise. + * @attr name android:fillViewport + */ + public void setFillViewport(boolean fillViewport) { + if (fillViewport != mFillViewport) { + mFillViewport = fillViewport; + requestLayout(); + } + } + + /** + * @return Whether arrow scrolling will animate its transition. + */ + public boolean isSmoothScrollingEnabled() { + return mSmoothScrollingEnabled; + } + + /** + * Set whether arrow scrolling will animate its transition. + * + * @param smoothScrollingEnabled whether arrow scrolling will animate its transition + */ + public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) { + mSmoothScrollingEnabled = smoothScrollingEnabled; + } + + @Override + protected void onScrollChanged(int l, int t, int oldl, int oldt) { + super.onScrollChanged(l, t, oldl, oldt); + + if (mOnScrollChangeListener != null) { + mOnScrollChangeListener.onScrollChange(this, l, t, oldl, oldt); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + if (!mFillViewport) { + return; + } + + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + if (heightMode == MeasureSpec.UNSPECIFIED) { + return; + } + + if (getChildCount() > 0) { + final View child = getChildAt(0); + int height = getMeasuredHeight(); + if (child.getMeasuredHeight() < height) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, + getPaddingLeft() + getPaddingRight(), lp.width); + height -= getPaddingTop(); + height -= getPaddingBottom(); + int childHeightMeasureSpec = + MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + } + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // Let the focused view and/or our descendants get the key first + return super.dispatchKeyEvent(event) || executeKeyEvent(event); + } + + /** + * You can call this function yourself to have the scroll view perform + * scrolling from a key event, just as if the event had been dispatched to + * it by the view hierarchy. + * + * @param event The key event to execute. + * @return Return true if the event was handled, else false. + */ + public boolean executeKeyEvent(KeyEvent event) { + mTempRect.setEmpty(); + + if (!canScroll()) { + if (isFocused() && event.getKeyCode() != KeyEvent.KEYCODE_BACK) { + View currentFocused = findFocus(); + if (currentFocused == this) currentFocused = null; + View nextFocused = FocusFinder.getInstance().findNextFocus(this, + currentFocused, View.FOCUS_DOWN); + return nextFocused != null + && nextFocused != this + && nextFocused.requestFocus(View.FOCUS_DOWN); + } + return false; + } + + boolean handled = false; + if (event.getAction() == KeyEvent.ACTION_DOWN) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_DPAD_UP: + if (!event.isAltPressed()) { + handled = arrowScroll(View.FOCUS_UP); + } else { + handled = fullScroll(View.FOCUS_UP); + } + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (!event.isAltPressed()) { + handled = arrowScroll(View.FOCUS_DOWN); + } else { + handled = fullScroll(View.FOCUS_DOWN); + } + break; + case KeyEvent.KEYCODE_SPACE: + pageScroll(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN); + break; + } + } + + return handled; + } + + private boolean inChild(int x, int y) { + if (getChildCount() > 0) { + final int scrollY = getScrollY(); + final View child = getChildAt(0); + return !(y < child.getTop() - scrollY + || y >= child.getBottom() - scrollY + || x < child.getLeft() + || x >= child.getRight()); + } + return false; + } + + private void initOrResetVelocityTracker() { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } else { + mVelocityTracker.clear(); + } + } + + private void initVelocityTrackerIfNotExists() { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + } + + private void recycleVelocityTracker() { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + @Override + public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { + if (disallowIntercept) { + recycleVelocityTracker(); + } + super.requestDisallowInterceptTouchEvent(disallowIntercept); + } + + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + /* + * This method JUST determines whether we want to intercept the motion. + * If we return true, onMotionEvent will be called and we do the actual + * scrolling there. + */ + + /* + * Shortcut the most recurring case: the subscribeUser is in the dragging + * state and he is moving his finger. We want to intercept this + * motion. + */ + final int action = ev.getAction(); + if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { + return true; + } + + switch (action & MotionEventCompat.ACTION_MASK) { + case MotionEvent.ACTION_MOVE: { + /* + * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check + * whether the subscribeUser has moved far enough from his original down touch. + */ + + /* + * Locally do absolute value. mLastMotionY is set to the y value + * of the down event. + */ + final int activePointerId = mActivePointerId; + if (activePointerId == INVALID_POINTER) { + // If we don't have a valid id, the touch down wasn't on content. + break; + } + + final int pointerIndex = ev.findPointerIndex(activePointerId); + if (pointerIndex == -1) { + Log.e(TAG, "Invalid pointerId=" + activePointerId + + " in onInterceptTouchEvent"); + break; + } + + final int y = (int) ev.getY(pointerIndex); + final int yDiff = Math.abs(y - mLastMotionY); + if (yDiff > mTouchSlop + && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) { + mIsBeingDragged = true; + mLastMotionY = y; + initVelocityTrackerIfNotExists(); + mVelocityTracker.addMovement(ev); + mNestedYOffset = 0; + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + } + break; + } + + case MotionEvent.ACTION_DOWN: { + final int y = (int) ev.getY(); + if (!inChild((int) ev.getX(), (int) y)) { + mIsBeingDragged = false; + recycleVelocityTracker(); + break; + } + + /* + * Remember location of down touch. + * ACTION_DOWN always refers to pointer index 0. + */ + mLastMotionY = y; + mActivePointerId = ev.getPointerId(0); + + initOrResetVelocityTracker(); + mVelocityTracker.addMovement(ev); + /* + * If being flinged and subscribeUser touches the screen, initiate drag; + * otherwise don't. mScroller.isFinished should be false when + * being flinged. We need to call computeScrollOffset() first so that + * isFinished() is correct. + */ + mScroller.computeScrollOffset(); + mIsBeingDragged = !mScroller.isFinished(); + startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); + break; + } + + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + /* Release the drag */ + mIsBeingDragged = false; + mActivePointerId = INVALID_POINTER; + recycleVelocityTracker(); + if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) { + ViewCompat.postInvalidateOnAnimation(this); + } + stopNestedScroll(); + break; + case MotionEventCompat.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + break; + } + + /* + * The only time we want to intercept motion events is if we are in the + * drag mode. + */ + return mIsBeingDragged; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + initVelocityTrackerIfNotExists(); + + MotionEvent vtev = MotionEvent.obtain(ev); + + final int actionMasked = MotionEventCompat.getActionMasked(ev); + + if (actionMasked == MotionEvent.ACTION_DOWN) { + mNestedYOffset = 0; + } + vtev.offsetLocation(0, mNestedYOffset); + + switch (actionMasked) { + case MotionEvent.ACTION_DOWN: { + if (getChildCount() == 0) { + return false; + } + if ((mIsBeingDragged = !mScroller.isFinished())) { + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + } + + /* + * If being flinged and subscribeUser touches, stop the fling. isFinished + * will be false if being flinged. + */ + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + + // Remember where the motion event started + mLastMotionY = (int) ev.getY(); + mActivePointerId = ev.getPointerId(0); + startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); + break; + } + case MotionEvent.ACTION_MOVE: + final int activePointerIndex = ev.findPointerIndex(mActivePointerId); + if (activePointerIndex == -1) { + Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); + break; + } + + final int y = (int) ev.getY(activePointerIndex); + int deltaY = mLastMotionY - y; + if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) { + deltaY -= mScrollConsumed[1]; + vtev.offsetLocation(0, mScrollOffset[1]); + mNestedYOffset += mScrollOffset[1]; + } + if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) { + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + mIsBeingDragged = true; + if (deltaY > 0) { + deltaY -= mTouchSlop; + } else { + deltaY += mTouchSlop; + } + } + if (mIsBeingDragged) { + // Scroll to follow the motion event + mLastMotionY = y - mScrollOffset[1]; + + final int oldY = getScrollY(); + final int range = getScrollRange(); + final int overscrollMode = getOverScrollMode(); + boolean canOverscroll = overscrollMode == View.OVER_SCROLL_ALWAYS + || (overscrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); + + // Calling overScrollByCompat will call onOverScrolled, which + // calls onScrollChanged if applicable. + if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0, + 0, true) && !hasNestedScrollingParent()) { + // Break our velocity if we hit a scroll barrier. + mVelocityTracker.clear(); + } + + final int scrolledDeltaY = getScrollY() - oldY; + final int unconsumedY = deltaY - scrolledDeltaY; + if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) { + mLastMotionY -= mScrollOffset[1]; + vtev.offsetLocation(0, mScrollOffset[1]); + mNestedYOffset += mScrollOffset[1]; + } else if (canOverscroll) { + ensureGlows(); + final int pulledToY = oldY + deltaY; + if (pulledToY < 0) { + mEdgeGlowTop.onPull((float) deltaY / getHeight(), + ev.getX(activePointerIndex) / getWidth()); + if (!mEdgeGlowBottom.isFinished()) { + mEdgeGlowBottom.onRelease(); + } + } else if (pulledToY > range) { + mEdgeGlowBottom.onPull((float) deltaY / getHeight(), + 1.f - ev.getX(activePointerIndex) + / getWidth()); + if (!mEdgeGlowTop.isFinished()) { + mEdgeGlowTop.onRelease(); + } + } + if (mEdgeGlowTop != null + && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + } + break; + case MotionEvent.ACTION_UP: + if (mIsBeingDragged) { + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(velocityTracker, + mActivePointerId); + + if ((Math.abs(initialVelocity) > mMinimumVelocity)) { + flingWithNestedDispatch(-initialVelocity); + } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, + getScrollRange())) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + mActivePointerId = INVALID_POINTER; + endDrag(); + break; + case MotionEvent.ACTION_CANCEL: + if (mIsBeingDragged && getChildCount() > 0) { + if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, + getScrollRange())) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + mActivePointerId = INVALID_POINTER; + endDrag(); + break; + case MotionEventCompat.ACTION_POINTER_DOWN: { + final int index = MotionEventCompat.getActionIndex(ev); + mLastMotionY = (int) ev.getY(index); + mActivePointerId = ev.getPointerId(index); + break; + } + case MotionEventCompat.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId)); + break; + } + + if (mVelocityTracker != null) { + mVelocityTracker.addMovement(vtev); + } + vtev.recycle(); + return true; + } + + private void onSecondaryPointerUp(MotionEvent ev) { + final int pointerIndex = (ev.getAction() & MotionEventCompat.ACTION_POINTER_INDEX_MASK) + >> MotionEventCompat.ACTION_POINTER_INDEX_SHIFT; + final int pointerId = ev.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mLastMotionY = (int) ev.getY(newPointerIndex); + mActivePointerId = ev.getPointerId(newPointerIndex); + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + } + } + } + + public boolean onGenericMotionEvent(MotionEvent event) { + if ((event.getSource() & InputDeviceCompat.SOURCE_CLASS_POINTER) != 0) { + switch (event.getAction()) { + case MotionEventCompat.ACTION_SCROLL: { + if (!mIsBeingDragged) { + final float vscroll = MotionEventCompat.getAxisValue(event, + MotionEventCompat.AXIS_VSCROLL); + if (vscroll != 0) { + final int delta = (int) (vscroll * getVerticalScrollFactorCompat()); + final int range = getScrollRange(); + int oldScrollY = getScrollY(); + int newScrollY = oldScrollY - delta; + if (newScrollY < 0) { + newScrollY = 0; + } else if (newScrollY > range) { + newScrollY = range; + } + if (newScrollY != oldScrollY) { + super.scrollTo(getScrollX(), newScrollY); + return true; + } + } + } + } + } + } + return false; + } + + private float getVerticalScrollFactorCompat() { + if (mVerticalScrollFactor == 0) { + TypedValue outValue = new TypedValue(); + final Context context = getContext(); + if (!context.getTheme().resolveAttribute( + android.R.attr.listPreferredItemHeight, outValue, true)) { + throw new IllegalStateException( + "Expected theme to define listPreferredItemHeight."); + } + mVerticalScrollFactor = outValue.getDimension( + context.getResources().getDisplayMetrics()); + } + return mVerticalScrollFactor; + } + + @Override + protected void onOverScrolled(int scrollX, int scrollY, + boolean clampedX, boolean clampedY) { + super.scrollTo(scrollX, scrollY); + } + + protected boolean overScrollByCompat(int deltaX, int deltaY, + int scrollX, int scrollY, + int scrollRangeX, int scrollRangeY, + int maxOverScrollX, int maxOverScrollY, + boolean isTouchEvent) { + final int overScrollMode = getOverScrollMode(); + final boolean canScrollHorizontal = + computeHorizontalScrollRange() > computeHorizontalScrollExtent(); + final boolean canScrollVertical = + computeVerticalScrollRange() > computeVerticalScrollExtent(); + final boolean overScrollHorizontal = overScrollMode == View.OVER_SCROLL_ALWAYS + || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal); + final boolean overScrollVertical = overScrollMode == View.OVER_SCROLL_ALWAYS + || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical); + + int newScrollX = scrollX + deltaX; + if (!overScrollHorizontal) { + maxOverScrollX = 0; + } + + int newScrollY = scrollY + deltaY; + if (!overScrollVertical) { + maxOverScrollY = 0; + } + + // Clamp values if at the limits and record + final int left = -maxOverScrollX; + final int right = maxOverScrollX + scrollRangeX; + final int top = -maxOverScrollY; + final int bottom = maxOverScrollY + scrollRangeY; + + boolean clampedX = false; + if (newScrollX > right) { + newScrollX = right; + clampedX = true; + } else if (newScrollX < left) { + newScrollX = left; + clampedX = true; + } + + boolean clampedY = false; + if (newScrollY > bottom) { + newScrollY = bottom; + clampedY = true; + } else if (newScrollY < top) { + newScrollY = top; + clampedY = true; + } + + if (clampedY) { + mScroller.springBack(newScrollX, newScrollY, 0, 0, 0, getScrollRange()); + } + + onOverScrolled(newScrollX, newScrollY, clampedX, clampedY); + + return clampedX || clampedY; + } + + int getScrollRange() { + int scrollRange = 0; + if (getChildCount() > 0) { + View child = getChildAt(0); + scrollRange = Math.max(0, + child.getHeight() - (getHeight() - getPaddingBottom() - getPaddingTop())); + } + return scrollRange; + } + + /** + *

    + * Finds the next focusable component that fits in the specified bounds. + *

    + * + * @param topFocus look for a candidate is the one at the top of the bounds + * if topFocus is true, or at the bottom of the bounds if topFocus is + * false + * @param top the top offset of the bounds in which a focusable must be + * found + * @param bottom the bottom offset of the bounds in which a focusable must + * be found + * @return the next focusable component in the bounds or null if none can + * be found + */ + private View findFocusableViewInBounds(boolean topFocus, int top, int bottom) { + + List focusables = getFocusables(View.FOCUS_FORWARD); + View focusCandidate = null; + + /* + * A fully contained focusable is one where its top is below the bound's + * top, and its bottom is above the bound's bottom. A partially + * contained focusable is one where some part of it is within the + * bounds, but it also has some part that is not within bounds. A fully contained + * focusable is preferred to a partially contained focusable. + */ + boolean foundFullyContainedFocusable = false; + + int count = focusables.size(); + for (int i = 0; i < count; i++) { + View view = focusables.get(i); + int viewTop = view.getTop(); + int viewBottom = view.getBottom(); + + if (top < viewBottom && viewTop < bottom) { + /* + * the focusable is in the target area, it is a candidate for + * focusing + */ + + final boolean viewIsFullyContained = (top < viewTop) && (viewBottom < bottom); + + if (focusCandidate == null) { + /* No candidate, take this one */ + focusCandidate = view; + foundFullyContainedFocusable = viewIsFullyContained; + } else { + final boolean viewIsCloserToBoundary = + (topFocus && viewTop < focusCandidate.getTop()) + || (!topFocus && viewBottom > focusCandidate.getBottom()); + + if (foundFullyContainedFocusable) { + if (viewIsFullyContained && viewIsCloserToBoundary) { + /* + * We're dealing with only fully contained views, so + * it has to be closer to the boundary to beat our + * candidate + */ + focusCandidate = view; + } + } else { + if (viewIsFullyContained) { + /* Any fully contained view beats a partially contained view */ + focusCandidate = view; + foundFullyContainedFocusable = true; + } else if (viewIsCloserToBoundary) { + /* + * Partially contained view beats another partially + * contained view if it's closer + */ + focusCandidate = view; + } + } + } + } + } + + return focusCandidate; + } + + /** + *

    Handles scrolling in response to a "page up/down" shortcut press. This + * method will scroll the view by one page up or down and give the focus + * to the topmost/bottommost component in the new visible area. If no + * component is a good candidate for focus, this scrollview reclaims the + * focus.

    + * + * @param direction the scroll direction: {@link View#FOCUS_UP} + * to go one page up or + * {@link View#FOCUS_DOWN} to go one page down + * @return true if the key event is consumed by this method, false otherwise + */ + public boolean pageScroll(int direction) { + boolean down = direction == View.FOCUS_DOWN; + int height = getHeight(); + + if (down) { + mTempRect.top = getScrollY() + height; + int count = getChildCount(); + if (count > 0) { + View view = getChildAt(count - 1); + if (mTempRect.top + height > view.getBottom()) { + mTempRect.top = view.getBottom() - height; + } + } + } else { + mTempRect.top = getScrollY() - height; + if (mTempRect.top < 0) { + mTempRect.top = 0; + } + } + mTempRect.bottom = mTempRect.top + height; + + return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom); + } + + /** + *

    Handles scrolling in response to a "home/end" shortcut press. This + * method will scroll the view to the top or bottom and give the focus + * to the topmost/bottommost component in the new visible area. If no + * component is a good candidate for focus, this scrollview reclaims the + * focus.

    + * + * @param direction the scroll direction: {@link View#FOCUS_UP} + * to go the top of the view or + * {@link View#FOCUS_DOWN} to go the bottom + * @return true if the key event is consumed by this method, false otherwise + */ + public boolean fullScroll(int direction) { + boolean down = direction == View.FOCUS_DOWN; + int height = getHeight(); + + mTempRect.top = 0; + mTempRect.bottom = height; + + if (down) { + int count = getChildCount(); + if (count > 0) { + View view = getChildAt(count - 1); + mTempRect.bottom = view.getBottom() + getPaddingBottom(); + mTempRect.top = mTempRect.bottom - height; + } + } + + return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom); + } + + /** + *

    Scrolls the view to make the area defined by top and + * bottom visible. This method attempts to give the focus + * to a component visible in this area. If no component can be focused in + * the new visible area, the focus is reclaimed by this ScrollView.

    + * + * @param direction the scroll direction: {@link View#FOCUS_UP} + * to go upward, {@link View#FOCUS_DOWN} to downward + * @param top the top offset of the new area to be made visible + * @param bottom the bottom offset of the new area to be made visible + * @return true if the key event is consumed by this method, false otherwise + */ + private boolean scrollAndFocus(int direction, int top, int bottom) { + boolean handled = true; + + int height = getHeight(); + int containerTop = getScrollY(); + int containerBottom = containerTop + height; + boolean up = direction == View.FOCUS_UP; + + View newFocused = findFocusableViewInBounds(up, top, bottom); + if (newFocused == null) { + newFocused = this; + } + + if (top >= containerTop && bottom <= containerBottom) { + handled = false; + } else { + int delta = up ? (top - containerTop) : (bottom - containerBottom); + doScrollY(delta); + } + + if (newFocused != findFocus()) newFocused.requestFocus(direction); + + return handled; + } + + /** + * Handle scrolling in response to an up or down arrow click. + * + * @param direction The direction corresponding to the arrow key that was + * pressed + * @return True if we consumed the event, false otherwise + */ + public boolean arrowScroll(int direction) { + + View currentFocused = findFocus(); + if (currentFocused == this) currentFocused = null; + + View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction); + + final int maxJump = getMaxScrollAmount(); + + if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump, getHeight())) { + nextFocused.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(nextFocused, mTempRect); + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + doScrollY(scrollDelta); + nextFocused.requestFocus(direction); + } else { + // no new focus + int scrollDelta = maxJump; + + if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) { + scrollDelta = getScrollY(); + } else if (direction == View.FOCUS_DOWN) { + if (getChildCount() > 0) { + int daBottom = getChildAt(0).getBottom(); + int screenBottom = getScrollY() + getHeight() - getPaddingBottom(); + if (daBottom - screenBottom < maxJump) { + scrollDelta = daBottom - screenBottom; + } + } + } + if (scrollDelta == 0) { + return false; + } + doScrollY(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta); + } + + if (currentFocused != null && currentFocused.isFocused() + && isOffScreen(currentFocused)) { + // previously focused item still has focus and is off screen, give + // it up (take it back to ourselves) + // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are + // sure to + // get it) + final int descendantFocusability = getDescendantFocusability(); // save + setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); + requestFocus(); + setDescendantFocusability(descendantFocusability); // restore + } + return true; + } + + /** + * @return whether the descendant of this scroll view is scrolled off + * screen. + */ + private boolean isOffScreen(View descendant) { + return !isWithinDeltaOfScreen(descendant, 0, getHeight()); + } + + /** + * @return whether the descendant of this scroll view is within delta + * pixels of being on the screen. + */ + private boolean isWithinDeltaOfScreen(View descendant, int delta, int height) { + descendant.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(descendant, mTempRect); + + return (mTempRect.bottom + delta) >= getScrollY() + && (mTempRect.top - delta) <= (getScrollY() + height); + } + + /** + * Smooth scroll by a Y delta + * + * @param delta the number of pixels to scroll by on the Y axis + */ + private void doScrollY(int delta) { + if (delta != 0) { + if (mSmoothScrollingEnabled) { + smoothScrollBy(0, delta); + } else { + scrollBy(0, delta); + } + } + } + + /** + * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. + * + * @param dx the number of pixels to scroll by on the X axis + * @param dy the number of pixels to scroll by on the Y axis + */ + public final void smoothScrollBy(int dx, int dy) { + if (getChildCount() == 0) { + // Nothing to do. + return; + } + long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll; + if (duration > ANIMATED_SCROLL_GAP) { + final int height = getHeight() - getPaddingBottom() - getPaddingTop(); + final int bottom = getChildAt(0).getHeight(); + final int maxY = Math.max(0, bottom - height); + final int scrollY = getScrollY(); + dy = Math.max(0, Math.min(scrollY + dy, maxY)) - scrollY; + + mScroller.startScroll(getScrollX(), scrollY, 0, dy); + ViewCompat.postInvalidateOnAnimation(this); + } else { + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + scrollBy(dx, dy); + } + mLastScroll = AnimationUtils.currentAnimationTimeMillis(); + } + + /** + * Like {@link #scrollTo}, but scroll smoothly instead of immediately. + * + * @param x the position where to scroll on the X axis + * @param y the position where to scroll on the Y axis + */ + public final void smoothScrollTo(int x, int y) { + smoothScrollBy(x - getScrollX(), y - getScrollY()); + } + + /** + *

    The scroll range of a scroll view is the overall height of all of its + * children.

    + * + * @hide + */ + @Override + public int computeVerticalScrollRange() { + final int count = getChildCount(); + final int contentHeight = getHeight() - getPaddingBottom() - getPaddingTop(); + if (count == 0) { + return contentHeight; + } + + int scrollRange = getChildAt(0).getBottom(); + final int scrollY = getScrollY(); + final int overscrollBottom = Math.max(0, scrollRange - contentHeight); + if (scrollY < 0) { + scrollRange -= scrollY; + } else if (scrollY > overscrollBottom) { + scrollRange += scrollY - overscrollBottom; + } + + return scrollRange; + } + + /** + * @hide + */ + @Override + public int computeVerticalScrollOffset() { + return Math.max(0, super.computeVerticalScrollOffset()); + } + + /** + * @hide + */ + @Override + public int computeVerticalScrollExtent() { + return super.computeVerticalScrollExtent(); + } + + /** + * @hide + */ + @Override + public int computeHorizontalScrollRange() { + return super.computeHorizontalScrollRange(); + } + + /** + * @hide + */ + @Override + public int computeHorizontalScrollOffset() { + return super.computeHorizontalScrollOffset(); + } + + /** + * @hide + */ + @Override + public int computeHorizontalScrollExtent() { + return super.computeHorizontalScrollExtent(); + } + + @Override + protected void measureChild(View child, int parentWidthMeasureSpec, + int parentHeightMeasureSpec) { + ViewGroup.LayoutParams lp = child.getLayoutParams(); + + int childWidthMeasureSpec; + int childHeightMeasureSpec; + + childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft() + + getPaddingRight(), lp.width); + + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + @Override + protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, + int parentHeightMeasureSpec, int heightUsed) { + final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); + + final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, + getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin + + widthUsed, lp.width); + final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( + lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + @Override + public void computeScroll() { + if (mScroller.computeScrollOffset()) { + int oldX = getScrollX(); + int oldY = getScrollY(); + int x = mScroller.getCurrX(); + int y = mScroller.getCurrY(); + + if (oldX != x || oldY != y) { + final int range = getScrollRange(); + final int overscrollMode = getOverScrollMode(); + final boolean canOverscroll = overscrollMode == View.OVER_SCROLL_ALWAYS + || (overscrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); + + overScrollByCompat(x - oldX, y - oldY, oldX, oldY, 0, range, + 0, 0, false); + + if (canOverscroll) { + ensureGlows(); + if (y <= 0 && oldY > 0) { + mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity()); + } else if (y >= range && oldY < range) { + mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity()); + } + } + } + } + } + + /** + * Scrolls the view to the given child. + * + * @param child the RefreshView to scroll to + */ + private void scrollToChild(View child) { + child.getDrawingRect(mTempRect); + + /* Offset from child's local coordinates to ScrollView coordinates */ + offsetDescendantRectToMyCoords(child, mTempRect); + + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + + if (scrollDelta != 0) { + scrollBy(0, scrollDelta); + } + } + + /** + * If rect is off screen, scroll just enough to get it (or at least the + * first screen size chunk of it) on screen. + * + * @param rect The rectangle. + * @param immediate True to scroll immediately without animation + * @return true if scrolling was performed + */ + private boolean scrollToChildRect(Rect rect, boolean immediate) { + final int delta = computeScrollDeltaToGetChildRectOnScreen(rect); + final boolean scroll = delta != 0; + if (scroll) { + if (immediate) { + scrollBy(0, delta); + } else { + smoothScrollBy(0, delta); + } + } + return scroll; + } + + /** + * Compute the amount to scroll in the Y direction in order to get + * a rectangle completely on the screen (or, if taller than the screen, + * at least the first screen size chunk of it). + * + * @param rect The rect. + * @return The scroll delta. + */ + protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) { + if (getChildCount() == 0) return 0; + + int height = getHeight(); + int screenTop = getScrollY(); + int screenBottom = screenTop + height; + + int fadingEdge = getVerticalFadingEdgeLength(); + + // leave room for top fading edge as long as rect isn't at very top + if (rect.top > 0) { + screenTop += fadingEdge; + } + + // leave room for bottom fading edge as long as rect isn't at very bottom + if (rect.bottom < getChildAt(0).getHeight()) { + screenBottom -= fadingEdge; + } + + int scrollYDelta = 0; + + if (rect.bottom > screenBottom && rect.top > screenTop) { + // need to move down to get it in view: move down just enough so + // that the entire rectangle is in view (or at least the first + // screen size chunk). + + if (rect.height() > height) { + // just enough to get screen size chunk on + scrollYDelta += (rect.top - screenTop); + } else { + // get entire rect at bottom of screen + scrollYDelta += (rect.bottom - screenBottom); + } + + // make sure we aren't scrolling beyond the end of our content + int bottom = getChildAt(0).getBottom(); + int distanceToBottom = bottom - screenBottom; + scrollYDelta = Math.min(scrollYDelta, distanceToBottom); + + } else if (rect.top < screenTop && rect.bottom < screenBottom) { + // need to move up to get it in view: move up just enough so that + // entire rectangle is in view (or at least the first screen + // size chunk of it). + + if (rect.height() > height) { + // screen size chunk + scrollYDelta -= (screenBottom - rect.bottom); + } else { + // entire rect at top + scrollYDelta -= (screenTop - rect.top); + } + + // make sure we aren't scrolling any further than the top our content + scrollYDelta = Math.max(scrollYDelta, -getScrollY()); + } + return scrollYDelta; + } + + @Override + public void requestChildFocus(View child, View focused) { + if (!mIsLayoutDirty) { + scrollToChild(focused); + } else { + // The child may not be laid out yet, we can't compute the scroll yet + mChildToScrollTo = focused; + } + super.requestChildFocus(child, focused); + } + + + /** + * When looking for focus in children of a scroll view, need to be a little + * more careful not to give focus to something that is scrolled off screen. + *

    + * This is more expensive than the default {@link ViewGroup} + * implementation, otherwise this behavior might have been made the default. + */ + @Override + protected boolean onRequestFocusInDescendants(int direction, + Rect previouslyFocusedRect) { + + // convert from forward / backward notation to up / down / left / right + // (ugh). + if (direction == View.FOCUS_FORWARD) { + direction = View.FOCUS_DOWN; + } else if (direction == View.FOCUS_BACKWARD) { + direction = View.FOCUS_UP; + } + + final View nextFocus = previouslyFocusedRect == null + ? FocusFinder.getInstance().findNextFocus(this, null, direction) + : FocusFinder.getInstance().findNextFocusFromRect( + this, previouslyFocusedRect, direction); + + if (nextFocus == null) { + return false; + } + + if (isOffScreen(nextFocus)) { + return false; + } + + return nextFocus.requestFocus(direction, previouslyFocusedRect); + } + + @Override + public boolean requestChildRectangleOnScreen(View child, Rect rectangle, + boolean immediate) { + // offset into coordinate space of this scroll view + rectangle.offset(child.getLeft() - child.getScrollX(), + child.getTop() - child.getScrollY()); + + return scrollToChildRect(rectangle, immediate); + } + + @Override + public void requestLayout() { + mIsLayoutDirty = true; + super.requestLayout(); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + mIsLayoutDirty = false; + // Give a child focus if it needs it + if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) { + scrollToChild(mChildToScrollTo); + } + mChildToScrollTo = null; + + if (!mIsLaidOut) { + if (mSavedState != null) { + scrollTo(getScrollX(), mSavedState.scrollPosition); + mSavedState = null; + } // mScrollY default value is "0" + + final int childHeight = (getChildCount() > 0) ? getChildAt(0).getMeasuredHeight() : 0; + final int scrollRange = Math.max(0, + childHeight - (b - t - getPaddingBottom() - getPaddingTop())); + + // Don't forget to clamp + if (getScrollY() > scrollRange) { + scrollTo(getScrollX(), scrollRange); + } else if (getScrollY() < 0) { + scrollTo(getScrollX(), 0); + } + } + + // Calling this with the present values causes it to re-claim them + scrollTo(getScrollX(), getScrollY()); + mIsLaidOut = true; + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + + mIsLaidOut = false; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + View currentFocused = findFocus(); + if (null == currentFocused || this == currentFocused) { + return; + } + + // If the currently-focused view was visible on the screen when the + // screen was at the old height, then scroll the screen to make that + // view visible with the new screen height. + if (isWithinDeltaOfScreen(currentFocused, 0, oldh)) { + currentFocused.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(currentFocused, mTempRect); + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + doScrollY(scrollDelta); + } + } + + /** + * Return true if child is a descendant of parent, (or equal to the parent). + */ + private static boolean isViewDescendantOf(View child, View parent) { + if (child == parent) { + return true; + } + + final ViewParent theParent = child.getParent(); + return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent); + } + + /** + * Fling the scroll view + * + * @param velocityY The initial velocity in the Y direction. Positive + * numbers mean that the finger/cursor is moving down the screen, + * which means we want to scroll towards the top. + */ + public void fling(int velocityY) { + if (getChildCount() > 0) { + int height = getHeight() - getPaddingBottom() - getPaddingTop(); + int bottom = getChildAt(0).getHeight(); + + mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, 0, + Math.max(0, bottom - height), 0, height / 2); + + ViewCompat.postInvalidateOnAnimation(this); + } + } + + private void flingWithNestedDispatch(int velocityY) { + final int scrollY = getScrollY(); + final boolean canFling = (scrollY > 0 || velocityY > 0) + && (scrollY < getScrollRange() || velocityY < 0); + if (!dispatchNestedPreFling(0, velocityY)) { + dispatchNestedFling(0, velocityY, canFling); + if (canFling) { + fling(velocityY); + } + } + } + + private void endDrag() { + mIsBeingDragged = false; + + recycleVelocityTracker(); + stopNestedScroll(); + + if (mEdgeGlowTop != null) { + mEdgeGlowTop.onRelease(); + mEdgeGlowBottom.onRelease(); + } + } + + /** + * {@inheritDoc} + *

    + *

    This version also clamps the scrolling to the bounds of our child. + */ + @Override + public void scrollTo(int x, int y) { + // we rely on the fact the RefreshView.scrollBy calls scrollTo. + if (getChildCount() > 0) { + View child = getChildAt(0); + x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth()); + y = clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight()); + if (x != getScrollX() || y != getScrollY()) { + super.scrollTo(x, y); + } + } + } + + private void ensureGlows() { + if (getOverScrollMode() != View.OVER_SCROLL_NEVER) { + if (mEdgeGlowTop == null) { + Context context = getContext(); + mEdgeGlowTop = new EdgeEffectCompat(context); + mEdgeGlowBottom = new EdgeEffectCompat(context); + } + } else { + mEdgeGlowTop = null; + mEdgeGlowBottom = null; + } + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + if (mEdgeGlowTop != null) { + final int scrollY = getScrollY(); + if (!mEdgeGlowTop.isFinished()) { + final int restoreCount = canvas.save(); + final int width = getWidth() - getPaddingLeft() - getPaddingRight(); + + canvas.translate(getPaddingLeft(), Math.min(0, scrollY)); + mEdgeGlowTop.setSize(width, getHeight()); + if (mEdgeGlowTop.draw(canvas)) { + ViewCompat.postInvalidateOnAnimation(this); + } + canvas.restoreToCount(restoreCount); + } + if (!mEdgeGlowBottom.isFinished()) { + final int restoreCount = canvas.save(); + final int width = getWidth() - getPaddingLeft() - getPaddingRight(); + final int height = getHeight(); + + canvas.translate(-width + getPaddingLeft(), + Math.max(getScrollRange(), scrollY) + height); + canvas.rotate(180, width, 0); + mEdgeGlowBottom.setSize(width, height); + if (mEdgeGlowBottom.draw(canvas)) { + ViewCompat.postInvalidateOnAnimation(this); + } + canvas.restoreToCount(restoreCount); + } + } + } + + private static int clamp(int n, int my, int child) { + if (my >= child || n < 0) { + /* my >= child is this case: + * |--------------- me ---------------| + * |------ child ------| + * or + * |--------------- me ---------------| + * |------ child ------| + * or + * |--------------- me ---------------| + * |------ child ------| + * + * n < 0 is this case: + * |------ me ------| + * |-------- child --------| + * |-- mScrollX --| + */ + return 0; + } + if ((my + n) > child) { + /* this case: + * |------ me ------| + * |------ child ------| + * |-- mScrollX --| + */ + return child - my; + } + return n; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (!(state instanceof SavedState)) { + super.onRestoreInstanceState(state); + return; + } + + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + mSavedState = ss; + requestLayout(); + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.scrollPosition = getScrollY(); + return ss; + } + + static class SavedState extends BaseSavedState { + public int scrollPosition; + + SavedState(Parcelable superState) { + super(superState); + } + + SavedState(Parcel source) { + super(source); + scrollPosition = source.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(scrollPosition); + } + + @Override + public String toString() { + return "HorizontalScrollView.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " scrollPosition=" + scrollPosition + "}"; + } + + public static final Creator CREATOR = + new Creator() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + static class AccessibilityDelegate extends AccessibilityDelegateCompat { + @Override + public boolean performAccessibilityAction(View host, int action, Bundle arguments) { + if (super.performAccessibilityAction(host, action, arguments)) { + return true; + } + final NestedScrollView nsvHost = (NestedScrollView) host; + if (!nsvHost.isEnabled()) { + return false; + } + switch (action) { + case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: { + final int viewportHeight = nsvHost.getHeight() - nsvHost.getPaddingBottom() + - nsvHost.getPaddingTop(); + final int targetScrollY = Math.min(nsvHost.getScrollY() + viewportHeight, + nsvHost.getScrollRange()); + if (targetScrollY != nsvHost.getScrollY()) { + nsvHost.smoothScrollTo(0, targetScrollY); + return true; + } + } + return false; + case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: { + final int viewportHeight = nsvHost.getHeight() - nsvHost.getPaddingBottom() + - nsvHost.getPaddingTop(); + final int targetScrollY = Math.max(nsvHost.getScrollY() - viewportHeight, 0); + if (targetScrollY != nsvHost.getScrollY()) { + nsvHost.smoothScrollTo(0, targetScrollY); + return true; + } + } + return false; + } + return false; + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(host, info); + final NestedScrollView nsvHost = (NestedScrollView) host; + info.setClassName(ScrollView.class.getName()); + if (nsvHost.isEnabled()) { + final int scrollRange = nsvHost.getScrollRange(); + if (scrollRange > 0) { + info.setScrollable(true); + if (nsvHost.getScrollY() > 0) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); + } + if (nsvHost.getScrollY() < scrollRange) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); + } + } + } + } + + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(host, event); + final NestedScrollView nsvHost = (NestedScrollView) host; + event.setClassName(ScrollView.class.getName()); + final AccessibilityRecordCompat record = AccessibilityEventCompat.asRecord(event); + final boolean scrollable = nsvHost.getScrollRange() > 0; + record.setScrollable(scrollable); + record.setScrollX(nsvHost.getScrollX()); + record.setScrollY(nsvHost.getScrollY()); + record.setMaxScrollX(nsvHost.getScrollX()); + record.setMaxScrollY(nsvHost.getScrollRange()); + } + } + +} diff --git a/lib_base/src/main/java/com/android/base/widget/pulltozoom/PullToZoomScrollView.java b/lib_base/src/main/java/com/android/base/widget/pulltozoom/PullToZoomScrollView.java new file mode 100644 index 0000000..02a1dc7 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/pulltozoom/PullToZoomScrollView.java @@ -0,0 +1,250 @@ +package com.android.base.widget.pulltozoom; + +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.animation.DecelerateInterpolator; + +import com.android.base.R; + +/** + *

    + *     {@code
    + *     
    + *
    + *         
    + *
    + *             //header
    + *             
    + *
    + *                  //image view to pull zoom
    + *                 
    + *
    + *              
    + *
    + *              //content
    + *              
    + *
    + *          
    + *
    + *         
    + *     }
    + * 
    + */ +public class PullToZoomScrollView extends NestedScrollView { + + private OnScrollListener mScrollListener; + + private View mZoomView; + private int mZoomViewId; + + private View mContainerView; + private int mContainerViewId; + + private int mMaxZoomHeight; + private int mOriginContainerViewHeight; + private int mOriginZoomViewHeight; + private int mDamp; + private float mZoomFactory; + + private TimeInterpolator mInterpolator; + private ValueAnimator mValueAnimator; + private static final int ANIMATION_TIME = 500; + + public PullToZoomScrollView(Context context) { + this(context, null); + } + + public PullToZoomScrollView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public PullToZoomScrollView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PullToZoomScrollView); + mZoomViewId = typedArray.getResourceId(R.styleable.PullToZoomScrollView_psv_zoom_view, -1); + mContainerViewId = typedArray.getResourceId(R.styleable.PullToZoomScrollView_psv_header_view, -1); + mDamp = typedArray.getInteger(R.styleable.PullToZoomScrollView_psv_damp, 2);//默认阻尼2 + mMaxZoomHeight = typedArray.getInteger(R.styleable.PullToZoomScrollView_psv_max_over, dpToPx(1500));//默认最大scroll 1500DP + mZoomFactory = typedArray.getFloat(R.styleable.PullToZoomScrollView_psv_zoom_factory, 2.0F); + typedArray.recycle(); + + init(); + } + + private void init() { + mInterpolator = new DecelerateInterpolator(); + } + + public void setScrollListener(OnScrollListener scrollListener) { + mScrollListener = scrollListener; + } + + public interface OnScrollListener { + void onScroll(int headerLayoutHeight, int zoomViewHeight, int scrollY); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + if (mZoomViewId != -1) { + mZoomView = findViewById(mZoomViewId); + } + if (mContainerViewId != -1) { + mContainerView = findViewById(mContainerViewId); + } + getInnerViewHeight(); + } + + private boolean hasInnerView() { + return mContainerView != null && mZoomView != null; + } + + @Override + protected boolean overScrollByCompat(int deltaX, int deltaY, + int scrollX, int scrollY, + int scrollRangeX, int scrollRangeY, + int maxOverScrollX, + int maxOverScrollY, boolean isTouchEvent) { + //overScrollBy如果返回True,那么ScrollView将不会滑动 + return processOverScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent) + || super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent); + } + + @SuppressWarnings("unused") + private boolean processOverScrollBy(int deltaX, int deltaY, int scrollX, + int scrollY, int scrollRangeX, int scrollRangeY, + int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) { + if (!hasInnerView()) { + return false; + } + if (mContainerView.getHeight() <= mMaxZoomHeight && isTouchEvent) { + if (deltaY < 0) { + int offset = (int) (deltaY * 1.0F / mDamp); + if (mContainerView.getHeight() - offset >= mOriginContainerViewHeight) { + int height = mContainerView + .getHeight() - offset < mMaxZoomHeight ? + mContainerView.getHeight() - offset : mMaxZoomHeight; + setContainerHeight(height); + } + } else { + if (mContainerView.getHeight() > mOriginContainerViewHeight) { + int height = mContainerView.getHeight() - deltaY > mOriginContainerViewHeight ? + mContainerView.getHeight() - deltaY : mOriginContainerViewHeight; + setContainerHeight(height); + return true; + } + } + } + return false; + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (hasInnerView()) { + int action = ev.getAction(); + if (action == MotionEvent.ACTION_DOWN) { + cancelAnim(); + } + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + zoomBackIfNeed(); + } + } + return super.onTouchEvent(ev); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (mScrollListener != null) { + if (mOriginContainerViewHeight != 0 && mOriginZoomViewHeight != 0) { + mScrollListener.onScroll(mOriginContainerViewHeight, mOriginZoomViewHeight, getScrollY()); + } + } + } + + private void zoomBackIfNeed() { + cancelAnim(); + if (mOriginContainerViewHeight - 1 < mContainerView.getHeight()) { + mValueAnimator = ValueAnimator.ofInt(mContainerView.getHeight(), mOriginContainerViewHeight); + mValueAnimator.setDuration(ANIMATION_TIME); + mValueAnimator.setInterpolator(mInterpolator); + mValueAnimator.addUpdateListener(mAnimatorUpdateListener); + mValueAnimator.start(); + } + } + + private void cancelAnim() { + if (mValueAnimator != null) { + mValueAnimator.cancel(); + mValueAnimator = null; + } + } + + private ValueAnimator.AnimatorUpdateListener mAnimatorUpdateListener = animation -> { + int animatedValue = (int) animation.getAnimatedValue(); + setContainerHeight(animatedValue); + }; + + private void setContainerHeight(int height) { + mContainerView.getLayoutParams().height = height; + if (height >= mOriginContainerViewHeight) { + zoomView(height); + } + mContainerView.setLayoutParams(mContainerView.getLayoutParams()); + } + + private void zoomView(int height) { + int measuredWidth = mZoomView.getMeasuredWidth(); + mZoomView.setPivotX(measuredWidth / 2); + mZoomView.setPivotY(mOriginContainerViewHeight / 3F); + float addOffset = (height - mOriginContainerViewHeight) * mZoomFactory / mOriginContainerViewHeight; + float scale = height * 1.0F / mOriginContainerViewHeight + addOffset; + mZoomView.setScaleX(scale); + mZoomView.setScaleY(scale); + } + + private void getInnerViewHeight() { + post(() -> { + if (mContainerView != null) { + mOriginContainerViewHeight = mContainerView.getHeight(); + } + if (mZoomView != null) { + mOriginZoomViewHeight = mZoomView.getHeight(); + } + }); + } + + public int dpToPx(int dp) { + return (int) (dp * getContext().getResources().getDisplayMetrics().density + 0.5f); + } + +} diff --git a/lib_base/src/main/java/com/android/base/widget/ratio/RatioHelper.java b/lib_base/src/main/java/com/android/base/widget/ratio/RatioHelper.java new file mode 100644 index 0000000..c38faf0 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/ratio/RatioHelper.java @@ -0,0 +1,54 @@ +package com.android.base.widget.ratio; + +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.View; + +import com.android.base.R; + + +class RatioHelper { + + private static final int[] ratioAttrs = {R.attr.layout_ratio}; + private float mRatio; + private View mView; + + RatioHelper(View view) { + mView = view; + } + + void resolveAttr(AttributeSet attrs) { + TypedArray typedArray = mView.getContext().obtainStyledAttributes(attrs, ratioAttrs); + mRatio = typedArray.getFloat(0, 0F); + typedArray.recycle(); + } + + void setRatio(float ratio) { + mRatio = ratio; + } + + int[] measure(int widthMeasureSpec, int heightMeasureSpec) { + + if (mRatio != 0) { + int widthMode = View.MeasureSpec.getMode(widthMeasureSpec); + int heightMode = View.MeasureSpec.getMode(heightMeasureSpec); + + int width = View.MeasureSpec.getSize(widthMeasureSpec) - mView.getPaddingLeft() - mView.getPaddingRight(); + int height = View.MeasureSpec.getSize(heightMeasureSpec) - mView.getPaddingTop() - mView.getPaddingBottom(); + + if (widthMode == View.MeasureSpec.EXACTLY && heightMode != View.MeasureSpec.EXACTLY) { + + height = (int) (width / mRatio + 0.5f); + heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height + mView.getPaddingTop() + mView.getPaddingBottom(), View.MeasureSpec.EXACTLY); + + } else if (widthMode != View.MeasureSpec.EXACTLY && heightMode == View.MeasureSpec.EXACTLY) { + + width = (int) (height * mRatio + 0.5f); + widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(width + mView.getPaddingLeft() + mView.getPaddingRight(), View.MeasureSpec.EXACTLY); + + } + } + return new int[]{widthMeasureSpec, heightMeasureSpec}; + } + +} diff --git a/lib_base/src/main/java/com/android/base/widget/ratio/RatioImageView.java b/lib_base/src/main/java/com/android/base/widget/ratio/RatioImageView.java new file mode 100644 index 0000000..911f0fc --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/ratio/RatioImageView.java @@ -0,0 +1,36 @@ +package com.android.base.widget.ratio; + +import android.content.Context; +import android.support.v7.widget.AppCompatImageView; +import android.util.AttributeSet; + + +public class RatioImageView extends AppCompatImageView { + + private RatioHelper mRatioHelper; + + public RatioImageView(Context context) { + this(context, null); + } + + public RatioImageView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public RatioImageView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mRatioHelper = new RatioHelper(this); + mRatioHelper.resolveAttr(attrs); + } + + public void setRatio(float ratio) { + mRatioHelper.setRatio(ratio); + requestLayout(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int[] measure = mRatioHelper.measure(widthMeasureSpec, heightMeasureSpec); + super.onMeasure(measure[0], measure[1]); + } +} diff --git a/lib_base/src/main/java/com/android/base/widget/ratio/RatioLayout.java b/lib_base/src/main/java/com/android/base/widget/ratio/RatioLayout.java new file mode 100644 index 0000000..842eef1 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/ratio/RatioLayout.java @@ -0,0 +1,57 @@ +package com.android.base.widget.ratio; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +import com.android.base.R; + + +public class RatioLayout extends FrameLayout { + + // 宽和高的比例 + private float ratio = 0.0f; + + public RatioLayout(Context context) { + this(context, null); + } + + public RatioLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public RatioLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RatioLayout); + ratio = a.getFloat(R.styleable.RatioLayout_layout_ratio, 0.0f); + a.recycle(); + } + + public void setRatio(float f) { + ratio = f; + requestLayout(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + + int width = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight(); + int height = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom(); + + if (widthMode == MeasureSpec.EXACTLY && heightMode != MeasureSpec.EXACTLY && ratio != 0.0f) { + + height = (int) (width / ratio + 0.5f); + heightMeasureSpec = MeasureSpec.makeMeasureSpec(height + getPaddingTop() + getPaddingBottom(), MeasureSpec.EXACTLY); + + } else if (widthMode != MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY && ratio != 0.0f) { + + width = (int) (height * ratio + 0.5f); + widthMeasureSpec = MeasureSpec.makeMeasureSpec(width + getPaddingLeft() + getPaddingRight(), MeasureSpec.EXACTLY); + + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } +} diff --git a/lib_base/src/main/java/com/android/base/widget/ratio/RatioTextView.java b/lib_base/src/main/java/com/android/base/widget/ratio/RatioTextView.java new file mode 100644 index 0000000..f1983c9 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/ratio/RatioTextView.java @@ -0,0 +1,36 @@ +package com.android.base.widget.ratio; + +import android.content.Context; +import android.support.v7.widget.AppCompatTextView; +import android.util.AttributeSet; + +public class RatioTextView extends AppCompatTextView { + + private RatioHelper mRatioHelper; + + public RatioTextView(Context context) { + this(context, null); + } + + public RatioTextView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public RatioTextView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mRatioHelper = new RatioHelper(this); + mRatioHelper.resolveAttr(attrs); + } + + public void setRatio(float ratio) { + mRatioHelper.setRatio(ratio); + requestLayout(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int[] measure = mRatioHelper.measure(widthMeasureSpec, heightMeasureSpec); + super.onMeasure(measure[0], measure[1]); + } + +} diff --git a/lib_base/src/main/java/com/android/base/widget/ratio/TopDrawableCenterTextView.java b/lib_base/src/main/java/com/android/base/widget/ratio/TopDrawableCenterTextView.java new file mode 100644 index 0000000..4fe0250 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/ratio/TopDrawableCenterTextView.java @@ -0,0 +1,58 @@ +package com.android.base.widget.ratio; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.drawable.Drawable; +import android.text.TextPaint; +import android.util.AttributeSet; + +/** + * 使 TopDrawable 居中,不要设置 gravity 属性为 center 或 center_vertical + * + * @author Ztiany + * Email: 1169654504@qq.com + */ +public class TopDrawableCenterTextView extends RatioTextView { + + private Paint.FontMetrics metrics = new Paint.FontMetrics(); + + public TopDrawableCenterTextView(Context context) { + super(context); + } + + public TopDrawableCenterTextView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public TopDrawableCenterTextView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void onDraw(Canvas canvas) { + Drawable[] compoundDrawables = getCompoundDrawables(); + setToCenterPadding(compoundDrawables, canvas); + super.onDraw(canvas); + } + + private void setToCenterPadding(Drawable[] compoundDrawables, Canvas canvas) { + if (compoundDrawables == null) { + return; + } + + Drawable topDrawable = compoundDrawables[1]; + + if (topDrawable == null) { + return; + } + + int compoundDrawablePadding = getCompoundDrawablePadding(); + TextPaint paint = getPaint(); + paint.getFontMetrics(metrics); + float contentHeight = (metrics.bottom - metrics.top + compoundDrawablePadding + topDrawable.getIntrinsicHeight()); + int measuredHeight = getMeasuredHeight(); + canvas.translate(0, measuredHeight / 2 - contentHeight / 2); + } + +} diff --git a/lib_base/src/main/java/com/android/base/widget/recyclerview/DividerItemDecoration.java b/lib_base/src/main/java/com/android/base/widget/recyclerview/DividerItemDecoration.java new file mode 100644 index 0000000..c751d1c --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/recyclerview/DividerItemDecoration.java @@ -0,0 +1,174 @@ +package com.android.base.widget.recyclerview; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.v4.view.ViewCompat; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.widget.LinearLayout; + +/** + * 可以指定 SkipDrawStartCount 和 SkipDrawEndCount + */ +public class DividerItemDecoration extends RecyclerView.ItemDecoration { + + public static final int HORIZONTAL = LinearLayout.HORIZONTAL; + public static final int VERTICAL = LinearLayout.VERTICAL; + + private static final int[] ATTRS = new int[]{android.R.attr.listDivider}; + + private Drawable mDivider; + + /** + * Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL}. + */ + private int mOrientation; + + private final Rect mBounds = new Rect(); + private int mSkipDrawStartCount; + private int mSkipDrawEndCount; + + /** + * Creates a divider {@link RecyclerView.ItemDecoration} that can be used with a + * {@link android.support.v7.widget.LinearLayoutManager}. + * + * @param context Current context, it will be used to access resources. + * @param orientation Divider orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL}. + */ + public DividerItemDecoration(Context context, int orientation) { + this(context, orientation, 0, 0); + } + + public DividerItemDecoration(Context context, int orientation, int skipDrawStartCount, int skipDrawEndCount) { + final TypedArray a = context.obtainStyledAttributes(ATTRS); + mDivider = a.getDrawable(0); + a.recycle(); + if (skipDrawStartCount < 0 || skipDrawEndCount < 0) { + throw new IllegalArgumentException("can not less than 0"); + } + mSkipDrawStartCount = skipDrawStartCount; + mSkipDrawEndCount = skipDrawEndCount; + setOrientation(orientation); + } + + /** + * Sets the orientation for this divider. This should be called if + * {@link RecyclerView.LayoutManager} changes orientation. + * + * @param orientation {@link #HORIZONTAL} or {@link #VERTICAL} + */ + public void setOrientation(int orientation) { + if (orientation != HORIZONTAL && orientation != VERTICAL) { + throw new IllegalArgumentException( + "Invalid orientation. It should be either HORIZONTAL or VERTICAL"); + } + mOrientation = orientation; + } + + /** + * Sets the {@link Drawable} for this divider. + * + * @param drawable Drawable that should be used as a divider. + */ + @SuppressWarnings("all") + public void setDrawable(@NonNull Drawable drawable) { + if (drawable == null) { + throw new IllegalArgumentException("Drawable cannot be null."); + } + mDivider = drawable; + } + + @Override + public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { + if (parent.getLayoutManager() == null) { + return; + } + if (mOrientation == VERTICAL) { + drawVertical(c, parent); + } else { + drawHorizontal(c, parent); + } + } + + @SuppressLint("NewApi") + private void drawVertical(Canvas canvas, RecyclerView parent) { + canvas.save(); + final int left; + final int right; + if (parent.getClipToPadding()) { + left = parent.getPaddingLeft(); + right = parent.getWidth() - parent.getPaddingRight(); + canvas.clipRect(left, parent.getPaddingTop(), right, + parent.getHeight() - parent.getPaddingBottom()); + } else { + left = 0; + right = parent.getWidth(); + } + + final int childCount = parent.getChildCount(); + int itemCount = parent.getLayoutManager().getItemCount(); + int endNotDraw = itemCount - mSkipDrawEndCount; + + for (int i = 0; i < childCount; i++) { + final View child = parent.getChildAt(i); + int childAdapterPosition = parent.getChildAdapterPosition(child); + if (childAdapterPosition < mSkipDrawStartCount || childAdapterPosition >= endNotDraw) { + continue; + } + parent.getDecoratedBoundsWithMargins(child, mBounds); + final int bottom = mBounds.bottom + Math.round(ViewCompat.getTranslationY(child)); + final int top = bottom - mDivider.getIntrinsicHeight(); + mDivider.setBounds(left, top, right, bottom); + mDivider.draw(canvas); + } + canvas.restore(); + } + + @SuppressLint("NewApi") + private void drawHorizontal(Canvas canvas, RecyclerView parent) { + canvas.save(); + final int top; + final int bottom; + if (parent.getClipToPadding()) { + top = parent.getPaddingTop(); + bottom = parent.getHeight() - parent.getPaddingBottom(); + canvas.clipRect(parent.getPaddingLeft(), top, + parent.getWidth() - parent.getPaddingRight(), bottom); + } else { + top = 0; + bottom = parent.getHeight(); + } + + final int childCount = parent.getChildCount(); + int itemCount = parent.getLayoutManager().getItemCount(); + int endNotDraw = itemCount - mSkipDrawEndCount; + for (int i = 0; i < childCount; i++) { + final View child = parent.getChildAt(i); + int childAdapterPosition = parent.getChildAdapterPosition(child); + if (childAdapterPosition < mSkipDrawStartCount || childAdapterPosition >= endNotDraw) { + continue; + } + parent.getLayoutManager().getDecoratedBoundsWithMargins(child, mBounds); + final int right = mBounds.right + Math.round(ViewCompat.getTranslationX(child)); + final int left = right - mDivider.getIntrinsicWidth(); + mDivider.setBounds(left, top, right, bottom); + mDivider.draw(canvas); + } + canvas.restore(); + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, + RecyclerView.State state) { + if (mOrientation == VERTICAL) { + outRect.set(0, 0, 0, mDivider.getIntrinsicHeight()); + } else { + outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0); + } + } +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/widget/recyclerview/HidingScrollListener.java b/lib_base/src/main/java/com/android/base/widget/recyclerview/HidingScrollListener.java new file mode 100644 index 0000000..6561a6c --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/recyclerview/HidingScrollListener.java @@ -0,0 +1,45 @@ +package com.android.base.widget.recyclerview; + +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; + +/** + * RecyclerView滑动时隐藏toolbar + */ +public abstract class HidingScrollListener extends RecyclerView.OnScrollListener { + + private static final int HIDE_THRESHOLD = 20; + private int scrolledDistance = 0; + private boolean controlsVisible = true; + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + int firstVisibleItem = ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition(); + //show views if first item is first visible position and views are hidden + if (firstVisibleItem == 0) { + if (!controlsVisible) { + onShow(); + controlsVisible = true; + } + } else { + if (scrolledDistance > HIDE_THRESHOLD && controlsVisible) { + onHide(); + controlsVisible = false; + scrolledDistance = 0; + } else if (scrolledDistance < -HIDE_THRESHOLD && !controlsVisible) { + onShow(); + controlsVisible = true; + scrolledDistance = 0; + } + } + if ((controlsVisible && dy > 0) || (!controlsVisible && dy < 0)) { // 显示 向上滑动 || 没有显示 向下滑动 + scrolledDistance += dy; + } + } + + public abstract void onHide(); + + public abstract void onShow(); + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/widget/recyclerview/MarginDecoration.java b/lib_base/src/main/java/com/android/base/widget/recyclerview/MarginDecoration.java new file mode 100644 index 0000000..b0c4ea8 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/recyclerview/MarginDecoration.java @@ -0,0 +1,39 @@ +package com.android.base.widget.recyclerview; + +import android.graphics.Rect; +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; +import android.view.View; + + +public class MarginDecoration extends RecyclerView.ItemDecoration { + + private int mTop; + private int mLeft; + private int mRight; + private int mBottom; + + public MarginDecoration(int left, int top, int right, int bottom) { + mTop = top; + mBottom = bottom; + mRight = right; + mLeft = left; + } + + public MarginDecoration(int margin) { + mTop = margin; + mBottom = margin; + mRight = margin; + mLeft = margin; + } + + @Override + public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { + super.getItemOffsets(outRect, view, parent, state); + outRect.top = mTop; + outRect.bottom = mBottom; + outRect.left = mLeft; + outRect.right = mRight; + } + +} diff --git a/lib_base/src/main/java/com/android/base/widget/recyclerview/OnRecyclerViewScrollBottomListener.java b/lib_base/src/main/java/com/android/base/widget/recyclerview/OnRecyclerViewScrollBottomListener.java new file mode 100644 index 0000000..0753096 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/recyclerview/OnRecyclerViewScrollBottomListener.java @@ -0,0 +1,99 @@ +package com.android.base.widget.recyclerview; + +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.StaggeredGridLayoutManager; + +/** + * RecyclerView多布局通用滑动底部监听器 + */ +@SuppressWarnings("all") +public abstract class OnRecyclerViewScrollBottomListener extends RecyclerView.OnScrollListener { + + /** + * layoutManager的类型(枚举) + */ + private int mLayoutManagerType; + + private static final int LINEAR = 1; + private static final int GRID = 2; + private static final int STAGGERED_GRID = 3; + + /** + * 最后一个的位置 + */ + private int[] mLastPositions; + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); + + if (mLayoutManagerType == 0) { + if (layoutManager instanceof GridLayoutManager) { + mLayoutManagerType = GRID; + } else if (layoutManager instanceof LinearLayoutManager) { + mLayoutManagerType = LINEAR; + } else if (layoutManager instanceof StaggeredGridLayoutManager) { + mLayoutManagerType = STAGGERED_GRID; + } else { + //do nothing + } + } + + //最后一个可见的item的位置 + int lastVisibleItemPosition; + switch (mLayoutManagerType) { + case LINEAR: + lastVisibleItemPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition(); + break; + case GRID: + lastVisibleItemPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition(); + break; + case STAGGERED_GRID: + StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager; + if (mLastPositions == null) { + mLastPositions = new int[staggeredGridLayoutManager.getSpanCount()]; + } + staggeredGridLayoutManager.findLastVisibleItemPositions(mLastPositions); + lastVisibleItemPosition = findMax(mLastPositions); + break; + default: { + throw new IllegalStateException("un support layoutManager"); + } + } + + int visibleItemCount = layoutManager.getChildCount(); + int totalItemCount = layoutManager.getItemCount(); + + if ((visibleItemCount > 0 && (lastVisibleItemPosition) >= totalItemCount - 1)) { + onBottom(); + } else { + onLeaveBottom(); + } + } + + private int findMax(int[] lastPositions) { + int max = lastPositions[0]; + for (int value : lastPositions) { + if (value > max) { + max = value; + } + } + return max; + } + + + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + super.onScrollStateChanged(recyclerView, newState); + } + + + protected abstract void onBottom(); + + protected abstract void onLeaveBottom(); + + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/widget/viewpager/BannerPagerAdapter.java b/lib_base/src/main/java/com/android/base/widget/viewpager/BannerPagerAdapter.java new file mode 100644 index 0000000..dcd8615 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/viewpager/BannerPagerAdapter.java @@ -0,0 +1,91 @@ +package com.android.base.widget.viewpager; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewCompat; +import android.text.TextUtils; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import com.android.base.imageloader.ImageLoaderFactory; + +import java.util.List; + + +class BannerPagerAdapter extends PagerAdapter { + + static ScalableBannerPagerFactory sBannerPagerFactory; + + private final String mTransitionName; + private final Context mContext; + private final List mEntities; + private OnPageClickListener mClickListener; + private boolean mIsLooper; + + BannerPagerAdapter(Context context, List entities, String transitionName) { + mContext = context; + this.mEntities = entities; + mTransitionName = transitionName; + mIsLooper = mEntities.size() > 1; + } + + void setOnBannerClickListener(OnPageClickListener clickListener) { + this.mClickListener = clickListener; + } + + @Override + public int getCount() { + return mEntities.size(); + } + + @Override + public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { + return view == object; + } + + @NonNull + @Override + public Object instantiateItem(@NonNull ViewGroup container, final int position) { + if (sBannerPagerFactory == null) { + throw new NullPointerException("You need to provide a ScalableBannerPagerFactory!"); + } + + ImageView imageView = sBannerPagerFactory.createBannerPagerView(mContext); + setTransitionName(imageView); + + sBannerPagerFactory.setOnClickListener(imageView, v -> { + if (mClickListener != null) { + mClickListener.onClick(imageView, mIsLooper ? position - 1 : position); + } + }); + + String url = mEntities.get(position); + ImageLoaderFactory.getImageLoader().display(imageView, url); + container.addView(imageView, 0); + return imageView; + } + + private void setTransitionName(ImageView bannerLayout) { + if (!TextUtils.isEmpty(mTransitionName)) { + ViewCompat.setTransitionName(bannerLayout, mTransitionName); + } + } + + @Override + public int getItemPosition(@NonNull Object object) { + return POSITION_NONE; + } + + @Override + public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + container.removeView((View) object); + } + + @Override + public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + super.setPrimaryItem(container, position, object); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/widget/viewpager/IPagerNumberView.java b/lib_base/src/main/java/com/android/base/widget/viewpager/IPagerNumberView.java new file mode 100644 index 0000000..af2a24a --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/viewpager/IPagerNumberView.java @@ -0,0 +1,16 @@ +package com.android.base.widget.viewpager; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-02 12:35 + */ +public interface IPagerNumberView { + + void setViewPager(ZViewPager viewPager); + + void setPageSize(int i); + + void setPageScrolled(int position, float positionOffset); + +} diff --git a/lib_base/src/main/java/com/android/base/widget/viewpager/NoScrollViewPager.java b/lib_base/src/main/java/com/android/base/widget/viewpager/NoScrollViewPager.java new file mode 100644 index 0000000..b520c13 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/viewpager/NoScrollViewPager.java @@ -0,0 +1,37 @@ +package com.android.base.widget.viewpager; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.view.MotionEvent; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-01-03 15:38 + */ +public class NoScrollViewPager extends ViewPager { + + public NoScrollViewPager(@NonNull Context context) { + super(context); + } + + public NoScrollViewPager(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouchEvent(MotionEvent ev) { + return false; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + return false; + } + +} diff --git a/lib_base/src/main/java/com/android/base/widget/viewpager/OnPageClickListener.java b/lib_base/src/main/java/com/android/base/widget/viewpager/OnPageClickListener.java new file mode 100644 index 0000000..385e228 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/viewpager/OnPageClickListener.java @@ -0,0 +1,7 @@ +package com.android.base.widget.viewpager; + +import android.view.View; + +public interface OnPageClickListener { + void onClick(View itemView, int position); +} diff --git a/lib_base/src/main/java/com/android/base/widget/viewpager/OptimizeBannerPagerAdapter.java b/lib_base/src/main/java/com/android/base/widget/viewpager/OptimizeBannerPagerAdapter.java new file mode 100644 index 0000000..5a46a16 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/viewpager/OptimizeBannerPagerAdapter.java @@ -0,0 +1,96 @@ +package com.android.base.widget.viewpager; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewCompat; +import android.support.v7.widget.AppCompatImageView; +import android.text.TextUtils; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import com.android.base.imageloader.ImageLoaderFactory; + +import java.util.ArrayList; +import java.util.List; + + +class OptimizeBannerPagerAdapter extends PagerAdapter { + + private final String mTransitionName; + private final Context mContext; + private final List mEntities; + private final List mLayouts = new ArrayList<>(); + private OnPageClickListener mOnPageClickListener; + private boolean mIsLooper; + + OptimizeBannerPagerAdapter(Context context, List entities, String transitionName) { + mContext = context; + mTransitionName = transitionName; + mEntities = entities; + mIsLooper = mEntities.size() > 1; + setLayouts(); + } + + private void setLayouts() { + ImageView view; + mLayouts.clear(); + for (int i = 0; i < mEntities.size(); i++) { + view = new AppCompatImageView(mContext); + mLayouts.add(view); + setTransitionName(view); + } + } + + private void setTransitionName(ImageView bannerLayout) { + if (!TextUtils.isEmpty(mTransitionName)) { + ViewCompat.setTransitionName(bannerLayout, mTransitionName); + } + } + + @Override + public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { + return view == object; + } + + @Override + public int getCount() { + return mEntities.size(); + } + + @NonNull + @Override + public Object instantiateItem(@NonNull ViewGroup container, final int position) { + ImageView image = mLayouts.get(position); + String url = mEntities.get(position); + image.setOnClickListener(v -> { + if (mOnPageClickListener != null) { + mOnPageClickListener.onClick(image, mIsLooper ? position - 1 : position); + } + }); + ImageLoaderFactory.getImageLoader().display(image, url); + container.addView(image, 0); + return mLayouts.get(position); + } + + @Override + public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + container.removeView(mLayouts.get(position)); + } + + @Override + public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + super.setPrimaryItem(container, position, object); + } + + @Override + public int getItemPosition(@NonNull Object object) { + return POSITION_NONE; + } + + void setOnBannerClickListener(OnPageClickListener onBannerClickListener) { + mOnPageClickListener = onBannerClickListener; + } + +} diff --git a/lib_base/src/main/java/com/android/base/widget/viewpager/PageNumberView.java b/lib_base/src/main/java/com/android/base/widget/viewpager/PageNumberView.java new file mode 100644 index 0000000..5943dac --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/viewpager/PageNumberView.java @@ -0,0 +1,111 @@ +package com.android.base.widget.viewpager; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.view.View; + +import com.android.base.R; +import com.android.base.utils.android.UnitConverter; + + +public class PageNumberView extends View implements IPagerNumberView { + + private static final String DIVISION = "/"; + + private int mPageSize; + private int mPosition; + + private int mCenterX; + + private Paint mPaint; + private int mTextHeight; + private int mTextBaseLine; + + public PageNumberView(Context context) { + this(context, null); + } + + public PageNumberView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public PageNumberView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PageNumberView); + + /*是否可以缩放*/ + int color = typedArray.getColor(R.styleable.PageNumberView_pnv_text_color, Color.BLACK); + /*用于支持5.0的transition动画*/ + int size = typedArray.getDimensionPixelSize(R.styleable.PageNumberView_pnv_text_size, UnitConverter.spToPx(14)); + + typedArray.recycle(); + + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mPaint.setTextSize(size); + mPaint.setColor(color); + + Paint.FontMetricsInt fontMetricsInt = mPaint.getFontMetricsInt(); + mTextHeight = fontMetricsInt.bottom - fontMetricsInt.top; + mTextBaseLine = mTextHeight - fontMetricsInt.bottom; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + String text = String.valueOf(mPageSize); + float textWidth = mPaint.measureText(text); + float divisionWidth = mPaint.measureText(DIVISION); + setMeasuredDimension((int) (textWidth * 2 + divisionWidth), mTextHeight); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + float divisionHalfSize = mPaint.measureText(DIVISION) / 2; + float left = mCenterX - divisionHalfSize; + float right = mCenterX + divisionHalfSize; + String positionText = String.valueOf(mPosition); + String sizeText = String.valueOf(mPageSize); + canvas.drawText(DIVISION, left, mTextBaseLine, mPaint); + canvas.drawText(positionText, left - mPaint.measureText(positionText), mTextBaseLine, mPaint); + canvas.drawText(sizeText, right, mTextBaseLine, mPaint); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + mCenterX = w / 2; + } + + @Override + public void setPageScrolled(int position, @SuppressWarnings("UnusedParameters") float positionOffset) { + mPosition = position; + if (mPageSize > 1) { + if (mPosition == 0) { + mPosition = mPageSize; + } else if (mPosition == mPageSize + 1) { + mPosition = 1; + } + } else { + mPosition = mPageSize; + } + invalidate(); + } + + @Override + public void setViewPager(ZViewPager viewPager) { + //no op + } + + @Override + public void setPageSize(int pageSize) { + mPageSize = pageSize; + mPosition = pageSize;//只有一個的時候 + requestLayout(); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/widget/viewpager/PhotoViewPager.java b/lib_base/src/main/java/com/android/base/widget/viewpager/PhotoViewPager.java new file mode 100644 index 0000000..b11a820 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/viewpager/PhotoViewPager.java @@ -0,0 +1,42 @@ +package com.android.base.widget.viewpager; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import timber.log.Timber; + +public class PhotoViewPager extends ViewPager { + + public PhotoViewPager(Context context) { + super(context); + } + + public PhotoViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + try { + return super.onInterceptTouchEvent(ev); + } catch (Exception e) { + Timber.d("onInterceptTouchEvent() called with: ev = [" + ev + "]"); + } + return false; + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouchEvent(MotionEvent ev) { + try { + return super.onTouchEvent(ev); + } catch (IllegalArgumentException ex) { + Timber.d("onTouchEvent() called with: ev = [" + ev + "]"); + } + return false; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/widget/viewpager/ScalableBannerPagerFactory.java b/lib_base/src/main/java/com/android/base/widget/viewpager/ScalableBannerPagerFactory.java new file mode 100644 index 0000000..0ada5ac --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/viewpager/ScalableBannerPagerFactory.java @@ -0,0 +1,32 @@ +package com.android.base.widget.viewpager; + +import android.content.Context; +import android.view.View; +import android.widget.ImageView; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-07-01 17:43 + */ +public interface ScalableBannerPagerFactory { + + /** + * return a scalable ImageView + */ + ImageView createBannerPagerView(Context context); + + /** + * Set a click listener for image view which is created by {@link #createBannerPagerView(Context)}, like PhotoView: + * + *
    {@code
    +     *         imageView.setOnPhotoTapListener((view, x, y) -> {
    +     *             if (mClickListener != null) {
    +     *                 mClickListener.onClick(imageView, mIsLooper ? position - 1 : position);
    +     *             }
    +     *         });
    +     * }
    + */ + void setOnClickListener(ImageView view, View.OnClickListener onClickListener); + +} diff --git a/lib_base/src/main/java/com/android/base/widget/viewpager/ZViewPager.java b/lib_base/src/main/java/com/android/base/widget/viewpager/ZViewPager.java new file mode 100644 index 0000000..30a2859 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/viewpager/ZViewPager.java @@ -0,0 +1,205 @@ +package com.android.base.widget.viewpager; + +import android.content.Context; +import android.content.res.TypedArray; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.view.View; +import android.widget.FrameLayout; + +import com.android.base.R; + +import java.util.ArrayList; +import java.util.List; + +/** + * 支持无限轮播的 ViewPager + * + * @author Ztiany + */ +public class ZViewPager extends FrameLayout { + + private final ViewPager mViewPager; + private IPagerNumberView mPageNumberView; + + private List mImageUrlList = new ArrayList<>(); + + private OnBannerPositionChangedListener mOnBannerPositionChangedListener; + + private boolean mScalable; + private String mTransitionName; + private OnPageClickListener mOnPageClickListener; + + public ZViewPager(Context context) { + this(context, null); + } + + public ZViewPager(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ZViewPager(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ZViewPager); + + /*是否可以缩放*/ + mScalable = typedArray.getBoolean(R.styleable.ZViewPager_zvp_scale, false); + /*用于支持5.0的transition动画*/ + mTransitionName = typedArray.getString(R.styleable.ZViewPager_zvp_item_transition_name); + int pageId = typedArray.getResourceId(R.styleable.ZViewPager_zvp_pager_number_id, -1); + + typedArray.recycle(); + + inflate(context, R.layout.base_widget_banner_view, this); + + mViewPager = getRootView().findViewById(R.id.base_widget_banner_vp); + + if (pageId != -1) { + View pageNumber = findViewById(pageId); + if (pageNumber instanceof IPagerNumberView) { + mPageNumberView = (IPagerNumberView) pageNumber; + mPageNumberView.setViewPager(this); + } + } + } + + public void setOnBannerPositionChangedListener(OnBannerPositionChangedListener onBannerPositionChangedListener) { + mOnBannerPositionChangedListener = onBannerPositionChangedListener; + } + + public void setPageNumberView(IPagerNumberView pageNumberView) { + mPageNumberView = pageNumberView; + } + + public void setImages(List entities) { + if (entities == null || entities.isEmpty()) { + mImageUrlList.clear(); + mViewPager.setAdapter(null); + setPageSize(0); + return; + } + mImageUrlList.clear(); + setPageSize(entities.size()); + if (entities.size() > 1) { + addExtraPage(entities); + showBanner(); + setLooper(); + } else { + mImageUrlList.addAll(entities); + showBanner(); + } + } + + private void setPageSize(int pageSize) { + if (mPageNumberView != null) { + mPageNumberView.setPageSize(pageSize); + } + } + + private void setPageScrolled(int position, float positionOffset) { + if (mPageNumberView != null) { + mPageNumberView.setPageScrolled(position, positionOffset); + } + } + + public void setCurrentPosition(int position) { + if (mImageUrlList.size() > 1) { + if (position <= mImageUrlList.size() - 2) { + position++; + } else { + position = mImageUrlList.size() - 2; + } + } + mViewPager.setCurrentItem(position); + } + + private void setLooper() { + mViewPager.setCurrentItem(1, false); + mViewPager.clearOnPageChangeListeners(); + + mViewPager.addOnPageChangeListener( + new ViewPager.SimpleOnPageChangeListener() { + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + if (positionOffsetPixels == 0.0) { + setPageScrolled(position, positionOffset); + } + } + + @Override + public void onPageScrollStateChanged(int state) { + super.onPageScrollStateChanged(state); + //(positionOffset为0的时候,并不一定是切换完成,所以动画还在执行,强制再次切换,就会闪屏) + switch (state) { + case ViewPager.SCROLL_STATE_IDLE:// 空闲状态,没有任何滚动正在进行(表明完成滚动) + setViewPagerItemPosition(mViewPager.getCurrentItem()); + break; + case ViewPager.SCROLL_STATE_DRAGGING:// 正在拖动page状态 + break; + case ViewPager.SCROLL_STATE_SETTLING:// 手指已离开屏幕,自动完成剩余的动画效果 + break; + } + } + + @Override + public void onPageSelected(int position) { + if (mOnBannerPositionChangedListener != null) { + if (mImageUrlList.size() > 1) { + if (position == 0) { + position = mImageUrlList.size() - 3; + } else if (position == mImageUrlList.size() - 1) { + position = 0; + } else { + position--; + } + } + mOnBannerPositionChangedListener.onPagePositionChanged(position); + } + } + }); + } + + private void addExtraPage(List entities) { + mImageUrlList.add(entities.get(entities.size() - 1)); + mImageUrlList.addAll(entities); + mImageUrlList.add(entities.get(0)); + } + + private void showBanner() { + PagerAdapter adapter; + if (mScalable) { + BannerPagerAdapter bannerPagerAdapter = new BannerPagerAdapter(getContext(), mImageUrlList, mTransitionName); + bannerPagerAdapter.setOnBannerClickListener(mOnPageClickListener); + adapter = bannerPagerAdapter; + } else { + OptimizeBannerPagerAdapter optimizeBannerPagerAdapter = new OptimizeBannerPagerAdapter(getContext(), mImageUrlList, mTransitionName); + optimizeBannerPagerAdapter.setOnBannerClickListener(mOnPageClickListener); + adapter = optimizeBannerPagerAdapter; + } + mViewPager.setAdapter(adapter); + } + + public void setOnPageClickListener(OnPageClickListener onPageClickListener) { + mOnPageClickListener = onPageClickListener; + } + + private void setViewPagerItemPosition(int position) { + if (position == mImageUrlList.size() - 1) { + mViewPager.setCurrentItem(1, false); + } else if (position == 0) { + mViewPager.setCurrentItem(mImageUrlList.size() - 2, false); + } + } + + public interface OnBannerPositionChangedListener { + void onPagePositionChanged(int position); + } + + public static void setScalableBannerPagerFactory(ScalableBannerPagerFactory scalableBannerPagerFactory) { + BannerPagerAdapter.sBannerPagerFactory = scalableBannerPagerFactory; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/res/drawable-xxhdpi/base_img_error.png b/lib_base/src/main/res/drawable-xxhdpi/base_img_error.png new file mode 100644 index 0000000..d79ee1f Binary files /dev/null and b/lib_base/src/main/res/drawable-xxhdpi/base_img_error.png differ diff --git a/lib_base/src/main/res/drawable-xxhdpi/base_img_no_network.png b/lib_base/src/main/res/drawable-xxhdpi/base_img_no_network.png new file mode 100644 index 0000000..e3b51e8 Binary files /dev/null and b/lib_base/src/main/res/drawable-xxhdpi/base_img_no_network.png differ diff --git a/lib_base/src/main/res/layout/base_fragment_list.xml b/lib_base/src/main/res/layout/base_fragment_list.xml new file mode 100644 index 0000000..347f478 --- /dev/null +++ b/lib_base/src/main/res/layout/base_fragment_list.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/lib_base/src/main/res/layout/base_fragment_refresh_list.xml b/lib_base/src/main/res/layout/base_fragment_refresh_list.xml new file mode 100644 index 0000000..534edcd --- /dev/null +++ b/lib_base/src/main/res/layout/base_fragment_refresh_list.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + diff --git a/lib_base/src/main/res/layout/base_layout_empty.xml b/lib_base/src/main/res/layout/base_layout_empty.xml new file mode 100644 index 0000000..b43a2ff --- /dev/null +++ b/lib_base/src/main/res/layout/base_layout_empty.xml @@ -0,0 +1,43 @@ + + + + + + + + + + \ No newline at end of file diff --git a/lib_base/src/main/res/layout/base_layout_error.xml b/lib_base/src/main/res/layout/base_layout_error.xml new file mode 100644 index 0000000..a6c6f7d --- /dev/null +++ b/lib_base/src/main/res/layout/base_layout_error.xml @@ -0,0 +1,46 @@ + + + + + + + + + + \ No newline at end of file diff --git a/lib_base/src/main/res/layout/base_layout_loading.xml b/lib_base/src/main/res/layout/base_layout_loading.xml new file mode 100644 index 0000000..0f60cb5 --- /dev/null +++ b/lib_base/src/main/res/layout/base_layout_loading.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/lib_base/src/main/res/layout/base_layout_no_network.xml b/lib_base/src/main/res/layout/base_layout_no_network.xml new file mode 100644 index 0000000..abffc91 --- /dev/null +++ b/lib_base/src/main/res/layout/base_layout_no_network.xml @@ -0,0 +1,46 @@ + + + + + + + + + + \ No newline at end of file diff --git a/lib_base/src/main/res/layout/base_layout_server_error.xml b/lib_base/src/main/res/layout/base_layout_server_error.xml new file mode 100644 index 0000000..d4e901b --- /dev/null +++ b/lib_base/src/main/res/layout/base_layout_server_error.xml @@ -0,0 +1,46 @@ + + + + + + + + + + \ No newline at end of file diff --git a/lib_base/src/main/res/layout/base_layout_toolbar.xml b/lib_base/src/main/res/layout/base_layout_toolbar.xml new file mode 100644 index 0000000..c4781bc --- /dev/null +++ b/lib_base/src/main/res/layout/base_layout_toolbar.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/lib_base/src/main/res/layout/base_widget_banner_view.xml b/lib_base/src/main/res/layout/base_widget_banner_view.xml new file mode 100644 index 0000000..47a06e5 --- /dev/null +++ b/lib_base/src/main/res/layout/base_widget_banner_view.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/lib_base/src/main/res/values-en/base_strings.xml b/lib_base/src/main/res/values-en/base_strings.xml new file mode 100644 index 0000000..2e95ced --- /dev/null +++ b/lib_base/src/main/res/values-en/base_strings.xml @@ -0,0 +1,32 @@ + + + Confirm + Cancel + + + + Failed to load/(ㄒoㄒ)/~~ + Click me to retry + There is no data oh ! + Network is not accessible ! + Server error! + + + %s permission denied + We need you to provide %s permission so that we can serve you better. + Allow %2$s in Settings->Apps->%1$s to use %1$s features. + Settings + + + Calendar + Camera + Contacts + Location + Microphone + Phone information + Body Sensors + SMS + Storage + Accounts + + diff --git a/lib_base/src/main/res/values/base_attrs.xml b/lib_base/src/main/res/values/base_attrs.xml new file mode 100644 index 0000000..2745c3e --- /dev/null +++ b/lib_base/src/main/res/values/base_attrs.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib_base/src/main/res/values/base_color_palette.xml b/lib_base/src/main/res/values/base_color_palette.xml new file mode 100644 index 0000000..dd06dde --- /dev/null +++ b/lib_base/src/main/res/values/base_color_palette.xml @@ -0,0 +1,18 @@ + + + + #0000FF + #FFFF00 + #FFA500 + #FF0000 + #800080 + #ff00ff00 + #FF00FFFF + #FFD700 + #C0C0C0 + #FFFFFF + #000000 + #ff969696 + #00000000 + + \ No newline at end of file diff --git a/lib_base/src/main/res/values/base_ids.xml b/lib_base/src/main/res/values/base_ids.xml new file mode 100644 index 0000000..c2d29c2 --- /dev/null +++ b/lib_base/src/main/res/values/base_ids.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib_base/src/main/res/values/base_strings.xml b/lib_base/src/main/res/values/base_strings.xml new file mode 100644 index 0000000..5960186 --- /dev/null +++ b/lib_base/src/main/res/values/base_strings.xml @@ -0,0 +1,31 @@ + + + 确认 + 取消 + + + 加载失败了/(ㄒoㄒ)/~~ + 点我重试吧 + 还没有相关数据哦! + 网络不可访问! + 服务器开小差了! + + + 去设置 + %s 权限被拒绝 + 我们需要您提供%s权限以使我们能够更好的为您服务。 + 在设置->应用->%1$s->权限管理中开启%2$s权限,以正常使用%1$s功能。 + + + 日历 + 相机 + 通讯录 + 位置信息 + 麦克风 + 获取手机信息 + 身体传感器 + 短信 + 存储空间 + 账户 + + diff --git a/lib_base/src/main/res/values/base_styles.xml b/lib_base/src/main/res/values/base_styles.xml new file mode 100644 index 0000000..0a99ebd --- /dev/null +++ b/lib_base/src/main/res/values/base_styles.xml @@ -0,0 +1,34 @@ + + + + + + + \ No newline at end of file diff --git a/lib_base/src/test/java/com/android/base/rx/RxBusTest.java b/lib_base/src/test/java/com/android/base/rx/RxBusTest.java new file mode 100644 index 0000000..8676648 --- /dev/null +++ b/lib_base/src/test/java/com/android/base/rx/RxBusTest.java @@ -0,0 +1,127 @@ +package com.android.base.rx; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-04-18 16:39 + */ +public class RxBusTest { + + private RxBus mRxBus = RxBus.newInstance(); + + private A mA; + private A mA1; + private B mB; + private B mB1; + private C mC; + private C mC1; + + private static class A { + final String name; + + private A(String name) { + this.name = "A " + name; + } + + @Override + public String toString() { + return "A{" + + "name='" + name + '\'' + + '}'; + } + } + + private static class C extends A { + private C(String name) { + super(name); + } + + @Override + public String toString() { + return "C {" + + "name='" + name + '\'' + + '}'; + } + } + + private static class B { + private final String name; + + private B(String name) { + this.name = "B " + name; + } + + @Override + public String toString() { + return "B{" + + "name='" + name + '\'' + + '}'; + } + } + + + @Before + public void start() { + + mRxBus.toObservable(A.class).subscribe(a -> { + System.out.println(a); + mA = a; + }); + + mRxBus.toObservable("A", A.class) + .subscribe(a -> { + System.out.println(" has id " + a); + mA1 = a; + }); + + mRxBus.toObservable(B.class).subscribe(b -> { + System.out.println(b); + mB = b; + }); + + mRxBus.toObservable("B", B.class) + .subscribe(b -> { + System.out.println(" has id " + b); + mB1 = b; + }); + + mRxBus.toObservable(C.class).subscribe(c -> { + System.out.println(c); + mC = c; + }); + + mRxBus.toObservable("C", C.class) + .subscribe(c -> { + System.out.println(" has id " + c); + mC1 = c; + }); + } + + @Test + public void send() throws Exception { + A a = new A("A01"); + mRxBus.send(a); + Assert.assertSame(a, mA); + Assert.assertNotSame(a, mA1); + + B b = new B("B01"); + mRxBus.send("B", b); + Assert.assertNotSame(b, mB); + Assert.assertSame(b, mB1); + + + C c = new C("C01"); + mRxBus.send("C", c); + Assert.assertNotSame(c, mC); + Assert.assertSame(c, mC1); + + mRxBus.send("A", c); + mRxBus.send(c); + + } + +} \ No newline at end of file diff --git a/lib_base/src/test/java/com/android/base/rx/TestApplication.java b/lib_base/src/test/java/com/android/base/rx/TestApplication.java new file mode 100644 index 0000000..c8dc357 --- /dev/null +++ b/lib_base/src/test/java/com/android/base/rx/TestApplication.java @@ -0,0 +1,28 @@ +package com.android.base.rx; + +import android.app.Application; +import android.content.Context; + +import com.android.base.app.BaseKit; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-28 17:50 + */ +public class TestApplication extends Application { + + @Override + protected void attachBaseContext(Context base) { + super.attachBaseContext(base); + BaseKit.get().onApplicationAttachBaseContext(base); + } + + @Override + public void onCreate() { + super.onCreate(); + BaseKit.get().onApplicationCreate(this); + } + + +} diff --git a/lib_base/src/test/java/com/android/base/rx/TestBaseActivity.java b/lib_base/src/test/java/com/android/base/rx/TestBaseActivity.java new file mode 100644 index 0000000..8fe9203 --- /dev/null +++ b/lib_base/src/test/java/com/android/base/rx/TestBaseActivity.java @@ -0,0 +1,29 @@ +package com.android.base.rx; + +import android.os.Bundle; +import android.support.annotation.Nullable; + +import com.android.base.app.activity.BaseActivity; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-06-22 09:37 + */ +public class TestBaseActivity extends BaseActivity { + + @Override + protected void initialize(@Nullable Bundle savedInstanceState) { + super.initialize(savedInstanceState); + } + + @Override + protected Object layout() { + return 0; + } + + @Override + protected void setupView(@Nullable Bundle savedInstanceState) { + } + +} diff --git a/lib_base/src/test/java/com/android/base/rx/TestListFragment.java b/lib_base/src/test/java/com/android/base/rx/TestListFragment.java new file mode 100644 index 0000000..4244264 --- /dev/null +++ b/lib_base/src/test/java/com/android/base/rx/TestListFragment.java @@ -0,0 +1,26 @@ +package com.android.base.rx; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.View; + +import com.android.base.adapter.recycler.MultiTypeAdapter; +import com.android.base.app.fragment.BaseListFragment; +import com.android.base.app.ui.AutoPageNumber; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-04-27 15:05 + */ +public class TestListFragment extends BaseListFragment { + + @Override + protected void onViewPrepared(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewPrepared(view, savedInstanceState); + MultiTypeAdapter recyclerAdapter = new MultiTypeAdapter(getContext()); + setupLoadMore(recyclerAdapter, new AutoPageNumber(this,recyclerAdapter)); + } + +} diff --git a/lib_base/src/test/java/com/android/base/rx/TestStateFragment.java b/lib_base/src/test/java/com/android/base/rx/TestStateFragment.java new file mode 100644 index 0000000..0145786 --- /dev/null +++ b/lib_base/src/test/java/com/android/base/rx/TestStateFragment.java @@ -0,0 +1,44 @@ +package com.android.base.rx; + +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.View; + +import com.android.base.app.fragment.BaseStateFragment; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-06-22 09:38 + */ +public class TestStateFragment extends BaseStateFragment { + + @Override + public void onAttach(Context context) { + super.onAttach(context); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + protected void onViewPrepared(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewPrepared(view, savedInstanceState); + getStateLayoutConfig(); + } + + @Override + protected void onRefresh() { + super.onRefresh(); + } + + @Override + public void refreshCompleted() { + super.refreshCompleted(); + } + +} diff --git a/lib_cache/.gitignore b/lib_cache/.gitignore new file mode 100644 index 0000000..c06fb85 --- /dev/null +++ b/lib_cache/.gitignore @@ -0,0 +1,22 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +*.iml +.idea/ +.gradle +/local.properties +.DS_Store +/build +/captures +*.apk +*.ap_ +*.dex +*.class +bin/ +gen/ +local.properties \ No newline at end of file diff --git a/lib_cache/README.md b/lib_cache/README.md new file mode 100644 index 0000000..00a6f40 --- /dev/null +++ b/lib_cache/README.md @@ -0,0 +1,8 @@ +# 缓存库 + +目前使用的核心缓存库: + +- 微信:MMKV +- DiskLruCache + +对外统一暴露接口: `Storage` \ No newline at end of file diff --git a/lib_cache/build.gradle b/lib_cache/build.gradle new file mode 100644 index 0000000..25af95f --- /dev/null +++ b/lib_cache/build.gradle @@ -0,0 +1,35 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + + compileSdkVersion rootProject.compileSdkVersion + buildToolsVersion rootProject.buildToolsVersion + + defaultConfig { + minSdkVersion rootProject.minSdkVersion + targetSdkVersion rootProject.targetSdkVersion + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation androidLibraries.androidAnnotations + implementation thirdLibraries.mmkv + implementation thirdLibraries.gson + implementation thirdLibraries.supportOptional + compileOnly thirdLibraries.rxJava + compileOnly kotlinLibraries.kotlinStdlib +} diff --git a/lib_cache/proguard-rules.pro b/lib_cache/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/lib_cache/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/lib_cache/src/main/AndroidManifest.xml b/lib_cache/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6f89825 --- /dev/null +++ b/lib_cache/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/lib_cache/src/main/java/com/android/sdk/cache/CacheEntity.java b/lib_cache/src/main/java/com/android/sdk/cache/CacheEntity.java new file mode 100644 index 0000000..f9f0c1f --- /dev/null +++ b/lib_cache/src/main/java/com/android/sdk/cache/CacheEntity.java @@ -0,0 +1,15 @@ +package com.android.sdk.cache; + +class CacheEntity { + + String mJsonData;//数据 + long mCacheTime;//有效时间 + long mStoreTime;//存储时间戳 + + CacheEntity(String jsonData, long cacheTime) { + mJsonData = jsonData; + mCacheTime = cacheTime; + mStoreTime = System.currentTimeMillis(); + } + +} \ No newline at end of file diff --git a/lib_cache/src/main/java/com/android/sdk/cache/CommonImpl.java b/lib_cache/src/main/java/com/android/sdk/cache/CommonImpl.java new file mode 100644 index 0000000..2c6efe1 --- /dev/null +++ b/lib_cache/src/main/java/com/android/sdk/cache/CommonImpl.java @@ -0,0 +1,79 @@ +package com.android.sdk.cache; + +import android.text.TextUtils; +import android.util.Log; + +import com.github.dmstocking.optional.java.util.Optional; + +import java.lang.reflect.Type; + +import io.reactivex.Flowable; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-01 17:11 + */ +final class CommonImpl { + + static void putEntity(String key, Object entity, long cacheTime, Storage storage) { + if (entity == null) { + storage.remove(key); + return; + } + CacheEntity cacheEntity = new CacheEntity(JsonUtils.toJson(entity), cacheTime); + storage.putString(key, JsonUtils.toJson(cacheEntity)); + } + + private static String getCacheEntity(String key, Storage storage) { + String cacheStr = storage.getString(key, null); + if (TextUtils.isEmpty(cacheStr)) { + return null; + } + + CacheEntity cacheEntity = JsonUtils.fromJson(cacheStr, CacheEntity.class); + + if (cacheEntity == null) { + return null; + } + + if (cacheEntity.mCacheTime == 0) { + return cacheEntity.mJsonData; + } + + if (System.currentTimeMillis() - cacheEntity.mStoreTime < cacheEntity.mCacheTime) { + return cacheEntity.mJsonData; + } else { + storage.remove(key); + } + return null; + } + + static T getEntity(String key, Type clazz, Storage storage) { + String cacheEntity = getCacheEntity(key, storage); + Log.d("cache", "cacheEntity = " + cacheEntity); + if (cacheEntity != null) { + return JsonUtils.fromJson(cacheEntity, clazz); + } + return null; + } + + static Flowable flowableEntity(String key, Type clazz, Storage storage) { + return Flowable.defer(() -> { + T entity = storage.getEntity(key, clazz); + if (entity == null) { + return Flowable.empty(); + } else { + return Flowable.just(entity); + } + }); + } + + static Flowable> flowableOptionalEntity(String key, Type clazz, Storage storage) { + return Flowable.fromCallable(() -> { + T entity = storage.getEntity(key, clazz); + return Optional.ofNullable(entity); + }); + } + +} \ No newline at end of file diff --git a/lib_cache/src/main/java/com/android/sdk/cache/DiskLruCache.java b/lib_cache/src/main/java/com/android/sdk/cache/DiskLruCache.java new file mode 100644 index 0000000..96ae866 --- /dev/null +++ b/lib_cache/src/main/java/com/android/sdk/cache/DiskLruCache.java @@ -0,0 +1,980 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.sdk.cache; + +import java.io.BufferedWriter; +import java.io.Closeable; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A cache that uses a bounded amount of space on a filesystem. Each cache + * entry has a string key and a fixed number of values. Each key must match + * the regex [a-z0-9_-]{1,120}. Values are byte sequences, + * accessible as streams or files. Each value must be between {@code 0} and + * {@code Integer.MAX_VALUE} bytes in length. + *

    + *

    The cache stores its data in a directory on the filesystem. This + * directory must be exclusive to the cache; the cache may delete or overwrite + * files from its directory. It is an error for multiple processes to use the + * same cache directory at the same time. + *

    + *

    This cache limits the number of bytes that it will store on the + * filesystem. When the number of stored bytes exceeds the limit, the cache will + * remove entries in the background until the limit is satisfied. The limit is + * not strict: the cache may temporarily exceed it while waiting for files to be + * deleted. The limit does not include filesystem overhead or the cache + * journal so space-sensitive applications should set a conservative limit. + *

    + *

    Clients call {@link #edit} to create or update the values of an entry. An + * entry may have only one editor at one time; if a value is not available to be + * edited then {@link #edit} will return null. + *

      + *
    • When an entry is being created it is necessary to + * supply a full set of values; the empty value should be used as a + * placeholder if necessary. + *
    • When an entry is being edited, it is not necessary + * to supply data for every value; values default to their previous + * value. + *
    + * Every {@link #edit} call must be matched by a call to {@link Editor#commit} + * or {@link Editor#abort}. Committing is atomic: a read observes the full set + * of values as they were before or after the commit, but never a mix of values. + *

    + *

    Clients call {@link #get} to read a snapshot of an entry. The read will + * observe the value at the time that {@link #get} was called. Updates and + * removals after the call do not impact ongoing reads. + *

    + *

    This class is tolerant of some I/O errors. If files are missing from the + * filesystem, the corresponding entries will be dropped from the cache. If + * an error occurs while writing a cache value, the edit will fail silently. + * Callers should handle other problems by catching {@code IOException} and + * responding appropriately. + */ +public final class DiskLruCache implements Closeable { + + static final String JOURNAL_FILE = "journal"; + static final String JOURNAL_FILE_TEMP = "journal.tmp"; + static final String JOURNAL_FILE_BACKUP = "journal.bkp"; + static final String MAGIC = "libcore.io.DiskLruCache"; + static final String VERSION_1 = "1"; + static final long ANY_SEQUENCE_NUMBER = -1; + static final String STRING_KEY_PATTERN = "[a-z0-9_-]{1,120}"; + static final Pattern LEGAL_KEY_PATTERN = Pattern.compile(STRING_KEY_PATTERN); + private static final String CLEAN = "CLEAN"; + private static final String DIRTY = "DIRTY"; + private static final String REMOVE = "REMOVE"; + private static final String READ = "READ"; + + /* + * This cache uses a journal file named "journal". A typical journal file + * looks like this: + * libcore.io.DiskLruCache + * 1 + * 100 + * 2 + * + * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 + * DIRTY 335c4c6028171cfddfbaae1a9c313c52 + * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342 + * REMOVE 335c4c6028171cfddfbaae1a9c313c52 + * DIRTY 1ab96a171faeeee38496d8b330771a7a + * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234 + * READ 335c4c6028171cfddfbaae1a9c313c52 + * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6 + * + * The first five lines of the journal form its header. They are the + * constant string "libcore.io.DiskLruCache", the disk cache's version, + * the application's version, the value count, and a blank line. + * + * Each of the subsequent lines in the file is a record of the state of a + * cache entry. Each line contains space-separated values: a state, a key, + * and optional state-specific values. + * o DIRTY lines track that an entry is actively being created or updated. + * Every successful DIRTY action should be followed by a CLEAN or REMOVE + * action. DIRTY lines without a matching CLEAN or REMOVE indicate that + * temporary files may need to be deleted. + * o CLEAN lines track a cache entry that has been successfully published + * and may be read. A publish line is followed by the lengths of each of + * its values. + * o READ lines track accesses for LRU. + * o REMOVE lines track entries that have been deleted. + * + * The journal file is appended to as cache operations occur. The journal may + * occasionally be compacted by dropping redundant lines. A temporary file named + * "journal.tmp" will be used during compaction; that file should be deleted if + * it exists when the cache is opened. + */ + + private final File directory; + private final File journalFile; + private final File journalFileTmp; + private final File journalFileBackup; + private final int appVersion; + private long maxSize; + private final int valueCount; + private long size = 0; + private Writer journalWriter; + private final LinkedHashMap lruEntries = + new LinkedHashMap(0, 0.75f, true); + private int redundantOpCount; + + /** + * To differentiate between old and current snapshots, each entry is given + * a sequence number each time an edit is committed. A snapshot is stale if + * its sequence number is not equal to its entry's sequence number. + */ + private long nextSequenceNumber = 0; + + /** + * This cache uses a single background thread to evict entries. + */ + final ThreadPoolExecutor executorService = + new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue()); + private final Callable cleanupCallable = new Callable() { + public Void call() throws Exception { + synchronized (DiskLruCache.this) { + if (journalWriter == null) { + return null; // Closed. + } + trimToSize(); + if (journalRebuildRequired()) { + rebuildJournal(); + redundantOpCount = 0; + } + } + return null; + } + }; + + private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) { + this.directory = directory; + this.appVersion = appVersion; + this.journalFile = new File(directory, JOURNAL_FILE); + this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP); + this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP); + this.valueCount = valueCount; + this.maxSize = maxSize; + } + + /** + * Opens the cache in {@code directory}, creating a cache if none exists + * there. + * + * @param directory a writable directory + * @param valueCount the number of values per cache entry. Must be positive. + * @param maxSize the maximum number of bytes this cache should use to store + * @throws IOException if reading or writing the cache directory fails + */ + public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) + throws IOException { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize <= 0"); + } + if (valueCount <= 0) { + throw new IllegalArgumentException("valueCount <= 0"); + } + + // If a bkp file exists, use it instead. + File backupFile = new File(directory, JOURNAL_FILE_BACKUP); + if (backupFile.exists()) { + File journalFile = new File(directory, JOURNAL_FILE); + // If journal file also exists just delete backup file. + if (journalFile.exists()) { + backupFile.delete(); + } else { + renameTo(backupFile, journalFile, false); + } + } + + // Prefer to pick up where we left off. + DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); + if (cache.journalFile.exists()) { + try { + cache.readJournal(); + cache.processJournal(); + return cache; + } catch (IOException journalIsCorrupt) { + System.out + .println("DiskLruCache " + + directory + + " is corrupt: " + + journalIsCorrupt.getMessage() + + ", removing"); + cache.delete(); + } + } + + // Create a new empty cache. + directory.mkdirs(); + cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); + cache.rebuildJournal(); + return cache; + } + + private void readJournal() throws IOException { + StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII); + try { + String magic = reader.readLine(); + String version = reader.readLine(); + String appVersionString = reader.readLine(); + String valueCountString = reader.readLine(); + String blank = reader.readLine(); + if (!MAGIC.equals(magic) + || !VERSION_1.equals(version) + || !Integer.toString(appVersion).equals(appVersionString) + || !Integer.toString(valueCount).equals(valueCountString) + || !"".equals(blank)) { + throw new IOException("unexpected journal header: [" + magic + ", " + version + ", " + + valueCountString + ", " + blank + "]"); + } + + int lineCount = 0; + while (true) { + try { + readJournalLine(reader.readLine()); + lineCount++; + } catch (EOFException endOfJournal) { + break; + } + } + redundantOpCount = lineCount - lruEntries.size(); + + // If we ended on a truncated line, rebuild the journal before appending to it. + if (reader.hasUnterminatedLine()) { + rebuildJournal(); + } else { + journalWriter = new BufferedWriter(new OutputStreamWriter( + new FileOutputStream(journalFile, true), Util.US_ASCII)); + } + } finally { + Util.closeQuietly(reader); + } + } + + private void readJournalLine(String line) throws IOException { + int firstSpace = line.indexOf(' '); + if (firstSpace == -1) { + throw new IOException("unexpected journal line: " + line); + } + + int keyBegin = firstSpace + 1; + int secondSpace = line.indexOf(' ', keyBegin); + final String key; + if (secondSpace == -1) { + key = line.substring(keyBegin); + if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) { + lruEntries.remove(key); + return; + } + } else { + key = line.substring(keyBegin, secondSpace); + } + + Entry entry = lruEntries.get(key); + if (entry == null) { + entry = new Entry(key); + lruEntries.put(key, entry); + } + + if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) { + String[] parts = line.substring(secondSpace + 1).split(" "); + entry.readable = true; + entry.currentEditor = null; + entry.setLengths(parts); + } else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) { + entry.currentEditor = new Editor(entry); + } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) { + // This work was already done by calling lruEntries.get(). + } else { + throw new IOException("unexpected journal line: " + line); + } + } + + /** + * Computes the initial size and collects garbage as a part of opening the + * cache. Dirty entries are assumed to be inconsistent and will be deleted. + */ + private void processJournal() throws IOException { + deleteIfExists(journalFileTmp); + for (Iterator i = lruEntries.values().iterator(); i.hasNext(); ) { + Entry entry = i.next(); + if (entry.currentEditor == null) { + for (int t = 0; t < valueCount; t++) { + size += entry.lengths[t]; + } + } else { + entry.currentEditor = null; + for (int t = 0; t < valueCount; t++) { + deleteIfExists(entry.getCleanFile(t)); + deleteIfExists(entry.getDirtyFile(t)); + } + i.remove(); + } + } + } + + /** + * Creates a new journal that omits redundant information. This replaces the + * current journal if it exists. + */ + private synchronized void rebuildJournal() throws IOException { + if (journalWriter != null) { + journalWriter.close(); + } + + Writer writer = new BufferedWriter( + new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII)); + try { + writer.write(MAGIC); + writer.write("\n"); + writer.write(VERSION_1); + writer.write("\n"); + writer.write(Integer.toString(appVersion)); + writer.write("\n"); + writer.write(Integer.toString(valueCount)); + writer.write("\n"); + writer.write("\n"); + + for (Entry entry : lruEntries.values()) { + if (entry.currentEditor != null) { + writer.write(DIRTY + ' ' + entry.key + '\n'); + } else { + writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); + } + } + } finally { + writer.close(); + } + + if (journalFile.exists()) { + renameTo(journalFile, journalFileBackup, true); + } + renameTo(journalFileTmp, journalFile, false); + journalFileBackup.delete(); + + journalWriter = new BufferedWriter( + new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII)); + } + + private static void deleteIfExists(File file) throws IOException { + if (file.exists() && !file.delete()) { + throw new IOException(); + } + } + + private static void renameTo(File from, File to, boolean deleteDestination) throws IOException { + if (deleteDestination) { + deleteIfExists(to); + } + if (!from.renameTo(to)) { + throw new IOException(); + } + } + + /** + * Returns a snapshot of the entry named {@code key}, or null if it doesn't + * exist is not currently readable. If a value is returned, it is moved to + * the head of the LRU queue. + */ + public synchronized Snapshot get(String key) throws IOException { + checkNotClosed(); + validateKey(key); + Entry entry = lruEntries.get(key); + if (entry == null) { + return null; + } + + if (!entry.readable) { + return null; + } + + // Open all streams eagerly to guarantee that we see a single published + // snapshot. If we opened streams lazily then the streams could come + // from different edits. + InputStream[] ins = new InputStream[valueCount]; + try { + for (int i = 0; i < valueCount; i++) { + ins[i] = new FileInputStream(entry.getCleanFile(i)); + } + } catch (FileNotFoundException e) { + // A file must have been deleted manually! + for (int i = 0; i < valueCount; i++) { + if (ins[i] != null) { + Util.closeQuietly(ins[i]); + } else { + break; + } + } + return null; + } + + redundantOpCount++; + journalWriter.append(READ + ' ' + key + '\n'); + if (journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + + return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths); + } + + /** + * Returns an editor for the entry named {@code key}, or null if another + * edit is in progress. + */ + public Editor edit(String key) throws IOException { + return edit(key, ANY_SEQUENCE_NUMBER); + } + + private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException { + checkNotClosed(); + validateKey(key); + Entry entry = lruEntries.get(key); + if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null + || entry.sequenceNumber != expectedSequenceNumber)) { + return null; // Snapshot is stale. + } + if (entry == null) { + entry = new Entry(key); + lruEntries.put(key, entry); + } else if (entry.currentEditor != null) { + return null; // Another edit is in progress. + } + + Editor editor = new Editor(entry); + entry.currentEditor = editor; + + // Flush the journal before creating files to prevent file leaks. + journalWriter.write(DIRTY + ' ' + key + '\n'); + journalWriter.flush(); + return editor; + } + + /** + * Returns the directory where this cache stores its data. + */ + public File getDirectory() { + return directory; + } + + /** + * Returns the maximum number of bytes that this cache should use to store + * its data. + */ + public synchronized long getMaxSize() { + return maxSize; + } + + /** + * Changes the maximum number of bytes the cache can store and queues a job + * to trim the existing store, if necessary. + */ + public synchronized void setMaxSize(long maxSize) { + this.maxSize = maxSize; + executorService.submit(cleanupCallable); + } + + /** + * Returns the number of bytes currently being used to store the values in + * this cache. This may be greater than the max size if a background + * deletion is pending. + */ + public synchronized long size() { + return size; + } + + private synchronized void completeEdit(Editor editor, boolean success) throws IOException { + Entry entry = editor.entry; + if (entry.currentEditor != editor) { + throw new IllegalStateException(); + } + + // If this edit is creating the entry for the first time, every index must have a value. + if (success && !entry.readable) { + for (int i = 0; i < valueCount; i++) { + if (!editor.written[i]) { + editor.abort(); + throw new IllegalStateException("Newly created entry didn't create value for index " + i); + } + if (!entry.getDirtyFile(i).exists()) { + editor.abort(); + return; + } + } + } + + for (int i = 0; i < valueCount; i++) { + File dirty = entry.getDirtyFile(i); + if (success) { + if (dirty.exists()) { + File clean = entry.getCleanFile(i); + dirty.renameTo(clean); + long oldLength = entry.lengths[i]; + long newLength = clean.length(); + entry.lengths[i] = newLength; + size = size - oldLength + newLength; + } + } else { + deleteIfExists(dirty); + } + } + + redundantOpCount++; + entry.currentEditor = null; + if (entry.readable | success) { + entry.readable = true; + journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); + if (success) { + entry.sequenceNumber = nextSequenceNumber++; + } + } else { + lruEntries.remove(entry.key); + journalWriter.write(REMOVE + ' ' + entry.key + '\n'); + } + journalWriter.flush(); + + if (size > maxSize || journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + } + + /** + * We only rebuild the journal when it will halve the size of the journal + * and eliminate at least 2000 ops. + */ + private boolean journalRebuildRequired() { + final int redundantOpCompactThreshold = 2000; + return redundantOpCount >= redundantOpCompactThreshold // + && redundantOpCount >= lruEntries.size(); + } + + /** + * Drops the entry for {@code key} if it exists and can be removed. Entries + * actively being edited cannot be removed. + * + * @return true if an entry was removed. + */ + public synchronized boolean remove(String key) throws IOException { + checkNotClosed(); + validateKey(key); + Entry entry = lruEntries.get(key); + if (entry == null || entry.currentEditor != null) { + return false; + } + + for (int i = 0; i < valueCount; i++) { + File file = entry.getCleanFile(i); + if (file.exists() && !file.delete()) { + throw new IOException("failed to delete " + file); + } + size -= entry.lengths[i]; + entry.lengths[i] = 0; + } + + redundantOpCount++; + journalWriter.append(REMOVE + ' ' + key + '\n'); + lruEntries.remove(key); + + if (journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + + return true; + } + + /** + * Returns true if this cache has been closed. + */ + public synchronized boolean isClosed() { + return journalWriter == null; + } + + private void checkNotClosed() { + if (journalWriter == null) { + throw new IllegalStateException("cache is closed"); + } + } + + /** + * Force buffered operations to the filesystem. + */ + public synchronized void flush() throws IOException { + checkNotClosed(); + trimToSize(); + journalWriter.flush(); + } + + /** + * Closes this cache. Stored values will remain on the filesystem. + */ + public synchronized void close() throws IOException { + if (journalWriter == null) { + return; // Already closed. + } + for (Entry entry : new ArrayList(lruEntries.values())) { + if (entry.currentEditor != null) { + entry.currentEditor.abort(); + } + } + trimToSize(); + journalWriter.close(); + journalWriter = null; + } + + private void trimToSize() throws IOException { + while (size > maxSize) { + Map.Entry toEvict = lruEntries.entrySet().iterator().next(); + remove(toEvict.getKey()); + } + } + + /** + * Closes the cache and deletes all of its stored values. This will delete + * all files in the cache directory including files that weren't created by + * the cache. + */ + public void delete() throws IOException { + close(); + Util.deleteContents(directory); + } + + private void validateKey(String key) { + Matcher matcher = LEGAL_KEY_PATTERN.matcher(key); + if (!matcher.matches()) { + throw new IllegalArgumentException("keys must match regex " + + STRING_KEY_PATTERN + ": \"" + key + "\""); + } + } + + private static String inputStreamToString(InputStream in) throws IOException { + return Util.readFully(new InputStreamReader(in, Util.UTF_8)); + } + + /** + * A snapshot of the values for an entry. + */ + public final class Snapshot implements Closeable { + private final String key; + private final long sequenceNumber; + private final InputStream[] ins; + private final long[] lengths; + + private Snapshot(String key, long sequenceNumber, InputStream[] ins, long[] lengths) { + this.key = key; + this.sequenceNumber = sequenceNumber; + this.ins = ins; + this.lengths = lengths; + } + + /** + * Returns an editor for this snapshot's entry, or null if either the + * entry has changed since this snapshot was created or if another edit + * is in progress. + */ + public Editor edit() throws IOException { + return DiskLruCache.this.edit(key, sequenceNumber); + } + + /** + * Returns the unbuffered stream with the value for {@code index}. + */ + public InputStream getInputStream(int index) { + return ins[index]; + } + + /** + * Returns the string value for {@code index}. + */ + public String getString(int index) throws IOException { + return inputStreamToString(getInputStream(index)); + } + + /** + * Returns the byte length of the value for {@code index}. + */ + public long getLength(int index) { + return lengths[index]; + } + + public void close() { + for (InputStream in : ins) { + Util.closeQuietly(in); + } + } + } + + private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() { + @Override + public void write(int b) throws IOException { + // Eat all writes silently. Nom nom. + } + }; + + /** + * Edits the values for an entry. + */ + public final class Editor { + private final Entry entry; + private final boolean[] written; + private boolean hasErrors; + private boolean committed; + + private Editor(Entry entry) { + this.entry = entry; + this.written = (entry.readable) ? null : new boolean[valueCount]; + } + + /** + * Returns an unbuffered input stream to read the last committed value, + * or null if no value has been committed. + */ + public InputStream newInputStream(int index) throws IOException { + synchronized (DiskLruCache.this) { + if (entry.currentEditor != this) { + throw new IllegalStateException(); + } + if (!entry.readable) { + return null; + } + try { + return new FileInputStream(entry.getCleanFile(index)); + } catch (FileNotFoundException e) { + return null; + } + } + } + + /** + * Returns the last committed value as a string, or null if no value + * has been committed. + */ + public String getString(int index) throws IOException { + InputStream in = newInputStream(index); + return in != null ? inputStreamToString(in) : null; + } + + /** + * Returns a new unbuffered output stream to write the value at + * {@code index}. If the underlying output stream encounters errors + * when writing to the filesystem, this edit will be aborted when + * {@link #commit} is called. The returned output stream does not throw + * IOExceptions. + */ + public OutputStream newOutputStream(int index) throws IOException { + if (index < 0 || index >= valueCount) { + throw new IllegalArgumentException("Expected index " + index + " to " + + "be greater than 0 and less than the maximum value count " + + "of " + valueCount); + } + synchronized (DiskLruCache.this) { + if (entry.currentEditor != this) { + throw new IllegalStateException(); + } + if (!entry.readable) { + written[index] = true; + } + File dirtyFile = entry.getDirtyFile(index); + FileOutputStream outputStream; + try { + outputStream = new FileOutputStream(dirtyFile); + } catch (FileNotFoundException e) { + // Attempt to recreate the cache directory. + directory.mkdirs(); + try { + outputStream = new FileOutputStream(dirtyFile); + } catch (FileNotFoundException e2) { + // We are unable to recover. Silently eat the writes. + return NULL_OUTPUT_STREAM; + } + } + return new FaultHidingOutputStream(outputStream); + } + } + + /** + * Sets the value at {@code index} to {@code value}. + */ + public void set(int index, String value) throws IOException { + Writer writer = null; + try { + writer = new OutputStreamWriter(newOutputStream(index), Util.UTF_8); + writer.write(value); + } finally { + Util.closeQuietly(writer); + } + } + + /** + * Commits this edit so it is visible to readers. This releases the + * edit lock so another edit may be started on the same key. + */ + public void commit() throws IOException { + if (hasErrors) { + completeEdit(this, false); + remove(entry.key); // The previous entry is stale. + } else { + completeEdit(this, true); + } + committed = true; + } + + /** + * Aborts this edit. This releases the edit lock so another edit may be + * started on the same key. + */ + public void abort() throws IOException { + completeEdit(this, false); + } + + public void abortUnlessCommitted() { + if (!committed) { + try { + abort(); + } catch (IOException ignored) { + } + } + } + + private class FaultHidingOutputStream extends FilterOutputStream { + private FaultHidingOutputStream(OutputStream out) { + super(out); + } + + @Override + public void write(int oneByte) { + try { + out.write(oneByte); + } catch (IOException e) { + hasErrors = true; + } + } + + @Override + public void write(byte[] buffer, int offset, int length) { + try { + out.write(buffer, offset, length); + } catch (IOException e) { + hasErrors = true; + } + } + + @Override + public void close() { + try { + out.close(); + } catch (IOException e) { + hasErrors = true; + } + } + + @Override + public void flush() { + try { + out.flush(); + } catch (IOException e) { + hasErrors = true; + } + } + } + } + + private final class Entry { + private final String key; + + /** + * Lengths of this entry's files. + */ + private final long[] lengths; + + /** + * True if this entry has ever been published. + */ + private boolean readable; + + /** + * The ongoing edit or null if this entry is not being edited. + */ + private Editor currentEditor; + + /** + * The sequence number of the most recently committed edit to this entry. + */ + private long sequenceNumber; + + private Entry(String key) { + this.key = key; + this.lengths = new long[valueCount]; + } + + public String getLengths() throws IOException { + StringBuilder result = new StringBuilder(); + for (long size : lengths) { + result.append(' ').append(size); + } + return result.toString(); + } + + /** + * Set lengths using decimal numbers like "10123". + */ + private void setLengths(String[] strings) throws IOException { + if (strings.length != valueCount) { + throw invalidLengths(strings); + } + + try { + for (int i = 0; i < strings.length; i++) { + lengths[i] = Long.parseLong(strings[i]); + } + } catch (NumberFormatException e) { + throw invalidLengths(strings); + } + } + + private IOException invalidLengths(String[] strings) throws IOException { + throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings)); + } + + public File getCleanFile(int i) { + return new File(directory, key + "." + i); + } + + public File getDirtyFile(int i) { + return new File(directory, key + "." + i + ".tmp"); + } + } +} diff --git a/lib_cache/src/main/java/com/android/sdk/cache/DiskLruCacheHelper.java b/lib_cache/src/main/java/com/android/sdk/cache/DiskLruCacheHelper.java new file mode 100644 index 0000000..497d0d8 --- /dev/null +++ b/lib_cache/src/main/java/com/android/sdk/cache/DiskLruCacheHelper.java @@ -0,0 +1,426 @@ +package com.android.sdk.cache; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.os.Environment; +import android.util.Log; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Serializable; + +/** + * Reference: https://github.com/hongyangAndroid/base-diskcache + * 创建对象时: + *

    + *     1. 默认从缓存大小为1G
    + *     2. 默认的存储文件夹在SD卡
    + *     3. 如果传入的Context对象为null,则永远使用默认的版本号,否则每次版本升级会删除之前的缓存
    + * 
    + */ +public class DiskLruCacheHelper { + + private static final String TAG = DiskLruCacheHelper.class.getSimpleName(); + + private static final String DIR_NAME = "diskCache"; + private static final int MAX_SIZE = 1000 * 1024 * 1024; + private static final int DEFAULT_APP_VERSION = 1; + private static final int DEFAULT_VALUE_COUNT = 1; + + private DiskLruCache mDiskLruCache; + + public DiskLruCacheHelper(Context context) throws IOException { + mDiskLruCache = generateCache(context, DIR_NAME, MAX_SIZE); + } + + public DiskLruCacheHelper(Context context, String dirName) throws IOException { + mDiskLruCache = generateCache(context, dirName, MAX_SIZE); + } + + public DiskLruCacheHelper(Context context, String dirName, int maxSize) throws IOException { + mDiskLruCache = generateCache(context, dirName, maxSize); + } + + //custom cache dir + public DiskLruCacheHelper(File dir) throws IOException { + mDiskLruCache = generateCache(null, dir, MAX_SIZE); + } + + public DiskLruCacheHelper(Context context, File dir) throws IOException { + mDiskLruCache = generateCache(context, dir, MAX_SIZE); + } + + public DiskLruCacheHelper(Context context, File dir, int maxSize) throws IOException { + mDiskLruCache = generateCache(context, dir, maxSize); + } + + private DiskLruCache generateCache(Context context, File dir, int maxSize) throws IOException { + if (!dir.exists() || !dir.isDirectory()) { + throw new IllegalArgumentException( + dir + " is not a directory or does not exists. "); + } + + int appVersion = context == null ? DEFAULT_APP_VERSION : Utils.getAppVersion(context); + + return DiskLruCache.open( + dir, + appVersion, + DEFAULT_VALUE_COUNT, + maxSize); + } + + private DiskLruCache generateCache(Context context, String dirName, int maxSize) throws IOException { + return DiskLruCache.open( + getDiskCacheDir(context, dirName), + Utils.getAppVersion(context), + DEFAULT_VALUE_COUNT, + maxSize); + } + + // ======================================= + // ============== String 数据 读写 ============= + // ======================================= + + public void put(String key, String value) { + DiskLruCache.Editor edit = null; + BufferedWriter bw = null; + try { + edit = editor(key); + if (edit == null) return; + OutputStream os = edit.newOutputStream(0); + bw = new BufferedWriter(new OutputStreamWriter(os)); + bw.write(value); + edit.commit();//write CLEAN + } catch (IOException e) { + e.printStackTrace(); + try { + //s + edit.abort();//write REMOVE + } catch (IOException e1) { + e1.printStackTrace(); + } + } finally { + try { + if (bw != null) + bw.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + public String getAsString(String key) { + InputStream inputStream = null; + inputStream = get(key); + if (inputStream == null) return null; + String str = null; + try { + str = Util.readFully(new InputStreamReader(inputStream, Util.UTF_8)); + } catch (IOException e) { + e.printStackTrace(); + try { + inputStream.close(); + } catch (IOException e1) { + e1.printStackTrace(); + } + } + return str; + } + + + public void put(String key, JSONObject jsonObject) { + put(key, jsonObject.toString()); + } + + public JSONObject getAsJson(String key) { + String val = getAsString(key); + try { + if (val != null) + return new JSONObject(val); + } catch (JSONException e) { + e.printStackTrace(); + } + return null; + } + + // ======================================= + // ============ JSONArray 数据 读写 ============= + // ======================================= + + public void put(String key, JSONArray jsonArray) { + put(key, jsonArray.toString()); + } + + public JSONArray getAsJSONArray(String key) { + String JSONString = getAsString(key); + try { + JSONArray obj = new JSONArray(JSONString); + return obj; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + // ======================================= + // ============== byte 数据 读写 ============= + // ======================================= + + /** + * 保存 byte数据 到 缓存中 + * + * @param key 保存的key + * @param value 保存的数据 + */ + public void put(String key, byte[] value) { + OutputStream out = null; + DiskLruCache.Editor editor = null; + try { + editor = editor(key); + if (editor == null) { + return; + } + out = editor.newOutputStream(0); + out.write(value); + out.flush(); + editor.commit();//write CLEAN + } catch (Exception e) { + e.printStackTrace(); + try { + editor.abort();//write REMOVE + } catch (IOException e1) { + e1.printStackTrace(); + } + + } finally { + if (out != null) { + try { + out.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + + public byte[] getAsBytes(String key) { + byte[] res = null; + InputStream is = get(key); + if (is == null) return null; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + byte[] buf = new byte[256]; + int len = 0; + while ((len = is.read(buf)) != -1) { + baos.write(buf, 0, len); + } + res = baos.toByteArray(); + } catch (IOException e) { + e.printStackTrace(); + } + return res; + } + + + // ======================================= + // ============== 序列化 数据 读写 ============= + // ======================================= + public void put(String key, Serializable value) { + DiskLruCache.Editor editor = editor(key); + ObjectOutputStream oos = null; + if (editor == null) return; + try { + OutputStream os = editor.newOutputStream(0); + oos = new ObjectOutputStream(os); + oos.writeObject(value); + oos.flush(); + editor.commit(); + } catch (IOException e) { + e.printStackTrace(); + try { + editor.abort(); + } catch (IOException e1) { + e1.printStackTrace(); + } + } finally { + try { + if (oos != null) + oos.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + public T getAsSerializable(String key) { + T t = null; + InputStream is = get(key); + ObjectInputStream ois = null; + if (is == null) return null; + try { + ois = new ObjectInputStream(is); + t = (T) ois.readObject(); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (ois != null) + ois.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + return t; + } + + // ======================================= + // ============== bitmap 数据 读写 ============= + // ======================================= + public void put(String key, Bitmap bitmap) { + put(key, Utils.bitmap2Bytes(bitmap)); + } + + public Bitmap getAsBitmap(String key) { + byte[] bytes = getAsBytes(key); + if (bytes == null) return null; + return Utils.bytes2Bitmap(bytes); + } + + // ======================================= + // ============= drawable 数据 读写 ============= + // ======================================= + public void put(String key, Drawable value) { + put(key, Utils.drawable2Bitmap(value)); + } + + public Drawable getAsDrawable(String key) { + byte[] bytes = getAsBytes(key); + if (bytes == null) { + return null; + } + return Utils.bitmap2Drawable(Utils.bytes2Bitmap(bytes)); + } + + // ======================================= + // ============= other methods ============= + // ======================================= + public boolean remove(String key) { + try { + key = Utils.hashKeyForDisk(key); + return mDiskLruCache.remove(key); + } catch (IOException e) { + e.printStackTrace(); + } + return false; + } + + public void close() throws IOException { + mDiskLruCache.close(); + } + + public void delete() throws IOException { + mDiskLruCache.delete(); + } + + public void flush() throws IOException { + mDiskLruCache.flush(); + } + + public boolean isClosed() { + return mDiskLruCache.isClosed(); + } + + public long size() { + return mDiskLruCache.size(); + } + + public void setMaxSize(long maxSize) { + mDiskLruCache.setMaxSize(maxSize); + } + + public File getDirectory() { + return mDiskLruCache.getDirectory(); + } + + public long getMaxSize() { + return mDiskLruCache.getMaxSize(); + } + + + // ======================================= + // ===遇到文件比较大的,可以直接通过流读写 ===== + // ======================================= + //basic editor + public DiskLruCache.Editor editor(String key) { + try { + key = Utils.hashKeyForDisk(key); + //wirte DIRTY + DiskLruCache.Editor edit = mDiskLruCache.edit(key); + //edit maybe null :the entry is editing + if (edit == null) { + Log.w(TAG, "the entry spcified key:" + key + " is editing by other . "); + } + return edit; + } catch (IOException e) { + e.printStackTrace(); + } + + return null; + } + + + //basic get + public InputStream get(String key) { + try { + DiskLruCache.Snapshot snapshot = mDiskLruCache.get(Utils.hashKeyForDisk(key)); + if (snapshot == null) //not find entry , or entry.readable = false + { + Log.e(TAG, "not find entry , or entry.readable = false"); + return null; + } + //write READ + return snapshot.getInputStream(0); + + } catch (IOException e) { + e.printStackTrace(); + return null; + } + + } + + + // ======================================= + // ============== 序列化 数据 读写 ============= + // ======================================= + + private File getDiskCacheDir(Context context, String uniqueName) { + String cachePath; + if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) + || !Environment.isExternalStorageRemovable()) { + cachePath = context.getExternalCacheDir().getPath(); + } else { + cachePath = context.getCacheDir().getPath(); + } + return new File(cachePath + File.separator + uniqueName); + } + +} + + + diff --git a/lib_cache/src/main/java/com/android/sdk/cache/DiskLruStorageFactoryImpl.java b/lib_cache/src/main/java/com/android/sdk/cache/DiskLruStorageFactoryImpl.java new file mode 100644 index 0000000..4e550ae --- /dev/null +++ b/lib_cache/src/main/java/com/android/sdk/cache/DiskLruStorageFactoryImpl.java @@ -0,0 +1,42 @@ +package com.android.sdk.cache; + +import android.content.Context; +import android.util.Log; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-09 11:20 + */ +public class DiskLruStorageFactoryImpl implements StorageFactory { + + private static final String TAG = DiskLruStorageFactoryImpl.class.getSimpleName(); + + @Override + public Builder newBuilder(Context context) { + return new DiskLruStorageBuilder(context); + } + + class DiskLruStorageBuilder extends Builder { + + DiskLruStorageBuilder(Context context) { + super(context); + } + + @Override + public Builder enableMultiProcess(boolean multiProcess) { + if (multiProcess) { + Log.d(TAG, "DiskLruStorage was initialized, but do not support multi process"); + } + super.enableMultiProcess(multiProcess); + return this; + } + + @Override + public Storage build() { + return new DiskLruStorageImpl(context, storageId); + } + + } + +} \ No newline at end of file diff --git a/lib_cache/src/main/java/com/android/sdk/cache/DiskLruStorageImpl.java b/lib_cache/src/main/java/com/android/sdk/cache/DiskLruStorageImpl.java new file mode 100644 index 0000000..0b57e0b --- /dev/null +++ b/lib_cache/src/main/java/com/android/sdk/cache/DiskLruStorageImpl.java @@ -0,0 +1,163 @@ +package com.android.sdk.cache; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.text.TextUtils; + +import com.github.dmstocking.optional.java.util.Optional; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Type; + +import io.reactivex.Flowable; + +@SuppressWarnings("WeakerAccess,unused") +public class DiskLruStorageImpl implements Storage { + + private DiskLruCacheHelper mDiskLruCacheHelper; + private static final int CACHE_SIZE = 50 * 1024 * 1024;//50M + private final File mDir; + private final int mSize; + + /** + * @param context 上下文 + * @param cachePath 缓存文件 + */ + public DiskLruStorageImpl(@NonNull Context context, @NonNull String cachePath) { + this(context, cachePath, CACHE_SIZE); + } + + /** + * @param context 上下文 + * @param cachePath 缓存文件 + * @param cacheSize 缓存大小,字节数 + */ + public DiskLruStorageImpl(@NonNull Context context, @NonNull String cachePath, int cacheSize) { + mDir = context.getDir(cachePath, Context.MODE_PRIVATE); + mSize = cacheSize; + @SuppressWarnings("unused") + boolean mkdirs = mDir.getParentFile().mkdirs(); + } + + private DiskLruCacheHelper getDiskLruCacheHelper() { + if (mDiskLruCacheHelper == null || mDiskLruCacheHelper.isClosed()) { + try { + mDiskLruCacheHelper = new DiskLruCacheHelper(null, mDir, mSize); + } catch (IOException e) { + e.printStackTrace(); + } + } + return mDiskLruCacheHelper; + } + + @Override + public void putString(String key, String value) { + if (value == null) { + getDiskLruCacheHelper().remove(key); + return; + } + getDiskLruCacheHelper().put(buildKey(key), value); + } + + @Override + public String getString(String key, String defaultValue) { + String result = getDiskLruCacheHelper().getAsString(buildKey(key)); + if (TextUtils.isEmpty(result)) { + result = defaultValue; + } + return result; + } + + @Override + public String getString(String key) { + return getDiskLruCacheHelper().getAsString(buildKey(key)); + } + + @Override + public void putLong(String key, long value) { + getDiskLruCacheHelper().put(buildKey(key), String.valueOf(value)); + } + + @Override + public long getLong(String key, long defaultValue) { + String strLong = getDiskLruCacheHelper().getAsString(buildKey(key)); + if (TextUtils.isEmpty(strLong)) { + return defaultValue; + } + return Long.parseLong(strLong); + } + + @Override + public void putInt(String key, int value) { + getDiskLruCacheHelper().put(buildKey(key), String.valueOf(value)); + } + + @Override + public int getInt(String key, int defaultValue) { + String strInt = getDiskLruCacheHelper().getAsString(buildKey(key)); + if (TextUtils.isEmpty(strInt)) { + return defaultValue; + } + return Integer.parseInt(strInt); + } + + @Override + public void putBoolean(String key, boolean value) { + int bool = value ? 1 : 0; + getDiskLruCacheHelper().put(buildKey(key), String.valueOf(bool)); + } + + @Override + public boolean getBoolean(String key, boolean defaultValue) { + String strInt = getDiskLruCacheHelper().getAsString(buildKey(key)); + if (TextUtils.isEmpty(strInt)) { + return defaultValue; + } + return Integer.parseInt(strInt) == 1; + } + + @Override + public void remove(String key) { + getDiskLruCacheHelper().remove(buildKey(key)); + } + + @Override + public void clearAll() { + try { + getDiskLruCacheHelper().delete(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private String buildKey(String originKey) { + return originKey; + } + + @Override + public void putEntity(String key, Object entity, long cacheTime) { + CommonImpl.putEntity(key, entity, cacheTime, this); + } + + @Override + public void putEntity(String key, Object entity) { + CommonImpl.putEntity(key, entity, 0, this); + } + + @Override + public T getEntity(String key, Type type) { + return CommonImpl.getEntity(key, type, this); + } + + @Override + public Flowable flowable(String key, Type type) { + return CommonImpl.flowableEntity(key, type, this); + } + + @Override + public Flowable> optionalFlowable(String key, Type type) { + return CommonImpl.flowableOptionalEntity(key, type, this); + } + +} \ No newline at end of file diff --git a/lib_cache/src/main/java/com/android/sdk/cache/JsonUtils.java b/lib_cache/src/main/java/com/android/sdk/cache/JsonUtils.java new file mode 100644 index 0000000..aeba3db --- /dev/null +++ b/lib_cache/src/main/java/com/android/sdk/cache/JsonUtils.java @@ -0,0 +1,51 @@ +package com.android.sdk.cache; + +import android.util.Log; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.lang.reflect.Modifier; +import java.lang.reflect.Type; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-01 16:38 + */ +final class JsonUtils { + + private static final String TAG = JsonUtils.class.getSimpleName(); + + private final static Gson GSON = new GsonBuilder() + .excludeFieldsWithModifiers(Modifier.TRANSIENT) + .excludeFieldsWithModifiers(Modifier.STATIC) + .create(); + + static String toJson(Object entity) { + if (entity == null) { + return ""; + } + try { + return GSON.toJson(entity); + } catch (Exception e) { + Log.e(TAG, "JsonSerializer toJson error with: entity = " + entity, e); + } + return ""; + } + + @SuppressWarnings("unchecked") + static T fromJson(String json, Type clazz) { + try { + if (clazz == String.class) { + return (T) json; + } else { + return GSON.fromJson(json, clazz); + } + } catch (Exception e) { + Log.e(TAG, "JsonSerializer fromJson error with: json = " + json + " class = " + clazz, e); + } + return null; + } + +} diff --git a/lib_cache/src/main/java/com/android/sdk/cache/MMKVStorageFactoryImpl.java b/lib_cache/src/main/java/com/android/sdk/cache/MMKVStorageFactoryImpl.java new file mode 100644 index 0000000..28dd871 --- /dev/null +++ b/lib_cache/src/main/java/com/android/sdk/cache/MMKVStorageFactoryImpl.java @@ -0,0 +1,30 @@ +package com.android.sdk.cache; + +import android.content.Context; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-01 17:35 + */ +public class MMKVStorageFactoryImpl implements StorageFactory { + + @Override + public Builder newBuilder(Context context) { + return new MMKVStorageBuilder(context); + } + + class MMKVStorageBuilder extends Builder { + + MMKVStorageBuilder(Context context) { + super(context); + } + + @Override + public Storage build() { + return new MMKVStorageImpl(context, storageId, multiProcess); + } + + } + +} diff --git a/lib_cache/src/main/java/com/android/sdk/cache/MMKVStorageImpl.java b/lib_cache/src/main/java/com/android/sdk/cache/MMKVStorageImpl.java new file mode 100644 index 0000000..42520aa --- /dev/null +++ b/lib_cache/src/main/java/com/android/sdk/cache/MMKVStorageImpl.java @@ -0,0 +1,172 @@ +package com.android.sdk.cache; + +import android.content.Context; +import android.util.Log; + +import com.github.dmstocking.optional.java.util.Optional; +import com.tencent.mmkv.MMKV; + +import java.lang.reflect.Type; +import java.util.concurrent.atomic.AtomicBoolean; + +import io.reactivex.Flowable; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-01 11:25 + */ +@SuppressWarnings("WeakerAccess,unused") +public class MMKVStorageImpl implements Storage { + + private static final String TAG = MMKVStorageImpl.class.getSimpleName(); + + private static final AtomicBoolean INITIALIZED = new AtomicBoolean(false); + + private final MMKV mMmkv; + + public MMKVStorageImpl(Context context, String mmkvId) { + this(context, mmkvId, false); + } + + public MMKVStorageImpl(Context context, String mmkvId, boolean multiProcess) { + + if (INITIALIZED.compareAndSet(false, true)) { + String rootDir = MMKV.initialize(context.getApplicationContext()); + Log.d(TAG, "MMKV initialized and rootDir is: " + rootDir); + } + + int mode = multiProcess ? MMKV.MULTI_PROCESS_MODE : MMKV.SINGLE_PROCESS_MODE; + mMmkv = MMKV.mmkvWithID(mmkvId, mode); + } + + @Override + public void putString(String key, String value) { + try { + if (value == null) { + remove(key); + return; + } + mMmkv.encode(key, value); + } catch (Error error) { + error.printStackTrace(); + } + } + + @Override + public String getString(String key, String defaultValue) { + try { + return mMmkv.decodeString(key, defaultValue); + } catch (Error error) { + error.printStackTrace(); + } + return defaultValue; + } + + @Override + public String getString(String key) { + try { + return mMmkv.decodeString(key); + } catch (Error error) { + error.printStackTrace(); + } + return null; + } + + @Override + public void putLong(String key, long value) { + try { + mMmkv.encode(key, value); + } catch (Error error) { + error.printStackTrace(); + } + } + + @Override + public long getLong(String key, long defaultValue) { + try { + return mMmkv.decodeLong(key, defaultValue); + } catch (Error error) { + error.printStackTrace(); + } + return defaultValue; + } + + @Override + public void putInt(String key, int value) { + try { + mMmkv.encode(key, value); + } catch (Error error) { + error.printStackTrace(); + } + } + + @Override + public int getInt(String key, int defaultValue) { + try { + return mMmkv.decodeInt(key, defaultValue); + } catch (Error error) { + error.printStackTrace(); + } + return defaultValue; + } + + @Override + public void putBoolean(String key, boolean value) { + try { + mMmkv.encode(key, value); + } catch (Error error) { + error.printStackTrace(); + } + } + + @Override + public boolean getBoolean(String key, boolean defaultValue) { + try { + return mMmkv.decodeBool(key, defaultValue); + } catch (Error error) { + error.printStackTrace(); + } + return defaultValue; + } + + @Override + public void remove(String key) { + try { + mMmkv.removeValueForKey(key); + } catch (Error error) { + error.printStackTrace(); + } + } + + @Override + public void clearAll() { + mMmkv.clear(); + } + + @Override + public void putEntity(String key, Object entity, long cacheTime) { + CommonImpl.putEntity(key, entity, cacheTime, this); + } + + @Override + public void putEntity(String key, Object entity) { + CommonImpl.putEntity(key, entity, 0, this); + } + + @Override + public T getEntity(String key, Type type) { + return CommonImpl.getEntity(key, type, this); + } + + @Override + public Flowable flowable(String key, Type type) { + return CommonImpl.flowableEntity(key, type, this); + } + + @Override + public Flowable> optionalFlowable(String key, Type type) { + return CommonImpl.flowableOptionalEntity(key, type, this); + } + +} \ No newline at end of file diff --git a/lib_cache/src/main/java/com/android/sdk/cache/Storage.java b/lib_cache/src/main/java/com/android/sdk/cache/Storage.java new file mode 100644 index 0000000..3c461f0 --- /dev/null +++ b/lib_cache/src/main/java/com/android/sdk/cache/Storage.java @@ -0,0 +1,59 @@ +package com.android.sdk.cache; + +import android.support.annotation.Nullable; + +import com.github.dmstocking.optional.java.util.Optional; + +import java.lang.reflect.Type; + +import io.reactivex.Flowable; + +/** + * 缓存接口 + * + * @author Ztiany + * Date : 2016-10-24 21:59 + */ +public interface Storage { + + void putEntity(String key, @Nullable Object entity, long cacheTime); + + void putEntity(String key, @Nullable Object entity); + + /** + * @param key 缓存的 key + * @param type 缓存实体类型,如果是泛型类型,请使用 {@link TypeFlag}标识 + * @param 缓存实体类型 + * @return 缓存 + */ + @Nullable + T getEntity(String key, Type type); + + Flowable flowable(String key, Type type); + + Flowable> optionalFlowable(String key, Type type); + + void putString(String key, String value); + + String getString(String key, String defaultValue); + + @Nullable + String getString(String key); + + void putLong(String key, long value); + + long getLong(String key, long defaultValue); + + void putInt(String key, int value); + + int getInt(String key, int defaultValue); + + void putBoolean(String key, boolean value); + + boolean getBoolean(String key, boolean defaultValue); + + void remove(String key); + + void clearAll(); + +} diff --git a/lib_cache/src/main/java/com/android/sdk/cache/StorageEx.kt b/lib_cache/src/main/java/com/android/sdk/cache/StorageEx.kt new file mode 100644 index 0000000..168e564 --- /dev/null +++ b/lib_cache/src/main/java/com/android/sdk/cache/StorageEx.kt @@ -0,0 +1,16 @@ +package com.android.sdk.cache + +import com.github.dmstocking.optional.java.util.Optional +import io.reactivex.Flowable + +inline fun Storage.entity(key: String): T? { + return this.getEntity(key, object : TypeFlag() {}.type) +} + +inline fun Storage.flowable(key: String): Flowable { + return this.flowable(key, object : TypeFlag() {}.type) +} + +inline fun Storage.optionalFlowable(key: String): Flowable> { + return this.optionalFlowable(key, object : TypeFlag() {}.type) +} \ No newline at end of file diff --git a/lib_cache/src/main/java/com/android/sdk/cache/StorageFactory.java b/lib_cache/src/main/java/com/android/sdk/cache/StorageFactory.java new file mode 100644 index 0000000..dc89f1a --- /dev/null +++ b/lib_cache/src/main/java/com/android/sdk/cache/StorageFactory.java @@ -0,0 +1,43 @@ +package com.android.sdk.cache; + +import android.content.Context; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-01 17:32 + */ +public interface StorageFactory { + + StorageFactory.Builder newBuilder(Context context); + + abstract class Builder { + + Context context; + String storageId; + boolean multiProcess; + + Builder(Context context) { + this.context = context; + } + + /** + * 是否允许跨进程访问存储 + */ + public Builder enableMultiProcess(boolean multiProcess) { + this.multiProcess = multiProcess; + return this; + } + + /** + * @param storageId 存储标识 + */ + public Builder storageId(String storageId) { + this.storageId = storageId; + return this; + } + + public abstract Storage build(); + } + +} \ No newline at end of file diff --git a/lib_cache/src/main/java/com/android/sdk/cache/StrictLineReader.java b/lib_cache/src/main/java/com/android/sdk/cache/StrictLineReader.java new file mode 100644 index 0000000..85f476e --- /dev/null +++ b/lib_cache/src/main/java/com/android/sdk/cache/StrictLineReader.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.sdk.cache; + +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; + +/** + * Buffers input from an {@link InputStream} for reading lines. + *

    + *

    This class is used for buffered reading of lines. For purposes of this class, a line ends + * with "\n" or "\r\n". End of input is reported by throwing {@code EOFException}. Unterminated + * line at end of input is invalid and will be ignored, the caller may use {@code + * hasUnterminatedLine()} to detect it after catching the {@code EOFException}. + *

    + *

    This class is intended for reading input that strictly consists of lines, such as line-based + * cache entries or cache journal. Unlike the {@link java.io.BufferedReader} which in conjunction + * with {@link java.io.InputStreamReader} provides similar functionality, this class uses different + * end-of-input reporting and a more restrictive definition of a line. + *

    + *

    This class supports only charsets that encode '\r' and '\n' as a single byte with value 13 + * and 10, respectively, and the representation of no other character contains these values. + * We currently check in constructor that the charset is one of US-ASCII, UTF-8 and ISO-8859-1. + * The default charset is US_ASCII. + */ +class StrictLineReader implements Closeable { + + private static final byte CR = (byte) '\r'; + private static final byte LF = (byte) '\n'; + + private final InputStream in; + private final Charset charset; + + /* + * Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end + * and the data in the range [pos, end) is buffered for reading. At end of input, if there is + * an unterminated line, we set end == -1, otherwise end == pos. If the underlying + * {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1. + */ + private byte[] buf; + private int pos; + private int end; + + /** + * Constructs a new {@code LineReader} with the specified charset and the default capacity. + * + * @param in the {@code InputStream} to read data from. + * @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are + * supported. + * @throws NullPointerException if {@code in} or {@code charset} is null. + * @throws IllegalArgumentException if the specified charset is not supported. + */ + public StrictLineReader(InputStream in, Charset charset) { + this(in, 8192, charset); + } + + /** + * Constructs a new {@code LineReader} with the specified capacity and charset. + * + * @param in the {@code InputStream} to read data from. + * @param capacity the capacity of the buffer. + * @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are + * supported. + * @throws NullPointerException if {@code in} or {@code charset} is null. + * @throws IllegalArgumentException if {@code capacity} is negative or zero + * or the specified charset is not supported. + */ + public StrictLineReader(InputStream in, int capacity, Charset charset) { + if (in == null || charset == null) { + throw new NullPointerException(); + } + if (capacity < 0) { + throw new IllegalArgumentException("capacity <= 0"); + } + if (!(charset.equals(Util.US_ASCII))) { + throw new IllegalArgumentException("Unsupported encoding"); + } + + this.in = in; + this.charset = charset; + buf = new byte[capacity]; + } + + /** + * Closes the reader by closing the underlying {@code InputStream} and + * marking this reader as closed. + * + * @throws IOException for errors when closing the underlying {@code InputStream}. + */ + public void close() throws IOException { + synchronized (in) { + if (buf != null) { + buf = null; + in.close(); + } + } + } + + /** + * Reads the next line. A line ends with {@code "\n"} or {@code "\r\n"}, + * this end of line marker is not included in the result. + * + * @return the next line from the input. + * @throws IOException for underlying {@code InputStream} errors. + * @throws EOFException for the end of source stream. + */ + public String readLine() throws IOException { + synchronized (in) { + if (buf == null) { + throw new IOException("LineReader is closed"); + } + + // Read more data if we are at the end of the buffered data. + // Though it's an error to read after an exception, we will let {@code fillBuf()} + // throw again if that happens; thus we need to handle end == -1 as well as end == pos. + if (pos >= end) { + fillBuf(); + } + // Try to find LF in the buffered data and return the line if successful. + for (int i = pos; i != end; ++i) { + if (buf[i] == LF) { + int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i; + String res = new String(buf, pos, lineEnd - pos, charset.name()); + pos = i + 1; + return res; + } + } + + // Let's anticipate up to 80 characters on top of those already read. + ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) { + @Override + public String toString() { + int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count; + try { + return new String(buf, 0, length, charset.name()); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(e); // Since we control the charset this will never happen. + } + } + }; + + while (true) { + out.write(buf, pos, end - pos); + // Mark unterminated line in case fillBuf throws EOFException or IOException. + end = -1; + fillBuf(); + // Try to find LF in the buffered data and return the line if successful. + for (int i = pos; i != end; ++i) { + if (buf[i] == LF) { + if (i != pos) { + out.write(buf, pos, i - pos); + } + pos = i + 1; + return out.toString(); + } + } + } + } + } + + public boolean hasUnterminatedLine() { + return end == -1; + } + + /** + * Reads new input data into the buffer. Call only with pos == end or end == -1, + * depending on the desired outcome if the function throws. + */ + private void fillBuf() throws IOException { + int result = in.read(buf, 0, buf.length); + if (result == -1) { + throw new EOFException(); + } + pos = 0; + end = result; + } +} + diff --git a/lib_cache/src/main/java/com/android/sdk/cache/TypeFlag.java b/lib_cache/src/main/java/com/android/sdk/cache/TypeFlag.java new file mode 100644 index 0000000..9512711 --- /dev/null +++ b/lib_cache/src/main/java/com/android/sdk/cache/TypeFlag.java @@ -0,0 +1,42 @@ +package com.android.sdk.cache; + +import com.google.gson.internal.$Gson$Types; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +/** + * copy from gson, 为了不让调用者依赖 gson. + * + * @param 类型标记 + */ +public abstract class TypeFlag { + + private final Class rawType; + private final Type type; + + @SuppressWarnings("unchecked") + protected TypeFlag() { + this.type = getSuperclassTypeParameter(getClass()); + this.rawType = (Class) $Gson$Types.getRawType(type); + } + + @SuppressWarnings("all") + private static Type getSuperclassTypeParameter(Class subclass) { + Type superclass = subclass.getGenericSuperclass(); + if (superclass instanceof Class) { + throw new RuntimeException("Missing type parameter."); + } + ParameterizedType parameterized = (ParameterizedType) superclass; + return $Gson$Types.canonicalize(parameterized.getActualTypeArguments()[0]); + } + + public final Class getRawType() { + return rawType; + } + + public final Type getType() { + return type; + } + +} \ No newline at end of file diff --git a/lib_cache/src/main/java/com/android/sdk/cache/Util.java b/lib_cache/src/main/java/com/android/sdk/cache/Util.java new file mode 100644 index 0000000..b9165d9 --- /dev/null +++ b/lib_cache/src/main/java/com/android/sdk/cache/Util.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.sdk.cache; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.Reader; +import java.io.StringWriter; +import java.nio.charset.Charset; + +/** + * Junk drawer of utility methods. + */ +final class Util { + static final Charset US_ASCII = Charset.forName("US-ASCII"); + static final Charset UTF_8 = Charset.forName("UTF-8"); + + private Util() { + } + + static String readFully(Reader reader) throws IOException { + try { + StringWriter writer = new StringWriter(); + char[] buffer = new char[1024]; + int count; + while ((count = reader.read(buffer)) != -1) { + writer.write(buffer, 0, count); + } + return writer.toString(); + } finally { + reader.close(); + } + } + + /** + * Deletes the contents of {@code dir}. Throws an IOException if any file + * could not be deleted, or if {@code dir} is not a readable directory. + */ + static void deleteContents(File dir) throws IOException { + File[] files = dir.listFiles(); + if (files == null) { + throw new IOException("not a readable directory: " + dir); + } + for (File file : files) { + if (file.isDirectory()) { + deleteContents(file); + } + if (!file.delete()) { + throw new IOException("failed to delete file: " + file); + } + } + } + + static void closeQuietly(/*Auto*/Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (RuntimeException rethrown) { + throw rethrown; + } catch (Exception ignored) { + } + } + } + + +} diff --git a/lib_cache/src/main/java/com/android/sdk/cache/Utils.java b/lib_cache/src/main/java/com/android/sdk/cache/Utils.java new file mode 100644 index 0000000..3560e13 --- /dev/null +++ b/lib_cache/src/main/java/com/android/sdk/cache/Utils.java @@ -0,0 +1,102 @@ +package com.android.sdk.cache; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.PixelFormat; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; + +import java.io.ByteArrayOutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +final class Utils { + + static int getAppVersion(Context context) { + try { + PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); + return info.versionCode; + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + return 1; + } + + + static String hashKeyForDisk(String key) { + String cacheKey; + try { + final MessageDigest mDigest = MessageDigest.getInstance("MD5"); + mDigest.update(key.getBytes()); + cacheKey = bytesToHexString(mDigest.digest()); + } catch (NoSuchAlgorithmException e) { + cacheKey = String.valueOf(key.hashCode()); + } + return cacheKey; + } + + private static String bytesToHexString(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < bytes.length; i++) { + String hex = Integer.toHexString(0xFF & bytes[i]); + if (hex.length() == 1) { + sb.append('0'); + } + sb.append(hex); + } + return sb.toString(); + } + + static byte[] bitmap2Bytes(Bitmap bm) { + if (bm == null) { + return null; + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + bm.compress(Bitmap.CompressFormat.PNG, 100, baos); + return baos.toByteArray(); + } + + static Bitmap bytes2Bitmap(byte[] bytes) { + return BitmapFactory.decodeByteArray(bytes, 0, bytes.length); + } + + + /** + * Drawable → Bitmap + */ + static Bitmap drawable2Bitmap(Drawable drawable) { + if (drawable == null) { + return null; + } + // 取 drawable 的长宽 + int w = drawable.getIntrinsicWidth(); + int h = drawable.getIntrinsicHeight(); + // 取 drawable 的颜色格式 + Bitmap.Config config = drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565; + // 建立对应 bitmap + Bitmap bitmap = Bitmap.createBitmap(w, h, config); + // 建立对应 bitmap 的画布 + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, w, h); + // 把 drawable 内容画到画布中 + drawable.draw(canvas); + return bitmap; + } + + /* + * Bitmap → Drawable + */ + @SuppressWarnings("deprecation") + static Drawable bitmap2Drawable(Bitmap bm) { + if (bm == null) { + return null; + } + BitmapDrawable bd = new BitmapDrawable(bm); + bd.setTargetDensity(bm.getDensity()); + return new BitmapDrawable(bm); + } +} \ No newline at end of file diff --git a/lib_cache/src/main/res/values/strings.xml b/lib_cache/src/main/res/values/strings.xml new file mode 100644 index 0000000..2018dac --- /dev/null +++ b/lib_cache/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + lib_cache + diff --git a/lib_media_selector/.gitignore b/lib_media_selector/.gitignore new file mode 100644 index 0000000..c06fb85 --- /dev/null +++ b/lib_media_selector/.gitignore @@ -0,0 +1,22 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +*.iml +.idea/ +.gradle +/local.properties +.DS_Store +/build +/captures +*.apk +*.ap_ +*.dex +*.class +bin/ +gen/ +local.properties \ No newline at end of file diff --git a/lib_media_selector/README.md b/lib_media_selector/README.md new file mode 100644 index 0000000..6f49fcd --- /dev/null +++ b/lib_media_selector/README.md @@ -0,0 +1,27 @@ +# 多媒体文件选择库 + + 目前基于boxing修改 + +## 1 AndroidN 在 FileProvider 的 xm l配置中加入: + +``` + + +``` + +## 2 So库 + +``` + defaultConfig { + ...... + ndk {//只打包armeabi架构的so库 + abiFilters 'armeabi' + } + } +``` + +## 3 其他备选参考 + +-  [boxing](https://github.com/Bilibili/boxing) +-  [uCrop](https://github.com/Yalantis/uCrop) +-  [ImagePicker](https://github.com/jeasonlzy/ImagePicker) diff --git a/lib_media_selector/build.gradle b/lib_media_selector/build.gradle new file mode 100644 index 0000000..c8e091f --- /dev/null +++ b/lib_media_selector/build.gradle @@ -0,0 +1,57 @@ +apply plugin: 'com.android.library' + +android { + + compileSdkVersion rootProject.compileSdkVersion + buildToolsVersion rootProject.buildToolsVersion + + defaultConfig { + minSdkVersion rootProject.minSdkVersion + targetSdkVersion rootProject.targetSdkVersion + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + //support + implementation androidLibraries.androidCompatV7 + implementation androidLibraries.androidDesign + + /*imageLoader*/ + compileOnly thirdLibraries.glide + + //other optional + //知乎 Matisse:https://github.com/zhihu/Matisse + //PictureSelector :https://github.com/LuckSiege/PictureSelector + //Album:https://github.com/yanzhenjie/Album + + implementation('com.bilibili:boxing:1.0.4') { + exclude group: 'com.android.support' + } + + implementation('com.bilibili:boxing-impl:1.0.4') { + exclude group: 'com.android.support' + } + + //other optional + //cropper about + // smartCropper:https://github.com/pqpo/SmartCropper + // simpleCropper:https:github.com/igreenwood/SimpleCropView + implementation('com.yalantis:ucrop:2.2.0') { + exclude group: 'com.android.support' + exclude group: 'com.squareup.okio' + exclude group: 'com.squareup.okhttp3' + } +} diff --git a/lib_media_selector/proguard-rules.pro b/lib_media_selector/proguard-rules.pro new file mode 100644 index 0000000..268073a --- /dev/null +++ b/lib_media_selector/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in D:\DevTools\SDK/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/lib_media_selector/src/main/AndroidManifest.xml b/lib_media_selector/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b14b601 --- /dev/null +++ b/lib_media_selector/src/main/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + diff --git a/lib_media_selector/src/main/java/com/android/sdk/mediaselector/BoxingGlideLoader.java b/lib_media_selector/src/main/java/com/android/sdk/mediaselector/BoxingGlideLoader.java new file mode 100644 index 0000000..79d1990 --- /dev/null +++ b/lib_media_selector/src/main/java/com/android/sdk/mediaselector/BoxingGlideLoader.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2017 Bilibili + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.sdk.mediaselector; + +import android.graphics.Bitmap; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.widget.ImageView; + +import com.bilibili.boxing.loader.IBoxingCallback; +import com.bilibili.boxing.loader.IBoxingMediaLoader; +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.DataSource; +import com.bumptech.glide.load.engine.GlideException; +import com.bumptech.glide.request.RequestListener; +import com.bumptech.glide.request.RequestOptions; +import com.bumptech.glide.request.target.Target; + +/** + * use https://github.com/bumptech/glide as media loader. + * + * @author ChenSL + */ +final class BoxingGlideLoader implements IBoxingMediaLoader { + + @Override + public void displayThumbnail(@NonNull ImageView img, @NonNull String absPath, int width, int height) { + String path = "file://" + absPath; + try { + RequestOptions requestOptions = new RequestOptions(); + Glide.with(img.getContext()).load(path).apply(requestOptions).into(img); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } + } + + @Override + public void displayRaw(@NonNull final ImageView imageView, @NonNull String absPath, int width, int height, final IBoxingCallback iBoxingCallback) { + String path = "file://" + absPath; + Glide.with(imageView.getContext()) + .asBitmap() + .load(path) + .listener(new RequestListener() { + @Override + public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { + if (iBoxingCallback != null) { + iBoxingCallback.onFail(e); + return true; + } + return false; + } + + @Override + public boolean onResourceReady(Bitmap resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { + if (resource != null && iBoxingCallback != null) { + imageView.setImageBitmap(resource); + iBoxingCallback.onSuccess(); + return true; + } + return false; + } + }) + .into(imageView); + } + +} diff --git a/lib_media_selector/src/main/java/com/android/sdk/mediaselector/BoxingUcrop.java b/lib_media_selector/src/main/java/com/android/sdk/mediaselector/BoxingUcrop.java new file mode 100644 index 0000000..2754c2d --- /dev/null +++ b/lib_media_selector/src/main/java/com/android/sdk/mediaselector/BoxingUcrop.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2017 Bilibili + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.sdk.mediaselector; + +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.v4.app.Fragment; +import android.support.v4.content.ContextCompat; + +import com.bilibili.boxing.loader.IBoxingCrop; +import com.bilibili.boxing.model.config.BoxingCropOption; +import com.yalantis.ucrop.UCrop; +import com.ztiany.mediaselector.R; + +/** + * use Ucrop(https://github.com/Yalantis/uCrop) as the implement for {@link IBoxingCrop} + * + * @author ChenSL + */ +final class BoxingUcrop implements IBoxingCrop { + + @Override + public void onStartCrop(Context context, Fragment fragment, @NonNull BoxingCropOption cropConfig, + @NonNull String path, int requestCode) { + + Uri uri = new Uri.Builder() + .scheme("file") + .appendPath(path) + .build(); + //参数 + UCrop.Options crop = new UCrop.Options(); + crop.setCompressionFormat(Bitmap.CompressFormat.JPEG); + crop.withMaxResultSize(cropConfig.getMaxWidth(), cropConfig.getMaxHeight()); + crop.withAspectRatio(cropConfig.getAspectRatioX(), cropConfig.getAspectRatioY()); + //颜色 + int color = ContextCompat.getColor(context, R.color.boxing_colorPrimaryDark); + crop.setToolbarColor(color); + crop.setStatusBarColor(color); + //开始裁减 + UCrop.of(uri, cropConfig.getDestination()) + .withOptions(crop) + .start(context, fragment, requestCode); + } + + @Override + public Uri onCropFinish(int resultCode, Intent data) { + if (data == null) { + return null; + } + Throwable throwable = UCrop.getError(data); + if (throwable != null) { + return null; + } + return UCrop.getOutput(data); + } +} diff --git a/lib_media_selector/src/main/java/com/android/sdk/mediaselector/CropOptions.java b/lib_media_selector/src/main/java/com/android/sdk/mediaselector/CropOptions.java new file mode 100644 index 0000000..a6c7f2c --- /dev/null +++ b/lib_media_selector/src/main/java/com/android/sdk/mediaselector/CropOptions.java @@ -0,0 +1,82 @@ +package com.android.sdk.mediaselector; + +import java.io.Serializable; + +/** + * 裁剪配置类 + * Author: JPH + * Date: 2016/7/27 13:19 + */ +public class CropOptions implements Serializable { + + private int aspectX = 1; + private int aspectY = 1; + private int outputX; + private int outputY; + + public CropOptions() { + + } + + int getAspectX() { + return aspectX; + } + + /** + * 裁剪宽度比例 与aspectY组合,如16:9 + * + * @param aspectX + * @return + */ + public CropOptions setAspectX(int aspectX) { + this.aspectX = aspectX; + return this; + } + + int getAspectY() { + return aspectY; + } + + /** + * 高度比例 与aspectX组合,如16:9 + * + * @param aspectY + * @return + */ + public CropOptions setAspectY(int aspectY) { + this.aspectY = aspectY; + return this; + } + + int getOutputX() { + return outputX; + } + + /** + * 输出图片的宽度 + * + * @param outputX + * @return + */ + public CropOptions setOutputX(int outputX) { + this.outputX = outputX; + return this; + } + + int getOutputY() { + return outputY; + } + + /** + * 输入图片的高度 + * + * @param outputY + * @return + */ + public CropOptions setOutputY(int outputY) { + this.outputY = outputY; + return this; + } + + +} diff --git a/lib_media_selector/src/main/java/com/android/sdk/mediaselector/MediaSelector.java b/lib_media_selector/src/main/java/com/android/sdk/mediaselector/MediaSelector.java new file mode 100644 index 0000000..f0fbacb --- /dev/null +++ b/lib_media_selector/src/main/java/com/android/sdk/mediaselector/MediaSelector.java @@ -0,0 +1,159 @@ +package com.android.sdk.mediaselector; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.support.v4.app.Fragment; +import android.text.TextUtils; + +import com.bilibili.boxing.Boxing; +import com.bilibili.boxing.BoxingCrop; +import com.bilibili.boxing.BoxingMediaLoader; +import com.bilibili.boxing.model.config.BoxingConfig; +import com.bilibili.boxing.model.config.BoxingCropOption; +import com.bilibili.boxing.model.entity.BaseMedia; +import com.bilibili.boxing.utils.BoxingFileHelper; +import com.bilibili.boxing.utils.BoxingLog; +import com.bilibili.boxing_impl.ui.BoxingActivity; +import com.ztiany.mediaselector.R; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * 基于 Boxing 的多媒体文件选择器 + *

    + *     FileProvider 的 Authorities为 {@code PackageName+ ".file.provider"} 才能正常工作
    + * 
    + */ +public class MediaSelector { + + static { + BoxingMediaLoader.getInstance().init(new BoxingGlideLoader()); + BoxingCrop.getInstance().init(new BoxingUcrop()); + } + + private static final int REQUEST_CODE_SINGLE = 100;//选择单图 + private static final int REQUEST_CODE_MULTI = 200;//选择多图 + + private final Callback mCallback; + private Activity mActivity; + private Fragment mFragment; + + public MediaSelector(Activity contextWrapper, Callback callback) { + mCallback = callback; + mActivity = contextWrapper; + } + + public MediaSelector(Fragment contextWrapper, Callback callback) { + mCallback = callback; + mFragment = contextWrapper; + } + + //Function + public void takeMultiPicture(boolean needCamera, int selectCount) { + BoxingConfig boxingConfig = new BoxingConfig(BoxingConfig.Mode.MULTI_IMG); + if (needCamera) { + boxingConfig.needCamera(R.drawable.ic_boxing_camera); + } + boxingConfig.withMaxCount(selectCount); + start(boxingConfig, REQUEST_CODE_MULTI); + } + + public void takeSinglePicture(boolean needCamera) { + BoxingConfig singleImgConfig = new BoxingConfig(BoxingConfig.Mode.SINGLE_IMG); + if (needCamera) { + singleImgConfig.needCamera(R.drawable.ic_boxing_camera); + } + start(singleImgConfig, REQUEST_CODE_SINGLE); + } + + public void takeSinglePictureWithCrop(boolean needCamera, CropOptions cropOptions) { + //裁减的图片放在App内部缓存目录 + String cachePath = BoxingFileHelper.getCacheDir(getContext()); + if (TextUtils.isEmpty(cachePath)) { + BoxingLog.d("takeSinglePictureWithCrop fail because getCacheDir fail"); + return; + } + + //裁剪信息 + Uri destUri = new Uri.Builder() + .scheme("file") + .appendPath(cachePath) + .appendPath(String.format(Locale.US, "%s.jpg", System.currentTimeMillis())) + .build(); + + BoxingCropOption cropOption = new BoxingCropOption(destUri) + .aspectRatio(cropOptions.getAspectX(), cropOptions.getAspectY()) + .withMaxResultSize(cropOptions.getOutputX(), cropOptions.getOutputY()); + + //获取图片配置 + BoxingConfig singleCropImgConfig = new BoxingConfig(BoxingConfig.Mode.SINGLE_IMG).withCropOption(cropOption); + + if (needCamera) { + singleCropImgConfig.needCamera(R.drawable.ic_boxing_camera); + } + + //去获取图片 + start(singleCropImgConfig, REQUEST_CODE_SINGLE); + } + + private Context getContext() { + if (mFragment != null) { + return mFragment.getContext(); + } else { + return mActivity; + } + } + + private void start(BoxingConfig boxingConfig, int requestCode) { + if (mFragment != null) { + Boxing boxing = Boxing.of(boxingConfig) + .withIntent(mFragment.getContext(), BoxingActivity.class); + boxing.start(mFragment, requestCode); + } else if (mActivity != null) { + Boxing boxing = Boxing.of(boxingConfig) + .withIntent(mActivity, BoxingActivity.class); + boxing.start(mActivity, requestCode); + } + } + + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == Activity.RESULT_OK) { + if (requestCode == REQUEST_CODE_SINGLE) { + processSingle(data); + } else if (requestCode == REQUEST_CODE_MULTI) { + processMultiResult(data); + } + } + } + + private void processSingle(Intent data) { + final ArrayList medias = Boxing.getResult(data); + if (medias != null) { + mCallback.onTakePictureSuccess(medias.get(0).getPath()); + } + } + + private void processMultiResult(Intent data) { + final ArrayList medias = Boxing.getResult(data); + if (medias != null) { + List strings = new ArrayList<>(); + for (BaseMedia media : medias) { + strings.add(media.getPath()); + } + mCallback.onTakeMultiPictureSuccess(strings); + } + } + + public interface Callback { + default void onTakeMultiPictureSuccess(List pictures) { + } + + default void onTakePictureSuccess(String picture) { + } + } + +} diff --git a/lib_media_selector/src/main/java/com/android/sdk/mediaselector/SystemMediaSelector.java b/lib_media_selector/src/main/java/com/android/sdk/mediaselector/SystemMediaSelector.java new file mode 100644 index 0000000..9ac4037 --- /dev/null +++ b/lib_media_selector/src/main/java/com/android/sdk/mediaselector/SystemMediaSelector.java @@ -0,0 +1,294 @@ +package com.android.sdk.mediaselector; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.support.v4.app.Fragment; +import android.text.TextUtils; +import android.util.Log; + +import java.io.File; + +/** + * 通过系统相册或者系统相机获取照片 + *
    + *     1. 默认的 FileProvider 的 Authorities 为 {@code PackageName+ ".file.provider"}
    + * 
    + * + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-08-09 10:50 + */ +public class SystemMediaSelector { + + private static final String TAG = SystemMediaSelector.class.getSimpleName(); + + private static final int REQUEST_CAMERA = 196; + private static final int REQUEST_CROP = 197; + private static final int REQUEST_ALBUM = 198; + private static final int REQUEST_FILE = 199; + + private static final String POSTFIX = ".file.provider"; + private String mAuthority; + + private final MediaSelectorCallback mMediaSelectorCallback; + private Activity mActivity; + private Fragment mFragment; + + private String mSavePhotoPath; + private CropOptions mCropOptions; + private final CropOptions mDefaultOptions = new CropOptions(); + private String mCropTitle; + private boolean mNeedCrop; + + public SystemMediaSelector(Activity activity, MediaSelectorCallback systemMediaSelectorCallback) { + mActivity = activity; + mMediaSelectorCallback = systemMediaSelectorCallback; + init(); + } + + public SystemMediaSelector(Fragment fragment, MediaSelectorCallback systemMediaSelectorCallback) { + mFragment = fragment; + mMediaSelectorCallback = systemMediaSelectorCallback; + init(); + } + + private void init() { + mAuthority = getContext().getPackageName().concat(POSTFIX); + mDefaultOptions.setAspectX(1); + mDefaultOptions.setAspectY(1); + mDefaultOptions.setOutputX(1000); + mDefaultOptions.setOutputY(1000); + } + + private Context getContext() { + if (mFragment != null) { + return mFragment.getContext(); + } else { + return mActivity; + } + } + + private void startActivityForResult(Intent intent, int code) { + if (mFragment != null) { + mFragment.startActivityForResult(intent, code); + } else { + mActivity.startActivityForResult(intent, code); + } + } + /////////////////////////////////////////////////////////////////////////// + // setter + /////////////////////////////////////////////////////////////////////////// + + /** + * 默认的authority 为"包名.fileProvider" + * + * @param authority 指定FileProvider的authority + */ + public void setAuthority(String authority) { + mAuthority = authority; + } + + private CropOptions getCropOptions() { + return mCropOptions == null ? mDefaultOptions : mCropOptions; + } + + /////////////////////////////////////////////////////////////////////////// + // Take method + /////////////////////////////////////////////////////////////////////////// + + public boolean takePhotoFromCamera(String savePath) { + mSavePhotoPath = savePath; + mNeedCrop = false; + return toCamera(); + } + + /** + * 为了保证裁裁剪图片不出问题,务必指定CropOptions中的各个参数(不要为0,比如魅族手机如果指定OutputX和OutputY为0,则只会裁减出一个像素),否则可能出现问题 + */ + public boolean takePhotoFromCameraAndCrop(String savePath, CropOptions cropOptions, String cropTitle) { + mSavePhotoPath = savePath; + mNeedCrop = true; + mCropOptions = cropOptions; + mCropTitle = cropTitle; + return toCamera(); + } + + public boolean takePhotoFormAlbum() { + mNeedCrop = false; + try { + startActivityForResult(Utils.makeAlbumIntent(), REQUEST_ALBUM); + return true; + } catch (Exception e) { + return false; + } + } + + public boolean takePhotoFormAlbumAndCrop(String savePhotoPath, CropOptions cropOptions, String cropTitle) { + mNeedCrop = true; + mSavePhotoPath = savePhotoPath; + mCropOptions = cropOptions; + mCropTitle = cropTitle; + try { + startActivityForResult(Utils.makeAlbumIntent(), REQUEST_ALBUM); + return true; + } catch (Exception e) { + return false; + } + } + + private boolean toCamera() { + if (!Utils.hasCamera(getContext())) { + return false; + } + File targetFile = new File(mSavePhotoPath); + Intent intent = Utils.makeCaptureIntent(getContext(), targetFile, mAuthority); + try { + startActivityForResult(intent, REQUEST_CAMERA); + return true; + } catch (Exception e) { + Log.e(TAG, "takePhotoFromCamera error", e); + } + return false; + } + + private boolean toCrop() { + File targetFile = new File(mSavePhotoPath); + Intent intent = Utils.makeCropIntent(getContext(), targetFile, mAuthority, getCropOptions(), mCropTitle); + try { + startActivityForResult(intent, REQUEST_CROP); + return true; + } catch (Exception e) { + Log.e(TAG, "toCrop error", e); + } + return false; + } + + private boolean toCrop(Uri uri) { + File targetFile = new File(mSavePhotoPath); + Intent intent = Utils.makeCropIntent(getContext(), uri, targetFile, mAuthority, getCropOptions(), mCropTitle); + try { + startActivityForResult(intent, REQUEST_CROP); + return true; + } catch (Exception e) { + Log.e(TAG, "toCrop error", e); + } + return false; + } + + public boolean takeFile() { + return takeFile(null); + } + + public boolean takeFile(String mimeType) { + Intent intent = Utils.makeFilesIntent(mimeType); + try { + startActivityForResult(intent, REQUEST_FILE); + } catch (Exception e) { + Log.e(TAG, "takeFile error", e); + return false; + } + return true; + } + /////////////////////////////////////////////////////////////////////////// + // Process Result + /////////////////////////////////////////////////////////////////////////// + + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == REQUEST_CAMERA) { + processCameraResult(resultCode, data); + } else if (requestCode == REQUEST_CROP) { + processCropResult(resultCode, data); + } else if (requestCode == REQUEST_ALBUM) { + processAlbumResult(resultCode, data); + } else if (requestCode == REQUEST_FILE) { + processFileResult(resultCode, data); + } + } + + private void processFileResult(int resultCode, Intent data) { + if (resultCode == Activity.RESULT_OK && data != null) { + Uri uri = data.getData(); + if (uri == null) { + mMediaSelectorCallback.onTakeFail(); + } else { + String absolutePath = Utils.getAbsolutePath(getContext(), uri); + mMediaSelectorCallback.onTakeSuccess(absolutePath); + } + } else { + mMediaSelectorCallback.onTakeFail(); + } + } + + private void processAlbumResult(int resultCode, Intent data) { + if (resultCode == Activity.RESULT_OK) { + if (data != null && data.getData() != null) { + Uri uri = data.getData(); + if (mNeedCrop) { + boolean success = toCrop(uri); + if (!success) { + mMediaSelectorCallback.onTakeSuccess(Utils.getAbsolutePath(getContext(), uri)); + } + } else { + mMediaSelectorCallback.onTakeSuccess(Utils.getAbsolutePath(getContext(), uri)); + } + } else { + mMediaSelectorCallback.onTakeFail(); + } + } + } + + private void processCropResult(int resultCode, @SuppressWarnings("unused") Intent data) { + //有时候,系统裁减的结果可能没有保存到我们指定的目录,而是通过data返回了 + if (resultCode == Activity.RESULT_OK) { + if (new File(mSavePhotoPath).exists()) { + mMediaSelectorCallback.onTakeSuccess(mSavePhotoPath); + } else if (data != null && data.getData() != null) { + String realPathFromURI = Utils.getAbsolutePath(getContext(), data.getData()); + if (TextUtils.isEmpty(realPathFromURI)) { + mMediaSelectorCallback.onTakeFail(); + } else { + mMediaSelectorCallback.onTakeSuccess(realPathFromURI); + } + } else { + mMediaSelectorCallback.onTakeFail(); + } + } + } + + private void processCameraResult(int resultCode, @SuppressWarnings("unused") Intent data) { + if (resultCode == Activity.RESULT_OK) { + //检测图片是否被保存下来 + if (!new File(mSavePhotoPath).exists()) { + mMediaSelectorCallback.onTakeFail(); + return; + } + //需要裁减,可以裁减则进行裁减,否则直接返回 + if (mNeedCrop) { + boolean success = toCrop(); + if (!success) { + mMediaSelectorCallback.onTakeSuccess(mSavePhotoPath); + } + } else { + mMediaSelectorCallback.onTakeSuccess(mSavePhotoPath); + } + } + } + + /////////////////////////////////////////////////////////////////////////// + // Interface + /////////////////////////////////////////////////////////////////////////// + + public interface MediaSelectorCallback { + + default void onTakeSuccess(String path) { + } + + default void onTakeFail() { + } + + } + + +} diff --git a/lib_media_selector/src/main/java/com/android/sdk/mediaselector/Utils.java b/lib_media_selector/src/main/java/com/android/sdk/mediaselector/Utils.java new file mode 100644 index 0000000..3490f06 --- /dev/null +++ b/lib_media_selector/src/main/java/com/android/sdk/mediaselector/Utils.java @@ -0,0 +1,368 @@ +package com.android.sdk.mediaselector; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.Matrix; +import android.media.ExifInterface; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.provider.DocumentsContract; +import android.provider.MediaStore; +import android.support.v4.content.FileProvider; +import android.text.TextUtils; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.List; + +/** + * See: + *
    + *      https://stackoverflow.com/questions/20067508/get-real-path-from-uri-android-kitkat-new-storage-access-framework
    + * 
    + * + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2017-08-09 10:54 + */ +final class Utils { + + + private Utils() { + throw new UnsupportedOperationException("Utils"); + } + + /////////////////////////////////////////////////////////////////////////// + // Camera + /////////////////////////////////////////////////////////////////////////// + + /** + * 判断系统中是否存在可以启动的相机应用 + * + * @return 存在返回true,不存在返回false + */ + static boolean hasCamera(Context context) { + PackageManager packageManager = context.getPackageManager(); + Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + List list = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); + return list.size() > 0; + } + + /** + * @param targetFile 源文件,裁剪之后新的图片覆盖此文件 + */ + static Intent makeCaptureIntent(Context context, File targetFile, String authority) { + makeFilePath(targetFile); + Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + if (Build.VERSION.SDK_INT < 24) { + Uri fileUri = Uri.fromFile(targetFile); + intent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri); + } else { + Uri fileUri = FileProvider.getUriForFile(context, authority, targetFile); + intent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + return intent; + } + + /////////////////////////////////////////////////////////////////////////// + // Files + /////////////////////////////////////////////////////////////////////////// + static Intent makeFilesIntent(String mimeType) { + if (TextUtils.isEmpty(mimeType)) { + mimeType = "*/*"; + } + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.setType(mimeType); + intent.addCategory(Intent.CATEGORY_OPENABLE); + return intent; + } + + /////////////////////////////////////////////////////////////////////////// + // Crop + /////////////////////////////////////////////////////////////////////////// + + /** + * @param targetFile 源文件,裁剪之后新的图片覆盖此文件 + */ + static Intent makeCropIntent(Context context, File targetFile, String authority, CropOptions cropOptions, String title) { + + makeFilePath(targetFile); + Intent intent = new Intent("com.android.camera.action.CROP"); + + Uri fileUri; + if (Build.VERSION.SDK_INT < 24) { + fileUri = Uri.fromFile(targetFile); + } else { + fileUri = FileProvider.getUriForFile(context, authority, targetFile); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + } + + intent.setDataAndType(fileUri, "image/*"); + intent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri); + intent.putExtra("aspectX", cropOptions.getAspectX()); + intent.putExtra("aspectY", cropOptions.getAspectY()); + intent.putExtra("outputX", cropOptions.getOutputX()); + intent.putExtra("outputY", cropOptions.getOutputY()); + intent.putExtra("scale", true); + intent.putExtra("return-data", false); + intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()); + intent.putExtra("noFaceDetection", true); + intent = Intent.createChooser(intent, title); + return intent; + } + + /** + * @param targetFile 目标文件,裁剪之后新的图片保存到此文件 + * @param src 源文件 + */ + static Intent makeCropIntent(Context context, Uri src, File targetFile, String authority, CropOptions cropOptions, String title) { + + Intent intent = new Intent("com.android.camera.action.CROP"); + intent.setDataAndType(src, "image/*"); + + Uri fileUri; + if (Build.VERSION.SDK_INT < 24) { + fileUri = Uri.fromFile(targetFile); + } else { + fileUri = FileProvider.getUriForFile(context, authority, targetFile); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + } + + intent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri); + intent.putExtra("aspectX", cropOptions.getAspectX()); + intent.putExtra("aspectY", cropOptions.getAspectY()); + intent.putExtra("outputX", cropOptions.getOutputX()); + intent.putExtra("outputY", cropOptions.getOutputY()); + intent.putExtra("scale", true); + intent.putExtra("return-data", false); + intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()); + intent.putExtra("noFaceDetection", true); + intent = Intent.createChooser(intent, title); + return intent; + } + + /////////////////////////////////////////////////////////////////////////// + // Album + /////////////////////////////////////////////////////////////////////////// + + static Intent makeAlbumIntent() { + return new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); + } + + + /////////////////////////////////////////////////////////////////////////// + // 从各种Uri中获取真实的路径 + /////////////////////////////////////////////////////////////////////////// + static String getAbsolutePath(final Context context, final Uri uri) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && DocumentsContract.isDocumentUri(context, uri)) { + // ExternalStorageProvider + if (isExternalStorageDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + if ("primary".equalsIgnoreCase(type)) { + return Environment.getExternalStorageDirectory() + "/" + split[1]; + } + // TODO handle non-primary volumes + } + // DownloadsProvider + else if (isDownloadsDocument(uri)) { + final String id = DocumentsContract.getDocumentId(uri); + final Uri contentUri = ContentUris.withAppendedId( + Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); + return getDataColumn(context, contentUri, null, null); + } + // MediaProvider + else if (isMediaDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + Uri contentUri = null; + if ("image".equals(type)) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } else if ("video".equals(type)) { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + } else if ("audio".equals(type)) { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + } + final String selection = "_id=?"; + final String[] selectionArgs = new String[]{split[1]}; + return getDataColumn(context, contentUri, selection, selectionArgs); + } + } + // MediaStore (and general) + else if ("content".equalsIgnoreCase(uri.getScheme())) { + return getDataColumn(context, uri, null, null); + } + // File + else if ("file".equalsIgnoreCase(uri.getScheme())) { + return uri.getPath(); + } + return null; + } + + /** + * Get the value of the data column for this Uri. This is useful for + * MediaStore Uris, and other file-based ContentProviders. + * + * @param context The context. + * @param uri The Uri to query. + * @param selection (Optional) Filter used in the query. + * @param selectionArgs (Optional) Selection arguments used in the query. + * @return The value of the _data column, which is typically a file path. + */ + private static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { + Cursor cursor = null; + final String column = MediaStore.Images.Media.DATA; + final String[] projection = {column}; + try { + cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); + if (cursor != null && cursor.moveToFirst()) { + final int column_index = cursor.getColumnIndexOrThrow(column); + return cursor.getString(column_index); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + return null; + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is ExternalStorageProvider. + */ + private static boolean isExternalStorageDocument(Uri uri) { + return "com.android.externalstorage.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is DownloadsProvider. + */ + private static boolean isDownloadsDocument(Uri uri) { + return "com.android.providers.downloads.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is MediaProvider. + */ + private static boolean isMediaDocument(Uri uri) { + return "com.android.providers.media.documents".equals(uri.getAuthority()); + } + + /////////////////////////////////////////////////////////////////////////// + // BitmapUtils:如果拍摄的图片存在角度问题,通过下面分发修正 + /////////////////////////////////////////////////////////////////////////// + + /** + * 读取图片的旋转的角度 + * + * @param path 图片绝对路径 + * @return 图片的旋转角度 + */ + @SuppressWarnings("unused") + public static int getBitmapDegree(String path) { + int degree = 0; + try { + // 从指定路径下读取图片,并获取其EXIF信息 + ExifInterface exifInterface = new ExifInterface(path); + // 获取图片的旋转信息 + int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, + ExifInterface.ORIENTATION_NORMAL); + switch (orientation) { + case ExifInterface.ORIENTATION_ROTATE_90: + degree = 90; + break; + case ExifInterface.ORIENTATION_ROTATE_180: + degree = 180; + break; + case ExifInterface.ORIENTATION_ROTATE_270: + degree = 270; + break; + } + } catch (IOException e) { + e.printStackTrace(); + } + return degree; + } + + /** + * 将图片按照某个角度进行旋转 + * + * @param bm 需要旋转的图片 + * @param degree 旋转角度 + * @return 旋转后的图片 + */ + @SuppressWarnings("unused") + public static Bitmap rotateBitmapByDegree(Bitmap bm, int degree) { + Bitmap returnBm = null; + // 根据旋转角度,生成旋转矩阵 + Matrix matrix = new Matrix(); + matrix.postRotate(degree); + try { + // 将原始图片按照旋转矩阵进行旋转,并得到新的图片 + returnBm = Bitmap.createBitmap(bm, 0, 0, bm.getWidth(), bm.getHeight(), matrix, true); + } catch (OutOfMemoryError e) { + e.printStackTrace(); + } + if (returnBm == null) { + returnBm = bm; + } + if (bm != returnBm) { + bm.recycle(); + } + return returnBm; + } + + /////////////////////////////////////////////////////////////////////////// + // FileUtils + /////////////////////////////////////////////////////////////////////////// + + private static boolean makeFilePath(File file) { + if (file == null) { + return false; + } + File parent = file.getParentFile(); + return parent.exists() || parent.mkdirs(); + } + + /////////////////////////////////////////////////////////////////////////// + //如果照片保存的文件目录是由 getExternalFilesDir() 所提供的,那么,媒体扫描器是不能访问这些文件的,因为照片对于你的APP来说是私有的。 + /////////////////////////////////////////////////////////////////////////// + + /** + * 显示图片到相册 + * + * @param photoFile 要保存的图片文件 + */ + public static void displayToGallery(Context context, File photoFile) { + if (photoFile == null || !photoFile.exists()) { + return; + } + String photoPath = photoFile.getAbsolutePath(); + String photoName = photoFile.getName(); + // 其次把文件插入到系统图库 + try { + ContentResolver contentResolver = context.getContentResolver(); + MediaStore.Images.Media.insertImage(contentResolver, photoPath, photoName, null); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + // 最后通知图库更新 + context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.parse("file://" + photoPath))); + } +} diff --git a/lib_media_selector/src/main/res/drawable-xxhdpi/ic_boxing_camera.png b/lib_media_selector/src/main/res/drawable-xxhdpi/ic_boxing_camera.png new file mode 100644 index 0000000..1a09cce Binary files /dev/null and b/lib_media_selector/src/main/res/drawable-xxhdpi/ic_boxing_camera.png differ diff --git a/lib_network/.gitignore b/lib_network/.gitignore new file mode 100644 index 0000000..c06fb85 --- /dev/null +++ b/lib_network/.gitignore @@ -0,0 +1,22 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +*.iml +.idea/ +.gradle +/local.properties +.DS_Store +/build +/captures +*.apk +*.ap_ +*.dex +*.class +bin/ +gen/ +local.properties \ No newline at end of file diff --git a/lib_network/README.md b/lib_network/README.md new file mode 100644 index 0000000..94eb263 --- /dev/null +++ b/lib_network/README.md @@ -0,0 +1 @@ +# 网络库 \ No newline at end of file diff --git a/lib_network/build.gradle b/lib_network/build.gradle new file mode 100644 index 0000000..4d8d551 --- /dev/null +++ b/lib_network/build.gradle @@ -0,0 +1,46 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + + compileSdkVersion rootProject.compileSdkVersion + buildToolsVersion rootProject.buildToolsVersion + + defaultConfig { + + minSdkVersion rootProject.minSdkVersion + targetSdkVersion rootProject.targetSdkVersion + versionCode 1 + versionName "1.0" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + lintOptions { + abortOnError false + } + +} + +dependencies { + implementation androidLibraries.androidAnnotations + implementation thirdLibraries.supportOptional + implementation thirdLibraries.timber + implementation thirdLibraries.retrofit + implementation thirdLibraries.retrofitConverterGson + implementation thirdLibraries.okHttp + implementation thirdLibraries.gson + implementation thirdLibraries.rxJava + implementation thirdLibraries.retrofitRxJava2CallAdapter + compileOnly kotlinLibraries.kotlinStdlib +} diff --git a/lib_network/proguard-rules.pro b/lib_network/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/lib_network/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/lib_network/src/main/AndroidManifest.xml b/lib_network/src/main/AndroidManifest.xml new file mode 100644 index 0000000..1d84e55 --- /dev/null +++ b/lib_network/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/lib_network/src/main/java/com/android/sdk/net/NetContext.java b/lib_network/src/main/java/com/android/sdk/net/NetContext.java new file mode 100644 index 0000000..472fa10 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/NetContext.java @@ -0,0 +1,119 @@ +package com.android.sdk.net; + +import android.support.annotation.NonNull; + +import com.android.sdk.net.core.ExceptionFactory; +import com.android.sdk.net.provider.ApiHandler; +import com.android.sdk.net.provider.ErrorDataAdapter; +import com.android.sdk.net.provider.ErrorMessage; +import com.android.sdk.net.provider.HttpConfig; +import com.android.sdk.net.provider.NetworkChecker; +import com.android.sdk.net.provider.PostTransformer; +import com.android.sdk.net.service.ServiceFactory; +import com.android.sdk.net.service.ServiceHelper; + +import okhttp3.OkHttpClient; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-08 11:06 + */ +public class NetContext { + + private static volatile NetContext CONTEXT; + + public static NetContext get() { + if (CONTEXT == null) { + synchronized (NetContext.class) { + if (CONTEXT == null) { + CONTEXT = new NetContext(); + } + } + } + return CONTEXT; + } + + private NetContext() { + mServiceHelper = new ServiceHelper(); + } + + private NetProvider mNetProvider; + private ServiceHelper mServiceHelper; + + public Builder newBuilder() { + return new Builder(); + } + + private void init(@NonNull NetProvider netProvider) { + mNetProvider = netProvider; + } + + public boolean connected() { + return mNetProvider.isConnected(); + } + + public NetProvider netProvider() { + NetProvider retProvider = mNetProvider; + + if (retProvider == null) { + throw new RuntimeException("NetContext has not be initialized"); + } + return retProvider; + } + + public OkHttpClient httpClient() { + return mServiceHelper.getOkHttpClient(netProvider().httpConfig()); + } + + public ServiceFactory serviceFactory() { + return mServiceHelper.getServiceFactory(netProvider().httpConfig()); + } + + public class Builder { + + private NetProviderImpl mNetProvider = new NetProviderImpl(); + + public Builder aipHandler(@NonNull ApiHandler apiHandler) { + mNetProvider.mApiHandler = apiHandler; + return this; + } + + public Builder httpConfig(@NonNull HttpConfig httpConfig) { + mNetProvider.mHttpConfig = httpConfig; + return this; + } + + public Builder errorMessage(@NonNull ErrorMessage errorMessage) { + mNetProvider.mErrorMessage = errorMessage; + return this; + } + + public Builder errorDataAdapter(@NonNull ErrorDataAdapter errorDataAdapter) { + mNetProvider.mErrorDataAdapter = errorDataAdapter; + return this; + } + + public Builder networkChecker(@NonNull NetworkChecker networkChecker) { + mNetProvider.mNetworkChecker = networkChecker; + return this; + } + + public Builder postTransformer(@NonNull PostTransformer postTransformer) { + mNetProvider.mPostTransformer = postTransformer; + return this; + } + + public Builder exceptionFactory(@NonNull ExceptionFactory exceptionFactory) { + mNetProvider.mExceptionFactory = exceptionFactory; + return this; + } + + public void setup() { + mNetProvider.checkRequired(); + NetContext.get().init(mNetProvider); + } + + } + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/NetProvider.java b/lib_network/src/main/java/com/android/sdk/net/NetProvider.java new file mode 100644 index 0000000..1066565 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/NetProvider.java @@ -0,0 +1,88 @@ +package com.android.sdk.net; + +import android.support.annotation.NonNull; + +import com.android.sdk.net.core.ExceptionFactory; +import com.android.sdk.net.provider.ApiHandler; +import com.android.sdk.net.provider.ErrorDataAdapter; +import com.android.sdk.net.provider.ErrorMessage; +import com.android.sdk.net.provider.HttpConfig; +import com.android.sdk.net.provider.NetworkChecker; +import com.android.sdk.net.provider.PostTransformer; + +public interface NetProvider { + + boolean isConnected(); + + ApiHandler aipHandler(); + + HttpConfig httpConfig(); + + @NonNull + ErrorMessage errorMessage(); + + @NonNull + ErrorDataAdapter errorDataAdapter(); + + PostTransformer postTransformer(); + + ExceptionFactory exceptionFactory(); + +} + +class NetProviderImpl implements NetProvider { + + ExceptionFactory mExceptionFactory; + ApiHandler mApiHandler; + HttpConfig mHttpConfig; + ErrorMessage mErrorMessage; + ErrorDataAdapter mErrorDataAdapter; + NetworkChecker mNetworkChecker; + PostTransformer mPostTransformer; + + @Override + public boolean isConnected() { + return mNetworkChecker.isConnected(); + } + + @NonNull + @Override + public ApiHandler aipHandler() { + return mApiHandler; + } + + @NonNull + @Override + public HttpConfig httpConfig() { + return mHttpConfig; + } + + @NonNull + @Override + public ErrorMessage errorMessage() { + return mErrorMessage; + } + + @NonNull + @Override + public ErrorDataAdapter errorDataAdapter() { + return mErrorDataAdapter; + } + + @Override + public PostTransformer postTransformer() { + return mPostTransformer; + } + + @Override + public ExceptionFactory exceptionFactory() { + return mExceptionFactory; + } + + void checkRequired() { + if (mErrorMessage == null || mErrorDataAdapter == null || mNetworkChecker == null || mHttpConfig == null) { + throw new NullPointerException("缺少必要的参数,必须提供:ErrorMessage、mErrorDataAdapter、mNetworkChecker、HttpConfig。"); + } + } + +} \ No newline at end of file diff --git a/lib_network/src/main/java/com/android/sdk/net/core/DataExtractor.java b/lib_network/src/main/java/com/android/sdk/net/core/DataExtractor.java new file mode 100644 index 0000000..6127885 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/core/DataExtractor.java @@ -0,0 +1,7 @@ +package com.android.sdk.net.core; + +public interface DataExtractor { + + S getDataFromHttpResult(Result rResult); + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/core/ExceptionFactory.java b/lib_network/src/main/java/com/android/sdk/net/core/ExceptionFactory.java new file mode 100644 index 0000000..18ef12b --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/core/ExceptionFactory.java @@ -0,0 +1,13 @@ +package com.android.sdk.net.core; + +/** + * 某些业务调用所产生的异常不是全局通用的,可以传递此接口用于创建特定的异常 + */ +public interface ExceptionFactory { + + /** + * 根据{@link Result}创建特定的业务异常 + */ + Exception create(Result result); + +} \ No newline at end of file diff --git a/lib_network/src/main/java/com/android/sdk/net/core/HttpResultTransformer.java b/lib_network/src/main/java/com/android/sdk/net/core/HttpResultTransformer.java new file mode 100644 index 0000000..270cf1a --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/core/HttpResultTransformer.java @@ -0,0 +1,106 @@ +package com.android.sdk.net.core; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.android.sdk.net.NetContext; +import com.android.sdk.net.exception.ApiErrorException; +import com.android.sdk.net.exception.NetworkErrorException; +import com.android.sdk.net.exception.ServerErrorException; +import com.android.sdk.net.provider.ApiHandler; +import com.android.sdk.net.provider.PostTransformer; + +import org.reactivestreams.Publisher; + +import io.reactivex.Flowable; +import io.reactivex.FlowableTransformer; +import io.reactivex.Observable; +import io.reactivex.ObservableSource; +import io.reactivex.ObservableTransformer; + +public class HttpResultTransformer> implements ObservableTransformer, FlowableTransformer { + + private final boolean mRequireNonNullData; + private final DataExtractor mDataExtractor; + @Nullable private final ExceptionFactory mExceptionFactory; + + public HttpResultTransformer(boolean requireNonNullData, @NonNull DataExtractor dataExtractor, @Nullable ExceptionFactory exceptionFactory) { + mRequireNonNullData = requireNonNullData; + mDataExtractor = dataExtractor; + if (exceptionFactory == null) { + exceptionFactory = NetContext.get().netProvider().exceptionFactory(); + } + mExceptionFactory = exceptionFactory; + } + + @Override + public Publisher apply(Flowable upstream) { + Flowable downstreamFlowable = upstream.map(this::processData); + @SuppressWarnings("unchecked") + PostTransformer postTransformer = (PostTransformer) NetContext.get().netProvider().postTransformer(); + if (postTransformer != null) { + return downstreamFlowable.compose(postTransformer); + } else { + return downstreamFlowable; + } + } + + @Override + public ObservableSource apply(Observable upstream) { + Observable downstreamObservable = upstream.map(this::processData); + @SuppressWarnings("unchecked") + PostTransformer postTransformer = (PostTransformer) NetContext.get().netProvider().postTransformer(); + if (postTransformer != null) { + return downstreamObservable.compose(postTransformer); + } else { + return downstreamObservable; + } + } + + private Downstream processData(Result rResult) { + if (rResult == null) { + + if (NetContext.get().connected()) { + throwAs(new ServerErrorException(ServerErrorException.UNKNOW_ERROR));//有连接无数据,服务器错误 + } else { + throw new NetworkErrorException();//无连接网络错误 + } + + } else if (NetContext.get().netProvider().errorDataAdapter().isErrorDataStub(rResult)) { + + throwAs(new ServerErrorException(ServerErrorException.SERVER_DATA_ERROR));//服务器数据格式错误 + + } else if (!rResult.isSuccess()) {//检测响应码是否正确 + ApiHandler apiHandler = NetContext.get().netProvider().aipHandler(); + if (apiHandler != null) { + apiHandler.onApiError(rResult); + } + throwAs(createException(rResult)); + } + + if (mRequireNonNullData) { + if (rResult.getData() == null) {//如果约定必须返回的数据却没有返回数据,则认为是服务器错误 + throwAs(new ServerErrorException(ServerErrorException.UNKNOW_ERROR)); + } + } + + return mDataExtractor.getDataFromHttpResult(rResult); + } + + private Throwable createException(@NonNull Result rResult) { + ExceptionFactory exceptionFactory = mExceptionFactory; + if (exceptionFactory != null) { + Exception exception = exceptionFactory.create(rResult); + if (exception != null) { + return exception; + } + } + return new ApiErrorException(rResult.getCode(), rResult.getMessage()); + } + + @SuppressWarnings("unchecked") + private void throwAs(Throwable throwable) throws E { + throw (E) throwable; + } + +} \ No newline at end of file diff --git a/lib_network/src/main/java/com/android/sdk/net/core/Result.java b/lib_network/src/main/java/com/android/sdk/net/core/Result.java new file mode 100644 index 0000000..3b08d5b --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/core/Result.java @@ -0,0 +1,31 @@ +package com.android.sdk.net.core; + +/** + * 业务数据模型抽象 + * + * @author Ztiany + * Date : 2018-08-13 + */ +public interface Result { + + /** + * 实际返回类型 + */ + T getData(); + + /** + * 响应码 + */ + int getCode(); + + /** + * 相应消息 + */ + String getMessage(); + + /** + * 请求是否成功 + */ + boolean isSuccess(); + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/errorhandler/ErrorMessageFactory.java b/lib_network/src/main/java/com/android/sdk/net/errorhandler/ErrorMessageFactory.java new file mode 100644 index 0000000..9b3aba5 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/errorhandler/ErrorMessageFactory.java @@ -0,0 +1,76 @@ +package com.android.sdk.net.errorhandler; + +import android.text.TextUtils; + +import com.android.sdk.net.NetContext; +import com.android.sdk.net.exception.ApiErrorException; +import com.android.sdk.net.exception.NetworkErrorException; +import com.android.sdk.net.exception.ServerErrorException; +import com.android.sdk.net.provider.ErrorMessage; + +import java.io.IOException; +import java.net.ConnectException; + +import retrofit2.HttpException; +import timber.log.Timber; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-08 16:11 + */ +public class ErrorMessageFactory { + + public static CharSequence createMessage(Throwable exception) { + ErrorMessage mErrorMessage = NetContext.get().netProvider().errorMessage(); + Timber.d("createMessage with:" + exception); + + CharSequence message = null; + //SocketTimeoutException android NetworkErrorException extends IOException + //1:网络连接错误处理 + if (exception instanceof ConnectException || + exception instanceof IOException + || exception instanceof NetworkErrorException) { + message = mErrorMessage.netErrorMessage(exception); + } + //2:服务器错误处理 + else if (exception instanceof ServerErrorException) { + int errorType = ((ServerErrorException) exception).getErrorType(); + if (errorType == ServerErrorException.SERVER_DATA_ERROR) { + message = mErrorMessage.serverDataErrorMessage(exception); + } else if (errorType == ServerErrorException.UNKNOW_ERROR) { + message = mErrorMessage.serverErrorMessage(exception); + } + } + //3:响应码非200处理 + else if (exception instanceof HttpException) { + int code = ((HttpException) exception).code(); + if (code >= 500/*http 500 表示服务器错误*/) { + message = mErrorMessage.serverErrorMessage(exception); + } else if (code >= 400/*http 400 表示客户端请求出错*/) { + message = mErrorMessage.clientRequestErrorMessage(exception); + } + } else { + //4:api 错误处理 + if (exception instanceof ApiErrorException) { + message = exception.getMessage(); + if (TextUtils.isEmpty(message)) { + message = mErrorMessage.apiErrorMessage((ApiErrorException) exception); + } + } else { + throw new RuntimeException(exception); + } + } + + if (isEmpty(message)) { + message = mErrorMessage.unknowErrorMessage(exception); + } + + return message; + } + + private static boolean isEmpty(CharSequence str) { + return str == null || str.toString().trim().length() == 0; + } + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/exception/ApiErrorException.java b/lib_network/src/main/java/com/android/sdk/net/exception/ApiErrorException.java new file mode 100644 index 0000000..8b38764 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/exception/ApiErrorException.java @@ -0,0 +1,27 @@ +package com.android.sdk.net.exception; + +/** + * ApiErrorException 表示当调用接口失败 + * + * @author Ztiany + * Date : 2016-10-13 11:39 + */ +public class ApiErrorException extends Exception { + + private final int mCode; + + public ApiErrorException(int code, String message) { + super(message); + mCode = code; + } + + public int getCode() { + return mCode; + } + + @Override + public String getMessage() { + return super.getMessage(); + } + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/exception/NetworkErrorException.java b/lib_network/src/main/java/com/android/sdk/net/exception/NetworkErrorException.java new file mode 100644 index 0000000..212e0d7 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/exception/NetworkErrorException.java @@ -0,0 +1,10 @@ +package com.android.sdk.net.exception; + +/** + * 网络异常,由于网络原型造成的调用失败,统一封装成成NetworkConnectionException + * + * @author Ztiany + * Date : 2016-05-06 17:23 + */ +public class NetworkErrorException extends RuntimeException { +} diff --git a/lib_network/src/main/java/com/android/sdk/net/exception/ServerErrorException.java b/lib_network/src/main/java/com/android/sdk/net/exception/ServerErrorException.java new file mode 100644 index 0000000..f9eedf0 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/exception/ServerErrorException.java @@ -0,0 +1,36 @@ +package com.android.sdk.net.exception; + +import android.support.annotation.NonNull; + +/** + * 表示服务器异常 + * + * @author Ztiany + * Date : 2016-05-06 17:23 + */ +public class ServerErrorException extends Exception { + + private final int mErrorType; + + public static final int UNKNOW_ERROR = 1; + public static final int SERVER_DATA_ERROR = 2; + + /** + * @param errorType {@link #UNKNOW_ERROR},{@link #SERVER_DATA_ERROR} + */ + public ServerErrorException(int errorType) { + mErrorType = errorType; + } + + public int getErrorType() { + return mErrorType; + } + + @NonNull + @Override + public String toString() { + String string = super.toString(); + return string + (mErrorType == UNKNOW_ERROR ? " 未知错误 " : " Json 格式错误 "); + } + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/gson/AutoGenTypeAdapterFactory.java b/lib_network/src/main/java/com/android/sdk/net/gson/AutoGenTypeAdapterFactory.java new file mode 100644 index 0000000..bac0ab4 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/gson/AutoGenTypeAdapterFactory.java @@ -0,0 +1,24 @@ +package com.android.sdk.net.gson; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; + +/** + * https://gist.github.com/JakeWharton/0d67d01badcee0ae7bc9 + * https://gist.github.com/Piasy/fa507251da452d36b221 + * 使用AutoGson注解自动映射,比如把需要反序列化的抽象类映射到具体的实现 + */ +public final class AutoGenTypeAdapterFactory implements TypeAdapterFactory { + + @SuppressWarnings("unchecked") + @Override + public TypeAdapter create(final Gson gson, final TypeToken type) { + final Class rawType = (Class) type.getRawType(); + final AutoGson annotation = rawType.getAnnotation(AutoGson.class); + // Only deserialize classes decorated with @AutoGson. + return annotation == null ? null : (TypeAdapter) gson.getAdapter(annotation.autoClass()); + } + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/gson/AutoGson.java b/lib_network/src/main/java/com/android/sdk/net/gson/AutoGson.java new file mode 100644 index 0000000..dc91002 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/gson/AutoGson.java @@ -0,0 +1,29 @@ +package com.android.sdk.net.gson; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * https://gist.github.com/Piasy/fa507251da452d36b221 + *

    + * Marks an AutoValue/AutoParcel-annotated type for proper + * Gson serialization. + *

    + * This annotation is needed because the {@linkplain Retention retention} of + * AutoValue/AutoParcel + * does not allow reflection at runtime. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface AutoGson { + + /** + * A reference to the Auto*-generated class (e.g. AutoValue_MyClass/AutoParcel_MyClass). This + * is necessary to handle obfuscation of the class names. + * + * @return the annotated class's real type. + */ + Class autoClass(); +} diff --git a/lib_network/src/main/java/com/android/sdk/net/gson/ErrorJsonLenientConverterFactory.java b/lib_network/src/main/java/com/android/sdk/net/gson/ErrorJsonLenientConverterFactory.java new file mode 100644 index 0000000..b641b0e --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/gson/ErrorJsonLenientConverterFactory.java @@ -0,0 +1,56 @@ +package com.android.sdk.net.gson; + + +import com.android.sdk.net.NetContext; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import okhttp3.RequestBody; +import okhttp3.ResponseBody; +import retrofit2.Converter; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; +import timber.log.Timber; + +/** + * json 解析容错处理,参考 + * + * @author Ztiany + * Date : 2018-08-13 + */ +public class ErrorJsonLenientConverterFactory extends Converter.Factory { + + private final GsonConverterFactory mGsonConverterFactory; + + public ErrorJsonLenientConverterFactory(GsonConverterFactory gsonConverterFactory) { + mGsonConverterFactory = gsonConverterFactory; + } + + @Override + public Converter requestBodyConverter(Type type, + Annotation[] parameterAnnotations, + Annotation[] methodAnnotations, + Retrofit retrofit) { + return mGsonConverterFactory.requestBodyConverter(type, parameterAnnotations, methodAnnotations, retrofit); + } + + @Override + public Converter responseBodyConverter(Type type, + Annotation[] annotations, + Retrofit retrofit) { + + final Converter delegateConverter = mGsonConverterFactory.responseBodyConverter(type, annotations, retrofit); + + return (Converter) value -> { + try { + return delegateConverter.convert(value); + } catch (Exception e/*防止闪退:JsonSyntaxException、IOException or MalformedJsonException*/) { + Timber.e(e, "Json covert error -->error "); + return NetContext.get().netProvider().errorDataAdapter().createErrorDataStub(type, annotations, retrofit, value);//服务器数据格式错误 + } + }; + + } + +} \ No newline at end of file diff --git a/lib_network/src/main/java/com/android/sdk/net/gson/GsonUtils.java b/lib_network/src/main/java/com/android/sdk/net/gson/GsonUtils.java new file mode 100644 index 0000000..2a26922 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/gson/GsonUtils.java @@ -0,0 +1,44 @@ +package com.android.sdk.net.gson; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.lang.reflect.Modifier; + +import kotlin.Unit; + +import static com.android.sdk.net.gson.JsonDeserializers.DoubleJsonDeserializer; +import static com.android.sdk.net.gson.JsonDeserializers.FloatJsonDeserializer; +import static com.android.sdk.net.gson.JsonDeserializers.IntegerJsonDeserializer; +import static com.android.sdk.net.gson.JsonDeserializers.StringJsonDeserializer; +import static com.android.sdk.net.gson.JsonDeserializers.UnitJsonDeserializer; +import static com.android.sdk.net.gson.JsonDeserializers.VoidJsonDeserializer; + +/** + * @author Ztiany + * Date: 2018-08-15 + */ +public class GsonUtils { + + private final static Gson GSON = new GsonBuilder() + .excludeFieldsWithModifiers(Modifier.TRANSIENT) + .excludeFieldsWithModifiers(Modifier.STATIC) + /*容错处理*/ + .registerTypeAdapter(int.class, new IntegerJsonDeserializer()) + .registerTypeAdapter(Integer.class, new IntegerJsonDeserializer()) + .registerTypeAdapter(double.class, new DoubleJsonDeserializer()) + .registerTypeAdapter(Double.class, new DoubleJsonDeserializer()) + .registerTypeAdapter(float.class, new FloatJsonDeserializer()) + .registerTypeAdapter(Float.class, new FloatJsonDeserializer()) + .registerTypeAdapter(String.class, new StringJsonDeserializer()) + .registerTypeAdapter(Void.class, new VoidJsonDeserializer()) + .registerTypeAdapter(Unit.class, new UnitJsonDeserializer()) + /*根据注解反序列化抽象类或接口*/ + .registerTypeAdapterFactory(new AutoGenTypeAdapterFactory()) + .create(); + + public static Gson gson() { + return GSON; + } + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/gson/JsonDeserializers.java b/lib_network/src/main/java/com/android/sdk/net/gson/JsonDeserializers.java new file mode 100644 index 0000000..abfdd21 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/gson/JsonDeserializers.java @@ -0,0 +1,93 @@ +package com.android.sdk.net.gson; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; + +import java.lang.reflect.Type; + +import kotlin.Unit; +import timber.log.Timber; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-13 14:57 + */ +class JsonDeserializers { + + static class DoubleJsonDeserializer implements JsonDeserializer { + + @Override + public Double deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + try { + return json.getAsDouble(); + } catch (Exception e) { + Timber.e(e, "DoubleJsonDeserializer-deserialize-error:" + (json != null ? json.toString() : "")); + return 0D; + } + } + + } + + static class IntegerJsonDeserializer implements JsonDeserializer { + + @Override + public Integer deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + try { + return json.getAsInt(); + } catch (Exception e) { + Timber.e(e, "IntegerJsonDeserializer-deserialize-error:" + (json != null ? json.toString() : "")); + return 0; + } + } + + } + + static class StringJsonDeserializer implements JsonDeserializer { + + @Override + public String deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + try { + return json.getAsString(); + } catch (Exception e) { + Timber.e(e, "StringJsonDeserializer-deserialize-error:" + (json != null ? json.toString() : "")); + return null; + } + } + } + + static class VoidJsonDeserializer implements JsonDeserializer { + + @Override + public Void deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return null; + } + + } + + static class FloatJsonDeserializer implements JsonDeserializer { + + @Override + public Float deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + try { + return json.getAsFloat(); + } catch (Exception e) { + Timber.e(e, "FloatJsonDeserializer-deserialize-error:" + (json != null ? json.toString() : "")); + return 0F; + } + } + + } + + static class UnitJsonDeserializer implements JsonDeserializer { + + @Override + public Unit deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return Unit.INSTANCE; + } + + } + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/https/HttpsUtils.java b/lib_network/src/main/java/com/android/sdk/net/https/HttpsUtils.java new file mode 100644 index 0000000..911db39 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/https/HttpsUtils.java @@ -0,0 +1,167 @@ +package com.android.sdk.net.https; + +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +/** + * https://github.com/hongyangAndroid/okhttputils + */ +public class HttpsUtils { + + public static class SSLParams { + public SSLSocketFactory sSLSocketFactory; + public X509TrustManager trustManager; + } + + /** + * 创建SSLParams + * + * @param certificates 本地证书流 + * @param bksFile 用于双向验证,本地bks证书 + * @param password 本地证书密码 + * @return 创建的SSLParams + */ + public static SSLParams getSslSocketFactory(InputStream[] certificates, InputStream bksFile, String password) { + SSLParams sslParams = new SSLParams(); + try { + TrustManager[] trustManagers = prepareTrustManager(certificates); + KeyManager[] keyManagers = prepareKeyManager(bksFile, password); + SSLContext sslContext = SSLContext.getInstance("TLS"); + X509TrustManager trustManager; + if (trustManagers != null) { + trustManager = new SafeTrustManager(chooseTrustManager(trustManagers)); + } else { + trustManager = new UnSafeTrustManager(); + } + sslContext.init(keyManagers, new TrustManager[]{trustManager}, null); + sslParams.sSLSocketFactory = sslContext.getSocketFactory(); + sslParams.trustManager = trustManager; + return sslParams; + } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) { + throw new AssertionError(e); + } + } + + private class UnSafeHostnameVerifier implements HostnameVerifier { + @Override + public boolean verify(String hostname, SSLSession session) { + return true; + } + } + + private static class UnSafeTrustManager implements X509TrustManager { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new java.security.cert.X509Certificate[]{}; + } + } + + private static TrustManager[] prepareTrustManager(InputStream... certificates) { + if (certificates == null || certificates.length <= 0) return null; + try { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null); + int index = 0; + for (InputStream certificate : certificates) { + String certificateAlias = Integer.toString(index++); + keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate)); + try { + if (certificate != null) + certificate.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + TrustManagerFactory trustManagerFactory; + trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(keyStore); + return trustManagerFactory.getTrustManagers(); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + private static KeyManager[] prepareKeyManager(InputStream bksFile, String password) { + try { + if (bksFile == null || password == null) return null; + KeyStore clientKeyStore = KeyStore.getInstance("BKS"); + clientKeyStore.load(bksFile, password.toCharArray()); + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(clientKeyStore, password.toCharArray()); + return keyManagerFactory.getKeyManagers(); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + private static X509TrustManager chooseTrustManager(TrustManager[] trustManagers) { + for (TrustManager trustManager : trustManagers) { + if (trustManager instanceof X509TrustManager) { + return (X509TrustManager) trustManager; + } + } + return null; + } + + + private static class SafeTrustManager implements X509TrustManager { + + private X509TrustManager defaultTrustManager; + private X509TrustManager localTrustManager; + + SafeTrustManager(X509TrustManager localTrustManager) throws NoSuchAlgorithmException, KeyStoreException { + TrustManagerFactory var4 = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + var4.init((KeyStore) null); + defaultTrustManager = chooseTrustManager(var4.getTrustManagers()); + this.localTrustManager = localTrustManager; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + try { + defaultTrustManager.checkServerTrusted(chain, authType); + } catch (CertificateException ce) { + localTrustManager.checkServerTrusted(chain, authType); + } + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + } +} diff --git a/lib_network/src/main/java/com/android/sdk/net/kit/Consumer.java b/lib_network/src/main/java/com/android/sdk/net/kit/Consumer.java new file mode 100644 index 0000000..549cc7b --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/kit/Consumer.java @@ -0,0 +1,14 @@ +package com.android.sdk.net.kit; + +import android.support.annotation.Nullable; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-05-15 19:04 + */ +public interface Consumer { + + void accept(@Nullable T t); + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/kit/Extensions.kt b/lib_network/src/main/java/com/android/sdk/net/kit/Extensions.kt new file mode 100644 index 0000000..1e0f0c2 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/kit/Extensions.kt @@ -0,0 +1,10 @@ +package com.android.sdk.net.kit + +import com.android.sdk.net.service.ServiceFactory + +/** + *@author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-04-02 11:09 + */ +inline fun ServiceFactory.create(): T = create(T::class.java) \ No newline at end of file diff --git a/lib_network/src/main/java/com/android/sdk/net/kit/ResultHandlers.java b/lib_network/src/main/java/com/android/sdk/net/kit/ResultHandlers.java new file mode 100644 index 0000000..ff36210 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/kit/ResultHandlers.java @@ -0,0 +1,120 @@ +package com.android.sdk.net.kit; + +import com.android.sdk.net.core.ExceptionFactory; +import com.android.sdk.net.core.HttpResultTransformer; +import com.android.sdk.net.core.Result; +import com.github.dmstocking.optional.java.util.Optional; + +/** + * 用于处理 Retrofit + RxJava2 网络请求返回的结果 + * + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-11-22 17:22 + */ +@SuppressWarnings("unused") +public class ResultHandlers { + + private static class ResultTransformer> extends HttpResultTransformer { + ResultTransformer() { + super(true, Result::getData, null); + } + + ResultTransformer(ExceptionFactory exceptionFactory) { + super(true, Result::getData, exceptionFactory); + } + } + + private static class OptionalResultTransformer> extends HttpResultTransformer, T> { + OptionalResultTransformer() { + super(false, rResult -> Optional.ofNullable(rResult.getData()), null); + } + + OptionalResultTransformer(ExceptionFactory exceptionFactory) { + super(false, rResult -> Optional.ofNullable(rResult.getData()), exceptionFactory); + } + } + + private static class ResultChecker> extends HttpResultTransformer { + @SuppressWarnings("unchecked") + ResultChecker() { + super(false, rResult -> (T) rResult, null); + } + + @SuppressWarnings("unchecked") + ResultChecker(ExceptionFactory exceptionFactory) { + super(false, rResult -> (T) rResult, exceptionFactory); + } + } + + private static final ResultTransformer DATA_TRANSFORMER = new ResultTransformer(); + + private static final OptionalResultTransformer OPTIONAL_TRANSFORMER = new OptionalResultTransformer(); + + private static final ResultChecker RESULT_CHECKER = new ResultChecker(); + + /** + * 返回一个Transformer,用于统一处理网络请求返回的 Observer 数据。对网络异常和请求结果做了通用处理: + *

    +     * 1.  网络无连接抛出 {@link com.android.sdk.net.exception.NetworkErrorException} 由下游处理
    +     * 2. HttpResult==null 抛出 {@link com.android.sdk.net.exception.NetworkErrorException} 由下游处理
    +     * 3. HttpResult.getCode() != SUCCESS 抛出 {@link com.android.sdk.net.exception.ApiErrorException} 由下游处理
    +     * 4. 返回的结果不符合约定的数据模型处理或为 null 抛出 {@link com.android.sdk.net.exception.ServerErrorException} 由下游处理
    +     * 5. 最后把 HttpResult<T> 中的数据 T 提取到下游
    +     * 
    + */ + @SuppressWarnings("unchecked") + private static > HttpResultTransformer _resultExtractor() { + return (HttpResultTransformer) DATA_TRANSFORMER; + } + + public static HttpResultTransformer> resultExtractor() { + return _resultExtractor(); + } + + /** + * 与{@link #resultExtractor()}的行为类型,但是最后把 HttpResult<T> 中的数据 T 用 {@link Optional} 包装后再转发到下游。 + * 适用于 HttpResult.getData() 可以为 Null 的情况 + */ + @SuppressWarnings("unchecked") + private static > HttpResultTransformer, T> _optionalExtractor() { + return (HttpResultTransformer, T>) OPTIONAL_TRANSFORMER; + } + + public static HttpResultTransformer, Result> optionalExtractor() { + return _optionalExtractor(); + } + + /** + * 不提取 HttpResult<T> 中的数据 T,只进行网络异常、空数据异常、错误JSON格式异常处理。 + */ + @SuppressWarnings("unchecked") + private static > HttpResultTransformer _resultChecker() { + return (HttpResultTransformer) RESULT_CHECKER; + } + + public static HttpResultTransformer, Result> resultChecker() { + return _resultChecker(); + } + + private static > HttpResultTransformer _newExtractor(ExceptionFactory exceptionFactory) { + return new ResultTransformer<>(exceptionFactory); + } + + public static HttpResultTransformer> newExtractor(ExceptionFactory exceptionFactory) { + return _newExtractor(exceptionFactory); + } + + private static > HttpResultTransformer, T> _newOptionalExtractor(ExceptionFactory exceptionFactory) { + return new OptionalResultTransformer<>(exceptionFactory); + } + + public static HttpResultTransformer, Result> newOptionalExtractor(ExceptionFactory exceptionFactory) { + return _newOptionalExtractor(exceptionFactory); + } + + private static > HttpResultTransformer newResultChecker(ExceptionFactory exceptionFactory) { + return new ResultChecker<>(exceptionFactory); + } + +} \ No newline at end of file diff --git a/lib_network/src/main/java/com/android/sdk/net/kit/RxResultKit.java b/lib_network/src/main/java/com/android/sdk/net/kit/RxResultKit.java new file mode 100644 index 0000000..b8a2b41 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/kit/RxResultKit.java @@ -0,0 +1,123 @@ +package com.android.sdk.net.kit; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.android.sdk.net.NetContext; +import com.android.sdk.net.exception.ApiErrorException; +import com.android.sdk.net.exception.NetworkErrorException; +import com.github.dmstocking.optional.java.util.Optional; + +import org.reactivestreams.Publisher; + +import java.io.IOException; +import java.net.ConnectException; + +import io.reactivex.Flowable; +import io.reactivex.flowables.ConnectableFlowable; +import io.reactivex.functions.Function; +import retrofit2.HttpException; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-05-15 19:17 + */ +public class RxResultKit { + + /** + *
    +     * 1. 如果网络不可用,直接返回缓存,如果没有缓存,报错没有网络连接
    +     * 2. 如果存在网络
    +     *      2.1 如果没有缓存,则从网络获取
    +     *      2.1 如果有缓存,则先返回缓存,然后从网络获取
    +     *      2.1 对比缓存与网络数据,如果没有更新,则忽略
    +     *      2.1 如果有更新,则更新缓存,并返回网络数据
    +     * 
    + * + * @param remote 网络数据源 + * @param local 本地数据源 + * @param onNewData 当有更新时,返回新的数据,可以在这里存储 + * @param 数据类型 + * @param selector 比较器,返回当 true 表示两者相等,参数顺序为 (local, remote) + * @return 组合后的Observable + * + */ + public static Flowable> composeMultiSource( + Flowable> remote, + Flowable> local, + Selector selector, + Consumer onNewData) { + + //没有网络 + if (!NetContext.get().connected()) { + return local.flatMap((Function, Publisher>>) tOptional -> { + if (tOptional.isPresent()) { + return Flowable.just(tOptional); + } else { + return Flowable.error(new NetworkErrorException()); + } + }); + } + + //有网络 + ConnectableFlowable> sharedLocal = local.replay(); + sharedLocal.connect(); + + //组合数据 + Flowable> complexRemote = sharedLocal + .flatMap((Function, Publisher>>) localData -> { + //没有缓存 + if (!localData.isPresent()) { + return remote.doOnNext(tOptional -> onNewData.accept(tOptional.orElse(null))); + } + /*有缓存是网络错误,不触发错误,只有在过期时返回新的数据*/ + return remote + .onErrorResumeNext(onErrorResumeFunction(onNewData)) + .filter(remoteData -> selector.test(localData.get(), remoteData.orElse(null))) + .doOnNext(newData -> onNewData.accept(newData.orElse(null))); + }); + + return Flowable.concat(sharedLocal.filter(Optional::isPresent), complexRemote); + } + + public static Flowable> selectLocalOrRemote(Flowable> remote, @Nullable T local, Selector selector, Consumer onNewData) { + //没有网络没有缓存 + if (!NetContext.get().connected() && local == null) { + return Flowable.error(new NetworkErrorException()); + } + //有缓存 + if (local != null) { + return Flowable.concat( + Flowable.just(Optional.of(local)), + /*有缓存是网络错误,不触发错误,只有在过期时返回新的数据*/ + remote.onErrorResumeNext(onErrorResumeFunction(onNewData)) + .filter(tOptional -> selector.test(local, tOptional.orElse(null))) + .doOnNext(tOptional -> onNewData.accept(tOptional.orElse(null)))); + } else { + return remote; + } + } + + @NonNull + private static Function>> onErrorResumeFunction(Consumer onNewData) { + return throwable -> { + if (isNetworkError(throwable)) { + return Flowable.never(); + } else { + if (throwable instanceof ApiErrorException) { + onNewData.accept(null); + } + return Flowable.error(throwable); + } + }; + } + + private static boolean isNetworkError(Throwable exception) { + return exception instanceof ConnectException + || exception instanceof IOException + || exception instanceof HttpException + || exception instanceof NetworkErrorException; + } + +} \ No newline at end of file diff --git a/lib_network/src/main/java/com/android/sdk/net/kit/RxResultKit.kt b/lib_network/src/main/java/com/android/sdk/net/kit/RxResultKit.kt new file mode 100644 index 0000000..1184e40 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/kit/RxResultKit.kt @@ -0,0 +1,58 @@ +package com.android.sdk.net.kit + +import com.android.sdk.net.core.Result +import com.github.dmstocking.optional.java.util.Optional +import io.reactivex.Flowable +import io.reactivex.Observable + + +fun , E> Observable.optionalExtractor(): Observable> { + return this.compose(ResultHandlers.optionalExtractor()) +} + +fun , E> Observable.resultExtractor(): Observable { + return this.compose(ResultHandlers.resultExtractor()) +} + +fun > Observable.resultChecker(): Observable> { + return (this.compose(ResultHandlers.resultChecker())) +} + +fun , E> Flowable.optionalExtractor(): Flowable> { + return this.compose(ResultHandlers.optionalExtractor()) +} + +fun , E> Flowable.resultExtractor(): Flowable { + return this.compose(ResultHandlers.resultExtractor()) +} + +fun > Flowable.resultChecker(): Flowable> { + return (this.compose(ResultHandlers.resultChecker())) +} + +/**组合远程数据与本地数据,参考 [RxResultKit.composeMultiSource]*/ +fun composeMultiSource( + remote: Flowable>, + local: Flowable>, + selector: (local: T, remote: T?) -> Boolean, + onNewData: (T?) -> Unit +): Flowable> { + return RxResultKit.composeMultiSource(remote, local, Selector(selector), Consumer { onNewData(it) }) +} + +/**组合远程数据与本地数据,参考 [RxResultKit.composeMultiSource]*/ +fun composeMultiSource( + remote: Flowable>, + local: Flowable>, + onNewData: (T?) -> Unit +): Flowable> { + return RxResultKit.composeMultiSource(remote, local, { t1, t2 -> t1 != t2 }, { onNewData(it) }) +} + +fun selectLocalOrRemote(remote: Flowable>, local: T?, selector: (local: T, remote: T?) -> Boolean, onNewData: (T?) -> Unit): Flowable> { + return RxResultKit.selectLocalOrRemote(remote, local, selector, onNewData) +} + +fun selectLocalOrRemote(remote: Flowable>, local: T?, onNewData: (T?) -> Unit): Flowable> { + return RxResultKit.selectLocalOrRemote(remote, local, { t1, t2 -> t1 != t2 }, { onNewData(it) }) +} \ No newline at end of file diff --git a/lib_network/src/main/java/com/android/sdk/net/kit/Selector.java b/lib_network/src/main/java/com/android/sdk/net/kit/Selector.java new file mode 100644 index 0000000..4b0f089 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/kit/Selector.java @@ -0,0 +1,13 @@ +package com.android.sdk.net.kit; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +public interface Selector { + + /** + * returning true means accept remote data + */ + boolean test(@NonNull T local, @Nullable T remote); + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/progress/Dispatcher.java b/lib_network/src/main/java/com/android/sdk/net/progress/Dispatcher.java new file mode 100644 index 0000000..079d0c4 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/progress/Dispatcher.java @@ -0,0 +1,18 @@ +package com.android.sdk.net.progress; + + +import android.os.Handler; +import android.os.Looper; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-09-22 16:52 + */ +class Dispatcher { + private static final Handler HANDLER = new Handler(Looper.getMainLooper()); + + static void dispatch(Runnable runnable) { + HANDLER.post(runnable); + } +} diff --git a/lib_network/src/main/java/com/android/sdk/net/progress/ProgressListener.java b/lib_network/src/main/java/com/android/sdk/net/progress/ProgressListener.java new file mode 100644 index 0000000..c776950 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/progress/ProgressListener.java @@ -0,0 +1,10 @@ +package com.android.sdk.net.progress; + + +public interface ProgressListener { + + void onProgress(long contentLength, long currentBytes, float percent, boolean isFinish); + + void onLoadFail(Exception e); + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/progress/ProgressRequestBody.java b/lib_network/src/main/java/com/android/sdk/net/progress/ProgressRequestBody.java new file mode 100644 index 0000000..a4e72db --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/progress/ProgressRequestBody.java @@ -0,0 +1,92 @@ +package com.android.sdk.net.progress; + +import android.os.SystemClock; +import android.support.annotation.NonNull; + +import java.io.IOException; + +import okhttp3.MediaType; +import okhttp3.RequestBody; +import okio.Buffer; +import okio.BufferedSink; +import okio.ForwardingSink; +import okio.Okio; +import okio.Sink; + +class ProgressRequestBody extends RequestBody { + + private final ProgressListener mProgressListener; + private final RequestBody mDelegate; + private final int mRefreshTime; + private BufferedSink mBufferedSink; + + ProgressRequestBody(RequestBody delegate, int refreshTime, ProgressListener progressListener) { + this.mDelegate = delegate; + mProgressListener = progressListener; + this.mRefreshTime = refreshTime; + } + + @Override + public MediaType contentType() { + return mDelegate.contentType(); + } + + @Override + public long contentLength() { + try { + return mDelegate.contentLength(); + } catch (IOException e) { + e.printStackTrace(); + } + return -1; + } + + @Override + public void writeTo(@NonNull BufferedSink sink) throws IOException { + if (mBufferedSink == null) { + mBufferedSink = Okio.buffer(new CountingSink(sink)); + } + try { + mDelegate.writeTo(mBufferedSink); + mBufferedSink.flush(); + } catch (IOException e) { + e.printStackTrace(); + mProgressListener.onLoadFail(e); + throw e; + } + } + + private final class CountingSink extends ForwardingSink { + + private long totalBytesRead = 0L; + private long lastRefreshTime = 0L; //最后一次刷新的时间 + private long mContentLength; + private boolean mIsFinish; + + CountingSink(Sink delegate) { + super(delegate); + } + + @Override + public void write(@NonNull Buffer source, long byteCount) throws IOException { + try { + super.write(source, byteCount); + } catch (IOException e) { + e.printStackTrace(); + mProgressListener.onLoadFail(e); + throw e; + } + if (mContentLength == 0) { //避免重复调用 contentLength() + mContentLength = contentLength(); + } + totalBytesRead += byteCount; + + long curTime = SystemClock.elapsedRealtime(); + mIsFinish = totalBytesRead == mContentLength; + if (curTime - lastRefreshTime >= mRefreshTime || mIsFinish) { + mProgressListener.onProgress(totalBytesRead, mContentLength, totalBytesRead * 1.0F / mContentLength, mIsFinish); + lastRefreshTime = curTime; + } + } + } +} diff --git a/lib_network/src/main/java/com/android/sdk/net/progress/ProgressResponseBody.java b/lib_network/src/main/java/com/android/sdk/net/progress/ProgressResponseBody.java new file mode 100644 index 0000000..a147d49 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/progress/ProgressResponseBody.java @@ -0,0 +1,79 @@ +package com.android.sdk.net.progress; + +import android.os.SystemClock; +import android.support.annotation.NonNull; + +import java.io.IOException; + +import okhttp3.MediaType; +import okhttp3.ResponseBody; +import okio.Buffer; +import okio.BufferedSource; +import okio.ForwardingSource; +import okio.Okio; +import okio.Source; + +class ProgressResponseBody extends ResponseBody { + + private final int mRefreshTime; + private final ResponseBody mDelegate; + private final ProgressListener mProgressListener; + private BufferedSource mBufferedSource; + + ProgressResponseBody(ResponseBody responseBody, int refreshTime, ProgressListener progressListener) { + this.mDelegate = responseBody; + mProgressListener = progressListener; + this.mRefreshTime = refreshTime; + } + + @Override + public MediaType contentType() { + return mDelegate.contentType(); + } + + @Override + public long contentLength() { + return mDelegate.contentLength(); + } + + @Override + public BufferedSource source() { + if (mBufferedSource == null) { + mBufferedSource = Okio.buffer(source(mDelegate.source())); + } + return mBufferedSource; + } + + private Source source(Source source) { + return new ForwardingSource(source) { + private long mContentLength; + private long totalBytesRead = 0L; + private long lastRefreshTime = 0L; //最后一次刷新的时间 + + @Override + public long read(@NonNull Buffer sink, long byteCount) throws IOException { + long bytesRead; + try { + bytesRead = super.read(sink, byteCount); + } catch (IOException e) { + e.printStackTrace(); + mProgressListener.onLoadFail(e); + throw e; + } + if (mContentLength == 0) { + mContentLength = contentLength(); + } + // read() returns the number of bytes read, or -1 if this source is exhausted. + totalBytesRead += bytesRead != -1 ? bytesRead : 0; + long curTime = SystemClock.elapsedRealtime(); + + if (curTime - lastRefreshTime >= mRefreshTime || bytesRead == -1 || totalBytesRead == mContentLength) { + boolean finish = bytesRead == -1 && totalBytesRead == mContentLength; + mProgressListener.onProgress(mContentLength, totalBytesRead, totalBytesRead * 1.0F / mContentLength, finish); + lastRefreshTime = curTime; + } + return bytesRead; + } + }; + } +} diff --git a/lib_network/src/main/java/com/android/sdk/net/progress/RequestProgressInterceptor.java b/lib_network/src/main/java/com/android/sdk/net/progress/RequestProgressInterceptor.java new file mode 100644 index 0000000..d3475cf --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/progress/RequestProgressInterceptor.java @@ -0,0 +1,59 @@ +package com.android.sdk.net.progress; + +import android.support.annotation.NonNull; + +import java.io.IOException; + +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-09-20 15:39 + */ +public class RequestProgressInterceptor implements Interceptor { + + private static final int DEFAULT_REFRESH_TIME = 150; + private final UrlProgressListener mInterceptorProgressListener; + private int mRefreshTime = DEFAULT_REFRESH_TIME;//进度刷新时间(单位ms),避免高频率调用 + + public RequestProgressInterceptor(UrlProgressListener interceptorProgressListener) { + mInterceptorProgressListener = interceptorProgressListener; + if (mInterceptorProgressListener == null) { + throw new NullPointerException(); + } + } + + public void setRefreshTime(int refreshTime) { + mRefreshTime = refreshTime; + } + + @Override + public Response intercept(@NonNull Chain chain) throws IOException { + return chain.proceed(wrapRequestBody(chain.request())); + } + + private Request wrapRequestBody(Request request) { + if (request == null || request.body() == null) { + return request; + } + final String key = request.url().toString(); + return request.newBuilder() + .method(request.method(), new ProgressRequestBody(request.body(), mRefreshTime, new ProgressListener() { + + @Override + public void onProgress(long contentLength, long currentBytes, float percent, boolean isFinish) { + Dispatcher.dispatch(() -> mInterceptorProgressListener.onProgress(key, contentLength, currentBytes, percent, isFinish)); + } + + @Override + public void onLoadFail(Exception e) { + Dispatcher.dispatch(() -> mInterceptorProgressListener.onError(key, e)); + } + })) + .build(); + } + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/progress/ResponseProgressInterceptor.java b/lib_network/src/main/java/com/android/sdk/net/progress/ResponseProgressInterceptor.java new file mode 100644 index 0000000..c369ddf --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/progress/ResponseProgressInterceptor.java @@ -0,0 +1,58 @@ +package com.android.sdk.net.progress; + +import android.support.annotation.NonNull; + +import java.io.IOException; + +import okhttp3.Interceptor; +import okhttp3.Response; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-09-20 15:39 + */ +public class ResponseProgressInterceptor implements Interceptor { + + private static final int DEFAULT_REFRESH_TIME = 150; + private final UrlProgressListener mInterceptorProgressListener; + private int mRefreshTime = DEFAULT_REFRESH_TIME;//进度刷新时间(单位ms),避免高频率调用 + + public ResponseProgressInterceptor(UrlProgressListener interceptorProgressListener) { + mInterceptorProgressListener = interceptorProgressListener; + if (mInterceptorProgressListener == null) { + throw new NullPointerException(); + } + } + + public void setRefreshTime(int refreshTime) { + mRefreshTime = refreshTime; + } + + @Override + public Response intercept(@NonNull Chain chain) throws IOException { + return wrapResponseBody(chain.proceed(chain.request())); + } + + private Response wrapResponseBody(Response response) { + if (response == null || response.body() == null) { + return response; + } + + final String key = response.request().url().toString(); + return response.newBuilder() + .body(new ProgressResponseBody(response.body(), mRefreshTime, new ProgressListener() { + + @Override + public void onProgress(long contentLength, long currentBytes, float percent, boolean isFinish) { + Dispatcher.dispatch(() -> mInterceptorProgressListener.onProgress(key, contentLength, currentBytes, percent, isFinish)); + } + + @Override + public void onLoadFail(Exception e) { + Dispatcher.dispatch(() -> mInterceptorProgressListener.onError(key, e)); + } + })).build(); + } + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/progress/UrlProgressListener.java b/lib_network/src/main/java/com/android/sdk/net/progress/UrlProgressListener.java new file mode 100644 index 0000000..4321225 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/progress/UrlProgressListener.java @@ -0,0 +1,8 @@ +package com.android.sdk.net.progress; + +public interface UrlProgressListener { + + void onProgress(String url, long contentLength, long currentBytes, float percent, boolean isFinish); + + void onError(String url, Exception e); +} diff --git a/lib_network/src/main/java/com/android/sdk/net/provider/ApiHandler.java b/lib_network/src/main/java/com/android/sdk/net/provider/ApiHandler.java new file mode 100644 index 0000000..4ba05b6 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/provider/ApiHandler.java @@ -0,0 +1,16 @@ +package com.android.sdk.net.provider; + +import android.support.annotation.NonNull; + +import com.android.sdk.net.core.Result; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-08 15:37 + */ +public interface ApiHandler { + + void onApiError(@NonNull Result result); + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/provider/ErrorDataAdapter.java b/lib_network/src/main/java/com/android/sdk/net/provider/ErrorDataAdapter.java new file mode 100644 index 0000000..faa1f02 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/provider/ErrorDataAdapter.java @@ -0,0 +1,20 @@ +package com.android.sdk.net.provider; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import okhttp3.ResponseBody; +import retrofit2.Retrofit; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-08 18:00 + */ +public interface ErrorDataAdapter { + + Object createErrorDataStub(Type type, Annotation[] annotations, Retrofit retrofit, ResponseBody value); + + boolean isErrorDataStub(Object object); + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/provider/ErrorMessage.java b/lib_network/src/main/java/com/android/sdk/net/provider/ErrorMessage.java new file mode 100644 index 0000000..5b98cec --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/provider/ErrorMessage.java @@ -0,0 +1,42 @@ +package com.android.sdk.net.provider; + +import com.android.sdk.net.exception.ApiErrorException; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-08 16:01 + */ +public interface ErrorMessage { + + /** + * 网络错误提示消息。 + */ + CharSequence netErrorMessage(Throwable exception); + + /** + * 服务器返回的数据格式异常消息。 + */ + CharSequence serverDataErrorMessage(Throwable exception); + + /** + * 服务器错误,比如 500-600 响应码。 + */ + CharSequence serverErrorMessage(Throwable exception); + + /** + * 客户端请求错误,比如 400-499 响应码 + */ + CharSequence clientRequestErrorMessage(Throwable exception); + + /** + * API 调用错误 + */ + CharSequence apiErrorMessage(ApiErrorException exception); + + /** + * 未知错误 + */ + CharSequence unknowErrorMessage(Throwable exception); + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/provider/HttpConfig.java b/lib_network/src/main/java/com/android/sdk/net/provider/HttpConfig.java new file mode 100644 index 0000000..a292d8f --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/provider/HttpConfig.java @@ -0,0 +1,25 @@ +package com.android.sdk.net.provider; + +import io.reactivex.schedulers.Schedulers; +import okhttp3.OkHttpClient; +import retrofit2.Retrofit; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-08 16:42 + */ +public interface HttpConfig { + + void configHttp(OkHttpClient.Builder builder); + + /** + * default config is {@link retrofit2.converter.gson.GsonConverterFactory}、{@link retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory} with {@link Schedulers#io()} + * + * @return if true, default config do nothing. + */ + boolean configRetrofit(Retrofit.Builder builder); + + String baseUrl(); + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/provider/NetworkChecker.java b/lib_network/src/main/java/com/android/sdk/net/provider/NetworkChecker.java new file mode 100644 index 0000000..73ddfa3 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/provider/NetworkChecker.java @@ -0,0 +1,15 @@ +package com.android.sdk.net.provider; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-12-11 18:00 + */ +public interface NetworkChecker { + + /** + * 网络是否连接 + */ + boolean isConnected(); + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/provider/PostTransformer.java b/lib_network/src/main/java/com/android/sdk/net/provider/PostTransformer.java new file mode 100644 index 0000000..3fdf0da --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/provider/PostTransformer.java @@ -0,0 +1,17 @@ +package com.android.sdk.net.provider; + +import com.android.sdk.net.kit.ResultHandlers; + +import io.reactivex.FlowableTransformer; +import io.reactivex.ObservableTransformer; + +/** + * 经过 {@link ResultHandlers} 处理网络结果后,可以添加此接口来添加统一的再处理逻辑,比如 token 实现后的重试。 + * + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-12-21 14:31 + */ +public interface PostTransformer extends ObservableTransformer, FlowableTransformer { + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/service/ServiceFactory.java b/lib_network/src/main/java/com/android/sdk/net/service/ServiceFactory.java new file mode 100644 index 0000000..bea094b --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/service/ServiceFactory.java @@ -0,0 +1,70 @@ +package com.android.sdk.net.service; + +import com.android.sdk.net.gson.ErrorJsonLenientConverterFactory; +import com.android.sdk.net.gson.GsonUtils; +import com.android.sdk.net.progress.RequestProgressInterceptor; +import com.android.sdk.net.progress.ResponseProgressInterceptor; +import com.android.sdk.net.progress.UrlProgressListener; +import com.android.sdk.net.provider.HttpConfig; + +import io.reactivex.schedulers.Schedulers; +import okhttp3.OkHttpClient; +import retrofit2.Retrofit; +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; +import retrofit2.converter.gson.GsonConverterFactory; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-06-07 18:19 + */ +public class ServiceFactory { + + private final OkHttpClient mOkHttpClient; + private final String mBaseUrl; + private final Retrofit mRetrofit; + + ServiceFactory(OkHttpClient okHttpClient, HttpConfig httpConfig) { + mOkHttpClient = okHttpClient; + mBaseUrl = httpConfig.baseUrl(); + + Retrofit.Builder builder = new Retrofit.Builder(); + boolean abort = httpConfig.configRetrofit(builder); + + if (!abort) { + builder.baseUrl(mBaseUrl) + .client(okHttpClient) + .addConverterFactory(new ErrorJsonLenientConverterFactory(GsonConverterFactory.create(GsonUtils.gson()))) + .addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io())); + } + + mRetrofit = builder.build(); + } + + public T create(Class clazz) { + return mRetrofit.create(clazz); + } + + public T createWithUploadProgress(Class clazz, UrlProgressListener urlProgressListener) { + OkHttpClient okHttpClient = mOkHttpClient + .newBuilder() + .addNetworkInterceptor(new RequestProgressInterceptor(urlProgressListener)) + .build(); + Retrofit newRetrofit = mRetrofit.newBuilder().client(okHttpClient).build(); + return newRetrofit.create(clazz); + } + + public T createWithDownloadProgress(Class clazz, UrlProgressListener urlProgressListener) { + OkHttpClient okHttpClient = mOkHttpClient + .newBuilder() + .addNetworkInterceptor(new ResponseProgressInterceptor(urlProgressListener)) + .build(); + Retrofit newRetrofit = mRetrofit.newBuilder().client(okHttpClient).build(); + return newRetrofit.create(clazz); + } + + public String baseUrl() { + return mBaseUrl; + } + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/service/ServiceHelper.java b/lib_network/src/main/java/com/android/sdk/net/service/ServiceHelper.java new file mode 100644 index 0000000..3fba206 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/service/ServiceHelper.java @@ -0,0 +1,33 @@ +package com.android.sdk.net.service; + +import com.android.sdk.net.provider.HttpConfig; + +import okhttp3.OkHttpClient; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-08 16:51 + */ +public class ServiceHelper { + + private OkHttpClient mOkHttpClient; + private ServiceFactory mServiceFactory; + + public OkHttpClient getOkHttpClient(HttpConfig httpConfig) { + if (mOkHttpClient == null) { + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + httpConfig.configHttp(builder); + mOkHttpClient = builder.build(); + } + return mOkHttpClient; + } + + public ServiceFactory getServiceFactory(HttpConfig httpConfig) { + if (mServiceFactory == null) { + mServiceFactory = new ServiceFactory(getOkHttpClient(httpConfig), httpConfig); + } + return mServiceFactory; + } + +} diff --git a/lib_push/.gitignore b/lib_push/.gitignore new file mode 100644 index 0000000..c06fb85 --- /dev/null +++ b/lib_push/.gitignore @@ -0,0 +1,22 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +*.iml +.idea/ +.gradle +/local.properties +.DS_Store +/build +/captures +*.apk +*.ap_ +*.dex +*.class +bin/ +gen/ +local.properties \ No newline at end of file diff --git a/lib_push/README.md b/lib_push/README.md new file mode 100644 index 0000000..be8de78 --- /dev/null +++ b/lib_push/README.md @@ -0,0 +1,3 @@ +# 推送库 + +目前集成的是极光推送,不允许暴露具体的推送 SDK 的 API 到其他模块中,必须采用统一的封装。 \ No newline at end of file diff --git a/lib_push/build.gradle b/lib_push/build.gradle new file mode 100644 index 0000000..304c337 --- /dev/null +++ b/lib_push/build.gradle @@ -0,0 +1,26 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion rootProject.compileSdkVersion + buildToolsVersion rootProject.buildToolsVersion + + defaultConfig { + minSdkVersion rootProject.minSdkVersion + targetSdkVersion rootProject.targetSdkVersion + versionCode 1 + versionName "1.0" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + compileOnly androidLibraries.androidAnnotations + implementation 'cn.jiguang.sdk:jpush:3.1.6' + implementation 'cn.jiguang.sdk:jcore:1.2.5' + implementation thirdLibraries.timber +} diff --git a/lib_push/src/main/AndroidManifest.xml b/lib_push/src/main/AndroidManifest.xml new file mode 100644 index 0000000..43f0d7b --- /dev/null +++ b/lib_push/src/main/AndroidManifest.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib_push/src/main/java/com/android/sdk/push/MessageHandler.java b/lib_push/src/main/java/com/android/sdk/push/MessageHandler.java new file mode 100644 index 0000000..bde64cc --- /dev/null +++ b/lib_push/src/main/java/com/android/sdk/push/MessageHandler.java @@ -0,0 +1,27 @@ +package com.android.sdk.push; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-03-09 11:31 + */ +public interface MessageHandler { + + /** + * 处理透传消息 + * + * @param pushMessage 消息 + */ + void onDirectMessageArrived(PushMessage pushMessage); + + /** + * 通知栏消息被点击 + */ + void handleOnNotificationMessageClicked(PushMessage pushMessage); + + /** + * 通知栏消息到达 + */ + void onNotificationMessageArrived(PushMessage pushMessage); + +} diff --git a/lib_push/src/main/java/com/android/sdk/push/Push.java b/lib_push/src/main/java/com/android/sdk/push/Push.java new file mode 100644 index 0000000..3ae56cb --- /dev/null +++ b/lib_push/src/main/java/com/android/sdk/push/Push.java @@ -0,0 +1,39 @@ +package com.android.sdk.push; + +import android.app.Activity; +import android.support.annotation.NonNull; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-03-09 11:31 + */ +public interface Push { + + void registerPush(@NonNull final PushCallBack pushCallBack); + + void setAlias(String alias); + + void clearAlias(); + + void setTag(String tag); + + void addTag(String tag); + + void deleteTag(String tag); + + void clearTag(); + + void enablePush(); + + void disablePush(); + + void setMessageHandler(MessageHandler messageHandler); + + MessageHandler getMessageHandler(); + + void onActivityCreate(Activity activity); + + void setChannel(String channel); + +} diff --git a/lib_push/src/main/java/com/android/sdk/push/PushCallBack.java b/lib_push/src/main/java/com/android/sdk/push/PushCallBack.java new file mode 100644 index 0000000..be9836f --- /dev/null +++ b/lib_push/src/main/java/com/android/sdk/push/PushCallBack.java @@ -0,0 +1,7 @@ +package com.android.sdk.push; + + +public interface PushCallBack { + void onRegisterPushSuccess(String registrationID); + void onRegisterPushFail(); +} diff --git a/lib_push/src/main/java/com/android/sdk/push/PushContext.java b/lib_push/src/main/java/com/android/sdk/push/PushContext.java new file mode 100644 index 0000000..af9aa72 --- /dev/null +++ b/lib_push/src/main/java/com/android/sdk/push/PushContext.java @@ -0,0 +1,53 @@ +package com.android.sdk.push; + +import android.app.Application; +import android.text.TextUtils; + +import com.android.sdk.push.jpush.JPush; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-03-03 16:22 + */ +public class PushContext { + + private static Application mApplication; + private static boolean isDebug; + private static Push sPush; + + private static String APP_ID; + private static String APP_KEY; + + public static void configPush(String appKey, String appId) { + APP_KEY = appKey; + APP_ID = appId; + } + + public static boolean isPushConfigured() { + return !TextUtils.isEmpty(APP_ID) && !TextUtils.isEmpty(APP_KEY); + } + + public static void init(Application application, boolean debug) { + isDebug = debug; + mApplication = application; + initPush(); + } + + private static void initPush() { + sPush = new JPush(mApplication); + } + + public static Push getPush() { + return sPush; + } + + public static boolean isDebug() { + return isDebug; + } + + public static Application getApplication() { + return mApplication; + } + +} diff --git a/lib_push/src/main/java/com/android/sdk/push/PushMessage.java b/lib_push/src/main/java/com/android/sdk/push/PushMessage.java new file mode 100644 index 0000000..3214be4 --- /dev/null +++ b/lib_push/src/main/java/com/android/sdk/push/PushMessage.java @@ -0,0 +1,76 @@ +package com.android.sdk.push; + + +import android.support.annotation.NonNull; + +public class PushMessage { + + private int messageId; + private int notificationId; + private String alertType; + private String content; + private String extra; + private String title; + + public int getNotificationId() { + return notificationId; + } + + public void setNotificationId(int notificationId) { + this.notificationId = notificationId; + } + + public String getAlertType() { + return alertType; + } + + public void setAlertType(String alertType) { + this.alertType = alertType; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getExtra() { + return extra; + } + + public void setExtra(String extra) { + this.extra = extra; + } + + public int getMessageId() { + return messageId; + } + + public void setMessageId(int messageId) { + this.messageId = messageId; + } + + @NonNull + @Override + public String toString() { + return "PushMessage{" + + "messageiId=" + messageId + + ", notificationId=" + notificationId + + ", alertType=" + alertType + + ", content='" + content + '\'' + + ", extra='" + extra + '\'' + + ", title='" + title + '\'' + + '}'; + } + +} diff --git a/lib_push/src/main/java/com/android/sdk/push/Utils.java b/lib_push/src/main/java/com/android/sdk/push/Utils.java new file mode 100644 index 0000000..f5aa5ec --- /dev/null +++ b/lib_push/src/main/java/com/android/sdk/push/Utils.java @@ -0,0 +1,25 @@ +package com.android.sdk.push; + +import android.content.Context; +import android.content.SharedPreferences; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-02-26 11:36 + */ +public class Utils { + + private static final String PUSH_SP_NAME = "push_sp_name"; + + public static void savePushId(String key, String id) { + SharedPreferences sharedPreferences = PushContext.getApplication().getSharedPreferences(PUSH_SP_NAME, Context.MODE_PRIVATE); + sharedPreferences.edit().putString(key, id).apply(); + } + + public static String getPushId(String key) { + SharedPreferences sharedPreferences = PushContext.getApplication().getSharedPreferences(PUSH_SP_NAME, Context.MODE_PRIVATE); + return sharedPreferences.getString(key, ""); + } + +} diff --git a/lib_push/src/main/java/com/android/sdk/push/exception/PushUnConfigException.java b/lib_push/src/main/java/com/android/sdk/push/exception/PushUnConfigException.java new file mode 100644 index 0000000..8707836 --- /dev/null +++ b/lib_push/src/main/java/com/android/sdk/push/exception/PushUnConfigException.java @@ -0,0 +1,16 @@ +package com.android.sdk.push.exception; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-05-18 12:00 + */ +public class PushUnConfigException extends RuntimeException { + + private static final String MESSAGE = "you have not deployed the %s id"; + + public PushUnConfigException(String message) { + super(String.format(MESSAGE, message)); + } + +} diff --git a/lib_push/src/main/java/com/android/sdk/push/jpush/JPush.java b/lib_push/src/main/java/com/android/sdk/push/jpush/JPush.java new file mode 100644 index 0000000..1cae4a4 --- /dev/null +++ b/lib_push/src/main/java/com/android/sdk/push/jpush/JPush.java @@ -0,0 +1,124 @@ +package com.android.sdk.push.jpush; + +import android.app.Activity; +import android.app.Application; +import android.content.Context; +import android.support.annotation.NonNull; +import android.text.TextUtils; + +import com.android.sdk.push.MessageHandler; +import com.android.sdk.push.Push; +import com.android.sdk.push.PushCallBack; +import com.android.sdk.push.PushContext; +import com.android.sdk.push.Utils; + +import cn.jpush.android.api.JPushInterface; +import timber.log.Timber; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-01-26 17:44 + */ +public class JPush implements Push { + + private Context mContext; + private MessageHandler mMessageHandler; + private PushCallBack mPushCallBack; + + static final String JPUSH_ID_KET = "jpush_id_key"; + + public JPush(Application mApplication) { + mContext = mApplication.getApplicationContext(); + } + + @Override + public void registerPush(@NonNull PushCallBack pushCallBack) { + this.mPushCallBack = pushCallBack; + //给 JPushReceiver 设置 JPush + JPushReceiver.sJPush = this; + // 设置开启日志,发布时请关闭日志 + JPushInterface.setDebugMode(PushContext.isDebug()); + // 初始化极光推送服务 + JPushInterface.init(mContext); + String registrationID = getRegistrationID(); + Timber.d("jpush registrationID = " + registrationID); + boolean isRegistrationSuccess = !TextUtils.isEmpty(registrationID); + if (isRegistrationSuccess) { + this.mPushCallBack.onRegisterPushSuccess(registrationID); + } + } + + private String getRegistrationID() { + String registrationID = JPushInterface.getRegistrationID(mContext); + if (TextUtils.isEmpty(registrationID)) { + registrationID = Utils.getPushId(JPUSH_ID_KET); + } + return registrationID; + } + + @Override + public void setAlias(String alias) { + JPushUtils.setAlias(mContext, alias); + } + + @Override + public void clearAlias() { + JPushUtils.clearAlias(mContext); + } + + @Override + public void setTag(String tag) { + JPushUtils.setTag(mContext, tag); + } + + @Override + public void addTag(String tag) { + JPushUtils.deleteTag(mContext, tag); + } + + @Override + public void deleteTag(String tag) { + JPushUtils.setTag(mContext, tag); + } + + @Override + public void clearTag() { + JPushUtils.clearTags(mContext); + } + + @Override + public void enablePush() { + JPushInterface.resumePush(mContext); + } + + @Override + public void disablePush() { + JPushInterface.stopPush(mContext); + } + + @Override + public void setMessageHandler(MessageHandler messageHandler) { + this.mMessageHandler = messageHandler; + } + + @Override + public MessageHandler getMessageHandler() { + return mMessageHandler; + } + + PushCallBack getPushCallBack() { + return mPushCallBack; + } + + @Override + public void setChannel(String channel) { + JPushInterface.setChannel(mContext, channel); + } + + @Override + public void onActivityCreate(Activity activity) { + // no op + } + +} diff --git a/lib_push/src/main/java/com/android/sdk/push/jpush/JPushReceiver.java b/lib_push/src/main/java/com/android/sdk/push/jpush/JPushReceiver.java new file mode 100644 index 0000000..65bc4cd --- /dev/null +++ b/lib_push/src/main/java/com/android/sdk/push/jpush/JPushReceiver.java @@ -0,0 +1,128 @@ +package com.android.sdk.push.jpush; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.NonNull; + +import com.android.sdk.push.PushContext; +import com.android.sdk.push.PushMessage; +import com.android.sdk.push.Utils; + +import cn.jpush.android.api.JPushInterface; +import timber.log.Timber; + +import static com.android.sdk.push.jpush.JPush.JPUSH_ID_KET; + +/** + * 极光推送-消息接收广播 + * + * @author Wangwb + * Email: 253123123@qq.com + * Date : 2019-01-28 11:21 + */ +public class JPushReceiver extends BroadcastReceiver { + + static JPush sJPush; + + @Override + public void onReceive(Context context, Intent intent) { + try { + processMessage(intent); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void processMessage(Intent intent) { + Bundle bundle = intent.getExtras(); + + if (PushContext.isDebug()) { + Timber.d("[JPushReceiver] onReceive - " + intent.getAction() + ", extras: " + JPushUtils.printBundle(bundle)); + } + + if (bundle == null) { + return; + } + + if (JPushInterface.ACTION_REGISTRATION_ID.equals(intent.getAction())) { + + processRegisterSuccess(bundle); + + } else if (JPushInterface.ACTION_MESSAGE_RECEIVED.equals(intent.getAction())) { + + processMessageReceived(bundle); + + } else if (JPushInterface.ACTION_NOTIFICATION_RECEIVED.equals(intent.getAction())) { + + processNotificationMessageReceived(bundle); + + } else if (JPushInterface.ACTION_NOTIFICATION_OPENED.equals(intent.getAction())) { + + //目前没有配置这个 action,不会接收此类消息 + processNotificationMessageClicked(bundle); + + } else if (JPushInterface.ACTION_CONNECTION_CHANGE.equals(intent.getAction())) { + + Timber.w("[JPushReceiver]" + intent.getAction() + " connected state change to " + intent.getBooleanExtra(JPushInterface.EXTRA_CONNECTION_CHANGE, false)); + + } else { + Timber.d("[JPushReceiver] Unhandled intent - " + intent.getAction()); + } + + } + + private void processNotificationMessageClicked(Bundle bundle) { + Timber.d("[JPushReceiver] 接收到推送下来的通知被点击了"); + PushMessage pushMessage = extractMessage(bundle); + sJPush.getMessageHandler().handleOnNotificationMessageClicked(pushMessage); + } + + // 在这里可以做些统计,或者做些其他工作 + private void processNotificationMessageReceived(Bundle bundle) { + Timber.d("[JPushReceiver] 接收到推送下来的通知"); + PushMessage pushMessage = extractMessage(bundle); + sJPush.getMessageHandler().onNotificationMessageArrived(pushMessage); + } + + @NonNull + private PushMessage extractMessage(Bundle bundle) { + int notificationId = bundle.getInt(JPushInterface.EXTRA_NOTIFICATION_ID); + String alertType = bundle.getString(JPushInterface.EXTRA_ALERT_TYPE); + String alert = bundle.getString(JPushInterface.EXTRA_ALERT); + String title = bundle.getString(JPushInterface.EXTRA_NOTIFICATION_TITLE); + String extras = bundle.getString(JPushInterface.EXTRA_EXTRA); + + PushMessage pushMessage = new PushMessage(); + pushMessage.setTitle(title); + pushMessage.setContent(alert); + pushMessage.setExtra(extras); + pushMessage.setNotificationId(notificationId); + pushMessage.setAlertType(alertType); + return pushMessage; + } + + private void processRegisterSuccess(Bundle bundle) { + String regId = bundle.getString(JPushInterface.EXTRA_REGISTRATION_ID); + Timber.d("[JPushReceiver] JPush 用户注册成功,接收Registration Id : " + regId); + //you can send the Registration Id to your server... + Utils.savePushId(JPUSH_ID_KET, regId); + sJPush.getPushCallBack().onRegisterPushSuccess(regId); + } + + // 自定义消息不会展示在通知栏,完全要开发者写代码去处理 + private void processMessageReceived(Bundle bundle) { + Timber.d("[JPushReceiver] 接收到推送下来的透传消息: " + bundle.getString(JPushInterface.EXTRA_MESSAGE)); + + String message = bundle.getString(JPushInterface.EXTRA_MESSAGE); + String extras = bundle.getString(JPushInterface.EXTRA_EXTRA); + + PushMessage pushMessage = new PushMessage(); + pushMessage.setContent(message); + pushMessage.setExtra(extras); + + sJPush.getMessageHandler().onDirectMessageArrived(pushMessage); + } + +} diff --git a/lib_push/src/main/java/com/android/sdk/push/jpush/JPushUtils.java b/lib_push/src/main/java/com/android/sdk/push/jpush/JPushUtils.java new file mode 100644 index 0000000..ba5974a --- /dev/null +++ b/lib_push/src/main/java/com/android/sdk/push/jpush/JPushUtils.java @@ -0,0 +1,195 @@ +package com.android.sdk.push.jpush; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.text.TextUtils; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import cn.jpush.android.api.JPushInterface; +import timber.log.Timber; + +import static com.android.sdk.push.jpush.TagAliasOperatorHelper.ACTION_ADD; +import static com.android.sdk.push.jpush.TagAliasOperatorHelper.ACTION_CLEAN; +import static com.android.sdk.push.jpush.TagAliasOperatorHelper.ACTION_DELETE; +import static com.android.sdk.push.jpush.TagAliasOperatorHelper.ACTION_SET; + +/** + * @author Wangwb + * Email: 253123123@qq.com + * Date : 2019-01-28 11:29 + */ +class JPushUtils { + + private static final String TAG = "JPushReceiver"; + + /** + * 打印 Bundle Extras + */ + static String printBundle(Bundle bundle) { + if (bundle == null) { + return "null"; + } + + StringBuilder sb = new StringBuilder(); + + for (String key : bundle.keySet()) { + switch (key) { + case JPushInterface.EXTRA_NOTIFICATION_ID: + sb.append("\nkey:").append(key).append(", value:").append(bundle.getInt(key)); + break; + case JPushInterface.EXTRA_CONNECTION_CHANGE: + sb.append("\nkey:").append(key).append(", value:").append(bundle.getBoolean(key)); + break; + case JPushInterface.EXTRA_EXTRA: + if (TextUtils.isEmpty(bundle.getString(JPushInterface.EXTRA_EXTRA))) { + Timber.i(TAG, "This message has no Extra data"); + continue; + } + + try { + JSONObject json = new JSONObject(bundle.getString(JPushInterface.EXTRA_EXTRA)); + Iterator it = json.keys(); + + while (it.hasNext()) { + String myKey = it.next(); + sb.append("\nkey:").append(key).append(", value: [").append(myKey).append(" - ").append(json.optString(myKey)).append("]"); + } + } catch (JSONException e) { + Timber.e(TAG, "Get message extra JSON error!"); + } + break; + default: + sb.append("\nkey:").append(key).append(", value:").append(bundle.get(key)); + break; + } + } + return sb.toString(); + } + + /** + * 获取极光推送AppKey + */ + @SuppressWarnings("unused") + static String appKey(Context context) { + Bundle metaData = null; + String appKey = null; + try { + ApplicationInfo ai = context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA); + if (null != ai) + metaData = ai.metaData; + if (null != metaData) { + appKey = metaData.getString("JPUSH_APPKEY"); + Timber.d("JPush AppKey: " + appKey + ", pkg: " + context.getPackageName()); + if ((null == appKey) || appKey.length() != 24) { + appKey = null; + } + } + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + return appKey; + } + + /** + * 校验Tag Alias 只能是数字,英文字母和中文 + */ + @SuppressWarnings("all") + private static boolean isValidTagAndAlias(String s) { + Pattern p = Pattern.compile("^[\u4E00-\u9FA50-9a-zA-Z_!@#$&*+=.|]+$"); + Matcher m = p.matcher(s); + return m.matches(); + } + + static boolean isConnected(Context context) { + ConnectivityManager conn = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + @SuppressLint("MissingPermission") + NetworkInfo info = conn.getActiveNetworkInfo(); + return (info == null || !info.isConnected()); + } + + /** + * 获取设备ID + */ + @SuppressWarnings("unused") + static String getDeviceId(Context context) { + return JPushInterface.getUdid(context); + } + + static void setTag(Context context, String tag) { + if (!JPushUtils.isValidTagAndAlias(tag)) { + Timber.e("Tag Alias 命名不符合规范"); + return; + } + + TagAliasOperatorHelper.TagAliasBean tagAliasBean = createTagAliasBean(tag); + tagAliasBean.action = ACTION_SET; + + TagAliasOperatorHelper.getInstance().handleAction(context, ++TagAliasOperatorHelper.sequence, tagAliasBean); + } + + static void deleteTag(Context context, String tag) { + if (!JPushUtils.isValidTagAndAlias(tag)) { + Timber.e("Tag Alias 命名不符合规范"); + return; + } + + TagAliasOperatorHelper.TagAliasBean tagAliasBean = createTagAliasBean(tag); + tagAliasBean.action = ACTION_DELETE; + + TagAliasOperatorHelper.getInstance().handleAction(context, ++TagAliasOperatorHelper.sequence, tagAliasBean); + } + + static void clearTags(Context context) { + TagAliasOperatorHelper.TagAliasBean tagAliasBean = createTagAliasBean(null); + tagAliasBean.action = ACTION_CLEAN; + + TagAliasOperatorHelper.getInstance().handleAction(context, ++TagAliasOperatorHelper.sequence, tagAliasBean); + } + + @NonNull + private static TagAliasOperatorHelper.TagAliasBean createTagAliasBean(String tag) { + TagAliasOperatorHelper.TagAliasBean tagAliasBean = new TagAliasOperatorHelper.TagAliasBean(); + tagAliasBean.isAliasAction = false; + + if (!TextUtils.isEmpty(tag)) { + LinkedHashSet tags = new LinkedHashSet<>(); + tags.add(tag); + tagAliasBean.tags = tags; + } + + return tagAliasBean; + } + + static void setAlias(Context context, String alias) { + TagAliasOperatorHelper.TagAliasBean tagAliasBean = new TagAliasOperatorHelper.TagAliasBean(); + + tagAliasBean.action = ACTION_ADD; + tagAliasBean.isAliasAction = true; + tagAliasBean.alias = alias; + + TagAliasOperatorHelper.getInstance().handleAction(context, ++TagAliasOperatorHelper.sequence, tagAliasBean); + } + + static void clearAlias(Context context) { + TagAliasOperatorHelper.TagAliasBean tagAliasBean = new TagAliasOperatorHelper.TagAliasBean(); + + tagAliasBean.action = ACTION_DELETE; + tagAliasBean.isAliasAction = true; + + TagAliasOperatorHelper.getInstance().handleAction(context, ++TagAliasOperatorHelper.sequence, tagAliasBean); + } + +} \ No newline at end of file diff --git a/lib_push/src/main/java/com/android/sdk/push/jpush/TagAliasJPushMessageReceiver.java b/lib_push/src/main/java/com/android/sdk/push/jpush/TagAliasJPushMessageReceiver.java new file mode 100644 index 0000000..92b8ea1 --- /dev/null +++ b/lib_push/src/main/java/com/android/sdk/push/jpush/TagAliasJPushMessageReceiver.java @@ -0,0 +1,37 @@ +package com.android.sdk.push.jpush; + +import android.content.Context; + +import cn.jpush.android.api.JPushMessage; +import cn.jpush.android.service.JPushMessageReceiver; + +/** + * 自定义 JPush message 接收器,包括操作 tag/alias 的结果返回(仅仅包含 tag/alias 新接口部分) + */ +public class TagAliasJPushMessageReceiver extends JPushMessageReceiver { + + @Override + public void onTagOperatorResult(Context context, JPushMessage jPushMessage) { + TagAliasOperatorHelper.getInstance().onTagOperatorResult(context, jPushMessage); + super.onTagOperatorResult(context, jPushMessage); + } + + @Override + public void onCheckTagOperatorResult(Context context, JPushMessage jPushMessage) { + TagAliasOperatorHelper.getInstance().onCheckTagOperatorResult(context, jPushMessage); + super.onCheckTagOperatorResult(context, jPushMessage); + } + + @Override + public void onAliasOperatorResult(Context context, JPushMessage jPushMessage) { + TagAliasOperatorHelper.getInstance().onAliasOperatorResult(context, jPushMessage); + super.onAliasOperatorResult(context, jPushMessage); + } + + @Override + public void onMobileNumberOperatorResult(Context context, JPushMessage jPushMessage) { + TagAliasOperatorHelper.getInstance().onMobileNumberOperatorResult(context, jPushMessage); + super.onMobileNumberOperatorResult(context, jPushMessage); + } + +} diff --git a/lib_push/src/main/java/com/android/sdk/push/jpush/TagAliasOperatorHelper.java b/lib_push/src/main/java/com/android/sdk/push/jpush/TagAliasOperatorHelper.java new file mode 100644 index 0000000..9927ca3 --- /dev/null +++ b/lib_push/src/main/java/com/android/sdk/push/jpush/TagAliasOperatorHelper.java @@ -0,0 +1,412 @@ +package com.android.sdk.push.jpush; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Handler; +import android.os.Message; +import android.support.annotation.NonNull; +import android.util.SparseArray; + +import java.util.Locale; +import java.util.Objects; +import java.util.Set; + +import cn.jpush.android.api.JPushInterface; +import cn.jpush.android.api.JPushMessage; +import timber.log.Timber; + +/** + * 处理 tag alias 相关的逻辑 + */ +class TagAliasOperatorHelper { + + private static final String TAG = "JIGUANG-TagAliasHelper"; + + static int sequence = 1; + + /** + * 增加 + */ + static final int ACTION_ADD = 1; + /** + * 覆盖 + */ + static final int ACTION_SET = 2; + /** + * 删除部分 + */ + static final int ACTION_DELETE = 3; + /** + * 删除所有 + */ + static final int ACTION_CLEAN = 4; + /** + * 查询 + */ + static final int ACTION_GET = 5; + /** + * 检查 + */ + static final int ACTION_CHECK = 6; + + private static final int DELAY_SEND_ACTION = 1; + + private static final int DELAY_SET_MOBILE_NUMBER_ACTION = 2; + + private Context context; + + @SuppressLint("StaticFieldLeak") + private static TagAliasOperatorHelper mInstance; + + private TagAliasOperatorHelper() { + } + + public static TagAliasOperatorHelper getInstance() { + if (mInstance == null) { + synchronized (TagAliasOperatorHelper.class) { + if (mInstance == null) { + mInstance = new TagAliasOperatorHelper(); + } + } + } + return mInstance; + } + + public void init(Context context) { + if (context != null) { + this.context = context.getApplicationContext(); + } + } + + private SparseArray setActionCache = new SparseArray<>(); + + public Object get(int sequence) { + return setActionCache.get(sequence); + } + + public Object remove(int sequence) { + return setActionCache.get(sequence); + } + + private void put(int sequence, Object tagAliasBean) { + setActionCache.put(sequence, tagAliasBean); + } + + @SuppressLint("HandlerLeak") + private Handler delaySendHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case DELAY_SEND_ACTION: + if (msg.obj instanceof TagAliasBean) { + Timber.w(TAG, "on delay time"); + sequence++; + TagAliasBean tagAliasBean = (TagAliasBean) msg.obj; + setActionCache.put(sequence, tagAliasBean); + if (context != null) { + handleAction(context, sequence, tagAliasBean); + } else { + Timber.w(TAG, "#unexcepted - context was null"); + } + } else { + Timber.w(TAG, "#unexcepted - msg obj was incorrect"); + } + break; + case DELAY_SET_MOBILE_NUMBER_ACTION: + if (msg.obj instanceof String) { + Timber.w(TAG, "retry set mobile number"); + sequence++; + String mobileNumber = (String) msg.obj; + setActionCache.put(sequence, mobileNumber); + if (context != null) { + handleAction(context, sequence, mobileNumber); + } else { + Timber.w(TAG, "#unexcepted - context was null"); + } + } else { + Timber.w(TAG, "#unexcepted - msg obj was incorrect"); + } + break; + } + } + }; + + private void handleAction(Context context, int sequence, String mobileNumber) { + put(sequence, mobileNumber); + Timber.d(TAG, "sequence:" + sequence + ",mobileNumber:" + mobileNumber); + JPushInterface.setMobileNumber(context, sequence, mobileNumber); + } + + /** + * 处理设置tag + */ + void handleAction(Context context, int sequence, TagAliasBean tagAliasBean) { + init(context); + if (tagAliasBean == null) { + Timber.w(TAG, "tagAliasBean was null"); + return; + } + put(sequence, tagAliasBean); + if (tagAliasBean.isAliasAction) { + switch (tagAliasBean.action) { + case ACTION_GET: + JPushInterface.getAlias(context, sequence); + break; + case ACTION_DELETE: + JPushInterface.deleteAlias(context, sequence); + break; + case ACTION_SET: + JPushInterface.setAlias(context, sequence, tagAliasBean.alias); + break; + default: + Timber.w(TAG, "unsupport alias action type"); + } + } else { + switch (tagAliasBean.action) { + case ACTION_ADD: + JPushInterface.addTags(context, sequence, tagAliasBean.tags); + break; + case ACTION_SET: + JPushInterface.setTags(context, sequence, tagAliasBean.tags); + break; + case ACTION_DELETE: + JPushInterface.deleteTags(context, sequence, tagAliasBean.tags); + break; + case ACTION_CHECK: + //一次只能check一个tag + String tag = (String) Objects.requireNonNull(tagAliasBean.tags.toArray())[0]; + JPushInterface.checkTagBindState(context, sequence, tag); + break; + case ACTION_GET: + JPushInterface.getAllTags(context, sequence); + break; + case ACTION_CLEAN: + JPushInterface.cleanTags(context, sequence); + break; + default: + Timber.w(TAG, "unsupport tag action type"); + } + } + } + + private boolean RetryActionIfNeeded(int errorCode, TagAliasBean tagAliasBean) { + if (JPushUtils.isConnected(context)) { + Timber.w(TAG, "no network"); + return true; + } + //返回的错误码为6002 超时,6014 服务器繁忙,都建议延迟重试 + if (errorCode == 6002 || errorCode == 6014) { + Timber.d(TAG, "need retry"); + if (tagAliasBean != null) { + Message message = new Message(); + message.what = DELAY_SEND_ACTION; + message.obj = tagAliasBean; + delaySendHandler.sendMessageDelayed(message, 1000 * 60); + String logs = getRetryStr(tagAliasBean.isAliasAction, tagAliasBean.action, errorCode); + Timber.w(TAG, logs); + return false; + } + } + return true; + } + + private boolean RetrySetMObileNumberActionIfNeeded(int errorCode, String mobileNumber) { + if (JPushUtils.isConnected(context)) { + Timber.w(TAG, "no network"); + return false; + } + //返回的错误码为6002 超时,6024 服务器内部错误,建议稍后重试 + if (errorCode == 6002 || errorCode == 6024) { + Timber.d(TAG, "need retry"); + Message message = new Message(); + message.what = DELAY_SET_MOBILE_NUMBER_ACTION; + message.obj = mobileNumber; + delaySendHandler.sendMessageDelayed(message, 1000 * 60); + String str = "Failed to set mobile number due to %s. Try again after 60s."; + str = String.format(Locale.ENGLISH, str, + (errorCode == 6002 ? "timeout" : "server internal error”")); + Timber.w(TAG, str); + return true; + } + return false; + } + + private String getRetryStr(boolean isAliasAction, int actionType, int errorCode) { + String str = "Failed to %s %s due to %s. Try again after 60s."; + str = String.format(Locale.ENGLISH, str, getActionStr(actionType), + (isAliasAction ? "alias" : " tags"), (errorCode == 6002 ? "timeout" : "server too busy")); + return str; + } + + private String getActionStr(int actionType) { + switch (actionType) { + case ACTION_ADD: + return "add"; + case ACTION_SET: + return "set"; + case ACTION_DELETE: + return "delete"; + case ACTION_GET: + return "get"; + case ACTION_CLEAN: + return "clean"; + case ACTION_CHECK: + return "check"; + } + return "unkonw operation"; + } + + void onTagOperatorResult(Context context, JPushMessage jPushMessage) { + int sequence = jPushMessage.getSequence(); + Timber.i(TAG, "action - onTagOperatorResult, sequence:" + sequence + ",tags:" + jPushMessage.getTags()); + if (jPushMessage.getTags() != null) { + Timber.i(TAG, "tags size:" + jPushMessage.getTags().size()); + } + + init(context); + + // 根据sequence从之前操作缓存中获取缓存记录 + TagAliasBean tagAliasBean = (TagAliasBean) setActionCache.get(sequence); + if (tagAliasBean == null) { + Timber.w(TAG, "获取缓存记录失败"); + return; + } + if (jPushMessage.getErrorCode() == 0) { + Timber.i(TAG, "action - modify tag Success,sequence:" + sequence); + setActionCache.remove(sequence); + String logs = getActionStr(tagAliasBean.action) + " tags success"; + Timber.w(TAG, logs); + // 极光推送已经初始化失败 + } else { + String logs = "Failed to " + getActionStr(tagAliasBean.action) + " tags"; + if (jPushMessage.getErrorCode() == 6018) { + //tag数量超过限制,需要先清除一部分再 add + logs += ", tags is exceed limit need to clean"; + } + logs += ", errorCode:" + jPushMessage.getErrorCode(); + Timber.w(TAG, logs); + if (RetryActionIfNeeded(jPushMessage.getErrorCode(), tagAliasBean)) { + Timber.w(TAG, logs); + } + // 极光推送已经初始化成功 + } + } + + void onCheckTagOperatorResult(Context context, JPushMessage jPushMessage) { + int sequence = jPushMessage.getSequence(); + Timber.i(TAG, "action - onCheckTagOperatorResult, sequence:" + + sequence + + ",checktag:" + + jPushMessage.getCheckTag()); + + init(context); + + // 根据sequence从之前操作缓存中获取缓存记录 + TagAliasBean tagAliasBean = (TagAliasBean) setActionCache.get(sequence); + if (tagAliasBean == null) { + Timber.w(TAG, "获取缓存记录失败"); + return; + } + if (jPushMessage.getErrorCode() == 0) { + Timber.i(TAG, "tagBean:" + tagAliasBean); + setActionCache.remove(sequence); + String logs = getActionStr(tagAliasBean.action) + + " tag " + + jPushMessage.getCheckTag() + + " bind state success,state:" + + jPushMessage.getTagCheckStateResult(); + Timber.w(TAG, logs); + } else { + String logs = "Failed to " + + getActionStr(tagAliasBean.action) + + " tags, errorCode:" + + jPushMessage.getErrorCode(); + Timber.w(TAG, logs); + if (RetryActionIfNeeded(jPushMessage.getErrorCode(), tagAliasBean)) { + Timber.w(TAG, logs); + } + } + } + + void onAliasOperatorResult(Context context, JPushMessage jPushMessage) { + int sequence = jPushMessage.getSequence(); + Timber.i(TAG, "action - onAliasOperatorResult, sequence:" + + sequence + + ",alias:" + + jPushMessage.getAlias()); + + init(context); + + // 根据sequence从之前操作缓存中获取缓存记录 + TagAliasBean tagAliasBean = (TagAliasBean) setActionCache.get(sequence); + if (tagAliasBean == null) { + Timber.w(TAG, "获取缓存记录失败"); + return; + } + if (jPushMessage.getErrorCode() == 0) { + Timber.i(TAG, "action - modify alias Success,sequence:" + sequence); + setActionCache.remove(sequence); + String logs = getActionStr(tagAliasBean.action) + " alias success"; + Timber.w(TAG, logs); + } else { + String logs = "Failed to " + + getActionStr(tagAliasBean.action) + + " alias, errorCode:" + + jPushMessage.getErrorCode(); + Timber.w(TAG, logs); + if (RetryActionIfNeeded(jPushMessage.getErrorCode(), tagAliasBean)) { + Timber.w(TAG, logs); + } + } + } + + /** + * 设置手机号码回调 + */ + void onMobileNumberOperatorResult(Context context, JPushMessage jPushMessage) { + int sequence = jPushMessage.getSequence(); + Timber.i(TAG, "action - onMobileNumberOperatorResult, sequence:" + + sequence + + ",mobileNumber:" + + jPushMessage.getMobileNumber()); + + init(context); + + if (jPushMessage.getErrorCode() == 0) { + Timber.i(TAG, "action - set mobile number Success,sequence:" + sequence); + setActionCache.remove(sequence); + } else { + String logs = "Failed to set mobile number, errorCode:" + jPushMessage.getErrorCode(); + Timber.e(TAG, logs); + if (!RetrySetMObileNumberActionIfNeeded(jPushMessage.getErrorCode(), + jPushMessage.getMobileNumber())) { + Timber.w(TAG, logs); + } + } + } + + public static class TagAliasBean { + + int action; + Set tags; + String alias; + boolean isAliasAction; + + @NonNull + @Override + public String toString() { + return "TagAliasBean{" + + "action=" + + action + + ", tags=" + + tags + + ", alias='" + + alias + + '\'' + + ", isAliasAction=" + + isAliasAction + + '}'; + } + } + +} diff --git a/lib_push/src/main/java/com/android/sdk/push/mipush/MiPush.java b/lib_push/src/main/java/com/android/sdk/push/mipush/MiPush.java new file mode 100644 index 0000000..b20ea77 --- /dev/null +++ b/lib_push/src/main/java/com/android/sdk/push/mipush/MiPush.java @@ -0,0 +1,7 @@ +package com.android.sdk.push.mipush; + +import com.android.sdk.push.Push; + + +public abstract class MiPush implements Push { +} diff --git a/lib_qrcode/.gitignore b/lib_qrcode/.gitignore new file mode 100644 index 0000000..c06fb85 --- /dev/null +++ b/lib_qrcode/.gitignore @@ -0,0 +1,22 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +*.iml +.idea/ +.gradle +/local.properties +.DS_Store +/build +/captures +*.apk +*.ap_ +*.dex +*.class +bin/ +gen/ +local.properties \ No newline at end of file diff --git a/lib_qrcode/README.md b/lib_qrcode/README.md new file mode 100644 index 0000000..3ee2da5 --- /dev/null +++ b/lib_qrcode/README.md @@ -0,0 +1,3 @@ +## 二维码扫描库 + +修改自 [BGAQRCode-Android](https://github.com/bingoogolapple/BGAQRCode-Android) \ No newline at end of file diff --git a/lib_qrcode/build.gradle b/lib_qrcode/build.gradle new file mode 100644 index 0000000..4163ab4 --- /dev/null +++ b/lib_qrcode/build.gradle @@ -0,0 +1,40 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + + compileSdkVersion rootProject.compileSdkVersion + buildToolsVersion rootProject.buildToolsVersion + + defaultConfig { + + minSdkVersion rootProject.minSdkVersion + targetSdkVersion rootProject.targetSdkVersion + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + lintOptions { + abortOnError false + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + +} + +dependencies { + implementation 'com.google.zxing:core:3.3.3' + api fileTree(dir: 'libs', include: ['*.jar', '*.aar']) + /*implementation uiLibraries.fotoapparat*/ + implementation kotlinLibraries.kotlinStdlib +} \ No newline at end of file diff --git a/lib_qrcode/libs/fotoapparat.aar b/lib_qrcode/libs/fotoapparat.aar new file mode 100644 index 0000000..17f988c Binary files /dev/null and b/lib_qrcode/libs/fotoapparat.aar differ diff --git a/lib_qrcode/proguard-rules.pro b/lib_qrcode/proguard-rules.pro new file mode 100644 index 0000000..268073a --- /dev/null +++ b/lib_qrcode/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in D:\DevTools\SDK/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/lib_qrcode/src/main/AndroidManifest.xml b/lib_qrcode/src/main/AndroidManifest.xml new file mode 100644 index 0000000..570fe1d --- /dev/null +++ b/lib_qrcode/src/main/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/lib_qrcode/src/main/java/com/android/sdk/qrcode/CameraUtils.java b/lib_qrcode/src/main/java/com/android/sdk/qrcode/CameraUtils.java new file mode 100644 index 0000000..58ea035 --- /dev/null +++ b/lib_qrcode/src/main/java/com/android/sdk/qrcode/CameraUtils.java @@ -0,0 +1,131 @@ +package com.android.sdk.qrcode; + +import android.content.Context; +import android.util.DisplayMetrics; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; + +import io.fotoapparat.parameter.Resolution; + +@SuppressWarnings("unused") +final class CameraUtils { + + private static final double MAX_ASPECT_DISTORTION = 0.15;//最大比例偏差 + private static final int MIN_PREVIEW_PIXELS = 480 * 800;//小于此预览尺寸直接移除 + + private static final class SizeComparator implements Comparator { + @Override + public int compare(Resolution lhs, Resolution rhs) { + return -(rhs.width * rhs.height - lhs.width * lhs.height); + } + } + + static Resolution findBestPictureSize(Context context, Collection collection) { + if (collection.isEmpty()) { + return null; + } + DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); + Resolution bestPictureResolution = findBestPictureResolution(new int[]{displayMetrics.heightPixels, displayMetrics.widthPixels}, new ArrayList<>(collection)); + Debug.log("bestPictureResolution:" + bestPictureResolution); + return bestPictureResolution; + } + + static Resolution findBestPreviewSize(Context context, Collection collection) { + if (collection.isEmpty()) { + return null; + } + DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); + Resolution bestPreviewResolution = findBestPreviewResolution(new int[]{displayMetrics.heightPixels, displayMetrics.widthPixels}, new ArrayList<>(collection)); + Debug.log("bestPreviewResolution:" + bestPreviewResolution); + return bestPreviewResolution; + } + + /** + * 找到最好的适配分辨率 + */ + private static Resolution findBestPictureResolution(int[] screenSize, List rawSupportedPicResolutions) { + // 排序 + List sortedSupportedPicResolutions = new ArrayList<>(rawSupportedPicResolutions); + //递增排序,重要 + Collections.sort(sortedSupportedPicResolutions, new SizeComparator()); + // 移除不符合条件的分辨率——高:宽 + float screenAspectRatio = 1.0F * screenSize[0] / screenSize[1]; + Debug.log("screenSize:" + Arrays.toString(screenSize)); + Debug.log("screenAspectRatio:" + screenAspectRatio); + Iterator it = sortedSupportedPicResolutions.iterator(); + + while (it.hasNext()) { + Resolution size = it.next(); + int width = size.width; + int height = size.height; + // 在camera分辨率与屏幕分辨率宽高比不相等的情况下,找出差距最小的一组分辨率 + // 由于camera的分辨率是width>height,我们设置的portrait模式中,width height; + int maybeFlippedWidth = isCandidatePortrait ? height : width; + int maybeFlippedHeight = isCandidatePortrait ? width : height; + float aspectRatio = 1.0F * maybeFlippedHeight / maybeFlippedWidth; + Debug.log("maybeFlippedWidth:" + maybeFlippedWidth + " maybeFlippedHeight:" + maybeFlippedHeight); + Debug.log("aspectRatio:" + aspectRatio); + float distortion = Math.abs(aspectRatio - screenAspectRatio); + if (distortion > MAX_ASPECT_DISTORTION) {//移除不满足比例的分辨率 + it.remove(); + } + } + if (sortedSupportedPicResolutions.isEmpty()) { + return rawSupportedPicResolutions.get(rawSupportedPicResolutions.size() - 1); + } + // 如果没有找到合适的,并且还有候选的像素,对于照片,则取其中最大比例的 + return sortedSupportedPicResolutions.get(sortedSupportedPicResolutions.size() - 1); + + } + + private static Resolution findBestPreviewResolution(int[] screenSize, List rawSupportedSizes) { + // 按照分辨率从大到小排序 + List supportedPreviewResolutions = new ArrayList<>(rawSupportedSizes); + Collections.sort(supportedPreviewResolutions, new SizeComparator()); + // 移除不符合条件的分辨率——高:宽 + double screenAspectRatio = 1.0F * screenSize[0] / screenSize[1]; + + Iterator it = supportedPreviewResolutions.iterator(); + Resolution size; + while (it.hasNext()) { + size = it.next(); + int width = size.width; + int height = size.height; + // 移除低于下限的分辨率 + if (width * height < MIN_PREVIEW_PIXELS) { + it.remove(); + continue; + } + // 在camera分辨率与屏幕分辨率宽高比不相等的情况下,找出差距最小的一组分辨率 + // 由于camera的分辨率是width>height,我们设置的portrait模式中,width height; + int maybeFlippedWidth = isCandidatePortrait ? height : width; + int maybeFlippedHeight = isCandidatePortrait ? width : height; + float aspectRatio = 1.0F * maybeFlippedHeight / maybeFlippedWidth; + double distortion = Math.abs(aspectRatio - screenAspectRatio); + if (distortion > MAX_ASPECT_DISTORTION) {//移除不符合比例的分辨率 + it.remove(); + continue; + } + // 找到与屏幕分辨率完全匹配的预览界面分辨率直接返回 + if (maybeFlippedWidth == screenSize[0] && maybeFlippedHeight == screenSize[1]) { + return size; + } + } + if (supportedPreviewResolutions.isEmpty()) { + return rawSupportedSizes.get(rawSupportedSizes.size() - 1); + } + // 如果没有找到最合适的 + return supportedPreviewResolutions.get(supportedPreviewResolutions.size() - 1); + } + +} diff --git a/lib_qrcode/src/main/java/com/android/sdk/qrcode/Debug.java b/lib_qrcode/src/main/java/com/android/sdk/qrcode/Debug.java new file mode 100644 index 0000000..ef829c9 --- /dev/null +++ b/lib_qrcode/src/main/java/com/android/sdk/qrcode/Debug.java @@ -0,0 +1,25 @@ +package com.android.sdk.qrcode; + +import android.util.Log; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-04-02 12:55 + */ +public class Debug { + + private static final String TAG = "QRCODE"; + private static boolean debug = BuildConfig.DEBUG; + + public static void setDebug(boolean debug) { + Debug.debug = debug; + } + + public static void log(String msg) { + if (debug && msg != null) { + Log.d(TAG, msg); + } + } + +} diff --git a/lib_qrcode/src/main/java/com/android/sdk/qrcode/ProcessDataTask.java b/lib_qrcode/src/main/java/com/android/sdk/qrcode/ProcessDataTask.java new file mode 100644 index 0000000..47532e9 --- /dev/null +++ b/lib_qrcode/src/main/java/com/android/sdk/qrcode/ProcessDataTask.java @@ -0,0 +1,66 @@ +package com.android.sdk.qrcode; + +import android.os.AsyncTask; + +import io.fotoapparat.parameter.Resolution; + + +class ProcessDataTask extends AsyncTask { + + private final Resolution mSize; + private byte[] mData; + private Delegate mDelegate; + + ProcessDataTask(byte[] data, Resolution size, @SuppressWarnings("unused") int rotation, Delegate delegate) { + mData = data; + mSize = size; + mDelegate = delegate; + } + + ProcessDataTask perform() { + executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + return this; + } + + void cancelTask() { + if (getStatus() != Status.FINISHED) { + cancel(true); + } + } + + @Override + protected void onCancelled() { + super.onCancelled(); + mDelegate = null; + } + + //https://stackoverflow.com/questions/16252791/zxing-camera-in-portrait-mode-on-android + @Override + protected String doInBackground(Void... params) { + int width = mSize.width; + int height = mSize.height; + byte[] rotatedData = new byte[mData.length]; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + rotatedData[x * height + height - y - 1] = mData[x + y * width]; + } + } + int tmp = width; + width = height; + height = tmp; + try { + if (mDelegate == null) { + return null; + } + return mDelegate.processData(rotatedData, width, height); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + public interface Delegate { + String processData(byte[] data, int width, int height); + } + +} diff --git a/lib_qrcode/src/main/java/com/android/sdk/qrcode/QRCodeView.kt b/lib_qrcode/src/main/java/com/android/sdk/qrcode/QRCodeView.kt new file mode 100644 index 0000000..249e49d --- /dev/null +++ b/lib_qrcode/src/main/java/com/android/sdk/qrcode/QRCodeView.kt @@ -0,0 +1,281 @@ +package com.android.sdk.qrcode + +import android.content.Context +import android.graphics.Point +import android.graphics.Rect +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout +import io.fotoapparat.Fotoapparat +import io.fotoapparat.configuration.CameraConfiguration +import io.fotoapparat.configuration.UpdateConfiguration +import io.fotoapparat.log.logcat +import io.fotoapparat.preview.Frame +import io.fotoapparat.selector.* +import io.fotoapparat.view.CameraView + +abstract class QRCodeView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr), ProcessDataTask.Delegate { + + private lateinit var mCameraView: CameraView + private lateinit var mFotoapparat: Fotoapparat + + private lateinit var mScanBoxView: ScanBoxView + protected var mDelegate: Delegate? = null + + private var mProcessDataTask: ProcessDataTask? = null + protected var mSpotAble = false + + private val framingRect = Rect() + private var framingRectInPreview: Rect? = null + + init { + initView(context, attrs) + } + + private fun initView(context: Context, attrs: AttributeSet?) { + mCameraView = CameraView(getContext()) + mScanBoxView = ScanBoxView(getContext()) + mScanBoxView.initCustomAttrs(context, attrs) + addView(mCameraView) + addView(mScanBoxView) + try { + mFotoapparat = createFotoapparat() + } catch (e: Exception) { + e.printStackTrace() + mDelegate?.onScanQRCodeOpenCameraError(e) + } + } + + private fun createFotoapparat(): Fotoapparat { + val configuration = CameraConfiguration( + previewResolution = firstAvailable( + wideRatio(highestResolution()), + standardRatio(highestResolution()) + ), + previewFpsRange = highestFps(), + flashMode = off(), + focusMode = firstAvailable( + continuousFocusPicture(), + autoFocus() + ), + frameProcessor = this::processFrame + ) + + return Fotoapparat( + context = this@QRCodeView.context, + view = mCameraView, + logger = logcat(), + lensPosition = back(), + cameraConfiguration = configuration, + cameraErrorCallback = { + mDelegate?.onScanQRCodeOpenCameraError(it) + } + ) + } + + private fun processFrame(frame: Frame) { + val processDataTask = mProcessDataTask + if (mSpotAble && (processDataTask == null || processDataTask.isCancelled)) { + + mProcessDataTask = object : ProcessDataTask(frame.image, frame.size, frame.rotation, this) { + override fun onPostExecute(result: String?) { + + if (mSpotAble) { + if (!result.isNullOrEmpty()) { + try { + mDelegate?.onScanQRCodeSuccess(result) + stopSpot() + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + cancelProcessDataTask() + } + }.perform() + } + } + + /** + * 设置扫描二维码的代理 + * + * @param delegate 扫描二维码的代理 + */ + fun setDelegate(delegate: Delegate) { + mDelegate = delegate + } + + /** + * 显示扫描框 + */ + fun showScanRect() { + mScanBoxView.visibility = View.VISIBLE + } + + /** + * 隐藏扫描框 + */ + fun hiddenScanRect() { + mScanBoxView.visibility = View.GONE + } + + /** + * 打开后置摄像头开始预览,但是并未开始识别 + */ + fun startCamera() { + try { + mFotoapparat.start() + } catch (throwable: Throwable) { + throwable.printStackTrace() + } + + } + + /** + * 关闭摄像头预览,并且隐藏扫描框 + */ + fun stopCamera() { + stopSpotAndHiddenRect() + try { + mFotoapparat.stop() + } catch (throwable: Throwable) { + throwable.printStackTrace() + } + } + + /** + * 延100开始识别 + */ + fun startSpot() { + postDelayed({ + mSpotAble = true + startCamera() + }, 100) + } + + /** + * 停止识别 + */ + fun stopSpot() { + cancelProcessDataTask() + mSpotAble = false + } + + /** + * 停止识别,并且隐藏扫描框 + */ + fun stopSpotAndHiddenRect() { + stopSpot() + hiddenScanRect() + } + + /** + * 显示扫描框,并且延迟1.5秒后开始识别 + */ + fun startSpotAndShowRect() { + startSpot() + showScanRect() + } + + /** + * 当前是否为条码扫描样式 + * + * @return + */ + val isScanBarcodeStyle: Boolean + get() = mScanBoxView.isBarcode + + /** + * 打开闪光灯 + */ + fun openFlashlight() { + try { + mFotoapparat.updateConfiguration(UpdateConfiguration(flashMode = firstAvailable(torch(), off()))) + } catch (e: Exception) { + e.printStackTrace() + } + + } + + /** + * 关闭散光灯 + */ + fun closeFlashlight() { + try { + mFotoapparat.updateConfiguration(UpdateConfiguration(flashMode = firstAvailable(off()))) + } catch (e: Exception) { + e.printStackTrace() + } + + } + + /** + * 销毁二维码扫描控件 + */ + fun onDestroy() { + mDelegate = null + } + + /** + * 取消数据处理任务 + */ + protected fun cancelProcessDataTask() { + mProcessDataTask?.cancelTask() + mProcessDataTask = null + } + + /** + * 切换成扫描条码样式 + */ + fun changeToScanBarcodeStyle() { + mScanBoxView.isBarcode = true + } + + /** + * 切换成扫描二维码样式 + */ + fun changeToScanQRCodeStyle() { + mScanBoxView.isBarcode = false + } + + fun setDebug(debug: Boolean) { + Debug.setDebug(debug) + } + + protected fun getFramingRectInPreview(previewWidth: Int, previewHeight: Int): Rect? { + if (!mScanBoxView.getScanBoxAreaRect(framingRect)) { + return null + } + if (framingRectInPreview == null) { + val rect = Rect(framingRect) + val cameraResolution = Point(previewWidth, previewHeight) + val screenResolution = Utils.getScreenResolution(context) + val x = cameraResolution.x * 1.0f / screenResolution.x + val y = cameraResolution.y * 1.0f / screenResolution.y + rect.left = (rect.left * x).toInt() + rect.right = (rect.right * x).toInt() + rect.top = (rect.top * y).toInt() + rect.bottom = (rect.bottom * y).toInt() + framingRectInPreview = rect + } + return framingRectInPreview + } + + interface Delegate { + + /** + * 处理扫描结果 + */ + fun onScanQRCodeSuccess(result: String) + + /** + * 处理打开相机出错 + */ + fun onScanQRCodeOpenCameraError(error: java.lang.Exception) + + } + +} \ No newline at end of file diff --git a/lib_qrcode/src/main/java/com/android/sdk/qrcode/ScanBoxView.java b/lib_qrcode/src/main/java/com/android/sdk/qrcode/ScanBoxView.java new file mode 100644 index 0000000..e15e080 --- /dev/null +++ b/lib_qrcode/src/main/java/com/android/sdk/qrcode/ScanBoxView.java @@ -0,0 +1,568 @@ +package com.android.sdk.qrcode; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.text.Layout; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.View; + + +public class ScanBoxView extends View { + + private int mMoveStepDistance; + private int mAnimDelayTime; + + private Rect mFramingRect; + private float mScanLineTop; + private float mScanLineLeft; + private Paint mPaint; + private TextPaint mTipPaint; + + private int mMaskColor; + private int mCornerColor; + private int mCornerLength; + private int mCornerSize; + private int mRectWidth; + private int mRectHeight; + private int mBarcodeRectHeight; + private int mTopOffset; + private int mScanLineSize; + private int mScanLineColor; + private int mScanLineMargin; + private boolean mIsShowDefaultScanLineDrawable; + private Drawable mCustomScanLineDrawable; + private Bitmap mScanLineBitmap; + private int mBorderSize; + private int mBorderColor; + private int mAnimTime; + private boolean mIsCenterVertical; + private int mToolbarHeight; + private boolean mIsBarcode; + private String mQRCodeTipText; + private String mBarCodeTipText; + private String mTipText; + private int mTipTextSize; + private int mTipTextColor; + private boolean mIsTipTextBelowRect; + private int mTipTextMargin; + private boolean mIsShowTipTextAsSingleLine; + private int mTipBackgroundColor; + private boolean mIsShowTipBackground; + private boolean mIsScanLineReverse; + private boolean mIsShowDefaultGridScanLineDrawable; + private Drawable mCustomGridScanLineDrawable; + private Bitmap mGridScanLineBitmap; + private float mGridScanLineBottom; + private float mGridScanLineRight; + + private Bitmap mOriginQRCodeScanLineBitmap; + private Bitmap mOriginBarCodeScanLineBitmap; + private Bitmap mOriginQRCodeGridScanLineBitmap; + private Bitmap mOriginBarCodeGridScanLineBitmap; + + + private float mHalfCornerSize; + private StaticLayout mTipTextSl; + private int mTipBackgroundRadius; + + private boolean mIsOnlyDecodeScanBoxArea; + + public ScanBoxView(Context context) { + super(context); + mPaint = new Paint(); + mPaint.setAntiAlias(true); + mMaskColor = Color.parseColor("#33FFFFFF"); + mCornerColor = Color.WHITE; + mCornerLength = Utils.dp2px(context, 20); + mCornerSize = Utils.dp2px(context, 3); + mScanLineSize = Utils.dp2px(context, 1); + mScanLineColor = Color.WHITE; + mTopOffset = Utils.dp2px(context, 90); + mRectWidth = Utils.dp2px(context, 200); + mBarcodeRectHeight = Utils.dp2px(context, 140); + mScanLineMargin = 0; + mIsShowDefaultScanLineDrawable = false; + mCustomScanLineDrawable = null; + mScanLineBitmap = null; + mBorderSize = Utils.dp2px(context, 1); + mBorderColor = Color.WHITE; + mAnimTime = 1000; + mIsCenterVertical = false; + mToolbarHeight = 0; + mIsBarcode = false; + mMoveStepDistance = Utils.dp2px(context, 2); + mTipText = null; + mTipTextSize = Utils.sp2px(context, 14); + mTipTextColor = Color.WHITE; + mIsTipTextBelowRect = false; + mTipTextMargin = Utils.dp2px(context, 20); + mIsShowTipTextAsSingleLine = false; + mTipBackgroundColor = Color.parseColor("#22000000"); + mIsShowTipBackground = false; + mIsScanLineReverse = false; + mIsShowDefaultGridScanLineDrawable = false; + + mTipPaint = new TextPaint(); + mTipPaint.setAntiAlias(true); + + mTipBackgroundRadius = Utils.dp2px(context, 4); + + mIsOnlyDecodeScanBoxArea = false; + } + + public void initCustomAttrs(Context context, AttributeSet attrs) { + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.QRCodeView); + final int count = typedArray.getIndexCount(); + for (int i = 0; i < count; i++) { + initCustomAttr(typedArray.getIndex(i), typedArray); + } + typedArray.recycle(); + + afterInitCustomAttrs(); + } + + private void initCustomAttr(int attr, TypedArray typedArray) { + if (attr == R.styleable.QRCodeView_qrcv_topOffset) { + mTopOffset = typedArray.getDimensionPixelSize(attr, mTopOffset); + } else if (attr == R.styleable.QRCodeView_qrcv_cornerSize) { + mCornerSize = typedArray.getDimensionPixelSize(attr, mCornerSize); + } else if (attr == R.styleable.QRCodeView_qrcv_cornerLength) { + mCornerLength = typedArray.getDimensionPixelSize(attr, mCornerLength); + } else if (attr == R.styleable.QRCodeView_qrcv_scanLineSize) { + mScanLineSize = typedArray.getDimensionPixelSize(attr, mScanLineSize); + } else if (attr == R.styleable.QRCodeView_qrcv_rectWidth) { + mRectWidth = typedArray.getDimensionPixelSize(attr, mRectWidth); + } else if (attr == R.styleable.QRCodeView_qrcv_rectWidth_percent) { + float percent = typedArray.getFloat(attr, 0.4F); + Point screenResolution = Utils.getScreenResolution(getContext()); + mRectWidth = (int) (Math.min(screenResolution.x, screenResolution.y) * percent); + } else if (attr == R.styleable.QRCodeView_qrcv_maskColor) { + mMaskColor = typedArray.getColor(attr, mMaskColor); + } else if (attr == R.styleable.QRCodeView_qrcv_cornerColor) { + mCornerColor = typedArray.getColor(attr, mCornerColor); + } else if (attr == R.styleable.QRCodeView_qrcv_scanLineColor) { + mScanLineColor = typedArray.getColor(attr, mScanLineColor); + } else if (attr == R.styleable.QRCodeView_qrcv_scanLineMargin) { + mScanLineMargin = typedArray.getDimensionPixelSize(attr, mScanLineMargin); + } else if (attr == R.styleable.QRCodeView_qrcv_isShowDefaultScanLineDrawable) { + mIsShowDefaultScanLineDrawable = typedArray.getBoolean(attr, mIsShowDefaultScanLineDrawable); + } else if (attr == R.styleable.QRCodeView_qrcv_customScanLineDrawable) { + mCustomScanLineDrawable = typedArray.getDrawable(attr); + } else if (attr == R.styleable.QRCodeView_qrcv_borderSize) { + mBorderSize = typedArray.getDimensionPixelSize(attr, mBorderSize); + } else if (attr == R.styleable.QRCodeView_qrcv_borderColor) { + mBorderColor = typedArray.getColor(attr, mBorderColor); + } else if (attr == R.styleable.QRCodeView_qrcv_animTime) { + mAnimTime = typedArray.getInteger(attr, mAnimTime); + } else if (attr == R.styleable.QRCodeView_qrcv_isCenterVertical) { + mIsCenterVertical = typedArray.getBoolean(attr, mIsCenterVertical); + } else if (attr == R.styleable.QRCodeView_qrcv_toolbarHeight) { + mToolbarHeight = typedArray.getDimensionPixelSize(attr, mToolbarHeight); + } else if (attr == R.styleable.QRCodeView_qrcv_barcodeRectHeight) { + mBarcodeRectHeight = typedArray.getDimensionPixelSize(attr, mBarcodeRectHeight); + } else if (attr == R.styleable.QRCodeView_qrcv_isBarcode) { + mIsBarcode = typedArray.getBoolean(attr, mIsBarcode); + } else if (attr == R.styleable.QRCodeView_qrcv_barCodeTipText) { + mBarCodeTipText = typedArray.getString(attr); + } else if (attr == R.styleable.QRCodeView_qrcv_qrCodeTipText) { + mQRCodeTipText = typedArray.getString(attr); + } else if (attr == R.styleable.QRCodeView_qrcv_tipTextSize) { + mTipTextSize = typedArray.getDimensionPixelSize(attr, mTipTextSize); + } else if (attr == R.styleable.QRCodeView_qrcv_tipTextColor) { + mTipTextColor = typedArray.getColor(attr, mTipTextColor); + } else if (attr == R.styleable.QRCodeView_qrcv_isTipTextBelowRect) { + mIsTipTextBelowRect = typedArray.getBoolean(attr, mIsTipTextBelowRect); + } else if (attr == R.styleable.QRCodeView_qrcv_tipTextMargin) { + mTipTextMargin = typedArray.getDimensionPixelSize(attr, mTipTextMargin); + } else if (attr == R.styleable.QRCodeView_qrcv_isShowTipTextAsSingleLine) { + mIsShowTipTextAsSingleLine = typedArray.getBoolean(attr, mIsShowTipTextAsSingleLine); + } else if (attr == R.styleable.QRCodeView_qrcv_isShowTipBackground) { + mIsShowTipBackground = typedArray.getBoolean(attr, mIsShowTipBackground); + } else if (attr == R.styleable.QRCodeView_qrcv_tipBackgroundColor) { + mTipBackgroundColor = typedArray.getColor(attr, mTipBackgroundColor); + } else if (attr == R.styleable.QRCodeView_qrcv_isScanLineReverse) { + mIsScanLineReverse = typedArray.getBoolean(attr, mIsScanLineReverse); + } else if (attr == R.styleable.QRCodeView_qrcv_isShowDefaultGridScanLineDrawable) { + mIsShowDefaultGridScanLineDrawable = typedArray.getBoolean(attr, mIsShowDefaultGridScanLineDrawable); + } else if (attr == R.styleable.QRCodeView_qrcv_customGridScanLineDrawable) { + mCustomGridScanLineDrawable = typedArray.getDrawable(attr); + } else if (attr == R.styleable.QRCodeView_qrcv_isOnlyDecodeScanBoxArea) { + mIsOnlyDecodeScanBoxArea = typedArray.getBoolean(attr, mIsOnlyDecodeScanBoxArea); + } + } + + private void afterInitCustomAttrs() { + if (mCustomGridScanLineDrawable != null) { + mOriginQRCodeGridScanLineBitmap = ((BitmapDrawable) mCustomGridScanLineDrawable).getBitmap(); + } + if (mOriginQRCodeGridScanLineBitmap == null) { + mOriginQRCodeGridScanLineBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.qrcode_default_grid_scan_line); + mOriginQRCodeGridScanLineBitmap = Utils.makeTintBitmap(mOriginQRCodeGridScanLineBitmap, mScanLineColor); + } + + mOriginBarCodeGridScanLineBitmap = Utils.adjustPhotoRotation(mOriginQRCodeGridScanLineBitmap, 90); + mOriginBarCodeGridScanLineBitmap = Utils.adjustPhotoRotation(mOriginBarCodeGridScanLineBitmap, 90); + mOriginBarCodeGridScanLineBitmap = Utils.adjustPhotoRotation(mOriginBarCodeGridScanLineBitmap, 90); + + if (mCustomScanLineDrawable != null) { + mOriginQRCodeScanLineBitmap = ((BitmapDrawable) mCustomScanLineDrawable).getBitmap(); + } + + if (mOriginQRCodeScanLineBitmap == null) { + mOriginQRCodeScanLineBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.qrcode_default_scan_line); + mOriginQRCodeScanLineBitmap = Utils.makeTintBitmap(mOriginQRCodeScanLineBitmap, mScanLineColor); + } + + mOriginBarCodeScanLineBitmap = Utils.adjustPhotoRotation(mOriginQRCodeScanLineBitmap, 90); + + mTopOffset += mToolbarHeight; + + mHalfCornerSize = 1.0f * mCornerSize / 2; + + mTipPaint.setTextSize(mTipTextSize); + mTipPaint.setColor(mTipTextColor); + + setIsBarcode(mIsBarcode); + } + + @Override + public void onDraw(Canvas canvas) { + if (mFramingRect == null) { + return; + } + + // 画遮罩层 + drawMask(canvas); + + // 画边框线 + drawBorderLine(canvas); + + // 画四个直角的线 + drawCornerLine(canvas); + + // 画扫描线 + drawScanLine(canvas); + + // 画提示文本 + drawTipText(canvas); + + // 移动扫描线的位置 + moveScanLine(); + + } + + /** + * 画遮罩层 + * + * @param canvas + */ + private void drawMask(Canvas canvas) { + int width = canvas.getWidth(); + int height = canvas.getHeight(); + + if (mMaskColor != Color.TRANSPARENT) { + mPaint.setStyle(Paint.Style.FILL); + mPaint.setColor(mMaskColor); + canvas.drawRect(0, 0, width, mFramingRect.top, mPaint); + canvas.drawRect(0, mFramingRect.top, mFramingRect.left, mFramingRect.bottom + 1, mPaint); + canvas.drawRect(mFramingRect.right + 1, mFramingRect.top, width, mFramingRect.bottom + 1, mPaint); + canvas.drawRect(0, mFramingRect.bottom + 1, width, height, mPaint); + } + } + + /** + * 画边框线 + * + * @param canvas + */ + private void drawBorderLine(Canvas canvas) { + if (mBorderSize > 0) { + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setColor(mBorderColor); + mPaint.setStrokeWidth(mBorderSize); + canvas.drawRect(mFramingRect, mPaint); + } + } + + /** + * 画四个直角的线 + * + * @param canvas + */ + private void drawCornerLine(Canvas canvas) { + if (mHalfCornerSize > 0) { + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setColor(mCornerColor); + mPaint.setStrokeWidth(mCornerSize); + canvas.drawLine(mFramingRect.left - mHalfCornerSize, mFramingRect.top, mFramingRect.left - mHalfCornerSize + mCornerLength, mFramingRect.top, mPaint); + canvas.drawLine(mFramingRect.left, mFramingRect.top - mHalfCornerSize, mFramingRect.left, mFramingRect.top - mHalfCornerSize + mCornerLength, mPaint); + canvas.drawLine(mFramingRect.right + mHalfCornerSize, mFramingRect.top, mFramingRect.right + mHalfCornerSize - mCornerLength, mFramingRect.top, mPaint); + canvas.drawLine(mFramingRect.right, mFramingRect.top - mHalfCornerSize, mFramingRect.right, mFramingRect.top - mHalfCornerSize + mCornerLength, mPaint); + + canvas.drawLine(mFramingRect.left - mHalfCornerSize, mFramingRect.bottom, mFramingRect.left - mHalfCornerSize + mCornerLength, mFramingRect.bottom, mPaint); + canvas.drawLine(mFramingRect.left, mFramingRect.bottom + mHalfCornerSize, mFramingRect.left, mFramingRect.bottom + mHalfCornerSize - mCornerLength, mPaint); + canvas.drawLine(mFramingRect.right + mHalfCornerSize, mFramingRect.bottom, mFramingRect.right + mHalfCornerSize - mCornerLength, mFramingRect.bottom, mPaint); + canvas.drawLine(mFramingRect.right, mFramingRect.bottom + mHalfCornerSize, mFramingRect.right, mFramingRect.bottom + mHalfCornerSize - mCornerLength, mPaint); + } + } + + /** + * 画扫描线 + * + * @param canvas + */ + private void drawScanLine(Canvas canvas) { + if (mIsBarcode) { + if (mGridScanLineBitmap != null) { + RectF dstGridRectF = new RectF(mFramingRect.left + mHalfCornerSize + 0.5f, mFramingRect.top + mHalfCornerSize + mScanLineMargin, mGridScanLineRight, mFramingRect.bottom - mHalfCornerSize - mScanLineMargin); + + Rect srcGridRect = new Rect((int) (mGridScanLineBitmap.getWidth() - dstGridRectF.width()), 0, mGridScanLineBitmap.getWidth(), mGridScanLineBitmap.getHeight()); + + if (srcGridRect.left < 0) { + srcGridRect.left = 0; + dstGridRectF.left = dstGridRectF.right - srcGridRect.width(); + } + + canvas.drawBitmap(mGridScanLineBitmap, srcGridRect, dstGridRectF, mPaint); + } else if (mScanLineBitmap != null) { + RectF lineRect = new RectF(mScanLineLeft, mFramingRect.top + mHalfCornerSize + mScanLineMargin, mScanLineLeft + mScanLineBitmap.getWidth(), mFramingRect.bottom - mHalfCornerSize - mScanLineMargin); + canvas.drawBitmap(mScanLineBitmap, null, lineRect, mPaint); + } else { + mPaint.setStyle(Paint.Style.FILL); + mPaint.setColor(mScanLineColor); + canvas.drawRect(mScanLineLeft, mFramingRect.top + mHalfCornerSize + mScanLineMargin, mScanLineLeft + mScanLineSize, mFramingRect.bottom - mHalfCornerSize - mScanLineMargin, mPaint); + } + } else { + if (mGridScanLineBitmap != null) { + RectF dstGridRectF = new RectF(mFramingRect.left + mHalfCornerSize + mScanLineMargin, mFramingRect.top + mHalfCornerSize + 0.5f, mFramingRect.right - mHalfCornerSize - mScanLineMargin, mGridScanLineBottom); + + Rect srcRect = new Rect(0, (int) (mGridScanLineBitmap.getHeight() - dstGridRectF.height()), mGridScanLineBitmap.getWidth(), mGridScanLineBitmap.getHeight()); + + if (srcRect.top < 0) { + srcRect.top = 0; + dstGridRectF.top = dstGridRectF.bottom - srcRect.height(); + } + + canvas.drawBitmap(mGridScanLineBitmap, srcRect, dstGridRectF, mPaint); + } else if (mScanLineBitmap != null) { + RectF lineRect = new RectF(mFramingRect.left + mHalfCornerSize + mScanLineMargin, mScanLineTop, mFramingRect.right - mHalfCornerSize - mScanLineMargin, mScanLineTop + mScanLineBitmap.getHeight()); + canvas.drawBitmap(mScanLineBitmap, null, lineRect, mPaint); + } else { + mPaint.setStyle(Paint.Style.FILL); + mPaint.setColor(mScanLineColor); + canvas.drawRect(mFramingRect.left + mHalfCornerSize + mScanLineMargin, mScanLineTop, mFramingRect.right - mHalfCornerSize - mScanLineMargin, mScanLineTop + mScanLineSize, mPaint); + } + } + } + + /** + * 画提示文本 + * + * @param canvas + */ + private void drawTipText(Canvas canvas) { + if (TextUtils.isEmpty(mTipText) || mTipTextSl == null) { + return; + } + + if (mIsTipTextBelowRect) { + if (mIsShowTipBackground) { + mPaint.setColor(mTipBackgroundColor); + mPaint.setStyle(Paint.Style.FILL); + if (mIsShowTipTextAsSingleLine) { + Rect tipRect = new Rect(); + mTipPaint.getTextBounds(mTipText, 0, mTipText.length(), tipRect); + float left = (canvas.getWidth() - tipRect.width()) / 2 - mTipBackgroundRadius; + canvas.drawRoundRect(new RectF(left, mFramingRect.bottom + mTipTextMargin - mTipBackgroundRadius, left + tipRect.width() + 2 * mTipBackgroundRadius, mFramingRect.bottom + mTipTextMargin + mTipTextSl.getHeight() + mTipBackgroundRadius), mTipBackgroundRadius, mTipBackgroundRadius, mPaint); + } else { + canvas.drawRoundRect(new RectF(mFramingRect.left, mFramingRect.bottom + mTipTextMargin - mTipBackgroundRadius, mFramingRect.right, mFramingRect.bottom + mTipTextMargin + mTipTextSl.getHeight() + mTipBackgroundRadius), mTipBackgroundRadius, mTipBackgroundRadius, mPaint); + } + } + + canvas.save(); + if (mIsShowTipTextAsSingleLine) { + canvas.translate(0, mFramingRect.bottom + mTipTextMargin); + } else { + canvas.translate(mFramingRect.left + mTipBackgroundRadius, mFramingRect.bottom + mTipTextMargin); + } + mTipTextSl.draw(canvas); + canvas.restore(); + } else { + if (mIsShowTipBackground) { + mPaint.setColor(mTipBackgroundColor); + mPaint.setStyle(Paint.Style.FILL); + + if (mIsShowTipTextAsSingleLine) { + Rect tipRect = new Rect(); + mTipPaint.getTextBounds(mTipText, 0, mTipText.length(), tipRect); + float left = (canvas.getWidth() - tipRect.width()) / 2 - mTipBackgroundRadius; + canvas.drawRoundRect(new RectF(left, mFramingRect.top - mTipTextMargin - mTipTextSl.getHeight() - mTipBackgroundRadius, left + tipRect.width() + 2 * mTipBackgroundRadius, mFramingRect.top - mTipTextMargin + mTipBackgroundRadius), mTipBackgroundRadius, mTipBackgroundRadius, mPaint); + } else { + canvas.drawRoundRect(new RectF(mFramingRect.left, mFramingRect.top - mTipTextMargin - mTipTextSl.getHeight() - mTipBackgroundRadius, mFramingRect.right, mFramingRect.top - mTipTextMargin + mTipBackgroundRadius), mTipBackgroundRadius, mTipBackgroundRadius, mPaint); + } + } + + canvas.save(); + if (mIsShowTipTextAsSingleLine) { + canvas.translate(0, mFramingRect.top - mTipTextMargin - mTipTextSl.getHeight()); + } else { + canvas.translate(mFramingRect.left + mTipBackgroundRadius, mFramingRect.top - mTipTextMargin - mTipTextSl.getHeight()); + } + mTipTextSl.draw(canvas); + canvas.restore(); + } + } + + /** + * 移动扫描线的位置 + */ + private void moveScanLine() { + if (mIsBarcode) { + if (mGridScanLineBitmap == null) { + // 处理非网格扫描图片的情况 + mScanLineLeft += mMoveStepDistance; + int scanLineSize = mScanLineSize; + if (mScanLineBitmap != null) { + scanLineSize = mScanLineBitmap.getWidth(); + } + + if (mIsScanLineReverse) { + if (mScanLineLeft + scanLineSize > mFramingRect.right - mHalfCornerSize || mScanLineLeft < mFramingRect.left + mHalfCornerSize) { + mMoveStepDistance = -mMoveStepDistance; + } + } else { + if (mScanLineLeft + scanLineSize > mFramingRect.right - mHalfCornerSize) { + mScanLineLeft = mFramingRect.left + mHalfCornerSize + 0.5f; + } + } + } else { + // 处理网格扫描图片的情况 + mGridScanLineRight += mMoveStepDistance; + if (mGridScanLineRight > mFramingRect.right - mHalfCornerSize) { + mGridScanLineRight = mFramingRect.left + mHalfCornerSize + 0.5f; + } + } + } else { + if (mGridScanLineBitmap == null) { + // 处理非网格扫描图片的情况 + mScanLineTop += mMoveStepDistance; + int scanLineSize = mScanLineSize; + if (mScanLineBitmap != null) { + scanLineSize = mScanLineBitmap.getHeight(); + } + + if (mIsScanLineReverse) { + if (mScanLineTop + scanLineSize > mFramingRect.bottom - mHalfCornerSize || mScanLineTop < mFramingRect.top + mHalfCornerSize) { + mMoveStepDistance = -mMoveStepDistance; + } + } else { + if (mScanLineTop + scanLineSize > mFramingRect.bottom - mHalfCornerSize) { + mScanLineTop = mFramingRect.top + mHalfCornerSize + 0.5f; + } + } + } else { + // 处理网格扫描图片的情况 + mGridScanLineBottom += mMoveStepDistance; + if (mGridScanLineBottom > mFramingRect.bottom - mHalfCornerSize) { + mGridScanLineBottom = mFramingRect.top + mHalfCornerSize + 0.5f; + } + } + + } + postInvalidateDelayed(mAnimDelayTime, mFramingRect.left, mFramingRect.top, mFramingRect.right, mFramingRect.bottom); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + calFramingRect(); + } + + private void calFramingRect() { + Point screenResolution = Utils.getScreenResolution(getContext()); + int leftOffset = (screenResolution.x - mRectWidth) / 2; + mFramingRect = new Rect(leftOffset, mTopOffset, leftOffset + mRectWidth, mTopOffset + mRectHeight); + + if (mIsBarcode) { + mGridScanLineRight = mScanLineLeft = mFramingRect.left + mHalfCornerSize + 0.5f; + } else { + mGridScanLineBottom = mScanLineTop = mFramingRect.top + mHalfCornerSize + 0.5f; + } + } + + public boolean getScanBoxAreaRect(Rect rect) { + if (mIsOnlyDecodeScanBoxArea) { + rect.set(mFramingRect); + return true; + } else { + return false; + } + } + + public void setIsBarcode(boolean isBarcode) { + mIsBarcode = isBarcode; + + if (mCustomGridScanLineDrawable != null || mIsShowDefaultGridScanLineDrawable) { + if (mIsBarcode) { + mGridScanLineBitmap = mOriginBarCodeGridScanLineBitmap; + } else { + mGridScanLineBitmap = mOriginQRCodeGridScanLineBitmap; + } + } else if (mCustomScanLineDrawable != null || mIsShowDefaultScanLineDrawable) { + if (mIsBarcode) { + mScanLineBitmap = mOriginBarCodeScanLineBitmap; + } else { + mScanLineBitmap = mOriginQRCodeScanLineBitmap; + } + } + + if (mIsBarcode) { + mTipText = mBarCodeTipText; + mRectHeight = mBarcodeRectHeight; + mAnimDelayTime = (int) ((1.0f * mAnimTime * mMoveStepDistance) / mRectWidth); + } else { + mTipText = mQRCodeTipText; + mRectHeight = mRectWidth; + mAnimDelayTime = (int) ((1.0f * mAnimTime * mMoveStepDistance) / mRectHeight); + } + + if (!TextUtils.isEmpty(mTipText)) { + if (mIsShowTipTextAsSingleLine) { + mTipTextSl = new StaticLayout(mTipText, mTipPaint, Utils.getScreenResolution(getContext()).x, Layout.Alignment.ALIGN_CENTER, 1.0f, 0, true); + } else { + mTipTextSl = new StaticLayout(mTipText, mTipPaint, mRectWidth - 2 * mTipBackgroundRadius, Layout.Alignment.ALIGN_CENTER, 1.0f, 0, true); + } + } + + if (mIsCenterVertical) { + int screenHeight = Utils.getScreenResolution(getContext()).y; + if (mToolbarHeight == 0) { + mTopOffset = (screenHeight - mRectHeight) / 2; + } else { + mTopOffset = (screenHeight - mRectHeight) / 2 + mToolbarHeight / 2; + } + } + + calFramingRect(); + + postInvalidate(); + } + + public boolean getIsBarcode() { + return mIsBarcode; + } + +} \ No newline at end of file diff --git a/lib_qrcode/src/main/java/com/android/sdk/qrcode/Utils.java b/lib_qrcode/src/main/java/com/android/sdk/qrcode/Utils.java new file mode 100644 index 0000000..95fe2a4 --- /dev/null +++ b/lib_qrcode/src/main/java/com/android/sdk/qrcode/Utils.java @@ -0,0 +1,84 @@ +package com.android.sdk.qrcode; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.util.TypedValue; +import android.view.Display; +import android.view.WindowManager; + +final class Utils { + + private Utils() { + } + + @SuppressLint("ObsoleteSdkInt") + static Point getScreenResolution(Context context) { + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + Display display = wm.getDefaultDisplay(); + Point screenResolution = new Point(); + if (android.os.Build.VERSION.SDK_INT >= 13) { + display.getSize(screenResolution); + } else { + screenResolution.set(display.getWidth(), display.getHeight()); + } + return screenResolution; + } + + static int dp2px(Context context, @SuppressWarnings("all") float dpValue) { + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, context.getResources().getDisplayMetrics()); + } + + static int sp2px(Context context, @SuppressWarnings("all") float spValue) { + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spValue, context.getResources().getDisplayMetrics()); + } + + + static Bitmap adjustPhotoRotation(Bitmap inputBitmap, @SuppressWarnings("all") int orientationDegree) { + + if (inputBitmap == null) { + return null; + } + + Matrix matrix = new Matrix(); + matrix.setRotate(orientationDegree, (float) inputBitmap.getWidth() / 2, (float) inputBitmap.getHeight() / 2); + float outputX, outputY; + if (orientationDegree == 90) { + outputX = inputBitmap.getHeight(); + outputY = 0; + } else { + outputX = inputBitmap.getHeight(); + outputY = inputBitmap.getWidth(); + } + + final float[] values = new float[9]; + matrix.getValues(values); + float x1 = values[Matrix.MTRANS_X]; + float y1 = values[Matrix.MTRANS_Y]; + matrix.postTranslate(outputX - x1, outputY - y1); + Bitmap outputBitmap = Bitmap.createBitmap(inputBitmap.getHeight(), inputBitmap.getWidth(), Bitmap.Config.ARGB_8888); + Paint paint = new Paint(); + Canvas canvas = new Canvas(outputBitmap); + canvas.drawBitmap(inputBitmap, matrix, paint); + return outputBitmap; + } + + static Bitmap makeTintBitmap(Bitmap inputBitmap, int tintColor) { + if (inputBitmap == null) { + return null; + } + Bitmap outputBitmap = Bitmap.createBitmap(inputBitmap.getWidth(), inputBitmap.getHeight(), inputBitmap.getConfig()); + Canvas canvas = new Canvas(outputBitmap); + Paint paint = new Paint(); + paint.setColorFilter(new PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_IN)); + canvas.drawBitmap(inputBitmap, 0, 0, paint); + return outputBitmap; + } + +} \ No newline at end of file diff --git a/lib_qrcode/src/main/java/com/android/sdk/qrcode/zxing/QRCodeDecoder.java b/lib_qrcode/src/main/java/com/android/sdk/qrcode/zxing/QRCodeDecoder.java new file mode 100644 index 0000000..f7ea79b --- /dev/null +++ b/lib_qrcode/src/main/java/com/android/sdk/qrcode/zxing/QRCodeDecoder.java @@ -0,0 +1,104 @@ +package com.android.sdk.qrcode.zxing; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.BinaryBitmap; +import com.google.zxing.DecodeHintType; +import com.google.zxing.MultiFormatReader; +import com.google.zxing.RGBLuminanceSource; +import com.google.zxing.Result; +import com.google.zxing.common.HybridBinarizer; + +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +@SuppressWarnings("unused,WeakerAccess") +public class QRCodeDecoder { + + static final Map HINTS = new EnumMap<>(DecodeHintType.class); + + static { + List allFormats = new ArrayList<>(); + allFormats.add(BarcodeFormat.AZTEC); + allFormats.add(BarcodeFormat.CODABAR); + allFormats.add(BarcodeFormat.CODE_39); + allFormats.add(BarcodeFormat.CODE_93); + allFormats.add(BarcodeFormat.CODE_128); + allFormats.add(BarcodeFormat.DATA_MATRIX); + allFormats.add(BarcodeFormat.EAN_8); + allFormats.add(BarcodeFormat.EAN_13); + allFormats.add(BarcodeFormat.ITF); + allFormats.add(BarcodeFormat.MAXICODE); + allFormats.add(BarcodeFormat.PDF_417); + allFormats.add(BarcodeFormat.QR_CODE); + allFormats.add(BarcodeFormat.RSS_14); + allFormats.add(BarcodeFormat.RSS_EXPANDED); + allFormats.add(BarcodeFormat.UPC_A); + allFormats.add(BarcodeFormat.UPC_E); + allFormats.add(BarcodeFormat.UPC_EAN_EXTENSION); + + HINTS.put(DecodeHintType.POSSIBLE_FORMATS, allFormats); + HINTS.put(DecodeHintType.CHARACTER_SET, "utf-8"); + } + + private QRCodeDecoder() { + } + + /** + * 同步解析本地图片二维码。该方法是耗时操作,请在子线程中调用。 + * + * @param picturePath 要解析的二维码图片本地路径 + * @return 返回二维码图片里的内容 或 null + */ + public static String syncDecodeQRCode(String picturePath) { + return syncDecodeQRCode(getDecodeAbleBitmap(picturePath)); + } + + /** + * 同步解析bitmap二维码。该方法是耗时操作,请在子线程中调用。 + * + * @param bitmap 要解析的二维码图片 + * @return 返回二维码图片里的内容 或 null + */ + public static String syncDecodeQRCode(Bitmap bitmap) { + try { + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + int[] pixels = new int[width * height]; + bitmap.getPixels(pixels, 0, width, 0, 0, width, height); + RGBLuminanceSource source = new RGBLuminanceSource(width, height, pixels); + Result result = new MultiFormatReader().decode(new BinaryBitmap(new HybridBinarizer(source)), HINTS); + return result.getText(); + } catch (Exception e) { + return null; + } + } + + /** + * 将本地图片文件转换成可解码二维码的 Bitmap。为了避免图片太大,这里对图片进行了压缩。感谢 https://github.com/devilsen 提的 PR + * + * @param picturePath 本地图片文件路径 + * @return Bitmap + */ + private static Bitmap getDecodeAbleBitmap(String picturePath) { + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(picturePath, options); + int sampleSize = options.outHeight / 400; + if (sampleSize <= 0) { + sampleSize = 1; + } + options.inSampleSize = sampleSize; + options.inJustDecodeBounds = false; + return BitmapFactory.decodeFile(picturePath, options); + } catch (Exception e) { + return null; + } + } + +} \ No newline at end of file diff --git a/lib_qrcode/src/main/java/com/android/sdk/qrcode/zxing/QRCodeEncoder.java b/lib_qrcode/src/main/java/com/android/sdk/qrcode/zxing/QRCodeEncoder.java new file mode 100644 index 0000000..1033ba5 --- /dev/null +++ b/lib_qrcode/src/main/java/com/android/sdk/qrcode/zxing/QRCodeEncoder.java @@ -0,0 +1,122 @@ +package com.android.sdk.qrcode.zxing; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.MultiFormatWriter; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; + +import java.util.EnumMap; +import java.util.Map; + +/** + * 创建二维码图片 + */ +@SuppressWarnings("unused,WeakerAccess") +public class QRCodeEncoder { + + private static final Map HINTS = new EnumMap<>(EncodeHintType.class); + + static { + HINTS.put(EncodeHintType.CHARACTER_SET, "utf-8"); + HINTS.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); + HINTS.put(EncodeHintType.MARGIN, 0); + } + + private QRCodeEncoder() { + } + + /** + * 同步创建黑色前景色、白色背景色的二维码图片。该方法是耗时操作,请在子线程中调用。 + * + * @param content 要生成的二维码图片内容 + * @param size 图片宽高,单位为px + */ + public static Bitmap syncEncodeQRCode(String content, int size) { + return syncEncodeQRCode(content, size, Color.BLACK, Color.WHITE, null); + } + + /** + * 同步创建指定前景色、白色背景色的二维码图片。该方法是耗时操作,请在子线程中调用。 + * + * @param content 要生成的二维码图片内容 + * @param size 图片宽高,单位为px + * @param foregroundColor 二维码图片的前景色 + */ + public static Bitmap syncEncodeQRCode(String content, int size, int foregroundColor) { + return syncEncodeQRCode(content, size, foregroundColor, Color.WHITE, null); + } + + /** + * 同步创建指定前景色、白色背景色、带logo的二维码图片。该方法是耗时操作,请在子线程中调用。 + * + * @param content 要生成的二维码图片内容 + * @param size 图片宽高,单位为px + * @param foregroundColor 二维码图片的前景色 + * @param logo 二维码图片的logo + */ + public static Bitmap syncEncodeQRCode(String content, int size, int foregroundColor, Bitmap logo) { + return syncEncodeQRCode(content, size, foregroundColor, Color.WHITE, logo); + } + + /** + * 同步创建指定前景色、指定背景色、带logo的二维码图片。该方法是耗时操作,请在子线程中调用。 + * + * @param content 要生成的二维码图片内容 + * @param size 图片宽高,单位为px + * @param foregroundColor 二维码图片的前景色 + * @param backgroundColor 二维码图片的背景色 + * @param logo 二维码图片的logo + */ + public static Bitmap syncEncodeQRCode(String content, int size, int foregroundColor, int backgroundColor, Bitmap logo) { + try { + BitMatrix matrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, size, size, HINTS); + int[] pixels = new int[size * size]; + for (int y = 0; y < size; y++) { + for (int x = 0; x < size; x++) { + if (matrix.get(x, y)) { + pixels[y * size + x] = foregroundColor; + } else { + pixels[y * size + x] = backgroundColor; + } + } + } + Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); + bitmap.setPixels(pixels, 0, size, 0, 0, size, size); + return addLogoToQRCode(bitmap, logo); + } catch (Exception e) { + return null; + } + } + + /** + * 添加logo到二维码图片上 + */ + private static Bitmap addLogoToQRCode(Bitmap src, Bitmap logo) { + if (src == null || logo == null) { + return src; + } + + int srcWidth = src.getWidth(); + int srcHeight = src.getHeight(); + int logoWidth = logo.getWidth(); + int logoHeight = logo.getHeight(); + + float scaleFactor = srcWidth * 1.0f / 5 / logoWidth; + Bitmap bitmap = Bitmap.createBitmap(srcWidth, srcHeight, Bitmap.Config.ARGB_8888); + try { + Canvas canvas = new Canvas(bitmap); + canvas.drawBitmap(src, 0, 0, null); + canvas.scale(scaleFactor, scaleFactor, srcWidth / 2, srcHeight / 2); + canvas.drawBitmap(logo, (srcWidth - logoWidth) / 2, (srcHeight - logoHeight) / 2, null); + } catch (Exception e) { + bitmap = null; + } + return bitmap; + } + +} \ No newline at end of file diff --git a/lib_qrcode/src/main/java/com/android/sdk/qrcode/zxing/ZXingView.java b/lib_qrcode/src/main/java/com/android/sdk/qrcode/zxing/ZXingView.java new file mode 100644 index 0000000..254c02d --- /dev/null +++ b/lib_qrcode/src/main/java/com/android/sdk/qrcode/zxing/ZXingView.java @@ -0,0 +1,61 @@ +package com.android.sdk.qrcode.zxing; + +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; + +import com.android.sdk.qrcode.QRCodeView; +import com.android.sdk.qrcode.Debug; +import com.google.zxing.BinaryBitmap; +import com.google.zxing.MultiFormatReader; +import com.google.zxing.PlanarYUVLuminanceSource; +import com.google.zxing.Result; +import com.google.zxing.common.HybridBinarizer; + +public class ZXingView extends QRCodeView { + + private MultiFormatReader mMultiFormatReader; + + public ZXingView(Context context, AttributeSet attributeSet) { + this(context, attributeSet, 0); + } + + public ZXingView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initMultiFormatReader(); + } + + private void initMultiFormatReader() { + mMultiFormatReader = new MultiFormatReader(); + mMultiFormatReader.setHints(QRCodeDecoder.HINTS); + } + + @Override + public String processData(byte[] data, int width, int height) { + String result = null; + Result rawResult = null; + + try { + PlanarYUVLuminanceSource source; + Rect rect = getFramingRectInPreview(width, height); + Debug.log("rect:" + rect); + Debug.log("data width * height:" + width + "*" + height); + if (rect != null) { + source = new PlanarYUVLuminanceSource(data, width, height, rect.left, rect.top, rect.width(), rect.height(), false); + } else { + source = new PlanarYUVLuminanceSource(data, width, height, 0, 0, width, height, false); + } + rawResult = mMultiFormatReader.decodeWithState(new BinaryBitmap(new HybridBinarizer(source))); + Debug.log("rawResult:" + rawResult); + } catch (Exception e) { + e.printStackTrace(); + } finally { + mMultiFormatReader.reset(); + } + if (rawResult != null) { + result = rawResult.getText(); + } + return result; + } + +} \ No newline at end of file diff --git a/lib_qrcode/src/main/res/mipmap-xxhdpi/qrcode_default_grid_scan_line.png b/lib_qrcode/src/main/res/mipmap-xxhdpi/qrcode_default_grid_scan_line.png new file mode 100644 index 0000000..8b642a3 Binary files /dev/null and b/lib_qrcode/src/main/res/mipmap-xxhdpi/qrcode_default_grid_scan_line.png differ diff --git a/lib_qrcode/src/main/res/mipmap-xxhdpi/qrcode_default_scan_line.png b/lib_qrcode/src/main/res/mipmap-xxhdpi/qrcode_default_scan_line.png new file mode 100644 index 0000000..6b17808 Binary files /dev/null and b/lib_qrcode/src/main/res/mipmap-xxhdpi/qrcode_default_scan_line.png differ diff --git a/lib_qrcode/src/main/res/values/qrcode_attrs.xml b/lib_qrcode/src/main/res/values/qrcode_attrs.xml new file mode 100644 index 0000000..78b7bde --- /dev/null +++ b/lib_qrcode/src/main/res/values/qrcode_attrs.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib_social/.gitignore b/lib_social/.gitignore new file mode 100644 index 0000000..c06fb85 --- /dev/null +++ b/lib_social/.gitignore @@ -0,0 +1,22 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +*.iml +.idea/ +.gradle +/local.properties +.DS_Store +/build +/captures +*.apk +*.ap_ +*.dex +*.class +bin/ +gen/ +local.properties \ No newline at end of file diff --git a/lib_social/README.md b/lib_social/README.md new file mode 100644 index 0000000..9d75756 --- /dev/null +++ b/lib_social/README.md @@ -0,0 +1,58 @@ +# 说明 + +## 微信登录 + +微信登录需要在`包名.wxapi` 包中添加一个名为 `WXEntryActivity` 的 Activity,并继承该 module 提供的 `AbsWXEntryActivity`。 + +```java +/** + * 微信分享、登录回调 + * + * @author Ztiany + */ +@SuppressWarnings("all") +public class WXEntryActivity extends AbsWXEntryActivity { + + +} +``` + +manifest 配置参考 + +```xml + + +``` + +## 微信支付 + +微信支付需要在`包名.wxapi` 包中添加一个名为 `WXPayEntryActivity` 的 Activity,并继承该 module 提供的 `AbsWeChatPayEntryActivity`。 + +```java +/** + * 微信分享、登录回调 + * + * @author Ztiany + */ +@SuppressWarnings("all") +public class WXPayEntryActivity extends AbsWeChatPayEntryActivity { + +} +``` + +manifest 配置参考 + +```xml + + +``` + +## 支付宝支付 + +- sdk 版本:alipaySdk-15.6.2-20190416165100-noUtdid \ No newline at end of file diff --git a/lib_social/build.gradle b/lib_social/build.gradle new file mode 100644 index 0000000..d2905cd --- /dev/null +++ b/lib_social/build.gradle @@ -0,0 +1,57 @@ +apply plugin: 'com.android.library' + +android { + + compileSdkVersion rootProject.compileSdkVersion + buildToolsVersion rootProject.buildToolsVersion + + defaultConfig { + + minSdkVersion rootProject.minSdkVersion + targetSdkVersion rootProject.targetSdkVersion + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + sourceSets { + main { + jniLibs.srcDirs = ['libs'] + } + } + + lintOptions { + abortOnError false + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + +} + +dependencies { + api fileTree(dir: 'libs', include: ['*.jar', '*.aar']) + implementation androidLibraries.lifecycleExtensions + implementation androidLibraries.androidCompatV4 + implementation androidLibraries.androidCompatV7 + implementation androidLibraries.androidAnnotations + implementation androidLibraries.lifecycle + implementation androidLibraries.lifecycleJava8 + implementation androidLibraries.lifecycleExtensions + implementation thirdLibraries.rxJava + implementation thirdLibraries.retrofit + implementation thirdLibraries.retrofitConverterGson + implementation thirdLibraries.okHttp + implementation thirdLibraries.gson + implementation thirdLibraries.retrofitRxJava2CallAdapter + implementation thirdLibraries.timber + api 'com.tencent.mm.opensdk:wechat-sdk-android-without-mta:5.1.6' +} diff --git a/lib_social/libs/alipaySdk.aar b/lib_social/libs/alipaySdk.aar new file mode 100644 index 0000000..b092a8c Binary files /dev/null and b/lib_social/libs/alipaySdk.aar differ diff --git a/lib_social/proguard-rules.pro b/lib_social/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/lib_social/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/lib_social/src/main/AndroidManifest.xml b/lib_social/src/main/AndroidManifest.xml new file mode 100644 index 0000000..93854f7 --- /dev/null +++ b/lib_social/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + diff --git a/lib_social/src/main/java/com/android/sdk/social/ali/AliPayExecutor.java b/lib_social/src/main/java/com/android/sdk/social/ali/AliPayExecutor.java new file mode 100644 index 0000000..83b8184 --- /dev/null +++ b/lib_social/src/main/java/com/android/sdk/social/ali/AliPayExecutor.java @@ -0,0 +1,132 @@ +package com.android.sdk.social.ali; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.text.TextUtils; + +import com.alipay.sdk.app.PayTask; + +import org.reactivestreams.Subscriber; + +import java.util.Map; + +import io.reactivex.Observable; +import io.reactivex.ObservableEmitter; +import io.reactivex.functions.Consumer; + +/** + * 支付宝支付执行器 + *
    + *     支付宝SDK版本:2016.01.20
    + * 
    + * + * @author Ztiany + */ +public class AliPayExecutor { + + private static final int PAY_RESULT_SUCCESS = 1; + private static final int PAY_RESULT_CANCEL = 2; + private static final int PAY_RESULT_FAIL = 3; + private static final int PAY_RESULT_WAIT_CONFIRM = 4; + + public static Observable doAliPay(final Activity activity, final String sign) { + return Observable.create( + (ObservableEmitter subscriber) -> { + if (subscriber.isDisposed()) { + return; + } + try { + PayTask payTask = new PayTask(activity); + Map pay = payTask.payV2(sign, false);//不要出现丑陋AliPay对话框-_-! + subscriber.onNext(new AliPayResult(pay)); + subscriber.onComplete(); + } catch (Exception e) { + subscriber.onError(e); + } + }); + } + + /** + * 检测是否安装支付宝 + */ + public static boolean isAliPayInstalled(Context context) { + Uri uri = Uri.parse("alipays://platformapi/startApp"); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + ComponentName componentName = intent.resolveActivity(context.getPackageManager()); + return componentName != null; + } + + /** + * 同步返回的结果必须放置到服务端进行验证(验证的规则请看https://doc.open.alipay.com/doc2/ + * detail.htm?spm=0.0.0.0.xdvAU6&treeId=59&articleId=103665& + * docType=1) 建议商户依赖异步通知 + */ + private static Integer parseResult(AliPayResult payResult) { + //String resultInfo = payResult.getResult();// 同步返回需要验证的信息 + String resultStatus = payResult.getResultStatus(); + // 判断resultStatus 为“9000”则代表支付成功,具体状态码代表含义可参考接口文档 + if (TextUtils.equals(resultStatus, "9000")) { + return PAY_RESULT_SUCCESS; + } else { + // 判断resultStatus 为非"9000"则代表可能支付失败 + // "8000"代表支付结果因为支付渠道原因或者系统原因还在等待支付结果确认,最终交易是否成功以服务端异步通知为准(小概率状态) + if (TextUtils.equals(resultStatus, "8000")) { + //Toast.makeText(PayDemoActivity.this, "支付结果确认中", Toast.LENGTH_SHORT).show(); + return PAY_RESULT_WAIT_CONFIRM; + } else if (TextUtils.equals(resultStatus, "6001")) { + return PAY_RESULT_CANCEL; + } else { + // 其他值就可以判断为支付失败,或者系统返回的错误 + return PAY_RESULT_FAIL; + } + } + } + + public static class PayConsumer implements Consumer { + PayResultCallback payResultCallback; + + public PayConsumer(PayResultCallback payResultCallback) { + this.payResultCallback = payResultCallback; + } + + @Override + public void accept(AliPayResult aliPayResult) { + int result = parseResult(aliPayResult); + if (result == PAY_RESULT_CANCEL) { + payResultCallback.onPayCancel(); + } else if (result == PAY_RESULT_FAIL) { + payResultCallback.onPayFail(aliPayResult.getMemo()); + } else if (result == PAY_RESULT_SUCCESS) { + payResultCallback.onPaySuccess(); + } else if (result == PAY_RESULT_WAIT_CONFIRM) { + payResultCallback.onPayNeedConfirmResult(); + } + } + } + + @SuppressWarnings("unused") + public abstract static class PaySubscriber implements PayResultCallback, Subscriber { + + @Override + public void onComplete() { + } + + @Override + public final void onNext(AliPayResult aliPayResult) { + int result = parseResult(aliPayResult); + if (result == PAY_RESULT_CANCEL) { + onPayCancel(); + } else if (result == PAY_RESULT_FAIL) { + onPayFail(aliPayResult.getMemo()); + } else if (result == PAY_RESULT_SUCCESS) { + onPaySuccess(); + } else if (result == PAY_RESULT_WAIT_CONFIRM) { + onPayNeedConfirmResult(); + } + } + } + +} diff --git a/lib_social/src/main/java/com/android/sdk/social/ali/AliPayResult.java b/lib_social/src/main/java/com/android/sdk/social/ali/AliPayResult.java new file mode 100644 index 0000000..416e546 --- /dev/null +++ b/lib_social/src/main/java/com/android/sdk/social/ali/AliPayResult.java @@ -0,0 +1,85 @@ +package com.android.sdk.social.ali; + +import android.support.annotation.NonNull; +import android.text.TextUtils; + +import java.util.Map; + +@SuppressWarnings("unused,WeakerAccess") +public class AliPayResult { + + private String resultStatus; + private String result; + private String memo; + + AliPayResult(String rawResult) { + + if (TextUtils.isEmpty(rawResult)) { + return; + } + + String[] resultParams = rawResult.split(";"); + for (String resultParam : resultParams) { + if (resultParam.startsWith("resultStatus")) { + resultStatus = gatValue(resultParam, "resultStatus"); + } + if (resultParam.startsWith("result")) { + result = gatValue(resultParam, "result"); + } + if (resultParam.startsWith("memo")) { + memo = gatValue(resultParam, "memo"); + } + } + } + + AliPayResult(Map rawResult) { + + if (rawResult == null) { + return; + } + for (String key : rawResult.keySet()) { + if (TextUtils.equals(key, "resultStatus")) { + resultStatus = rawResult.get(key); + } else if (TextUtils.equals(key, "result")) { + result = rawResult.get(key); + } else if (TextUtils.equals(key, "memo")) { + memo = rawResult.get(key); + } + } + } + + @NonNull + @Override + public String toString() { + return "resultStatus={" + resultStatus + "};memo={" + memo + + "};result={" + result + "}"; + } + + private String gatValue(String content, String key) { + String prefix = key + "={"; + return content.substring(content.indexOf(prefix) + prefix.length(), + content.lastIndexOf("}")); + } + + /** + * @return the resultStatus + */ + String getResultStatus() { + return resultStatus; + } + + /** + * @return the memo + */ + public String getMemo() { + return memo; + } + + /** + * @return the result + */ + public String getResult() { + return result; + } + +} diff --git a/lib_social/src/main/java/com/android/sdk/social/ali/PayResultCallback.java b/lib_social/src/main/java/com/android/sdk/social/ali/PayResultCallback.java new file mode 100644 index 0000000..d9bdf0f --- /dev/null +++ b/lib_social/src/main/java/com/android/sdk/social/ali/PayResultCallback.java @@ -0,0 +1,14 @@ +package com.android.sdk.social.ali; + + +public interface PayResultCallback { + + void onPayCancel(); + + void onPayFail(String errStr); + + void onPaySuccess(); + + void onPayNeedConfirmResult(); + +} diff --git a/lib_social/src/main/java/com/android/sdk/social/common/Status.java b/lib_social/src/main/java/com/android/sdk/social/common/Status.java new file mode 100644 index 0000000..735e2a3 --- /dev/null +++ b/lib_social/src/main/java/com/android/sdk/social/common/Status.java @@ -0,0 +1,97 @@ +package com.android.sdk.social.common; + +import android.support.annotation.NonNull; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-07 17:42 + */ +@SuppressWarnings("WeakerAccess,unused") +public class Status { + + public static final int STATE_SUCCESS = 0; + public static final int STATE_FAILED = 1; + public static final int STATE_CANCEL = 2; + public static final int STATE_REQUESTING = 3; + + private final T result; + private final Throwable t; + private final int status; + + private Status(T result, Throwable t, int status) { + this.result = result; + this.t = t; + this.status = status; + } + + public int getStatus() { + return status; + } + + public boolean isRequesting() { + return status == STATE_REQUESTING; + } + + public boolean isError() { + return status == STATE_FAILED; + } + + public boolean isSuccess() { + return status == STATE_SUCCESS; + } + + public boolean isCancel() { + return status == STATE_CANCEL; + } + + public boolean hasData() { + return result != null; + } + + public T getResult() { + return result; + } + + public T getResultOrElse(T whenNull) { + if (result == null) { + return whenNull; + } + return result; + } + + public Throwable getError() { + return t; + } + + public static Status success(T t) { + return new Status<>(t, null, STATE_SUCCESS); + } + + public static Status success() { + return new Status<>(null, null, STATE_SUCCESS); + } + + public static Status error(Throwable throwable) { + return new Status<>(null, throwable, STATE_FAILED); + } + + public static Status loading() { + return new Status<>(null, null, STATE_REQUESTING); + } + + public static Status cancel() { + return new Status<>(null, null, STATE_CANCEL); + } + + @NonNull + @Override + public String toString() { + return "Status{" + + "result=" + result + + ", t=" + t + + ", status=" + status + + '}'; + } + +} diff --git a/lib_social/src/main/java/com/android/sdk/social/common/Utils.java b/lib_social/src/main/java/com/android/sdk/social/common/Utils.java new file mode 100644 index 0000000..f2d7305 --- /dev/null +++ b/lib_social/src/main/java/com/android/sdk/social/common/Utils.java @@ -0,0 +1,17 @@ +package com.android.sdk.social.common; + +import android.text.TextUtils; + + +public class Utils { + + private Utils() { + } + + public static void requestNotNull(String str, String errorMsg) { + if (TextUtils.isEmpty(str)) { + throw new NullPointerException(errorMsg); + } + } + +} diff --git a/lib_social/src/main/java/com/android/sdk/social/wechat/AbsWXEntryActivity.java b/lib_social/src/main/java/com/android/sdk/social/wechat/AbsWXEntryActivity.java new file mode 100644 index 0000000..7448883 --- /dev/null +++ b/lib_social/src/main/java/com/android/sdk/social/wechat/AbsWXEntryActivity.java @@ -0,0 +1,58 @@ +package com.android.sdk.social.wechat; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; + +import com.tencent.mm.opensdk.modelbase.BaseReq; +import com.tencent.mm.opensdk.modelbase.BaseResp; +import com.tencent.mm.opensdk.openapi.IWXAPIEventHandler; + +import timber.log.Timber; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-07 13:51 + */ +public class AbsWXEntryActivity extends Activity implements IWXAPIEventHandler { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Timber.d("onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); + // 如果分享的时候,该界面没有开启,那么微信开始这个 Activity 时会调用 onCreate,所以这里要处理微信的返回结果。 + // 注意:第三方开发者如果使用透明界面来实现 WXEntryActivity,则需要判断 handleIntent 的返回值。 + // 如果返回值为false,则说明入参不合法未被 SDK 处理,应finish当前透明界面,避免外部通过传递非法参数的 Intent 导致停留在透明界面,引起用户的疑惑。 + boolean result = WeChatManager.handleIntent(getIntent(), this); + Timber.w("onCreate handleIntent result = " + result); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + Timber.d("onNewIntent() called with: intent = [" + intent + "]"); + setIntent(intent); + boolean result = WeChatManager.handleIntent(intent, this); + Timber.w("onCreate onNewIntent result = " + result); + } + + /** + * 微信发送请求到第三方应用时,会回调到该方法 + */ + @Override + public void onReq(BaseReq baseReq) { + Timber.d("onReq() called with: baseReq = [" + baseReq + "]"); + } + + /** + * 第三方应用发送到微信的请求处理后的响应结果,会回调到该方法 + */ + @Override + public void onResp(BaseResp baseResp) { + Timber.d("onResp() called with: baseResp = [" + baseResp + "]"); + WeChatManager.handleOnWxEntryResp(baseResp); + this.finish(); + } + +} diff --git a/lib_social/src/main/java/com/android/sdk/social/wechat/AbsWXPayEntryActivity.java b/lib_social/src/main/java/com/android/sdk/social/wechat/AbsWXPayEntryActivity.java new file mode 100644 index 0000000..5364fe1 --- /dev/null +++ b/lib_social/src/main/java/com/android/sdk/social/wechat/AbsWXPayEntryActivity.java @@ -0,0 +1,41 @@ +package com.android.sdk.social.wechat; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; + +import com.tencent.mm.opensdk.modelbase.BaseReq; +import com.tencent.mm.opensdk.modelbase.BaseResp; +import com.tencent.mm.opensdk.openapi.IWXAPIEventHandler; + +public abstract class AbsWXPayEntryActivity extends Activity implements IWXAPIEventHandler { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + WeChatManager.handleIntent(getIntent(), this); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + WeChatManager.handleIntent(getIntent(), this); + } + + @Override + public void onReq(BaseReq baseReq) { + } + + @Override + public void onResp(BaseResp baseResp) { + WeChatManager.handleOnWxEntryResp(baseResp); + this.finish(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + } + +} diff --git a/lib_social/src/main/java/com/android/sdk/social/wechat/AuthResult.java b/lib_social/src/main/java/com/android/sdk/social/wechat/AuthResult.java new file mode 100644 index 0000000..d01883d --- /dev/null +++ b/lib_social/src/main/java/com/android/sdk/social/wechat/AuthResult.java @@ -0,0 +1,32 @@ +package com.android.sdk.social.wechat; + +import android.support.annotation.NonNull; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-07 15:17 + */ +@SuppressWarnings("unused") +public class AuthResult { + + private int errcode; + private String errmsg; + + int getErrcode() { + return errcode; + } + + String getErrmsg() { + return errmsg; + } + + @NonNull + @Override + public String toString() { + return "AuthResult{" + + "errcode=" + errcode + + ", errmsg='" + errmsg + '\'' + + '}'; + } +} diff --git a/lib_social/src/main/java/com/android/sdk/social/wechat/PayInfo.java b/lib_social/src/main/java/com/android/sdk/social/wechat/PayInfo.java new file mode 100644 index 0000000..54b93c9 --- /dev/null +++ b/lib_social/src/main/java/com/android/sdk/social/wechat/PayInfo.java @@ -0,0 +1,86 @@ +package com.android.sdk.social.wechat; + + +import android.support.annotation.NonNull; + +@SuppressWarnings("unused,WeakerAccess") +public class PayInfo { + + private String mAppId; + private String mPartnerId; + private String mPrepayId; + private String mPackage; + private String mNonceStr; + private String mTimestamp; + private String mSign; + + public String getAppId() { + return mAppId; + } + + public void setAppId(String appId) { + mAppId = appId; + } + + public String getPartnerId() { + return mPartnerId; + } + + public void setPartnerId(String partnerId) { + mPartnerId = partnerId; + } + + public String getPrepayId() { + return mPrepayId; + } + + public void setPrepayId(String prepayId) { + mPrepayId = prepayId; + } + + public String getPackage() { + return mPackage; + } + + public void setPackage(String aPackage) { + mPackage = aPackage; + } + + public String getNonceStr() { + return mNonceStr; + } + + public void setNonceStr(String nonceStr) { + mNonceStr = nonceStr; + } + + public String getTimestamp() { + return mTimestamp; + } + + public void setTimestamp(String timestamp) { + mTimestamp = timestamp; + } + + public String getSign() { + return mSign; + } + + public void setSign(String sign) { + mSign = sign; + } + + @NonNull + @Override + public String toString() { + return "PayInfo{" + + "mAppId='" + mAppId + '\'' + + ", mPartnerId='" + mPartnerId + '\'' + + ", mPrepayId='" + mPrepayId + '\'' + + ", mPackage='" + mPackage + '\'' + + ", mNonceStr='" + mNonceStr + '\'' + + ", mTimestamp='" + mTimestamp + '\'' + + ", mSign='" + mSign + '\'' + + '}'; + } +} diff --git a/lib_social/src/main/java/com/android/sdk/social/wechat/SingleLiveData.java b/lib_social/src/main/java/com/android/sdk/social/wechat/SingleLiveData.java new file mode 100644 index 0000000..3f03d46 --- /dev/null +++ b/lib_social/src/main/java/com/android/sdk/social/wechat/SingleLiveData.java @@ -0,0 +1,42 @@ +package com.android.sdk.social.wechat; + +import android.arch.lifecycle.LifecycleOwner; +import android.arch.lifecycle.MediatorLiveData; +import android.arch.lifecycle.Observer; +import android.support.annotation.NonNull; + +class SingleLiveData extends MediatorLiveData { + + private int mVersion = 0; + + @Override + public void observe(@NonNull LifecycleOwner owner, @NonNull Observer observer) { + + final int observerVersion = mVersion; + + super.observe(owner, t -> { + if (observerVersion < mVersion) { + observer.onChanged(t); + } + }); + } + + @Override + public void observeForever(@NonNull Observer observer) { + + final int observerVersion = mVersion; + + super.observeForever(t -> { + if (observerVersion < mVersion) { + observer.onChanged(t); + } + }); + } + + @Override + public void setValue(T value) { + mVersion++; + super.setValue(value); + } + +} \ No newline at end of file diff --git a/lib_social/src/main/java/com/android/sdk/social/wechat/WXApiFactory.java b/lib_social/src/main/java/com/android/sdk/social/wechat/WXApiFactory.java new file mode 100644 index 0000000..1693efc --- /dev/null +++ b/lib_social/src/main/java/com/android/sdk/social/wechat/WXApiFactory.java @@ -0,0 +1,45 @@ +package com.android.sdk.social.wechat; + +import io.reactivex.Observable; +import io.reactivex.schedulers.Schedulers; +import retrofit2.Retrofit; +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; +import retrofit2.converter.gson.GsonConverterFactory; +import retrofit2.http.GET; +import retrofit2.http.Query; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-07 14:50 + */ +class WXApiFactory { + + static ServiceApi createWXApi() { + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://api.weixin.qq.com/sns/") + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io())) + .build(); + + return retrofit.create(ServiceApi.class); + } + + interface ServiceApi { + + //获取openid、accessToken值用于后期操作 + @GET("oauth2/access_token") + Observable getAccessToken(@Query("appid") String appId, @Query("secret") String appSecret, @Query("code") String code, @Query("grant_type") String granType); + + //检验授权凭证(access_token)是否有效 + @GET("auth") + Observable validateToken(@Query("access_token") String access_token, @Query("openid") String openid); + + //获取用户个人信息 + @GET("userinfo") + Observable getWeChatUser(@Query("access_token") String access_token, @Query("openid") String openid); + + } + + +} diff --git a/lib_social/src/main/java/com/android/sdk/social/wechat/WXToken.java b/lib_social/src/main/java/com/android/sdk/social/wechat/WXToken.java new file mode 100644 index 0000000..5d7141d --- /dev/null +++ b/lib_social/src/main/java/com/android/sdk/social/wechat/WXToken.java @@ -0,0 +1,40 @@ +package com.android.sdk.social.wechat; + +import android.support.annotation.NonNull; + +@SuppressWarnings("unused") +class WXToken extends AuthResult { + + private String access_token; + private String expires_in; + private String refresh_token; + private String openid; + private String scope; + private String unionid; + + String getAccess_token() { + return access_token; + } + + public String getRefresh_token() { + return refresh_token; + } + + String getOpenid() { + return openid; + } + + @NonNull + @Override + public String toString() { + return "WXToken{" + + "access_token='" + access_token + '\'' + + ", expires_in='" + expires_in + '\'' + + ", refresh_token='" + refresh_token + '\'' + + ", openid='" + openid + '\'' + + ", scope='" + scope + '\'' + + ", unionid='" + unionid + '\'' + + '}'; + } + +} \ No newline at end of file diff --git a/lib_social/src/main/java/com/android/sdk/social/wechat/WXUser.java b/lib_social/src/main/java/com/android/sdk/social/wechat/WXUser.java new file mode 100644 index 0000000..f637e2b --- /dev/null +++ b/lib_social/src/main/java/com/android/sdk/social/wechat/WXUser.java @@ -0,0 +1,92 @@ +package com.android.sdk.social.wechat; + +import android.support.annotation.NonNull; + +import java.util.List; + +@SuppressWarnings("unused") +public class WXUser extends AuthResult { + + /** + * 普通用户的标识,对当前开发者帐号唯一 + */ + private String openid; + + private String nickname; + + /** + * 普通用户性别,1为男性,2为女性 + */ + private int sex; + + private String province; + private String city; + private String country; + + /** + * 用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空 + */ + private String headimgurl; + + /** + * 开发者最好保存unionID信息,以便以后在不同应用之间进行用户信息互通。 + */ + private String unionid; + + private List privilege; + + public String getOpenid() { + return openid; + } + + public String getNickname() { + return nickname; + } + + public int getSex() { + return sex; + } + + public String getProvince() { + return province; + } + + public String getCity() { + return city; + } + + public String getCountry() { + return country; + } + + public String getHeadimgurl() { + return headimgurl; + } + + public String getUnionid() { + return unionid; + } + + public List getPrivilege() { + return privilege; + } + + @NonNull + @Override + public String toString() { + return "WXUser{" + + "openid='" + openid + '\'' + + ", nickname='" + nickname + '\'' + + ", sex=" + sex + + ", province='" + province + '\'' + + ", city='" + city + '\'' + + ", country='" + country + '\'' + + ", headimgurl='" + headimgurl + '\'' + + ", unionid='" + unionid + '\'' + + ", privilege=" + privilege + + ", errcode=" + getErrcode() + + ", errmsg=" + getErrmsg() + + '}'; + } + +} \ No newline at end of file diff --git a/lib_social/src/main/java/com/android/sdk/social/wechat/WeChatLoginException.java b/lib_social/src/main/java/com/android/sdk/social/wechat/WeChatLoginException.java new file mode 100644 index 0000000..7c6a8c2 --- /dev/null +++ b/lib_social/src/main/java/com/android/sdk/social/wechat/WeChatLoginException.java @@ -0,0 +1,56 @@ +package com.android.sdk.social.wechat; + +import android.support.annotation.NonNull; + +import com.tencent.mm.opensdk.modelbase.BaseResp; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-07 17:35 + */ +class WeChatLoginException extends Exception { + + private int mErrorCode; + private String mErrMsg; + + WeChatLoginException(int errorCode, String errMsg) { + mErrorCode = errorCode; + mErrMsg = errMsg; + } + + @Override + public String getMessage() { + return getMessageFormBaseResp(); + } + + @NonNull + @Override + public String toString() { + return getMessageFormBaseResp() + "---" + super.toString(); + } + + private String getMessageFormBaseResp() { + String message = "未知错误"; + + switch (mErrorCode) { + case BaseResp.ErrCode.ERR_USER_CANCEL: + message = "发送取消"; + break; + case BaseResp.ErrCode.ERR_SENT_FAILED: + message = "发送失败"; + break; + case BaseResp.ErrCode.ERR_AUTH_DENIED: + message = "发送被拒绝"; + break; + case BaseResp.ErrCode.ERR_UNSUPPORT: + message = "不支持错误"; + break; + case BaseResp.ErrCode.ERR_COMM: + message = "一般错误"; + break; + } + return message + " errMsg = " + mErrMsg; + } + +} diff --git a/lib_social/src/main/java/com/android/sdk/social/wechat/WeChatLoginImpl.java b/lib_social/src/main/java/com/android/sdk/social/wechat/WeChatLoginImpl.java new file mode 100644 index 0000000..e1654a3 --- /dev/null +++ b/lib_social/src/main/java/com/android/sdk/social/wechat/WeChatLoginImpl.java @@ -0,0 +1,24 @@ +package com.android.sdk.social.wechat; + + +import com.tencent.mm.opensdk.modelmsg.SendAuth; + +import io.reactivex.Observable; +import io.reactivex.ObservableSource; +import io.reactivex.functions.Function; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-07 14:36 + */ +class WeChatLoginImpl { + + static Observable doWeChatLogin(SendAuth.Resp resp) { + final WXApiFactory.ServiceApi serviceApi = WXApiFactory.createWXApi(); + return serviceApi + .getAccessToken(WeChatManager.getAppId(), WeChatManager.getAppSecret(), resp.code, "authorization_code"/*固定参数*/) + .flatMap((Function>) wxToken -> serviceApi.getWeChatUser(wxToken.getAccess_token(), wxToken.getOpenid())); + } + +} diff --git a/lib_social/src/main/java/com/android/sdk/social/wechat/WeChatManager.java b/lib_social/src/main/java/com/android/sdk/social/wechat/WeChatManager.java new file mode 100644 index 0000000..598ce14 --- /dev/null +++ b/lib_social/src/main/java/com/android/sdk/social/wechat/WeChatManager.java @@ -0,0 +1,291 @@ +package com.android.sdk.social.wechat; + +import android.annotation.SuppressLint; +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.MutableLiveData; +import android.content.Context; +import android.content.Intent; +import android.support.annotation.NonNull; +import android.text.TextUtils; + +import com.android.sdk.social.common.Status; +import com.android.sdk.social.common.Utils; +import com.tencent.mm.opensdk.constants.ConstantsAPI; +import com.tencent.mm.opensdk.modelbase.BaseResp; +import com.tencent.mm.opensdk.modelbiz.WXLaunchMiniProgram; +import com.tencent.mm.opensdk.modelmsg.SendAuth; +import com.tencent.mm.opensdk.modelpay.PayReq; +import com.tencent.mm.opensdk.openapi.IWXAPI; +import com.tencent.mm.opensdk.openapi.IWXAPIEventHandler; +import com.tencent.mm.opensdk.openapi.WXAPIFactory; + +import timber.log.Timber; + + +@SuppressWarnings("unused") +public class WeChatManager { + + private static WeChatManager sWeChatManager; + private final IWXAPI mWxApi; + + private static String sAppId; + private static String sAppSecret; + + public static synchronized void initWeChatSDK(Context context, String appId, String appSecret) { + if (sWeChatManager != null) { + throw new UnsupportedOperationException("WeChatManager has already been initialized"); + } + sAppId = appId; + sAppSecret = appSecret; + sWeChatManager = new WeChatManager(context); + } + + private static synchronized void destroy() { + sWeChatManager.currentState = null; + sWeChatManager.mWxApi.unregisterApp(); + sWeChatManager.mWxApi.detach(); + sWeChatManager = null; + } + + static String getAppId() { + Utils.requestNotNull(sAppId, "weChat app id"); + return sAppId; + } + + static String getAppSecret() { + Utils.requestNotNull(sAppSecret, "weChat appSecret"); + return sAppSecret; + } + + private WeChatManager(Context context) { + mWxApi = WXAPIFactory.createWXAPI(context.getApplicationContext(), getAppId(), false); + mWxApi.registerApp(getAppId()); + sWeChatManager = this; + } + + public static synchronized WeChatManager getInstance() { + if (sWeChatManager == null) { + throw new UnsupportedOperationException("WeChatManager has not been initialized"); + } + return sWeChatManager; + } + + /////////////////////////////////////////////////////////////////////////// + // 通用 + /////////////////////////////////////////////////////////////////////////// + + static boolean handleIntent(Intent intent, IWXAPIEventHandler iwxapiEventHandler) { + WeChatManager weChatManager = sWeChatManager; + if (weChatManager != null) { + return weChatManager.mWxApi.handleIntent(intent, iwxapiEventHandler); + } else { + Timber.w("WeChatManager handleIntent called, but WeChatManager has not been initialized"); + } + return false; + } + + @SuppressWarnings("unused") + public boolean isInstalledWeChat() { + return mWxApi.isWXAppInstalled(); + } + + static void handleOnWxEntryResp(BaseResp baseResp) { + Timber.d("baseResp.type = " + baseResp.getType()); + if (ConstantsAPI.COMMAND_SENDAUTH == baseResp.getType()) { + handAuthResp(baseResp); + } else if (ConstantsAPI.COMMAND_PAY_BY_WX == baseResp.getType()) { + handleOnWxEntryPayResp(baseResp); + } else if (ConstantsAPI.COMMAND_LAUNCH_WX_MINIPROGRAM == baseResp.getType()) { + handleMiniProgramResp(baseResp); + } + } + + /////////////////////////////////////////////////////////////////////////// + // 小程序 + /////////////////////////////////////////////////////////////////////////// + + /** + * 跳转小程序 + * + * @param userName 小程序原始id + * @param path 拉起小程序页面的可带参路径,不填默认拉起小程序首页 + */ + public void navToMinProgram(String userName, String path) { + WXLaunchMiniProgram.Req req = new WXLaunchMiniProgram.Req(); + req.userName = userName; + req.path = path; + // 可选打开开发版,体验版和正式版 + req.miniprogramType = WXLaunchMiniProgram.Req.MINIPTOGRAM_TYPE_RELEASE; + mWxApi.sendReq(req); + } + + private static void handleMiniProgramResp(BaseResp baseResp) { + WXLaunchMiniProgram.Resp launchMiniProResp = (WXLaunchMiniProgram.Resp) baseResp; + //对应小程序组件