[Modify]安卓10以上适配加入基础库

This commit is contained in:
wushaocheng
2023-04-13 18:47:53 +08:00
parent c678061633
commit 684bd5260d
252 changed files with 26889 additions and 10 deletions

View File

@@ -48,6 +48,9 @@ import com.netease.nimlib.sdk.msg.constant.MsgTypeEnum;
import com.netease.nimlib.sdk.msg.model.IMMessage;
import com.netease.nimlib.sdk.util.NIMUtil;
import com.nnbc123.app.qiyukefu.CustomerServerHelper;
import com.nnbc123.library.common.application.BaseApp;
import com.nnbc123.library.common.file.FileHelper;
import com.nnbc123.library.utils.ResUtil;
import com.opensource.svgaplayer.SVGAParser;
import com.orhanobut.logger.AndroidLogAdapter;
import com.orhanobut.logger.Logger;
@@ -127,7 +130,7 @@ import io.realm.RealmConfiguration;
* @date 2017/2/11
*/
public class XChatApplication extends Application {
public class XChatApplication extends BaseApp {
public static final String TAG = "XChatApplication";
private static final MessageNotifierCustomization messageNotifierCustomization = new MessageNotifierCustomization() {
@Override
@@ -391,7 +394,7 @@ public class XChatApplication extends Application {
// 如果第三方 APP 需要缓存清理功能, 清理这个目录下面个子目录的内容即可。
String sdkPath = null;
try {
sdkPath = Environment.getExternalStorageDirectory() + "/" + instance.getPackageName() + "/nim";
sdkPath = FileHelper.getRootCacheDir().getPath() + "/nim";
} catch (ArrayIndexOutOfBoundsException e) {
}
@@ -620,12 +623,29 @@ public class XChatApplication extends Application {
public void onCreate() {
super.onCreate();
instance = this;
BaseApp.init(this);
BasicConfig.INSTANCE.setAppContext(this.getApplicationContext());
SharedPreferenceUtils.init(this);
ResUtil.init(this);
boolean isShowPrivacyAgreement = (boolean) SharedPreferenceUtils.get(SplashActivity.SHOW_PRIVACY_AGREEMENT, true);
if (!isShowPrivacyAgreement) {
initOtherSDK();
}
initContext(this);
}
/**
* 初始化Application实例
*/
public void initContext(Application application) {
gContext = application;
}
/**
* @return 获取Application实例
*/
public static Application getApplication() {
return gContext;
}
@Override

View File

@@ -23,6 +23,29 @@ android {
enabled = true
}
sourceSets {
main {
java.srcDirs = [
'src/main/java',
'src/module_easypermission/java',
'src/module_luban/java',
'src/module_easyphoto/java',
'src/module_common/java',
]
res.srcDirs = [
'src/main/res',
'src/module_easypermission/res',
'src/module_easyphoto/res',
'src/module_common/res',
]
}
}
buildTypes {
release {
minifyEnabled true
@@ -40,7 +63,7 @@ dependencies {
def glideVersion = "4.11.0"
def retrofitVersion = "2.9.0"
def okhttp3 = "3.14.9"
def okio = "2.2.2"
def okio = "2.8.0"
def rxjava_adapter = "2.3.0"
def rxjava = "2.1.7"
def rxjava_android = "2.0.1"
@@ -49,31 +72,31 @@ dependencies {
def qiniu = "7.3.15"
def SmartRefreshLayoutVersion = "1.0.3"
def eventbusVersion = "3.0.0"
def fragment_version = "1.3.5"
def fragment_version = "1.4.1"
def GlideTransformationsVersion = "3.0.1"
implementation fileTree(dir: 'libs', include: ['*.jar'])
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
api 'androidx.constraintlayout:constraintlayout:2.1.3'
api 'androidx.appcompat:appcompat:1.3.0'
api 'androidx.constraintlayout:constraintlayout:2.1.4'
api 'androidx.appcompat:appcompat:1.4.2'
api 'androidx.recyclerview:recyclerview:1.2.1'
api 'androidx.cardview:cardview:1.0.0'
api 'androidx.gridlayout:gridlayout:1.0.0'
api "androidx.core:core-ktx:1.7.0"
api "androidx.fragment:fragment-ktx:$fragment_version"
api 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
api 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
api 'androidx.lifecycle:lifecycle-extensions:2.2.0'
api 'com.google.android.material:material:1.4.0'
api 'com.google.android.material:material:1.6.1'
api "com.squareup.retrofit2:retrofit:${retrofitVersion}"
api "com.squareup.okhttp3:okhttp:${okhttp3}"
api "com.squareup.okhttp3:logging-interceptor:${okhttp3}"
api "com.squareup.retrofit2:adapter-rxjava2:${rxjava_adapter}"
api 'com.google.code.gson:gson:2.8.9'
api 'com.google.code.gson:gson:2.9.0'
api "com.squareup.okio:okio:${okio}"
api "com.scwang.smartrefresh:SmartRefreshLayout:${SmartRefreshLayoutVersion}"
@@ -98,6 +121,15 @@ dependencies {
api 'io.github.razerdp:BasePopup:3.2.1'
api 'androidx.legacy:legacy-support-v4:1.0.0'
api 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
api 'com.github.chrisbanes:PhotoView:2.3.0'
//mmkv
api 'com.tencent:mmkv:1.2.13'
api "jp.wasabeef:glide-transformations:${GlideTransformationsVersion}"
}
repositories {
mavenCentral()

View File

@@ -0,0 +1,18 @@
package com.nnbc123.library.utils;
import android.app.Application;
import androidx.annotation.StringRes;
public class ResUtil {
private static Application context;
public static void init(Application context) {
ResUtil.context = context;
}
public static String getString(@StringRes int resId) {
return context.getString(resId);
}
}

View File

@@ -0,0 +1,12 @@
package com.nnbc123.library.common;
public class Constants {
//上传的图片 默认大小不能超过大小 640KB
public static final int UPLOAD_IMAGE_MAX_FILE_LENGTH = 640;
//上传的图片 默认宽高最大值 2340
public static final int UPLOAD_IMAGE_MAX_SIZE = 2340;
//上传的gif 默认大小不能超过 1MB
public static final int UPLOAD_GIF_MAX_SIZE = 1 << 20;
}

View File

@@ -0,0 +1,5 @@
package com.nnbc123.library.common
object SpConstants {
const val TAB_INFO_LIST = "tab_info_list"
}

View File

@@ -0,0 +1,38 @@
package com.nnbc123.library.common.application;
import android.app.Application;
import android.content.Context;
/**
* Application的代理类
*/
public abstract class BaseApp extends Application{
private static final String TAG = "BaseApp";
public static Application gContext;
/**
* @return 获取Application上下文对象
*/
public static Context getContext() {
return gContext;
}
/**
* @return 获取Application实例
*/
public static Application getApplication() {
return gContext;
}
public static void init(Application application) {
gContext = application;
}
/**
* debug 环境 受到实验室模式影响
*/
public static boolean isDebug() {
return Env.isDebug();
}
}

View File

@@ -0,0 +1,146 @@
package com.nnbc123.library.common.application;
import com.nnbc123.library.R;
import com.nnbc123.library.utils.ResUtil;
import com.nnbc123.library.utils.config.BasicConfig;
import com.nnbc123.library.utils.pref.CommonPref;
/**
* 环境配置类
*/
public class Env {
public static final String KEY_ENVIRONMENT = "environment";
/**
* 当前环境
*/
private static EnvType mEnvType;
/**
* 真实环境
*/
private static boolean mRealDebug;
private Env() {
}
public enum EnvType {
/**
* 测试环境
*/
Debug(0),
/**
* 待发布环境
*/
Staging(1),
/**
* 线上环境
*/
Release(2);
public int code;
EnvType(int code) {
this.code = code;
}
public static EnvType create(int code) {
EnvType env = null;
if (code == EnvType.Debug.code) {
env = Debug;
} else if (code == EnvType.Staging.code) {
env = Staging;
} else if (code == EnvType.Release.code) {
env = Release;
}
return env;
}
}
/**
* 初始化环境参数
*
* @param defaultEnv 用于初始化最初环境
* @param isRealDebug 判断是否是真的debug模式不受环境影响
*/
public static void initEnv(String defaultEnv, boolean isRealDebug) {
if (defaultEnv == null || defaultEnv.isEmpty()) {
throw new RuntimeException(ResUtil.getString(R.string.android_core_env_01));
}
int environment = CommonPref.instance(BasicConfig.INSTANCE.getAppContext()).getInt(KEY_ENVIRONMENT);
EnvType envType;
if (environment == -1) {
envType = EnvType.valueOf(firstChar2Up(defaultEnv));
changeEnv(envType);
} else {
envType = EnvType.create(environment);
}
if (envType == null) {
throw new RuntimeException(ResUtil.getString(R.string.android_core_env_02));
}
mEnvType = envType;
mRealDebug = isRealDebug;
}
/**
* 第一个字符大写,用于把字符串转成类名
*
* @param s
* @return
*/
private static String firstChar2Up(String s) {
if (s == null || s.length() < 1) {
return null;
}
String newStr = s.substring(0, 1).toUpperCase() + s.substring(1);
return newStr;
}
/**
* 修改偏好设置里面的值
*
* @param env
*/
public static void changeEnv(EnvType env) {
CommonPref.instance(BasicConfig.INSTANCE.getAppContext()).putInt(KEY_ENVIRONMENT, env.code);
}
/**
* 受到环境印象(和实验室有关)
*
* @return
*/
public static boolean isDebug() {
return mEnvType == EnvType.Debug && mRealDebug;
}
/**
* 真实包环境(不受实验室环境影响)
*
* @return
*/
public static boolean isRealDebug() {
return mRealDebug;
}
/**
* 获取当前环境
* @return
*/
public static EnvType getCurrentEnv() {
if (mEnvType == null) {
throw new RuntimeException(ResUtil.getString(R.string.android_core_env_03));
}
return mEnvType;
}
}

View File

@@ -0,0 +1,10 @@
package com.nnbc123.library.common.application;
import android.app.Application;
public interface IAppLifeCycle {
void init(Application application);
}

View File

@@ -0,0 +1,211 @@
package com.nnbc123.library.common.base;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.viewbinding.ViewBinding;
import java.lang.reflect.Field;
import java.util.List;
import com.nnbc123.library.common.fragmentation.ISupportActivity;
import com.nnbc123.library.common.fragmentation.SupportActivityDelegate;
import com.nnbc123.library.common.util.Logger;
import com.nnbc123.library.common.util.ViewBindingUtil;
public abstract class BaseActivity<VB extends ViewBinding> extends AppCompatActivity implements ISupportActivity {
private final String TAG = getClass().getSimpleName();
private final SupportActivityDelegate mActivityDelegate = new SupportActivityDelegate(this);
@Nullable
private VB mViewBinding;
@CallSuper
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
try {
super.onActivityResult(requestCode, resultCode, data);
this.handleFragmentActivityResult(this.getSupportFragmentManager(), requestCode, resultCode, data);
} catch (Exception e) {
Logger.error(TAG, "onActivityResult", e);
}
}
private void handleFragmentActivityResult(FragmentManager fragmentManager, int requestCode, int resultCode, Intent data) {
if (fragmentManager != null) {
List<Fragment> fragmentList = fragmentManager.getFragments();
int size = fragmentList.size();
if (size > 0) {
for (int i = 0; i < size; i++) {
Fragment fragment = fragmentList.get(i);
if (fragment != null) {
fragment.onActivityResult(requestCode, resultCode, data);
this.handleFragmentActivityResult(fragment.getChildFragmentManager(), requestCode, resultCode, data);
}
}
}
}
}
@CallSuper
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.mActivityDelegate.onCreate();
this.initBefore(savedInstanceState);
mViewBinding = ViewBindingUtil.inflateWithActivity(this, getLayoutInflater());
if (mViewBinding != null) {
View mContentView = mViewBinding.getRoot();
mContentView.setTag(BaseViewTag.TAG_NAME, this);
setContentView(mContentView);
}
this.findView();
this.setView();
this.setListener();
}
@CallSuper
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
}
/**
* 建议子类相关操作写在super.onResume()之前
*/
@CallSuper
@Override
protected void onResume() {
try {
super.onResume();
} catch (Exception e) {
callUpActivity();
Logger.error(TAG, "onResume", e);
}
}
/**
* 解决onResume()不明原因导致的java.lang.IllegalArgumentException
* 参考文档https://blog.csdn.net/ahubenkui/article/details/80038381
*/
private void callUpActivity() {
try {
Class<Activity> superClass = Activity.class;
Field field = superClass.getDeclaredField("mCalled");
field.setAccessible(true);
field.setBoolean(this, true);
} catch (Exception e) {
Logger.error(TAG, "callUpActivity", e);
}
}
/**
* 建议子类相关操作写在super.onPause()之前
*/
@CallSuper
@Override
protected void onPause() {
super.onPause();
//当isFinishing()返回为true时表示当前Activity将要销毁但最终不会走Activity#onDestroy()方法。
if (this.isFinishing()) {
this.onWillDestroy();
}
}
@CallSuper
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
try {
super.onSaveInstanceState(outState);
} catch (Exception e) {
Logger.error(TAG, "onSaveInstanceState", e);
}
}
/**
* 不建议子类在Activity#onDestroy()方法里面做各种释放操作因为onDestroy()方法不一定会执行有可能会导致内存泄露请使用onWillDestroy()方法。
* 参考文档https://blog.csdn.net/wangsf1112/article/details/79108856
*/
@CallSuper
@Override
protected void onDestroy() {
super.onDestroy();
this.onWillDestroy();
}
/**
* 建议子类各种释放操作放到onWillDestroy()中方法执行因为Activity#onDestroy()方法不一定会执行,有可能会导致内存泄露。
*/
protected void onWillDestroy() {
}
@Override
public final SupportActivityDelegate getSupportDelegate() {
return this.mActivityDelegate;
}
@Override
public final void onBackPressed() {
this.mActivityDelegate.onBackPressedPage();
}
@Override
public void onBackPressedSupport() {
this.mActivityDelegate.onBackPressedSupport();
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
try {
return super.dispatchTouchEvent(ev);
} catch (Exception e) {
Logger.error(TAG, "dispatchTouchEvent", e);
return false;
}
}
@Nullable
protected final VB getBinding() {
return mViewBinding;
}
////////////////////////////////////////以下是提供给子类复写的方法////////////////////////////////////////
/**
* 该方法是在onCreate()方法里执行在setContentView()方法被调用之前触发可用于处理解析Activity#getIntent()中的数据时的场景
*/
protected void initBefore(@Nullable Bundle savedInstanceState) {
}
/**
* 该方法是在onCreate()方法里执行在setContentView()方法被调用之后触发,可用于处理控件的初始化
*/
protected void findView() {
}
/**
* 该方法是在onCreate()方法里执行在setContentView()方法被调用之后触发,可用于处理控件的加载数据
*/
protected void setView() {
}
/**
* 该方法是在onCreate()方法里执行在setContentView()方法被调用之后触发,可用于处理控件的设置监听器
*/
protected void setListener() {
}
}

View File

@@ -0,0 +1,233 @@
package com.nnbc123.library.common.base;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import androidx.annotation.CallSuper;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import androidx.viewbinding.ViewBinding;
import com.nnbc123.library.common.fragmentation.ISupportActivity;
import com.nnbc123.library.common.fragmentation.ISupportFragment;
import com.nnbc123.library.common.fragmentation.SupportFragmentDelegate;
import com.nnbc123.library.common.fragmentation.windowcallback.WindowCallbackProxyUtil;
import com.nnbc123.library.common.util.ActivityHelper;
import com.nnbc123.library.common.util.Logger;
import com.nnbc123.library.common.util.ViewBindingUtil;
public abstract class BaseDialogFragment<VB extends ViewBinding> extends DialogFragment implements ISupportFragment {
private final String TAG = getClass().getSimpleName();
private final SupportFragmentDelegate mFragmentDelegate = new SupportFragmentDelegate(this);
private View mContentView;
@Nullable
private VB mViewBinding;
private boolean isLoaded = false;
@Override
public void onStart() {
super.onStart();
if (getDialog() != null && getDialog().getWindow() != null) {
Window window = getDialog().getWindow();
Window.Callback callback = WindowCallbackProxyUtil.createWindowCallBack(window.getCallback(), true, this.getClass().getSimpleName());
if (callback != null) {
window.setCallback(callback);
}
}
}
@CallSuper
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (getActivity() == null) {
//这里需要关联一个Activity而不是Context
throw new IllegalStateException("Current class " + getClass().getName() + " not attached to an activity.");
}
if (!(getActivity() instanceof ISupportActivity)) {
//Activity必须是实现了ISupportActivity接口才可以
throw new IllegalStateException(getActivity().getClass().getName() + " must impl ISupportActivity! Current class is " + getClass().getName());
}
this.mFragmentDelegate.onAttach();
}
@CallSuper
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.mFragmentDelegate.onCreate(savedInstanceState);
this.initBefore(savedInstanceState);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mViewBinding = ViewBindingUtil.inflateWithFragment(this, inflater, container);
if (mViewBinding != null) {
this.mContentView = mViewBinding.getRoot();
this.mContentView.setTag(BaseViewTag.TAG_NAME, this);
}
this.findView();
return this.mContentView;
}
@CallSuper
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
this.mFragmentDelegate.onViewCreated();
this.setView();//重写该方法即可实现预加载数据即当Fragment创建布局以后还没有真正可见时就会加载数据不同与onResume()方法和onLazyLoad()方法
this.setListener();
}
@CallSuper
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
this.mFragmentDelegate.onSaveInstanceState(outState);
}
/**
* 重写该方法即可实现每次Fragment可见时都能加载数据不同与setView()方法和onLazyLoad()方法
*/
@CallSuper
@Override
public void onResume() {
super.onResume();
if (!isLoaded && !isHidden()) {
onLazyLoad();
isLoaded = true;
}
}
/**
* 重写该方法可实现懒加载数据即当前Fragment第一次可见时才会执行一次不同与setView()方法和onResume()方法
*/
protected void onLazyLoad() {
}
/**
* 重写该方法即可监听当前Fragment变为不可见时
*/
@CallSuper
@Override
public void onPause() {
super.onPause();
}
@CallSuper
@Override
public void onDestroyView() {
super.onDestroyView();
this.mFragmentDelegate.onDestroyView();
isLoaded = false;
this.onWillDestroy();
}
/**
* 建议子类各种释放操作放到onWillDestroy()方法中执行因为Fragment#onDestroyView()方法一定会执行所以也会触发该方法,在其他方法中处理有可能会导致内存泄露。
*/
@CallSuper
public void onWillDestroy() {
this.mContentView = null;
}
@CallSuper
@Override
public void show(@NonNull FragmentManager manager, @Nullable String tag) {
try {
if (!manager.isDestroyed() && !manager.isStateSaved() && !isAdded()) {
super.show(manager, tag);
}
} catch (Exception e) {
Logger.error(TAG, "show", e);
}
}
@Override
public final void dismiss() {
dismissAllowingStateLoss();
}
@Override
public final void dismissAllowingStateLoss() {
if (ActivityHelper.isCanUse(getActivity())) {
super.dismissAllowingStateLoss();
}
}
@Nullable
@Override
public final View getView() {
return this.mContentView;
}
protected final <T extends View> T findViewById(@IdRes int id) {
if (this.getView() != null) {
return this.getView().findViewById(id);
}
return null;
}
@Override
public final SupportFragmentDelegate getSupportDelegate() {
return this.mFragmentDelegate;
}
@Override
public final void setFragmentResult(int resultCode, Bundle bundle) {
this.mFragmentDelegate.setFragmentResult(resultCode, bundle);
}
@Override
public void onFragmentResult(int requestCode, int resultCode, Bundle data) {
this.mFragmentDelegate.onFragmentResult(requestCode, resultCode, data);
}
@Override
public boolean onBackPressedSupport() {
return this.mFragmentDelegate.onBackPressedSupport();
}
@Nullable
protected final VB getBinding() {
return mViewBinding;
}
////////////////////////////////////////以下是提供给子类复写的方法////////////////////////////////////////
/**
* 该方法是在onCreate()方法里执行在onCreateView()方法被调用之前触发可用于处理解析Fragment#getArguments()中的数据时的场景
*/
protected void initBefore(@Nullable Bundle savedInstanceState) {
}
/**
* 该方法是在onCreateView()方法里触发,可用于处理控件的初始化
*/
protected void findView() {
}
/**
* 该方法是在onViewCreated()方法里触发重写该方法即可实现预加载数据即当Fragment创建布局以后还没有真正可见时就会加载数据不同与onResume()方法和onLazyLoad()方法
*/
protected void setView() {
}
/**
* 该方法是在onViewCreated()方法里触发,可用于处理控件的设置监听器
*/
protected void setListener() {
}
}

View File

@@ -0,0 +1,165 @@
package com.nnbc123.library.common.base;
import android.app.Activity;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.widget.LinearLayout;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.viewbinding.ViewBinding;
import com.nnbc123.library.common.util.ActivityHelper;
import com.nnbc123.library.common.util.ViewBindingUtil;
public abstract class BaseLinearLayout<VB extends ViewBinding> extends LinearLayout implements DefaultLifecycleObserver {
private boolean mViewCreatedCalled = false;
@Nullable
private VB mViewBinding;
public BaseLinearLayout(Context context) {
this(context, null);
}
public BaseLinearLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BaseLinearLayout(Context context, AttributeSet attrs, int defStyle) {
this(context, attrs, defStyle, 0);
}
public BaseLinearLayout(Context context, AttributeSet attrs, int defStyle, int defStyleRes) {
super(context, attrs, defStyle, defStyleRes);
init();
}
private void init() {
mViewBinding = ViewBindingUtil.inflateWithView(this, LayoutInflater.from(getContext()), this);
BaseViewTag.registerLifecycle(this);
findView();
}
@CallSuper
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (!mViewCreatedCalled) {
onLazyLoad();
mViewCreatedCalled = true;
}
}
/**
* 重写该方法可实现懒加载数据,即当前控件第一次可见时才会执行一次,不同于在构造函数里面使用预加载机制直接加载数据的方式
*/
@CallSuper
protected void onLazyLoad() {
setView();
setListener();
}
public final Activity getActivity() {
return ActivityHelper.getActivityFromView(this);
}
@Nullable
protected final VB getBinding() {
return mViewBinding;
}
/**
* 来自于Activity或Fragment的onCreate()方法被回调
*
* @param owner 生命周期提供者一般是Activity或Fragment
*/
@CallSuper
@Override
public void onCreate(@NonNull LifecycleOwner owner) {
}
/**
* 来自于Activity或Fragment的onStart()方法被回调
*
* @param owner 生命周期提供者一般是Activity或Fragment
*/
@CallSuper
@Override
public void onStart(@NonNull LifecycleOwner owner) {
}
/**
* 来自于Activity或Fragment的onResume()方法被回调
*
* @param owner 生命周期提供者一般是Activity或Fragment
*/
@CallSuper
@Override
public void onResume(@NonNull LifecycleOwner owner) {
}
/**
* 来自于Activity或Fragment的onPause()方法被回调
*
* @param owner 生命周期提供者一般是Activity或Fragment
*/
@CallSuper
@Override
public void onPause(@NonNull LifecycleOwner owner) {
}
/**
* 来自于Activity或Fragment的onStop()方法被回调
*
* @param owner 生命周期提供者一般是Activity或Fragment
*/
@CallSuper
@Override
public void onStop(@NonNull LifecycleOwner owner) {
}
/**
* 来自于Activity或Fragment的onDestroy()方法被回调
*
* @param owner 生命周期提供者一般是Activity或Fragment
*/
@CallSuper
@Override
public void onDestroy(@NonNull LifecycleOwner owner) {
BaseViewTag.unRegisterLifecycle(this, owner);
}
////////////////////////////////////////以下是提供给子类复写的方法////////////////////////////////////////
/**
* 该方法是在构造函数里执行,可用于处理控件的初始化
*/
protected void findView() {
}
/**
* 该方法是在onLazyLoad()方法里执行,可用于处理控件的加载数据
*/
protected void setView() {
}
/**
* 该方法是在onLazyLoad()方法里执行,可用于处理控件的设置监听器
*/
protected void setListener() {
}
}

View File

@@ -0,0 +1,119 @@
package com.nnbc123.library.common.base;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import com.nnbc123.library.R;
import com.nnbc123.library.common.util.Logger;
/**
* author: wushaocheng
* time: 2022/11/15
* desc:将View关联上Activity或者Fragment的生命周期
*/
public class BaseViewTag {
private static final String TAG = "BaseViewTag";
public static final int TAG_NAME = R.id.baseViewTagID;
/**
* 注册Activity或者Fragment生命周期监听接口
*
* @param view 需要关联Activity或者Fragment生命周期的控件注意该控件需要实现LifecycleObserver接口
*/
public static void registerLifecycle(View view) {
if (view == null) {
Logger.warn(TAG, "registerLifecycle view is null");
return;
}
if (!(view instanceof LifecycleObserver)) {
Logger.warn(TAG, "registerLifecycle view: " + view.getClass().getSimpleName() + ", view must implements LifecycleObserver");
return;
}
view.post(new Runnable() {
@Override
public void run() {
try {
Object tag = view.getTag(BaseViewTag.TAG_NAME);
if (tag != null) {
register(view, tag);
return;
}
ViewParent viewParent = view.getParent();
ViewGroup parentView = (ViewGroup) viewParent;
while (parentView != null) {
tag = parentView.getTag(BaseViewTag.TAG_NAME);
if (tag instanceof LifecycleOwner) {
register(view, tag);
return;
}
viewParent = parentView.getParent();
if (viewParent instanceof ViewGroup) {
parentView = (ViewGroup) viewParent;
} else {
break;
}
}
register(view, view.getContext());
} catch (Exception e) {
Logger.error(TAG, "registerLifecycle view: " + view.getClass().getSimpleName() + ", error", e);
}
}
});
}
/**
* 真正将view关联到Activity或者Fragment的生命周期
*
* @param view 需要关联Activity或者Fragment生命周期的控件注意该控件需要实现LifecycleObserver接口
* @param owner 生命周期提供者一般是Activity或Fragment
*/
private static void register(View view, Object owner) {
if (view == null) {
Logger.warn(TAG, "registerLifecycle view is null");
return;
}
if (owner == null) {
Logger.warn(TAG, "registerLifecycle owner is null");
return;
}
try {
if (owner instanceof LifecycleOwner) {
LifecycleOwner lifecycleOwner = (LifecycleOwner) owner;
lifecycleOwner.getLifecycle().addObserver((LifecycleObserver) view);//核心代码
view.setTag(BaseViewTag.TAG_NAME, owner);
Logger.debug(TAG, "registerLifecycle view: " + view.getClass().getSimpleName() + ", owner: " + owner.getClass().getSimpleName());
} else {
Logger.warn(TAG, "registerLifecycle view:" + view.getClass().getSimpleName() + ", owner is not instanceof LifecycleOwner");
}
} catch (Exception e) {
Logger.error(TAG, "registerLifecycle view: " + view.getClass().getSimpleName() + ", owner: " + owner.getClass().getSimpleName() + ", error", e);
}
}
/**
* 解绑Activity或者Fragment生命周期监听接口
*
* @param view 需要关联Activity或者Fragment生命周期的控件注意该控件需要实现LifecycleObserver接口
* @param owner 生命周期提供者一般是Activity或Fragment
*/
public static void unRegisterLifecycle(View view, LifecycleOwner owner) {
if (view == null) {
Logger.warn(TAG, "unRegisterLifecycle view is null");
return;
}
if (owner == null) {
Logger.warn(TAG, "unRegisterLifecycle owner is null");
return;
}
try {
view.setTag(BaseViewTag.TAG_NAME, null);
owner.getLifecycle().removeObserver((LifecycleObserver) view);
Logger.debug(TAG, "unRegisterLifecycle view: " + view.getClass().getSimpleName() + ", owner: " + owner.getClass().getSimpleName());
} catch (Exception e) {
Logger.error(TAG, "unRegisterLifecycle view: " + view.getClass().getSimpleName() + ", owner: " + owner.getClass().getSimpleName() + ", error", e);
}
}
}

View File

@@ -0,0 +1,50 @@
package com.nnbc123.library.common.delegate
import com.nnbc123.library.common.application.BaseApp
import com.nnbc123.library.common.util.SPUtils
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
/**
* author: wushaocheng
* time: 2022/11/15
* desc: sp存储和取出委托
*/
class SpDelegate<T>(private val key: String, private val default: T) : ReadWriteProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
val value = when (default) {
is Boolean -> SPUtils.getBoolean(key, default)
is String -> SPUtils.getString(key, default)
is Long -> SPUtils.getLong(key, default)
is Int -> SPUtils.getInt(key, default)
is Float -> SPUtils.getFloat(key, default)
is Double -> SPUtils.getDouble(key, default)
is ByteArray -> SPUtils.getBytes(key, default)
else -> {
if (BaseApp.isDebug()) {
throw IllegalArgumentException("SpDelegate: this type is no supported")
} else {
null
}
}
}
return (value as? T) ?: default
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
when (value) {
is Boolean -> SPUtils.putBoolean(key, value)
is String -> SPUtils.putString(key, value)
is Long -> SPUtils.putLong(key, value)
is Int -> SPUtils.putInt(key, value)
is Float -> SPUtils.putFloat(key, value)
is Double -> SPUtils.putDouble(key, value)
is ByteArray -> SPUtils.putBytes(key, value)
else -> {
if (BaseApp.isDebug()) {
throw IllegalArgumentException("SpDelegate: this type is no supported")
}
}
}
}
}

View File

@@ -0,0 +1,10 @@
package com.nnbc123.library.common.entity
import androidx.fragment.app.Fragment
/**
* author: wushaocheng
* time: 2022/2/16
* desc: 公共tab
*/
class CommonTabEntity(val frgClazz: Class<out Fragment>, val title: String)

View File

@@ -0,0 +1,917 @@
package com.nnbc123.library.common.file;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Environment;
import android.text.TextUtils;
import com.nnbc123.library.utils.FP;
import com.qiniu.android.utils.StringUtils;
import com.nnbc123.library.common.application.BaseApp;
import com.nnbc123.library.common.util.Logger;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* 文件工具类
*/
public class FileHelper {
private static final String TAG = "FileHelper";
private static final String NO_MEDIA = ".nomedia";
private static File rootCacheDir;
private static HashMap<String, File> rootFileDirMap = new HashMap<>();
private static final String[] mCs = new String[]{"/", "\\", "?", "*", ":", "<", ">", "|", "\""};
private static final char UNICODE_SURROGATE_START_CHAR = '\ud800';
private static final char UNICODE_SURROGATE_END_CHAR = '\udfff';
/**
* 创建.nomedia文件
*
* @param parentFile 上级目录
*/
private static void createNoMediaFile(File parentFile) {
File no_media = new File(parentFile, NO_MEDIA);
if (!no_media.exists()) {
try {
no_media.createNewFile();
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 获取Android/data/当前应用包名/cache文件夹
*
* @return 当前应用缓存根目录
*/
public static File getRootCacheDir() {
if (rootCacheDir != null) {
//因为频繁调用getExternalCacheDir方法会在某些机器上面出现ANR所以这里降低频率调用。
return rootCacheDir;
}
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
File file = BaseApp.getContext().getExternalCacheDir();
if (file != null) {
createNoMediaFile(file);
//因为频繁调用getExternalCacheDir方法会在某些机器上面出现ANR所以这里降低频率调用。
rootCacheDir = file;
return file;
}
}
File file = BaseApp.getContext().getCacheDir();
createNoMediaFile(file);
//因为频繁调用getExternalCacheDir方法会在某些机器上面出现ANR所以这里降低频率调用。
rootCacheDir = file;
return file;
}
/**
* 获取Android/data/当前应用包名/files文件夹
*
* @param type 类型从以下方式中选择也可以为null:
* {@link Environment#DIRECTORY_MUSIC},
* {@link Environment#DIRECTORY_PODCASTS},
* {@link Environment#DIRECTORY_RINGTONES},
* {@link Environment#DIRECTORY_ALARMS},
* {@link Environment#DIRECTORY_NOTIFICATIONS},
* {@link Environment#DIRECTORY_PICTURES},
* {@link Environment#DIRECTORY_MOVIES},
* {@link Environment#DIRECTORY_DOWNLOADS},
* {@link Environment#DIRECTORY_DCIM},
* {@link Environment#DIRECTORY_DOCUMENTS},
* {@link Environment#DIRECTORY_AUDIOBOOKS}
* @return 当前应用文件根目录
*/
public static File getRootFilesDir(@androidx.annotation.Nullable String type) {
String dirName = (type != null && type.length() > 0) ? type.trim() : null;
if (TextUtils.isEmpty(dirName)) {
//因为频繁调用getExternalFilesDir方法会在某些机器上面出现ANR所以这里降低频率调用。
File file = rootFileDirMap.get("empty");
if (file != null) {
return file;
}
} else {
//因为频繁调用getExternalFilesDir方法会在某些机器上面出现ANR所以这里降低频率调用。
File file = rootFileDirMap.get(dirName);
if (file != null) {
return file;
}
}
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
File file = BaseApp.getContext().getExternalFilesDir(dirName);
if (file != null) {
createNoMediaFile(file);
//因为频繁调用getExternalFilesDir方法会在某些机器上面出现ANR所以这里降低频率调用。
if (TextUtils.isEmpty(dirName)) {
rootFileDirMap.put("empty", file);
} else {
rootFileDirMap.put(dirName, file);
}
return file;
}
}
File file = BaseApp.getContext().getFilesDir();
createNoMediaFile(file);
//因为频繁调用getExternalFilesDir方法会在某些机器上面出现ANR所以这里降低频率调用。
rootFileDirMap.put("empty", file);
return file;
}
/**
* 创建临时文件
*
* @param dir 文件夹
* @return 临时文件
*/
public static File createTempFile(File dir) {
try {
File file = File.createTempFile("pic", null, dir);
file.deleteOnExit();
return file;
} catch (IOException e) {
return null;
}
}
/**
* 判断文件或文件夹是否存在
*
* @param filePath 文件路径
* @return 是否存在这个文件或文件夹
*/
public static boolean isFileExist(String filePath) {
if (TextUtils.isEmpty(filePath)) {
return false;
}
File file = new File(filePath);
return file.exists();
}
/**
* 确保文件夹存在,不存在的时候就创建该目录
*
* @param dirPath 文件夹路径
* @return 确保是否已存在这个文件夹
*/
public static boolean ensureDirExists(String dirPath) {
File dirFile = new File(dirPath);
if (!dirFile.exists()) {
return dirFile.mkdirs();
}
return true;
}
/**
* 确保该文件的文件夹存在,不存在的时候就创建该文件的文件夹
*
* @param filePath 文件路径
* @return 确保是否已存在该文件的文件夹
*/
public static boolean ensureFileDirExists(String filePath) {
String dir = getDirOfFilePath(filePath);
if (TextUtils.isEmpty(dir)) {
return false;
}
ensureDirExists(dir);
return true;
}
/**
* 确保该文件存在,不存在的时候就创建该文件
*
* @param filePath 文件路径
* @return 确保是否已存在该文件
*/
public static File ensureFileExists(String filePath) {
if (!ensureFileDirExists(filePath)) {
return null;
}
File file = new File(filePath);
if (file.exists()) {
return file;
}
try {
if (!file.exists() && !file.createNewFile()) {
file = null;
}
} catch (IOException e) {
e.printStackTrace();
file = null;
}
return file;
}
/**
* 从文件路径里提取文件夹的路径
*
* @param filePath 文件路径
* @return 文件夹的路径
*/
public static String getDirOfFilePath(String filePath) {
if (TextUtils.isEmpty(filePath)) {
return null;
}
int sepPos = filePath.lastIndexOf(File.separatorChar);
if (sepPos == -1) {
return null;
}
return filePath.substring(0, sepPos);
}
/**
* 删除单个文件
*
* @param filePath 文件路径
*/
public static void removeFile(String filePath) {
if (!TextUtils.isEmpty(filePath)) {
try {
File file = new File(filePath);
file.delete();
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 删除文件或文件夹下面所有的文件
*
* @param filePath 文件路径
* @return 是否删除成功
*/
public static boolean removeAllFile(String filePath) {
if (TextUtils.isEmpty(filePath)) {
return true;
}
File file = new File(filePath);
if (!file.exists()) {
return true;
}
if (file.isFile()) {
try {
return file.delete();
} catch (Exception e) {
return false;
}
}
if (!file.isDirectory()) {
return false;
}
File[] files = file.listFiles();
if (files != null && files.length > 0) {
for (File f : files) {
if (f.isFile()) {
try {
f.delete();
} catch (Exception e) {
e.printStackTrace();
}
} else if (f.isDirectory()) {
removeAllFile(f.getAbsolutePath());
}
}
}
try {
return file.delete();
} catch (Exception e) {
return false;
}
}
/**
* 将数据存储到缓存文件夹里
*
* @param data 数据
* @param filePath 文件路径
*/
public static void saveByteArrayIntoFile(byte[] data, String filePath) {
try {
File f = new File(filePath);
if (f.createNewFile()) {
FileOutputStream fos = new FileOutputStream(f);
fos.write(data);
fos.flush();
fos.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 将Bitmap保存为JPEG图片
*/
public static void saveBitmapAsJPEG(Bitmap bmp, String filePath) {
if (bmp == null) {
return;
}
FileOutputStream fos = null;
try {
if (!ensureFileDirExists(filePath)) {
return;
}
File file = new File(filePath);
if (file.exists()) {
file.delete();
}
fos = new FileOutputStream(file);
bmp.compress(Bitmap.CompressFormat.JPEG, 100, fos);
fos.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (fos != null) {
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 将Bitmap保存为PNG图片
*
* @param bmp 图片
* @param filePath 文件路径
* @return 文件路径
*/
public static String saveBitmapAsPNG(Bitmap bmp, String filePath) {
if (bmp == null) {
return "";
}
FileOutputStream fos = null;
try {
if (!ensureFileDirExists(filePath)) {
return "";
}
File file = new File(filePath);
if (file.exists()) {
file.delete();
}
fos = new FileOutputStream(file);
bmp.compress(Bitmap.CompressFormat.PNG, 100, fos);
fos.flush();
return filePath;
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (fos != null) {
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return "";
}
/**
* 解压zip到指定的路径
*
* @param zipFileString ZIP的名称
* @param outPathString 要解压缩路径
*/
public static boolean unzipFile(String zipFileString, String outPathString) {
ZipInputStream inZip = null;
try {
inZip = new ZipInputStream(new FileInputStream(zipFileString));
ZipEntry zipEntry;
while ((zipEntry = inZip.getNextEntry()) != null) {
String szName = zipEntry.getName();
if (szName.contains("/")) {
szName = szName.substring(szName.indexOf("/") + 1);
if (TextUtils.isEmpty(szName)) {
continue;
}
}
if (!zipEntry.isDirectory()) {
File file = new File(outPathString + File.separator + szName);
if (!file.exists()) {
if (file.getParentFile() != null) {
file.getParentFile().mkdirs();
}
file.createNewFile();
}
FileOutputStream out = new FileOutputStream(file);
int len;
byte[] buffer = new byte[1024];
while ((len = inZip.read(buffer)) != -1) {
out.write(buffer, 0, len);
out.flush();
}
out.close();
} else {
szName = szName.substring(0, szName.length() - 1);
File folder = new File(outPathString + File.separator + szName);
folder.mkdirs();
}
}
} catch (Exception e) {
e.printStackTrace();
return false;
} finally {
try {
if (inZip != null) {
inZip.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
return true;
}
/**
* 解压压缩包
*
* @param srcZipPath 原压缩包路径
* @param destFilePath 目标文件夹路径
* @return 解压之后的文件组
* @throws IOException 抛出异常
*/
public static File[] unzip(String srcZipPath, String destFilePath) throws IOException {
if (TextUtils.isEmpty(destFilePath)) {
throw new IOException();
}
if (!destFilePath.endsWith(File.separator)) {
destFilePath = destFilePath + File.separator;
}
File destDir = new File(destFilePath);
if (!destDir.isDirectory() || !destDir.exists()) {
destDir.mkdir();
}
ArrayList<File> extractedFileList = new ArrayList<>();
ZipInputStream inZip = null;
try {
ZipEntry zipEntry;
inZip = new ZipInputStream(new FileInputStream(srcZipPath));
String szName;
while ((zipEntry = inZip.getNextEntry()) != null) {
szName = zipEntry.getName();
if (zipEntry.isDirectory()) {
szName = szName.substring(0, szName.length() - 1);
File folder = new File(destDir.getAbsolutePath() + File.separator + szName);
folder.mkdirs();
continue;
}
FileOutputStream out = null;
try {
int len;
File file = new File(destDir.getAbsolutePath() + File.separator + szName);
file = ensureFileExists(file.getAbsolutePath());
if (file != null) {
out = new FileOutputStream(file);
byte[] buffer = new byte[1024];
while ((len = inZip.read(buffer)) != -1) {
out.write(buffer, 0, len);
out.flush();
}
out.close();
extractedFileList.add(file);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (out != null) {
out.close();
}
}
}
} finally {
if (inZip != null) {
inZip.close();
}
}
File[] extractedFiles = new File[extractedFileList.size()];
extractedFileList.toArray(extractedFiles);
return extractedFiles;
}
/**
* 从文件路径里面提取文件的后缀
*
* @param filePath 文件路径
* @return 后缀,如.mp3
*/
public static String getFileExtension(String filePath) {
String fileName = getFileName(filePath);
if (TextUtils.isEmpty(fileName)) {
return null;
}
int index = fileName.lastIndexOf(".");
if (index != -1) {
return fileName.substring(index);
}
return null;
}
/**
* 从文件路径里面提取文件名
*
* @param filePath 文件路径
* @return 文件名称
*/
public static String getFileName(String filePath) {
if (filePath != null) {
String slash = "/";
int pos = filePath.lastIndexOf(slash) + 1;
if (pos > 0) {
String name = filePath.substring(pos);
if (!TextUtils.isEmpty(name)) {
name = name.replace("?", "");
}
return name;
}
}
return null;
}
/**
* 从URL里面提取版本名
*
* @param url 链接
* @return 文件名
*/
public static String getFileNameWithVer(String url) {
String versionName = "";
String subName = getFileName(url);
if (subName != null && subName.contains("?")) {
String[] tempArr = subName.split("\\?");
if (tempArr.length > 1) {
subName = tempArr[0];
versionName = tempArr[1];
}
}
String fileName = dropExt(subName);
fileName = fileName + versionName;
return fileName;
}
/**
* 从文件名里面踢出点得到可用的文件名
*
* @param fileName 文件名
* @return 文件名
*/
public static String dropExt(String fileName) {
if (!TextUtils.isEmpty(fileName)) {
int pos = fileName.lastIndexOf(".");
if (pos != -1) {
return FP.take(pos, fileName);
}
}
return fileName;
}
/**
* 读取文件内容转换为字符串
*
* @param filePath 文件路径
*/
public static String getStringFromFile(String filePath) {
String result = "";
InputStream is = null;
try {
is = new FileInputStream(filePath);
int length = is.available();
byte[] buffer = new byte[length];
is.read(buffer);
result = new String(buffer, StandardCharsets.UTF_8);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (is != null) {
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
/**
* 读取Assets内容转换为字符串
*
* @param fileName 文件名
* @return 字符串内容
*/
public static String getStringFromAssets(String fileName) {
String result = "";
try {
InputStream is = BaseApp.getContext().getAssets().open(fileName);
int length = is.available();
byte[] buffer = new byte[length];
is.read(buffer);
result = new String(buffer, StandardCharsets.UTF_8);
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 从Assets里面读取内容覆盖本地文件
*
* @param dir 文件夹路径
* @param fileName 文件名称
* @param overwrite 是否覆盖,即删除旧文件重新创建新文件
* @return 操作是否成功
*/
public static boolean copyFileFromAssets(String dir, String fileName, boolean overwrite) {
String path = dir + File.separator + fileName;
File file = new File(path);
if (file.exists() && overwrite) {
file.delete();
}
if (!file.exists()) {
try {
if (!ensureDirExists(dir)) {
return false;
}
file.createNewFile();
InputStream in = BaseApp.getContext().getAssets().open(fileName);
OutputStream out = new FileOutputStream(file);
byte[] buffer = new byte[4096];
int n;
while ((n = in.read(buffer)) > 0) {
out.write(buffer, 0, n);
}
out.flush();
in.close();
out.close();
} catch (Exception e) {
try {
file.delete();
} catch (Exception ex) {
ex.printStackTrace();
}
return false;
}
}
return true;
}
/**
* 将字节数组写入文件中
*
* @param buffer 字节数组
* @param folderPath 文件夹路径
* @param fileName 文件名称
*/
public static void saveDataToFile(byte[] buffer, String folderPath, String fileName) {
File fileDir = new File(folderPath);
if (!fileDir.exists()) {
fileDir.mkdirs();
}
File file = new File(folderPath + File.separator + fileName);
FileOutputStream out = null;
try {
out = new FileOutputStream(file);
out.write(buffer);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (out != null) {
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 格式化大小,带单位
*/
public static String formatSize(long size) {
//获取到的size为1705230
int GB = 1024 * 1024 * 1024;//定义GB的计算常量
int MB = 1024 * 1024;//定义MB的计算常量
int KB = 1024;//定义KB的计算常量
DecimalFormat df = new DecimalFormat("0.0");//格式化小数
String resultSize = "";
if (size / GB >= 1) {
//如果当前Byte的值大于等于1GB
resultSize = df.format(size / (float) GB) + "GB";
} else if (size / MB >= 1) {
//如果当前Byte的值大于等于1MB
resultSize = df.format(size / (float) MB) + "MB";
} else if (size / KB >= 1) {
//如果当前Byte的值大于等于1KB
resultSize = df.format(size / (float) KB) + "KB";
} else {
resultSize = size + "B";
}
return resultSize;
}
/**
* @param fileName 文件名
* @return 文件名是否正确
*/
public static boolean isFileNameCorrect(String fileName) {
if (null == fileName) {
return false;
} else {
fileName = fileName.trim();
if (fileName.length() == 0) {
return false;
} else {
for (String c : mCs) {
if (fileName.contains(c)) {
return false;
}
}
return !containsSurrogateChar(fileName);
}
}
}
/**
* @param string 字符串
* @return 是否包含需要替代的字符串
*/
public static boolean containsSurrogateChar(String string) {
if (TextUtils.isEmpty(string)) {
return false;
} else {
int length = string.length();
boolean hasSurrogateChar = false;
for (int i = 0; i < length; ++i) {
char c = string.charAt(i);
if (UNICODE_SURROGATE_START_CHAR <= c && c <= UNICODE_SURROGATE_END_CHAR) {
hasSurrogateChar = true;
break;
}
}
return hasSurrogateChar;
}
}
/**
* 把Uri转换为文件
*
* @param uri 源文件的Uri的路径
* @param path 要存入的文件的路径
* @param overwrite 是否需要复写
* @return 是否已经把Uri转换成功为文件了
*/
public static boolean copyFileFromUri(Uri uri, String path, boolean overwrite) {
File file = new File(path);
if (file.exists() && overwrite) {
file.delete();
}
if (!file.exists()) {
try {
if (!FileHelper.ensureDirExists(file.getParentFile().getAbsolutePath())) {
return false;
}
InputStream stream = BaseApp.getContext().getContentResolver().openInputStream(uri);
if (stream != null) {
file.createNewFile();
OutputStream out = new FileOutputStream(file);
byte buffer[] = new byte[4096];
int n;
while ((n = stream.read(buffer)) > 0) {
out.write(buffer, 0, n);
}
out.flush();
stream.close();
out.close();
}
} catch (Exception e) {
try {
file.delete();
} catch (Exception ex) {
ex.printStackTrace();
}
return false;
}
}
return true;
}
/**
* 将文件转换成字节数组
*
* @param file 文件
* @return 字节数组
*/
public static byte[] fileToByteArray(File file) {
if (file.exists() && file.canRead()) {
try {
return streamToBytes(new FileInputStream(file));
} catch (Exception e) {
Logger.error(TAG, String.valueOf(e));
}
}
return null;
}
/**
* 将文件流转换成字节数组
*
* @param inputStream 输入流
* @return 字节数组
*/
public static byte[] streamToBytes(InputStream inputStream) {
byte[] content = null;
ByteArrayOutputStream baos = null;
BufferedInputStream bis = null;
try {
baos = new ByteArrayOutputStream();
bis = new BufferedInputStream(inputStream);
byte[] buffer = new byte[1024];
int length;
while ((length = bis.read(buffer)) != -1) {
baos.write(buffer, 0, length);
}
content = baos.toByteArray();
if (content.length == 0) {
content = null;
}
} catch (IOException e) {
Logger.error(TAG, String.valueOf(e));
} finally {
if (baos != null) {
try {
baos.close();
} catch (IOException e) {
Logger.error(TAG, String.valueOf(e));
}
}
if (bis != null) {
try {
bis.close();
} catch (IOException e) {
Logger.error(TAG, String.valueOf(e));
}
}
}
return content;
}
/**
* 文件转换成字符串
*
* @param filePath 文件路径
* @return 字符串内容
*/
public static String getTxtFileContent(String filePath) {
String content = "";
if (!StringUtils.isNullOrEmpty(filePath)) {
File file = new File(filePath);
if (file.isFile()) {
FileInputStream inputStream = null;
try {
inputStream = new FileInputStream(file);
String line;
StringBuilder sb = new StringBuilder();
BufferedReader buffReader = new BufferedReader(new InputStreamReader(inputStream));
while ((line = buffReader.readLine()) != null) {
sb.append(line).append("\n");
}
content = sb.toString();
} catch (Exception e) {
Logger.error(TAG, "getTxtFileContent read fail, e = " + e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (Exception ignore) {
}
}
}
}
}
return content;
}
}

View File

@@ -0,0 +1,13 @@
package com.nnbc123.library.common.fragmentation;
/**
* Created by YoKey on 17/6/13.
*/
public interface ISupportActivity {
SupportActivityDelegate getSupportDelegate();
void onBackPressedPage();
void onBackPressedSupport();
}

View File

@@ -0,0 +1,36 @@
package com.nnbc123.library.common.fragmentation;
import android.os.Bundle;
import androidx.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Created by YoKey on 17/6/23.
*/
public interface ISupportFragment {
// LaunchMode
int STANDARD = 0;
int SINGLETOP = 1;
int SINGLETASK = 2;
// ResultCode
int RESULT_CANCELED = 0;
int RESULT_OK = -1;
@IntDef({STANDARD, SINGLETOP, SINGLETASK})
@Retention(RetentionPolicy.SOURCE)
@interface LaunchMode {
}
SupportFragmentDelegate getSupportDelegate();
void setFragmentResult(int resultCode, Bundle bundle);
void onFragmentResult(int requestCode, int resultCode, Bundle data);
boolean onBackPressedSupport();
}

View File

@@ -0,0 +1,99 @@
package com.nnbc123.library.common.fragmentation;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import com.nnbc123.library.common.fragmentation.queue.Action;
public class SupportActivityDelegate {
private final ISupportActivity mSupport;
private final FragmentActivity mActivity;
private TransactionDelegate mTransactionDelegate;
public SupportActivityDelegate(ISupportActivity support) {
if (!(support instanceof FragmentActivity))
throw new RuntimeException("Must extends FragmentActivity/AppCompatActivity");
this.mSupport = support;
this.mActivity = (FragmentActivity) support;
}
public void onCreate() {
mTransactionDelegate = getTransactionDelegate();
}
public TransactionDelegate getTransactionDelegate() {
if (mTransactionDelegate == null) {
mTransactionDelegate = new TransactionDelegate();
}
return mTransactionDelegate;
}
/**
* 不建议复写该方法,请使用 {@link #onBackPressedSupport} 代替
*/
public void onBackPressedPage() {
mTransactionDelegate.mActionQueue.enqueue(new Action(Action.ACTION_BACK, getSupportFragmentManager()) {
@Override
public void run() {
// 获取activeFragment:即从栈顶开始 状态为show的那个Fragment
ISupportFragment activeFragment = SupportHelper.getAddedFragment(getSupportFragmentManager());
if (mTransactionDelegate.dispatchBackPressedEvent(activeFragment)) return;
mSupport.onBackPressedSupport();
}
});
}
/**
* 该方法回调时机为,Activity回退栈内Fragment的数量 小于等于1 时,默认finish Activity
* 请尽量复写该方法,避免复写onBackPress(),以保证SupportFragment内的onBackPressedSupport()回退事件正常执行
*/
public void onBackPressedSupport() {
if (getSupportFragmentManager().getBackStackEntryCount() > 1) {
pop();
} else {
ActivityCompat.finishAfterTransition(mActivity);
}
}
//**********************************************************************************************//
/**
* 加载根Fragment, 即Activity内的第一个Fragment 或 Fragment内的第一个子Fragment
*/
public void loadRootFragment(int containerId, ISupportFragment toFragment) {
loadRootFragment(containerId, toFragment, true);
}
public void loadRootFragment(int containerId, ISupportFragment toFragment, boolean addToBackStack) {
mTransactionDelegate.loadRootTransaction(getSupportFragmentManager(), containerId, toFragment, addToBackStack);
}
public void start(ISupportFragment toFragment) {
start(toFragment, ISupportFragment.STANDARD);
}
/**
* @param launchMode Similar to Activity's LaunchMode.
*/
public void start(ISupportFragment toFragment, @ISupportFragment.LaunchMode int launchMode) {
mTransactionDelegate.dispatchStartTransaction(getSupportFragmentManager(), getTopFragment(), toFragment, 0, launchMode, TransactionDelegate.TYPE_ADD);
}
/**
* Pop the child fragment.
*/
public void pop() {
mTransactionDelegate.pop(getSupportFragmentManager());
}
private FragmentManager getSupportFragmentManager() {
return mActivity.getSupportFragmentManager();
}
private ISupportFragment getTopFragment() {
return SupportHelper.getTopFragment(getSupportFragmentManager());
}
}

View File

@@ -0,0 +1,172 @@
package com.nnbc123.library.common.fragmentation;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import com.nnbc123.library.common.fragmentation.internal.ResultRecord;
import com.nnbc123.library.common.fragmentation.internal.TransactionRecord;
public class SupportFragmentDelegate {
int mContainerId;
private TransactionDelegate mTransactionDelegate;
TransactionRecord mTransactionRecord;
private final ISupportFragment mSupportF;
private final Fragment mFragment;
public SupportFragmentDelegate(ISupportFragment support) {
if (!(support instanceof Fragment))
throw new RuntimeException("Must extends Fragment");
this.mSupportF = support;
this.mFragment = (Fragment) support;
}
public void onAttach() {
FragmentActivity activity = mFragment.getActivity();
if (activity instanceof ISupportActivity) {
ISupportActivity mSupportA = (ISupportActivity) activity;
mTransactionDelegate = mSupportA.getSupportDelegate().getTransactionDelegate();
} else {
if (activity != null) {
throw new RuntimeException(activity.getClass().getSimpleName() + " must impl ISupportActivity!");
} else {
throw new RuntimeException("fragment attached activity must not be null");
}
}
}
public void onCreate(@Nullable Bundle savedInstanceState) {
Bundle bundle = mFragment.getArguments();
if (bundle != null) {
mContainerId = bundle.getInt(TransactionDelegate.FRAGMENTATION_ARG_CONTAINER);
}
if (savedInstanceState != null) {
savedInstanceState.setClassLoader(getClass().getClassLoader());
mContainerId = savedInstanceState.getInt(TransactionDelegate.FRAGMENTATION_ARG_CONTAINER);
}
}
public void onSaveInstanceState(Bundle outState) {
outState.putInt(TransactionDelegate.FRAGMENTATION_ARG_CONTAINER, mContainerId);
}
public void onViewCreated() {
View view = mFragment.getView();
if (view != null) {
view.setClickable(true);
setBackground(view);
}
}
public void onDestroyView() {
mTransactionDelegate.handleResultRecord(mFragment);
}
/**
* 类似 {@link Activity#setResult(int, Intent)}
* <p>
* Similar to {@link Activity#setResult(int, Intent)}
*
* @see #startForResult(ISupportFragment, int)
*/
public void setFragmentResult(int resultCode, Bundle bundle) {
Bundle args = mFragment.getArguments();
if (args == null || !args.containsKey(TransactionDelegate.FRAGMENTATION_ARG_RESULT_RECORD)) {
return;
}
ResultRecord resultRecord = args.getParcelable(TransactionDelegate.FRAGMENTATION_ARG_RESULT_RECORD);
if (resultRecord != null) {
resultRecord.resultCode = resultCode;
resultRecord.resultBundle = bundle;
}
}
/**
* 类似Activity的onActivityResult(int, int, Intent)
*
* @see #startForResult(ISupportFragment, int)
*/
public void onFragmentResult(int requestCode, int resultCode, Bundle data) {
}
/**
* Back Event
*
* @return false则继续向上传递, true则消费掉该事件
*/
public boolean onBackPressedSupport() {
return false;
}
//**********************************************************************************************//
/**
* 加载根Fragment, 即Activity内的第一个Fragment 或 Fragment内的第一个子Fragment
*/
public void loadRootFragment(int containerId, ISupportFragment toFragment) {
loadRootFragment(containerId, toFragment, true);
}
public void loadRootFragment(int containerId, ISupportFragment toFragment, boolean addToBackStack) {
mTransactionDelegate.loadRootTransaction(getChildFragmentManager(), containerId, toFragment, addToBackStack);
}
public void start(ISupportFragment toFragment) {
start(toFragment, ISupportFragment.STANDARD);
}
/**
* @param launchMode Similar to Activity's LaunchMode.
*/
public void start(final ISupportFragment toFragment, @ISupportFragment.LaunchMode int launchMode) {
mTransactionDelegate.dispatchStartTransaction(mFragment.getFragmentManager(), mSupportF, toFragment, 0, launchMode, TransactionDelegate.TYPE_ADD);
}
/**
* Launch an fragment for which you would like a result when it poped.
*/
public void startForResult(ISupportFragment toFragment, int requestCode) {
mTransactionDelegate.dispatchStartTransaction(mFragment.getFragmentManager(), mSupportF, toFragment, requestCode, ISupportFragment.STANDARD, TransactionDelegate.TYPE_ADD_RESULT);
}
public void startChild(ISupportFragment toFragment) {
startChild(toFragment, ISupportFragment.STANDARD);
}
public void startChild(final ISupportFragment toFragment, @ISupportFragment.LaunchMode int launchMode) {
mTransactionDelegate.dispatchStartTransaction(getChildFragmentManager(), getTopFragment(), toFragment, 0, launchMode, TransactionDelegate.TYPE_ADD);
}
public void pop() {
mTransactionDelegate.pop(mFragment.getFragmentManager());
}
private FragmentManager getChildFragmentManager() {
return mFragment.getChildFragmentManager();
}
private ISupportFragment getTopFragment() {
return SupportHelper.getTopFragment(getChildFragmentManager());
}
public void setBackground(View view) {
if ((mFragment.getTag() != null && mFragment.getTag().startsWith("android:switcher:")) ||
view.getBackground() != null) {
return;
}
view.setBackgroundColor(Color.TRANSPARENT);
}
}

View File

@@ -0,0 +1,209 @@
package com.nnbc123.library.common.fragmentation;
import android.content.Context;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import java.util.ArrayList;
import java.util.List;
/**
* Created by YoKey on 17/6/13.
*/
public class SupportHelper {
private static final long SHOW_SPACE = 200L;
private SupportHelper() {
}
/**
* 显示软键盘
*/
public static void showSoftInput(final View view) {
if (view == null || view.getContext() == null) return;
final InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
view.requestFocus();
view.postDelayed(new Runnable() {
@Override
public void run() {
imm.showSoftInput(view, InputMethodManager.SHOW_FORCED);
}
}, SHOW_SPACE);
}
/**
* 隐藏软键盘
*/
public static void hideSoftInput(View view) {
if (view == null || view.getContext() == null) return;
InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
/**
* 获得栈顶SupportFragment
*/
public static ISupportFragment getTopFragment(FragmentManager fragmentManager) {
return getTopFragment(fragmentManager, 0);
}
public static ISupportFragment getTopFragment(FragmentManager fragmentManager, int containerId) {
List<Fragment> fragmentList = fragmentManager.getFragments();
for (int i = fragmentList.size() - 1; i >= 0; i--) {
Fragment fragment = fragmentList.get(i);
if (fragment instanceof ISupportFragment) {
ISupportFragment iFragment = (ISupportFragment) fragment;
if (containerId == 0) return iFragment;
if (containerId == iFragment.getSupportDelegate().mContainerId) {
return iFragment;
}
}
}
return null;
}
/**
* Same as fragmentManager.findFragmentByTag(fragmentClass.getName());
* find Fragment from FragmentStack
*/
public static <T extends ISupportFragment> T findFragment(FragmentManager fragmentManager, Class<T> fragmentClass) {
return findAddedFragment(fragmentClass, null, fragmentManager);
}
/**
* Same as fragmentManager.findFragmentByTag(fragmentTag);
* <p>
* find Fragment from FragmentStack
*/
public static <T extends ISupportFragment> T findFragment(FragmentManager fragmentManager, String fragmentTag) {
return findAddedFragment(null, fragmentTag, fragmentManager);
}
/**
* 从栈顶开始寻找FragmentManager以及其所有子栈, 直到找到状态为show & userVisible的Fragment
*/
public static ISupportFragment getAddedFragment(FragmentManager fragmentManager) {
return getAddedFragment(fragmentManager, null);
}
static <T extends ISupportFragment> T findAddedFragment(Class<T> fragmentClass, String toFragmentTag, FragmentManager fragmentManager) {
Fragment fragment = null;
if (toFragmentTag == null) {
List<Fragment> fragmentList = fragmentManager.getFragments();
int sizeChildFrgList = fragmentList.size();
for (int i = sizeChildFrgList - 1; i >= 0; i--) {
Fragment brotherFragment = fragmentList.get(i);
if (brotherFragment instanceof ISupportFragment && brotherFragment.getClass().getName().equals(fragmentClass.getName())) {
fragment = brotherFragment;
break;
}
}
} else {
fragment = fragmentManager.findFragmentByTag(toFragmentTag);
if (fragment == null) return null;
}
return (T) fragment;
}
private static ISupportFragment getAddedFragment(FragmentManager fragmentManager, ISupportFragment parentFragment) {
List<Fragment> fragmentList = fragmentManager.getFragments();
if (fragmentList.size() == 0) {
return parentFragment;
}
for (int i = fragmentList.size() - 1; i >= 0; i--) {
Fragment fragment = fragmentList.get(i);
if (fragment instanceof ISupportFragment) {
if (fragment.isResumed() && !fragment.isHidden() && fragment.getUserVisibleHint()) {
return getAddedFragment(fragment.getChildFragmentManager(), (ISupportFragment) fragment);
}
}
}
return parentFragment;
}
/**
* Get the topFragment from BackStack
*/
public static ISupportFragment getBackStackTopFragment(FragmentManager fragmentManager) {
return getBackStackTopFragment(fragmentManager, 0);
}
/**
* Get the topFragment from BackStack
*/
public static ISupportFragment getBackStackTopFragment(FragmentManager fragmentManager, int containerId) {
int count = fragmentManager.getBackStackEntryCount();
for (int i = count - 1; i >= 0; i--) {
FragmentManager.BackStackEntry entry = fragmentManager.getBackStackEntryAt(i);
Fragment fragment = fragmentManager.findFragmentByTag(entry.getName());
if (fragment instanceof ISupportFragment) {
ISupportFragment supportFragment = (ISupportFragment) fragment;
if (containerId == 0) return supportFragment;
if (containerId == supportFragment.getSupportDelegate().mContainerId) {
return supportFragment;
}
}
}
return null;
}
static <T extends ISupportFragment> T findBackStackFragment(Class<T> fragmentClass, String toFragmentTag, FragmentManager fragmentManager) {
int count = fragmentManager.getBackStackEntryCount();
if (toFragmentTag == null) {
toFragmentTag = fragmentClass.getName();
}
for (int i = count - 1; i >= 0; i--) {
FragmentManager.BackStackEntry entry = fragmentManager.getBackStackEntryAt(i);
if (toFragmentTag.equals(entry.getName())) {
Fragment fragment = fragmentManager.findFragmentByTag(entry.getName());
if (fragment instanceof ISupportFragment) {
return (T) fragment;
}
}
}
return null;
}
static List<Fragment> getWillPopFragments(FragmentManager fm, String targetTag) {
Fragment target = fm.findFragmentByTag(targetTag);
List<Fragment> willPopFragments = new ArrayList<>();
List<Fragment> fragmentList = fm.getFragments();
int size = fragmentList.size();
int startIndex = -1;
for (int i = size - 1; i >= 0; i--) {
if (target == fragmentList.get(i)) {
if (i + 1 < size) {
startIndex = i + 1;
}
break;
}
}
if (startIndex == -1) return willPopFragments;
for (int i = size - 1; i >= startIndex; i--) {
Fragment fragment = fragmentList.get(i);
if (fragment != null && fragment.getView() != null) {
willPopFragments.add(fragment);
}
}
return willPopFragments;
}
}

View File

@@ -0,0 +1,305 @@
package com.nnbc123.library.common.fragmentation;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import com.nnbc123.library.common.fragmentation.internal.ResultRecord;
import com.nnbc123.library.common.fragmentation.internal.TransactionRecord;
import com.nnbc123.library.common.fragmentation.queue.Action;
import com.nnbc123.library.common.fragmentation.queue.ActionQueue;
import java.util.List;
/**
* Controller
* Created by YoKeyword on 16/1/22.
*/
class TransactionDelegate {
private static final String TAG = "Fragmentation";
static final String FRAGMENTATION_ARG_RESULT_RECORD = "fragment_arg_result_record";
static final String FRAGMENTATION_ARG_CONTAINER = "fragmentation_arg_container";
static final String FRAGMENTATION_ARG_REPLACE = "fragmentation_arg_replace";
private static final String FRAGMENTATION_STATE_SAVE_RESULT = "fragmentation_state_save_result";
static final int TYPE_ADD = 0;
static final int TYPE_ADD_RESULT = 1;
static final int TYPE_ADD_WITHOUT_HIDE = 2;
static final int TYPE_ADD_RESULT_WITHOUT_HIDE = 3;
static final int TYPE_REPLACE = 10;
static final int TYPE_REPLACE_DONT_BACK = 11;
ActionQueue mActionQueue;
TransactionDelegate() {
Handler mHandler = new Handler(Looper.getMainLooper());
mActionQueue = new ActionQueue(mHandler);
}
void loadRootTransaction(final FragmentManager fm, final int containerId, final ISupportFragment to, final boolean addToBackStack) {
enqueue(fm, new Action(Action.ACTION_LOAD, fm) {
@Override
public void run() {
bindContainerId(containerId, to);
String toFragmentTag = to.getClass().getName();
TransactionRecord transactionRecord = to.getSupportDelegate().mTransactionRecord;
if (transactionRecord != null) {
if (transactionRecord.tag != null) {
toFragmentTag = transactionRecord.tag;
}
}
start(fm, null, to, toFragmentTag, !addToBackStack, TYPE_REPLACE);
}
});
}
private void start(FragmentManager fm, final ISupportFragment from, ISupportFragment to, String toFragmentTag, boolean dontAddToBackStack, int type) {
FragmentTransaction ft = fm.beginTransaction();
boolean addMode = (type == TYPE_ADD || type == TYPE_ADD_RESULT || type == TYPE_ADD_WITHOUT_HIDE || type == TYPE_ADD_RESULT_WITHOUT_HIDE);
Fragment fromF = (Fragment) from;
Fragment toF = (Fragment) to;
Bundle args = getArguments(toF);
args.putBoolean(FRAGMENTATION_ARG_REPLACE, !addMode);
if (from == null) {
ft.replace(args.getInt(FRAGMENTATION_ARG_CONTAINER), toF, toFragmentTag);
if (!addMode) {
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
}
} else {
if (addMode) {
ft.add(from.getSupportDelegate().mContainerId, toF, toFragmentTag);
if (type != TYPE_ADD_WITHOUT_HIDE && type != TYPE_ADD_RESULT_WITHOUT_HIDE) {
ft.hide(fromF);
}
} else {
ft.replace(from.getSupportDelegate().mContainerId, toF, toFragmentTag);
}
}
if (!dontAddToBackStack && type != TYPE_REPLACE_DONT_BACK) {
ft.addToBackStack(toFragmentTag);
}
supportCommit(ft);
}
/**
* Pop
*/
void pop(final FragmentManager fm) {
enqueue(fm, new Action(Action.ACTION_POP, fm) {
@Override
public void run() {
removeTopFragment(fm);
fm.popBackStackImmediate();
}
});
}
private void removeTopFragment(FragmentManager fm) {
try { // Safe popBackStack()
ISupportFragment top = SupportHelper.getBackStackTopFragment(fm);
if (top != null) {
fm.beginTransaction()
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE)
.remove((Fragment) top)
.commitAllowingStateLoss();
}
} catch (Exception ignored) {
}
}
/**
* Dispatch the start transaction.
*/
void dispatchStartTransaction(final FragmentManager fm, final ISupportFragment from, final ISupportFragment to, final int requestCode, final int launchMode, final int type) {
enqueue(fm, new Action(launchMode == ISupportFragment.SINGLETASK ? Action.ACTION_POP_MOCK : Action.ACTION_NORMAL, fm) {
@Override
public void run() {
doDispatchStartTransaction(fm, from, to, requestCode, launchMode, type);
}
});
}
private void doDispatchStartTransaction(FragmentManager fm, ISupportFragment from, ISupportFragment to, int requestCode, int launchMode, int type) {
if (to == null) {
throw new NullPointerException("toFragment == null");
}
if ((type == TYPE_ADD_RESULT || type == TYPE_ADD_RESULT_WITHOUT_HIDE) && from != null) {
if (!((Fragment) from).isAdded()) {
Log.w(TAG, ((Fragment) from).getClass().getSimpleName() + " has not been attached yet! startForResult() converted to start()");
} else {
saveRequestCode(fm, (Fragment) from, (Fragment) to, requestCode);
}
}
from = getTopFragmentForStart(from, fm);
int containerId = getArguments((Fragment) to).getInt(FRAGMENTATION_ARG_CONTAINER, 0);
if (from == null && containerId == 0) {
Log.e(TAG, "There is no Fragment in the FragmentManager, maybe you need to call loadRootFragment()!");
return;
}
if (from != null && containerId == 0) {
bindContainerId(from.getSupportDelegate().mContainerId, to);
}
// process ExtraTransaction
String toFragmentTag = to.getClass().getName();
boolean dontAddToBackStack = false;
TransactionRecord transactionRecord = to.getSupportDelegate().mTransactionRecord;
if (transactionRecord != null) {
if (transactionRecord.tag != null) {
toFragmentTag = transactionRecord.tag;
}
dontAddToBackStack = transactionRecord.dontAddToBackStack;
}
if (handleLaunchMode(fm, from, to, toFragmentTag, launchMode)) return;
start(fm, from, to, toFragmentTag, dontAddToBackStack, type);
}
private void doPopTo(final String targetFragmentTag, FragmentManager fm) {
Fragment targetFragment = fm.findFragmentByTag(targetFragmentTag);
if (targetFragment == null) {
Log.e(TAG, "Pop failure! Can't find FragmentTag:" + targetFragmentTag + " in the FragmentManager's Stack.");
return;
}
List<Fragment> willPopFragments = SupportHelper.getWillPopFragments(fm, targetFragmentTag);
if (willPopFragments.size() <= 0) return;
safePopTo(targetFragmentTag, fm, willPopFragments);
}
private void safePopTo(String fragmentTag, final FragmentManager fm, List<Fragment> willPopFragments) {
// 批量删除fragment static final int OP_REMOVE = 3;
FragmentTransaction transaction = fm.beginTransaction()
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE);
for (Fragment fragment : willPopFragments) {
transaction.remove(fragment);
}
transaction.commitAllowingStateLoss();
// 弹栈到指定fragment从数据上来看和上面的效果完全一样把栈中所有的backStackRecord 包含多个记录,每个记录包含多个操作
// 在每个记录中,对操作索引 按照从大到小的顺序,逐个进行反操作。
// 除了第一个记录其余每个记录都有两个操作一个是添加OP_ADD = 1;(反操作是remove) 一个是OP_HIDE = 4;反操作是show这是在start中设定的
// 之所以有上面的批量删除在执行动画的时候发现f.mView == null 就不去执行show动画。
fm.popBackStackImmediate(fragmentTag, 0);
}
private ISupportFragment getTopFragmentForStart(ISupportFragment from, FragmentManager fm) {
ISupportFragment top;
if (from == null) {
top = SupportHelper.getTopFragment(fm);
} else {
if (from.getSupportDelegate().mContainerId == 0) {
Fragment fromF = (Fragment) from;
if (fromF.getTag() != null && !fromF.getTag().startsWith("android:switcher:")) {
throw new IllegalStateException("Can't find container, please call loadRootFragment() first!");
}
}
top = SupportHelper.getTopFragment(fm, from.getSupportDelegate().mContainerId);
}
return top;
}
/**
* Dispatch the pop-event. Priority of the top of the stack of Fragment
*/
boolean dispatchBackPressedEvent(ISupportFragment activeFragment) {
if (activeFragment != null) {
boolean result = activeFragment.onBackPressedSupport();
if (result) {
return true;
}
Fragment parentFragment = ((Fragment) activeFragment).getParentFragment();
return dispatchBackPressedEvent((ISupportFragment) parentFragment);
}
return false;
}
void handleResultRecord(Fragment from) {
try {
Bundle args = from.getArguments();
if (args == null) return;
final ResultRecord resultRecord = args.getParcelable(FRAGMENTATION_ARG_RESULT_RECORD);
if (resultRecord == null) return;
ISupportFragment targetFragment = (ISupportFragment) from.getFragmentManager().getFragment(from.getArguments(), FRAGMENTATION_STATE_SAVE_RESULT);
targetFragment.onFragmentResult(resultRecord.requestCode, resultRecord.resultCode, resultRecord.resultBundle);
} catch (IllegalStateException ignored) {
// Fragment no longer exists
}
}
private void enqueue(FragmentManager fm, Action action) {
if (fm == null) {
Log.w(TAG, "FragmentManager is null, skip the action!");
return;
}
mActionQueue.enqueue(action);
}
private void bindContainerId(int containerId, ISupportFragment to) {
Bundle args = getArguments((Fragment) to);
args.putInt(FRAGMENTATION_ARG_CONTAINER, containerId);
}
private Bundle getArguments(Fragment fragment) {
Bundle bundle = fragment.getArguments();
if (bundle == null) {
bundle = new Bundle();
fragment.setArguments(bundle);
}
return bundle;
}
private void supportCommit(FragmentTransaction transaction) {
transaction.commitAllowingStateLoss();
}
private boolean handleLaunchMode(FragmentManager fm, ISupportFragment topFragment, final ISupportFragment to, String toFragmentTag, int launchMode) {
if (topFragment == null) return false;
final ISupportFragment stackToFragment = SupportHelper.findBackStackFragment(to.getClass(), toFragmentTag, fm);
if (stackToFragment == null) return false;
if (launchMode == ISupportFragment.SINGLETOP) {
if (to == topFragment || to.getClass().getName().equals(topFragment.getClass().getName())) {
return true;
}
} else if (launchMode == ISupportFragment.SINGLETASK) {
doPopTo(toFragmentTag, fm);
return true;
}
return false;
}
/**
* save requestCode
*/
private void saveRequestCode(FragmentManager fm, Fragment from, Fragment to, int requestCode) {
Bundle bundle = getArguments(to);
ResultRecord resultRecord = new ResultRecord();
resultRecord.requestCode = requestCode;
bundle.putParcelable(FRAGMENTATION_ARG_RESULT_RECORD, resultRecord);
fm.putFragment(bundle, FRAGMENTATION_STATE_SAVE_RESULT, from);
}
}

View File

@@ -0,0 +1,48 @@
package com.nnbc123.library.common.fragmentation.internal;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
/**
* @Hide Result 记录
* Created by YoKeyword on 16/6/2.
*/
public final class ResultRecord implements Parcelable {
public int requestCode;
public int resultCode = 0;
public Bundle resultBundle;
public ResultRecord() {
}
protected ResultRecord(Parcel in) {
requestCode = in.readInt();
resultCode = in.readInt();
resultBundle = in.readBundle(getClass().getClassLoader());
}
public static final Creator<ResultRecord> CREATOR = new Creator<ResultRecord>() {
@Override
public ResultRecord createFromParcel(Parcel in) {
return new ResultRecord(in);
}
@Override
public ResultRecord[] newArray(int size) {
return new ResultRecord[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(requestCode);
dest.writeInt(resultCode);
dest.writeBundle(resultBundle);
}
}

View File

@@ -0,0 +1,9 @@
package com.nnbc123.library.common.fragmentation.internal;
/**
* @hide Created by YoKey on 16/11/25.
*/
public final class TransactionRecord {
public String tag;
public boolean dontAddToBackStack = false;
}

View File

@@ -0,0 +1,35 @@
package com.nnbc123.library.common.fragmentation.queue;
import androidx.fragment.app.FragmentManager;
/**
* Created by YoKey on 17/12/28.
*/
public abstract class Action {
public static final long DEFAULT_POP_TIME = 300L;
public static final int ACTION_NORMAL = 0;
public static final int ACTION_POP = 1;
public static final int ACTION_POP_MOCK = 2;
public static final int ACTION_BACK = 3;
public static final int ACTION_LOAD = 4;
public FragmentManager fragmentManager;
public int action = ACTION_NORMAL;
public long duration = 0;
public Action(int action) {
this.action = action;
}
public Action(int action, FragmentManager fragmentManager) {
this(action);
this.fragmentManager = fragmentManager;
}
public Action(FragmentManager fragmentManager) {
this.fragmentManager = fragmentManager;
}
public abstract void run();
}

View File

@@ -0,0 +1,81 @@
package com.nnbc123.library.common.fragmentation.queue;
import android.os.Handler;
import android.os.Looper;
import java.util.LinkedList;
import java.util.Queue;
/**
* The queue of perform action.
* <p>
* Created by YoKey on 17/12/29.
*/
public class ActionQueue {
private Queue<Action> mQueue = new LinkedList<>();
private Handler mMainHandler;
public ActionQueue(Handler mainHandler) {
this.mMainHandler = mainHandler;
}
public void enqueue(final Action action) {
if (isThrottleBACK(action)) return;
if (action.action == Action.ACTION_LOAD && mQueue.isEmpty()
&& Thread.currentThread() == Looper.getMainLooper().getThread()) {
action.run();
return;
}
mMainHandler.post(new Runnable() {
@Override
public void run() {
enqueueAction(action);
}
});
}
private void enqueueAction(Action action) {
mQueue.add(action);
//第一次进来的时候执行完上局队列只有一个一旦进入handleAction就会一直执行直到队列为空
if (mQueue.size() == 1) {
handleAction();
}
}
private void handleAction() {
if (mQueue.isEmpty()) return;
Action action = mQueue.peek();
if (action == null || action.fragmentManager.isStateSaved()) {
mQueue.clear();
return;
}
action.run();
executeNextAction(action);
}
private void executeNextAction(Action action) {
if (action.action == Action.ACTION_POP) {
action.duration = Action.DEFAULT_POP_TIME;
}
mMainHandler.postDelayed(new Runnable() {
@Override
public void run() {
mQueue.poll();
handleAction();
}
}, action.duration);
}
private boolean isThrottleBACK(Action action) {
if (action.action == Action.ACTION_BACK) {
Action head = mQueue.peek();
return head != null && head.action == Action.ACTION_POP;
}
return false;
}
}

View File

@@ -0,0 +1,14 @@
package com.nnbc123.library.common.fragmentation.windowcallback
import android.view.Window
/**
* author: wushaocheng
* time: 2022/11/15
* desc:
*/
interface IWindowCallbackProxy {
fun createCallBack(
callback: Window.Callback?, isDialog: Boolean, tag: String?
): Window.Callback?
}

View File

@@ -0,0 +1,25 @@
package com.nnbc123.library.common.fragmentation.windowcallback
import android.view.Window
/**
* author: wushaocheng
* time: 2022/11/15
* desc: WindowCallback代理类工具
*/
object WindowCallbackProxyUtil {
var mCallbackProxy: IWindowCallbackProxy? = null
@JvmStatic
fun setWindowCallbackProxy(proxy: IWindowCallbackProxy?) {
mCallbackProxy = proxy
}
@JvmStatic
@JvmOverloads
fun createWindowCallBack(
callback: Window.Callback?, isDialog: Boolean = false, tag: String? = null
): Window.Callback? {
return mCallbackProxy?.createCallBack(callback, isDialog, tag)
}
}

View File

@@ -0,0 +1,73 @@
package com.nnbc123.library.common.glide;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import com.nnbc123.library.easyphoto.engine.ImageEngine;
/**
* Created by wushaocheng on 2021/3/29 15:18.
* DescGlide实现类
*/
public class GlideEngine implements ImageEngine {
/**
* 加载图片到ImageView
*
* @param context 上下文
* @param uri 图片路径Uri
* @param imageView 加载到的ImageView
*/
//安卓10推荐uri并且path的方式不再可用
@Override
public void loadPhoto(@NonNull Context context, @NonNull Uri uri, @NonNull ImageView imageView) {
GlideUtils.instance().loadUriCrossFade(uri, imageView);
}
/**
* 加载gif动图图片到ImageViewgif动图不动
*
* @param context 上下文
* @param gifUri gif动图路径Uri
* @param imageView 加载到的ImageView
* <p>
* 备注:不支持动图显示的情况下可以不写
*/
//安卓10推荐uri并且path的方式不再可用
@Override
public void loadGifAsBitmap(@NonNull Context context, @NonNull Uri gifUri, @NonNull ImageView imageView) {
GlideUtils.instance().loadUriGift(gifUri, imageView);
}
/**
* 加载gif动图到ImageViewgif动图动
*
* @param context 上下文
* @param gifUri gif动图路径Uri
* @param imageView 加载动图的ImageView
* <p>
* 备注:不支持动图显示的情况下可以不写
*/
//安卓10推荐uri并且path的方式不再可用
@Override
public void loadGif(@NonNull Context context, @NonNull Uri gifUri, @NonNull ImageView imageView) {
GlideUtils.instance().loadUriGiftAndCrossFade(gifUri, imageView);
}
/**
* 获取图片加载框架中的缓存Bitmap不用拼图功能可以直接返回null
*
* @param context 上下文
* @param uri 图片路径
* @param width 图片宽度
* @param height 图片高度
* @return Bitmap
* @throws Exception 异常直接抛出EasyPhotos内部处理
*/
//安卓10推荐uri并且path的方式不再可用
@Override
public Bitmap getCacheBitmap(@NonNull Context context, @NonNull Uri uri, int width, int height) throws Exception {
return null;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,226 @@
package com.nnbc123.library.common.photo
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Environment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.nnbc123.library.common.application.BaseApp
import com.nnbc123.library.common.delegate.SpDelegate
import com.nnbc123.library.common.file.FileHelper
import com.nnbc123.library.common.glide.GlideEngine
import com.nnbc123.library.common.util.Logger
import com.nnbc123.library.easyphoto.EasyPhotos
import com.nnbc123.library.easyphoto.constant.Type.*
import com.nnbc123.library.easyphoto.models.album.entity.Photo
import com.nnbc123.library.utils.TimeUtils
import com.nnbc123.library.utils.TimeUtils.TIME_FORMAT
import kotlinx.coroutines.*
import java.io.File
/**
* Created by wushaocheng on 2022/11/15
* Desc图片选择二次封装
*/
object PhotoProviderNew {
private const val TAG = "PhotoProvider"
//上一次选择的时间,避免用户连续进入选择图片页导致删除缓存
private var mLastSelectTime: Long by SpDelegate("PhotoProvider_last_select_time", 0L)
/**
* easyPhoto库选择文件copy到内部的目录名
*/
private const val FOLD_EASY_PHOTO_INTERNAL = "selectPhotoTemp"
private var mPhotoJob: Job? = null
@JvmStatic
@JvmOverloads
fun photoCamera(fragment: Fragment, resultCode: Int, isClearCache: Boolean = true) {
cancelJop()
mPhotoJob = MainScope().launch {
if (isClearCache && isClearByTime()) {
withContext(Dispatchers.IO) { clearCache() }
}
EasyPhotos.createCamera(fragment, false)//参数说明:上下文,是否使用宽高数据false时宽高数据为0扫描速度更快
.setFileProviderAuthority("${BaseApp.getApplication().packageName}.fileprovider")//参数说明:见下方`FileProvider的配置`
.start(resultCode)
}
}
/**
* 喵圈发布动态专用去掉bmp格式的图片
*/
@JvmStatic
@JvmOverloads
fun photoProviderPublish(activity: Activity, maxSelect: Int = 1, canChooseGif: Boolean = false, resultCode: Int, isClearCache: Boolean = true) {
cancelJop()
mPhotoJob = MainScope().launch {
if (isClearCache && isClearByTime()) {
withContext(Dispatchers.IO) { clearCache() }
}
EasyPhotos.createAlbum(activity, false, false, GlideEngine())//参数说明上下文是否显示相机按钮是否使用宽高数据false时宽高数据为0扫描速度更快[配置Glide为图片加载引擎](https://github.com/HuanTanSheng/EasyPhotos/wiki/12-%E9%85%8D%E7%BD%AEImageEngine%EF%BC%8C%E6%94%AF%E6%8C%81%E6%89%80%E6%9C%89%E5%9B%BE%E7%89%87%E5%8A%A0%E8%BD%BD%E5%BA%93)
.setCount(maxSelect)//参数说明最大可选数默认1
.setGif(canChooseGif)
.filter(JPEG, JPG, PNG, WEBP)
.setPuzzleMenu(false)
.setCleanMenu(false)
.start(resultCode)
}
}
@JvmStatic
@JvmOverloads
fun videoProvider(activity: Activity, maxSelect: Int = 1, resultCode: Int, isClearCache: Boolean = true) {
cancelJop()
mPhotoJob = MainScope().launch {
if (isClearCache && isClearByTime()) {
withContext(Dispatchers.IO) { clearCache() }
}
EasyPhotos.createAlbum(activity, false, false, GlideEngine())
.setCount(maxSelect)//参数说明最大可选数默认1
.setPuzzleMenu(false)
.onlyVideo()
.setCleanMenu(false)
.start(resultCode)
}
}
@JvmStatic
@JvmOverloads
fun photoProvider(activity: Activity, maxSelect: Int = 1, canChooseGif: Boolean = false, resultCode: Int, isClearCache: Boolean = true) {
cancelJop()
mPhotoJob = MainScope().launch {
if (isClearCache && isClearByTime()) {
withContext(Dispatchers.IO) { clearCache() }
}
EasyPhotos.createAlbum(activity, false, false, GlideEngine())//参数说明上下文是否显示相机按钮是否使用宽高数据false时宽高数据为0扫描速度更快[配置Glide为图片加载引擎](https://github.com/HuanTanSheng/EasyPhotos/wiki/12-%E9%85%8D%E7%BD%AEImageEngine%EF%BC%8C%E6%94%AF%E6%8C%81%E6%89%80%E6%9C%89%E5%9B%BE%E7%89%87%E5%8A%A0%E8%BD%BD%E5%BA%93)
.setCount(maxSelect)//参数说明最大可选数默认1
.setGif(canChooseGif)
.setPuzzleMenu(false)
.setCleanMenu(false)
.start(resultCode)
}
}
@JvmStatic
@JvmOverloads
fun photoProvider(activity: FragmentActivity, maxSelect: Int = 1, canChooseGif: Boolean = false, resultCode: Int, isClearCache: Boolean = true) {
cancelJop()
mPhotoJob = MainScope().launch {
if (isClearCache && isClearByTime()) {
withContext(Dispatchers.IO) { clearCache() }
}
EasyPhotos.createAlbum(activity, false, false, GlideEngine())//参数说明上下文是否显示相机按钮是否使用宽高数据false时宽高数据为0扫描速度更快[配置Glide为图片加载引擎](https://github.com/HuanTanSheng/EasyPhotos/wiki/12-%E9%85%8D%E7%BD%AEImageEngine%EF%BC%8C%E6%94%AF%E6%8C%81%E6%89%80%E6%9C%89%E5%9B%BE%E7%89%87%E5%8A%A0%E8%BD%BD%E5%BA%93)
.setCount(maxSelect)//参数说明最大可选数默认1
.setGif(canChooseGif)
.setPuzzleMenu(false)
.setCleanMenu(false)
.start(resultCode)
}
}
@JvmStatic
@JvmOverloads
fun photoProvider(fragment: Fragment, maxSelect: Int = 1, canChooseGif: Boolean = false, resultCode: Int, isClearCache: Boolean = true, minFileSize: Long = 0L) {
cancelJop()
mPhotoJob = MainScope().launch {
if (isClearCache && isClearByTime()) {
withContext(Dispatchers.IO) { clearCache() }
}
EasyPhotos.createAlbum(fragment, false, false, GlideEngine())//参数说明上下文是否显示相机按钮是否使用宽高数据false时宽高数据为0扫描速度更快[配置Glide为图片加载引擎](https://github.com/HuanTanSheng/EasyPhotos/wiki/12-%E9%85%8D%E7%BD%AEImageEngine%EF%BC%8C%E6%94%AF%E6%8C%81%E6%89%80%E6%9C%89%E5%9B%BE%E7%89%87%E5%8A%A0%E8%BD%BD%E5%BA%93)
.setCount(maxSelect)//参数说明最大可选数默认1
.setGif(canChooseGif)
.setMinFileSize(minFileSize)
.setPuzzleMenu(false)
.setCleanMenu(false)
.start(resultCode)
}
}
@JvmStatic
fun getResultUriList(data: Intent?): List<Uri>? {
val list: List<Photo>? = data?.getParcelableArrayListExtra(EasyPhotos.RESULT_PHOTOS)
return list?.takeIf { it.isNotEmpty() }?.map { it.uri }
}
@JvmStatic
fun getResultPhotoList(data: Intent?): List<Photo>? {
return data?.getParcelableArrayListExtra(EasyPhotos.RESULT_PHOTOS)
}
@JvmStatic
fun getResultPathListAsync(data: Intent?, resultListener: ((List<String>?) -> Unit)) {
cancelJop()
mPhotoJob = MainScope().launch {
val list: List<Photo>? = data?.getParcelableArrayListExtra(EasyPhotos.RESULT_PHOTOS)
val result = withContext(Dispatchers.IO) { copyToInternalCache(list) }
resultListener.invoke(result)
}
}
/**
* 外部的文件复制到项目目录下,再获取对应路径
* 修改方案原因:
* 1. android Q 外部文件path变更为类似这种结构/external/images/media/{文件id}导致无法通过path读取文件信息文件名字及格式
* 2. android Q 支持Uri获取文件但uri获取不到文件类型及不少自身或者sdk的函数从参数需要用到path
* 3. 原本项目功能逻辑很多用到了path(包括不仅仅文件大小,文件类型,作为参数传给其他函数使用(比如BitmapFactory.decodeFile))直接全局替换为Uri,影响面过大直接copy一份到自己内部返回内部的路径使得外部调用无感知
*
* 发现几个重点问题:
* 1. 项目使用到BitmapFactory.decodeFile(imgPath, options)之类的方法该方法在android Q直接使用外部path测试中发现获取图片信息失败
*
*/
private fun copyToInternalCache(photos: List<Photo>?): List<String>? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val foldPath = getInternalPath() + File.separator
val newPaths = ArrayList<String>()
photos?.forEach {
if (it.uri != null && !it.name.isNullOrEmpty()) {
val path = "$foldPath${it.name}"
if (FileHelper.copyFileFromUri(it.uri, path, true)) {
newPaths.add(path)
Logger.debug(TAG, "path: ${it.path} , displayName: ${it.name} , newPath: $path ")
}
}
}
newPaths
} else {
photos?.takeIf { it.isNotEmpty() }?.map { it.path }
}
}
/**
* 清除复制缓存
*/
fun clearCache() {
Logger.debug(
TAG, "clearCache => mLastSelectTime: ${TimeUtils.getDateTimeString(
mLastSelectTime, TIME_FORMAT)}")
FileHelper.removeAllFile(getInternalPath() + File.separator)
}
/**
* 检查时间,判断是否要删除缓冲
*/
private fun isClearByTime(): Boolean {
val currentTime = System.currentTimeMillis()
val isClear = currentTime - mLastSelectTime > 10 * 60 * 1000
mLastSelectTime = currentTime
return isClear
}
private fun cancelJop() {
if (mPhotoJob?.isActive == true) {
mPhotoJob?.cancel()
}
}
/**
* easyPhoto内部复制缓存的路径
*/
private fun getInternalPath(): String {
return ("${FileHelper.getRootFilesDir(Environment.DIRECTORY_PICTURES).absolutePath}${File.separator}$FOLD_EASY_PHOTO_INTERNAL")
}
}

View File

@@ -0,0 +1,96 @@
package com.nnbc123.library.common.transform
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.RectF
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import java.security.MessageDigest
/**
* author: lishangming
* e-mail: lishangming@miya818.com
* time: 2021/09/17
* desc: glide转化器原图片宽高短的一边设置为指定目标值长的自适应拉伸按照指定宽高比例和截取方式进行截取
*/
class AssignScaleTransformation(private val targetSize: Int, private val whRadio: Float, private val clipType: ClipType) : BitmapTransformation() {
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
messageDigest.update(("AssignScaleTransformation(${targetSize}_${whRadio}_${clipType})").toByteArray())
}
override fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
return crop(pool, toTransform) ?: toTransform
}
private fun crop(pool: BitmapPool, source: Bitmap?): Bitmap? {
if (targetSize <= 0 || whRadio <= 0) return null
if (source == null) return null
//计算截取原文件的宽高
var clipWidth = 0
var clipHeight = 0
when {
source.width / whRadio <= source.height -> {
//用宽度计算,按比例拉伸高度比原本的小,故宽度不变,高度按比例设置
clipWidth = source.width
clipHeight = (source.width / whRadio).toInt()
}
source.height * whRadio <= source.width -> {
//用高度计算,按比例拉伸宽度比原本的小,故高度不变,宽度按比例设置
clipWidth = (source.height * whRadio).toInt()
clipHeight = source.height
}
else -> {
clipWidth = source.width
clipHeight = source.height
}
}
//需要生成图片的宽高
val resultWidth = targetSize
val resultHeight = (targetSize / whRadio).toInt()
//截取比例
val left = if (clipWidth < source.width) (source.width - clipWidth) / 2 else 0
val right = left + clipWidth
var result: Bitmap? = pool[resultWidth, resultHeight, Bitmap.Config.ARGB_8888]
if (result == null) {
result = Bitmap.createBitmap(resultWidth, resultHeight, Bitmap.Config.ARGB_8888)
}
val targetRect = RectF(0f, 0f, resultWidth.toFloat(), resultHeight.toFloat())
val sourceRect = when (clipType) {
ClipType.TOP -> {
//顶部截取,固定截取高度
Rect(left, 0, right, clipHeight)
}
else -> {
//默认中间截取
val top = if (clipHeight < source.height) (source.height - clipHeight) / 2 else 0
val bottom = top + clipHeight
Rect(left, top, right, bottom)
}
}
if (result != null) {
val canvas = Canvas(result)
if (clipWidth < source.width || clipHeight < source.height) {
canvas.drawBitmap(source, sourceRect, targetRect, null)
} else {
canvas.drawBitmap(source, null, targetRect, null)
}
}
return result
}
override fun equals(obj: Any?): Boolean {
return obj is AssignScaleTransformation
}
override fun hashCode(): Int {
return javaClass.name.hashCode()
}
enum class ClipType {
TOP,//截取顶部
CENTER,//截取中间
}
}

View File

@@ -0,0 +1,152 @@
package com.nnbc123.library.common.transform;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
import java.security.MessageDigest;
/**
* Created by zhl on 2020/1/3.
*/
public class ComplexTransformation extends BitmapTransformation {
private ComplexParamsBuilder mBuilder;
public ComplexTransformation(@NonNull ComplexParamsBuilder builder) {
mBuilder = builder;
}
@Override
protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
return crop(pool, toTransform, mBuilder);
}
private static Bitmap crop(BitmapPool pool, Bitmap source, ComplexParamsBuilder builder) {
if (source == null) return null;
int resultWidth = builder.mMaxWidth == 0 ? source.getWidth() : builder.mMaxWidth;
int resultHeight = builder.mMaxHeight == 0 ? source.getHeight() : builder.mMaxHeight;
if (builder.mIsNeedScale) {
if ((source.getWidth() > resultWidth || source.getHeight() > resultHeight)) {
float scaleX = (float) resultWidth / source.getWidth();
float scaleY = (float) resultHeight / source.getHeight();
float scale = Math.min(scaleX, scaleY);
resultWidth = (int) (scale * source.getWidth());
resultHeight = (int) (scale * source.getHeight());
} else {
resultWidth = source.getWidth();
resultHeight = source.getHeight();
}
}
Bitmap result = pool.get(resultWidth, resultHeight, Bitmap.Config.ARGB_8888);
RectF targetRect = new RectF(0, 0, resultWidth, resultHeight);
Canvas canvas = new Canvas(result);
if (builder.mIsNeedCorner && source.getWidth() == resultWidth && source.getHeight() == resultHeight) {
//该图没有过缩放,可直接绘制圆角
Paint paint = new Paint();
paint.setShader(new BitmapShader(source, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP));
paint.setAntiAlias(true);
canvas.drawRoundRect(targetRect, builder.mCorner, builder.mCorner, paint);
return result;
}
if (builder.mIsNeedCropCenter && source.getHeight() != source.getWidth()) {
int min = Math.min(source.getWidth(), source.getHeight());
Rect sourceRect = new Rect((source.getWidth() - min) / 2,
(source.getHeight() - min) / 2,
(source.getWidth() - min) / 2 + min,
(source.getHeight() - min) / 2 + min);
canvas.drawBitmap(source, sourceRect, targetRect, null);
} else {
canvas.drawBitmap(source, null, targetRect, null);
}
if (builder.mIsNeedCorner) {
Paint paint = new Paint();
paint.setShader(new BitmapShader(result, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP));
paint.setAntiAlias(true);
Bitmap cornerResult = pool.get(resultWidth, resultHeight, Bitmap.Config.ARGB_8888);
Canvas cornerCanvas = new Canvas(cornerResult);
cornerCanvas.drawRoundRect(targetRect, builder.mCorner, builder.mCorner, paint);
return cornerResult;
}
return result;
}
@Override
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
messageDigest.update(("ComplexTransformation(" + mBuilder.toString() + ")").getBytes());
}
public static class ComplexParamsBuilder {
int mMaxWidth;
int mMaxHeight;
boolean mIsNeedScale;
boolean mIsNeedCropCenter;
boolean mIsNeedCorner;
float mCorner;
public ComplexParamsBuilder setMaxWidth(int mMaxWidth) {
this.mMaxWidth = mMaxWidth;
return this;
}
public ComplexParamsBuilder setMaxHeight(int mMaxHeight) {
this.mMaxHeight = mMaxHeight;
return this;
}
public ComplexParamsBuilder setIsNeedScale(boolean mIsNeedScale) {
this.mIsNeedScale = mIsNeedScale;
return this;
}
public ComplexParamsBuilder setIsNeedCropCenter(boolean mIsNeedCropCenter) {
this.mIsNeedCropCenter = mIsNeedCropCenter;
return this;
}
public ComplexParamsBuilder setIsNeedCorner(boolean mIsNeedCorner) {
this.mIsNeedCorner = mIsNeedCorner;
return this;
}
public ComplexParamsBuilder setCorner(float mCorner) {
this.mCorner = mCorner;
return this;
}
@Override
public String toString() {
return "ComplexParamsBuilder{" +
"mMaxWidth=" + mMaxWidth +
", mMaxHeight=" + mMaxHeight +
", mIsNeedScale=" + mIsNeedScale +
", mIsNeedCropCenter=" + mIsNeedCropCenter +
", mIsNeedCorner=" + mIsNeedCorner +
", mCorner=" + mCorner +
'}';
}
}
@Override
public boolean equals(@Nullable Object obj) {
return obj instanceof ComplexTransformation;
}
@Override
public int hashCode() {
return getClass().getName().hashCode();
}
}

View File

@@ -0,0 +1,48 @@
package com.nnbc123.library.common.util;
import android.app.Activity;
import android.content.Context;
import android.content.ContextWrapper;
import android.view.ContextThemeWrapper;
import android.view.View;
import androidx.appcompat.widget.TintContextWrapper;
/**
* author: wushaocheng
* time: 2022/11/15
* desc: 判断Activity是否存在
*/
public class ActivityHelper {
/**
* 检查Activity是否可用
*/
public static boolean isCanUse(Activity activity) {
return activity != null && !activity.isFinishing() && !activity.isDestroyed();
}
/**
* 从View里面获取Activity对象
*/
public static Activity getActivityFromView(View view) {
Context context = view.getContext();
return getActivityFromContext(context);
}
/**
* 从Context里面获取Activity对象
*/
public static Activity getActivityFromContext(Context context) {
if (context instanceof Activity) {
return (Activity) context;
} else if (context instanceof ContextThemeWrapper && ((ContextThemeWrapper) context).getBaseContext() instanceof Activity) {
return (Activity) ((ContextWrapper) context).getBaseContext();
} else if (context instanceof TintContextWrapper) {
return (Activity) ((ContextWrapper) context).getBaseContext();
} else {
return null;
}
}
}

View File

@@ -0,0 +1,313 @@
package com.nnbc123.library.common.util
import android.content.ContentValues
import android.content.Context
import android.graphics.Bitmap
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import com.nnbc123.library.common.application.BaseApp
import com.nnbc123.library.common.file.FileHelper
import com.nnbc123.library.common.glide.GlideUtils
import java.io.*
/**
* create by ysx 2020/9/25 0025
*文件工具类,为了适配android10,11
*/
object AlbumUtils {
/**
* 兼容android Q
* 网络图片保存本地
* 返回是否保存成功
*/
fun addUrlToAlbum(context: Context?, url: String?, callback: (Boolean) -> Unit) {
if (url == null) {
callback.invoke(false)
return
}
getGlideImagePath(context, url) { glideImagePath ->
glideImagePath?.also {
val fileName: String
val imageType: String
if (glideImagePath.endsWith(".gif", true)) {
fileName = System.currentTimeMillis().toString() + ".gif"
imageType = "image/gif"
} else {
fileName = System.currentTimeMillis().toString() + ".jpg"
imageType = "image/jpeg"
}
val values = ContentValues()
values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
values.put(MediaStore.MediaColumns.MIME_TYPE, imageType)
values.put(MediaStore.MediaColumns.SIZE, 1)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
} else {
values.put(
MediaStore.MediaColumns.DATA,
"${Environment.getExternalStorageDirectory().path}/${Environment.DIRECTORY_DCIM}/$fileName"
)
}
val contentResolver = BaseApp.getContext().contentResolver
val uri =
contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
if (uri != null) {
try {
val oldFile = File(glideImagePath)
if (oldFile.exists()) { //文件存在时
val inStream = FileInputStream(glideImagePath) //读入原文件
val outputStream = contentResolver.openOutputStream(uri)
if (outputStream != null) {
val bos = BufferedOutputStream(outputStream)
val buffer = ByteArray(1024)
var bytes = inStream.read(buffer)
while (bytes >= 0) {
bos.write(buffer, 0, bytes)
bos.flush()
bytes = inStream.read(buffer)
}
bos.close()
callback.invoke(true)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
callback.invoke(false)
}
/**
* 兼容android Q
* 将bitmap保存到本地图库
* displayName 文件名 "xxx.jpg"
* mimeType "image/jpeg"
*/
@JvmOverloads
@JvmStatic
fun addBitmapToAlbum(
bitmap: Bitmap?,
displayName: String = "${System.currentTimeMillis()}.jpg",
mimeType: String = "image/jpeg",
compressFormat: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG
): Boolean {
if (bitmap == null) return false
val values = ContentValues()
values.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
} else {
values.put(
MediaStore.MediaColumns.DATA,
"${Environment.getExternalStorageDirectory().path}/${Environment.DIRECTORY_DCIM}/$displayName"
)
}
var outputStream: OutputStream? = null
try {
val contentResolver = BaseApp.getContext().contentResolver
val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
if (uri != null) {
outputStream = contentResolver.openOutputStream(uri)
if (outputStream != null) {
bitmap.compress(compressFormat, 100, outputStream)
return true
}
}
} catch (e: Exception) {
} finally {
try {
outputStream?.close()
} catch (ex: Exception) {
}
}
return false
}
/**
* 兼容android Q
* 本地缓存文件保存本地相册
* 返回是否保存成功
*/
fun addImageFileToAlbum(file: File?): Boolean {
if (file == null) return false
val start: Int = file.name.lastIndexOf(".")
val suffix: String = file.name.substring(start + 1)
val fileName: String
val imageType: String
if (suffix == "gif" || suffix == "GIF") {
fileName = System.currentTimeMillis().toString() + ".gif"
imageType = "image/gif"
} else {
fileName = System.currentTimeMillis().toString() + ".jpg"
imageType = "image/jpeg"
}
val values = ContentValues()
values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
values.put(MediaStore.MediaColumns.MIME_TYPE, imageType)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
} else {
values.put(
MediaStore.MediaColumns.DATA,
"${Environment.getExternalStorageDirectory().path}/${Environment.DIRECTORY_DCIM}/$fileName"
)
}
val contentResolver = BaseApp.getContext().contentResolver
val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
if (uri != null) {
var inStream: FileInputStream? = null
var outputStream: OutputStream? = null
var bos: BufferedOutputStream? = null
try {
if (file.exists()) { //文件存在时
inStream = FileInputStream(file) //读入原文件
outputStream = contentResolver.openOutputStream(uri)
if (outputStream != null) {
bos = BufferedOutputStream(outputStream)
val buffer = ByteArray(1024)
var bytes = inStream.read(buffer)
while (bytes >= 0) {
bos.write(buffer, 0, bytes)
bos.flush()
bytes = inStream.read(buffer)
}
return true
}
}
} catch (e: Exception) {
} finally {
try {
inStream?.close()
} catch (ex: Exception) {
}
try {
outputStream?.close()
} catch (ex: Exception) {
}
try {
bos?.close()
} catch (ex: Exception) {
}
}
}
return false
}
/**
* Glide 获得图片路径
*/
private fun getGlideImagePath(context: Context?, imgUrl: String, callback: (String?) -> Unit) {
try {
GlideUtils.instance().downloadFromUrl(context, imgUrl, object : RequestListener<File?> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<File?>?,
isFirstResource: Boolean
): Boolean {
callback.invoke("")
return false
}
override fun onResourceReady(
resource: File?,
model: Any?,
target: Target<File?>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
callback.invoke(resource?.absolutePath)
return false
}
})
} catch (e: Exception) {
callback.invoke("")
}
}
/**
* 动态获得h5图片路径
*/
fun getTrendImagePath(context: Context?, imgUrl: String, callback: (String?) -> Unit) {
try {
GlideUtils.instance().downloadFromUrl(context, imgUrl, object : RequestListener<File?> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<File?>?,
isFirstResource: Boolean
): Boolean {
callback.invoke("")
return false
}
override fun onResourceReady(
resource: File?,
model: Any?,
target: Target<File?>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
val fileDir =
FileHelper.getRootFilesDir(Environment.DIRECTORY_PICTURES).absolutePath + "/trend/test"
//获取到下载得到的图片,进行本地保存
//第二个参数为你想要保存的目录名称
val appDir = File(fileDir)
if (!appDir.exists()) {
appDir.mkdirs()
}
val fileName = System.currentTimeMillis().toString() + ".jpg"
val destFile = File(appDir, fileName)
//把gilde下载得到图片复制到定义好的目录中去
copy(resource, destFile) {
callback.invoke(destFile.absolutePath)
}
return false
}
})
} catch (e: Exception) {
callback.invoke("")
}
}
/**
* 复制文件
*
* @param source 输入文件
* @param target 输出文件
*/
fun copy(source: File?, target: File?, callback: () -> Unit) {
var fileInputStream: FileInputStream? = null
var fileOutputStream: FileOutputStream? = null
try {
fileInputStream = FileInputStream(source)
fileOutputStream = FileOutputStream(target)
val buffer = ByteArray(1024)
while (fileInputStream.read(buffer) > 0) {
fileOutputStream.write(buffer)
}
} catch (e: java.lang.Exception) {
e.printStackTrace()
} finally {
try {
fileInputStream?.close()
fileOutputStream?.close()
callback.invoke()
} catch (e: IOException) {
e.printStackTrace()
}
}
}
}

View File

@@ -0,0 +1,76 @@
package com.nnbc123.library.common.util
import android.view.View
object ClickUtils {
/***
* 设置延迟时间的View扩展
* @param delay Long 延迟时间默认600毫秒
* @return T
*/
fun <T : View> T.withTrigger(delay: Long = 600): T {
triggerDelay = delay
return this
}
/***
* 点击事件的View扩展
* @param block: (T) -> Unit 函数
* @return Unit
*/
fun <T : View> T.click(block: (T) -> Unit) = setOnClickListener {
block(it as T)
}
/***
* 带延迟过滤的点击事件View扩展
* @param delay Long 延迟时间默认600毫秒
* @param block: (T) -> Unit 函数
* @return Unit
*/
fun <T : View> T.clickWithTrigger(time: Long = 600, block: (T) -> Unit) {
triggerDelay = time
setOnClickListener {
if (clickEnable()) {
block(it as T)
}
}
}
private var <T : View> T.triggerLastTime: Long
get() = if (getTag(1123460103) != null) getTag(1123460103) as Long else -601
set(value) {
setTag(1123460103, value)
}
private var <T : View> T.triggerDelay: Long
get() = if (getTag(1123461123) != null) getTag(1123461123) as Long else 600
set(value) {
setTag(1123461123, value)
}
private fun <T : View> T.clickEnable(): Boolean {
var flag = false
val currentClickTime = System.currentTimeMillis()
if (currentClickTime - triggerLastTime >= triggerDelay) {
flag = true
triggerLastTime = currentClickTime
}
return flag
}
/***
* 带延迟过滤的点击事件监听,见[View.OnClickListener]
* 延迟时间根据triggerDelay获取600毫秒不能动态设置
*/
interface OnLazyClickListener : View.OnClickListener {
override fun onClick(v: View?) {
if (v?.clickEnable() == true) {
onLazyClick(v)
}
}
fun onLazyClick(v: View)
}
}

View File

@@ -0,0 +1,195 @@
package com.nnbc123.library.common.util;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Parcelable;
import com.tencent.mmkv.MMKV;
import java.util.Set;
/**
* author: wushaocheng
* time: 2022/11/15
* desc: 使用腾讯MMKV框架替代SharedPreference参考文档https://github.com/Tencent/MMKV/wiki/android_tutorial_cn
*/
public class Config {
private volatile static Config instance = null;
private MMKV mmkv;
public static Config getInstance(Context context) {
if (instance == null) {
synchronized (Config.class) {
if (instance == null) {
instance = new Config(context);
}
}
}
return instance;
}
private Config(Context context) {
try {
if (context == null) {
return;
}
MMKV.initialize(context);
mmkv = MMKV.mmkvWithID("config", MMKV.MULTI_PROCESS_MODE);
} catch (Exception e) {
e.printStackTrace();
}
}
public void setOnChangeListener(SharedPreferences.OnSharedPreferenceChangeListener listener) {
if (listener != null && mmkv != null) {
mmkv.registerOnSharedPreferenceChangeListener(listener);
}
}
/************************************** 编码方法 **************************************/
public boolean putBytes(String key, byte[] bytes) {
if (mmkv == null) {
return false;
}
return mmkv.encode(key, bytes);
}
public boolean putInt(String key, int value) {
if (mmkv == null) {
return false;
}
return mmkv.encode(key, value);
}
public boolean putLong(String key, long value) {
if (mmkv == null) {
return false;
}
return mmkv.encode(key, value);
}
public boolean putFloat(String key, float value) {
if (mmkv == null) {
return false;
}
return mmkv.encode(key, value);
}
public boolean putDouble(String key, double value) {
if (mmkv == null) {
return false;
}
return mmkv.encode(key, value);
}
public boolean putBoolean(String key, boolean value) {
if (mmkv == null) {
return false;
}
return mmkv.encode(key, value);
}
public boolean putString(String key, String value) {
if (mmkv == null) {
return false;
}
return mmkv.encode(key, value);
}
public boolean putStringSet(String key, Set<String> value) {
if (mmkv == null) {
return false;
}
return mmkv.encode(key, value);
}
public boolean putParcelable(String key, Parcelable value) {
if (mmkv == null) {
return false;
}
return mmkv.encode(key, value);
}
/************************************** 解码方法 **************************************/
public byte[] getBytes(String key, byte[] defaultValue) {
if (mmkv == null) {
return defaultValue;
}
return mmkv.decodeBytes(key, defaultValue);
}
public int getInt(String key, int defaultValue) {
if (mmkv == null) {
return defaultValue;
}
return mmkv.decodeInt(key, defaultValue);
}
public long getLong(String key, long defaultValue) {
if (mmkv == null) {
return defaultValue;
}
return mmkv.decodeLong(key, defaultValue);
}
public float getFloat(String key, float defaultValue) {
if (mmkv == null) {
return defaultValue;
}
return mmkv.decodeFloat(key, defaultValue);
}
public double getDouble(String key, double defaultValue) {
if (mmkv == null) {
return defaultValue;
}
return mmkv.decodeDouble(key, defaultValue);
}
public boolean getBoolean(String key, boolean defaultValue) {
if (mmkv == null) {
return defaultValue;
}
return mmkv.decodeBool(key, defaultValue);
}
public String getString(String key, String defaultValue) {
if (mmkv == null) {
return defaultValue;
}
return mmkv.decodeString(key, defaultValue);
}
public Set<String> getStringSet(String key, Set<String> defaultValue) {
if (mmkv == null) {
return defaultValue;
}
return mmkv.decodeStringSet(key, defaultValue);
}
public <T extends Parcelable> T getParcelable(String key, Class<T> tClass, T defaultValue) {
if (mmkv == null) {
return defaultValue;
}
return mmkv.decodeParcelable(key, tClass, defaultValue);
}
/************************************** 清理方法 **************************************/
public void remove(String key) {
if (mmkv == null) {
return;
}
mmkv.remove(key);
}
public void clearAll() {
if (mmkv == null) {
return;
}
mmkv.clearAll();
}
}

View File

@@ -0,0 +1,59 @@
package com.nnbc123.library.common.util;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.EventBusException;
public class CoreUtils {
private static final String TAG = "CoreUtils";
private static EventBus sEventBus;
public static <T> void send(T moduleCallback) {
if (moduleCallback == null) {
Logger.warn(TAG, "moduleCallback == null");
return;
}
try {
CoreUtils.getEventBus().post(moduleCallback);
} catch (Exception e) {
Logger.error(TAG, "EventBus exception", e);
}
}
public static <T> void register(T receiver) {
try {
if (!CoreUtils.getEventBus().isRegistered(receiver)) {
CoreUtils.getEventBus().register(receiver);
}
} catch (EventBusException e) {
//ignore
} catch (Exception e) {
Logger.error(TAG, "register error", e);
}
}
public static <T> void unregister(T receiver) {
try {
if (CoreUtils.getEventBus().isRegistered(receiver)) {
CoreUtils.getEventBus().unregister(receiver);
}
} catch (EventBusException e) {
//ignore
} catch (Exception e) {
Logger.error(TAG, "unregister error", e);
}
}
private static EventBus getEventBus() {
if (sEventBus == null) {
synchronized (CoreUtils.class) {
if (sEventBus == null) {
sEventBus = EventBus.builder().throwSubscriberException(false)
.logSubscriberExceptions(false).logNoSubscriberMessages(false)
.build();
}
}
}
return sEventBus;
}
}

View File

@@ -0,0 +1,99 @@
package com.nnbc123.library.common.util;
import java.math.BigDecimal;
import java.text.DecimalFormat;
/**
* 由于Java的简单类型不能够精确的对浮点数进行运算这个工具类提供精 确的浮点数运算,包括加减乘除和四舍五入。
*/
public class DoubleUtils{
private static final int DEF_DIV_SCALE = 2;
/**
* @Description 两个Double数相加
*
* @param d1
* @param d2
* @return Double
*/
public static Double add(Double d1,Double d2){
BigDecimal b1 = new BigDecimal(d1.toString());
BigDecimal b2 = new BigDecimal(d2.toString());
return b1.add(b2).doubleValue();
}
/**
* @Description 两个Double数相减
*
* @param d1
* @param d2
* @return Double
*/
public static Double sub(Double d1,Double d2){
BigDecimal b1 = new BigDecimal(d1.toString());
BigDecimal b2 = new BigDecimal(d2.toString());
return b1.subtract(b2).doubleValue();
}
/**
* @Description 两个Double数相乘
*
* @param d1
* @param d2
* @return Double
*/
public static Double mul(Double d1,Double d2){
BigDecimal b1 = new BigDecimal(d1.toString());
BigDecimal b2 = new BigDecimal(d2.toString());
return b1.multiply(b2).doubleValue();
}
/**
* @Description 两个Double数相除
*
* @param d1
* @param d2
* @return Double
*/
public static Double div(Double d1,Double d2){
BigDecimal b1 = new BigDecimal(d1.toString());
BigDecimal b2 = new BigDecimal(d2.toString());
return b1.divide(b2,DEF_DIV_SCALE,BigDecimal.ROUND_HALF_UP).doubleValue();
}
/**
* @Description 两个Double数相除并保留scale位小数
*
* @param d1
* @param d2
* @param scale
* @return Double
*/
public static Double div(Double d1,Double d2,int scale){
if(scale<0){
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(d1.toString());
BigDecimal b2 = new BigDecimal(d2.toString());
return b1.divide(b2,scale,BigDecimal.ROUND_HALF_UP).doubleValue();
}
/**
* @Description String类型小数与Double类型的转换
*/
public static void StrToDouble(){
String str="1234.5678";
double num;
DecimalFormat myformat = new DecimalFormat("#0.00");
num = Double.parseDouble(str);//直接转换为double类型
num = Double.parseDouble(myformat.format(num));//保留2为小数
System.out.println(num);
}
public static String convertDoubleToString(double number) {
DecimalFormat df = new DecimalFormat("######0");
return df.format(number);
}
}

View File

@@ -0,0 +1,57 @@
package com.nnbc123.library.common.util;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
public class ExecutorCenter {
private static final String TAG = "ExecutorCenter";
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static ExecutorCenter msInstance = null;
private static ScheduledExecutor mExecutor;
public static ExecutorCenter getInstance() {
if (null != msInstance) return msInstance;
synchronized (ExecutorCenter.class) {
if (null != msInstance) return msInstance;
msInstance = new ExecutorCenter();
return msInstance;
}
}
private ExecutorCenter() {
mExecutor = Pools.newScheduledThreadPoolExecutor("self-executor", CPU_COUNT + 1);
}
public Executor getExecutor() {
return mExecutor;
}
public void post(Runnable runnable) {
if (this.checkNull(runnable)) {
return;
}
mExecutor.execute(runnable);
}
public void postDelay(Runnable runnable, long delay) {
if (this.checkNull(runnable)) {
return;
}
mExecutor.execute(runnable, delay);
}
public Future submitDelay(Runnable runnable, long delay) {
if (this.checkNull(runnable)) {
return null;
}
return mExecutor.submit(runnable, delay);
}
private boolean checkNull(Runnable runnable) {
if (null == runnable) {
Logger.error(TAG, "runnable null!!!!");
return true;
}
return false;
}
}

View File

@@ -0,0 +1,62 @@
package com.nnbc123.library.common.util;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
/**
* 防快速点击工具类
*/
public class LimitClickUtils {
private Map<String, OneClick> mClickMap = new HashMap<>();
public boolean check() {
return this.check(null);
}
public boolean check(Object object) {
return this.checkForTime(object, OneClick.MIN_CLICK_DELAY_TIME);
}
public boolean checkForTime(Object object, int limitTime) {
String flag;
if (object == null) {
flag = Thread.currentThread().getStackTrace()[2].getMethodName();
} else {
flag = object.toString();
}
if (this.mClickMap.get(flag) == null) {
this.mClickMap.put(flag, new OneClick(limitTime));
}
return this.mClickMap.get(flag).check();
}
public boolean checkForTime(int limitTime) {
return this.checkForTime(null, limitTime);
}
public void destroy() {
this.mClickMap.clear();
}
private static class OneClick {
public static final int MIN_CLICK_DELAY_TIME = 1000;
private long mLastClickTime = 0L;
private int mLimitTime;
public OneClick(int limitTime) {
this.mLimitTime = limitTime;
}
public boolean check() {
long currentTime = Calendar.getInstance().getTimeInMillis();
if (currentTime - this.mLastClickTime > (long) this.mLimitTime) {
this.mLastClickTime = currentTime;
return false;
}
return true;
}
}
}

View File

@@ -0,0 +1,719 @@
/**
* Log类。可以直接使用静态函数
* 也可以用某个tag生成一个logger对象
* 使用前需要先调用init初始化
* 内部使用android的Log类实现并支持写入文件
*/
package com.nnbc123.library.common.util;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import com.nnbc123.library.utils.StringUtils;
import com.nnbc123.library.utils.config.BasicConfig;
import com.nnbc123.library.utils.log.MLog;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author daixiang
*
*/
@SuppressLint("SimpleDateFormat")
public class Logger {
public enum LogLevel {
Verbose,
Debug,
Info,
Warn,
Error
}
// 写log文件策略
public enum LogFilePolicy {
NoLogFile, // 不写文件
PerDay, // 一天只产生一个log文件
PerLaunch // 每次运行均产生一个log文件
}
public static class LogConfig {
public String dir; // log文件目录绝对路径
public LogFilePolicy policy;
public LogLevel outputLevel; // 输出级别大于等于此级别的log才会输出
public LogLevel fileLevel; // 输出到文件的级别大于等于此级别的log才会写入文件
public int fileFlushCount; // 每次累计log超过此条数时会检查是否需要flush log文件
public int fileFlushInterval; // 定时每隔一定秒数检查是否需要flush log文件
public int fileFlushMinInterval; // 距离上次flush最少需要多少秒
public LogConfig() {
policy = LogFilePolicy.PerLaunch;
outputLevel = LogLevel.Verbose;
fileLevel = LogLevel.Info;
fileFlushCount = 10;
fileFlushInterval = 60;
fileFlushMinInterval = 10;
}
public LogConfig(LogConfig cfg) {
this.dir = cfg.dir;
this.policy = cfg.policy;
this.outputLevel = cfg.outputLevel;
this.fileLevel = cfg.fileLevel;
this.fileFlushCount = cfg.fileFlushCount;
this.fileFlushInterval = cfg.fileFlushInterval;
this.fileFlushMinInterval = cfg.fileFlushMinInterval;
}
}
private static ConcurrentHashMap<String, Logger> loggers = new ConcurrentHashMap<String, Logger>();
// private static Context context;
private static LoggerThread loggerThread; // 用于在另一个线程写log文件
private static LogConfig config = new LogConfig();
// 写文件线程未准备好的时候将可以写入文件的log先缓存起来
private static List<String> logList = Collections.synchronizedList(new ArrayList<String>());
private String myTag;
private Logger(String tag) {
myTag = tag;
}
public String getTag() {
return myTag;
}
// public static void init(Context ctx) {
//
// LogConfig cfg = new LogConfig();
// if (ctx != null) {
// File f = ctx.getExternalCacheDir();
// if (f != null
// && Environment.getExternalStorageState().equals(
// Environment.MEDIA_MOUNTED)) {
// Log.i("Logger", "cache dir = " + f.getAbsolutePath());
// cfg.dir = f.getAbsolutePath() + "/logs";
// } else {
// Log.i("Logger", "no extenal storage available");
// f = ctx.getCacheDir();
// if (f != null) {
// cfg.dir = f.getAbsolutePath() + "/logs";
// }
// }
// }
// cfg.policy = LogFilePolicy.PerLaunch;
// cfg.outputLevel = LogLevel.Verbose;
// cfg.fileLevel = LogLevel.Info;
//
// Logger.init(ctx, cfg);
// }
/**
* 使用Logger之前必须先init
* @param cfg
*/
public static void init(LogConfig cfg) {
// context = ctx;
info("Logger", "init Logger");
config = new LogConfig(cfg);
// if (config.policy != LogFilePolicy.NoLogFile && loggerThread == null) {
// loggerThread = new LoggerThread("LoggerThread", config);
// loggerThread.start();
// }
initMLog(config);
}
public static void initMLog(LogConfig cfg) {
if (cfg.policy != LogFilePolicy.NoLogFile) {
String logDir = cfg.dir;
MLog.LogOptions options = new MLog.LogOptions();
if (BasicConfig.INSTANCE.isDebuggable()) {
options.logLevel = MLog.LogOptions.LEVEL_VERBOSE;
} else {
options.logLevel = MLog.LogOptions.LEVEL_INFO;
}
options.honorVerbose = false;
options.logFileName = "logs.txt";
MLog.initialize(logDir, options);
MLog.info("Logger", "init MLog, logFilePath = " + logDir + File.separator + options.logFileName);
}
}
public static Logger getLogger(String tag) {
if (StringUtils.isEmpty(tag)) {
tag = "Default";
}
Logger logger;
try {
logger = loggers.get(tag);
if (logger == null) {
logger = new Logger(tag);
loggers.put(tag, logger);
}
} catch (Exception e) {
MLog.error("Logger", "getLogger error! " + e);
logger = new Logger(tag);
}
return logger;
}
public static Logger getLogger(Class<?> cls) {
if (cls == null) {
return Logger.getLogger("");
}
// String className = cls.getName();
// String tag = className.substring(className.lastIndexOf(".") + 1);
return Logger.getLogger(cls.getSimpleName());
}
private static boolean isLoggable(LogLevel level) {
return level.compareTo(config.outputLevel) >= 0;
}
private static String levelToString(LogLevel level) {
String str = "";
switch (level) {
case Debug:
str = "Debug";
break;
case Error:
str = "Error";
break;
case Info:
str = "Info";
break;
case Verbose:
str = "Verbose";
break;
case Warn:
str = "Warn";
break;
default:
str = "Debug";
break;
}
return str;
}
public static String getLogFilePath() {
if (loggerThread != null) {
return loggerThread.getFilePath();
} else {
return null;
}
}
private static void logToFile(String tag, LogLevel level, String message, Throwable t) {
if (config.policy != LogFilePolicy.NoLogFile) {
if (loggerThread == null || !loggerThread.isReady()) {
// 文件线程未准备好,先缓存
logList.add(LoggerThread.getFormattedString(tag, level, message));
} else {
loggerThread.logToFile(tag, level, message, t);
}
}
}
public static void log(String tag, LogLevel level, String message) {
if (Logger.isLoggable(level)) {
message = msgForTextLog(tag, message);
switch (level) {
case Debug:
// Log.d(tag, message);
MLog.debugWithoutLineNumber(tag, message);
break;
case Error:
// Log.e(tag, message);
MLog.errorWithoutLineNumber(tag, message);
break;
case Info:
// Log.i(tag, message);
MLog.infoWithoutLineNumber(tag, message);
break;
case Verbose:
// Log.v(tag, message);
MLog.verboseWithoutLineNumber(tag, message);
break;
case Warn:
// Log.w(tag, message);
MLog.warnWithoutLineNumber(tag, message);
break;
default:
// Log.d(tag, message);
MLog.debugWithoutLineNumber(tag, message);
break;
}
// logToFile(tag, level, message, null);
}
}
private static void logError(String tag, String msg, Throwable tr) {
if (Logger.isLoggable(LogLevel.Error)) {
// msg = msgForTextLog(tag, msg);
if (tr == null) {
// Log.e(tag, msg);
MLog.error(tag, msg);
} else {
// Log.e(tag, msg, tr);
MLog.error(tag, msg, tr);
}
// logToFile(tag, LogLevel.Error, msg, tr);
}
}
// public static void log(String tag, LogLevel level, String message, Throwable throwable) {
// switch (level) {
// case Debug:
// Log.d(tag, message, throwable);
// break;
// case Error:
// Log.e(tag, message, throwable);
// break;
// case Info:
// Log.i(tag, message, throwable);
// break;
// case Verbose:
// Log.v(tag, message, throwable);
// break;
// case Warn:
// Log.v(tag, message, throwable);
// break;
// default:
// Log.d(tag, message, throwable);
// break;
// }
// }
private static String msgForTextLog(String tag, String message) {
if (message == null) {
message = "null";
}
int line = -1;
String filename = null;
if (Thread.currentThread().getStackTrace().length > 4) {
line = Thread.currentThread().getStackTrace()[4].getLineNumber();
filename = Thread.currentThread().getStackTrace()[4].getFileName();
}
StringBuilder sb = new StringBuilder();
sb.append("[");
sb.append(tag);
sb.append("] ");
sb.append(message);
sb.append("(P:");
sb.append(android.os.Process.myPid());
sb.append(")");
sb.append("(T:");
if (Looper.getMainLooper() == Looper.myLooper())
sb.append("Main&");
else
sb.append(Thread.currentThread().getId());
sb.append(")");
// sb.append("(C:");
// sb.append(tag);
// sb.append(")");
if (filename != null) {
sb.append(" at (");
sb.append(filename);
}
if (line > 0) {
sb.append(":");
sb.append(line);
sb.append(")");
}
return sb.toString();
}
public static void verbose(String tag, String message) {
// message = msgForTextLog(tag, message);
Logger.log(tag, LogLevel.Verbose, message);
}
public static void debug(String tag, String message) {
// message = msgForTextLog(tag, message);
Logger.log(tag, LogLevel.Debug, message);
}
public static void info(String tag, String message) {
// message = msgForTextLog(tag, message);
Logger.log(tag, LogLevel.Info, message);
}
public static void warn(String tag, String message) {
// message = msgForTextLog(tag, message);
Logger.log(tag, LogLevel.Warn, message);
}
public static void error(String tag, String message) {
// message = msgForTextLog(tag, message);
Logger.log(tag, LogLevel.Error, message);
}
public static void error(String tag, String message, Throwable throwable) {
// message = msgForTextLog(tag, message);
Logger.logError(tag, message, throwable);
}
public void verbose(String message) {
Logger.verbose(myTag, message);
// message = msgForTextLog(myTag, message);
// MLog.verboseWithoutLineNumber(myTag, message);
}
public void debug(String message) {
Logger.debug(myTag, message);
// message = msgForTextLog(myTag, message);
// MLog.debugWithoutLineNumber(myTag, message);
}
public void info(String message) {
Logger.info(myTag, message);
// message = msgForTextLog(myTag, message);
// MLog.infoWithoutLineNumber(myTag, message);
}
public void warn(String message) {
Logger.warn(myTag, message);
// message = msgForTextLog(myTag, message);
// MLog.warnWithoutLineNumber(myTag, message);
}
public void error(String message) {
Logger.error(myTag, message);
// message = msgForTextLog(myTag, message);
// MLog.errorWithoutLineNumber(myTag, message);
}
public void error(String message, Throwable throwable) {
Logger.logError(myTag, message, throwable);
// message = msgForTextLog(myTag, message);
// MLog.errorWithoutLineNumber(myTag, message, throwable);
}
public static void onTerminate() {
if (loggerThread != null) {
loggerThread.sendFlush();
}
}
// private static class SdkLogger implements ILog {
//
// @Override
// public void verbose(String tag, String msg) {
//
// Logger.verbose(tag, msg);
// }
//
// @Override
// public void debug(String tag, String msg) {
//
// Logger.debug(tag, msg);
// }
//
// @Override
// public void info(String tag, String msg) {
//
// Logger.info(tag, msg);
// }
//
// @Override
// public void warn(String tag, String msg) {
//
// Logger.warn(tag, msg);
// }
//
// @Override
// public void error(String tag, String msg) {
//
// Logger.error(tag, msg);
// }
//
// @Override
// public void error(String tag, String msg, Throwable t) {
// Logger.error(tag, msg, t);
// }
//
// }
/**
* 用于写log文件的线程
* @author daixiang
*
*/
private static class LoggerThread extends Thread {
private static final int LogMessageType = 0;
private static final int TimerMessageType = 1;
private static final int LogThrowableType = 2;
private static final int FlushLog = 3;
private LogThreadHandler handler; // 使用此handler将log消息发到此线程处理
private LogConfig config;
private String filePath;
private boolean isReady = false;
public LoggerThread(String name, LogConfig cfg) {
super(name);
config = cfg;
}
public boolean isReady() {
return isReady;
}
private static String getFormattedString(String tag, LogLevel level, String msg) {
String thread = (Looper.getMainLooper() == Looper.myLooper()) ? "[Main]"
: ("[" + Thread.currentThread().getId() + "]");
String strLevel = "[" + Logger.levelToString(level) + "]";
String logMsg = thread + "[" + tag + "]" + strLevel + " " + msg;
return logMsg;
}
public void logToFile(String tag, LogLevel level, String msg, Throwable t) {
if ((config.policy != LogFilePolicy.NoLogFile)
&& (level.compareTo(config.fileLevel) >= 0)
&& (handler != null)) {
String logMsg = getFormattedString(tag, level, msg);
Message threadMessage = null;
if (t == null) {
threadMessage = handler.obtainMessage(LogMessageType);
threadMessage.obj = logMsg;
} else {
threadMessage = handler.obtainMessage(LogThrowableType);
threadMessage.obj = logMsg;
Bundle b = new Bundle();
b.putSerializable("throwable", t);
threadMessage.setData(b);
}
if (threadMessage != null) {
handler.sendMessage(threadMessage);
}
}
}
public void sendFlush() {
if (handler != null) {
handler.sendEmptyMessage(FlushLog);
}
}
// public void logToFile(String tag, LogLevel level, String msg) {
// logToFile(tag, level, msg, null);
// }
public String getFilePath() {
return filePath;
}
public void run() {
Looper.prepare();
File logDir = new File(config.dir);
if (!logDir.exists()) {
Logger.info("Logger", "create log dir: " + logDir.getAbsolutePath());
logDir.mkdirs();
}
SimpleDateFormat f;
if (config.policy == LogFilePolicy.PerLaunch) {
f = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss-SSS");
} else {
f = new SimpleDateFormat("yyyy-MM-dd");
}
filePath = config.dir + "/" + f.format(new Date()) + ".log";
Logger.info("Logger", "log file name: " + filePath);
handler = new LogThreadHandler(this);
isReady = true;
// 将之前缓存的log先写入文件
List<String> list = new ArrayList<String>(logList);
try {
if (list.size() > 0) {
Logger.debug("Logger", "write logs before logger thread ready to file: " + list.size());
for (String s : list) {
handler.writeLine(s);
}
handler.flush(true);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
logList.clear();
list.clear();
list = null;
Looper.loop();
}
private static class LogThreadHandler extends Handler {
private SimpleDateFormat dateFormat;
private BufferedWriter writer;
private LoggerThread loggerThread;
private int logCounter;
private long lastFlushTime;
private void writeLine(String formattedStr) throws IOException {
if (writer != null) {
writer.write(dateFormat.format(new Date()) + " " + formattedStr);
writer.newLine();
}
}
public LogThreadHandler(LoggerThread thread) {
loggerThread = thread;
dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
boolean append;
if (loggerThread.config.policy == LogFilePolicy.PerLaunch) {
append = false;
} else {
append = true;
}
try {
FileWriter fw = new FileWriter(loggerThread.filePath, append);
writer = new BufferedWriter(fw);
if (loggerThread.config.policy == LogFilePolicy.PerDay) {
writer.newLine();
}
// writer.write(dateFormat.format(new Date()) + " " + loggerThread.getFormattedString("Logger", loggerThread.config.fileLevel, "---------------------Log Begin---------------------"));
// writer.newLine();
// 在文件开头加入一个易于识别的行
writeLine(getFormattedString("Logger", loggerThread.config.fileLevel, "---------------------Log Begin---------------------"));
flush(true);
} catch (IOException e) {
writer = null;
e.printStackTrace();
}
if (writer != null && loggerThread.config.fileFlushInterval > 0) {
long time = loggerThread.config.fileFlushInterval * 1000;
new Timer().schedule(new TimerTask() {
@Override
public void run() {
Message msg = obtainMessage(TimerMessageType);
sendMessage(msg);
}
}, time, time);
}
}
public void flush(boolean force) throws IOException {
if (writer != null) {
long now = System.currentTimeMillis();
// 不要太频繁flush最低间隔
if ((now - lastFlushTime) > (loggerThread.config.fileFlushMinInterval * 1000)) {
writer.flush();
lastFlushTime = System.currentTimeMillis();
logCounter = 0;
} else {
logCounter++;
}
}
}
public void flushIfNeeded() throws IOException {
if (logCounter > loggerThread.config.fileFlushCount) {
flush(false);
} else {
logCounter++;
}
}
@Override
public void handleMessage(Message msg) {
if (writer == null) {
return;
}
try {
switch (msg.what) {
case LogMessageType:
{
// String str = dateFormat.format(new Date()) + " "
// + msg.obj;
// writer.write(str);
// writer.newLine();
writeLine((String)msg.obj);
flushIfNeeded();
break;
}
case LogThrowableType:
{
// String str = dateFormat.format(new Date()) + " "
// + msg.obj;
// writer.write(str);
// writer.newLine();
writeLine((String)msg.obj);
Bundle data = msg.getData();
if (data != null) {
Throwable t = (Throwable) data
.getSerializable("throwable");
if (t != null) {
PrintWriter pw = new PrintWriter(writer);
t.printStackTrace(pw);
//pw.close(); // 不能close否则内部的bufferedwriter也会被close
writer.newLine();
flush(true); // 异常立刻flush
} else {
flushIfNeeded();
}
} else {
flushIfNeeded();
}
break;
}
case TimerMessageType:
flush(false);
break;
case FlushLog:
flush(true);
break;
default:
break;
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}

View File

@@ -0,0 +1,182 @@
package com.nnbc123.library.common.util
import android.content.Context
import android.os.Environment
import android.text.TextUtils
import android.util.Log
import com.nnbc123.library.luban.Luban
import com.nnbc123.library.common.file.FileHelper
import com.nnbc123.library.common.Constants
import com.nnbc123.library.common.application.BaseApp
import kotlinx.coroutines.*
import java.io.File
object PhotoCompressUtil {
private const val TAG = "PhotoCompressUtil"
private const val TAG_IMAGE_COMPRESS = "default"
private val photoExtensions = arrayOf("jpg", "png", "jpeg", "bmp", "webp")
private fun checkIsPhoto(path: String?): Boolean {
if (TextUtils.isEmpty(path)) {
return false
}
for (extension in photoExtensions) {
if (path!!.endsWith(extension, true)) {
return true
}
}
return false
}
@JvmOverloads
@JvmStatic
fun compress(context: Context, imgList: MutableList<String>, outPath: String?, callback: PhotosCompressCallback?, leastCompressSize: Int = 200, focusAlpha: Boolean = false, maxSize: Int = Constants.UPLOAD_IMAGE_MAX_SIZE, mostCompressSize: Int = Constants.UPLOAD_IMAGE_MAX_FILE_LENGTH): Job? {
val notImgMap = HashMap<Int, String>()
val imgs = ArrayList<String>()
imgList.forEachIndexed { index, s ->
if (checkIsPhoto(s)) {
imgs.add(s)
} else {
notImgMap[index] = s
}
}
if (notImgMap.size == imgList.size) {
//纯非图片
(imgList as? ArrayList<String>)?.let { callback?.onSuccess(it) }
return null
} else {
return CoroutineScope(Dispatchers.Main + SupervisorJob()).launch {
val deferred = async(Dispatchers.IO) {
var list: MutableList<File>? = null
try {
list = Luban.with(context)
.load<String>(imgs)
.ignoreBy(leastCompressSize)
.setTargetDir(outPath)
.setFocusAlpha(focusAlpha)
.setMaxSize(maxSize)
.setMostCompressSize(mostCompressSize)
.get()
} catch (e: Exception) {
Log.e(TAG, "compress error: $e")
}
list
}
val compressedFileList = deferred.await()
if (!isActive) return@launch
if (compressedFileList.isNullOrEmpty()) {
callback?.onFail(Throwable("compress fail"))
return@launch
}
val compressedList = compressedFileList.map { it.path } as? ArrayList
try {
if (notImgMap.isNotEmpty()) {
notImgMap.forEach {
compressedList?.add(it.key, it.value)
}
}
compressedList?.let { callback?.onSuccess(it) }
} catch (e: Exception) {
callback?.onFail(Throwable("compress fail"))
}
}
}
}
@JvmOverloads
@JvmStatic
fun compress(context: Context, imgPath: String, outPath: String?, callback: PhotoCompressCallback?, leastCompressSize: Int = 200, focusAlpha: Boolean = false, maxSize: Int = Constants.UPLOAD_IMAGE_MAX_SIZE, mostCompressSize: Int = Constants.UPLOAD_IMAGE_MAX_FILE_LENGTH):Job? {
if (!checkIsPhoto(imgPath)) {
callback?.onSuccess(imgPath)
return null
} else {
return CoroutineScope(Dispatchers.Main + SupervisorJob()).launch {
val deferred = async(Dispatchers.IO) {
var list: MutableList<File>? = null
try {
list = Luban.with(context)
.load(imgPath)
.ignoreBy(leastCompressSize)
.setTargetDir(outPath)
.setFocusAlpha(focusAlpha)
.setMaxSize(maxSize)
.setMostCompressSize(mostCompressSize)
.get()
} catch (e: Exception) {
Log.e(TAG, "compress error: $e")
}
list
}
val compressedFileList = deferred.await()
if (!isActive) return@launch
if (compressedFileList.isNullOrEmpty()) {
callback?.onFail(Throwable("compress fail"))
} else {
callback?.onSuccess(compressedFileList[0].path)
}
}
}
}
/**
* 同步线程-压缩
*/
@JvmOverloads
@JvmStatic
fun synCompress(imgPath: String, outPath: String?, leastCompressSize: Int = 200, focusAlpha: Boolean = false, maxSize: Int = Constants.UPLOAD_IMAGE_MAX_SIZE, mostCompressSize: Int = Constants.UPLOAD_IMAGE_MAX_FILE_LENGTH): String? {
return if (!checkIsPhoto(imgPath)) {
imgPath
} else {
var compressedFileList: MutableList<File>? = null
try {
compressedFileList = Luban.with(BaseApp.getContext())
.load(imgPath)
.ignoreBy(leastCompressSize)
.setTargetDir(outPath)
.setFocusAlpha(focusAlpha)
.setMaxSize(maxSize)
.setMostCompressSize(mostCompressSize)
.get()
} catch (e: Exception) {
Log.e(TAG, "compress error: $e")
}
if (compressedFileList.isNullOrEmpty()) {
""
} else {
compressedFileList[0].path
}
}
}
@JvmStatic
fun getCompressCachePath(tag: String = TAG_IMAGE_COMPRESS): String {
val path = getCompressLocationPath(tag)
FileHelper.ensureDirExists(path)
return path
}
@JvmStatic
fun clearCompressCache(tag: String = TAG_IMAGE_COMPRESS) {
FileHelper.removeAllFile(getCompressLocationPath(tag))
}
private fun getCompressLocationPath(tag: String): String {
return "${FileHelper.getRootFilesDir(Environment.DIRECTORY_PICTURES).absolutePath}/compress/${tag}/"
}
}
interface PhotosCompressCallback {
fun onSuccess(compressedImgList: ArrayList<String>)
fun onFail(e: Throwable)
}
interface PhotoCompressCallback {
fun onSuccess(compressedImg: String)
fun onFail(e: Throwable)
}

View File

@@ -0,0 +1,36 @@
package com.nnbc123.library.common.util;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
public class Pools {
private static final String TAG = "Pools";
public static ScheduledExecutor newScheduledThreadPoolExecutor(String threadNamePrefix, int coreCount) {
return new ScheduledExecutorAdapter(new ScheduledThreadPoolExecutor(coreCount, new DefaultThreadFactory(threadNamePrefix)) {
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
if (t != null) {
Logger.error(TAG, String.valueOf(t));
}
}
});
}
private static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
DefaultThreadFactory(String threadNamePrefix) {
this.namePrefix = threadNamePrefix + poolNumber.getAndIncrement() + "-thread-";
}
@Override
public Thread newThread(Runnable r) {
return new Thread(r, this.namePrefix + this.threadNumber.getAndIncrement());
}
}
}

View File

@@ -0,0 +1,40 @@
package com.nnbc123.library.common.util;
import java.lang.reflect.Field;
public class ReflectionUtils {
private static final String TAG = "ReflectionUtils";
public static Field getDeclaredField(Object object, String fieldName) {
Class<?> clazz = object.getClass();
while (clazz != Object.class) {
try {
return clazz.getDeclaredField(fieldName);
} catch (Exception e) {
clazz = clazz.getSuperclass();
}
}
return null;
}
public static void setFieldValue(Object object, String fieldName, Object value) {
Field field = ReflectionUtils.getDeclaredField(object, fieldName);
try {
field.setAccessible(true);
field.set(object, value);
} catch (Exception e) {
Logger.error(TAG, "ReflectionUtils setFieldValue", e);
}
}
public static Object getFieldValue(Object object, String fieldName) {
Field field = ReflectionUtils.getDeclaredField(object, fieldName);
try {
field.setAccessible(true);
return field.get(object);
} catch (Exception e) {
Logger.error(TAG, "ReflectionUtils getFieldValue", e);
return null;
}
}
}

View File

@@ -0,0 +1,99 @@
package com.nnbc123.library.common.util;
import android.os.Parcelable;
import com.nnbc123.library.common.application.BaseApp;
import com.nnbc123.library.utils.TimeUtils;
import java.util.Date;
import java.util.Set;
/**
* author: wushaocheng
* time: 2022/11/15
* desc: 封装底层com.tcloud.core.util.Config方便使用
*/
public class SPUtils {
private static Config sConfig = Config.getInstance(BaseApp.getContext());
public static void putBytes(String key, byte[] bytes) {
sConfig.putBytes(key, bytes);
}
public static void putInt(String key, int value) {
sConfig.putInt(key, value);
}
public static void putLong(String key, long value) {
sConfig.putLong(key, value);
}
public static void putFloat(String key, float value) {
sConfig.putFloat(key, value);
}
public static void putDouble(String key, double value) {
sConfig.putDouble(key, value);
}
public static void putBoolean(String key, boolean value) {
sConfig.putBoolean(key, value);
}
public static void putString(String key, String value) {
sConfig.putString(key, value);
}
public static void putStringSet(String key, Set<String> value) {
sConfig.putStringSet(key, value);
}
public static void putParcelable(String key, Parcelable value) {
sConfig.putParcelable(key, value);
}
public static byte[] getBytes(String key, byte[] defaultValue) {
return sConfig.getBytes(key, defaultValue);
}
public static int getInt(String key, int defaultValue) {
return sConfig.getInt(key, defaultValue);
}
public static long getLong(String key, long defaultValue) {
return sConfig.getLong(key, defaultValue);
}
public static float getFloat(String key, float defaultValue) {
return sConfig.getFloat(key, defaultValue);
}
public static double getDouble(String key, double defaultValue) {
return sConfig.getDouble(key, defaultValue);
}
public static boolean getBoolean(String key, boolean defaultValue) {
return sConfig.getBoolean(key, defaultValue);
}
public static String getString(String key, String defaultValue) {
return sConfig.getString(key, defaultValue);
}
public static Set<String> getStringSet(String key, Set<String> defaultValue) {
return sConfig.getStringSet(key, defaultValue);
}
public static <T extends Parcelable> T getParcelable(String key, Class<T> tClass, T defaultValue) {
return sConfig.getParcelable(key, tClass, defaultValue);
}
public static void remove(String key) {
sConfig.remove(key);
}
public static void clearAll() {
sConfig.clearAll();
}
}

View File

@@ -0,0 +1,11 @@
package com.nnbc123.library.common.util;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
public interface ScheduledExecutor extends Executor {
void execute(Runnable runnable, long delay);
Future submit(Runnable runnable, long delay);
}

View File

@@ -0,0 +1,34 @@
package com.nnbc123.library.common.util;
import androidx.annotation.NonNull;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ScheduledExecutorAdapter implements ScheduledExecutor {
private ScheduledExecutorService mExecutor;
public ScheduledExecutorAdapter(ScheduledExecutorService executor) {
if (null == executor) {
throw new NullPointerException("ScheduledThreadPoolExecutor may not be null");
}
this.mExecutor = executor;
}
@Override
public void execute(Runnable command, long delay) {
this.mExecutor.schedule(command, delay, TimeUnit.MILLISECONDS);
}
@Override
public void execute(@NonNull Runnable command) {
this.mExecutor.execute(command);
}
@Override
public Future submit(Runnable command, long delay) {
return this.mExecutor.schedule(command, delay, TimeUnit.MILLISECONDS);
}
}

View File

@@ -0,0 +1,89 @@
package com.nnbc123.library.common.util;
import android.graphics.Rect;
import android.view.View;
import android.view.ViewTreeObserver;
import java.util.LinkedList;
import java.util.List;
public class SoftKeyboardStateHelper implements ViewTreeObserver.OnGlobalLayoutListener {
public interface SoftKeyboardStateListener {
void onSoftKeyboardOpened(int keyboardHeightInPx);
void onSoftKeyboardClosed();
}
private final List<SoftKeyboardStateListener> listeners = new LinkedList<SoftKeyboardStateListener>();
private final View activityRootView;
private int lastSoftKeyboardHeightInPx;
private boolean isSoftKeyboardOpened;
public SoftKeyboardStateHelper(View activityRootView) {
this(activityRootView, false);
}
public SoftKeyboardStateHelper(View activityRootView, boolean isSoftKeyboardOpened) {
this.activityRootView = activityRootView;
this.isSoftKeyboardOpened = isSoftKeyboardOpened;
activityRootView.getViewTreeObserver().addOnGlobalLayoutListener(this);
}
@Override
public void onGlobalLayout() {
final Rect r = new Rect();
//r will be populated with the coordinates of your view that area still visible.
activityRootView.getWindowVisibleDisplayFrame(r);
final int heightDiff = activityRootView.getRootView().getHeight() - (r.bottom - r.top);
if (!isSoftKeyboardOpened && heightDiff > 100) { // if more than 100 pixels, its probably a keyboard...
isSoftKeyboardOpened = true;
notifyOnSoftKeyboardOpened(heightDiff);
} else if (isSoftKeyboardOpened && heightDiff < 100) {
isSoftKeyboardOpened = false;
notifyOnSoftKeyboardClosed();
}
}
public void setIsSoftKeyboardOpened(boolean isSoftKeyboardOpened) {
this.isSoftKeyboardOpened = isSoftKeyboardOpened;
}
public boolean isSoftKeyboardOpened() {
return isSoftKeyboardOpened;
}
/**
* Default value is zero (0)
* @return last saved keyboard height in px
*/
public int getLastSoftKeyboardHeightInPx() {
return lastSoftKeyboardHeightInPx;
}
public void addSoftKeyboardStateListener(SoftKeyboardStateListener listener) {
listeners.add(listener);
}
public void removeSoftKeyboardStateListener(SoftKeyboardStateListener listener) {
listeners.remove(listener);
}
private void notifyOnSoftKeyboardOpened(int keyboardHeightInPx) {
this.lastSoftKeyboardHeightInPx = keyboardHeightInPx;
for (SoftKeyboardStateListener listener : listeners) {
if (listener != null) {
listener.onSoftKeyboardOpened(keyboardHeightInPx);
}
}
}
private void notifyOnSoftKeyboardClosed() {
for (SoftKeyboardStateListener listener : listeners) {
if (listener != null) {
listener.onSoftKeyboardClosed();
}
}
}
}

View File

@@ -0,0 +1,178 @@
package com.nnbc123.library.common.util;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.WindowManager;
import androidx.fragment.app.FragmentManager;
import java.lang.reflect.Field;
import java.util.List;
/**
* author: wushaocheng
* time: 2022/11/15
* desc: 转换帮助类
*/
public class Utils {
public static <T> boolean notEmpty(List<T> list) {
return !isEmpty(list);
}
public static <T> boolean isEmpty(List<T> list) {
if (list == null || list.size() == 0) {
return true;
}
return false;
}
public static int getNavigationBarHeight(Context context) {
int statusBarHeight = 0;
int resourceId = context.getResources().getIdentifier("config_showNavigationBar", "bool", "android");
if (resourceId != 0) {
resourceId = context.getResources().getIdentifier("navigation_bar_height", "dimen", "android");
//根据资源ID获取响应的尺寸值
statusBarHeight = context.getResources().getDimensionPixelSize(resourceId);
}
return statusBarHeight;
}
/**
* 判断底部navigator是否已经显示
*/
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
public static boolean hasSoftKeys(Context context) {
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display d = windowManager.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;
return (realWidth - displayWidth) > 0 || (realHeight - displayHeight) > 0;
}
// 将px值转换为dip或dp值
public static int px2dip(Context context, float pxValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5f);
}
// 将dip或dp值转换为px值
public static int dip2px(Context context, float dipValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dipValue * scale + 0.5f);
}
// 将px值转换为sp值
public static int px2sp(Context context, float pxValue) {
final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (pxValue / fontScale + 0.5f);
}
// 将sp值转换为px值
public static int sp2px(Context context, float spValue) {
final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue * fontScale + 0.5f);
}
// 屏幕宽度(像素)
public static int getWindowWidth(Context context) {
DisplayMetrics dm = context.getApplicationContext().getResources().getDisplayMetrics();
return dm.widthPixels;
}
// 屏幕高度(像素)
public static int getWindowHeight(Context context) {
DisplayMetrics dm = context.getApplicationContext().getResources().getDisplayMetrics();
return dm.heightPixels;
}
// 根据Unicode编码判断中文汉字和符号
private static boolean isChinese(char c) {
Character.UnicodeBlock ub = Character.UnicodeBlock.of(c);
if (ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS || ub == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS
|| ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A || ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B
|| ub == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION || ub == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS
|| ub == Character.UnicodeBlock.GENERAL_PUNCTUATION) {
return true;
}
return false;
}
// 判断中文汉字和符号
public static boolean isChinese(String strName) {
char[] ch = strName.toCharArray();
for (int i = 0; i < ch.length; i++) {
char c = ch[i];
if (isChinese(c)) {
return true;
}
}
return false;
}
public static int getScreenPxWidth(Context context) {
WindowManager wm = (WindowManager) context
.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(outMetrics);
return outMetrics.widthPixels;
}
public static int getScreenDpWidth(Context context) {
int pxWidth = getScreenPxWidth(context);
return (int) px2dip(context, pxWidth);
}
public static int getScreenPxHeight(Context context) {
WindowManager wm = (WindowManager) context
.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(outMetrics);
return outMetrics.heightPixels;
}
public static int getScreenDpHeight(Context context) {
int pxHeight = getScreenPxHeight(context);
return (int) px2dip(context, pxHeight);
}
public static void executePendingTransactionsSafely(String TAG, FragmentManager fragmentManager) {
if (fragmentManager == null) {
Logger.error(TAG, "executePendingTransactionsSafely fragmentManager == null");
return;
}
try {
fragmentManager.executePendingTransactions();
} catch (Exception e) {
Logger.error(TAG, String.valueOf(e));
try {
Field mExecutingActions = fragmentManager.getClass().getDeclaredField("mExecutingActions");
mExecutingActions.setAccessible(true);
mExecutingActions.set(fragmentManager, false);
} catch (Exception e1) {
Logger.error(TAG, "set field value fail", e1);
}
}
}
}

View File

@@ -0,0 +1,96 @@
package com.nnbc123.library.common.util
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.viewbinding.ViewBinding
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.ParameterizedType
/**
* author: wushaocheng
* time: 2022/11/15
* desc: viewBinding fragment、activity绑定布局
* @see 参考 https://github.com/DylanCaiCoding/ViewBindingKTX/blob/master/viewbinding-base/src/main/java/com/dylanc/viewbinding/base/ViewBindingUtil.kt
*/
object ViewBindingUtil {
private const val TAG = "ViewBindingUtil"
@JvmStatic
fun <VB : ViewBinding> inflateWithActivity(
genericOwner: Any, layoutInflater: LayoutInflater
): VB? = withGenericBindingClass<VB>(genericOwner) { clazz ->
clazz.getMethod("inflate", LayoutInflater::class.java).invoke(null, layoutInflater) as? VB
}
@JvmStatic
fun <VB : ViewBinding> inflateWithFragment(
genericOwner: Any,
layoutInflater: LayoutInflater,
parent: ViewGroup?,
): VB? = withGenericBindingClass<VB>(genericOwner) { clazz ->
clazz.getMethod(
"inflate", LayoutInflater::class.java, ViewGroup::class.java, Boolean::class.java
).invoke(null, layoutInflater, parent, false) as? VB
}
@JvmStatic
fun <VB : ViewBinding> inflateWithView(
genericOwner: Any,
layoutInflater: LayoutInflater,
parent: ViewGroup?,
): VB? = withGenericBindingClass<VB>(genericOwner) { clazz ->
var vb = try {
clazz.getMethod(
"inflate", LayoutInflater::class.java, ViewGroup::class.java, Boolean::class.java
).invoke(null, layoutInflater, parent, true) as? VB
} catch (e: Exception) {
//merge 标签会找不到
Logger.debug(
TAG,
"inflateWithView => maybe use merge " + genericOwner.javaClass.simpleName + ", genericSuperclass: " + genericOwner.javaClass.genericSuperclass
)
null
}
if (vb == null) {
//merge 标签的只有该方法
vb = clazz.getMethod(
"inflate", LayoutInflater::class.java, ViewGroup::class.java
).invoke(null, layoutInflater, parent) as? VB
}
return@withGenericBindingClass vb
}
private fun <VB : ViewBinding> withGenericBindingClass(
genericOwner: Any, block: (Class<VB>) -> VB?
): VB? {
var genericSuperclass = genericOwner.javaClass.genericSuperclass
var superclass = genericOwner.javaClass.superclass
while (superclass != null) {
if (genericSuperclass is ParameterizedType) {
genericSuperclass.actualTypeArguments.forEach {
try {
return block.invoke(it as Class<VB>)
} catch (e: NoSuchMethodException) {
} catch (e: ClassCastException) {
} catch (e: InvocationTargetException) {
Logger.error(
TAG,
"withGenericBindingClass => ${e.message}" + ", class: " + genericOwner.javaClass.simpleName
)
return null
}
}
}
genericSuperclass = superclass.genericSuperclass
superclass = superclass.superclass
}
Logger.error(
TAG,
"withGenericBindingClass " + genericOwner.javaClass.simpleName + ", genericSuperclass: " + genericOwner.javaClass.genericSuperclass
)
return null
}
}

View File

@@ -0,0 +1,70 @@
package com.nnbc123.library.common.widget
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.ViewConfiguration
import androidx.recyclerview.widget.RecyclerView
import kotlin.math.abs
/**
* author : wushaocheng
* date : 2023/2/3
* desc : 滑动冲突。斜着往上滑的时候会滑动viewpage
*/
class VpRecyclerView(context: Context, attrs: AttributeSet?) : RecyclerView(context, attrs) {
private var mTouchSlop = 0
var startX = 0 //手指碰到屏幕时的 X坐标
var startY = 0 //手机碰到屏幕时的 Y坐标
init {
val vc = ViewConfiguration.get(context)
mTouchSlop = vc.scaledTouchSlop
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
when (ev?.action) {
MotionEvent.ACTION_DOWN -> {
//当手指按下时得到了XY
startX = ev.x.toInt()
startY = ev.y.toInt()
//
parent.requestDisallowInterceptTouchEvent(true)
}
MotionEvent.ACTION_MOVE -> {
//抬起手后得到的坐标,
val endX = ev.x.toInt()
val endY = ev.y.toInt()
//得到绝对值 。
val disX = abs(endX - startX)
val disY = abs(endY - startY)
//如果X轴 大于Y 轴,说明实在左右移动 为什么?
// 屏幕坐标XY从左上角开始。00
if (disX > disY) {
Log.e("ACTIONdisX > disY:", "$disX")
//这个地方判断了左右滑动的灵敏度只有当左右滑动距离110 此时父布局才有作用,不拦截。
if (disX > 110) { //结束的时候大于
//当滑动的距离大于100的时候才不拦截parent的事件 父控件才会有用。
parent.requestDisallowInterceptTouchEvent(false)
}
} else {
// 说明是上下滑动 //canScrollVertically 检查此视图是否可以按某个方向垂直滚动。 负数表示上下滚动。正数表示左右滚动
//return true如果视图可以按指定的方向滚动否则为false。
//既然是上下滑动,此时,父控件就不能有 事件 true停止
parent.requestDisallowInterceptTouchEvent(true)
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> parent.requestDisallowInterceptTouchEvent(false)
}
return super.dispatchTouchEvent(ev)
}
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="baseViewTagID" type="id" />
</resources>

View File

@@ -0,0 +1,7 @@
<resources>
<string name="text_bitmap_too_large">上傳失敗,圖片太大啦~</string>
<string name="text_bitmap_too_small">上傳圖片不能小於20kb</string>
<string name="android_core_env_01">請輸入正確的環境</string>
<string name="android_core_env_02">請輸入正確的環境</string>
<string name="android_core_env_03">請先初始化環境</string>
</resources>

View File

@@ -0,0 +1,29 @@
/*
* 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.nnbc123.library.easypermisssion;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AfterPermissionGranted {
int value();
}

View File

@@ -0,0 +1,358 @@
package com.nnbc123.library.easypermisssion;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.StringRes;
import androidx.annotation.StyleRes;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import com.nnbc123.library.R;
/**
* Dialog to prompt the user to go to the app's settings screen and enable permissions. If the user
* clicks 'OK' on the dialog, they are sent to the settings screen. The result is returned to the
* Activity via {@see Activity#onActivityResult(int, int, Intent)}.
* <p>
* Use the {@link Builder} to create and display a dialog.
*/
public class AppSettingsDialog implements Parcelable {
private static final String TAG = "EasyPermissions";
public static final int DEFAULT_SETTINGS_REQ_CODE = 16061;
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static final Creator<AppSettingsDialog> CREATOR = new Creator<AppSettingsDialog>() {
@Override
public AppSettingsDialog createFromParcel(Parcel in) {
return new AppSettingsDialog(in);
}
@Override
public AppSettingsDialog[] newArray(int size) {
return new AppSettingsDialog[size];
}
};
static final String EXTRA_APP_SETTINGS = "extra_app_settings";
@StyleRes
private final int mThemeResId;
private final String mRationale;
private final String mTitle;
private final String mPositiveButtonText;
private final String mNegativeButtonText;
private final int mRequestCode;
private final int mIntentFlags;
private Object mActivityOrFragment;
private Context mContext;
private AppSettingsDialog(Parcel in) {
mThemeResId = in.readInt();
mRationale = in.readString();
mTitle = in.readString();
mPositiveButtonText = in.readString();
mNegativeButtonText = in.readString();
mRequestCode = in.readInt();
mIntentFlags = in.readInt();
}
private AppSettingsDialog(@NonNull final Object activityOrFragment,
@StyleRes int themeResId,
@Nullable String rationale,
@Nullable String title,
@Nullable String positiveButtonText,
@Nullable String negativeButtonText,
int requestCode,
int intentFlags) {
setActivityOrFragment(activityOrFragment);
mThemeResId = themeResId;
mRationale = rationale;
mTitle = title;
mPositiveButtonText = positiveButtonText;
mNegativeButtonText = negativeButtonText;
mRequestCode = requestCode;
mIntentFlags = intentFlags;
}
static AppSettingsDialog fromIntent(Intent intent, Activity activity) {
AppSettingsDialog dialog = intent.getParcelableExtra(AppSettingsDialog.EXTRA_APP_SETTINGS);
// It's not clear how this could happen, but in the case that it does we should try
// to avoid a runtime crash and just use the default dialog.
// https://github.com/googlesamples/easypermissions/issues/278
if (dialog == null) {
Log.e(TAG, "Intent contains null value for EXTRA_APP_SETTINGS: "
+ "intent=" + intent
+ ", "
+ "extras=" + intent.getExtras());
dialog = new Builder(activity).build();
}
dialog.setActivityOrFragment(activity);
return dialog;
}
private void setActivityOrFragment(Object activityOrFragment) {
mActivityOrFragment = activityOrFragment;
if (activityOrFragment instanceof Activity) {
mContext = (Activity) activityOrFragment;
} else if (activityOrFragment instanceof Fragment) {
mContext = ((Fragment) activityOrFragment).getContext();
} else {
throw new IllegalStateException("Unknown object: " + activityOrFragment);
}
}
private void startForResult(Intent intent) {
if (mActivityOrFragment instanceof Activity) {
((Activity) mActivityOrFragment).startActivityForResult(intent, mRequestCode);
} else if (mActivityOrFragment instanceof Fragment) {
((Fragment) mActivityOrFragment).startActivityForResult(intent, mRequestCode);
}
}
/**
* Display the built dialog.
*/
public void show() {
startForResult(AppSettingsDialogHolderActivity.createShowDialogIntent(mContext, this));
}
/**
* Show the dialog. {@link #show()} is a wrapper to ensure backwards compatibility
*/
AlertDialog showDialog(DialogInterface.OnClickListener positiveListener,
DialogInterface.OnClickListener negativeListener) {
AlertDialog.Builder builder;
if (mThemeResId != -1) {
builder = new AlertDialog.Builder(mContext, mThemeResId);
} else {
builder = new AlertDialog.Builder(mContext);
}
return builder
.setCancelable(false)
.setTitle(mTitle)
.setMessage(mRationale)
.setPositiveButton(mPositiveButtonText, positiveListener)
.setNegativeButton(mNegativeButtonText, negativeListener)
.show();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeInt(mThemeResId);
dest.writeString(mRationale);
dest.writeString(mTitle);
dest.writeString(mPositiveButtonText);
dest.writeString(mNegativeButtonText);
dest.writeInt(mRequestCode);
dest.writeInt(mIntentFlags);
}
int getIntentFlags() {
return mIntentFlags;
}
/**
* Builder for an {@link AppSettingsDialog}.
*/
public static class Builder {
private final Object mActivityOrFragment;
private final Context mContext;
@StyleRes
private int mThemeResId = -1;
private String mRationale;
private String mTitle;
private String mPositiveButtonText;
private String mNegativeButtonText;
private int mRequestCode = -1;
private boolean mOpenInNewTask = false;
/**
* Create a new Builder for an {@link AppSettingsDialog}.
*
* @param activity the {@link Activity} in which to display the dialog.
*/
public Builder(@NonNull Activity activity) {
mActivityOrFragment = activity;
mContext = activity;
}
/**
* Create a new Builder for an {@link AppSettingsDialog}.
*
* @param fragment the {@link Fragment} in which to display the dialog.
*/
public Builder(@NonNull Fragment fragment) {
mActivityOrFragment = fragment;
mContext = fragment.getContext();
}
/**
* Set the dialog theme.
*/
@NonNull
public Builder setThemeResId(@StyleRes int themeResId) {
mThemeResId = themeResId;
return this;
}
/**
* Set the title dialog. Default is "Permissions Required".
*/
@NonNull
public Builder setTitle(@Nullable String title) {
mTitle = title;
return this;
}
/**
* Set the title dialog. Default is "Permissions Required".
*/
@NonNull
public Builder setTitle(@StringRes int title) {
mTitle = mContext.getString(title);
return this;
}
/**
* Set the rationale dialog. Default is
* "This app may not work correctly without the requested permissions.
* Open the app settings screen to modify app permissions."
*/
@NonNull
public Builder setRationale(@Nullable String rationale) {
mRationale = rationale;
return this;
}
/**
* Set the rationale dialog. Default is
* "This app may not work correctly without the requested permissions.
* Open the app settings screen to modify app permissions."
*/
@NonNull
public Builder setRationale(@StringRes int rationale) {
mRationale = mContext.getString(rationale);
return this;
}
/**
* Set the positive button text, default is {@link android.R.string#ok}.
*/
@NonNull
public Builder setPositiveButton(@Nullable String text) {
mPositiveButtonText = text;
return this;
}
/**
* Set the positive button text, default is {@link android.R.string#ok}.
*/
@NonNull
public Builder setPositiveButton(@StringRes int textId) {
mPositiveButtonText = mContext.getString(textId);
return this;
}
/**
* Set the negative button text, default is {@link android.R.string#cancel}.
* <p>
* To know if a user cancelled the request, check if your permissions were given with {@link
* EasyPermissions#hasPermissions(Context, String...)} in {@see
* Activity#onActivityResult(int, int, Intent)}. If you still don't have the right
* permissions, then the request was cancelled.
*/
@NonNull
public Builder setNegativeButton(@Nullable String text) {
mNegativeButtonText = text;
return this;
}
/**
* Set the negative button text, default is {@link android.R.string#cancel}.
*/
@NonNull
public Builder setNegativeButton(@StringRes int textId) {
mNegativeButtonText = mContext.getString(textId);
return this;
}
/**
* Set the request code use when launching the Settings screen for result, can be retrieved
* in the calling Activity's {@see Activity#onActivityResult(int, int, Intent)} method.
* Default is {@link #DEFAULT_SETTINGS_REQ_CODE}.
*/
@NonNull
public Builder setRequestCode(int requestCode) {
mRequestCode = requestCode;
return this;
}
/**
* Set whether the settings screen should be opened in a separate task. This is achieved by
* setting {@link Intent#FLAG_ACTIVITY_NEW_TASK#FLAG_ACTIVITY_NEW_TASK} on
* the Intent used to open the settings screen.
*/
@NonNull
public Builder setOpenInNewTask(boolean openInNewTask) {
mOpenInNewTask = openInNewTask;
return this;
}
/**
* Build the {@link AppSettingsDialog} from the specified options. Generally followed by a
* call to {@link AppSettingsDialog#show()}.
*/
@NonNull
public AppSettingsDialog build() {
mRationale = TextUtils.isEmpty(mRationale) ?
mContext.getString(R.string.rationale_ask_again) : mRationale;
mTitle = TextUtils.isEmpty(mTitle) ?
mContext.getString(R.string.title_settings_dialog) : mTitle;
mPositiveButtonText = TextUtils.isEmpty(mPositiveButtonText) ?
mContext.getString(android.R.string.ok) : mPositiveButtonText;
mNegativeButtonText = TextUtils.isEmpty(mNegativeButtonText) ?
mContext.getString(android.R.string.cancel) : mNegativeButtonText;
mRequestCode = mRequestCode > 0 ? mRequestCode : DEFAULT_SETTINGS_REQ_CODE;
int intentFlags = 0;
if (mOpenInNewTask) {
intentFlags |= Intent.FLAG_ACTIVITY_NEW_TASK;
}
return new AppSettingsDialog(
mActivityOrFragment,
mThemeResId,
mRationale,
mTitle,
mPositiveButtonText,
mNegativeButtonText,
mRequestCode,
intentFlags);
}
}
}

View File

@@ -0,0 +1,65 @@
package com.nnbc123.library.easypermisssion;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
import androidx.annotation.RestrictTo;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class AppSettingsDialogHolderActivity extends AppCompatActivity implements DialogInterface.OnClickListener {
private static final int APP_SETTINGS_RC = 7534;
private AlertDialog mDialog;
private int mIntentFlags;
public static Intent createShowDialogIntent(Context context, AppSettingsDialog dialog) {
Intent intent = new Intent(context, AppSettingsDialogHolderActivity.class);
intent.putExtra(AppSettingsDialog.EXTRA_APP_SETTINGS, dialog);
return intent;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AppSettingsDialog appSettingsDialog = AppSettingsDialog.fromIntent(getIntent(), this);
mIntentFlags = appSettingsDialog.getIntentFlags();
mDialog = appSettingsDialog.showDialog(this, this);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mDialog != null && mDialog.isShowing()) {
mDialog.dismiss();
}
}
@Override
public void onClick(DialogInterface dialog, int which) {
if (which == Dialog.BUTTON_POSITIVE) {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
.setData(Uri.fromParts("package", getPackageName(), null));
intent.addFlags(mIntentFlags);
startActivityForResult(intent, APP_SETTINGS_RC);
} else if (which == Dialog.BUTTON_NEGATIVE) {
setResult(Activity.RESULT_CANCELED);
finish();
} else {
throw new IllegalStateException("Unknown button type: " + which);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
setResult(resultCode, data);
finish();
}
}

View File

@@ -0,0 +1,358 @@
/*
* 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.nnbc123.library.easypermisssion;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Size;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.Fragment;
import androidx.core.content.ContextCompat;
import android.util.Log;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import com.nnbc123.library.easypermisssion.helper.PermissionHelper;
/**
* Utility to request and check System permissions for apps targeting Android M (API &gt;= 23).
*/
public class EasyPermissions {
/**
* Callback interface to receive the results of {@code EasyPermissions.requestPermissions()}
* calls.
*/
public interface PermissionCallbacks extends ActivityCompat.OnRequestPermissionsResultCallback {
void onPermissionsGranted(int requestCode, @NonNull List<String> perms);
void onPermissionsDenied(int requestCode, @NonNull List<String> perms);
}
/**
* Callback interface to receive button clicked events of the rationale dialog
*/
public interface RationaleCallbacks {
void onRationaleAccepted(int requestCode);
void onRationaleDenied(int requestCode);
}
private static final String TAG = "EasyPermissions";
/**
* Check if the calling context has a set of permissions.
*
* @param context the calling context.
* @param perms one ore more permissions, such as {@link Manifest.permission#CAMERA}.
* @return true if all permissions are already granted, false if at least one permission is not
* yet granted.
* @see Manifest.permission
*/
public static boolean hasPermissions(@NonNull Context context,
@Size(min = 1) @NonNull String... perms) {
// Always return true for SDK < M, let the system deal with the permissions
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
Log.w(TAG, "hasPermissions: API version < M, returning true by default");
// DANGER ZONE!!! Changing this will break the library.
return true;
}
// Null context may be passed if we have detected Low API (less than M) so getting
// to this point with a null context should not be possible.
if (context == null) {
throw new IllegalArgumentException("Can't check permissions for null context");
}
for (String perm : perms) {
if (ContextCompat.checkSelfPermission(context, perm)
!= PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
/**
* Request a set of permissions, showing a rationale if the system requests it.
*
* @param host requesting context.
* @param rationale a message explaining why the application needs this set of permissions;
* will be displayed if the user rejects the request the first time.
* @param requestCode request code to track this request, must be &lt; 256.
* @param perms a set of permissions to be requested.
* @see Manifest.permission
*/
public static void requestPermissions(
@NonNull Activity host, @NonNull String rationale,
@IntRange(from = 0, to = 255) int requestCode, @Size(min = 1) @NonNull String... perms) {
requestPermissions(
new PermissionRequest.Builder(host, requestCode, perms)
.setRationale(rationale)
.build());
}
/**
* Request permissions from a Support Fragment with standard OK/Cancel buttons.
*
* @see #requestPermissions(Activity, String, int, String...)
*/
public static void requestPermissions(
@NonNull Fragment host, @NonNull String rationale,
@IntRange(from = 0, to = 255) int requestCode, @Size(min = 1) @NonNull String... perms) {
requestPermissions(
new PermissionRequest.Builder(host, requestCode, perms)
.setRationale(rationale)
.build());
}
/**
* Request a set of permissions.
*
* @param request the permission request
* @see PermissionRequest
*/
public static void requestPermissions(PermissionRequest request) {
// Check for permissions before dispatching the request
if (hasPermissions(request.getHelper().getContext(), request.getPerms())) {
notifyAlreadyHasPermissions(
request.getHelper().getHost(), request.getRequestCode(), request.getPerms());
return;
}
// Request permissions
request.getHelper().requestPermissions(
request.getRationale(),
request.getPositiveButtonText(),
request.getNegativeButtonText(),
request.getTheme(),
request.getRequestCode(),
request.getPerms());
}
/**
* Handle the result of a permission request, should be called from the calling {@link
* Activity}'s {@link ActivityCompat.OnRequestPermissionsResultCallback#onRequestPermissionsResult(int,
* String[], int[])} method.
* <p>
* If any permissions were granted or denied, the {@code object} will receive the appropriate
* callbacks through {@link PermissionCallbacks} and methods annotated with {@link
* AfterPermissionGranted} will be run if appropriate.
*
* @param requestCode requestCode argument to permission result callback.
* @param permissions permissions argument to permission result callback.
* @param grantResults grantResults argument to permission result callback.
* @param receivers an array of objects that have a method annotated with {@link
* AfterPermissionGranted} or implement {@link PermissionCallbacks}.
*/
public static void onRequestPermissionsResult(@IntRange(from = 0, to = 255) int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults,
@NonNull Object... receivers) {
// Make a collection of granted and denied permissions from the request.
List<String> granted = new ArrayList<>();
List<String> 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);
}
}
// iterate through all receivers
for (Object object : receivers) {
// Report granted permissions, if any.
if (!granted.isEmpty()) {
if (object instanceof PermissionCallbacks) {
((PermissionCallbacks) object).onPermissionsGranted(requestCode, granted);
}
}
// Report denied permissions, if any.
if (!denied.isEmpty()) {
if (object instanceof PermissionCallbacks) {
((PermissionCallbacks) object).onPermissionsDenied(requestCode, denied);
}
}
// If 100% successful, call annotated methods
if (!granted.isEmpty() && denied.isEmpty()) {
runAnnotatedMethods(object, requestCode);
}
}
}
/**
* Check if at least one permission in the list of denied permissions has been permanently
* denied (user clicked "Never ask again").
*
* <b>Note</b>: Due to a limitation in the information provided by the Android
* framework permissions API, this method only works after the permission
* has been denied and your app has received the onPermissionsDenied callback.
* Otherwise the library cannot distinguish permanent denial from the
* "not yet denied" case.
*
* @param host context requesting permissions.
* @param deniedPermissions list of denied permissions, usually from {@link
* PermissionCallbacks#onPermissionsDenied(int, List)}
* @return {@code true} if at least one permission in the list was permanently denied.
*/
public static boolean somePermissionPermanentlyDenied(@NonNull Activity host,
@NonNull List<String> deniedPermissions) {
return PermissionHelper.newInstance(host)
.somePermissionPermanentlyDenied(deniedPermissions);
}
/**
* @see #somePermissionPermanentlyDenied(Activity, List)
*/
public static boolean somePermissionPermanentlyDenied(@NonNull Fragment host,
@NonNull List<String> deniedPermissions) {
return PermissionHelper.newInstance(host)
.somePermissionPermanentlyDenied(deniedPermissions);
}
/**
* Check if a permission has been permanently denied (user clicked "Never ask again").
*
* @param host context requesting permissions.
* @param deniedPermission denied permission.
* @return {@code true} if the permissions has been permanently denied.
*/
public static boolean permissionPermanentlyDenied(@NonNull Activity host,
@NonNull String deniedPermission) {
return PermissionHelper.newInstance(host).permissionPermanentlyDenied(deniedPermission);
}
/**
* @see #permissionPermanentlyDenied(Activity, String)
*/
public static boolean permissionPermanentlyDenied(@NonNull Fragment host,
@NonNull String deniedPermission) {
return PermissionHelper.newInstance(host).permissionPermanentlyDenied(deniedPermission);
}
/**
* See if some denied permission has been permanently denied.
*
* @param host requesting context.
* @param perms array of permissions.
* @return true if the user has previously denied any of the {@code perms} and we should show a
* rationale, false otherwise.
*/
public static boolean somePermissionDenied(@NonNull Activity host,
@NonNull String... perms) {
return PermissionHelper.newInstance(host).somePermissionDenied(perms);
}
/**
* @see #somePermissionDenied(Activity, String...)
*/
public static boolean somePermissionDenied(@NonNull Fragment host,
@NonNull String... perms) {
return PermissionHelper.newInstance(host).somePermissionDenied(perms);
}
/**
* Run permission callbacks on an object that requested permissions but already has them by
* simulating {@link PackageManager#PERMISSION_GRANTED}.
*
* @param object the object requesting permissions.
* @param requestCode the permission request code.
* @param perms a list of permissions requested.
*/
private static void notifyAlreadyHasPermissions(@NonNull Object object,
int requestCode,
@NonNull String[] perms) {
int[] grantResults = new int[perms.length];
for (int i = 0; i < perms.length; i++) {
grantResults[i] = PackageManager.PERMISSION_GRANTED;
}
onRequestPermissionsResult(requestCode, perms, grantResults, object);
}
/**
* Find all methods annotated with {@link AfterPermissionGranted} on a given object with the
* correct requestCode argument.
*
* @param object the object with annotated methods.
* @param requestCode the requestCode passed to the annotation.
*/
private static void runAnnotatedMethods(@NonNull Object object, int requestCode) {
Class clazz = object.getClass();
if (isUsingAndroidAnnotations(object)) {
clazz = clazz.getSuperclass();
}
while (clazz != null) {
for (Method method : clazz.getDeclaredMethods()) {
AfterPermissionGranted ann = method.getAnnotation(AfterPermissionGranted.class);
if (ann != null) {
// Check for annotated methods with matching request code.
if (ann.value() == requestCode) {
// Method must be void so that we can invoke it
if (method.getParameterTypes().length > 0) {
throw new RuntimeException(
"Cannot execute method " + method.getName() + " because it is non-void method and/or has input parameters.");
}
try {
// Make method accessible if private
if (!method.isAccessible()) {
method.setAccessible(true);
}
method.invoke(object);
} catch (IllegalAccessException e) {
Log.e(TAG, "runDefaultMethod:IllegalAccessException", e);
} catch (InvocationTargetException e) {
Log.e(TAG, "runDefaultMethod:InvocationTargetException", e);
}
}
}
}
clazz = clazz.getSuperclass();
}
}
/**
* Determine if the project is using the AndroidAnnotations library.
*/
private static boolean isUsingAndroidAnnotations(@NonNull Object object) {
if (!object.getClass().getSimpleName().endsWith("_")) {
return false;
}
try {
Class clazz = Class.forName("org.androidannotations.api.view.HasViews");
return clazz.isInstance(object);
} catch (ClassNotFoundException e) {
return false;
}
}
}

View File

@@ -0,0 +1,260 @@
package com.nnbc123.library.easypermisssion;
import android.app.Activity;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.Size;
import androidx.annotation.StringRes;
import androidx.annotation.StyleRes;
import androidx.fragment.app.Fragment;
import java.util.Arrays;
import com.nnbc123.library.R;
import com.nnbc123.library.easypermisssion.helper.PermissionHelper;
/**
* An immutable model object that holds all of the parameters associated with a permission request,
* such as the permissions, request code, and rationale.
*
* @see EasyPermissions#requestPermissions(PermissionRequest)
* @see Builder
*/
public final class PermissionRequest {
private final PermissionHelper mHelper;
private final String[] mPerms;
private final int mRequestCode;
private final String mRationale;
private final String mPositiveButtonText;
private final String mNegativeButtonText;
private final int mTheme;
private PermissionRequest(PermissionHelper helper,
String[] perms,
int requestCode,
String rationale,
String positiveButtonText,
String negativeButtonText,
int theme) {
mHelper = helper;
mPerms = perms.clone();
mRequestCode = requestCode;
mRationale = rationale;
mPositiveButtonText = positiveButtonText;
mNegativeButtonText = negativeButtonText;
mTheme = theme;
}
@NonNull
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public PermissionHelper getHelper() {
return mHelper;
}
@NonNull
public String[] getPerms() {
return mPerms.clone();
}
public int getRequestCode() {
return mRequestCode;
}
@NonNull
public String getRationale() {
return mRationale;
}
@NonNull
public String getPositiveButtonText() {
return mPositiveButtonText;
}
@NonNull
public String getNegativeButtonText() {
return mNegativeButtonText;
}
@StyleRes
public int getTheme() {
return mTheme;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PermissionRequest request = (PermissionRequest) o;
return Arrays.equals(mPerms, request.mPerms) && mRequestCode == request.mRequestCode;
}
@Override
public int hashCode() {
int result = Arrays.hashCode(mPerms);
result = 31 * result + mRequestCode;
return result;
}
@Override
public String toString() {
return "PermissionRequest{" +
"mHelper=" + mHelper +
", mPerms=" + Arrays.toString(mPerms) +
", mRequestCode=" + mRequestCode +
", mRationale='" + mRationale + '\'' +
", mPositiveButtonText='" + mPositiveButtonText + '\'' +
", mNegativeButtonText='" + mNegativeButtonText + '\'' +
", mTheme=" + mTheme +
'}';
}
/**
* Builder to build a permission request with variable options.
*
* @see PermissionRequest
*/
public static final class Builder {
private final PermissionHelper mHelper;
private final int mRequestCode;
private final String[] mPerms;
private String mRationale;
private String mPositiveButtonText;
private String mNegativeButtonText;
private int mTheme = -1;
/**
* Construct a new permission request builder with a host, request code, and the requested
* permissions.
*
* @param activity the permission request host
* @param requestCode request code to track this request; must be &lt; 256
* @param perms the set of permissions to be requested
*/
public Builder(@NonNull Activity activity, int requestCode,
@NonNull @Size(min = 1) String... perms) {
mHelper = PermissionHelper.newInstance(activity);
mRequestCode = requestCode;
mPerms = perms;
}
/**
* @see #Builder(Activity, int, String...)
*/
public Builder(@NonNull Fragment fragment, int requestCode,
@NonNull @Size(min = 1) String... perms) {
mHelper = PermissionHelper.newInstance(fragment);
mRequestCode = requestCode;
mPerms = perms;
}
/**
* Set the rationale to display to the user if they don't allow your permissions on the
* first try. This rationale will be shown as long as the user has denied your permissions
* at least once, but has not yet permanently denied your permissions. Should the user
* permanently deny your permissions, use the {@link AppSettingsDialog} instead.
* <p>
*
* @param rationale the rationale to be displayed to the user should they deny your
* permission at least once
*/
@NonNull
public Builder setRationale(@Nullable String rationale) {
mRationale = rationale;
return this;
}
/**
* @param resId the string resource to be used as a rationale
* @see #setRationale(String)
*/
@NonNull
public Builder setRationale(@StringRes int resId) {
mRationale = mHelper.getContext().getString(resId);
return this;
}
/**
* Set the positive button text for the rationale dialog should it be shown.
* <p>
* The default is {@link android.R.string#ok}
*/
@NonNull
public Builder setPositiveButtonText(@Nullable String positiveButtonText) {
mPositiveButtonText = positiveButtonText;
return this;
}
/**
* @see #setPositiveButtonText(String)
*/
@NonNull
public Builder setPositiveButtonText(@StringRes int resId) {
mPositiveButtonText = mHelper.getContext().getString(resId);
return this;
}
/**
* Set the negative button text for the rationale dialog should it be shown.
* <p>
* The default is {@link android.R.string#cancel}
*/
@NonNull
public Builder setNegativeButtonText(@Nullable String negativeButtonText) {
mNegativeButtonText = negativeButtonText;
return this;
}
/**
* @see #setNegativeButtonText(String)
*/
@NonNull
public Builder setNegativeButtonText(@StringRes int resId) {
mNegativeButtonText = mHelper.getContext().getString(resId);
return this;
}
/**
* Set the theme to be used for the rationale dialog should it be shown.
*
* @param theme a style resource
*/
@NonNull
public Builder setTheme(@StyleRes int theme) {
mTheme = theme;
return this;
}
/**
* Build the permission request.
*
* @return the permission request
* @see EasyPermissions#requestPermissions(PermissionRequest)
* @see PermissionRequest
*/
@NonNull
public PermissionRequest build() {
if (mRationale == null) {
mRationale = mHelper.getContext().getString(R.string.rationale_ask);
}
if (mPositiveButtonText == null) {
mPositiveButtonText = mHelper.getContext().getString(android.R.string.ok);
}
if (mNegativeButtonText == null) {
mNegativeButtonText = mHelper.getContext().getString(android.R.string.cancel);
}
return new PermissionRequest(
mHelper,
mPerms,
mRequestCode,
mRationale,
mPositiveButtonText,
mNegativeButtonText,
mTheme);
}
}
}

View File

@@ -0,0 +1,77 @@
package com.nnbc123.library.easypermisssion;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import androidx.fragment.app.Fragment;
import java.util.Arrays;
import com.nnbc123.library.easypermisssion.helper.PermissionHelper;
/**
* Click listener for either {@link RationaleDialogFragment} or {@link RationaleDialogFragmentCompat}.
*/
class RationaleDialogClickListener implements Dialog.OnClickListener {
private Object mHost;
private RationaleDialogConfig mConfig;
private EasyPermissions.PermissionCallbacks mCallbacks;
private EasyPermissions.RationaleCallbacks mRationaleCallbacks;
RationaleDialogClickListener(RationaleDialogFragmentCompat compatDialogFragment,
RationaleDialogConfig config,
EasyPermissions.PermissionCallbacks callbacks,
EasyPermissions.RationaleCallbacks rationaleCallbacks) {
mHost = compatDialogFragment.getParentFragment() != null
? compatDialogFragment.getParentFragment()
: compatDialogFragment.getActivity();
mConfig = config;
mCallbacks = callbacks;
mRationaleCallbacks = rationaleCallbacks;
}
RationaleDialogClickListener(RationaleDialogFragment dialogFragment,
RationaleDialogConfig config,
EasyPermissions.PermissionCallbacks callbacks,
EasyPermissions.RationaleCallbacks dialogCallback) {
mHost = dialogFragment.getActivity();
mConfig = config;
mCallbacks = callbacks;
mRationaleCallbacks = dialogCallback;
}
@Override
public void onClick(DialogInterface dialog, int which) {
int requestCode = mConfig.requestCode;
if (which == Dialog.BUTTON_POSITIVE) {
String[] permissions = mConfig.permissions;
if (mRationaleCallbacks != null) {
mRationaleCallbacks.onRationaleAccepted(requestCode);
}
if (mHost instanceof Fragment) {
PermissionHelper.newInstance((Fragment) mHost).directRequestPermissions(requestCode, permissions);
} else if (mHost instanceof Activity) {
PermissionHelper.newInstance((Activity) mHost).directRequestPermissions(requestCode, permissions);
} else {
throw new RuntimeException("Host must be an Activity or Fragment!");
}
} else {
if (mRationaleCallbacks != null) {
mRationaleCallbacks.onRationaleDenied(requestCode);
}
notifyPermissionDenied();
}
}
private void notifyPermissionDenied() {
if (mCallbacks != null) {
mCallbacks.onPermissionsDenied(mConfig.requestCode, Arrays.asList(mConfig.permissions));
}
}
}

View File

@@ -0,0 +1,143 @@
package com.nnbc123.library.easypermisssion;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.StyleRes;
import androidx.appcompat.app.AlertDialog;
import com.nnbc123.library.R;
/**
* Configuration for either {@link RationaleDialogFragment} or {@link RationaleDialogFragmentCompat}.
*/
class RationaleDialogConfig {
private static final String KEY_POSITIVE_BUTTON = "positiveButton";
private static final String KEY_NEGATIVE_BUTTON = "negativeButton";
private static final String KEY_RATIONALE_MESSAGE = "rationaleMsg";
private static final String KEY_THEME = "theme";
private static final String KEY_REQUEST_CODE = "requestCode";
private static final String KEY_PERMISSIONS = "permissions";
String positiveButton;
String negativeButton;
int theme;
int requestCode;
String rationaleMsg;
String[] permissions;
RationaleDialogConfig(@NonNull String positiveButton,
@NonNull String negativeButton,
@NonNull String rationaleMsg,
@StyleRes int theme,
int requestCode,
@NonNull String[] permissions) {
this.positiveButton = positiveButton;
this.negativeButton = negativeButton;
this.rationaleMsg = rationaleMsg;
this.theme = theme;
this.requestCode = requestCode;
this.permissions = permissions;
}
RationaleDialogConfig(Bundle bundle) {
positiveButton = bundle.getString(KEY_POSITIVE_BUTTON);
negativeButton = bundle.getString(KEY_NEGATIVE_BUTTON);
rationaleMsg = bundle.getString(KEY_RATIONALE_MESSAGE);
theme = bundle.getInt(KEY_THEME);
requestCode = bundle.getInt(KEY_REQUEST_CODE);
permissions = bundle.getStringArray(KEY_PERMISSIONS);
}
Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putString(KEY_POSITIVE_BUTTON, positiveButton);
bundle.putString(KEY_NEGATIVE_BUTTON, negativeButton);
bundle.putString(KEY_RATIONALE_MESSAGE, rationaleMsg);
bundle.putInt(KEY_THEME, theme);
bundle.putInt(KEY_REQUEST_CODE, requestCode);
bundle.putStringArray(KEY_PERMISSIONS, permissions);
return bundle;
}
Dialog createDialog(Context context, Dialog.OnClickListener listener) {
Dialog dialog = new Dialog(context, R.style.Dialog_Transparent);
dialog.setCancelable(false);
dialog.setCanceledOnTouchOutside(false);
View view = LayoutInflater.from(context).inflate(R.layout.layout_permission_rationale_dialog, null);
View tv_rationale = view.findViewById(R.id.tv_rationale);
if (tv_rationale instanceof TextView) {
((TextView) tv_rationale).setText(rationaleMsg);
}
View btn_cancel = view.findViewById(R.id.btn_cancel);
if (btn_cancel instanceof Button) {
((Button) btn_cancel).setText(negativeButton);
btn_cancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dialog.dismiss();
if (listener != null) {
listener.onClick(dialog, DialogInterface.BUTTON_NEGATIVE);
}
}
});
}
View btn_ok = view.findViewById(R.id.btn_ok);
if (btn_ok instanceof Button) {
((Button) btn_ok).setText(positiveButton);
btn_ok.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dialog.dismiss();
if (listener != null) {
listener.onClick(dialog, DialogInterface.BUTTON_POSITIVE);
}
}
});
}
dialog.setContentView(view);
dialog.create();
return dialog;
}
AlertDialog createSupportDialog(Context context, Dialog.OnClickListener listener) {
AlertDialog.Builder builder;
if (theme > 0) {
builder = new AlertDialog.Builder(context, theme);
} else {
builder = new AlertDialog.Builder(context);
}
return builder
.setCancelable(false)
.setPositiveButton(positiveButton, listener)
.setNegativeButton(negativeButton, listener)
.setMessage(rationaleMsg)
.create();
}
android.app.AlertDialog createFrameworkDialog(Context context, Dialog.OnClickListener listener) {
android.app.AlertDialog.Builder builder;
if (theme > 0) {
builder = new android.app.AlertDialog.Builder(context, theme);
} else {
builder = new android.app.AlertDialog.Builder(context);
}
return builder
.setCancelable(false)
.setPositiveButton(positiveButton, listener)
.setNegativeButton(negativeButton, listener)
.setMessage(rationaleMsg)
.create();
}
}

View File

@@ -0,0 +1,113 @@
package com.nnbc123.library.easypermisssion;
import android.app.Dialog;
import android.app.DialogFragment;
import android.app.FragmentManager;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.annotation.StyleRes;
/**
* {@link DialogFragment} to display rationale for permission requests when the request comes from
* a Fragment or Activity that can host a Fragment.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class RationaleDialogFragment extends DialogFragment {
public static final String TAG = "RationaleDialogFragment";
private EasyPermissions.PermissionCallbacks mPermissionCallbacks;
private EasyPermissions.RationaleCallbacks mRationaleCallbacks;
private boolean mStateSaved = false;
public static RationaleDialogFragment newInstance(
@NonNull String positiveButton,
@NonNull String negativeButton,
@NonNull String rationaleMsg,
@StyleRes int theme,
int requestCode,
@NonNull String[] permissions) {
// Create new Fragment
RationaleDialogFragment dialogFragment = new RationaleDialogFragment();
// Initialize configuration as arguments
RationaleDialogConfig config = new RationaleDialogConfig(
positiveButton, negativeButton, rationaleMsg, theme, requestCode, permissions);
dialogFragment.setArguments(config.toBundle());
return dialogFragment;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && getParentFragment() != null) {
if (getParentFragment() instanceof EasyPermissions.PermissionCallbacks) {
mPermissionCallbacks = (EasyPermissions.PermissionCallbacks) getParentFragment();
}
if (getParentFragment() instanceof EasyPermissions.RationaleCallbacks){
mRationaleCallbacks = (EasyPermissions.RationaleCallbacks) getParentFragment();
}
}
if (context instanceof EasyPermissions.PermissionCallbacks) {
mPermissionCallbacks = (EasyPermissions.PermissionCallbacks) context;
}
if (context instanceof EasyPermissions.RationaleCallbacks) {
mRationaleCallbacks = (EasyPermissions.RationaleCallbacks) context;
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
mStateSaved = true;
super.onSaveInstanceState(outState);
}
/**
* Version of {@link #show(FragmentManager, String)} that no-ops when an IllegalStateException
* would otherwise occur.
*/
public void showAllowingStateLoss(FragmentManager manager, String tag) {
// API 26 added this convenient method
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (manager.isStateSaved()) {
return;
}
}
if (mStateSaved) {
return;
}
show(manager, tag);
}
@Override
public void onDetach() {
super.onDetach();
mPermissionCallbacks = null;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
// Rationale dialog should not be cancelable
setCancelable(false);
// Get config from arguments, create click listener
RationaleDialogConfig config = new RationaleDialogConfig(getArguments());
RationaleDialogClickListener clickListener =
new RationaleDialogClickListener(this, config, mPermissionCallbacks, mRationaleCallbacks);
// Create an AlertDialog
return config.createDialog(getActivity(), clickListener);
}
}

View File

@@ -0,0 +1,97 @@
package com.nnbc123.library.easypermisssion;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.annotation.StyleRes;
import androidx.fragment.app.FragmentManager;
import androidx.appcompat.app.AppCompatDialogFragment;
/**
* {@link AppCompatDialogFragment} to display rationale for permission requests when the request
* comes from a Fragment or Activity that can host a Fragment.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class RationaleDialogFragmentCompat extends AppCompatDialogFragment {
public static final String TAG = "RationaleDialogFragmentCompat";
private EasyPermissions.PermissionCallbacks mPermissionCallbacks;
private EasyPermissions.RationaleCallbacks mRationaleCallbacks;
public static RationaleDialogFragmentCompat newInstance(
@NonNull String rationaleMsg,
@NonNull String positiveButton,
@NonNull String negativeButton,
@StyleRes int theme,
int requestCode,
@NonNull String[] permissions) {
// Create new Fragment
RationaleDialogFragmentCompat dialogFragment = new RationaleDialogFragmentCompat();
// Initialize configuration as arguments
RationaleDialogConfig config = new RationaleDialogConfig(
positiveButton, negativeButton, rationaleMsg, theme, requestCode, permissions);
dialogFragment.setArguments(config.toBundle());
return dialogFragment;
}
/**
* Version of {@link #show(FragmentManager, String)} that no-ops when an IllegalStateException
* would otherwise occur.
*/
public void showAllowingStateLoss(FragmentManager manager, String tag) {
if (manager.isStateSaved()) {
return;
}
show(manager, tag);
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (getParentFragment() != null) {
if (getParentFragment() instanceof EasyPermissions.PermissionCallbacks) {
mPermissionCallbacks = (EasyPermissions.PermissionCallbacks) getParentFragment();
}
if (getParentFragment() instanceof EasyPermissions.RationaleCallbacks){
mRationaleCallbacks = (EasyPermissions.RationaleCallbacks) getParentFragment();
}
}
if (context instanceof EasyPermissions.PermissionCallbacks) {
mPermissionCallbacks = (EasyPermissions.PermissionCallbacks) context;
}
if (context instanceof EasyPermissions.RationaleCallbacks) {
mRationaleCallbacks = (EasyPermissions.RationaleCallbacks) context;
}
}
@Override
public void onDetach() {
super.onDetach();
mPermissionCallbacks = null;
mRationaleCallbacks = null;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
// Rationale dialog should not be cancelable
setCancelable(false);
// Get config from arguments, create click listener
RationaleDialogConfig config = new RationaleDialogConfig(getArguments());
RationaleDialogClickListener clickListener =
new RationaleDialogClickListener(this, config, mPermissionCallbacks, mRationaleCallbacks);
// Create an AlertDialog
return config.createDialog(getContext(), clickListener);
}
}

View File

@@ -0,0 +1,59 @@
package com.nnbc123.library.easypermisssion.helper;
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.StyleRes;
import androidx.core.app.ActivityCompat;
import android.util.Log;
import com.nnbc123.library.easypermisssion.RationaleDialogFragment;
/**
* Permissions helper for {@link Activity}.
*/
class ActivityPermissionHelper extends PermissionHelper<Activity> {
private static final String TAG = "ActPermissionHelper";
public ActivityPermissionHelper(Activity host) {
super(host);
}
@Override
public void directRequestPermissions(int requestCode, @NonNull String... perms) {
ActivityCompat.requestPermissions(getHost(), perms, requestCode);
}
@Override
public boolean shouldShowRequestPermissionRationale(@NonNull String perm) {
return ActivityCompat.shouldShowRequestPermissionRationale(getHost(), perm);
}
@Override
public Context getContext() {
return getHost();
}
@Override
public void showRequestPermissionRationale(@NonNull String rationale,
@NonNull String positiveButton,
@NonNull String negativeButton,
@StyleRes int theme,
int requestCode,
@NonNull String... perms) {
FragmentManager fm = getHost().getFragmentManager();
// Check if fragment is already showing
Fragment fragment = fm.findFragmentByTag(RationaleDialogFragment.TAG);
if (fragment instanceof RationaleDialogFragment) {
Log.d(TAG, "Found existing fragment, not showing rationale.");
return;
}
RationaleDialogFragment
.newInstance(positiveButton, negativeButton, rationale, theme, requestCode, perms)
.showAllowingStateLoss(fm, RationaleDialogFragment.TAG);
}
}

View File

@@ -0,0 +1,37 @@
package com.nnbc123.library.easypermisssion.helper;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.FragmentManager;
import androidx.appcompat.app.AppCompatActivity;
/**
* Permissions helper for {@link AppCompatActivity}.
*/
class AppCompatActivityPermissionsHelper extends BaseSupportPermissionsHelper<AppCompatActivity> {
public AppCompatActivityPermissionsHelper(AppCompatActivity host) {
super(host);
}
@Override
public FragmentManager getSupportFragmentManager() {
return getHost().getSupportFragmentManager();
}
@Override
public void directRequestPermissions(int requestCode, @NonNull String... perms) {
ActivityCompat.requestPermissions(getHost(), perms, requestCode);
}
@Override
public boolean shouldShowRequestPermissionRationale(@NonNull String perm) {
return ActivityCompat.shouldShowRequestPermissionRationale(getHost(), perm);
}
@Override
public Context getContext() {
return getHost();
}
}

View File

@@ -0,0 +1,45 @@
package com.nnbc123.library.easypermisssion.helper;
import androidx.annotation.NonNull;
import androidx.annotation.StyleRes;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import android.util.Log;
import com.nnbc123.library.easypermisssion.RationaleDialogFragmentCompat;
/**
* Implementation of {@link PermissionHelper} for Support Library host classes.
*/
public abstract class BaseSupportPermissionsHelper<T> extends PermissionHelper<T> {
private static final String TAG = "BSPermissionsHelper";
public BaseSupportPermissionsHelper(@NonNull T host) {
super(host);
}
public abstract FragmentManager getSupportFragmentManager();
@Override
public void showRequestPermissionRationale(@NonNull String rationale,
@NonNull String positiveButton,
@NonNull String negativeButton,
@StyleRes int theme,
int requestCode,
@NonNull String... perms) {
FragmentManager fm = getSupportFragmentManager();
// Check if fragment is already showing
Fragment fragment = fm.findFragmentByTag(RationaleDialogFragmentCompat.TAG);
if (fragment instanceof RationaleDialogFragmentCompat) {
Log.d(TAG, "Found existing fragment, not showing rationale.");
return;
}
RationaleDialogFragmentCompat
.newInstance(rationale, positiveButton, negativeButton, theme, requestCode, perms)
.showAllowingStateLoss(fm, RationaleDialogFragmentCompat.TAG);
}
}

View File

@@ -0,0 +1,47 @@
package com.nnbc123.library.easypermisssion.helper;
import android.app.Activity;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.StyleRes;
import androidx.fragment.app.Fragment;
/**
* Permissions helper for apps built against API < 23, which do not need runtime permissions.
*/
class LowApiPermissionsHelper<T> extends PermissionHelper<T> {
public LowApiPermissionsHelper(@NonNull T host) {
super(host);
}
@Override
public void directRequestPermissions(int requestCode, @NonNull String... perms) {
throw new IllegalStateException("Should never be requesting permissions on API < 23!");
}
@Override
public boolean shouldShowRequestPermissionRationale(@NonNull String perm) {
return false;
}
@Override
public void showRequestPermissionRationale(@NonNull String rationale,
@NonNull String positiveButton,
@NonNull String negativeButton,
@StyleRes int theme,
int requestCode,
@NonNull String... perms) {
throw new IllegalStateException("Should never be requesting permissions on API < 23!");
}
@Override
public Context getContext() {
if (getHost() instanceof Activity) {
return (Context) getHost();
} else if (getHost() instanceof Fragment) {
return ((Fragment) getHost()).getContext();
} else {
throw new IllegalStateException("Unknown host: " + getHost());
}
}
}

View File

@@ -0,0 +1,113 @@
package com.nnbc123.library.easypermisssion.helper;
import android.app.Activity;
import android.content.Context;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.StyleRes;
import androidx.fragment.app.Fragment;
import androidx.appcompat.app.AppCompatActivity;
import java.util.List;
/**
* Delegate class to make permission calls based on the 'host' (Fragment, Activity, etc).
*/
public abstract class PermissionHelper<T> {
private T mHost;
@NonNull
public static PermissionHelper<? extends Activity> newInstance(Activity host) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return new LowApiPermissionsHelper<>(host);
}
if (host instanceof AppCompatActivity)
return new AppCompatActivityPermissionsHelper((AppCompatActivity) host);
else {
return new ActivityPermissionHelper(host);
}
}
@NonNull
public static PermissionHelper<Fragment> newInstance(Fragment host) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return new LowApiPermissionsHelper<>(host);
}
return new SupportFragmentPermissionHelper(host);
}
// ============================================================================
// Public concrete methods
// ============================================================================
public PermissionHelper(@NonNull T host) {
mHost = host;
}
private boolean shouldShowRationale(@NonNull String... perms) {
for (String perm : perms) {
if (shouldShowRequestPermissionRationale(perm)) {
return true;
}
}
return false;
}
public void requestPermissions(@NonNull String rationale,
@NonNull String positiveButton,
@NonNull String negativeButton,
@StyleRes int theme,
int requestCode,
@NonNull String... perms) {
if (shouldShowRationale(perms)) {
showRequestPermissionRationale(
rationale, positiveButton, negativeButton, theme, requestCode, perms);
} else {
directRequestPermissions(requestCode, perms);
}
}
public boolean somePermissionPermanentlyDenied(@NonNull List<String> perms) {
for (String deniedPermission : perms) {
if (permissionPermanentlyDenied(deniedPermission)) {
return true;
}
}
return false;
}
public boolean permissionPermanentlyDenied(@NonNull String perms) {
return !shouldShowRequestPermissionRationale(perms);
}
public boolean somePermissionDenied(@NonNull String... perms) {
return shouldShowRationale(perms);
}
@NonNull
public T getHost() {
return mHost;
}
// ============================================================================
// Public abstract methods
// ============================================================================
public abstract void directRequestPermissions(int requestCode, @NonNull String... perms);
public abstract boolean shouldShowRequestPermissionRationale(@NonNull String perm);
public abstract void showRequestPermissionRationale(@NonNull String rationale,
@NonNull String positiveButton,
@NonNull String negativeButton,
@StyleRes int theme,
int requestCode,
@NonNull String... perms);
public abstract Context getContext();
}

View File

@@ -0,0 +1,36 @@
package com.nnbc123.library.easypermisssion.helper;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
/**
* Permissions helper for {@link Fragment} from the support library.
*/
class SupportFragmentPermissionHelper extends BaseSupportPermissionsHelper<Fragment> {
public SupportFragmentPermissionHelper(@NonNull Fragment host) {
super(host);
}
@Override
public FragmentManager getSupportFragmentManager() {
return getHost().getChildFragmentManager();
}
@Override
public void directRequestPermissions(int requestCode, @NonNull String... perms) {
getHost().requestPermissions(perms, requestCode);
}
@Override
public boolean shouldShowRequestPermissionRationale(@NonNull String perm) {
return getHost().shouldShowRequestPermissionRationale(perm);
}
@Override
public Context getContext() {
return getHost().getActivity();
}
}

View File

@@ -0,0 +1,4 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
package com.nnbc123.library.easypermisssion.helper;
import androidx.annotation.RestrictTo;

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="30dp" />
<solid android:color="#E6E6F0" />
</shape>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="180"
android:endColor="#13E2F5"
android:centerColor="#9DB4FF"
android:startColor="#CC67FF"
android:type="linear"
android:useLevel="true" />
<corners android:radius="30dp" />
</shape>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FFFFFF" />
<corners android:radius="20dp" />
</shape>

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/shape_white_20dp_round"
android:orientation="vertical"
tools:background="@color/black_transparent_10">
<TextView
android:id="@+id/tv_rationale"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="26dp"
android:layout_marginBottom="32dp"
android:gravity="center"
android:lineSpacingMultiplier="1.2"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:textColor="@color/color_333333"
android:textSize="14sp"
tools:text="殴打司法局案件司法局OA四季豆覅降低" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:gravity="center_horizontal"
android:orientation="horizontal">
<Button
android:id="@+id/btn_cancel"
android:layout_width="120dp"
android:layout_height="38dp"
android:layout_marginEnd="16dp"
android:background="@drawable/bg_common_cancel"
android:gravity="center"
android:text="@string/cancel"
android:textColor="@color/color_B3B3C3"
android:textSize="15sp" />
<Button
android:id="@+id/btn_ok"
android:layout_width="120dp"
android:layout_height="38dp"
android:background="@drawable/bg_common_confirm_normal"
android:gravity="center"
android:text="@string/sure"
android:textColor="@color/color_white"
android:textSize="15sp" />
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Use system defaults for devs not using AppCompat -->
<color name="colorEasyPrimary">#ff212121</color>
<color name="colorEasyPrimaryDark">@android:color/black</color>
<color name="colorEasyAccent">#ff80cbc4</color>
<color name="black_transparent_10">#1A000000</color>
<color name="color_333333">#333333</color>
<color name="color_B3B3B3">#B3B3B3</color>
<color name="color_B3B3C3">#B3B3C3</color>
<color name="color_white">#FFFFFF</color>
<color name="color_7154EE">#FFA936</color>
</resources>

View File

@@ -0,0 +1,7 @@
<resources>
<string name="rationale_ask">如果沒有請求的權限,此應用程式可能無法正常工作。</string>
<string name="rationale_ask_again">如果沒有請求的權限,此應用程式可能無法正常工作,打開應用設置頁面以修改應用權限。</string>
<string name="title_settings_dialog">所需權限</string>
<string name="sure">確定</string>
<string name="cancel">取消</string>
</resources>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="EasyPermissions" parent="Theme.AppCompat.DayNight.DarkActionBar">
<item name="colorPrimary">@color/colorEasyPrimary</item>
<item name="colorPrimaryDark">@color/colorEasyPrimaryDark</item>
<item name="colorAccent">@color/colorEasyAccent</item>
</style>
<style name="EasyPermissions.Transparent">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">true</item>
<item name="android:backgroundDimEnabled">false</item>
</style>
<style name="Dialog_Transparent" parent="@android:style/Theme.Dialog">
<item name="android:windowFrame">@null</item><!--边框-->
<item name="android:windowIsFloating">true</item><!--是否浮现在activity之上-->
<item name="android:windowIsTranslucent">false</item><!--半透明-->
<item name="android:windowNoTitle">true</item><!--无标题-->
<item name="android:windowBackground">@android:color/transparent</item><!--背景透明-->
<item name="android:backgroundDimEnabled">true</item><!--模糊-->
<item name="android:backgroundDimAmount">0.3</item>
</style>
</resources>

View File

@@ -0,0 +1,345 @@
package com.nnbc123.library.easyphoto;
import android.app.Activity;
import android.app.Fragment;
import android.content.Context;
import android.graphics.Bitmap;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentActivity;
import com.nnbc123.library.easyphoto.builder.AlbumBuilder;
import com.nnbc123.library.easyphoto.callback.PuzzleCallback;
import com.nnbc123.library.easyphoto.engine.ImageEngine;
import com.nnbc123.library.easyphoto.models.ad.AdListener;
import com.nnbc123.library.easyphoto.models.album.AlbumModel;
import com.nnbc123.library.easyphoto.models.album.entity.Photo;
import com.nnbc123.library.easyphoto.models.sticker.StickerModel;
import com.nnbc123.library.easyphoto.models.sticker.entity.TextStickerData;
import com.nnbc123.library.easyphoto.ui.PuzzleActivity;
import com.nnbc123.library.easyphoto.utils.bitmap.BitmapUtils;
import com.nnbc123.library.easyphoto.utils.bitmap.SaveBitmapCallBack;
import com.nnbc123.library.easyphoto.utils.media.MediaScannerConnectionUtils;
import com.nnbc123.library.easyphoto.utils.result.EasyResult;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* EasyPhotos的启动管理器
* Created by huan on 2017/10/18.
*/
public class EasyPhotos {
//easyPhotos的返回数据Key
public static final String RESULT_PHOTOS = "keyOfEasyPhotosResult";
public static final String RESULT_SELECTED_ORIGINAL = "keyOfEasyPhotosResultSelectedOriginal";
/**
* 预加载
* 调不调用该方法都可以不调用不影响EasyPhotos正常使用
* 第一次扫描媒体库可能会慢,调用预加载会使真正打开相册的速度加快
* 若调用该方法建议自行判断代码书写位置建议在用户打开相册的3秒前调用比如app主页面或调用相册的上一页
* 该方法如果没有授权读取权限的话,是无效的,所以外部加不加权限控制都可以,加的话保证执行,不加也不影响程序正常使用
*
* @param cxt 上下文
*/
public static void preLoad(Context cxt) {
AlbumModel.getInstance().query(cxt, null);
}
/**
* 预加载
* 调不调用该方法都可以不调用不影响EasyPhotos正常使用
* 第一次扫描媒体库可能会慢,调用预加载会使真正打开相册的速度加快
* 若调用该方法建议自行判断代码书写位置建议在用户打开相册的3秒前调用比如app主页面或调用相册的上一页
* 该方法如果没有授权读取权限的话,是无效的,所以外部加不加权限控制都可以,加的话保证执行,不加也不影响程序正常使用
*
* @param cxt 上下文
* @param callBack 预加载完成的回调若进行UI操作需自行切回主线程。
*/
public static void preLoad(Context cxt, AlbumModel.CallBack callBack) {
AlbumModel.getInstance().query(cxt, callBack);
}
/**
* 创建相机
*
* @param activity 上下文
* @param useWidth 是否使用宽高数据
* @return AlbumBuilder
*/
public static AlbumBuilder createCamera(Activity activity,
boolean useWidth) {
return AlbumBuilder.createCamera(activity).setUseWidth(useWidth);
}
public static AlbumBuilder createCamera(Fragment fragment,
boolean useWidth) {
return AlbumBuilder.createCamera(fragment).setUseWidth(useWidth);
}
public static AlbumBuilder createCamera(FragmentActivity activity,
boolean useWidth) {
return AlbumBuilder.createCamera(activity).setUseWidth(useWidth);
}
public static AlbumBuilder createCamera(androidx.fragment.app.Fragment fragmentV,
boolean useWidth) {
return AlbumBuilder.createCamera(fragmentV).setUseWidth(useWidth);
}
/**
* 创建相册
*
* @param activity 上下文
* @param isShowCamera 是否显示相机按钮
* @param useWidth 是否使用宽高数据。
* true会保证宽高数据的正确性返回速度慢耗时尤其在华为mate30上可能点击完成后会加载三四秒才能返回。
* false:有宽高数据但不保证正确性点击完成后秒回但可能有因旋转问题导致的宽高相反的情况以及极少数的宽高为0情况。
* @param imageEngine 图片加载引擎的具体实现
* @return AlbumBuilder 建造者模式配置其他选项
*/
public static AlbumBuilder createAlbum(Activity activity, boolean isShowCamera,
boolean useWidth, @NonNull ImageEngine imageEngine) {
return AlbumBuilder.createAlbum(activity, isShowCamera, imageEngine).setUseWidth(useWidth);
}
public static AlbumBuilder createAlbum(Fragment fragment, boolean isShowCamera,
boolean useWidth, @NonNull ImageEngine imageEngine) {
return AlbumBuilder.createAlbum(fragment, isShowCamera, imageEngine).setUseWidth(useWidth);
}
public static AlbumBuilder createAlbum(FragmentActivity activity, boolean isShowCamera,
boolean useWidth, @NonNull ImageEngine imageEngine) {
return AlbumBuilder.createAlbum(activity, isShowCamera, imageEngine).setUseWidth(useWidth);
}
public static AlbumBuilder createAlbum(androidx.fragment.app.Fragment fragmentV,
boolean isShowCamera, boolean useWidth,
@NonNull ImageEngine imageEngine) {
return AlbumBuilder.createAlbum(fragmentV, isShowCamera, imageEngine).setUseWidth(useWidth);
}
//*********************AD************************************
/**
* 设置广告监听
* 内部使用,无需关心
*
* @param adListener 广告监听
*/
public static void setAdListener(AdListener adListener) {
AlbumBuilder.setAdListener(adListener);
}
/**
* 刷新图片列表广告数据
*/
public static void notifyPhotosAdLoaded() {
AlbumBuilder.notifyPhotosAdLoaded();
}
/**
* 刷新专辑项目列表广告
*/
public static void notifyAlbumItemsAdLoaded() {
AlbumBuilder.notifyAlbumItemsAdLoaded();
}
//*************************bitmap功能***********************************/
/**
* 回收bitmap
*
* @param bitmap 要回收的bitmap
*/
public static void recycle(Bitmap bitmap) {
BitmapUtils.recycle(bitmap);
}
/**
* 回收bitmap数组中的所有图片
*
* @param bitmaps 要回收的bitmap数组
*/
public static void recycle(Bitmap... bitmaps) {
BitmapUtils.recycle(bitmaps);
}
/**
* 回收bitmap集合中的所有图片
*
* @param bitmaps 要回收的bitmap集合
*/
public static void recycle(List<Bitmap> bitmaps) {
BitmapUtils.recycle(bitmaps);
}
/**
* 给图片添加水印,水印会根据图片宽高自动缩放处理
*
* @param watermark 水印
* @param image 添加水印的图片
* @param srcImageWidth 水印对应的原图片宽度,即ui制作水印时参考的要添加水印的图片的宽度
* @param offsetX 添加水印的X轴偏移量
* @param offsetY 添加水印的Y轴偏移量
* @param addInLeft true 在左下角添加水印false 在右下角添加水印
* @param orientation Bitmap的旋转角度。当useWidth为true时Photo实体类中会有orientation若bitmap
* 不是用户手机内图片填0即可。
* @return 添加水印后的bitmap
*/
public static Bitmap addWatermark(Bitmap watermark, Bitmap image, int srcImageWidth,
int offsetX, int offsetY, boolean addInLeft,
int orientation) {
return BitmapUtils.addWatermark(watermark, image, srcImageWidth, offsetX, offsetY,
addInLeft, orientation);
}
/**
* 给图片添加带文字和图片的水印,水印会根据图片宽高自动缩放处理
*
* @param watermark 水印图片
* @param image 要加水印的图片
* @param srcImageWidth 水印对应的原图片宽度,即ui制作水印时参考的要添加水印的图片的宽度
* @param text 要添加的文字
* @param offsetX 添加水印的X轴偏移量
* @param offsetY 添加水印的Y轴偏移量
* @param addInLeft true 在左下角添加水印false 在右下角添加水印
* @param orientation Bitmap的旋转角度。当useWidth为true时Photo实体类中会有orientation若bitmap
* 不是用户手机内图片填0即可。
* @return 添加水印后的bitmap
*/
public static Bitmap addWatermarkWithText(Bitmap watermark, Bitmap image, int srcImageWidth,
@NonNull String text, int offsetX, int offsetY,
boolean addInLeft, int orientation) {
return BitmapUtils.addWatermarkWithText(watermark, image, srcImageWidth, text, offsetX,
offsetY,
addInLeft, orientation);
}
/**
* 保存Bitmap到指定文件夹
*
* @param act 上下文
* @param dirPath 文件夹全路径
* @param bitmap bitmap
* @param namePrefix 保存文件的前缀名,文件最终名称格式为:前缀名+自动生成的唯一数字字符+.png
* @param notifyMedia 是否更新到媒体库
* @param callBack 保存图片后的回调回调已经处于UI线程
*/
public static void saveBitmapToDir(Activity act, String dirPath, String namePrefix,
Bitmap bitmap, boolean notifyMedia,
SaveBitmapCallBack callBack) {
BitmapUtils.saveBitmapToDir(act, dirPath, namePrefix, bitmap, notifyMedia, callBack);
}
/**
* 把View画成Bitmap
*
* @param view 要处理的View
* @return Bitmap
*/
public static Bitmap createBitmapFromView(View view) {
return BitmapUtils.createBitmapFromView(view);
}
/**
* 启动拼图最多对9张图片进行拼图
*
* @param act 上下文
* @param photos 图片集合最多对9张图片进行拼图
* @param puzzleSaveDirPath 拼图完成保存的文件夹全路径
* @param puzzleSaveNamePrefix 拼图完成保存的文件名前缀,最终格式:前缀+默认生成唯一数字标识+.png
* @param requestCode 请求code
* @param replaceCustom 单击替换拼图中的某张图片时是否以startForResult的方式启动你的自定义界面该界面与传进来的act
* 为同一界面。false则在EasyPhotos内部完成正常需求直接写false即可。
* true的情况适用于用于拼图的图片集合中包含网络图片是在你的act界面中获取并下载的也可以直接用网络地址不用下载后的本地地址也就是可以不下载下来而非单纯本地相册。举例你的act中有两个按钮一个指向本地相册一个指向网络相册用户在该界面任意选择选择好图片后跳转到拼图界面用户在拼图界面点击替换按钮将会启动一个新的act界面这时act只让用户在网络相册和本地相册选择一张图片选择好执行
* Intent intent = new Intent();
* intent.putParcelableArrayListExtra(AlbumBuilder.RESULT_PHOTOS
* , photos);
* act.setResult(RESULT_OK,intent); 并关闭act回到拼图界面完成替换。
* @param imageEngine 图片加载引擎的具体实现
*/
public static void startPuzzleWithPhotos(Activity act, ArrayList<Photo> photos,
String puzzleSaveDirPath,
String puzzleSaveNamePrefix, int requestCode,
boolean replaceCustom,
@NonNull ImageEngine imageEngine) {
act.setResult(Activity.RESULT_OK);
PuzzleActivity.startWithPhotos(act, photos, puzzleSaveDirPath, puzzleSaveNamePrefix,
requestCode, replaceCustom, imageEngine);
}
public static void startPuzzleWithPhotos(FragmentActivity act, ArrayList<Photo> photos,
String puzzleSaveDirPath,
String puzzleSaveNamePrefix, boolean replaceCustom,
@NonNull ImageEngine imageEngine,
PuzzleCallback callback) {
act.setResult(Activity.RESULT_OK);
EasyResult.get(act).startPuzzleWithPhotos(photos, puzzleSaveDirPath, puzzleSaveNamePrefix
, replaceCustom, imageEngine, callback);
}
//**************更新媒体库***********************
/**
* 更新媒体文件到媒体库
*
* @param cxt 上下文
* @param filePaths 更新的文件地址
*/
public static void notifyMedia(Context cxt, String... filePaths) {
MediaScannerConnectionUtils.refresh(cxt, filePaths);
}
/**
* 更新媒体文件到媒体库
*
* @param cxt 上下文
* @param files 更新的文件
*/
public static void notifyMedia(Context cxt, File... files) {
MediaScannerConnectionUtils.refresh(cxt, files);
}
/**
* 更新媒体文件到媒体库
*
* @param cxt 上下文
* @param fileList 更新的文件地址集合
*/
public static void notifyMedia(Context cxt, List<String> fileList) {
MediaScannerConnectionUtils.refresh(cxt, fileList);
}
//*********************************贴纸***************************
/**
* 添加文字贴纸的文字数据
*
* @param textStickerData 文字贴纸的文字数据
*/
public static void addTextStickerData(TextStickerData... textStickerData) {
StickerModel.textDataList.addAll(Arrays.asList(textStickerData));
}
/**
* 清空文字贴纸的数据
*/
public static void clearTextStickerDataList() {
StickerModel.textDataList.clear();
}
}

View File

@@ -0,0 +1,618 @@
package com.nnbc123.library.easyphoto.builder;
import android.app.Activity;
import android.net.Uri;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import com.nnbc123.library.easyphoto.callback.SelectCallback;
import com.nnbc123.library.easyphoto.constant.Type;
import com.nnbc123.library.easyphoto.engine.ImageEngine;
import com.nnbc123.library.easyphoto.models.ad.AdListener;
import com.nnbc123.library.easyphoto.models.album.entity.Photo;
import com.nnbc123.library.easyphoto.result.Result;
import com.nnbc123.library.easyphoto.setting.Setting;
import com.nnbc123.library.easyphoto.ui.EasyPhotosActivity;
import com.nnbc123.library.easyphoto.utils.result.EasyResult;
import com.nnbc123.library.easyphoto.utils.uri.UriUtils;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
/**
* EasyPhotos的启动管理器
* Created by huan on 2017/10/18.
*/
public class AlbumBuilder {
/**
* 启动模式
* CAMERA-相机
* ALBUM-相册专辑
* ALBUM_CAMERA-带有相机按钮的相册专辑
*/
private enum StartupType {
CAMERA,
ALBUM,
ALBUM_CAMERA
}
private static AlbumBuilder instance;
private WeakReference<Activity> mActivity;
private WeakReference<Fragment> mFragmentV;
private WeakReference<android.app.Fragment> mFragment;
private StartupType startupType;
private WeakReference<AdListener> adListener;
//私有构造函数,不允许外部调用,真正实例化通过静态方法实现
private AlbumBuilder(Activity activity, StartupType startupType) {
mActivity = new WeakReference<>(activity);
this.startupType = startupType;
}
private AlbumBuilder(android.app.Fragment fragment, StartupType startupType) {
mFragment = new WeakReference<>(fragment);
this.startupType = startupType;
}
private AlbumBuilder(FragmentActivity activity, StartupType startupType) {
mActivity = new WeakReference<>(activity);
this.startupType = startupType;
}
private AlbumBuilder(Fragment fragment, StartupType startupType) {
mFragmentV = new WeakReference<>(fragment);
this.startupType = startupType;
}
/**
* 内部处理相机和相册的实例
*
* @param activity Activity的实例
* @return AlbumBuilder EasyPhotos的实例
*/
private static AlbumBuilder with(Activity activity, StartupType startupType) {
clear();
instance = new AlbumBuilder(activity, startupType);
return instance;
}
private static AlbumBuilder with(android.app.Fragment fragment, StartupType startupType) {
clear();
instance = new AlbumBuilder(fragment, startupType);
return instance;
}
private static AlbumBuilder with(FragmentActivity activity, StartupType startupType) {
clear();
instance = new AlbumBuilder(activity, startupType);
return instance;
}
private static AlbumBuilder with(Fragment fragmentV, StartupType startupType) {
clear();
instance = new AlbumBuilder(fragmentV, startupType);
return instance;
}
/**
* 创建相机
*
* @param activity 上下文
* @return AlbumBuilder
*/
public static AlbumBuilder createCamera(Activity activity) {
return AlbumBuilder.with(activity, StartupType.CAMERA);
}
public static AlbumBuilder createCamera(android.app.Fragment fragment) {
return AlbumBuilder.with(fragment, StartupType.CAMERA);
}
public static AlbumBuilder createCamera(FragmentActivity activity) {
return AlbumBuilder.with(activity, StartupType.CAMERA);
}
public static AlbumBuilder createCamera(Fragment fragmentV) {
return AlbumBuilder.with(fragmentV, StartupType.CAMERA);
}
/**
* 创建相册
*
* @param activity 上下文
* @param isShowCamera 是否显示相机按钮
* @param imageEngine 图片加载引擎的具体实现
*/
public static AlbumBuilder createAlbum(Activity activity, boolean isShowCamera,
@NonNull ImageEngine imageEngine) {
if (Setting.imageEngine != imageEngine) {
Setting.imageEngine = imageEngine;
}
if (isShowCamera) {
return AlbumBuilder.with(activity, StartupType.ALBUM_CAMERA);
} else {
return AlbumBuilder.with(activity, StartupType.ALBUM);
}
}
public static AlbumBuilder createAlbum(android.app.Fragment fragment, boolean isShowCamera,
@NonNull ImageEngine imageEngine) {
if (Setting.imageEngine != imageEngine) {
Setting.imageEngine = imageEngine;
}
if (isShowCamera) {
return AlbumBuilder.with(fragment, StartupType.ALBUM_CAMERA);
} else {
return AlbumBuilder.with(fragment, StartupType.ALBUM);
}
}
public static AlbumBuilder createAlbum(FragmentActivity activity, boolean isShowCamera,
@NonNull ImageEngine imageEngine) {
if (Setting.imageEngine != imageEngine) {
Setting.imageEngine = imageEngine;
}
if (isShowCamera) {
return AlbumBuilder.with(activity, StartupType.ALBUM_CAMERA);
} else {
return AlbumBuilder.with(activity, StartupType.ALBUM);
}
}
public static AlbumBuilder createAlbum(Fragment fragmentV, boolean isShowCamera,
@NonNull ImageEngine imageEngine) {
if (Setting.imageEngine != imageEngine) {
Setting.imageEngine = imageEngine;
}
if (isShowCamera) {
return AlbumBuilder.with(fragmentV, StartupType.ALBUM_CAMERA);
} else {
return AlbumBuilder.with(fragmentV, StartupType.ALBUM);
}
}
/**
* 设置fileProvider字段
*
* @param fileProviderAuthority fileProvider字段
* @return AlbumBuilder
*/
public AlbumBuilder setFileProviderAuthority(String fileProviderAuthority) {
Setting.fileProviderAuthority = fileProviderAuthority;
return AlbumBuilder.this;
}
/**
* 设置选择数
*
* @param selectorMaxCount 最大选择数
* @return AlbumBuilder
*/
public AlbumBuilder setCount(int selectorMaxCount) {
if (Setting.complexSelector) return AlbumBuilder.this;
Setting.count = selectorMaxCount;
return AlbumBuilder.this;
}
/**
* 设置是否使用宽高数据
*
* @param useWidth 是否使用宽高数据需要使用写true不用写false。
* true会保证宽高数据的正确性返回速度慢耗时。
* false:宽高数据为0。
* @return AlbumBuilder
*/
public AlbumBuilder setUseWidth(boolean useWidth) {
Setting.useWidth = useWidth;
return AlbumBuilder.this;
}
/**
* 支持复杂选择情况
*
* @param singleType 是否只能选择一种文件类型如用户选择视频后不可以选择图片若false则可以同时选择
* @param videoCount 可选择视频类型文件的最大数
* @param pictureCount 可选择图片类型文件的最大数
*/
public AlbumBuilder complexSelector(boolean singleType, int videoCount, int pictureCount) {
Setting.complexSelector = true;
Setting.complexSingleType = singleType;
Setting.complexVideoCount = videoCount;
Setting.complexPictureCount = pictureCount;
Setting.count = videoCount + pictureCount;
Setting.showVideo = true;
return AlbumBuilder.this;
}
/**
* 设置相机按钮位置
*
* @param cLocation 使用Material Design风格相机按钮 默认 BOTTOM_RIGHT
* @return AlbumBuilder
*/
public AlbumBuilder setCameraLocation(@Setting.Location int cLocation) {
Setting.cameraLocation = cLocation;
return AlbumBuilder.this;
}
/**
* 设置显示照片的最小文件大小
*
* @param minFileSize 最小文件大小单位Bytes
* @return AlbumBuilder
*/
public AlbumBuilder setMinFileSize(long minFileSize) {
Setting.minSize = minFileSize;
return AlbumBuilder.this;
}
/**
* 设置显示照片的最小宽度
*
* @param minWidth 照片的最小宽度单位Px
* @return AlbumBuilder
*/
public AlbumBuilder setMinWidth(int minWidth) {
Setting.minWidth = minWidth;
return AlbumBuilder.this;
}
/**
* 设置显示照片的最小高度
*
* @param minHeight 显示照片的最小高度单位Px
* @return AlbumBuilder
*/
public AlbumBuilder setMinHeight(int minHeight) {
Setting.minHeight = minHeight;
return AlbumBuilder.this;
}
/**
* 设置默认选择图片集合
*
* @param selectedPhotos 默认选择图片集合
* @return AlbumBuilder
*/
public AlbumBuilder setSelectedPhotos(ArrayList<Photo> selectedPhotos) {
Setting.selectedPhotos.clear();
if (selectedPhotos.isEmpty()) {
return AlbumBuilder.this;
}
Setting.selectedPhotos.addAll(selectedPhotos);
Setting.selectedOriginal = selectedPhotos.get(0).selectedOriginal;
return AlbumBuilder.this;
}
/**
* 设置默认选择图片地址集合
*
* @param selectedPhotoPaths 默认选择图片地址集合
* @return AlbumBuilder
* @Deprecated android 10 不推荐使用直接使用Path方式推荐使用Photo类
*/
@Deprecated
public AlbumBuilder setSelectedPhotoPaths(ArrayList<String> selectedPhotoPaths) {
Setting.selectedPhotos.clear();
ArrayList<Photo> selectedPhotos = new ArrayList<>();
for (String path : selectedPhotoPaths) {
File file = new File(path);
Uri uri = null;
if (null != mActivity && null != mActivity.get()) {
uri = UriUtils.getUri(mActivity.get(), file);
}
if (null != mFragment && null != mFragment.get()) {
uri = UriUtils.getUri(mFragment.get().getActivity(), file);
}
if (null != mFragmentV && null != mFragmentV.get()) {
uri = UriUtils.getUri(mFragmentV.get().getActivity(), file);
}
if (uri == null) {
uri = Uri.fromFile(file);
}
Photo photo = new Photo(null, uri, path, 0, 0, 0, 0, 0, 0, null);
selectedPhotos.add(photo);
}
Setting.selectedPhotos.addAll(selectedPhotos);
return AlbumBuilder.this;
}
/**
* 原图按钮设置,不调用该方法不显示原图按钮
*
* @param isChecked 原图选项默认状态是否为选中状态
* @param usable 原图按钮是否可使用
* @param unusableHint 原图按钮不可使用时给用户的文字提示
* @return AlbumBuilder
*/
public AlbumBuilder setOriginalMenu(boolean isChecked, boolean usable, String unusableHint) {
Setting.showOriginalMenu = true;
Setting.selectedOriginal = isChecked;
Setting.originalMenuUsable = usable;
Setting.originalMenuUnusableHint = unusableHint;
return AlbumBuilder.this;
}
/**
* 是否显示拼图按钮
*
* @param shouldShow 是否显示
* @return AlbumBuilder
*/
public AlbumBuilder setPuzzleMenu(boolean shouldShow) {
Setting.showPuzzleMenu = shouldShow;
return AlbumBuilder.this;
}
/**
* 只显示Video
*
* @return @return AlbumBuilder
*/
public AlbumBuilder onlyVideo() {
return filter(Type.VIDEO);
}
/**
* 过滤
*
* @param types {@link Type}
* @return @return AlbumBuilder
*/
public AlbumBuilder filter(String... types) {
Setting.filterTypes = Arrays.asList(types);
return AlbumBuilder.this;
}
/**
* 是否显示gif图
*
* @param shouldShow 是否显示
* @return @return AlbumBuilder
*/
public AlbumBuilder setGif(boolean shouldShow) {
Setting.showGif = shouldShow;
return AlbumBuilder.this;
}
/**
* 是否显示video
*
* @param shouldShow 是否显示
* @return @return AlbumBuilder
*/
public AlbumBuilder setVideo(boolean shouldShow) {
Setting.showVideo = shouldShow;
return AlbumBuilder.this;
}
/**
* 显示最少多少秒的视频
*
* @param second 秒
* @return @return AlbumBuilder
*/
public AlbumBuilder setVideoMinSecond(int second) {
Setting.videoMinSecond = second * 1000;
return AlbumBuilder.this;
}
/**
* 显示最多多少秒的视频
*
* @param second 秒
* @return @return AlbumBuilder
*/
public AlbumBuilder setVideoMaxSecond(int second) {
Setting.videoMaxSecond = second * 1000;
return AlbumBuilder.this;
}
/**
* 相册选择页是否显示清空按钮
*/
public AlbumBuilder setCleanMenu(boolean shouldShow) {
Setting.showCleanMenu = shouldShow;
return AlbumBuilder.this;
}
private void setSettingParams() {
switch (startupType) {
case CAMERA:
Setting.onlyStartCamera = true;
Setting.isShowCamera = true;
break;
case ALBUM:
Setting.isShowCamera = false;
break;
case ALBUM_CAMERA:
Setting.isShowCamera = true;
break;
}
if (!Setting.filterTypes.isEmpty()) {
if (Setting.isFilter(Type.GIF)) {
Setting.showGif = true;
}
if (Setting.isFilter(Type.VIDEO)) {
Setting.showVideo = true;
}
}
if (Setting.isOnlyVideo()) {
//只选择视频 暂不支持拍照/拼图等
Setting.isShowCamera = false;
Setting.showPuzzleMenu = false;
Setting.showGif = false;
Setting.showVideo = true;
}
}
/**
* 启动onActivityResult方式
*
* @param requestCode startActivityForResult的请求码
*/
public void start(int requestCode) {
setSettingParams();
launchEasyPhotosActivity(requestCode);
}
/**
* 启动,链式调用
*/
public void start(SelectCallback callback) {
setSettingParams();
if (null != mActivity && null != mActivity.get() && mActivity.get() instanceof FragmentActivity) {
EasyResult.get((FragmentActivity) mActivity.get()).startEasyPhoto(callback);
return;
}
if (null != mFragmentV && null != mFragmentV.get()) {
EasyResult.get(mFragmentV.get()).startEasyPhoto(callback);
return;
}
throw new RuntimeException("mActivity or mFragmentV maybe null, you can not use this " +
"method... ");
}
/**
* 正式启动
*
* @param requestCode startActivityForResult的请求码
*/
private void launchEasyPhotosActivity(int requestCode) {
if (null != mActivity && null != mActivity.get()) {
EasyPhotosActivity.start(mActivity.get(), requestCode);
return;
}
if (null != mFragment && null != mFragment.get()) {
EasyPhotosActivity.start(mFragment.get(), requestCode);
return;
}
if (null != mFragmentV && null != mFragmentV.get()) {
EasyPhotosActivity.start(mFragmentV.get(), requestCode);
}
}
/**
* 清除所有数据
*/
private static void clear() {
Result.clear();
Setting.clear();
instance = null;
}
//*********************AD************************************
/**
* 设置广告(不设置该选项则表示不使用广告)
*
* @param photosAdView 使用图片列表的广告View
* @param photosAdIsLoaded 图片列表广告是否加载完毕
* @param albumItemsAdView 使用专辑项目列表的广告View
* @param albumItemsAdIsLoaded 专辑项目列表广告是否加载完毕
* @return AlbumBuilder
*/
public AlbumBuilder setAdView(View photosAdView, boolean photosAdIsLoaded,
View albumItemsAdView, boolean albumItemsAdIsLoaded) {
Setting.photosAdView = new WeakReference<View>(photosAdView);
Setting.albumItemsAdView = new WeakReference<View>(albumItemsAdView);
Setting.photoAdIsOk = photosAdIsLoaded;
Setting.albumItemsAdIsOk = albumItemsAdIsLoaded;
return AlbumBuilder.this;
}
/**
* 设置广告监听
* 内部使用,无需关心
*
* @param adListener 广告监听
*/
public static void setAdListener(AdListener adListener) {
if (null == instance) return;
if (instance.startupType == StartupType.CAMERA) return;
instance.adListener = new WeakReference<AdListener>(adListener);
}
/**
* 刷新图片列表广告数据
*/
public static void notifyPhotosAdLoaded() {
if (Setting.photoAdIsOk) {
return;
}
if (null == instance) {
return;
}
if (instance.startupType == StartupType.CAMERA) {
return;
}
if (null == instance.adListener) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (null != instance && null != instance.adListener) {
Setting.photoAdIsOk = true;
instance.adListener.get().onPhotosAdLoaded();
}
}
}).start();
return;
}
Setting.photoAdIsOk = true;
instance.adListener.get().onPhotosAdLoaded();
}
/**
* 刷新专辑项目列表广告
*/
public static void notifyAlbumItemsAdLoaded() {
if (Setting.albumItemsAdIsOk) {
return;
}
if (null == instance) {
return;
}
if (instance.startupType == StartupType.CAMERA) {
return;
}
if (null == instance.adListener) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (null != instance && null != instance.adListener) {
Setting.albumItemsAdIsOk = true;
instance.adListener.get().onAlbumItemsAdLoaded();
}
}
}).start();
return;
}
Setting.albumItemsAdIsOk = true;
instance.adListener.get().onAlbumItemsAdLoaded();
}
}

View File

@@ -0,0 +1,22 @@
package com.nnbc123.library.easyphoto.callback;
import com.nnbc123.library.easyphoto.models.album.entity.Photo;
/**
* PuzzleCallback
*
* @author joker
* @date 2019/4/9.
*/
public abstract class PuzzleCallback {
/**
* 选择结果回调
*
* @param photo 返回对象:如果你需要了解图片的宽、高、大小、用户是否选中原图选项等信息,可以用这个
*/
public abstract void onResult(Photo photo);
/**
* 什么都没选,取消选择回调
*/
public abstract void onCancel();
}

View File

@@ -0,0 +1,26 @@
package com.nnbc123.library.easyphoto.callback;
import com.nnbc123.library.easyphoto.models.album.entity.Photo;
import java.util.ArrayList;
/**
* SelectCallback
*
* @author joker
* @date 2019/4/9.
*/
public abstract class SelectCallback {
/**
* 选择结果回调
*
* @param photos 返回对象集合:如果你需要了解图片的宽、高、大小、用户是否选中原图选项等信息,可以用这个
* @param isOriginal 返回图片地址集合时如果你需要知道用户选择图片时是否选择了原图选项,用如下方法获取
*/
public abstract void onResult(ArrayList<Photo> photos, boolean isOriginal);
/**
* 什么都没选,取消选择回调
*/
public abstract void onCancel();
}

View File

@@ -0,0 +1,21 @@
package com.nnbc123.library.easyphoto.constant;
/**
* Code常量
* Created by huan on 2017/10/19.
*/
public class Code {
//相机请求码
public static final int REQUEST_CAMERA = 11;
//权限请求码
public static final int REQUEST_PERMISSION = 12;
//预览activity请求码
public static final int REQUEST_PREVIEW_ACTIVITY = 13;
//请求应用详情
public static final int REQUEST_SETTING_APP_DETAILS = 14;
//拼图activity请求吗
public static final int REQUEST_PUZZLE = 15;
//拼图选择activity请求吗
public static final int REQUEST_PUZZLE_SELECTOR = 16;
}

View File

@@ -0,0 +1,23 @@
package com.nnbc123.library.easyphoto.constant;
/**
* key的常量
* Created by huan on 2017/10/19.
*/
public class Key {
//预览图片的当前角标
public static final String PREVIEW_PHOTO_INDEX = "keyOfPreviewPhotoIndex";
//当前预览界面的专辑index
public static final String PREVIEW_ALBUM_ITEM_INDEX = "keyOfPreviewAlbumItemIndex";
//预览界面是否点击完成
public static final String PREVIEW_CLICK_DONE = "keyOfPreviewClickDone";
//拼图界面图片类型,true-Photo,false-String
public static final String PUZZLE_FILE_IS_PHOTO = "keyOfPuzzleFilesTypeIsPhoto";
//拼图界面图片结合
public static final String PUZZLE_FILES = "keyOfPuzzleFiles";
//拼图界面图片保存文件夹地址
public static final String PUZZLE_SAVE_DIR = "keyOfPuzzleSaveDir";
//拼图界面图片保存文件名前缀
public static final String PUZZLE_SAVE_NAME_PREFIX = "keyOfPuzzleSaveNamePrefix";
}

View File

@@ -0,0 +1,15 @@
package com.nnbc123.library.easyphoto.constant;
/**
* Created by huan on 2018/1/9.
*/
public class Type {
public static final String GIF = "gif";
public static final String VIDEO = "video";
public static final String JPEG = "jpeg";
public static final String JPG = "jpg";
public static final String PNG = "png";
public static final String BMP = "bmp";
public static final String WEBP = "webp";
}

View File

@@ -0,0 +1,63 @@
package com.nnbc123.library.easyphoto.engine;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import android.widget.ImageView;
import androidx.annotation.NonNull;
/**
* 自定义图片加载方式
* Created by huan on 2018/1/15.
*/
public interface ImageEngine {
/**
* 加载图片到ImageView
*
* @param context 上下文
* @param uri 图片Uri
* @param imageView 加载到的ImageView
*/
//安卓10推荐uri并且path的方式不再可用
void loadPhoto(@NonNull Context context, @NonNull Uri uri,@NonNull ImageView imageView);
/**
* 加载gif动图图片到ImageViewgif动图不动
*
* @param context 上下文
* @param gifUri gif动图路径Uri
* @param imageView 加载到的ImageView
* <p>
* 备注:不支持动图显示的情况下可以不写
*/
//安卓10推荐uri并且path的方式不再可用
void loadGifAsBitmap(@NonNull Context context,@NonNull Uri gifUri,@NonNull ImageView imageView);
/**
* 加载gif动图到ImageViewgif动图动
*
* @param context 上下文
* @param gifUri gif动图路径Uri
* @param imageView 加载动图的ImageView
* <p>
* 备注:不支持动图显示的情况下可以不写
*/
//安卓10推荐uri并且path的方式不再可用
void loadGif(@NonNull Context context,@NonNull Uri gifUri,@NonNull ImageView imageView);
/**
* 获取图片加载框架中的缓存Bitmap不用拼图功能可以直接返回null
*
* @param context 上下文
* @param uri 图片路径
* @param width 图片宽度
* @param height 图片高度
* @return Bitmap
* @throws Exception 异常直接抛出EasyPhotos内部处理
*/
//安卓10推荐uri并且path的方式不再可用
Bitmap getCacheBitmap(@NonNull Context context,@NonNull Uri uri, int width, int height) throws Exception;
}

View File

@@ -0,0 +1,18 @@
package com.nnbc123.library.easyphoto.models.ad;
import android.view.View;
/**
* 广告实体
* Created by huan on 2017/10/24.
*/
public class AdEntity {
public View adView;
public int lineIndex;
public AdEntity(View adView, int lineIndex) {
this.adView = adView;
this.lineIndex = lineIndex;
}
}

View File

@@ -0,0 +1,11 @@
package com.nnbc123.library.easyphoto.models.ad;
/**
* 广告监听
* Created by huan on 2017/10/24.
*/
public interface AdListener {
void onPhotosAdLoaded();
void onAlbumItemsAdLoaded();
}

View File

@@ -0,0 +1,20 @@
package com.nnbc123.library.easyphoto.models.ad;
import androidx.recyclerview.widget.RecyclerView;
import android.view.View;
import android.widget.FrameLayout;
import com.nnbc123.library.R;
/**
* 广告viewolder
* Created by huan on 2017/10/28.
*/
public class AdViewHolder extends RecyclerView.ViewHolder {
public FrameLayout adFrame;
public AdViewHolder(View itemView) {
super(itemView);
adFrame = (FrameLayout) itemView.findViewById(R.id.ad_frame_easy_photos);
}
}

View File

@@ -0,0 +1,361 @@
package com.nnbc123.library.easyphoto.models.album;
import android.Manifest;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.provider.MediaStore;
import android.text.TextUtils;
import androidx.core.content.PermissionChecker;
import com.nnbc123.library.R;
import com.nnbc123.library.easyphoto.constant.Type;
import com.nnbc123.library.easyphoto.models.album.entity.Album;
import com.nnbc123.library.easyphoto.result.Result;
import com.nnbc123.library.easyphoto.setting.Setting;
import com.nnbc123.library.easyphoto.utils.string.StringUtils;
import com.nnbc123.library.easyphoto.models.album.entity.AlbumItem;
import com.nnbc123.library.easyphoto.models.album.entity.Photo;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
* 专辑模型
* Created by huan on 2017/10/20.
* <p>
* Modified by Eagle on 2018/08/31.
* 修改内容将AlbumModel的实例化与数据查询分开
*/
public class AlbumModel {
public static AlbumModel instance;
public Album album;
private String[] projections;
private AlbumModel() {
album = new Album();
}
public static AlbumModel getInstance() {
if (null == instance) {
synchronized (AlbumModel.class) {
if (null == instance) {
instance = new AlbumModel();
}
}
}
return instance;
}
/**
* 专辑查询
*/
public volatile boolean canRun = true;
public void query(Context context, final CallBack callBack) {
final Context appCxt = context.getApplicationContext();
if (PermissionChecker.checkSelfPermission(context,
Manifest.permission.READ_EXTERNAL_STORAGE) != PermissionChecker.PERMISSION_GRANTED) {
if (null != callBack) callBack.onAlbumWorkedCallBack();
return;
}
canRun = true;
new Thread(new Runnable() {
@Override
public void run() {
try {
initAlbum(appCxt);
if (null != callBack) callBack.onAlbumWorkedCallBack();
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
public void stopQuery() {
canRun = false;
}
private synchronized void initAlbum(Context context) {
album.clear();
if (Setting.selectedPhotos.size() > Setting.count) {
throw new RuntimeException("AlbumBuilder: 默认勾选的图片张数不能大于设置的选择数!" + "|默认勾选图片张数:" + Setting.selectedPhotos.size() + "|设置的选择数:" + Setting.count);
}
final String sortOrder = MediaStore.MediaColumns.DATE_MODIFIED + " DESC";
Uri contentUri;
String selection = null;
String[] selectionAllArgs = null;
if (Setting.isOnlyVideo()) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if (!Setting.showVideo) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else {
contentUri = MediaStore.Files.getContentUri("external");
selection = "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + " OR " + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?)";
selectionAllArgs = new String[]{String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE),
String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO)};
}
ContentResolver contentResolver = context.getContentResolver();
List<String> projectionList = new ArrayList<>();
projectionList.add(MediaStore.MediaColumns._ID);
projectionList.add(MediaStore.MediaColumns.DATA);
projectionList.add(MediaStore.MediaColumns.DISPLAY_NAME);
projectionList.add(MediaStore.MediaColumns.DATE_MODIFIED);
projectionList.add(MediaStore.MediaColumns.MIME_TYPE);
projectionList.add(MediaStore.MediaColumns.SIZE);
projectionList.add(MediaStore.MediaColumns.BUCKET_DISPLAY_NAME);
if (!Setting.useWidth) {
if (Setting.minWidth != 1 && Setting.minHeight != 1)
Setting.useWidth = true;
}
if (Setting.useWidth) {
projectionList.add(MediaStore.MediaColumns.WIDTH);
projectionList.add(MediaStore.MediaColumns.HEIGHT);
if (!Setting.isOnlyVideo())
projectionList.add(MediaStore.MediaColumns.ORIENTATION);
}
if (Setting.showVideo) {
projectionList.add(MediaStore.MediaColumns.DURATION);
}
projections = projectionList.toArray(new String[0]);
Cursor cursor = contentResolver.query(contentUri, projections, selection, selectionAllArgs, sortOrder);
if (cursor != null && !cursor.isClosed() && cursor.moveToFirst()) {
String albumItem_all_name = getAllAlbumName(context);
String albumItem_video_name = context.getString(R.string.selector_folder_video_easy_photos);
int albumNameCol = cursor.getColumnIndex(MediaStore.MediaColumns.BUCKET_DISPLAY_NAME);
int durationCol = cursor.getColumnIndex(MediaStore.MediaColumns.DURATION);
int WidthCol = 0;
int HeightCol = 0;
int orientationCol = -1;
if (Setting.useWidth) {
WidthCol = cursor.getColumnIndex(MediaStore.MediaColumns.WIDTH);
HeightCol = cursor.getColumnIndex(MediaStore.MediaColumns.HEIGHT);
orientationCol = cursor.getColumnIndex(MediaStore.MediaColumns.ORIENTATION);
}
boolean hasTime = durationCol > 0;
do {
long id = cursor.getLong(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
String path = cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DATA));
String name = cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME));
long dateTime = cursor.getLong(cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED));
String type = cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.MIME_TYPE));
long size = cursor.getLong(cursor.getColumnIndex(MediaStore.MediaColumns.SIZE));
long duration = 0;
if (TextUtils.isEmpty(path) || TextUtils.isEmpty(type)) {
continue;
}
if (size < Setting.minSize) {
continue;
}
boolean isVideo = type.contains(Type.VIDEO);// 是否是视频
int width = 0;
int height = 0;
int orientation = 0;
if (isVideo) {
if (hasTime)
duration = cursor.getLong(durationCol);
if (duration <= Setting.videoMinSecond || duration >= Setting.videoMaxSecond) {
continue;
}
} else {
if (orientationCol != -1) {
orientation = cursor.getInt(orientationCol);
}
boolean showGif = path.endsWith(Type.GIF) || type.endsWith(Type.GIF);
if (!Setting.showGif) {
if (showGif) {
continue;
}
}
if (!Setting.filterTypes.isEmpty()) {
List<String> list = Setting.filterTypes;
boolean hasFilter = false;
for (String filterType : list) {
if (path.endsWith(filterType) || type.endsWith(filterType)) {
hasFilter = true;
break;
}
}
if (Setting.showGif) {
if (showGif) {
hasFilter = true;
}
}
if (!hasFilter) {
continue;
}
}
if (Setting.useWidth) {
width = cursor.getInt(WidthCol);
height = cursor.getInt(HeightCol);
if (width == 0 || height == 0) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, options);
width = options.outWidth;
height = options.outHeight;
}
if (orientation == 90 || orientation == 270) {
int temp = width;
width = height;
height = temp;
}
if (width < Setting.minWidth || height < Setting.minHeight) {
continue;
}
}
}
Uri uri = ContentUris.withAppendedId(isVideo ?
MediaStore.Video.Media.EXTERNAL_CONTENT_URI :
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
//某些机型,特定情况下三方应用或用户操作删除媒体文件时,没有通知媒体库,
// 导致媒体库表中还有其数据,但真实文件已经不存在
File file = new File(path);
if (!file.isFile()) {
continue;
}
Photo imageItem = new Photo(name, uri, path, dateTime, width, height, orientation
, size, duration, type);
if (!Setting.selectedPhotos.isEmpty()) {
int selectSize = Setting.selectedPhotos.size();
for (int i = 0; i < selectSize; i++) {
Photo selectedPhoto = Setting.selectedPhotos.get(i);
if (path.equals(selectedPhoto.path)) {
imageItem.selectedOriginal = Setting.selectedOriginal;
Result.addPhoto(imageItem);
}
}
}
// 初始化“全部”专辑
if (album.isEmpty()) {
// 用第一个图片作为专辑的封面
album.addAlbumItem(albumItem_all_name, "", path, uri);
}
// 把图片全部放进“全部”专辑
if (album.getAlbumItem(albumItem_all_name) != null) {
album.getAlbumItem(albumItem_all_name).addImageItem(imageItem);
}
if (Setting.showVideo && isVideo && !albumItem_video_name.equals(albumItem_all_name)) {
album.addAlbumItem(albumItem_video_name, "", path, uri);
album.getAlbumItem(albumItem_video_name).addImageItem(imageItem);
}
// 添加当前图片的专辑到专辑模型实体中
String albumName;
String folderPath;
if (albumNameCol > 0) {
albumName = cursor.getString(albumNameCol);
folderPath = albumName;
} else {
File parentFile = new File(path).getParentFile();
if (null == parentFile) {
continue;
}
folderPath = parentFile.getAbsolutePath();
albumName = StringUtils.getLastPathSegment(folderPath);
}
album.addAlbumItem(albumName, folderPath, path, uri);
album.getAlbumItem(albumName).addImageItem(imageItem);
} while (!cursor.isClosed() && cursor.moveToNext() && canRun);
cursor.close();
}
}
/**
* 获取全部专辑名
*
* @return 专辑名
*/
public String getAllAlbumName(Context context) {
String albumItem_all_name = context.getString(R.string.selector_folder_all_video_photo_easy_photos);
if (Setting.isOnlyVideo()) {
albumItem_all_name = context.getString(R.string.selector_folder_video_easy_photos);
} else if (!Setting.showVideo) {
//不显示视频
albumItem_all_name = context.getString(R.string.selector_folder_all_easy_photos);
}
return albumItem_all_name;
}
/**
* 获取当前专辑项目的图片集
*
* @return 当前专辑项目的图片集
*/
public ArrayList<Photo> getCurrAlbumItemPhotos(int currAlbumItemIndex) {
return album.getAlbumItem(currAlbumItemIndex).photos;
}
/**
* 获取专辑项目集
*
* @return 专辑项目集
*/
public ArrayList<AlbumItem> getAlbumItems() {
return album.albumItems;
}
public interface CallBack {
void onAlbumWorkedCallBack();
}
/**
* 获取projections
*/
public String[] getProjections() {
if (null == projections || projections.length == 0) {
if (Setting.useWidth) {
projections = new String[]{MediaStore.MediaColumns._ID,
MediaStore.MediaColumns.DATA,
MediaStore.MediaColumns.DISPLAY_NAME,
MediaStore.MediaColumns.DATE_MODIFIED,
MediaStore.MediaColumns.MIME_TYPE,
MediaStore.MediaColumns.SIZE,
MediaStore.MediaColumns.BUCKET_DISPLAY_NAME,
MediaStore.MediaColumns.WIDTH,
MediaStore.MediaColumns.HEIGHT,
MediaStore.MediaColumns.ORIENTATION};
} else {
projections = new String[]{MediaStore.MediaColumns._ID,
MediaStore.MediaColumns.DATA,
MediaStore.MediaColumns.DISPLAY_NAME,
MediaStore.MediaColumns.DATE_MODIFIED,
MediaStore.MediaColumns.MIME_TYPE,
MediaStore.MediaColumns.SIZE,
MediaStore.MediaColumns.BUCKET_DISPLAY_NAME};
}
}
return projections;
}
}

View File

@@ -0,0 +1,49 @@
package com.nnbc123.library.easyphoto.models.album.entity;
import android.net.Uri;
import java.util.ArrayList;
import java.util.LinkedHashMap;
/**
* 专辑模型实体类
* Created by huan on 2017/10/20.
*/
public class Album {
public ArrayList<AlbumItem> albumItems;
private LinkedHashMap<String, AlbumItem> hasAlbumItems;//用于记录专辑项目
public Album() {
albumItems = new ArrayList<>();
hasAlbumItems = new LinkedHashMap<>();
}
private void addAlbumItem(AlbumItem albumItem) {
this.hasAlbumItems.put(albumItem.name, albumItem);
this.albumItems.add(albumItem);
}
public void addAlbumItem(String name, String folderPath, String coverImagePath, Uri coverImageUri) {
if (null == hasAlbumItems.get(name)) {
addAlbumItem(new AlbumItem(name, folderPath, coverImagePath,coverImageUri));
}
}
public AlbumItem getAlbumItem(String name) {
return hasAlbumItems.get(name);
}
public AlbumItem getAlbumItem(int currIndex) {
return albumItems.get(currIndex);
}
public boolean isEmpty() {
return albumItems.isEmpty();
}
public void clear() {
albumItems.clear();
hasAlbumItems.clear();
}
}

View File

@@ -0,0 +1,34 @@
package com.nnbc123.library.easyphoto.models.album.entity;
import android.net.Uri;
import java.util.ArrayList;
/**
* 专辑项目实体类
* Created by huan on 2017/10/20.
*/
public class AlbumItem {
public String name;
public String folderPath;
public String coverImagePath;
public Uri coverImageUri;
public ArrayList<Photo> photos;
AlbumItem(String name, String folderPath, String coverImagePath, Uri coverImageUri) {
this.name = name;
this.folderPath = folderPath;
this.coverImagePath = coverImagePath;
this.coverImageUri = coverImageUri;
this.photos = new ArrayList<>();
}
public void addImageItem(Photo imageItem) {
this.photos.add(imageItem);
}
public void addImageItem(int index, Photo imageItem) {
this.photos.add(index, imageItem);
}
}

View File

@@ -0,0 +1,114 @@
package com.nnbc123.library.easyphoto.models.album.entity;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
/**
* 图片item实体类
* Created by huan on 2017/10/20.
*/
public class Photo implements Parcelable {
private static final String TAG = "Photo";
public Uri uri;//图片Uri
public String name;//图片名称
public String path;//图片全路径
public String type;//图片类型
public int width;//图片宽度
public int height;//图片高度
public int orientation;//图片旋转角度
public long size;//图片文件大小单位Bytes
public long duration;//视频时长,单位:毫秒
public long time;//图片拍摄的时间戳,单位:毫秒
public boolean selected;//是否被选中,内部使用,无需关心
public boolean selectedOriginal;//用户选择时是否选择了原图选项
public Photo(String name, Uri uri, String path, long time, int width, int height, int orientation, long size, long duration, String type) {
this.name = name;
this.uri = uri;
this.path = path;
this.time = time;
this.width = width;
this.height = height;
this.orientation = orientation;
this.type = type;
this.size = size;
this.duration = duration;
this.selected = false;
this.selectedOriginal = false;
}
@Override
public boolean equals(Object o) {
try {
Photo other = (Photo) o;
return this.path.equalsIgnoreCase(other.path);
} catch (ClassCastException e) {
Log.e(TAG, "equals: " + Log.getStackTraceString(e));
}
return super.equals(o);
}
@Override
public String toString() {
return "Photo{" +
"name='" + name + '\'' +
", uri='" + uri.toString() + '\'' +
", path='" + path + '\'' +
", time=" + time + '\'' +
", minWidth=" + width + '\'' +
", minHeight=" + height +
", orientation=" + orientation +
'}';
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeParcelable(this.uri, flags);
dest.writeString(this.name);
dest.writeString(this.path);
dest.writeString(this.type);
dest.writeInt(this.width);
dest.writeInt(this.height);
dest.writeInt(this.orientation);
dest.writeLong(this.size);
dest.writeLong(this.duration);
dest.writeLong(this.time);
dest.writeByte(this.selected ? (byte) 1 : (byte) 0);
dest.writeByte(this.selectedOriginal ? (byte) 1 : (byte) 0);
}
protected Photo(Parcel in) {
this.uri = in.readParcelable(Uri.class.getClassLoader());
this.name = in.readString();
this.path = in.readString();
this.type = in.readString();
this.width = in.readInt();
this.height = in.readInt();
this.orientation = in.readInt();
this.size = in.readLong();
this.duration = in.readLong();
this.time = in.readLong();
this.selected = in.readByte() != 0;
this.selectedOriginal = in.readByte() != 0;
}
public static final Creator<Photo> CREATOR = new Creator<Photo>() {
@Override
public Photo createFromParcel(Parcel source) {
return new Photo(source);
}
@Override
public Photo[] newArray(int size) {
return new Photo[size];
}
};
}

View File

@@ -0,0 +1,65 @@
package com.nnbc123.library.easyphoto.models.puzzle;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.RectF;
import java.util.List;
/**
* @author wupanjie
*/
public interface Area {
float left();
float top();
float right();
float bottom();
float centerX();
float centerY();
float width();
float height();
PointF getCenterPoint();
boolean contains(PointF point);
boolean contains(float x, float y);
boolean contains(Line line);
Path getAreaPath();
RectF getAreaRect();
List<Line> getLines();
PointF[] getHandleBarPoints(Line line);
float radian();
void setRadian(float radian);
float getPaddingLeft();
float getPaddingTop();
float getPaddingRight();
float getPaddingBottom();
void setPadding(float padding);
void setPadding(float paddingLeft, float paddingTop, float paddingRight, float paddingBottom);
}

View File

@@ -0,0 +1,354 @@
package com.nnbc123.library.easyphoto.models.puzzle;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import androidx.core.content.ContextCompat;
import com.nnbc123.library.R;
/**
* @author wupanjie
*/
public class DegreeSeekBar extends View {
private static final String TAG = "DegreeSeekBar";
private Paint mTextPaint;
private Paint mCirclePaint;
private Paint.FontMetricsInt mFontMetrics;
private int mBaseLine;
private float[] mTextWidths;
private final Rect mCanvasClipBounds = new Rect();
private ScrollingListener mScrollingListener;
private float mLastTouchedPosition;
private Paint mPointPaint;
private float mPointMargin;
private boolean mScrollStarted;
private int mTotalScrollDistance;
private Path mIndicatorPath = new Path();
private int mCurrentDegrees = 0;
private int mPointCount = 51;
private int mPointColor;
private int mTextColor;
private int mCenterTextColor;
//阻尼系数的倒数
private float mDragFactor = 2.1f;
private int mMinReachableDegrees = -45;
private int mMaxReachableDegrees = 45;
private String suffix = "";
public DegreeSeekBar(Context context) {
this(context, null);
}
public DegreeSeekBar(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DegreeSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public DegreeSeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private void init() {
mPointColor = ContextCompat.getColor(getContext(), R.color.easy_photos_fg_primary);
mTextColor = ContextCompat.getColor(getContext(), R.color.easy_photos_fg_primary);
mCenterTextColor = ContextCompat.getColor(getContext(), R.color.easy_photos_fg_accent);
mPointPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPointPaint.setStyle(Paint.Style.STROKE);
mPointPaint.setColor(mPointColor);
mPointPaint.setStrokeWidth(2);
mTextPaint = new Paint();
mTextPaint.setColor(mTextColor);
mTextPaint.setStyle(Paint.Style.STROKE);
mTextPaint.setAntiAlias(true);
mTextPaint.setTextSize(24f);
mTextPaint.setTextAlign(Paint.Align.LEFT);
mTextPaint.setAlpha(100);
mFontMetrics = mTextPaint.getFontMetricsInt();
mTextWidths = new float[1];
mTextPaint.getTextWidths("0", mTextWidths);
mCirclePaint = new Paint();
mCirclePaint.setStyle(Paint.Style.FILL);
mCirclePaint.setAlpha(255);
mCirclePaint.setAntiAlias(true);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mPointMargin = (float) w / mPointCount;
mBaseLine = (h - mFontMetrics.bottom + mFontMetrics.top) / 2 - mFontMetrics.top;
mIndicatorPath.moveTo(w / 2, h / 2 + mFontMetrics.top / 2 - 18);
mIndicatorPath.rLineTo(-8, -8);
mIndicatorPath.rLineTo(16, 0);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastTouchedPosition = event.getX();
if (!mScrollStarted) {
mScrollStarted = true;
if (mScrollingListener != null) {
mScrollingListener.onScrollStart();
}
}
break;
case MotionEvent.ACTION_UP:
mScrollStarted = false;
if (mScrollingListener != null) {
mScrollingListener.onScrollEnd();
}
invalidate();
break;
case MotionEvent.ACTION_MOVE:
float distance = event.getX() - mLastTouchedPosition;
if (mCurrentDegrees >= mMaxReachableDegrees && distance < 0) {
mCurrentDegrees = mMaxReachableDegrees;
invalidate();
break;
}
if (mCurrentDegrees <= mMinReachableDegrees && distance > 0) {
mCurrentDegrees = mMinReachableDegrees;
invalidate();
break;
}
if (distance != 0) {
onScrollEvent(event, distance);
}
break;
}
return true;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.getClipBounds(mCanvasClipBounds);
int zeroIndex = mPointCount / 2 + (0 - mCurrentDegrees) / 2;
mPointPaint.setColor(mPointColor);
for (int i = 0; i < mPointCount; i++) {
if (i > zeroIndex - Math.abs(mMinReachableDegrees) / 2
&& i < zeroIndex + Math.abs(mMaxReachableDegrees) / 2
&& mScrollStarted) {
mPointPaint.setAlpha(255);
} else {
mPointPaint.setAlpha(100);
}
if (i > mPointCount / 2 - 8
&& i < mPointCount / 2 + 8
&& i > zeroIndex - Math.abs(mMinReachableDegrees) / 2
&& i < zeroIndex + Math.abs(mMaxReachableDegrees) / 2) {
if (mScrollStarted) {
mPointPaint.setAlpha(Math.abs(mPointCount / 2 - i) * 255 / 8);
} else {
mPointPaint.setAlpha(Math.abs(mPointCount / 2 - i) * 100 / 8);
}
}
canvas.drawPoint(mCanvasClipBounds.centerX() + (i - mPointCount / 2) * mPointMargin,
mCanvasClipBounds.centerY(), mPointPaint);
if (mCurrentDegrees != 0 && i == zeroIndex) {
if (mScrollStarted) {
mTextPaint.setAlpha(255);
} else {
mTextPaint.setAlpha(192);
}
mPointPaint.setStrokeWidth(4);
canvas.drawPoint((mCanvasClipBounds.centerX() + (i - mPointCount / 2) * mPointMargin),
mCanvasClipBounds.centerY(), mPointPaint);
mPointPaint.setStrokeWidth(2);
mTextPaint.setAlpha(100);
}
}
for (int i = -180; i <= 180; i += 15) {
if (i >= mMinReachableDegrees && i <= mMaxReachableDegrees) {
drawDegreeText(i, canvas, true);
} else {
drawDegreeText(i, canvas, false);
}
}
mTextPaint.setTextSize(28f);
mTextPaint.setAlpha(255);
mTextPaint.setColor(mCenterTextColor);
if (mCurrentDegrees >= 10) {
canvas.drawText(mCurrentDegrees + suffix, getWidth() / 2 - mTextWidths[0], mBaseLine,
mTextPaint);
} else if (mCurrentDegrees <= -10) {
canvas.drawText(mCurrentDegrees + suffix, getWidth() / 2 - mTextWidths[0] / 2 * 3, mBaseLine,
mTextPaint);
} else if (mCurrentDegrees < 0) {
canvas.drawText(mCurrentDegrees + suffix, getWidth() / 2 - mTextWidths[0], mBaseLine,
mTextPaint);
} else {
canvas.drawText(mCurrentDegrees + suffix, getWidth() / 2 - mTextWidths[0] / 2, mBaseLine,
mTextPaint);
}
mTextPaint.setAlpha(100);
mTextPaint.setTextSize(24f);
mTextPaint.setColor(mTextColor);
//画中心三角
mCirclePaint.setColor(mCenterTextColor);
canvas.drawPath(mIndicatorPath, mCirclePaint);
mCirclePaint.setColor(mCenterTextColor);
}
private void drawDegreeText(int degrees, Canvas canvas, boolean canReach) {
if (canReach) {
if (mScrollStarted) {
mTextPaint.setAlpha(Math.min(255, Math.abs(degrees - mCurrentDegrees) * 255 / 15));
if (Math.abs(degrees - mCurrentDegrees) <= 7) {
mTextPaint.setAlpha(0);
}
} else {
mTextPaint.setAlpha(100);
if (Math.abs(degrees - mCurrentDegrees) <= 7) {
mTextPaint.setAlpha(0);
}
}
} else {
mTextPaint.setAlpha(100);
}
if (degrees == 0) {
if (Math.abs(mCurrentDegrees) >= 15 && !mScrollStarted) {
mTextPaint.setAlpha(180);
}
canvas.drawText("",
getWidth() / 2 - mTextWidths[0] / 2 - mCurrentDegrees / 2 * mPointMargin,
getHeight() / 2 - 10, mTextPaint);
} else {
canvas.drawText(degrees + suffix, getWidth() / 2 + mPointMargin * degrees / 2
- mTextWidths[0] / 2 * 3
- mCurrentDegrees / 2 * mPointMargin, getHeight() / 2 - 10, mTextPaint);
}
}
private void onScrollEvent(MotionEvent event, float distance) {
mTotalScrollDistance -= distance;
postInvalidate();
mLastTouchedPosition = event.getX();
mCurrentDegrees = (int) ((mTotalScrollDistance * mDragFactor) / mPointMargin);
if (mScrollingListener != null) {
mScrollingListener.onScroll(mCurrentDegrees);
}
}
public void setDegreeRange(int min, int max) {
if (min > max) {
Log.e(TAG, "setDegreeRange: error, max must greater than min");
} else {
mMinReachableDegrees = min;
mMaxReachableDegrees = max;
if (mCurrentDegrees > mMaxReachableDegrees || mCurrentDegrees < mMinReachableDegrees) {
mCurrentDegrees = (mMinReachableDegrees + mMaxReachableDegrees) / 2;
}
mTotalScrollDistance = (int) (mCurrentDegrees * mPointMargin / mDragFactor);
invalidate();
}
}
public void setCurrentDegrees(int degrees) {
if (degrees <= mMaxReachableDegrees && degrees >= mMinReachableDegrees) {
mCurrentDegrees = degrees;
mTotalScrollDistance = (int) (degrees * mPointMargin / mDragFactor);
invalidate();
}
}
public void setScrollingListener(ScrollingListener scrollingListener) {
mScrollingListener = scrollingListener;
}
public int getPointColor() {
return mPointColor;
}
public void setPointColor(int pointColor) {
mPointColor = pointColor;
mPointPaint.setColor(mPointColor);
postInvalidate();
}
public int getTextColor() {
return mTextColor;
}
public void setTextColor(int textColor) {
mTextColor = textColor;
mTextPaint.setColor(textColor);
postInvalidate();
}
public int getCenterTextColor() {
return mCenterTextColor;
}
public void setCenterTextColor(int centerTextColor) {
mCenterTextColor = centerTextColor;
postInvalidate();
}
public float getDragFactor() {
return mDragFactor;
}
public void setDragFactor(float dragFactor) {
mDragFactor = dragFactor;
}
public void setSuffix(String suffix) {
this.suffix = suffix;
}
public interface ScrollingListener {
void onScrollStart();
void onScroll(int currentDegrees);
void onScrollEnd();
}
}

View File

@@ -0,0 +1,52 @@
package com.nnbc123.library.easyphoto.models.puzzle;
import android.graphics.PointF;
/**
* @author wupanjie
*/
public interface Line {
enum Direction {
HORIZONTAL, VERTICAL
}
float length();
PointF startPoint();
PointF endPoint();
Line lowerLine();
Line upperLine();
Line attachStartLine();
Line attachEndLine();
void setLowerLine(Line lowerLine);
void setUpperLine(Line upperLine);
Direction direction();
float slope();
boolean contains(float x, float y, float extra);
void prepareMove();
boolean move(float offset, float extra);
void update(float layoutWidth, float layoutHeight);
float minX();
float maxX();
float minY();
float maxY();
void offset(float x, float y);
}

View File

@@ -0,0 +1,167 @@
package com.nnbc123.library.easyphoto.models.puzzle;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import java.util.Arrays;
import static java.lang.Math.round;
/**
* some useful matrix operation methods
*
* @author wupanjie
*/
public class MatrixUtils {
private MatrixUtils() {
//no instance
}
private static final float[] sMatrixValues = new float[9];
private static final Matrix sTempMatrix = new Matrix();
/**
* This method calculates scale value for given Matrix object.
*/
public static float getMatrixScale(Matrix matrix) {
return (float) Math.sqrt(Math.pow(getMatrixValue(matrix, Matrix.MSCALE_X), 2) + Math.pow(
getMatrixValue(matrix, Matrix.MSKEW_Y), 2));
}
/**
* This method calculates rotation angle for given Matrix object.
*/
public static float getMatrixAngle(Matrix matrix) {
return (float) -(Math.atan2(getMatrixValue(matrix, Matrix.MSKEW_X),
getMatrixValue(matrix, Matrix.MSCALE_X)) * (180 / Math.PI));
}
public static float getMatrixValue(Matrix matrix, int valueIndex) {
matrix.getValues(sMatrixValues);
return sMatrixValues[valueIndex];
}
public static float getMinMatrixScale(PuzzlePiece piece) {
if (piece != null) {
sTempMatrix.reset();
sTempMatrix.setRotate(-piece.getMatrixAngle());
float[] unrotatedCropBoundsCorners = getCornersFromRect(piece.getArea().getAreaRect());
sTempMatrix.mapPoints(unrotatedCropBoundsCorners);
RectF unrotatedCropRect = trapToRect(unrotatedCropBoundsCorners);
return Math.max(unrotatedCropRect.width() / piece.getWidth(),
unrotatedCropRect.height() / piece.getHeight());
}
return 1f;
}
//判断剪裁框是否在图片内
static boolean judgeIsImageContainsBorder(PuzzlePiece piece, float rotateDegrees) {
sTempMatrix.reset();
sTempMatrix.setRotate(-rotateDegrees);
float[] unrotatedWrapperCorner = new float[8];
float[] unrotateBorderCorner = new float[8];
sTempMatrix.mapPoints(unrotatedWrapperCorner, piece.getCurrentDrawablePoints());
sTempMatrix.mapPoints(unrotateBorderCorner, getCornersFromRect(piece.getArea().getAreaRect()));
return trapToRect(unrotatedWrapperCorner).contains(trapToRect(unrotateBorderCorner));
}
static float[] calculateImageIndents(PuzzlePiece piece) {
if (piece == null) return new float[]{0, 0, 0, 0, 0, 0, 0, 0};
sTempMatrix.reset();
sTempMatrix.setRotate(-piece.getMatrixAngle());
final float[] currentImageCorners = piece.getCurrentDrawablePoints();
final float[] unrotatedImageCorners =
Arrays.copyOf(currentImageCorners, currentImageCorners.length);
final float[] unrotatedCropBoundsCorners = getCornersFromRect(piece.getArea().getAreaRect());
sTempMatrix.mapPoints(unrotatedImageCorners);
sTempMatrix.mapPoints(unrotatedCropBoundsCorners);
RectF unrotatedImageRect = trapToRect(unrotatedImageCorners);
RectF unrotatedCropRect = trapToRect(unrotatedCropBoundsCorners);
float deltaLeft = unrotatedImageRect.left - unrotatedCropRect.left;
float deltaTop = unrotatedImageRect.top - unrotatedCropRect.top;
float deltaRight = unrotatedImageRect.right - unrotatedCropRect.right;
float deltaBottom = unrotatedImageRect.bottom - unrotatedCropRect.bottom;
float indents[] = new float[4];
indents[0] = (deltaLeft > 0) ? deltaLeft : 0;
indents[1] = (deltaTop > 0) ? deltaTop : 0;
indents[2] = (deltaRight < 0) ? deltaRight : 0;
indents[3] = (deltaBottom < 0) ? deltaBottom : 0;
sTempMatrix.reset();
sTempMatrix.setRotate(piece.getMatrixAngle());
sTempMatrix.mapPoints(indents);
return indents;
}
//计算包含给出点的最小矩形
public static RectF trapToRect(float[] array) {
RectF r = new RectF(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, Float.NEGATIVE_INFINITY,
Float.NEGATIVE_INFINITY);
int length = array.length;
for (int i = 1; i < length; i += 2) {
float x = round(array[i - 1] * 10) / 10.f;
float y = round(array[i] * 10) / 10.f;
r.left = (x < r.left) ? x : r.left;
r.top = (y < r.top) ? y : r.top;
r.right = (x > r.right) ? x : r.right;
r.bottom = (y > r.bottom) ? y : r.bottom;
}
r.sort();
return r;
}
public static float[] getCornersFromRect(RectF r) {
return new float[]{
r.left, r.top, r.right, r.top, r.right, r.bottom, r.left, r.bottom
};
}
public static Matrix generateMatrix(PuzzlePiece piece, float extra) {
return generateMatrix(piece.getArea(), piece.getDrawable(), extra);
}
public static Matrix generateMatrix(Area area, Drawable drawable, float extraSize) {
return generateCenterCropMatrix(area, drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight(), extraSize);
}
private static Matrix generateCenterCropMatrix(Area area, int width, int height,
float extraSize) {
final RectF rectF = area.getAreaRect();
Matrix matrix = new Matrix();
float offsetX = rectF.centerX() - width / 2;
float offsetY = rectF.centerY() - height / 2;
matrix.postTranslate(offsetX, offsetY);
float scale;
if (width * rectF.height() > rectF.width() * height) {
scale = (rectF.height() + extraSize) / height;
} else {
scale = (rectF.width() + extraSize) / width;
}
matrix.postScale(scale, scale, rectF.centerX(), rectF.centerY());
return matrix;
}
}

View File

@@ -0,0 +1,88 @@
package com.nnbc123.library.easyphoto.models.puzzle;
import android.graphics.RectF;
import java.util.ArrayList;
import java.util.List;
/**
* @author wupanjie
*/
public interface PuzzleLayout {
void setOuterBounds(RectF bounds);
void layout();
int getAreaCount();
List<Line> getOuterLines();
List<Line> getLines();
Area getOuterArea();
void update();
void reset();
Area getArea(int position);
float width();
float height();
void setPadding(float padding);
float getPadding();
float getRadian();
void setRadian(float radian);
Info generateInfo();
void setColor(int color);
int getColor();
class Info {
public static final int TYPE_STRAIGHT = 0;
public static final int TYPE_SLANT = 1;
public int type;
public ArrayList<Step> steps;
public ArrayList<LineInfo> lineInfos;
public float padding;
public float radian;
public int color;
}
class Step {
public static final int ADD_LINE = 0;
public static final int ADD_CROSS = 1;
public static final int CUT_EQUAL_PART_ONE = 2;
public static final int CUT_EQUAL_PART_TWO = 3;
public static final int CUT_SPIRAL = 4;
public int type;
public int direction;
public int position;
public int part;
public int hSize;
public int vSize;
}
class LineInfo {
public float startX;
public float startY;
public float endX;
public float endY;
public LineInfo(Line line) {
startX = line.startPoint().x;
startY = line.startPoint().y;
endX = line.endPoint().x;
endY = line.endPoint().y;
}
}
}

View File

@@ -0,0 +1,433 @@
package com.nnbc123.library.easyphoto.models.puzzle;
import android.animation.ValueAnimator;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Xfermode;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.DecelerateInterpolator;
import static com.nnbc123.library.easyphoto.models.puzzle.MatrixUtils.calculateImageIndents;
import static com.nnbc123.library.easyphoto.models.puzzle.MatrixUtils.getMinMatrixScale;
import static com.nnbc123.library.easyphoto.models.puzzle.MatrixUtils.judgeIsImageContainsBorder;
/**
* @author wupanjie
*/
@SuppressWarnings("WeakerAccess")
public class PuzzlePiece {
private static Xfermode SRC_IN = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
private Drawable drawable;
private Matrix matrix;
private Matrix previousMatrix;
private Area area;
private Rect drawableBounds;
private float[] drawablePoints;
private float[] mappedDrawablePoints;
private float previousMoveX;
private float previousMoveY;
private final RectF mappedBounds;
private final PointF centerPoint;
private final PointF mappedCenterPoint;
private ValueAnimator animator;
private int duration = 300;
private Matrix tempMatrix;
PuzzlePiece(Drawable drawable, Area area, Matrix matrix) {
this.drawable = drawable;
this.area = area;
this.matrix = matrix;
this.previousMatrix = new Matrix();
this.drawableBounds = new Rect(0, 0, getWidth(), getHeight());
this.drawablePoints = new float[]{
0f, 0f, getWidth(), 0f, getWidth(), getHeight(), 0f, getHeight()
};
this.mappedDrawablePoints = new float[8];
this.mappedBounds = new RectF();
this.centerPoint = new PointF(area.centerX(), area.centerY());
this.mappedCenterPoint = new PointF();
this.animator = ValueAnimator.ofFloat(0f, 1f);
this.animator.setInterpolator(new DecelerateInterpolator());
this.tempMatrix = new Matrix();
}
void draw(Canvas canvas) {
draw(canvas, 255, true);
}
void draw(Canvas canvas, int alpha) {
draw(canvas, alpha, false);
}
private void draw(Canvas canvas, int alpha, boolean needClip) {
if (drawable instanceof BitmapDrawable) {
int saved = canvas.saveLayer(null, null, Canvas.ALL_SAVE_FLAG);
Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
Paint paint = ((BitmapDrawable) drawable).getPaint();
paint.setColor(Color.WHITE);
paint.setAlpha(alpha);
if (needClip) {
canvas.drawPath(area.getAreaPath(), paint);
paint.setXfermode(SRC_IN);
}
canvas.drawBitmap(bitmap, matrix, paint);
paint.setXfermode(null);
canvas.restoreToCount(saved);
} else {
canvas.save();
if (needClip) {
canvas.clipPath(area.getAreaPath());
}
canvas.concat(matrix);
drawable.setBounds(drawableBounds);
drawable.setAlpha(alpha);
drawable.draw(canvas);
canvas.restore();
}
}
public Area getArea() {
return area;
}
public void setDrawable(Drawable drawable) {
this.drawable = drawable;
this.drawableBounds = new Rect(0, 0, getWidth(), getHeight());
this.drawablePoints = new float[]{
0f, 0f, getWidth(), 0f, getWidth(), getHeight(), 0f, getHeight()
};
}
public Drawable getDrawable() {
return drawable;
}
public int getWidth() {
return drawable.getIntrinsicWidth();
}
public int getHeight() {
return drawable.getIntrinsicHeight();
}
public boolean contains(float x, float y) {
return area.contains(x, y);
}
public boolean contains(Line line) {
return area.contains(line);
}
public Rect getDrawableBounds() {
return drawableBounds;
}
void setPreviousMoveX(float previousMoveX) {
this.previousMoveX = previousMoveX;
}
void setPreviousMoveY(float previousMoveY) {
this.previousMoveY = previousMoveY;
}
private RectF getCurrentDrawableBounds() {
matrix.mapRect(mappedBounds, new RectF(drawableBounds));
return mappedBounds;
}
private PointF getCurrentDrawableCenterPoint() {
getCurrentDrawableBounds();
mappedCenterPoint.x = mappedBounds.centerX();
mappedCenterPoint.y = mappedBounds.centerY();
return mappedCenterPoint;
}
public PointF getAreaCenterPoint() {
centerPoint.x = area.centerX();
centerPoint.y = area.centerY();
return centerPoint;
}
private float getMatrixScale() {
return MatrixUtils.getMatrixScale(matrix);
}
float getMatrixAngle() {
return MatrixUtils.getMatrixAngle(matrix);
}
float[] getCurrentDrawablePoints() {
matrix.mapPoints(mappedDrawablePoints, drawablePoints);
return mappedDrawablePoints;
}
boolean isFilledArea() {
RectF bounds = getCurrentDrawableBounds();
return !(bounds.left > area.left()
|| bounds.top > area.top()
|| bounds.right < area.right()
|| bounds.bottom < area.bottom());
}
boolean canFilledArea() {
float scale = MatrixUtils.getMatrixScale(matrix);
float minScale = getMinMatrixScale(this);
return scale >= minScale;
}
void record() {
previousMatrix.set(matrix);
}
void translate(float offsetX, float offsetY) {
matrix.set(previousMatrix);
postTranslate(offsetX, offsetY);
}
private void zoom(float scaleX, float scaleY, PointF midPoint) {
matrix.set(previousMatrix);
postScale(scaleX, scaleY, midPoint);
}
void zoomAndTranslate(float scaleX, float scaleY, PointF midPoint, float offsetX, float offsetY) {
matrix.set(previousMatrix);
postTranslate(offsetX, offsetY);
postScale(scaleX, scaleY, midPoint);
}
void set(Matrix matrix) {
this.matrix.set(matrix);
moveToFillArea(null);
}
void postTranslate(float x, float y) {
this.matrix.postTranslate(x, y);
}
void postScale(float scaleX, float scaleY, PointF midPoint) {
this.matrix.postScale(scaleX, scaleY, midPoint.x, midPoint.y);
}
void postFlipVertically() {
this.matrix.postScale(1, -1, area.centerX(), area.centerY());
}
void postFlipHorizontally() {
this.matrix.postScale(-1, 1, area.centerX(), area.centerY());
}
void postRotate(float degree) {
this.matrix.postRotate(degree, area.centerX(), area.centerY());
float minScale = getMinMatrixScale(this);
if (getMatrixScale() < minScale) {
final PointF midPoint = new PointF();
midPoint.set(getCurrentDrawableCenterPoint());
postScale(minScale / getMatrixScale(), minScale / getMatrixScale(), midPoint);
}
if (!judgeIsImageContainsBorder(this, getMatrixAngle())) {
final float[] imageIndents = calculateImageIndents(this);
float deltaX = -(imageIndents[0] + imageIndents[2]);
float deltaY = -(imageIndents[1] + imageIndents[3]);
postTranslate(deltaX, deltaY);
}
}
private void animateTranslate(final View view, final float translateX, final float translateY) {
animator.end();
animator.removeAllUpdateListeners();
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float x = translateX * (float) animation.getAnimatedValue();
float y = translateY * (float) animation.getAnimatedValue();
translate(x, y);
view.invalidate();
}
});
animator.setDuration(duration);
animator.start();
}
void moveToFillArea(final View view) {
if (isFilledArea()) return;
record();
RectF rectF = getCurrentDrawableBounds();
float offsetX = 0f;
float offsetY = 0f;
if (rectF.left > area.left()) {
offsetX = area.left() - rectF.left;
}
if (rectF.top > area.top()) {
offsetY = area.top() - rectF.top;
}
if (rectF.right < area.right()) {
offsetX = area.right() - rectF.right;
}
if (rectF.bottom < area.bottom()) {
offsetY = area.bottom() - rectF.bottom;
}
if (view == null) {
postTranslate(offsetX, offsetY);
} else {
animateTranslate(view, offsetX, offsetY);
}
}
void fillArea(final View view, boolean quick) {
if (isFilledArea()) return;
record();
final float startScale = getMatrixScale();
final float endScale = getMinMatrixScale(this);
final PointF midPoint = new PointF();
midPoint.set(getCurrentDrawableCenterPoint());
tempMatrix.set(matrix);
tempMatrix.postScale(endScale / startScale, endScale / startScale, midPoint.x, midPoint.y);
RectF rectF = new RectF(drawableBounds);
tempMatrix.mapRect(rectF);
float offsetX = 0f;
float offsetY = 0f;
if (rectF.left > area.left()) {
offsetX = area.left() - rectF.left;
}
if (rectF.top > area.top()) {
offsetY = area.top() - rectF.top;
}
if (rectF.right < area.right()) {
offsetX = area.right() - rectF.right;
}
if (rectF.bottom < area.bottom()) {
offsetY = area.bottom() - rectF.bottom;
}
final float translateX = offsetX;
final float translateY = offsetY;
animator.end();
animator.removeAllUpdateListeners();
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
float scale = (startScale + (endScale - startScale) * value) / startScale;
float x = translateX * value;
float y = translateY * value;
zoom(scale, scale, midPoint);
postTranslate(x, y);
view.invalidate();
}
});
if (quick) {
animator.setDuration(0);
} else {
animator.setDuration(duration);
}
animator.start();
}
void updateWith(final MotionEvent event, final Line line) {
float offsetX = (event.getX() - previousMoveX) / 2;
float offsetY = (event.getY() - previousMoveY) / 2;
if (!canFilledArea()) {
final Area area = getArea();
float deltaScale = getMinMatrixScale(this) / getMatrixScale();
postScale(deltaScale, deltaScale, area.getCenterPoint());
record();
previousMoveX = event.getX();
previousMoveY = event.getY();
}
if (line.direction() == Line.Direction.HORIZONTAL) {
translate(0, offsetY);
} else if (line.direction() == Line.Direction.VERTICAL) {
translate(offsetX, 0);
}
final RectF rectF = getCurrentDrawableBounds();
final Area area = getArea();
float moveY = 0f;
if (rectF.top > area.top()) {
moveY = area.top() - rectF.top;
}
if (rectF.bottom < area.bottom()) {
moveY = area.bottom() - rectF.bottom;
}
float moveX = 0f;
if (rectF.left > area.left()) {
moveX = area.left() - rectF.left;
}
if (rectF.right < area.right()) {
moveX = area.right() - rectF.right;
}
if (moveX != 0 || moveY != 0) {
previousMoveX = event.getX();
previousMoveY = event.getY();
postTranslate(moveX, moveY);
record();
}
}
public void setArea(Area area) {
this.area = area;
}
boolean isAnimateRunning() {
return animator.isRunning();
}
void setAnimateDuration(int duration) {
this.duration = duration;
}
}

View File

@@ -0,0 +1,93 @@
package com.nnbc123.library.easyphoto.models.puzzle;
import com.nnbc123.library.easyphoto.models.puzzle.template.slant.OneSlantLayout;
import com.nnbc123.library.easyphoto.models.puzzle.template.slant.SlantLayoutHelper;
import com.nnbc123.library.easyphoto.models.puzzle.template.slant.ThreeSlantLayout;
import com.nnbc123.library.easyphoto.models.puzzle.template.slant.TwoSlantLayout;
import com.nnbc123.library.easyphoto.models.puzzle.template.straight.EightStraightLayout;
import com.nnbc123.library.easyphoto.models.puzzle.template.straight.FiveStraightLayout;
import com.nnbc123.library.easyphoto.models.puzzle.template.straight.FourStraightLayout;
import com.nnbc123.library.easyphoto.models.puzzle.template.straight.NineStraightLayout;
import com.nnbc123.library.easyphoto.models.puzzle.template.straight.OneStraightLayout;
import com.nnbc123.library.easyphoto.models.puzzle.template.straight.SevenStraightLayout;
import com.nnbc123.library.easyphoto.models.puzzle.template.straight.SixStraightLayout;
import com.nnbc123.library.easyphoto.models.puzzle.template.straight.StraightLayoutHelper;
import com.nnbc123.library.easyphoto.models.puzzle.template.straight.ThreeStraightLayout;
import com.nnbc123.library.easyphoto.models.puzzle.template.straight.TwoStraightLayout;
import java.util.ArrayList;
import java.util.List;
/**
* @author wupanjie
*/
public class PuzzleUtils {
private PuzzleUtils() {
//no instance
}
public static PuzzleLayout getPuzzleLayout(int type, int borderSize, int themeId) {
if (type == 0) {
switch (borderSize) {
case 1:
return new OneSlantLayout(themeId);
case 2:
return new TwoSlantLayout(themeId);
case 3:
return new ThreeSlantLayout(themeId);
default:
return new OneSlantLayout(themeId);
}
} else {
switch (borderSize) {
case 1:
return new OneStraightLayout(themeId);
case 2:
return new TwoStraightLayout(themeId);
case 3:
return new ThreeStraightLayout(themeId);
case 4:
return new FourStraightLayout(themeId);
case 5:
return new FiveStraightLayout(themeId);
case 6:
return new SixStraightLayout(themeId);
case 7:
return new SevenStraightLayout(themeId);
case 8:
return new EightStraightLayout(themeId);
case 9:
return new NineStraightLayout(themeId);
default:
return new OneStraightLayout(themeId);
}
}
}
public static List<PuzzleLayout> getAllPuzzleLayouts() {
List<PuzzleLayout> puzzleLayouts = new ArrayList<>();
//slant layout
puzzleLayouts.addAll(SlantLayoutHelper.getAllThemeLayout(2));
puzzleLayouts.addAll(SlantLayoutHelper.getAllThemeLayout(3));
// straight layout
puzzleLayouts.addAll(StraightLayoutHelper.getAllThemeLayout(2));
puzzleLayouts.addAll(StraightLayoutHelper.getAllThemeLayout(3));
puzzleLayouts.addAll(StraightLayoutHelper.getAllThemeLayout(4));
puzzleLayouts.addAll(StraightLayoutHelper.getAllThemeLayout(5));
puzzleLayouts.addAll(StraightLayoutHelper.getAllThemeLayout(6));
puzzleLayouts.addAll(StraightLayoutHelper.getAllThemeLayout(7));
puzzleLayouts.addAll(StraightLayoutHelper.getAllThemeLayout(8));
puzzleLayouts.addAll(StraightLayoutHelper.getAllThemeLayout(9));
return puzzleLayouts;
}
public static List<PuzzleLayout> getPuzzleLayouts(int pieceCount) {
List<PuzzleLayout> puzzleLayouts = new ArrayList<>();
puzzleLayouts.addAll(SlantLayoutHelper.getAllThemeLayout(pieceCount));
puzzleLayouts.addAll(StraightLayoutHelper.getAllThemeLayout(pieceCount));
return puzzleLayouts;
}
}

View File

@@ -0,0 +1,792 @@
package com.nnbc123.library.easyphoto.models.puzzle;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import androidx.core.content.ContextCompat;
import com.nnbc123.library.R;
import java.util.ArrayList;
import java.util.List;
/**
* @author wupanjie
*/
public class PuzzleView extends View {
private static final String TAG = "SlantPuzzleView";
private enum ActionMode {
NONE,
DRAG,
ZOOM,
MOVE,
SWAP
}
private ActionMode currentMode = ActionMode.NONE;
private List<PuzzlePiece> puzzlePieces = new ArrayList<>();
private List<PuzzlePiece> needChangePieces = new ArrayList<>();
private PuzzleLayout puzzleLayout;
private RectF bounds;
private int lineSize;
private int duration;
private Line handlingLine;
private PuzzlePiece handlingPiece;
private PuzzlePiece replacePiece;
private PuzzlePiece previousHandlingPiece;
private Paint linePaint;
private Paint selectedAreaPaint;
private Paint handleBarPaint;
private float downX;
private float downY;
private float previousDistance;
private PointF midPoint;
private boolean needDrawLine;
private boolean needDrawOuterLine;
private boolean touchEnable = true;
private int lineColor;
private int selectedLineColor;
private int handleBarColor;
private float piecePadding;
private float pieceRadian;
private boolean needResetPieceMatrix = true;
private OnPieceSelectedListener onPieceSelectedListener;
private Runnable switchToSwapAction = new Runnable() {
@Override
public void run() {
currentMode = ActionMode.SWAP;
invalidate();
}
};
public PuzzleView(Context context) {
this(context, null);
}
public PuzzleView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PuzzleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
if (attrs != null) {
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.PuzzleView);
lineSize = ta.getInt(R.styleable.PuzzleView_line_size, 4);
lineColor = ta.getColor(R.styleable.PuzzleView_line_color,
ContextCompat.getColor(getContext(), R.color.easy_photos_fg_primary));
selectedLineColor =
ta.getColor(R.styleable.PuzzleView_selected_line_color,
ContextCompat.getColor(getContext(), R.color.easy_photos_fg_accent));
handleBarColor =
ta.getColor(R.styleable.PuzzleView_handle_bar_color,
ContextCompat.getColor(getContext(), R.color.easy_photos_fg_accent));
piecePadding = ta.getDimensionPixelSize(R.styleable.PuzzleView_piece_padding, 0);
needDrawLine = ta.getBoolean(R.styleable.PuzzleView_need_draw_line, false);
needDrawOuterLine = ta.getBoolean(R.styleable.PuzzleView_need_draw_outer_line, false);
duration = ta.getInt(R.styleable.PuzzleView_animation_duration, 300);
pieceRadian = ta.getFloat(R.styleable.PuzzleView_radian, 0f);
ta.recycle();
}
bounds = new RectF();
// init some paint
linePaint = new Paint();
linePaint.setAntiAlias(true);
linePaint.setColor(lineColor);
linePaint.setStrokeWidth(lineSize);
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setStrokeJoin(Paint.Join.ROUND);
linePaint.setStrokeCap(Paint.Cap.SQUARE);
selectedAreaPaint = new Paint();
selectedAreaPaint.setAntiAlias(true);
selectedAreaPaint.setStyle(Paint.Style.STROKE);
selectedAreaPaint.setStrokeJoin(Paint.Join.ROUND);
selectedAreaPaint.setStrokeCap(Paint.Cap.ROUND);
selectedAreaPaint.setColor(selectedLineColor);
selectedAreaPaint.setStrokeWidth(lineSize);
handleBarPaint = new Paint();
handleBarPaint.setAntiAlias(true);
handleBarPaint.setStyle(Paint.Style.FILL);
handleBarPaint.setColor(handleBarColor);
handleBarPaint.setStrokeWidth(lineSize * 3);
midPoint = new PointF();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
resetPuzzleBounds();
if (puzzlePieces.size() != 0) {
int size = puzzlePieces.size();
for (int i = 0; i < size; i++) {
PuzzlePiece piece = puzzlePieces.get(i);
piece.setArea(puzzleLayout.getArea(i));
if (needResetPieceMatrix) {
piece.set(MatrixUtils.generateMatrix(piece, 0f));
} else {
piece.fillArea(this, true);
}
}
}
invalidate();
}
private void resetPuzzleBounds() {
bounds.left = getPaddingLeft();
bounds.top = getPaddingTop();
bounds.right = getWidth() - getPaddingRight();
bounds.bottom = getHeight() - getPaddingBottom();
if (puzzleLayout != null) {
puzzleLayout.reset();
puzzleLayout.setOuterBounds(bounds);
puzzleLayout.layout();
puzzleLayout.setPadding(piecePadding);
puzzleLayout.setRadian(pieceRadian);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (puzzleLayout == null) {
return;
}
linePaint.setStrokeWidth(lineSize);
selectedAreaPaint.setStrokeWidth(lineSize);
handleBarPaint.setStrokeWidth(lineSize * 3);
// draw pieces
int count = puzzleLayout.getAreaCount();
for (int i = 0; i < count; i++) {
if (i >= puzzlePieces.size()) {
break;
}
PuzzlePiece piece = puzzlePieces.get(i);
if (piece == handlingPiece && currentMode == ActionMode.SWAP) {
continue;
}
if (puzzlePieces.size() > i) {
piece.draw(canvas);
}
}
// draw outer bounds
if (needDrawOuterLine) {
for (Line outerLine : puzzleLayout.getOuterLines()) {
drawLine(canvas, outerLine);
}
}
// draw slant lines
if (needDrawLine) {
for (Line line : puzzleLayout.getLines()) {
drawLine(canvas, line);
}
}
// draw selected area
if (handlingPiece != null && currentMode != ActionMode.SWAP) {
drawSelectedArea(canvas, handlingPiece);
}
// draw swap piece
if (handlingPiece != null && currentMode == ActionMode.SWAP) {
handlingPiece.draw(canvas, 128);
if (replacePiece != null) {
drawSelectedArea(canvas, replacePiece);
}
}
}
private void drawSelectedArea(Canvas canvas, PuzzlePiece piece) {
final Area area = piece.getArea();
// draw select area
canvas.drawPath(area.getAreaPath(), selectedAreaPaint);
// draw handle bar
for (Line line : area.getLines()) {
if (puzzleLayout.getLines().contains(line)) {
PointF[] handleBarPoints = area.getHandleBarPoints(line);
canvas.drawLine(handleBarPoints[0].x, handleBarPoints[0].y, handleBarPoints[1].x,
handleBarPoints[1].y, handleBarPaint);
canvas.drawCircle(handleBarPoints[0].x, handleBarPoints[0].y, lineSize * 3 / 2,
handleBarPaint);
canvas.drawCircle(handleBarPoints[1].x, handleBarPoints[1].y, lineSize * 3 / 2,
handleBarPaint);
}
}
}
private void drawLine(Canvas canvas, Line line) {
canvas.drawLine(line.startPoint().x, line.startPoint().y, line.endPoint().x,
line.endPoint().y,
linePaint);
}
public void setPuzzleLayout(PuzzleLayout puzzleLayout) {
clearPieces();
this.puzzleLayout = puzzleLayout;
this.puzzleLayout.setOuterBounds(bounds);
this.puzzleLayout.layout();
invalidate();
}
public PuzzleLayout getPuzzleLayout() {
return this.puzzleLayout;
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!touchEnable) {
return super.onTouchEvent(event);
}
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
downX = event.getX();
downY = event.getY();
decideActionMode(event);
prepareAction(event);
break;
case MotionEvent.ACTION_POINTER_DOWN:
previousDistance = calculateDistance(event);
calculateMidPoint(event, midPoint);
decideActionMode(event);
break;
case MotionEvent.ACTION_MOVE:
performAction(event);
if ((Math.abs(event.getX() - downX) > 10 || Math.abs(event.getY() - downY) > 10)
&& currentMode != ActionMode.SWAP) {
removeCallbacks(switchToSwapAction);
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
finishAction(event);
currentMode = ActionMode.NONE;
removeCallbacks(switchToSwapAction);
break;
}
invalidate();
return true;
}
// 决定应该执行什么Action
private void decideActionMode(MotionEvent event) {
for (PuzzlePiece piece : puzzlePieces) {
if (piece.isAnimateRunning()) {
currentMode = ActionMode.NONE;
return;
}
}
if (event.getPointerCount() == 1) {
handlingLine = findHandlingLine();
if (handlingLine != null) {
currentMode = ActionMode.MOVE;
} else {
handlingPiece = findHandlingPiece();
if (handlingPiece != null) {
currentMode = ActionMode.DRAG;
postDelayed(switchToSwapAction, 500);
}
}
} else if (event.getPointerCount() > 1) {
if (handlingPiece != null
&& handlingPiece.contains(event.getX(1), event.getY(1))
&& currentMode == ActionMode.DRAG) {
currentMode = ActionMode.ZOOM;
}
}
}
// 执行Action前的准备工作
@SuppressWarnings("unused")
private void prepareAction(MotionEvent event) {
switch (currentMode) {
case NONE:
break;
case DRAG:
handlingPiece.record();
break;
case ZOOM:
handlingPiece.record();
break;
case MOVE:
handlingLine.prepareMove();
needChangePieces.clear();
needChangePieces.addAll(findNeedChangedPieces());
for (PuzzlePiece piece : needChangePieces) {
piece.record();
piece.setPreviousMoveX(downX);
piece.setPreviousMoveY(downY);
}
break;
}
}
// 执行Action
private void performAction(MotionEvent event) {
switch (currentMode) {
case NONE:
break;
case DRAG:
dragPiece(handlingPiece, event);
break;
case ZOOM:
zoomPiece(handlingPiece, event);
break;
case SWAP:
dragPiece(handlingPiece, event);
replacePiece = findReplacePiece(event);
break;
case MOVE:
moveLine(handlingLine, event);
break;
}
}
// 结束Action
private void finishAction(MotionEvent event) {
switch (currentMode) {
case NONE:
break;
case DRAG:
if (handlingPiece != null && !handlingPiece.isFilledArea()) {
handlingPiece.moveToFillArea(this);
}
if (previousHandlingPiece == handlingPiece
&& Math.abs(downX - event.getX()) < 3
&& Math.abs(downY - event.getY()) < 3) {
handlingPiece = null;
}
// trigger listener
if (onPieceSelectedListener != null) {
onPieceSelectedListener.onPieceSelected(handlingPiece,
puzzlePieces.indexOf(handlingPiece));
}
previousHandlingPiece = handlingPiece;
break;
case ZOOM:
if (handlingPiece != null && !handlingPiece.isFilledArea()) {
if (handlingPiece.canFilledArea()) {
handlingPiece.moveToFillArea(this);
} else {
handlingPiece.fillArea(this, false);
}
}
previousHandlingPiece = handlingPiece;
break;
case MOVE:
break;
case SWAP:
if (handlingPiece != null && replacePiece != null) {
Drawable temp = handlingPiece.getDrawable();
handlingPiece.setDrawable(replacePiece.getDrawable());
replacePiece.setDrawable(temp);
handlingPiece.fillArea(this, true);
replacePiece.fillArea(this, true);
handlingPiece = null;
replacePiece = null;
previousHandlingPiece = null;
// trigger listener
if (onPieceSelectedListener != null) {
onPieceSelectedListener.onPieceSelected(null,
0);
}
}
break;
}
handlingLine = null;
needChangePieces.clear();
}
private void moveLine(Line line, MotionEvent event) {
if (line == null || event == null) return;
boolean needUpdate;
if (line.direction() == Line.Direction.HORIZONTAL) {
needUpdate = line.move(event.getY() - downY, 80);
} else {
needUpdate = line.move(event.getX() - downX, 80);
}
if (needUpdate) {
puzzleLayout.update();
updatePiecesInArea(line, event);
}
}
private void updatePiecesInArea(Line line, MotionEvent event) {
int size = needChangePieces.size();
for (int i = 0; i < size; i++) {
needChangePieces.get(i).updateWith(event, line);
}
}
private void zoomPiece(PuzzlePiece piece, MotionEvent event) {
if (piece == null || event == null || event.getPointerCount() < 2) return;
float scale = calculateDistance(event) / previousDistance;
piece.zoomAndTranslate(scale, scale, midPoint, event.getX() - downX, event.getY() - downY);
}
private void dragPiece(PuzzlePiece piece, MotionEvent event) {
if (piece == null || event == null) return;
piece.translate(event.getX() - downX, event.getY() - downY);
}
public void replace(Bitmap bitmap) {
replace(new BitmapDrawable(getResources(), bitmap));
}
public void replace(final Drawable bitmapDrawable) {
post(new Runnable() {
@Override
public void run() {
if (handlingPiece == null) {
return;
}
handlingPiece.setDrawable(bitmapDrawable);
handlingPiece.set(MatrixUtils.generateMatrix(handlingPiece, 0f));
postInvalidate();
}
});
}
public void flipVertically() {
if (handlingPiece == null) {
return;
}
handlingPiece.postFlipVertically();
handlingPiece.record();
invalidate();
}
public void flipHorizontally() {
if (handlingPiece == null) {
return;
}
handlingPiece.postFlipHorizontally();
handlingPiece.record();
invalidate();
}
public void rotate(float degree) {
if (handlingPiece == null) {
return;
}
handlingPiece.postRotate(degree);
handlingPiece.record();
invalidate();
}
private PuzzlePiece findHandlingPiece() {
for (PuzzlePiece piece : puzzlePieces) {
if (piece.contains(downX, downY)) {
return piece;
}
}
return null;
}
private Line findHandlingLine() {
for (Line line : puzzleLayout.getLines()) {
if (line.contains(downX, downY, 40)) {
return line;
}
}
return null;
}
private PuzzlePiece findReplacePiece(MotionEvent event) {
for (PuzzlePiece piece : puzzlePieces) {
if (piece.contains(event.getX(), event.getY())) {
return piece;
}
}
return null;
}
private List<PuzzlePiece> findNeedChangedPieces() {
if (handlingLine == null) return new ArrayList<>();
List<PuzzlePiece> needChanged = new ArrayList<>();
for (PuzzlePiece piece : puzzlePieces) {
if (piece.contains(handlingLine)) {
needChanged.add(piece);
}
}
return needChanged;
}
private float calculateDistance(MotionEvent event) {
float x = event.getX(0) - event.getX(1);
float y = event.getY(0) - event.getY(1);
return (float) Math.sqrt(x * x + y * y);
}
private void calculateMidPoint(MotionEvent event, PointF point) {
point.x = (event.getX(0) + event.getX(1)) / 2;
point.y = (event.getY(0) + event.getY(1)) / 2;
}
public void reset() {
clearPieces();
if (puzzleLayout != null) {
puzzleLayout.reset();
}
}
public void clearPieces() {
handlingLine = null;
handlingPiece = null;
replacePiece = null;
needChangePieces.clear();
puzzlePieces.clear();
}
public void addPieces(List<Bitmap> bitmaps) {
for (Bitmap bitmap : bitmaps) {
addPiece(bitmap);
}
postInvalidate();
}
public void addPiece(Bitmap bitmap) {
BitmapDrawable bitmapDrawable = new BitmapDrawable(getResources(), bitmap);
bitmapDrawable.setAntiAlias(true);
bitmapDrawable.setFilterBitmap(true);
addPiece(bitmapDrawable);
}
public void addPiece(Drawable drawable) {
int position = puzzlePieces.size();
if (position >= puzzleLayout.getAreaCount()) {
Log.e(TAG, "addPiece: can not add more. the current puzzle layout can contains "
+ puzzleLayout.getAreaCount()
+ " puzzle piece.");
return;
}
final Area area = puzzleLayout.getArea(position);
area.setPadding(piecePadding);
PuzzlePiece piece = new PuzzlePiece(drawable, area, new Matrix());
final Matrix matrix = MatrixUtils.generateMatrix(area, drawable, 0f);
piece.set(matrix);
piece.setAnimateDuration(duration);
puzzlePieces.add(piece);
setPiecePadding(piecePadding);
setPieceRadian(pieceRadian);
invalidate();
}
public void setAnimateDuration(int duration) {
this.duration = duration;
for (PuzzlePiece piece : puzzlePieces) {
piece.setAnimateDuration(duration);
}
}
public boolean isNeedDrawLine() {
return needDrawLine;
}
public void setNeedDrawLine(boolean needDrawLine) {
this.needDrawLine = needDrawLine;
handlingPiece = null;
previousHandlingPiece = null;
invalidate();
}
public boolean isNeedDrawOuterLine() {
return needDrawOuterLine;
}
public void setNeedDrawOuterLine(boolean needDrawOuterLine) {
this.needDrawOuterLine = needDrawOuterLine;
invalidate();
}
public int getLineColor() {
return lineColor;
}
public void setLineColor(int lineColor) {
this.lineColor = lineColor;
this.linePaint.setColor(lineColor);
invalidate();
}
public int getLineSize() {
return lineSize;
}
public void setLineSize(int lineSize) {
this.lineSize = lineSize;
invalidate();
}
public int getSelectedLineColor() {
return selectedLineColor;
}
public void setSelectedLineColor(int selectedLineColor) {
this.selectedLineColor = selectedLineColor;
this.selectedAreaPaint.setColor(selectedLineColor);
invalidate();
}
public int getHandleBarColor() {
return handleBarColor;
}
public void setHandleBarColor(int handleBarColor) {
this.handleBarColor = handleBarColor;
this.handleBarPaint.setColor(handleBarColor);
invalidate();
}
public boolean isTouchEnable() {
return touchEnable;
}
public void setTouchEnable(boolean touchEnable) {
this.touchEnable = touchEnable;
}
public void clearHandling() {
handlingPiece = null;
handlingLine = null;
replacePiece = null;
previousHandlingPiece = null;
needChangePieces.clear();
}
public void setPiecePadding(float padding) {
this.piecePadding = padding;
if (puzzleLayout != null) {
puzzleLayout.setPadding(padding);
}
invalidate();
}
public void setPieceRadian(float radian) {
this.pieceRadian = radian;
if (puzzleLayout != null) {
puzzleLayout.setRadian(radian);
}
invalidate();
}
@Override
public void setBackgroundColor(int color) {
super.setBackgroundColor(color);
if (puzzleLayout != null) {
puzzleLayout.setColor(color);
}
}
public void setNeedResetPieceMatrix(boolean needResetPieceMatrix) {
this.needResetPieceMatrix = needResetPieceMatrix;
}
public float getPiecePadding() {
return piecePadding;
}
public float getPieceRadian() {
return pieceRadian;
}
public void setOnPieceSelectedListener(OnPieceSelectedListener onPieceSelectedListener) {
this.onPieceSelectedListener = onPieceSelectedListener;
}
public interface OnPieceSelectedListener {
void onPieceSelected(PuzzlePiece piece, int position);
}
}

View File

@@ -0,0 +1,32 @@
package com.nnbc123.library.easyphoto.models.puzzle;
import android.content.Context;
import android.util.AttributeSet;
/**
* @author wupanjie
*/
public class SquarePuzzleView extends PuzzleView {
public SquarePuzzleView(Context context) {
super(context);
}
public SquarePuzzleView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SquarePuzzleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMeasuredWidth();
int height = getMeasuredHeight();
int length = width > height ? height : width;
setMeasuredDimension(length, length);
}
}

Some files were not shown because too many files have changed in this diff Show More