From 68b2e0754ca6a856d46af5422695bf58c58636a5 Mon Sep 17 00:00:00 2001 From: tongtongstudio Date: Mon, 20 Oct 2025 21:30:28 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=BB=E9=A1=B5=E5=9B=BA=E5=AE=9A=E6=98=BE?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 67 +- app/src/main/AndroidManifest.xml | 35 +- .../ttstd/dialer/activity/MainActivity.java | 16 - .../dialer/activity/main/MainActivity.java | 105 +++ .../dialer/activity/main/MainViewModel.java | 9 + .../dialer/annotations/CustomGlideModule.java | 30 + .../dialer/annotations/ImageViewAdapter.java | 53 ++ .../dialer/base/BaseAlertDialogBuilder.java | 50 ++ .../ttstd/dialer/base/BaseApplication.java | 100 +++ .../dialer/base/BaseDataBindingActivity.java | 88 +++ .../ttstd/dialer/base/BaseDialogFragment.java | 44 ++ .../com/ttstd/dialer/base/BaseFragment.java | 44 ++ .../dialer/base/BaseTransparentActivity.java | 91 +++ .../dialer/base/mvp/BaseMvpActivity.java | 35 + .../ttstd/dialer/base/mvp/BasePresenter.java | 8 + .../com/ttstd/dialer/base/mvp/BaseView.java | 6 + .../dialer/base/mvvm/BaseMvvmActivity.java | 54 ++ .../ttstd/dialer/base/mvvm/BaseViewModel.java | 54 ++ .../base/mvvm/fragment/BaseMvvmFragment.java | 275 ++++++++ .../ttstd/dialer/base/rx/BaseRxActivity.java | 94 +++ .../dialer/base/rx/BaseRxDialogFragment.java | 123 ++++ .../ttstd/dialer/base/rx/BaseRxFragment.java | 123 ++++ .../ttstd/dialer/base/rx/BaseRxService.java | 62 ++ .../com/ttstd/dialer/config/CommonConfig.java | 8 + .../com/ttstd/dialer/contact/AppDatabase.java | 28 + .../com/ttstd/dialer/contact/Contact.java | 75 +++ .../com/ttstd/dialer/contact/ContactDao.java | 37 ++ .../dialer/contact/ContactRepository.java | 56 ++ .../dialer/contact/ContactViewModel.java | 46 ++ .../fragment/contact/ContactFragment.java | 47 ++ .../fragment/contact/ContactViewModel.java | 9 + .../dialer/fragment/home/HomeFragment.java | 242 +++++++ .../dialer/fragment/home/HomeViewModel.java | 9 + .../java/com/ttstd/dialer/utils/ApkUtils.java | 86 +++ .../java/com/ttstd/dialer/utils/DataUtil.java | 29 + .../utils/LunarCalendarFestivalUtils.java | 600 ++++++++++++++++++ .../com/ttstd/dialer/utils/ScreenUtils.java | 77 +++ .../com/ttstd/dialer/utils/SystemUtils.java | 24 + .../com/ttstd/dialer/utils/TimeUtils.java | 19 + .../dialer/view/BaseFragmentPagerAdapter.java | 203 ++++++ .../dialer/view/ScaleCircleNavigator.java | 323 ++++++++++ app/src/main/res/drawable/ic_add.xml | 12 + app/src/main/res/drawable/ic_contact.xml | 18 + app/src/main/res/drawable/ic_douyin.xml | 18 + app/src/main/res/drawable/ic_settings.xml | 15 + app/src/main/res/drawable/ic_weixin.xml | 15 + app/src/main/res/layout/activity_main.xml | 48 +- app/src/main/res/layout/fragment_contact.xml | 22 + app/src/main/res/layout/fragment_home.xml | 251 ++++++++ app/src/main/res/values/colors.xml | 248 ++++++++ app/src/main/res/values/dimens.xml | 4 + app/src/main/res/values/strings.xml | 3 + app/src/main/res/values/styles.xml | 16 +- app/src/main/res/xml/file_paths.xml | 18 + app/src/main/res/xml/network.xml | 4 + iconloader/.gitignore | 1 + iconloader/build.gradle | 37 ++ iconloader/consumer-rules.pro | 0 iconloader/proguard-rules.pro | 21 + .../iconloader/ExampleInstrumentedTest.java | 27 + iconloader/src/main/AndroidManifest.xml | 1 + .../uiui/iconloader/CacheKeyGenerator.java | 16 + .../com/uiui/iconloader/IconCacheManager.java | 79 +++ .../java/com/uiui/iconloader/IconLoader.java | 97 +++ .../com/uiui/iconloader/ExampleUnitTest.java | 17 + niceimageview/.gitignore | 1 + niceimageview/build.gradle | 48 ++ niceimageview/proguard-rules.pro | 21 + niceimageview/src/main/AndroidManifest.xml | 2 + .../java/com/shehuan/niv/NiceImageView.java | 338 ++++++++++ .../src/main/java/com/shehuan/niv/Utils.java | 11 + niceimageview/src/main/res/values/attrs.xml | 17 + niceimageview/src/main/res/values/strings.xml | 3 + settings.gradle | 2 +- 74 files changed, 4881 insertions(+), 34 deletions(-) delete mode 100644 app/src/main/java/com/ttstd/dialer/activity/MainActivity.java create mode 100644 app/src/main/java/com/ttstd/dialer/activity/main/MainActivity.java create mode 100644 app/src/main/java/com/ttstd/dialer/activity/main/MainViewModel.java create mode 100644 app/src/main/java/com/ttstd/dialer/annotations/CustomGlideModule.java create mode 100644 app/src/main/java/com/ttstd/dialer/annotations/ImageViewAdapter.java create mode 100644 app/src/main/java/com/ttstd/dialer/base/BaseAlertDialogBuilder.java create mode 100644 app/src/main/java/com/ttstd/dialer/base/BaseApplication.java create mode 100644 app/src/main/java/com/ttstd/dialer/base/BaseDataBindingActivity.java create mode 100644 app/src/main/java/com/ttstd/dialer/base/BaseDialogFragment.java create mode 100644 app/src/main/java/com/ttstd/dialer/base/BaseFragment.java create mode 100644 app/src/main/java/com/ttstd/dialer/base/BaseTransparentActivity.java create mode 100644 app/src/main/java/com/ttstd/dialer/base/mvp/BaseMvpActivity.java create mode 100644 app/src/main/java/com/ttstd/dialer/base/mvp/BasePresenter.java create mode 100644 app/src/main/java/com/ttstd/dialer/base/mvp/BaseView.java create mode 100644 app/src/main/java/com/ttstd/dialer/base/mvvm/BaseMvvmActivity.java create mode 100644 app/src/main/java/com/ttstd/dialer/base/mvvm/BaseViewModel.java create mode 100644 app/src/main/java/com/ttstd/dialer/base/mvvm/fragment/BaseMvvmFragment.java create mode 100644 app/src/main/java/com/ttstd/dialer/base/rx/BaseRxActivity.java create mode 100644 app/src/main/java/com/ttstd/dialer/base/rx/BaseRxDialogFragment.java create mode 100644 app/src/main/java/com/ttstd/dialer/base/rx/BaseRxFragment.java create mode 100644 app/src/main/java/com/ttstd/dialer/base/rx/BaseRxService.java create mode 100644 app/src/main/java/com/ttstd/dialer/config/CommonConfig.java create mode 100644 app/src/main/java/com/ttstd/dialer/contact/AppDatabase.java create mode 100644 app/src/main/java/com/ttstd/dialer/contact/Contact.java create mode 100644 app/src/main/java/com/ttstd/dialer/contact/ContactDao.java create mode 100644 app/src/main/java/com/ttstd/dialer/contact/ContactRepository.java create mode 100644 app/src/main/java/com/ttstd/dialer/contact/ContactViewModel.java create mode 100644 app/src/main/java/com/ttstd/dialer/fragment/contact/ContactFragment.java create mode 100644 app/src/main/java/com/ttstd/dialer/fragment/contact/ContactViewModel.java create mode 100644 app/src/main/java/com/ttstd/dialer/fragment/home/HomeFragment.java create mode 100644 app/src/main/java/com/ttstd/dialer/fragment/home/HomeViewModel.java create mode 100644 app/src/main/java/com/ttstd/dialer/utils/ApkUtils.java create mode 100644 app/src/main/java/com/ttstd/dialer/utils/DataUtil.java create mode 100644 app/src/main/java/com/ttstd/dialer/utils/LunarCalendarFestivalUtils.java create mode 100644 app/src/main/java/com/ttstd/dialer/utils/ScreenUtils.java create mode 100644 app/src/main/java/com/ttstd/dialer/utils/SystemUtils.java create mode 100644 app/src/main/java/com/ttstd/dialer/utils/TimeUtils.java create mode 100644 app/src/main/java/com/ttstd/dialer/view/BaseFragmentPagerAdapter.java create mode 100644 app/src/main/java/com/ttstd/dialer/view/ScaleCircleNavigator.java create mode 100644 app/src/main/res/drawable/ic_add.xml create mode 100644 app/src/main/res/drawable/ic_contact.xml create mode 100644 app/src/main/res/drawable/ic_douyin.xml create mode 100644 app/src/main/res/drawable/ic_settings.xml create mode 100644 app/src/main/res/drawable/ic_weixin.xml create mode 100644 app/src/main/res/layout/fragment_contact.xml create mode 100644 app/src/main/res/layout/fragment_home.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/xml/file_paths.xml create mode 100644 app/src/main/res/xml/network.xml create mode 100644 iconloader/.gitignore create mode 100644 iconloader/build.gradle create mode 100644 iconloader/consumer-rules.pro create mode 100644 iconloader/proguard-rules.pro create mode 100644 iconloader/src/androidTest/java/com/uiui/iconloader/ExampleInstrumentedTest.java create mode 100644 iconloader/src/main/AndroidManifest.xml create mode 100644 iconloader/src/main/java/com/uiui/iconloader/CacheKeyGenerator.java create mode 100644 iconloader/src/main/java/com/uiui/iconloader/IconCacheManager.java create mode 100644 iconloader/src/main/java/com/uiui/iconloader/IconLoader.java create mode 100644 iconloader/src/test/java/com/uiui/iconloader/ExampleUnitTest.java create mode 100644 niceimageview/.gitignore create mode 100644 niceimageview/build.gradle create mode 100644 niceimageview/proguard-rules.pro create mode 100644 niceimageview/src/main/AndroidManifest.xml create mode 100644 niceimageview/src/main/java/com/shehuan/niv/NiceImageView.java create mode 100644 niceimageview/src/main/java/com/shehuan/niv/Utils.java create mode 100644 niceimageview/src/main/res/values/attrs.xml create mode 100644 niceimageview/src/main/res/values/strings.xml diff --git a/app/build.gradle b/app/build.gradle index d05a21b..d76c0e5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -31,6 +31,12 @@ android { abortOnError false } + javaCompileOptions { + annotationProcessorOptions { + arguments = [AROUTER_MODULE_NAME: project.getName()] + } + } + dataBinding { enabled true } @@ -46,7 +52,10 @@ android { } dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) +// implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation project(path: ':niceimageview') + implementation project(path: ':iconloader') //保持1.3.1 更新会报错 implementation 'androidx.appcompat:appcompat:1.3.1' @@ -59,6 +68,18 @@ dependencies { implementation "androidx.fragment:fragment:1.4.1" implementation "androidx.viewpager2:viewpager2:1.0.0" implementation 'androidx.legacy:legacy-support-v4:1.0.0' + // Room依赖 + implementation "androidx.room:room-runtime:2.3.0" + implementation "androidx.room:room-rxjava3:2.3.0" + annotationProcessor "androidx.room:room-compiler:2.3.0" + // ViewModel和LiveData + implementation "androidx.lifecycle:lifecycle-viewmodel:2.3.0" + implementation "androidx.lifecycle:lifecycle-livedata:2.3.0" + implementation "androidx.lifecycle:lifecycle-runtime:2.3.0" + annotationProcessor "androidx.lifecycle:lifecycle-compiler:2.3.0" + + // 添加这行,使用 BOM 统一 Kotlin 相关库的版本 + implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.10")) testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.3.0' @@ -80,4 +101,48 @@ dependencies { //Gson implementation 'com.google.code.gson:gson:2.9.0' implementation 'com.google.zxing:core:3.5.0' + //生命周期管理 + implementation 'com.trello.rxlifecycle4:rxlifecycle:4.0.2' + implementation 'com.trello.rxlifecycle4:rxlifecycle-android:4.0.2' + implementation 'com.trello.rxlifecycle4:rxlifecycle-components:4.0.2' + implementation 'com.trello.rxlifecycle4:rxlifecycle-components-preference:4.0.2' + implementation 'com.trello.rxlifecycle4:rxlifecycle-android-lifecycle:4.0.2' + + + implementation 'com.jakewharton.rxbinding4:rxbinding:4.0.0' + /*https://github.com/JeremyLiao/LiveEventBus*/ + implementation 'com.jeremyliao:live-event-bus-x:1.7.3' + + implementation 'com.facebook.rebound:rebound:0.3.8' + //MMKV + implementation 'com.tencent:mmkv-static:1.2.14' + //bugly + implementation 'com.tencent.bugly:crashreport:4.1.9.2' + implementation 'com.iqiyi.xcrash:xcrash-android-lib:3.0.0' + // 替换成最新版本, 需要注意的是api + // 要与compiler匹配使用,均使用最新版可以保证兼容 + implementation 'com.alibaba:arouter-api:1.5.2' + annotationProcessor 'com.alibaba:arouter-compiler:1.5.2' + + //指示器 + implementation 'com.github.hackware1993:MagicIndicator:1.7.0' + //工具类 + implementation 'com.blankj:utilcodex:1.31.0' + //aria + implementation 'com.arialyy.aria:core:3.8.15' + annotationProcessor 'com.arialyy.aria:compiler:3.8.15' + //状态栏透明 + implementation 'com.gitee.zackratos:UltimateBarX:0.8.0' + //指示器 + implementation 'com.github.hackware1993:MagicIndicator:1.7.0' + + // 吐司框架:https://github.com/getActivity/Toaster + implementation 'com.github.getActivity:Toaster:12.6' + // 权限请求框架:https://github.com/getActivity/XXPermissions + implementation 'com.github.getActivity:XXPermissions:20.0' + //autosize会改变第三方view的大小 + //https://github.com/JessYanCoding/AndroidAutoSize + implementation 'me.jessyan:autosize:1.2.1' + + } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8073b1b..000bd48 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,22 +2,53 @@ + + android:name=".activity.main.MainActivity" + android:exported="true" + android:launchMode="singleTask" + android:screenOrientation="portrait" + android:theme="@style/AppThemeFitsSystemWindows"> + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/ttstd/dialer/activity/MainActivity.java b/app/src/main/java/com/ttstd/dialer/activity/MainActivity.java deleted file mode 100644 index 6cc3156..0000000 --- a/app/src/main/java/com/ttstd/dialer/activity/MainActivity.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.ttstd.dialer.activity; - -import androidx.appcompat.app.AppCompatActivity; - -import android.os.Bundle; - -import com.ttstd.dialer.R; - -public class MainActivity extends AppCompatActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - } -} diff --git a/app/src/main/java/com/ttstd/dialer/activity/main/MainActivity.java b/app/src/main/java/com/ttstd/dialer/activity/main/MainActivity.java new file mode 100644 index 0000000..099cee7 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/activity/main/MainActivity.java @@ -0,0 +1,105 @@ +package com.ttstd.dialer.activity.main; + +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + +import com.tencent.mmkv.MMKV; +import com.ttstd.dialer.R; +import com.ttstd.dialer.base.mvvm.BaseMvvmActivity; +import com.ttstd.dialer.config.CommonConfig; +import com.ttstd.dialer.databinding.ActivityMainBinding; +import com.ttstd.dialer.fragment.contact.ContactFragment; +import com.ttstd.dialer.fragment.home.HomeFragment; +import com.ttstd.dialer.view.BaseFragmentPagerAdapter; +import com.ttstd.dialer.view.ScaleCircleNavigator; + +import net.lucode.hackware.magicindicator.ViewPagerHelper; + +import java.util.ArrayList; +import java.util.List; + +public class MainActivity extends BaseMvvmActivity { + protected MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE); + + private FragmentManager mFragmentManager = getSupportFragmentManager(); + private BaseFragmentPagerAdapter mBaseFragmentPagerAdapter; + + private List mFragments = new ArrayList<>(); + private HomeFragment mHomeFragment; + private ContactFragment mContactFragment; + + private int mCurrentIndex = 0; + + private ScaleCircleNavigator mScaleCircleNavigator; + + + @Override + public boolean setNightMode() { + return true; + } + + @Override + public boolean setfitWindow() { + return true; + } + + @Override + protected int getLayoutId() { + return R.layout.activity_main; + } + + @Override + protected void initDataBinding() { + mViewModel.setContext(this); + mViewModel.setVDBinding(mViewDataBinding); + mViewModel.setLifecycle(getLifecycleSubject()); + mViewDataBinding.setClick(new BtnClick()); + } + + @Override + protected void initView() { + mScaleCircleNavigator = new ScaleCircleNavigator(this); + + if (mContactFragment == null) { + mContactFragment = new ContactFragment(); + mFragments.add(mContactFragment); + } + + boolean contactHome = mMMKV.decodeBool(CommonConfig.CONTACT_HOME_PAGE, false); + if (!contactHome) { + mCurrentIndex += 1; + } + if (mHomeFragment == null) { + mHomeFragment = new HomeFragment(); + mFragments.add(mHomeFragment); + } + + + mScaleCircleNavigator.setCircleCount(mFragments.size()); + mScaleCircleNavigator.notifyDataSetChanged(); + mScaleCircleNavigator.setNormalCircleColor(getColor(R.color.indicator_color_normal)); + mScaleCircleNavigator.setSelectedCircleColor(getColor(R.color.indicator_color_selected)); + mScaleCircleNavigator.setCircleClickListener(new ScaleCircleNavigator.OnCircleClickListener() { + @Override + public void onClick(int index) { + mViewDataBinding.viewPager.setCurrentItem(index); + } + }); + mBaseFragmentPagerAdapter = new BaseFragmentPagerAdapter(mFragmentManager, mFragments); + mViewDataBinding.viewPager.setAdapter(mBaseFragmentPagerAdapter); + mViewDataBinding.viewPager.setOffscreenPageLimit(10); + mViewDataBinding.viewPager.setCurrentItem(mCurrentIndex); + + mViewDataBinding.magicIndicator.setNavigator(mScaleCircleNavigator); + ViewPagerHelper.bind(mViewDataBinding.magicIndicator, mViewDataBinding.viewPager); + } + + @Override + protected void initData() { + + } + + public class BtnClick { + + } +} diff --git a/app/src/main/java/com/ttstd/dialer/activity/main/MainViewModel.java b/app/src/main/java/com/ttstd/dialer/activity/main/MainViewModel.java new file mode 100644 index 0000000..0c6d8aa --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/activity/main/MainViewModel.java @@ -0,0 +1,9 @@ +package com.ttstd.dialer.activity.main; + +import com.trello.rxlifecycle4.android.ActivityEvent; +import com.ttstd.dialer.base.mvvm.BaseViewModel; +import com.ttstd.dialer.databinding.ActivityMainBinding; + +public class MainViewModel extends BaseViewModel { + +} diff --git a/app/src/main/java/com/ttstd/dialer/annotations/CustomGlideModule.java b/app/src/main/java/com/ttstd/dialer/annotations/CustomGlideModule.java new file mode 100644 index 0000000..49c0a09 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/annotations/CustomGlideModule.java @@ -0,0 +1,30 @@ +package com.ttstd.dialer.annotations; + +import android.content.Context; + +import androidx.annotation.NonNull; + +import com.bumptech.glide.GlideBuilder; +import com.bumptech.glide.annotation.GlideModule; +import com.bumptech.glide.load.engine.bitmap_recycle.LruBitmapPool; +import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory; +import com.bumptech.glide.load.engine.cache.LruResourceCache; +import com.bumptech.glide.module.AppGlideModule; + +@GlideModule +public class CustomGlideModule extends AppGlideModule { + @Override + public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) { + super.applyOptions(context, builder); + //内存缓存 + int memoryCacheSizeBytes = 1024 * 1024 * 32; // 20mb + builder.setMemoryCache(new LruResourceCache(memoryCacheSizeBytes)); + //Bitmap 池 + int bitmapPoolSizeBytes = 1024 * 1024 * 64; // 30mb + builder.setBitmapPool(new LruBitmapPool(bitmapPoolSizeBytes)); + //磁盘缓存 + int diskCacheSizeBytes = 1024 * 1024 * 128; // 100MB + builder.setDiskCache(new InternalCacheDiskCacheFactory(context, diskCacheSizeBytes)); + + } +} diff --git a/app/src/main/java/com/ttstd/dialer/annotations/ImageViewAdapter.java b/app/src/main/java/com/ttstd/dialer/annotations/ImageViewAdapter.java new file mode 100644 index 0000000..96bb788 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/annotations/ImageViewAdapter.java @@ -0,0 +1,53 @@ +package com.ttstd.dialer.annotations; + +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.widget.ImageView; + +import androidx.databinding.BindingAdapter; + +import com.bumptech.glide.Glide; +import com.ttstd.dialer.R; + +public class ImageViewAdapter { + @BindingAdapter("android:src") + public static void setSrc(ImageView view, Bitmap bitmap) { + view.setImageBitmap(bitmap); + } + + @BindingAdapter("android:src") + public static void setSrc(ImageView view, int resId) { + view.setImageResource(resId); + } + + @BindingAdapter("imageUrl") + public static void setSrc(ImageView imageView, String url) { + Glide.with(imageView.getContext()) + .load(url) + .error(R.mipmap.ic_launcher) + .centerCrop() + .into(imageView); + } + + /** + * 自定义设置图片属性 - 在匹配时自定义命名空间会被忽略 + */ + @BindingAdapter({"imageUrl", "error"}) + public static void loadImage(ImageView imageView, String url, Drawable error) { + Glide.with(imageView.getContext()) + .load(url) + .error(error) + .into(imageView); + } + + @BindingAdapter({"imageAvatarUrl", "error"}) + public static void loadAvatarImage(ImageView imageView, String url, Drawable error) { + Glide.with(imageView.getContext()) + .load(url) + .error(error) + .into(imageView); + } + + + +} \ No newline at end of file diff --git a/app/src/main/java/com/ttstd/dialer/base/BaseAlertDialogBuilder.java b/app/src/main/java/com/ttstd/dialer/base/BaseAlertDialogBuilder.java new file mode 100644 index 0000000..7885e64 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/base/BaseAlertDialogBuilder.java @@ -0,0 +1,50 @@ +package com.ttstd.dialer.base; + +import android.content.Context; +import android.content.ContextWrapper; +import android.content.res.Resources; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; + +import com.ttstd.dialer.utils.ScreenUtils; + +import me.jessyan.autosize.AutoSizeCompat; + +public class BaseAlertDialogBuilder extends AlertDialog.Builder { + + /** + * + */ + private static final float ALERT_BASE_WIDTH = 480f; + private static final float ALERT_BASE_WIDTH_TABLE = 640f; + + public BaseAlertDialogBuilder(@NonNull Context context) { + super(adjustAutoSize(context)); + } + + public BaseAlertDialogBuilder(@NonNull Context context, int themeResId) { + super(adjustAutoSize(context), themeResId); + } + + private static Context adjustAutoSize(Context context) { + return new ContextWrapper(context) { + private Resources mResources; + + { + Resources oldResources = super.getResources(); + mResources = new Resources(oldResources.getAssets(), oldResources.getDisplayMetrics(), oldResources.getConfiguration()); + } + + @Override + public Resources getResources() { + if (ScreenUtils.isTablet(context)) { + AutoSizeCompat.autoConvertDensityBaseOnWidth(mResources, ALERT_BASE_WIDTH_TABLE); + } else { + AutoSizeCompat.autoConvertDensityBaseOnWidth(mResources, ALERT_BASE_WIDTH); + } + return mResources; + } + }; + } +} diff --git a/app/src/main/java/com/ttstd/dialer/base/BaseApplication.java b/app/src/main/java/com/ttstd/dialer/base/BaseApplication.java new file mode 100644 index 0000000..97a2ff4 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/base/BaseApplication.java @@ -0,0 +1,100 @@ +package com.ttstd.dialer.base; + +import android.annotation.SuppressLint; +import android.app.Application; +import android.content.Context; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.text.TextUtils; +import android.util.Log; + +import com.alibaba.android.arouter.launcher.ARouter; +import com.arialyy.aria.core.Aria; +import com.hjq.toast.Toaster; +import com.tencent.bugly.crashreport.CrashReport; +import com.tencent.mmkv.MMKV; +import com.ttstd.dialer.BuildConfig; +import com.ttstd.dialer.config.CommonConfig; +import com.ttstd.dialer.utils.SystemUtils; + + +public class BaseApplication extends Application { + private static final String TAG = "BaseApplication"; + + /** + * ViewModel中因为经常旋转导致弱引用为空 + */ + @SuppressLint("StaticFieldLeak") + private static Context context; + + public static Context getContext() { + return context; + } + + @Override + public void onCreate() { + super.onCreate(); + Log.e(TAG, "onCreate: "); + context = getApplicationContext(); + if (!BuildConfig.DEBUG) { + catchException(); + } + // 在开始分析的地方调用,传入路径 + // 如果是放到外部路径,需要添加权限 + // 默认存储在/sdcard/Android/data/packagename/files +// Debug.startMethodTracing("App" + System.currentTimeMillis()); + init(); + } + + private void init() { + Log.e(TAG, "init: "); + if (SystemUtils.isMainProcessName(this, android.os.Process.myPid())) { + String rootDir = MMKV.initialize(this); + Log.e(TAG, "mmkv root: " + rootDir); + + if (BuildConfig.DEBUG) { // 这两行必须写在init之前,否则这些配置在init过程中将无效 + ARouter.openLog(); // 打印日志 + ARouter.openDebug(); // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险) + } + ARouter.init(this); // 尽可能早,推荐在Application中初始化 + + // 初始化 Toast 框架 + Toaster.init(this); + + Log.e(TAG, "slowInit: "); + Aria.init(this); + CrashReport.initCrashReport(getApplicationContext(), "845e3ed68c", false); + CrashReport.setDeviceId(this, Build.MODEL); + xcrash.XCrash.init(this); + } + } + + private void catchException() { + Thread.setDefaultUncaughtExceptionHandler( + new Thread.UncaughtExceptionHandler() { + @Override + public void uncaughtException(Thread t, Throwable e) { + Log.e("捕获异常子线程:", Thread.currentThread().getName() + + "在:" + e.getStackTrace()[0].getClassName()); + } + } + ); + //下面是新增方法! + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + while (true) { + try { + Looper.loop(); //会先执行这个方法,然后在执行下面的异常捕获方法! + } catch (Exception e) { + Log.e("捕获异常主线程:", Thread.currentThread().getName() + "在:" + e.getStackTrace()[0].getClassName()); + e.printStackTrace(); + } + } + } + }); + } + + +} diff --git a/app/src/main/java/com/ttstd/dialer/base/BaseDataBindingActivity.java b/app/src/main/java/com/ttstd/dialer/base/BaseDataBindingActivity.java new file mode 100644 index 0000000..f5b2c39 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/base/BaseDataBindingActivity.java @@ -0,0 +1,88 @@ +package com.ttstd.dialer.base; + +import android.os.Build; +import android.os.Bundle; +import android.view.View; + +import androidx.annotation.CallSuper; +import androidx.annotation.Nullable; +import androidx.core.graphics.Insets; +import androidx.core.view.OnApplyWindowInsetsListener; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; + +import com.ttstd.dialer.R; +import com.ttstd.dialer.base.rx.BaseRxActivity; +import com.zackratos.ultimatebarx.ultimatebarx.java.UltimateBarX; + +public abstract class BaseDataBindingActivity extends BaseRxActivity { + + public BaseDataBindingActivity() { + super(); + } + + @Override + @CallSuper + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); +// StatusBarUtil.init(this); + UltimateBarX.statusBar(this) + .transparent() + .colorRes(R.color.colorPrimaryDark) + .light(setNightMode()) + .fitWindow(setfitWindow()) + .apply(); + UltimateBarX.navigationBar(this) + .transparent() + .colorRes(R.color.colorPrimaryDark) + .light(setNightMode()) + .fitWindow(setfitWindow()) + .apply(); + initDataBinding(); + initView(); + initData(); + } + + /** + * @return 是否是黑色状态栏 + */ +// protected abstract boolean setNightMode(); + public boolean setNightMode() { + return false; + } + + /** + * @return 是否是入侵 + */ +// protected abstract boolean setNightMode(); + public boolean setfitWindow() { + return false; + } + + + protected abstract void initDataBinding(); + + /** + * 初始化视图 + */ + protected abstract void initView(); + + /** + * 初始化数据 + */ + protected abstract void initData(); + + public void addNavigationBarBottomPadding(View view) { + UltimateBarX.addNavigationBarBottomPadding(view); + if (Build.VERSION.SDK_INT >= 35) { + ViewCompat.setOnApplyWindowInsetsListener(view, new OnApplyWindowInsetsListener() { + @Override + public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) { + Insets systemInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars()); + v.setPadding(0, 0, 0, systemInsets.bottom); + return insets; + } + }); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ttstd/dialer/base/BaseDialogFragment.java b/app/src/main/java/com/ttstd/dialer/base/BaseDialogFragment.java new file mode 100644 index 0000000..0e0a7ff --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/base/BaseDialogFragment.java @@ -0,0 +1,44 @@ +package com.ttstd.dialer.base; + +import android.os.Bundle; + +import com.ttstd.dialer.base.rx.BaseRxDialogFragment; + +public abstract class BaseDialogFragment extends BaseRxDialogFragment { + + protected boolean isViewInitiated; + protected boolean isVisibleToUser; + protected boolean isDataInitiated; + + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + isViewInitiated = true; + prepareFetchData(); + } + + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + super.setUserVisibleHint(isVisibleToUser); + this.isVisibleToUser = isVisibleToUser; + prepareFetchData(); + } + + public abstract void fetchData(); + + public boolean prepareFetchData() { + return prepareFetchData(false); + } + + public boolean prepareFetchData(boolean forceUpdate) { + if (isVisibleToUser && isViewInitiated && (!isDataInitiated || forceUpdate)) { + fetchData(); + //注释掉保证每次都更新数据 +// isDataInitiated = true; + return true; + } + return false; + } + +} diff --git a/app/src/main/java/com/ttstd/dialer/base/BaseFragment.java b/app/src/main/java/com/ttstd/dialer/base/BaseFragment.java new file mode 100644 index 0000000..a7ed578 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/base/BaseFragment.java @@ -0,0 +1,44 @@ +package com.ttstd.dialer.base; + +import android.os.Bundle; + +import com.ttstd.dialer.base.rx.BaseRxFragment; + +public abstract class BaseFragment extends BaseRxFragment { + + protected boolean isViewInitiated; + protected boolean isVisibleToUser; + protected boolean isDataInitiated; + + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + isViewInitiated = true; + prepareFetchData(); + } + + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + super.setUserVisibleHint(isVisibleToUser); + this.isVisibleToUser = isVisibleToUser; + prepareFetchData(); + } + + public abstract void fetchData(); + + public boolean prepareFetchData() { + return prepareFetchData(false); + } + + public boolean prepareFetchData(boolean forceUpdate) { + if (isVisibleToUser && isViewInitiated && (!isDataInitiated || forceUpdate)) { + fetchData(); + //注释掉保证每次都更新数据 +// isDataInitiated = true; + return true; + } + return false; + } + +} diff --git a/app/src/main/java/com/ttstd/dialer/base/BaseTransparentActivity.java b/app/src/main/java/com/ttstd/dialer/base/BaseTransparentActivity.java new file mode 100644 index 0000000..e584ff5 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/base/BaseTransparentActivity.java @@ -0,0 +1,91 @@ +package com.ttstd.dialer.base; + +import android.os.Build; +import android.os.Bundle; +import android.view.View; + +import androidx.annotation.CallSuper; +import androidx.annotation.Nullable; +import androidx.core.graphics.Insets; +import androidx.core.view.OnApplyWindowInsetsListener; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; + +import com.ttstd.dialer.R; +import com.ttstd.dialer.base.rx.BaseRxActivity; +import com.zackratos.ultimatebarx.ultimatebarx.java.UltimateBarX; + +import me.jessyan.autosize.AutoSizeCompat; + +public abstract class BaseTransparentActivity extends BaseRxActivity { + + public BaseTransparentActivity() { + super(); + } + + @Override + @CallSuper + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); +// StatusBarUtil.init(this); + UltimateBarX.statusBar(this) + .transparent() + .colorRes(R.color.colorPrimaryDark) + .light(setNightMode()) + .fitWindow(setfitWindow()) + .apply(); + UltimateBarX.navigationBar(this) + .transparent() + .colorRes(R.color.colorPrimaryDark) + .light(setNightMode()) + .fitWindow(setfitWindow()) + .apply(); + } + + /** + * 修补autozie RecyclerView item大小不一致 + */ + @Override + protected void onResume() { + super.onResume(); + AutoSizeCompat.autoConvertDensityOfGlobal(getResources()); + } + + /** + * 设置布局 + */ + protected abstract int getLayoutId(); + + /** + * @return 是否是黑色状态栏 + */ +// protected abstract boolean setNightMode(); + public boolean setNightMode() { + return false; + } + + /** + * @return 是否是入侵 + */ +// protected abstract boolean setNightMode(); + public boolean setfitWindow() { + return false; + } + + /** + * @param view android 15 edge-to-edge会覆盖导航栏 + */ + public void addNavigationBarBottomPadding(View view) { + UltimateBarX.addNavigationBarBottomPadding(view); + if (Build.VERSION.SDK_INT >= 35) { + ViewCompat.setOnApplyWindowInsetsListener(view, new OnApplyWindowInsetsListener() { + @Override + public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) { + Insets systemInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars()); + v.setPadding(0, 0, 0, systemInsets.bottom); + return insets; + } + }); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ttstd/dialer/base/mvp/BaseMvpActivity.java b/app/src/main/java/com/ttstd/dialer/base/mvp/BaseMvpActivity.java new file mode 100644 index 0000000..3c6ddd5 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/base/mvp/BaseMvpActivity.java @@ -0,0 +1,35 @@ +package com.ttstd.dialer.base.mvp; + +import android.os.Bundle; + +import androidx.annotation.CallSuper; +import androidx.annotation.Nullable; + +import com.ttstd.dialer.base.BaseTransparentActivity; + +@Deprecated +public abstract class BaseMvpActivity extends BaseTransparentActivity { + + public BaseMvpActivity() { + super(); + } + + @Override + @CallSuper + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(getLayoutId()); + initView(); + initData(); + } + + /** + * 初始化视图 + */ + protected abstract void initView(); + + /** + * 初始化数据 + */ + protected abstract void initData(); +} diff --git a/app/src/main/java/com/ttstd/dialer/base/mvp/BasePresenter.java b/app/src/main/java/com/ttstd/dialer/base/mvp/BasePresenter.java new file mode 100644 index 0000000..acd0917 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/base/mvp/BasePresenter.java @@ -0,0 +1,8 @@ +package com.ttstd.dialer.base.mvp; + +@Deprecated +public interface BasePresenter { + void attachView(V view); + + void detachView(); +} \ No newline at end of file diff --git a/app/src/main/java/com/ttstd/dialer/base/mvp/BaseView.java b/app/src/main/java/com/ttstd/dialer/base/mvp/BaseView.java new file mode 100644 index 0000000..2c13af6 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/base/mvp/BaseView.java @@ -0,0 +1,6 @@ +package com.ttstd.dialer.base.mvp; + +@Deprecated +public interface BaseView { + +} \ No newline at end of file diff --git a/app/src/main/java/com/ttstd/dialer/base/mvvm/BaseMvvmActivity.java b/app/src/main/java/com/ttstd/dialer/base/mvvm/BaseMvvmActivity.java new file mode 100644 index 0000000..3a71a1a --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/base/mvvm/BaseMvvmActivity.java @@ -0,0 +1,54 @@ +package com.ttstd.dialer.base.mvvm; + +import android.os.Bundle; +import android.util.Log; + +import androidx.annotation.Nullable; +import androidx.databinding.DataBindingUtil; +import androidx.databinding.ViewDataBinding; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; + +import com.ttstd.dialer.base.BaseTransparentActivity; + +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; + +public abstract class BaseMvvmActivity extends BaseTransparentActivity { + + private static final String TAG = BaseMvvmActivity.class.getSimpleName(); + + protected VM mViewModel; + protected VDB mViewDataBinding; + protected Class vmClass; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + //ViewDataBinding + mViewDataBinding = DataBindingUtil.setContentView(this, getLayoutId()); + mViewDataBinding.setLifecycleOwner(this); + //ViewModel + vmClass = (Class) ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[0]; + boolean isAbstract = Modifier.isAbstract(vmClass.getModifiers()); + Log.e(TAG, "isLocalClass:" + vmClass.getSimpleName().equals(ViewModel.class.getSimpleName()) + " isAbstract:" + isAbstract); + if (!isAbstract) {//不是一个抽象类 + mViewModel = new ViewModelProvider(this).get(vmClass); + } + initDataBinding(); + initView(); + initData(); + } + + protected abstract void initDataBinding(); + + /** + * 初始化视图 + */ + protected abstract void initView(); + + /** + * 初始化数据 + */ + protected abstract void initData(); +} diff --git a/app/src/main/java/com/ttstd/dialer/base/mvvm/BaseViewModel.java b/app/src/main/java/com/ttstd/dialer/base/mvvm/BaseViewModel.java new file mode 100644 index 0000000..75102ae --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/base/mvvm/BaseViewModel.java @@ -0,0 +1,54 @@ +package com.ttstd.dialer.base.mvvm; + +import android.content.Context; + +import androidx.databinding.ViewDataBinding; +import androidx.lifecycle.ViewModel; + +import java.lang.ref.WeakReference; + +import io.reactivex.rxjava3.subjects.BehaviorSubject; + +public abstract class BaseViewModel extends ViewModel { + + /** + * 当前viewmodel对应的页面binding + */ + protected VDB binding; + + public void setVDBinding(ViewDataBinding vdBinding) { + binding = (VDB) vdBinding; + } + + public VDB getVDBinding() { + if (binding == null) { + throw new NullPointerException("BaseViewModel >> getVDBinding >> null!!!"); + } + return binding; + } + + private WeakReference weakContext; + + public void setContext(Context context) { + if (weakContext == null) { + weakContext = new WeakReference<>(context.getApplicationContext()); + } + } + + public Context getSafeContext() { + if (weakContext == null) { + throw new NullPointerException("BaseViewModel >> getCtx >> null!!!"); + } + return weakContext.get(); + } + + private BehaviorSubject mBehaviorSubject; + + public void setLifecycle(BehaviorSubject subject) { + this.mBehaviorSubject = (BehaviorSubject) subject; + } + + public BehaviorSubject getLifecycle() { + return mBehaviorSubject; + } +} diff --git a/app/src/main/java/com/ttstd/dialer/base/mvvm/fragment/BaseMvvmFragment.java b/app/src/main/java/com/ttstd/dialer/base/mvvm/fragment/BaseMvvmFragment.java new file mode 100644 index 0000000..40f2b1f --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/base/mvvm/fragment/BaseMvvmFragment.java @@ -0,0 +1,275 @@ +package com.ttstd.dialer.base.mvvm.fragment; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; + +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.databinding.DataBindingUtil; +import androidx.databinding.ViewDataBinding; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; + +import com.ttstd.dialer.base.BaseFragment; + +import java.lang.ref.WeakReference; +import java.lang.reflect.ParameterizedType; + +/** + * @author: lml + * @date: 2021/12/15 + */ +public abstract class BaseMvvmFragment extends BaseFragment { + protected String mTag = this.getClass().getSimpleName(); + /** + * 是否顯示了 + */ + protected boolean mIsVisible; + /** + * 是否準備好了-Created + */ + protected boolean mHasPrepare; + + + protected VM mViewModel; + protected VDB mViewDataBinding; + protected Class vmClass; + // +// protected Toolbar toolbar; +// protected View statusBarView; + // + protected Bundle bundle;//来自getArguments() + protected Bundle savedInstanceState; + +// protected Context context; + + /** + * 上下文 + */ + private WeakReference ctx; + + public Context getCtx() { + return ctx == null ? null : ctx.get(); + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); +// this.context = context; + ctx = new WeakReference<>(context); + } + + /** + * onCreate、onResume里不能调用 + * + * @return + */ + public boolean isAttached() { + boolean flag = getCtx() != null && isAdded(); + Log.e(" >> isAttached >>", "flag = " + flag); + return flag; + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + //ViewDataBinding + mViewDataBinding = DataBindingUtil.inflate(inflater, getLayoutId(), container, false); + mViewDataBinding.setLifecycleOwner(this); + + //ViewModel + vmClass = (Class) ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[0]; + mViewModel = new ViewModelProvider(this).get(vmClass); + // + return mViewDataBinding.getRoot(); + } + + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + +// if (initStatusBarToolBar()) { +// toolbar = getToolbar(); +// } + //注册eventbus +// if (getClass().isAnnotationPresent(BindEventBus.class)) +// EventBusManager.register(this); + // + +// fitsLayoutOverlap(); + initDataBinding(); + initView(bundle = getArguments()); + // + initData(this.savedInstanceState = savedInstanceState); + // + if (mIsVisible) { + onEnter(); + } + mHasPrepare = true; + // +// LiveDataBus.get().with(ConstantUtils.DATA_BUS_LOADING_FRAGMENT, Boolean.class).observe(getActivity(), bool -> { +// L.e(" >> LiveDataBus >> DATA_BUS_LOADING_FRAGMENT: %s", bool); +// if(bool) { +// showLoading(R.string.str_please_wait); +// } else { +// hideLoading(); +// } +// }); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + mHasPrepare = false; + mViewDataBinding = null; + //移除eventbus +// if (getClass().isAnnotationPresent(BindEventBus.class)) +// EventBusManager.unregister(this); + // + } + + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + super.setUserVisibleHint(isVisibleToUser); + if (mIsVisible == getUserVisibleHint()) + return; + mIsVisible = getUserVisibleHint(); + if (mIsVisible) { + if (!mHasPrepare) + return; + onEnter(); + } else { + onExit(); + } + } + + @LayoutRes + protected abstract int getLayoutId(); + +// protected abstract Toolbar getToolbar(); + +// protected View getStatusView() { +// return null; +// } + + protected abstract void initDataBinding(); + + protected abstract void initView(Bundle bundle); + + protected abstract void initData(Bundle savedInstanceState); + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); +// fitsLayoutOverlap(); + } + +// protected boolean isImmersionBarEnabled() { +// return true; +// } + +// protected boolean initStatusBarToolBar() { +// return true; +// } + + +// private void fitsLayoutOverlap() { +// if (!isImmersionBarEnabled()) return; +// if (statusBarView != null) { +// ImmersionBar.setStatusBarView(getActivity(), statusBarView); +// } +// if (toolbar != null) { +// ImmersionBar.setTitleBar(getActivity(), toolbar); +// } +// } + + protected void hideInputMethod(Activity activity) { + InputMethodManager imm = (InputMethodManager) activity.getSystemService(Activity.INPUT_METHOD_SERVICE); + View view = activity.getCurrentFocus(); + if (view != null) { + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + } + + protected void hideInputMethod(Activity activity, EditText editText) { + InputMethodManager imm = (InputMethodManager) editText.getContext().getSystemService(Activity.INPUT_METHOD_SERVICE); + View view = activity.getCurrentFocus(); + if (view != null) { + imm.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); + } + } + + protected void showInputMethod(EditText editText) { + InputMethodManager imm = (InputMethodManager) editText.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(editText, InputMethodManager.SHOW_FORCED); + } + + +// private CustomDialog mWaitDialog; +// +// public void showLoading(@StringRes int contentID) { +// showLoading(contentID, R.color.white); +// } +// +// public void showLoading(@StringRes int contentID, @ColorRes int color) { +// hideLoading(); +// DialogX.init(getActivity()); +// if (color == R.color.white) { +// mWaitDialog = DialogXUtil.getInstance().showLoading(getActivity(), getString(contentID), getResources().getColor(color)); +// } else { +// mWaitDialog = DialogXUtil.getInstance().showLoading_black(getActivity(), getString(contentID), getResources().getColor(color)); +// } +// } +// +// public void updateLoadingTip(@StringRes int messageID, int percent) { +// try { +// if (mWaitDialog != null && mWaitDialog.isShow()) { +// TextView tvTip = mWaitDialog.getCustomView().findViewById(R.id.tv_load_tip); +// if (tvTip != null) +// tvTip.setText(getResources().getString(messageID) + (percent == -1 ? "" : percent + "%")); +// } +// } catch (Exception e) { +// e.printStackTrace(); +// } +// } +// +// public boolean isShowLoading() { +// return mWaitDialog != null && mWaitDialog.isShow(); +// } +// +// public void hideLoading() { +// try { +// boolean isShow = isShowLoading(); +// L.d(" >> hideLoading :: isShow: %s", isShow); +// if (isShow) +// mWaitDialog.dismiss(); +// } catch (Exception e) { +// e.printStackTrace(); +// } +// } + + /** + * 進入界面 + */ + protected void onEnter() { + + } + + /** + * 離開界面 + */ + protected void onExit() { + + } + +} diff --git a/app/src/main/java/com/ttstd/dialer/base/rx/BaseRxActivity.java b/app/src/main/java/com/ttstd/dialer/base/rx/BaseRxActivity.java new file mode 100644 index 0000000..02d7501 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/base/rx/BaseRxActivity.java @@ -0,0 +1,94 @@ +package com.ttstd.dialer.base.rx; + +import android.os.Bundle; + +import androidx.annotation.CallSuper; +import androidx.annotation.CheckResult; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + +import com.trello.rxlifecycle4.LifecycleProvider; +import com.trello.rxlifecycle4.LifecycleTransformer; +import com.trello.rxlifecycle4.RxLifecycle; +import com.trello.rxlifecycle4.android.ActivityEvent; +import com.trello.rxlifecycle4.android.RxLifecycleAndroid; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.subjects.BehaviorSubject; + +/** + * {@link com.trello.rxlifecycle4.components.RxActivity} + * copied form RxActivity} + */ +public abstract class BaseRxActivity extends AppCompatActivity implements LifecycleProvider { + private final BehaviorSubject lifecycleSubject = BehaviorSubject.create(); + + public BehaviorSubject getLifecycleSubject() { + return lifecycleSubject; + } + + @Override + @NonNull + @CheckResult + public final Observable lifecycle() { + return lifecycleSubject.hide(); + } + + @Override + @NonNull + @CheckResult + public final LifecycleTransformer bindUntilEvent(@NonNull ActivityEvent event) { + return RxLifecycle.bindUntilEvent(lifecycleSubject, event); + } + + @Override + @NonNull + @CheckResult + public final LifecycleTransformer bindToLifecycle() { + return RxLifecycleAndroid.bindActivity(lifecycleSubject); + } + + @Override + @CallSuper + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + lifecycleSubject.onNext(ActivityEvent.CREATE); + } + + @Override + @CallSuper + protected void onStart() { + super.onStart(); + lifecycleSubject.onNext(ActivityEvent.START); + } + + @Override + @CallSuper + protected void onResume() { + super.onResume(); + lifecycleSubject.onNext(ActivityEvent.RESUME); + } + + @Override + @CallSuper + protected void onPause() { + lifecycleSubject.onNext(ActivityEvent.PAUSE); + super.onPause(); + } + + @Override + @CallSuper + protected void onStop() { + lifecycleSubject.onNext(ActivityEvent.STOP); + super.onStop(); + } + + @Override + @CallSuper + protected void onDestroy() { + lifecycleSubject.onNext(ActivityEvent.DESTROY); + super.onDestroy(); + } +} + diff --git a/app/src/main/java/com/ttstd/dialer/base/rx/BaseRxDialogFragment.java b/app/src/main/java/com/ttstd/dialer/base/rx/BaseRxDialogFragment.java new file mode 100644 index 0000000..20704f7 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/base/rx/BaseRxDialogFragment.java @@ -0,0 +1,123 @@ +package com.ttstd.dialer.base.rx; + +import android.os.Bundle; +import android.view.View; + +import androidx.annotation.CallSuper; +import androidx.annotation.CheckResult; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; + +import com.trello.rxlifecycle4.LifecycleProvider; +import com.trello.rxlifecycle4.LifecycleTransformer; +import com.trello.rxlifecycle4.RxLifecycle; +import com.trello.rxlifecycle4.android.FragmentEvent; +import com.trello.rxlifecycle4.android.RxLifecycleAndroid; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.subjects.BehaviorSubject; + +/** + * {@link com.trello.rxlifecycle4.components.RxFragment} + * copied form RxFragment} + */ +public class BaseRxDialogFragment extends DialogFragment implements LifecycleProvider { + private final BehaviorSubject lifecycleSubject = BehaviorSubject.create(); + + public BehaviorSubject getLifecycleSubject() { + return lifecycleSubject; + } + + @Override + @NonNull + @CheckResult + public final Observable lifecycle() { + return lifecycleSubject.hide(); + } + + @Override + @NonNull + @CheckResult + public final LifecycleTransformer bindUntilEvent(@NonNull FragmentEvent event) { + return RxLifecycle.bindUntilEvent(lifecycleSubject, event); + } + + @Override + @NonNull + @CheckResult + public final LifecycleTransformer bindToLifecycle() { + return RxLifecycleAndroid.bindFragment(lifecycleSubject); + } + + @Override + @CallSuper + public void onAttach(android.app.Activity activity) { + super.onAttach(activity); + lifecycleSubject.onNext(FragmentEvent.ATTACH); + } + + @Override + @CallSuper + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + lifecycleSubject.onNext(FragmentEvent.CREATE); + } + + @Override + @CallSuper + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + lifecycleSubject.onNext(FragmentEvent.CREATE_VIEW); + } + + @Override + @CallSuper + public void onStart() { + super.onStart(); + lifecycleSubject.onNext(FragmentEvent.START); + } + + @Override + @CallSuper + public void onResume() { + super.onResume(); + lifecycleSubject.onNext(FragmentEvent.RESUME); + } + + @Override + @CallSuper + public void onPause() { + lifecycleSubject.onNext(FragmentEvent.PAUSE); + super.onPause(); + } + + @Override + @CallSuper + public void onStop() { + lifecycleSubject.onNext(FragmentEvent.STOP); + super.onStop(); + } + + @Override + @CallSuper + public void onDestroyView() { + lifecycleSubject.onNext(FragmentEvent.DESTROY_VIEW); + super.onDestroyView(); + } + + @Override + @CallSuper + public void onDestroy() { + lifecycleSubject.onNext(FragmentEvent.DESTROY); + super.onDestroy(); + } + + @Override + @CallSuper + public void onDetach() { + lifecycleSubject.onNext(FragmentEvent.DETACH); + super.onDetach(); + } +} + diff --git a/app/src/main/java/com/ttstd/dialer/base/rx/BaseRxFragment.java b/app/src/main/java/com/ttstd/dialer/base/rx/BaseRxFragment.java new file mode 100644 index 0000000..26738d1 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/base/rx/BaseRxFragment.java @@ -0,0 +1,123 @@ +package com.ttstd.dialer.base.rx; + +import android.os.Bundle; +import android.view.View; + +import androidx.annotation.CallSuper; +import androidx.annotation.CheckResult; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.trello.rxlifecycle4.LifecycleProvider; +import com.trello.rxlifecycle4.LifecycleTransformer; +import com.trello.rxlifecycle4.RxLifecycle; +import com.trello.rxlifecycle4.android.FragmentEvent; +import com.trello.rxlifecycle4.android.RxLifecycleAndroid; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.subjects.BehaviorSubject; + +/** + * {@link com.trello.rxlifecycle4.components.RxFragment} + * copied form RxFragment} + */ +public class BaseRxFragment extends Fragment implements LifecycleProvider { + private final BehaviorSubject lifecycleSubject = BehaviorSubject.create(); + + public BehaviorSubject getLifecycleSubject() { + return lifecycleSubject; + } + + @Override + @NonNull + @CheckResult + public final Observable lifecycle() { + return lifecycleSubject.hide(); + } + + @Override + @NonNull + @CheckResult + public final LifecycleTransformer bindUntilEvent(@NonNull FragmentEvent event) { + return RxLifecycle.bindUntilEvent(lifecycleSubject, event); + } + + @Override + @NonNull + @CheckResult + public final LifecycleTransformer bindToLifecycle() { + return RxLifecycleAndroid.bindFragment(lifecycleSubject); + } + + @Override + @CallSuper + public void onAttach(android.app.Activity activity) { + super.onAttach(activity); + lifecycleSubject.onNext(FragmentEvent.ATTACH); + } + + @Override + @CallSuper + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + lifecycleSubject.onNext(FragmentEvent.CREATE); + } + + @Override + @CallSuper + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + lifecycleSubject.onNext(FragmentEvent.CREATE_VIEW); + } + + @Override + @CallSuper + public void onStart() { + super.onStart(); + lifecycleSubject.onNext(FragmentEvent.START); + } + + @Override + @CallSuper + public void onResume() { + super.onResume(); + lifecycleSubject.onNext(FragmentEvent.RESUME); + } + + @Override + @CallSuper + public void onPause() { + lifecycleSubject.onNext(FragmentEvent.PAUSE); + super.onPause(); + } + + @Override + @CallSuper + public void onStop() { + lifecycleSubject.onNext(FragmentEvent.STOP); + super.onStop(); + } + + @Override + @CallSuper + public void onDestroyView() { + lifecycleSubject.onNext(FragmentEvent.DESTROY_VIEW); + super.onDestroyView(); + } + + @Override + @CallSuper + public void onDestroy() { + lifecycleSubject.onNext(FragmentEvent.DESTROY); + super.onDestroy(); + } + + @Override + @CallSuper + public void onDetach() { + lifecycleSubject.onNext(FragmentEvent.DETACH); + super.onDetach(); + } +} + diff --git a/app/src/main/java/com/ttstd/dialer/base/rx/BaseRxService.java b/app/src/main/java/com/ttstd/dialer/base/rx/BaseRxService.java new file mode 100644 index 0000000..0b2975a --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/base/rx/BaseRxService.java @@ -0,0 +1,62 @@ +package com.ttstd.dialer.base.rx; + +import android.app.Service; +import android.content.Intent; + +import androidx.annotation.CheckResult; +import androidx.annotation.NonNull; + +import com.trello.rxlifecycle4.LifecycleProvider; +import com.trello.rxlifecycle4.LifecycleTransformer; +import com.trello.rxlifecycle4.RxLifecycle; +import com.trello.rxlifecycle4.android.ActivityEvent; +import com.trello.rxlifecycle4.android.RxLifecycleAndroid; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.subjects.BehaviorSubject; + +public abstract class BaseRxService extends Service implements LifecycleProvider { + private final BehaviorSubject lifecycleSubject = BehaviorSubject.create(); + + public BehaviorSubject getLifecycleSubject() { + return lifecycleSubject; + } + + @Override + @NonNull + @CheckResult + public final Observable lifecycle() { + return lifecycleSubject.hide(); + } + + @Override + @NonNull + @CheckResult + public final LifecycleTransformer bindUntilEvent(@NonNull ActivityEvent event) { + return RxLifecycle.bindUntilEvent(lifecycleSubject, event); + } + + @Override + @NonNull + @CheckResult + public final LifecycleTransformer bindToLifecycle() { + return RxLifecycleAndroid.bindActivity(lifecycleSubject); + } + + @Override + public void onCreate() { + super.onCreate(); + lifecycleSubject.onNext(ActivityEvent.CREATE); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + return super.onStartCommand(intent, flags, startId); + } + + @Override + public void onDestroy() { + super.onDestroy(); + lifecycleSubject.onNext(ActivityEvent.STOP); + } +} diff --git a/app/src/main/java/com/ttstd/dialer/config/CommonConfig.java b/app/src/main/java/com/ttstd/dialer/config/CommonConfig.java new file mode 100644 index 0000000..e5dff8f --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/config/CommonConfig.java @@ -0,0 +1,8 @@ +package com.ttstd.dialer.config; + +public class CommonConfig { + public static final String MMKV_ID = "InterProcessKV"; + + public static final String CONTACT_HOME_PAGE = "contact_home_page_key"; + +} diff --git a/app/src/main/java/com/ttstd/dialer/contact/AppDatabase.java b/app/src/main/java/com/ttstd/dialer/contact/AppDatabase.java new file mode 100644 index 0000000..6642137 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/contact/AppDatabase.java @@ -0,0 +1,28 @@ +package com.ttstd.dialer.contact; + +import androidx.room.Database; +import androidx.room.Room; +import androidx.room.RoomDatabase; +import android.content.Context; + +@Database(entities = {Contact.class}, version = 1, exportSchema = false) +public abstract class AppDatabase extends RoomDatabase { + public abstract ContactDao contactDao(); + + private static volatile AppDatabase INSTANCE; + + // 单例模式获取数据库实例 + public static AppDatabase getDatabase(final Context context) { + if (INSTANCE == null) { + synchronized (AppDatabase.class) { + if (INSTANCE == null) { + INSTANCE = Room.databaseBuilder(context.getApplicationContext(), + AppDatabase.class, "contact_database") + .allowMainThreadQueries() // 为了简化示例,允许主线程查询 + .build(); + } + } + } + return INSTANCE; + } +} diff --git a/app/src/main/java/com/ttstd/dialer/contact/Contact.java b/app/src/main/java/com/ttstd/dialer/contact/Contact.java new file mode 100644 index 0000000..73f2f25 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/contact/Contact.java @@ -0,0 +1,75 @@ +package com.ttstd.dialer.contact; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.room.Entity; +import androidx.room.PrimaryKey; + +import com.google.gson.Gson; +import com.google.gson.JsonParser; + +import java.io.Serializable; +import java.util.Objects; + +@Entity(tableName = "contacts") +public class Contact implements Serializable { + + @PrimaryKey(autoGenerate = true) + private int id; + private String name; + private String phoneNumber; + private String avatar; + + public Contact(String name, String phoneNumber) { + this.name = name; + this.phoneNumber = phoneNumber; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getPhoneNumber() { + return phoneNumber; + } + + public void setPhoneNumber(String phoneNumber) { + this.phoneNumber = phoneNumber; + } + + public String getAvatar() { + return avatar; + } + + public void setAvatar(String avatar) { + this.avatar = avatar; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj instanceof Contact) { + return Objects.equals(((Contact) obj).phoneNumber, phoneNumber) + || ((Contact) obj).id == id; + } + return false; + } + + @NonNull + @Override + public String toString() { + return JsonParser.parseString(new Gson().toJson(this)).getAsJsonObject().toString(); + } +} + diff --git a/app/src/main/java/com/ttstd/dialer/contact/ContactDao.java b/app/src/main/java/com/ttstd/dialer/contact/ContactDao.java new file mode 100644 index 0000000..4760e8e --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/contact/ContactDao.java @@ -0,0 +1,37 @@ +package com.ttstd.dialer.contact; + +import androidx.room.Dao; +import androidx.room.Delete; +import androidx.room.Insert; +import androidx.room.Query; +import androidx.room.Update; + +import java.util.List; + +@Dao +public interface ContactDao { + @Insert + void insert(Contact contact); + + @Update + void update(Contact contact); + + @Delete + void delete(Contact contact); + + @Query("DELETE FROM contacts WHERE id = :id") + void deleteById(int id); + + @Query("DELETE FROM contacts") + void deleteAll(); + + @Query("SELECT * FROM contacts ORDER BY name ASC") + List getAllContacts(); + + @Query("SELECT * FROM contacts WHERE id = :id") + Contact getContactById(int id); + + @Query("SELECT * FROM contacts WHERE name LIKE :searchQuery OR phoneNumber LIKE :searchQuery") + List searchContacts(String searchQuery); +} + diff --git a/app/src/main/java/com/ttstd/dialer/contact/ContactRepository.java b/app/src/main/java/com/ttstd/dialer/contact/ContactRepository.java new file mode 100644 index 0000000..a45e7c8 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/contact/ContactRepository.java @@ -0,0 +1,56 @@ +package com.ttstd.dialer.contact; + +import android.content.Context; +import java.util.List; + +public class ContactRepository { + private ContactDao mContactDao; + private List mAllContacts; + + // 构造函数,获取数据库访问对象 + public ContactRepository(Context context) { + AppDatabase db = AppDatabase.getDatabase(context); + mContactDao = db.contactDao(); + mAllContacts = mContactDao.getAllContacts(); + } + + // 获取所有联系人 + public List getAllContacts() { + return mContactDao.getAllContacts(); + } + + // 根据ID获取联系人 + public Contact getContactById(int id) { + return mContactDao.getContactById(id); + } + + // 搜索联系人 + public List searchContacts(String query) { + return mContactDao.searchContacts("%" + query + "%"); + } + + // 添加联系人 + public void insert(Contact contact) { + mContactDao.insert(contact); + } + + // 更新联系人 + public void update(Contact contact) { + mContactDao.update(contact); + } + + // 删除联系人 + public void delete(Contact contact) { + mContactDao.delete(contact); + } + + // 根据ID删除联系人 + public void deleteById(int id) { + mContactDao.deleteById(id); + } + + // 删除所有联系人 + public void deleteAll() { + mContactDao.deleteAll(); + } +} diff --git a/app/src/main/java/com/ttstd/dialer/contact/ContactViewModel.java b/app/src/main/java/com/ttstd/dialer/contact/ContactViewModel.java new file mode 100644 index 0000000..fd066e5 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/contact/ContactViewModel.java @@ -0,0 +1,46 @@ +package com.ttstd.dialer.contact; + +import android.app.Application; +import androidx.lifecycle.AndroidViewModel; +import java.util.List; + +public class ContactViewModel extends AndroidViewModel { + private ContactRepository mRepository; + + public ContactViewModel(Application application) { + super(application); + mRepository = new ContactRepository(application); + } + + public List getAllContacts() { + return mRepository.getAllContacts(); + } + + public Contact getContactById(int id) { + return mRepository.getContactById(id); + } + + public List searchContacts(String query) { + return mRepository.searchContacts(query); + } + + public void insert(Contact contact) { + mRepository.insert(contact); + } + + public void update(Contact contact) { + mRepository.update(contact); + } + + public void delete(Contact contact) { + mRepository.delete(contact); + } + + public void deleteById(int id) { + mRepository.deleteById(id); + } + + public void deleteAll() { + mRepository.deleteAll(); + } +} diff --git a/app/src/main/java/com/ttstd/dialer/fragment/contact/ContactFragment.java b/app/src/main/java/com/ttstd/dialer/fragment/contact/ContactFragment.java new file mode 100644 index 0000000..421f194 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/fragment/contact/ContactFragment.java @@ -0,0 +1,47 @@ +package com.ttstd.dialer.fragment.contact; + +import android.app.Activity; +import android.os.Bundle; + +import com.ttstd.dialer.R; +import com.ttstd.dialer.base.mvvm.fragment.BaseMvvmFragment; +import com.ttstd.dialer.databinding.FragmentContactBinding; + +public class ContactFragment extends BaseMvvmFragment { + private static final String TAG ="ContactFragment"; + + private Activity mContext; + + @Override + protected int getLayoutId() { + return R.layout.fragment_contact; + } + + @Override + protected void initDataBinding() { + mContext = getActivity(); + mViewModel.setContext(mContext); + mViewModel.setVDBinding(mViewDataBinding); + mViewModel.setLifecycle(getLifecycleSubject()); + mViewDataBinding.setClick(new BtnClick()); + } + + @Override + protected void initView(Bundle bundle) { + + } + + @Override + protected void initData(Bundle savedInstanceState) { + + } + + @Override + public void fetchData() { + + } + + public class BtnClick{ + + } +} diff --git a/app/src/main/java/com/ttstd/dialer/fragment/contact/ContactViewModel.java b/app/src/main/java/com/ttstd/dialer/fragment/contact/ContactViewModel.java new file mode 100644 index 0000000..0ee3790 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/fragment/contact/ContactViewModel.java @@ -0,0 +1,9 @@ +package com.ttstd.dialer.fragment.contact; + +import com.trello.rxlifecycle4.android.FragmentEvent; +import com.ttstd.dialer.base.mvvm.BaseViewModel; +import com.ttstd.dialer.databinding.FragmentContactBinding; + +public class ContactViewModel extends BaseViewModel { + +} diff --git a/app/src/main/java/com/ttstd/dialer/fragment/home/HomeFragment.java b/app/src/main/java/com/ttstd/dialer/fragment/home/HomeFragment.java new file mode 100644 index 0000000..c71ecdf --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/fragment/home/HomeFragment.java @@ -0,0 +1,242 @@ +package com.ttstd.dialer.fragment.home; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.Uri; +import android.os.Bundle; + +import androidx.fragment.app.Fragment; + +import android.provider.Settings; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.hjq.toast.Toaster; +import com.ttstd.dialer.R; +import com.ttstd.dialer.base.BaseFragment; +import com.ttstd.dialer.base.mvvm.fragment.BaseMvvmFragment; +import com.ttstd.dialer.databinding.FragmentHomeBinding; +import com.ttstd.dialer.utils.ApkUtils; +import com.ttstd.dialer.utils.DataUtil; +import com.ttstd.dialer.utils.LunarCalendarFestivalUtils; +import com.ttstd.dialer.utils.TimeUtils; + +/** + * A simple {@link Fragment} subclass. + * Use the {@link HomeFragment#newInstance} factory method to + * create an instance of this fragment. + */ +public class HomeFragment extends BaseMvvmFragment { + private static final String TAG = "HomeFragment"; + + private Activity mContext; + private LunarCalendarFestivalUtils mFestivalUtils; + + // TODO: Rename parameter arguments, choose names that match + // the fragment initialization parameters, e.g. ARG_ITEM_NUMBER + private static final String ARG_PARAM1 = "param1"; + private static final String ARG_PARAM2 = "param2"; + + // TODO: Rename and change types of parameters + private String mParam1; + private String mParam2; + + public HomeFragment() { + // Required empty public constructor + } + + /** + * Use this factory method to create a new instance of + * this fragment using the provided parameters. + * + * @param param1 Parameter 1. + * @param param2 Parameter 2. + * @return A new instance of fragment HomeFragment. + */ + // TODO: Rename and change types and number of parameters + public static HomeFragment newInstance(String param1, String param2) { + HomeFragment fragment = new HomeFragment(); + Bundle args = new Bundle(); + args.putString(ARG_PARAM1, param1); + args.putString(ARG_PARAM2, param2); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (getArguments() != null) { + mParam1 = getArguments().getString(ARG_PARAM1); + mParam2 = getArguments().getString(ARG_PARAM2); + } + } + + @Override + protected int getLayoutId() { + return R.layout.fragment_home; + } + + @Override + protected void initDataBinding() { + mContext = getActivity(); + mViewModel.setContext(mContext); + mViewModel.setVDBinding(mViewDataBinding); + mViewModel.setLifecycle(getLifecycleSubject()); + mViewDataBinding.setClick(new BtnClick()); + } + + @Override + protected void initView(Bundle bundle) { + mFestivalUtils = new LunarCalendarFestivalUtils(); + setTime(); + } + + @Override + protected void initData(Bundle savedInstanceState) { + + } + + @Override + public void fetchData() { + + } + + @Override + public void onStart() { + super.onStart(); + Log.e(TAG, "onStart: "); + registerReceivers(); + } + + @Override + public void onStop() { + super.onStop(); + Log.e(TAG, "onStop: "); + unregisterReceivers(); + } + + public void registerReceivers() { + registerTimeReceiver(); + } + + private void unregisterReceivers() { + if (null != mTimeReceiver) { + mContext.unregisterReceiver(mTimeReceiver); + } + + } + + + /** + * 时间 + */ + private void registerTimeReceiver() { + if (null == mTimeReceiver) { + mTimeReceiver = new TimeReceiver(); + } + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_DATE_CHANGED); + filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); + filter.addAction(Intent.ACTION_TIME_CHANGED); + filter.addAction(Intent.ACTION_TIME_TICK); + mContext.registerReceiver(mTimeReceiver, filter); + } + + private TimeReceiver mTimeReceiver; + + private class TimeReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + Log.e("TimeReceiver", "onReceive: " + action); + switch (action) { + case Intent.ACTION_DATE_CHANGED: + case Intent.ACTION_TIMEZONE_CHANGED: + case Intent.ACTION_TIME_CHANGED: + case Intent.ACTION_TIME_TICK: + setTime(); + break; + default: + } + } + } + + private void setTime() { + if (isAdded()) { + mViewDataBinding.tvTime.setText(DataUtil.formatDateHour()); +// mViewDataBinding.tvDate.setText(DataUtil.formatDateDay()); +// mViewDataBinding.tvWeek.setText(TimeUtils.getWeek()); +// mViewDataBinding.tvLunar.setText(mFestivalUtils.getLunarCalendar()); + } + } + + public void openAppStore(String pkg) { + Uri uri = Uri.parse("market://details?id=" + pkg); + Intent storeIntent = new Intent(Intent.ACTION_VIEW, uri); + storeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + try { + startActivity(storeIntent); + } catch (Exception e1) { + Log.e(TAG, "openWeixin storeIntent: " + e1.getMessage()); + } + } + + public class BtnClick { + public void openContact(View view) { + Intent intent = new Intent(); + try { + startActivity(intent); + } catch (Exception e) { + Log.e(TAG, "openSettings: " + e.getMessage()); + } + } + + public void openSettings(View view) { + Intent intent = new Intent(Settings.ACTION_SETTINGS); + try { + startActivity(intent); + } catch (Exception e) { + Log.e(TAG, "openSettings: " + e.getMessage()); + } + } + + public void openDouyin(View view) { + if (ApkUtils.isInstalled(mContext, "com.ss.android.ugc.aweme")) { + ApkUtils.openPackage(mContext, "com.ss.android.ugc.aweme"); + } else { + Toaster.show("抖音未安装,请安装后使用"); + } + } + + public void openWeixin(View view) { + if (ApkUtils.isInstalled(mContext, "com.tencent.mm")) { + Intent intent = new Intent(); + ComponentName cmp = new ComponentName("com.tencent.mm", "com.tencent.mm.ui.LauncherUI"); + intent.setAction(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + intent.setComponent(cmp); + try { + startActivity(intent); + } catch (Exception e) { + Log.e(TAG, "launchWeChat: " + e.getMessage()); + Toaster.show("打开微信失败"); + openAppStore("com.tencent.mm"); + } + } else { + Toaster.show("微信未安装,请安装后使用"); + openAppStore("com.tencent.mm"); + } + } + } + +} diff --git a/app/src/main/java/com/ttstd/dialer/fragment/home/HomeViewModel.java b/app/src/main/java/com/ttstd/dialer/fragment/home/HomeViewModel.java new file mode 100644 index 0000000..9615b25 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/fragment/home/HomeViewModel.java @@ -0,0 +1,9 @@ +package com.ttstd.dialer.fragment.home; + +import com.trello.rxlifecycle4.android.FragmentEvent; +import com.ttstd.dialer.base.mvvm.BaseViewModel; +import com.ttstd.dialer.databinding.FragmentHomeBinding; + +public class HomeViewModel extends BaseViewModel { + +} diff --git a/app/src/main/java/com/ttstd/dialer/utils/ApkUtils.java b/app/src/main/java/com/ttstd/dialer/utils/ApkUtils.java new file mode 100644 index 0000000..1ac71fd --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/utils/ApkUtils.java @@ -0,0 +1,86 @@ +package com.ttstd.dialer.utils; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.text.TextUtils; +import android.util.Log; + +import com.hjq.toast.Toaster; + +import java.util.List; + +public class ApkUtils { + + private static final String TAG = "ApkUtils"; + + public static boolean isInstalled(Context context, String packageName) { + if (TextUtils.isEmpty(packageName)) return false; + PackageManager packageManager = context.getPackageManager(); + try { + PackageInfo packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES); + return packageInfo != null; + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + return false; + } + + public static boolean openPackage(Context context, String packageName) { + Context pkgContext = getPackageContext(context, packageName); + Intent intent = getAppOpenIntentByPackageName(context, packageName); + if (pkgContext != null && intent != null) { + pkgContext.startActivity(intent); + return true; + } + Log.e(TAG, "openPackage: can not open " + packageName); + Toaster.show("打开失败"); + return false; + } + + public static Context getPackageContext(Context context, String packageName) { + Context pkgContext = null; + if (context.getPackageName().equals(packageName)) { + pkgContext = context; + } else { + // 创建第三方应用的上下文环境 + try { + pkgContext = context.createPackageContext(packageName, + Context.CONTEXT_IGNORE_SECURITY + | Context.CONTEXT_INCLUDE_CODE); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + } + return pkgContext; + } + + + public static Intent getAppOpenIntentByPackageName(Context context, String packageName) { + //Activity完整名 + String mainAct = null; + //根据包名寻找 + PackageManager pkgMag = context.getPackageManager(); + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + intent.setFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED | Intent.FLAG_ACTIVITY_NEW_TASK); + + List list = pkgMag.queryIntentActivities(intent, + PackageManager.GET_ACTIVITIES); + for (int i = 0; i < list.size(); i++) { + ResolveInfo info = list.get(i); + if (info.activityInfo.packageName.equals(packageName)) { + mainAct = info.activityInfo.name; + break; + } + } + if (TextUtils.isEmpty(mainAct)) { + return null; + } + intent.setComponent(new ComponentName(packageName, mainAct)); + return intent; + } +} diff --git a/app/src/main/java/com/ttstd/dialer/utils/DataUtil.java b/app/src/main/java/com/ttstd/dialer/utils/DataUtil.java new file mode 100644 index 0000000..6a698dc --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/utils/DataUtil.java @@ -0,0 +1,29 @@ +package com.ttstd.dialer.utils; + +import java.text.SimpleDateFormat; +import java.util.Date; + +public class DataUtil { + private static SimpleDateFormat day = new SimpleDateFormat("M月d日"); + private static SimpleDateFormat hour = new SimpleDateFormat("HH:mm"); + private static SimpleDateFormat minute = new SimpleDateFormat("mm"); + + /** + * 格式化日期(精确到天) + */ + public static String formatDateDay() { + return day.format(new Date()); + } + + public static String formatDateTime() { + return day.format(new Date()); + } + + /** + * 格式化日期(hour) + */ + public static String formatDateHour() { + return hour.format(new Date()); + } + +} diff --git a/app/src/main/java/com/ttstd/dialer/utils/LunarCalendarFestivalUtils.java b/app/src/main/java/com/ttstd/dialer/utils/LunarCalendarFestivalUtils.java new file mode 100644 index 0000000..6f79244 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/utils/LunarCalendarFestivalUtils.java @@ -0,0 +1,600 @@ +package com.ttstd.dialer.utils; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +/** + * 获取输入公历日期的生肖、天干地支、农历年、农历月、农历日、公历节日、农历节日、24节气等数据 + * DATE 2020.08.13 + * https://www.cnblogs.com/weihbs/p/13955786.html + */ +public class LunarCalendarFestivalUtils { + //生肖年 + private String animal; + //干支年 + private String ganZhiYear; + //阴历年 + private String lunarYear; + //阴历月 + private String lunarMonth; + //阴历日 + private String lunarDay; + //阳历节日 + private String solarFestival; + //阴历节日 + private String lunarFestival; + //节气 + private String lunarTerm; + + public LunarCalendarFestivalUtils() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + Date date = new Date(System.currentTimeMillis()); + String timeStamp = sdf.format(date); + initLunarCalendarInfo(timeStamp); + } + + public String getLunarCalendar() { + return lunarMonth + "月" + lunarDay; + } + + /** + * 获取查询日期的年份生肖 + * + * @return + */ + public String getAnimal() { + return animal; + } + + /** + * 获取查询日期年份的天干地支 + * + * @return + */ + public String getGanZhiYear() { + return ganZhiYear; + } + + /** + * 获取查询日期的农历年份 + * + * @return + */ + public String getLunarYear() { + return lunarYear; + } + + /** + * 获取查询日期的农历月份 + * + * @return + */ + public String getLunarMonth() { + return lunarMonth; + } + + /** + * 获取查询日期的农历日 + * + * @return + */ + public String getLunarDay() { + return lunarDay; + } + + /** + * 获取查询日期的公历节日(不是节日返回空) + * + * @return + */ + public String getSolarFestival() { + return solarFestival; + } + + /** + * 获取查询日期的农历节日(不是节日返回空) + * + * @return + */ + public String getLunarFestival() { + return lunarFestival; + } + + /** + * 获取查询日期的节气数据(不是节气返回空) + * + * @return + */ + public String getLunarTerm() { + return lunarTerm; + } + + + final static long[] lunarInfo = new long[]{ + 0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2, + 0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2, 0x095b0, 0x14977, + 0x04970, 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970, + 0x06566, 0x0d4a0, 0x0ea50, 0x06e95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950, + 0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2, 0x0a950, 0x0b557, + 0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5d0, 0x14573, 0x052d0, 0x0a9a8, 0x0e950, 0x06aa0, + 0x0aea6, 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0, + 0x096d0, 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b5a0, 0x195a6, + 0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46, 0x0ab60, 0x09570, + 0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58, 0x055c0, 0x0ab60, 0x096d5, 0x092e0, + 0x0c960, 0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5, + 0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930, + 0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530, + 0x05aa0, 0x076a3, 0x096d0, 0x04bd7, 0x04ad0, 0x0a4d0, 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45, + 0x0b5a0, 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0 + }; + //阳历天数 + final static int[] solarMonths = new int[]{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + //生肖 + final static String[] animals = new String[]{"鼠", "牛", "虎", "兔", "龙", "蛇", "马", "羊", "猴", "鸡", "狗", "猪"}; + //天干 + final static String[] tGan = new String[]{"甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"}; + //地支 + final static String[] dZhi = new String[]{"子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"}; + //二十四节气 + final static String[] solarTerms = new String[]{"小寒", "大寒", "立春", "雨水", "惊蛰", "春分", "清明", "谷雨", "立夏", + "小满", "芒种", "夏至", "小暑", "大暑", "立秋", "处暑", "白露", "秋分", "寒露", "霜降", "立冬", "小雪", "大雪", "冬至"}; + //二十四节气日期偏移度 + private static final double D = 0.2422; + //特殊年份节气日期偏移 + private final static Map INCREASE_OFFSETMAP = new HashMap();//+1偏移 + private final static Map DECREASE_OFFSETMAP = new HashMap();//-1偏移 + + static { + INCREASE_OFFSETMAP.put(0, new Integer[]{1982});//小寒 + DECREASE_OFFSETMAP.put(0, new Integer[]{2019});//小寒 + INCREASE_OFFSETMAP.put(1, new Integer[]{2082});//大寒 + DECREASE_OFFSETMAP.put(3, new Integer[]{2026});//雨水 + INCREASE_OFFSETMAP.put(5, new Integer[]{2084});//春分 + INCREASE_OFFSETMAP.put(9, new Integer[]{2008});//小满 + INCREASE_OFFSETMAP.put(10, new Integer[]{1902});//芒种 + INCREASE_OFFSETMAP.put(11, new Integer[]{1928});//夏至 + INCREASE_OFFSETMAP.put(12, new Integer[]{1925, 2016});//小暑 + INCREASE_OFFSETMAP.put(13, new Integer[]{1922});//大暑 + INCREASE_OFFSETMAP.put(14, new Integer[]{2002});//立秋 + INCREASE_OFFSETMAP.put(16, new Integer[]{1927});//白露 + INCREASE_OFFSETMAP.put(17, new Integer[]{1942});//秋分 + INCREASE_OFFSETMAP.put(19, new Integer[]{2089});//霜降 + INCREASE_OFFSETMAP.put(20, new Integer[]{2089});//立冬 + INCREASE_OFFSETMAP.put(21, new Integer[]{1978});//小雪 + INCREASE_OFFSETMAP.put(22, new Integer[]{1954});//大雪 + DECREASE_OFFSETMAP.put(23, new Integer[]{1918, 2021});//冬至 + } + + //定义一个二维数组,第一维数组存储的是20世纪的节气C值,第二维数组存储的是21世纪的节气C值,0到23个,依次代表立春、雨水...大寒节气的C值 + private static final double[][] CENTURY_ARRAY = { + {6.11, 20.84, 4.6295, 19.4599, 6.3826, 21.4155, 5.59, 20.888, 6.318, 21.86, 6.5, 22.2, 7.928, 23.65, 8.35, 23.95, 8.44, 23.822, 9.098, 24.218, 8.218, 23.08, 7.9, 22.6}, + {5.4055, 20.12, 3.87, 18.73, 5.63, 20.646, 4.81, 20.1, 5.52, 21.04, 5.678, 21.37, 7.108, 22.83, 7.5, 23.13, 7.646, 23.042, 8.318, 23.438, 7.438, 22.36, 7.18, 21.94} + }; + //农历月份 + final static String lunarNumber[] = {"一", "二", "三", "四", "五", "六", "七", "八", "九", "十", "十一", "十二"}; + //农历年 + final static String[] lunarYears = new String[]{"零", "一", "二", "三", "四", "五", "六", "七", "八", "九"}; + final static String[] chineseTen = new String[]{"初", "十", "廿", "卅"}; + //农历节日 + final static String[] lunarHoliday = new String[]{"0101 春节", "0115 元宵节", "0202 龙头节", "0505 端午节", "0707 七夕节", "0715 中元节", + "0815 中秋节", "0909 重阳节", "1001 寒衣节", "1015 下元节", "1208 腊八节", "1223 小年"}; + //公立节日 + final static String[] solarHoliday = new String[]{"0101 元旦", "0214 情人节", "0308 妇女节", "0312 植树节", "0315 消费者权益日", + "0401 愚人节", "0422 地球日", "0423 读书日", "0501 劳动节", "0504 青年节", "0512 护士节", "0518 博物馆日", "0519 旅游日", "0601 儿童节", + "0701 建党节", "0801 建军节", "0910 教师节", "1001 国庆节", "1024 联合国日", "1204 宪法日", "1224 平安夜", "1225 圣诞节"}; + //格式化日期 + static SimpleDateFormat chineseDateFormat = new SimpleDateFormat("yyyy年MM月dd日", Locale.CHINA); + static SimpleDateFormat solarDateFormat = new SimpleDateFormat("yyyy-MM-dd"); + + /** + * 返回农历y年的总天数 + * + * @param y + * @return + */ + private int lunarYearDays(int y) { + int i, sum = 348; + for (i = 0x8000; i > 0x8; i >>= 1) { + sum += ((lunarInfo[y - 1900] & i) != 0 ? 1 : 0); + } + return (sum + leapDays(y)); + } + + /** + * 返回农历y年闰月的天数 + */ + private int leapDays(int y) { + if (leapMonth(y) != 0) { + return ((lunarInfo[y - 1900] & 0x10000) != 0 ? 30 : 29); + } else + return 0; + } + + /** + * 判断y年的农历中那个月是闰月,不是闰月返回0 + * + * @param y + * @return + */ + private int leapMonth(int y) { + return (int) (lunarInfo[y - 1900] & 0xf); + } + + /** + * 返回农历y年m月的总天数 + * + * @param y + * @param m + * @return + */ + private int monthDays(int y, int m) { + return ((lunarInfo[y - 1900] & (0x10000 >> m)) != 0 ? 30 : 29); + } + + /** + * 获取阴历年 + * + * @param year + * @return + */ + private String getLunarYearString(String year) { + int y1 = Integer.parseInt(year.charAt(0) + ""); + int y2 = Integer.parseInt(year.charAt(1) + ""); + int y3 = Integer.parseInt(year.charAt(2) + ""); + int y4 = Integer.parseInt(year.charAt(3) + ""); + return lunarYears[y1] + lunarYears[y2] + lunarYears[y3] + lunarYears[y4]; + } + + /** + * 获取阴历日 + */ + private String getLunarDayString(int day) { + int n = day % 10 == 0 ? 9 : day % 10 - 1; + if (day > 30) + return ""; + if (day == 10) + return "初十"; + else + return chineseTen[day / 10] + lunarNumber[n]; + } + + /** + * 特例,特殊的年分的节气偏移量,由于公式并不完善,所以算出的个别节气的第几天数并不准确,在此返回其偏移量 + * + * @param year 年份 + * @param n 节气编号 + * @return 返回其偏移量 + */ + private int specialYearOffset(int year, int n) { + int offset = 0; + offset += getOffset(DECREASE_OFFSETMAP, year, n, -1); + offset += getOffset(INCREASE_OFFSETMAP, year, n, 1); + return offset; + } + + /** + * 节气偏移量计算 + * + * @param map + * @param year + * @param n + * @param offset + * @return + */ + private int getOffset(Map map, int year, int n, int offset) { + int off = 0; + Integer[] years = map.get(n); + if (null != years) { + for (int i : years) { + if (i == year) { + off = offset; + break; + } + } + } + return off; + } + + /** + * 获取某年的第n个节气为几日(从0小寒起算) + * + * @param year + * @param n + * @return + */ + private int sTerm(int year, int n) { + double centuryValue = 0;//节气的世纪值,每个节气的每个世纪值都不同 + int centuryIndex = -1; + if (year >= 1901 && year <= 2000) {//20世纪 + centuryIndex = 0; + } else if (year >= 2001 && year <= 2100) {//21世纪 + centuryIndex = 1; + } else { + throw new RuntimeException("不支持此年份:" + year + ",目前只支持1901年到2100年的时间范围"); + } + centuryValue = CENTURY_ARRAY[centuryIndex][n]; + int dateNum = 0; + + int y = year % 100;//步骤1:取年分的后两位数 + if (year % 4 == 0 && year % 100 != 0 || year % 400 == 0) {//闰年 + if (n == 0 || n == 1 || n == 2 || n == 3) { + //注意:凡闰年3月1日前闰年数要减一,即:L=[(Y-1)/4],因为小寒、大寒、立春、雨水这两个节气都小于3月1日,所以 y = y-1 + y = y - 1;//步骤2 + } + } + dateNum = (int) (y * D + centuryValue) - (int) (y / 4);//步骤3,使用公式[Y*D+C]-L计算 + dateNum += specialYearOffset(year, n);//步骤4,加上特殊的年分的节气偏移量 + return dateNum; + } + + /** + * 母亲节和父亲节 + * + * @param year + * @param month + * @param day + * @return + */ + private String getMotherOrFatherDay(int year, int month, int day) { + if (month != 5 && month != 6) return null; + if ((month == 5 && (day < 8 || day > 14)) || (month == 6 && (day < 15 || day > 21))) + return null; + Calendar calendar = Calendar.getInstance(); + calendar.set(year, month - 1, 1); + int weekDate = calendar.get(Calendar.DAY_OF_WEEK); + weekDate = (weekDate == 1) ? 7 : weekDate - 1; + switch (month) { + case 5: + if (day == 15 - weekDate) { + return "母亲节"; + } + break; + case 6: + if (day == 22 - weekDate) { + return "父亲节"; + } + break; + } + return null; + } + + /** + * 感恩节 + * + * @param year + * @param month + * @param day + * @return + */ + private String thanksgiving(int year, int month, int day) { + if (month != 11) return null; + if ((month == 11 && (day < 19 || day > 28))) return null; + Calendar calendar = Calendar.getInstance(); + calendar.set(year, month - 1, 1); + int weekDate = calendar.get(Calendar.DAY_OF_WEEK); + weekDate = (weekDate == 1) ? 7 : weekDate - 1; + switch (month) { + case 11: + if (day == 29 - weekDate + 4) { + return "感恩节"; + } + break; + } + return null; + } + + /** + * 获取复活节 + * + * @param year + * @param month + * @param day + * @return + */ + private String getEasterDay(int year, int month, int day) { + int n = year - 1900; + int a = n % 19; + int q = n / 4; + int b = (7 * a + 1) / 19; + int m = (11 * a + 4 - b) % 29; + int w = (n + q + 31 - m) % 7; + int answer = 25 - m - w; + String easterDay = ""; + if (answer > 0) { + easterDay = year + "-" + 4 + "-" + answer; + } else { + easterDay = year + "-" + 3 + "-" + (31 + answer); + } + String searchDay = year + "-" + month + "-" + day; + if (searchDay.equals(easterDay)) { + return "复活节"; + } + return null; + } + + /** + * 输入公历日期初始化当前日期的生肖、天干地支、农历年、农历月、农历日、公历节日、农历节日、24节气 + * 输入日期的格式为(YYYY-MM-DD) + * + * @param currentDate + */ + public void initLunarCalendarInfo(String currentDate) { + String[] splitDate = currentDate.split("-"); + //设置生肖 + int year = Integer.parseInt(splitDate[0]); + this.animal = animals[(year - 4) % 12]; + //设置天干地支 + int num = year - 1900 + 36; + this.ganZhiYear = (tGan[num % 10] + dZhi[num % 12]); + ///////////设置阴历///////////////////////////////////////////////////////// + //基准日期 + Date baseDate = null; + //当前日期 + Date nowaday = null; + try { + baseDate = chineseDateFormat.parse("1900年1月31日"); + nowaday = solarDateFormat.parse(currentDate); + } catch (ParseException e) { + e.printStackTrace(); + } + // 获取当前日期与1900年1月31日相差的天数 + int offset = (int) ((nowaday.getTime() - baseDate.getTime()) / 86400000L); + + //用offset减去每农历年的天数,计算当天是农历第几天 iYear最终结果是农历的年份 + int iYear, daysOfYear = 0; + for (iYear = 1900; iYear < 10000 && offset > 0; iYear++) { + daysOfYear = lunarYearDays(iYear); + offset -= daysOfYear; + } + if (offset < 0) { + offset += daysOfYear; + iYear--; + } + this.lunarYear = getLunarYearString(iYear + ""); + int leapMonth = leapMonth(iYear); // 闰哪个月,1-12 + boolean leap = false; + + // 用当年的天数offset,逐个减去每月(农历)的天数,求出当天是本月的第几天 + int iMonth, daysOfMonth = 0; + for (iMonth = 1; iMonth < 13 && offset > 0; iMonth++) { + // 闰月 + if (leapMonth > 0 && iMonth == (leapMonth + 1) && !leap) { + --iMonth; + leap = true; + daysOfMonth = leapDays(iYear); + } else + daysOfMonth = monthDays(iYear, iMonth); + + offset -= daysOfMonth; + // 解除闰月 + if (leap && iMonth == (leapMonth + 1)) + leap = false; + } + // offset为0时,并且刚才计算的月份是闰月,要校正 + if (offset == 0 && leapMonth > 0 && iMonth == leapMonth + 1) { + if (leap) { + leap = false; + } else { + leap = true; + --iMonth; + } + } + // offset小于0时,也要校正 + if (offset < 0) { + offset += daysOfMonth; + --iMonth; + } + // 设置对应的阴历月份 + this.lunarMonth = lunarNumber[iMonth - 1]; + if ("一".equals(this.lunarMonth)) { + this.lunarMonth = "正"; + } + if ("十二".equals(this.lunarMonth)) { + this.lunarMonth = "腊"; + } + if (leap) { + this.lunarMonth = "闰" + this.lunarMonth; + } + + //设置阴历日 + int iDay = offset + 1; + this.lunarDay = getLunarDayString(iDay); + + //设置节气 + int month = Integer.parseInt(splitDate[1]); + int day = Integer.parseInt(splitDate[2]); + if (day == sTerm(year, (month - 1) * 2)) { + this.lunarTerm = solarTerms[(month - 1) * 2]; + } else if (day == sTerm(year, (month - 1) * 2 + 1)) { + this.lunarTerm = solarTerms[(month - 1) * 2 + 1]; + } else { + this.lunarTerm = ""; + } + + //设置阳历节日 + String solarFestival = ""; + for (int i = 0; i < solarHoliday.length; i++) { + // 返回公历节假日名称 + String sd = solarHoliday[i].split(" ")[0]; // 节假日的日期 + String sdv = solarHoliday[i].split(" ")[1]; // 节假日的名称 + String smonth_v = splitDate[1]; + String sday_v = splitDate[2]; + String smd = smonth_v + sday_v; + if (sd.trim().equals(smd.trim())) { + solarFestival = sdv; + break; + } + } + //判断节日是否是父亲节或母亲节 + String motherOrFatherDay = getMotherOrFatherDay(year, month, day); + if (motherOrFatherDay != null) { + solarFestival = motherOrFatherDay; + } + //判断节日是否是复活节 + String easterDay = getEasterDay(year, month, day); + if (easterDay != null) { + solarFestival = easterDay; + } + //判断节日是否是感恩节 + String thanksgiving = thanksgiving(year, month, day); + if (thanksgiving != null) { + solarFestival = thanksgiving; + } + this.solarFestival = solarFestival; + + //设置阴历节日 + String lunarFestival = ""; + for (int i = 0; i < lunarHoliday.length; i++) { + //阴历闰月节日 + if (leap) { + break; + } + // 返回农历节假日名称 + String ld = lunarHoliday[i].split(" ")[0]; // 节假日的日期 + String ldv = lunarHoliday[i].split(" ")[1]; // 节假日的名称 + String lmonth_v = iMonth + ""; + String lday_v = iDay + ""; + String lmd = ""; + if (iMonth < 10) { + lmonth_v = "0" + iMonth; + } + if (iDay < 10) { + lday_v = "0" + iDay; + } + lmd = lmonth_v + lday_v; + if ("12".equals(lmonth_v)) { // 除夕夜需要特殊处理 + if ((daysOfMonth == 29 && iDay == 29) || (daysOfMonth == 30 && iDay == 30)) { + lunarFestival = "除夕"; + break; + } + } + if (ld.trim().equals(lmd.trim())) { + lunarFestival = ldv; + break; + } + } + if ("清明".equals(this.lunarTerm)) { + lunarFestival = "清明节"; + } + this.lunarFestival = lunarFestival; + } + +// /** +// * 测试方法 +// * @param args +// */ +// public static void main(String[] args) { +// LunarCalendarFestivalUtils festival = new LunarCalendarFestivalUtils(); +// festival.initLunarCalendarInfo("2021-06-25"); +// System.out.println("农历"+festival.getLunarYear()+"年"+festival.getLunarMonth()+"月"+festival.getLunarDay()+"日"); +// System.out.println(festival.getGanZhiYear()+"【"+festival.getAnimal()+"】年"); +// System.out.println(festival.getLunarTerm()); +// System.out.println(festival.getSolarFestival()); +// System.out.println(festival.getLunarFestival()); +// } +} diff --git a/app/src/main/java/com/ttstd/dialer/utils/ScreenUtils.java b/app/src/main/java/com/ttstd/dialer/utils/ScreenUtils.java new file mode 100644 index 0000000..dba8a28 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/utils/ScreenUtils.java @@ -0,0 +1,77 @@ +package com.ttstd.dialer.utils; + +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.util.Log; + +import java.lang.reflect.Method; + +public class ScreenUtils { + private static final String TAG = "ScreenUtils"; + + /** + * 根据手机的分辨率从 dp 的单位 转成为 px(像素) + */ + public static int dip2px(Context context, float dpValue) { + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (dpValue * scale + 0.5f); + } + + /** + * 根据手机的分辨率从 px(像素) 的单位 转成为 dp + */ + public static int px2dip(Context context, float pxValue) { + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (pxValue / scale + 0.5f); + } + + public static int dp2px(Resources resources, float dp) { + final float scale = resources.getDisplayMetrics().density; + return (int) (dp * scale + 0.5f); + } + + public static int sp2px(Resources resources, float sp) { + final float scale = resources.getDisplayMetrics().scaledDensity; + return (int) (sp * scale); + } + + /** + * 应用需反射调用 + */ + public static boolean isTablet() { + try { + // 1. 反射获取 SystemProperties 类 + Class systemPropertiesClass = Class.forName("android.os.SystemProperties"); + // 2. 获取 get(String key) 方法 + Method getMethod = systemPropertiesClass.getDeclaredMethod("get", String.class); + // 3. 调用方法获取属性值 + String characteristics = (String) getMethod.invoke(null, "ro.build.characteristics"); + Log.e(TAG, "isTablet: " + characteristics); + // 4. 判断是否包含 "tablet" 标识 + return characteristics != null && characteristics.contains("tablet"); + } catch (Exception e) { + // 反射失败时的处理(如属性不存在或权限问题) + Log.e(TAG, "Reflection failed: " + e.getMessage()); + return false; + } + } + + public static boolean isTablet(Context context) { + boolean isTablet = (context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_LARGE; + Log.e(TAG, "isTablet: " + isTablet); + return isTablet; + } + + + /** + * 是否是平板 + * + * @param context 上下文 + * @return 是平板则返回true,反之返回false + */ + public static boolean isPad(Context context) { + return (context.getResources().getConfiguration().screenLayout + & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_LARGE; + } +} diff --git a/app/src/main/java/com/ttstd/dialer/utils/SystemUtils.java b/app/src/main/java/com/ttstd/dialer/utils/SystemUtils.java new file mode 100644 index 0000000..a4f0b39 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/utils/SystemUtils.java @@ -0,0 +1,24 @@ +package com.ttstd.dialer.utils; + +import android.app.ActivityManager; +import android.content.Context; + +import java.util.List; + +public class SystemUtils { + + public static boolean isMainProcessName(Context cxt, int pid) { + String packageName = cxt.getPackageName(); + ActivityManager am = (ActivityManager) cxt.getSystemService(Context.ACTIVITY_SERVICE); + List runningApps = am.getRunningAppProcesses(); + if (runningApps == null) { + return false; + } + for (ActivityManager.RunningAppProcessInfo procInfo : runningApps) { + if (procInfo.pid == pid) { + return procInfo.processName.equals(packageName); + } + } + return false; + } +} diff --git a/app/src/main/java/com/ttstd/dialer/utils/TimeUtils.java b/app/src/main/java/com/ttstd/dialer/utils/TimeUtils.java new file mode 100644 index 0000000..521583f --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/utils/TimeUtils.java @@ -0,0 +1,19 @@ +package com.ttstd.dialer.utils; + +import java.util.Calendar; +import java.util.Date; + +public class TimeUtils { + // 根据日期取得星期几 + public static String getWeek() { + Date date = new Date(); + String[] weeks = {"星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"}; + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + int weekIndex = cal.get(Calendar.DAY_OF_WEEK) - 1; + if (weekIndex < 0) { + weekIndex = 0; + } + return weeks[weekIndex]; + } +} diff --git a/app/src/main/java/com/ttstd/dialer/view/BaseFragmentPagerAdapter.java b/app/src/main/java/com/ttstd/dialer/view/BaseFragmentPagerAdapter.java new file mode 100644 index 0000000..d2be0fb --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/view/BaseFragmentPagerAdapter.java @@ -0,0 +1,203 @@ +package com.ttstd.dialer.view; + +import android.util.SparseArray; + +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; +import androidx.fragment.app.FragmentTransaction; + +import java.util.List; + +/** + * 加载显示Fragment的ViewPagerAdapter基类 + * 提供可以刷新的方法 + * + * @author Fly + * @e-mail 1285760616@qq.com + * @time 2018/3/22 + */ +public class BaseFragmentPagerAdapter extends FragmentPagerAdapter { + private List mFragmentList; + private FragmentManager mFragmentManager; + /**下面两个值用来保存Fragment的位置信息,用以判断该位置是否需要更新*/ + private SparseArray mFragmentPositionMap; + private SparseArray mFragmentPositionMapAfterUpdate; + + public BaseFragmentPagerAdapter(FragmentManager fm, List fragments) { + super(fm); + mFragmentManager = fm; + mFragmentList = fragments; + mFragmentPositionMap = new SparseArray<>(); + mFragmentPositionMapAfterUpdate = new SparseArray<>(); + setFragmentPositionMap(); + setFragmentPositionMapForUpdate(); + } + + /** + * 保存更新之前的位置信息,用的键值对结构来保存 + */ + private void setFragmentPositionMap() { + mFragmentPositionMap.clear(); + for (int i = 0; i < mFragmentList.size(); i++) { + mFragmentPositionMap.put(Long.valueOf(getItemId(i)).intValue(), String.valueOf(i)); + } + } + + /** + * 保存更新之后的位置信息,用的键值对结构来保存 + */ + private void setFragmentPositionMapForUpdate() { + mFragmentPositionMapAfterUpdate.clear(); + for (int i = 0; i < mFragmentList.size(); i++) { + mFragmentPositionMapAfterUpdate.put(Long.valueOf(getItemId(i)).intValue(), String.valueOf(i)); + } + } + + /** + * 在此方法中找到需要更新的位置返回POSITION_NONE,否则返回POSITION_UNCHANGED即可 + */ + @Override + public int getItemPosition(Object object) { + int hashCode = object.hashCode(); + //查找object在更新后的列表中的位置 + String position = mFragmentPositionMapAfterUpdate.get(hashCode); + //更新后的列表中不存在该object的位置了 + if (position == null) { + return POSITION_NONE; + } else { + //如果更新后的列表中存在该object的位置, 查找该object之前的位置并判断位置是否发生了变化 + int size = mFragmentPositionMap.size(); + for (int i = 0; i < size ; i++) { + int key = mFragmentPositionMap.keyAt(i); + if (key == hashCode) { + String index = mFragmentPositionMap.get(key); + if (position.equals(index)) { + //位置没变依然返回POSITION_UNCHANGED + return POSITION_UNCHANGED; + } else { + //位置变了 + return POSITION_NONE; + } + } + } + } + return POSITION_UNCHANGED; + } + + /** + * 将指定的Fragment替换/更新为新的Fragment + * @param oldFragment 旧Fragment + * @param newFragment 新Fragment + */ + public void replaceFragment(Fragment oldFragment, Fragment newFragment) { + int position = mFragmentList.indexOf(oldFragment); + if (position == -1) { + return; + } + //从Transaction移除旧的Fragment + removeFragmentInternal(oldFragment); + //替换List中对应的Fragment + mFragmentList.set(position, newFragment); + //刷新Adapter + notifyItemChanged(); + } + + /** + * 将指定位置的Fragment替换/更新为新的Fragment,同{@link #replaceFragment(Fragment oldFragment, Fragment newFragment)} + * @param position 旧Fragment的位置 + * @param newFragment 新Fragment + */ + public void replaceFragment(int position, Fragment newFragment) { + Fragment oldFragment = mFragmentList.get(position); + removeFragmentInternal(oldFragment); + mFragmentList.set(position, newFragment); + notifyItemChanged(); + } + + /** + * 移除指定的Fragment + * @param fragment 目标Fragment + */ + public void removeFragment(Fragment fragment) { + //先从List中移除 + mFragmentList.remove(fragment); + //然后从Transaction移除 + removeFragmentInternal(fragment); + //最后刷新Adapter + notifyItemChanged(); + } + + /** + * 移除指定位置的Fragment,同 {@link #removeFragment(Fragment fragment)} + * @param position + */ + public void removeFragment(int position) { + Fragment fragment = mFragmentList.get(position); + //然后从List中移除 + mFragmentList.remove(fragment); + //先从Transaction移除 + removeFragmentInternal(fragment); + //最后刷新Adapter + notifyItemChanged(); + } + + /** + * 添加Fragment + * @param fragment 目标Fragment + */ + public void addFragment(Fragment fragment) { + mFragmentList.add(fragment); + notifyItemChanged(); + } + + /** + * 在指定位置插入一个Fragment + * @param position 插入位置 + * @param fragment 目标Fragment + */ + public void insertFragment(int position, Fragment fragment) { + mFragmentList.add(position, fragment); + notifyItemChanged(); + } + + public void notifyItemChanged() { + //刷新之前重新收集位置信息 + setFragmentPositionMapForUpdate(); + notifyDataSetChanged(); + setFragmentPositionMap(); + } + + /** + * 从Transaction移除Fragment + * @param fragment 目标Fragment + */ + private void removeFragmentInternal(Fragment fragment) { + FragmentTransaction transaction = mFragmentManager.beginTransaction(); + transaction.remove(fragment); + transaction.commitAllowingStateLoss(); + } + + /** + * 此方法不用position做返回值即可破解fragment tag异常的错误 + */ + @Override + public long getItemId(int position) { + // 获取当前数据的hashCode,其实这里不用hashCode用自定义的可以关联当前Item对象的唯一值也可以,只要不是直接返回position + return mFragmentList.get(position).hashCode(); + } + + @Override + public Fragment getItem(int position) { + return mFragmentList.get(position); + } + + @Override + public int getCount() { + return mFragmentList.size(); + } + + public List getFragments() { + return mFragmentList; + } +} diff --git a/app/src/main/java/com/ttstd/dialer/view/ScaleCircleNavigator.java b/app/src/main/java/com/ttstd/dialer/view/ScaleCircleNavigator.java new file mode 100644 index 0000000..181ad6d --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/view/ScaleCircleNavigator.java @@ -0,0 +1,323 @@ +package com.ttstd.dialer.view; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PointF; +import android.util.SparseArray; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.animation.Interpolator; +import android.view.animation.LinearInterpolator; + +import net.lucode.hackware.magicindicator.NavigatorHelper; +import net.lucode.hackware.magicindicator.abs.IPagerNavigator; +import net.lucode.hackware.magicindicator.buildins.ArgbEvaluatorHolder; +import net.lucode.hackware.magicindicator.buildins.UIUtil; + +import java.util.ArrayList; +import java.util.List; + +// _oo0oo_ +// o8888888o +// 88" . "88 +// (| -_- |) +// 0\ = /0 +// ___/`---'\___ +// .' \\| |// '. +// / \\||| : |||// \ +// / _||||| -:- |||||- \ +// | | \\\ - /// | | +// | \_| ''\---/'' |_/ | +// \ .-\__ '-' ___/-. / +// ___'. .' /--.--\ `. .'___ +// ."" '< `.___\_<|>_/___.' >' "". +// | | : `- \`.;`\ _ /`;.`/ - ` : | | +// \ \ `_. \_ __\ /__ _/ .-` / / +// =====`-.____`.___ \_____/___.-`___.-'===== +// `=---=' +// +// +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// 佛祖保佑 永无BUG + +/** + * 类似CircleIndicator的效果 + * Created by hackware on 2016/9/3. + */ + +public class ScaleCircleNavigator extends View implements IPagerNavigator, NavigatorHelper.OnNavigatorScrollListener { + private int mMinRadius; + private int mMaxRadius; + private int mNormalCircleColor = Color.LTGRAY; + private int mSelectedCircleColor = Color.GRAY; + private int mCircleSpacing; + private int mCircleCount; + + private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private List mCirclePoints = new ArrayList(); + private SparseArray mCircleRadiusArray = new SparseArray(); + + // 事件回调 + private boolean mTouchable; + private OnCircleClickListener mCircleClickListener; + private float mDownX; + private float mDownY; + private int mTouchSlop; + + private boolean mFollowTouch = true; // 是否跟随手指滑动 + private NavigatorHelper mNavigatorHelper = new NavigatorHelper(); + private Interpolator mStartInterpolator = new LinearInterpolator(); + + public ScaleCircleNavigator(Context context) { + super(context); + init(context); + } + + private void init(Context context) { + mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + mMinRadius = UIUtil.dip2px(context, 3); + mMaxRadius = UIUtil.dip2px(context, 4); + mCircleSpacing = UIUtil.dip2px(context, 8); + mNavigatorHelper.setNavigatorScrollListener(this); + mNavigatorHelper.setSkimOver(true); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec)); + } + + private int measureWidth(int widthMeasureSpec) { + int mode = MeasureSpec.getMode(widthMeasureSpec); + int width = MeasureSpec.getSize(widthMeasureSpec); + int result = 0; + switch (mode) { + case MeasureSpec.EXACTLY: + result = width; + break; + case MeasureSpec.AT_MOST: + case MeasureSpec.UNSPECIFIED: + if (mCircleCount <= 0) { + result = getPaddingLeft() + getPaddingRight(); + } else { + result = (mCircleCount - 1) * mMinRadius * 2 + mMaxRadius * 2 + (mCircleCount - 1) * mCircleSpacing + getPaddingLeft() + getPaddingRight(); + } + break; + default: + break; + } + return result; + } + + private int measureHeight(int heightMeasureSpec) { + int mode = MeasureSpec.getMode(heightMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + int result = 0; + switch (mode) { + case MeasureSpec.EXACTLY: + result = height; + break; + case MeasureSpec.AT_MOST: + case MeasureSpec.UNSPECIFIED: + result = mMaxRadius * 2 + getPaddingTop() + getPaddingBottom(); + break; + default: + break; + } + return result; + } + + @Override + protected void onDraw(Canvas canvas) { + for (int i = 0, j = mCirclePoints.size(); i < j; i++) { + PointF point = mCirclePoints.get(i); + float radius = mCircleRadiusArray.get(i, (float) mMinRadius); + mPaint.setColor(ArgbEvaluatorHolder.eval((radius - mMinRadius) / (mMaxRadius - mMinRadius), mNormalCircleColor, mSelectedCircleColor)); + canvas.drawCircle(point.x, getHeight() / 2.0f, radius, mPaint); + } + } + + private void prepareCirclePoints() { + mCirclePoints.clear(); + if (mCircleCount > 0) { + int y = Math.round(getHeight() / 2.0f); + int centerSpacing = mMinRadius * 2 + mCircleSpacing; + int startX = mMaxRadius + getPaddingLeft(); + for (int i = 0; i < mCircleCount; i++) { + PointF pointF = new PointF(startX, y); + mCirclePoints.add(pointF); + startX += centerSpacing; + } + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + float x = event.getX(); + float y = event.getY(); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + if (mTouchable) { + mDownX = x; + mDownY = y; + return true; + } + break; + case MotionEvent.ACTION_UP: + if (mCircleClickListener != null) { + if (Math.abs(x - mDownX) <= mTouchSlop && Math.abs(y - mDownY) <= mTouchSlop) { + float max = Float.MAX_VALUE; + int index = 0; + for (int i = 0; i < mCirclePoints.size(); i++) { + PointF pointF = mCirclePoints.get(i); + float offset = Math.abs(pointF.x - x); + if (offset < max) { + max = offset; + index = i; + } + } + mCircleClickListener.onClick(index); + } + } + break; + default: + break; + } + return super.onTouchEvent(event); + } + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + mNavigatorHelper.onPageScrolled(position, positionOffset, positionOffsetPixels); + } + + @Override + public void onPageSelected(int position) { + mNavigatorHelper.onPageSelected(position); + } + + @Override + public void onPageScrollStateChanged(int state) { + mNavigatorHelper.onPageScrollStateChanged(state); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + prepareCirclePoints(); + } + + @Override + public void notifyDataSetChanged() { + prepareCirclePoints(); + requestLayout(); + } + + @Override + public void onAttachToMagicIndicator() { + } + + @Override + public void onDetachFromMagicIndicator() { + } + + public void setMinRadius(int minRadius) { + mMinRadius = minRadius; + prepareCirclePoints(); + invalidate(); + } + + public void setMaxRadius(int maxRadius) { + mMaxRadius = maxRadius; + prepareCirclePoints(); + invalidate(); + } + + public void setNormalCircleColor(int normalCircleColor) { + mNormalCircleColor = normalCircleColor; + invalidate(); + } + + public void setSelectedCircleColor(int selectedCircleColor) { + mSelectedCircleColor = selectedCircleColor; + invalidate(); + } + + public void setCircleSpacing(int circleSpacing) { + mCircleSpacing = circleSpacing; + prepareCirclePoints(); + invalidate(); + } + + public void setStartInterpolator(Interpolator startInterpolator) { + mStartInterpolator = startInterpolator; + if (mStartInterpolator == null) { + mStartInterpolator = new LinearInterpolator(); + } + } + + public void setCircleCount(int count) { + mCircleCount = count; // 此处不调用invalidate,让外部调用notifyDataSetChanged + mNavigatorHelper.setTotalCount(mCircleCount); + } + + public void setTouchable(boolean touchable) { + mTouchable = touchable; + } + + public void setFollowTouch(boolean followTouch) { + mFollowTouch = followTouch; + } + + public void setSkimOver(boolean skimOver) { + mNavigatorHelper.setSkimOver(skimOver); + } + + public void setCircleClickListener(OnCircleClickListener circleClickListener) { + if (!mTouchable) { + mTouchable = true; + } + mCircleClickListener = circleClickListener; + } + + @Override + public void onEnter(int index, int totalCount, float enterPercent, boolean leftToRight) { + if (mFollowTouch) { + float radius = mMinRadius + (mMaxRadius - mMinRadius) * mStartInterpolator.getInterpolation(enterPercent); + mCircleRadiusArray.put(index, radius); + invalidate(); + } + } + + @Override + public void onLeave(int index, int totalCount, float leavePercent, boolean leftToRight) { + if (mFollowTouch) { + float radius = mMaxRadius + (mMinRadius - mMaxRadius) * mStartInterpolator.getInterpolation(leavePercent); + mCircleRadiusArray.put(index, radius); + invalidate(); + } + } + + @Override + public void onSelected(int index, int totalCount) { + if (!mFollowTouch) { + mCircleRadiusArray.put(index, (float) mMaxRadius); + invalidate(); + } + } + + @Override + public void onDeselected(int index, int totalCount) { + if (!mFollowTouch) { + mCircleRadiusArray.put(index, (float) mMinRadius); + invalidate(); + } + } + + public interface OnCircleClickListener { + void onClick(int index); + } +} diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml new file mode 100644 index 0000000..0eed934 --- /dev/null +++ b/app/src/main/res/drawable/ic_add.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_contact.xml b/app/src/main/res/drawable/ic_contact.xml new file mode 100644 index 0000000..c359d19 --- /dev/null +++ b/app/src/main/res/drawable/ic_contact.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_douyin.xml b/app/src/main/res/drawable/ic_douyin.xml new file mode 100644 index 0000000..ac2ad0d --- /dev/null +++ b/app/src/main/res/drawable/ic_douyin.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 0000000..0361242 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_weixin.xml b/app/src/main/res/drawable/ic_weixin.xml new file mode 100644 index 0000000..7ec88f8 --- /dev/null +++ b/app/src/main/res/drawable/ic_weixin.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index de1395d..2c6f67d 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,18 +1,40 @@ - + tools:context=".activity.main.MainActivity"> - + - \ No newline at end of file + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_contact.xml b/app/src/main/res/layout/fragment_contact.xml new file mode 100644 index 0000000..1dcee3c --- /dev/null +++ b/app/src/main/res/layout/fragment_contact.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml new file mode 100644 index 0000000..52007e2 --- /dev/null +++ b/app/src/main/res/layout/fragment_home.xml @@ -0,0 +1,251 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 030098f..bf50e31 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -3,4 +3,252 @@ #6200EE #3700B3 #03DAC5 + + #99bd2f25 + #BD2F25 + + #F8CBBD + + + + + #00000000 + #FFE2C59B + #FFFFFFF0 + #FFFFFFE0 + #FFFFFF00 + #FFFFFAFA + #FFFFFAF0 + #FFFFFACD + #FFFFF8DC + #FFFFF5EE + #FFFFF0F5 + #FFFFEFD5 + #FFFFEBCD + #FFFFE4E1 + #FFFFE4C4 + #FFFFE4B5 + #FFFFDEAD + #FFFFDAB9 + #FFFFD700 + #FFFFC0CB + #FFFFB6C1 + #FFFFA500 + #FFFFA07A + #FFFF8C00 + #FFFF7F50 + #FFFF69B4 + #FFFF6347 + #FFFF4500 + #FFFF1493 + #FFFF00FF + #FFFF0000 + #FFFDF5E6 + #FFFAFAD2 + #FFFAF0E6 + #FFFAEBD7 + #FFFA8072 + #FFF8F8FF + #FFF5FFFA + #FFF5F5F5 + #FFF5F5DC + #FFF5DEB3 + #FFF4A460 + #FFF0FFFF + #FFF0F8FF + #FFF0E68C + #FFF08080 + #FFEEE8AA + #FFEE82EE + #FFE9967A + #FFE6E6FA + #FFE0FFFF + #FFDEB887 + #FFDDA0DD + #FFDCDCDC + #FFDC143C + #FFDB7093 + #FFDAA520 + #FFDA70D6 + #FFD8BFD8 + #FFD3D3D3 + #FFD2B48C + #FFD2691E + #FFCD853F + #FFCD5C5C + #FFC71585 + #FFC0C0C0 + #FFBDB76B + #FFBC8F8F + #FFBA55D3 + #FFB8860B + #FFB22222 + #FFB0E0E6 + #FFB0C4DE + #FFAFEEEE + #FFADFF2F + #FFADD8E6 + #FFA9A9A9 + #FFA52A2A + #FFA0522D + #FF9932CC + #FF98FB98 + #FF9400D3 + #FF9370DB + #FF90EE90 + #FF8FBC8F + #FF8B4513 + #FF8B008B + #FF8B0000 + #FF8A2BE2 + #FF87CEFA + #FF87CEEB + #FF808080 + #FF808000 + #FF800080 + #FF800000 + #FF7FFFD4 + #FF7FFF00 + #FF7CFC00 + #FF7B68EE + #FF778899 + #FF708090 + #FF6B8E23 + #FF6A5ACD + #FF696969 + #FF66CDAA + #FF6495ED + #FF5F9EA0 + #FF556B2F + #FF4B0082 + #FF48D1CC + #FF483D8B + #FF4682B4 + #FF4169E1 + #FF40E0D0 + #FF3CB371 + #FF32CD32 + #FF2F4F4F + #FF2E8B57 + #FF228B22 + #FF20B2AA + #FF1E90FF + #FF191970 + #FF00FFFF + #FF00FF7F + #FF00FF00 + #FF00FA9A + #FF00CED1 + #FF00BFFF + #FF008B8B + #FF008080 + #FF008000 + #FF006400 + #FF0000FF + #FF0000CD + #FF00008B + #FF000080 + #FF2B2B2B + + + #ffffff + #ffffe0 + #fffaf0 + #fffacd + #fff8dc + #fff5ee + #fff0f5 + #ffefd5 + #ffebcd + #ffe4e1 + #ffdead + #ffdab9 + #ffb6c1 + #ffa07a + #ff8c00 + #ff69b4 + #ff4500 + #ff1493 + #ff00ff + #fdf5e6 + #fafad2 + #faebd7 + #f8f8ff + #f5fffa + #f5f5f5 + #f4a460 + #f0fff0 + #f0f8ff + #f08080 + #eee8aa + #e9967a + #e0ffff + #deb887 + #dcdcdc + #db7093 + #d3d3d3 + #d3d3d3 + #cd5c5c + #c71585 + #bdb76b + #bc8f8f + #ba55d3 + #b8860b + #b0e0e6 + #b0c4de + #afeeee + #adff2f + #add8e6 + #a9a9a9 + #a9a9a9 + #9932cc + #98fb98 + #9400d3 + #9370db + #90ee90 + #8fbc8f + #8b4513 + #8b008b + #8b0000 + #8a2be2 + #87cefa + #87ceeb + #808080 + #7cfc00 + #7b68ee + #778899 + #778899 + #708090 + #708090 + #6b8e23 + #6a5acd + #696969 + #696969 + #66cdaa + #6495ed + #5f9ea0 + #556b2f + #48d1cc + #483d8b + #4682b4 + #4169e1 + #3cb371 + #32cd32 + #2f4f4f + #2f4f4f + #2e8b57 + #228b22 + #20b2aa + #1e90ff + #191970 + #00ffff + #00ff7f + #00fa9a + #00ced1 + #00bfff + #008b8b + #006400 + #0000cd + #00008b + #000000 diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..aa3fee9 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,4 @@ + + + 20sp + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4c71e84..fe24b8d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,6 @@ 拨号助手 + + + Hello blank fragment diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 5885930..39ed8f0 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,11 +1,25 @@ - --> + + + diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..c69abbb --- /dev/null +++ b/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/app/src/main/res/xml/network.xml b/app/src/main/res/xml/network.xml new file mode 100644 index 0000000..dca93c0 --- /dev/null +++ b/app/src/main/res/xml/network.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/iconloader/.gitignore b/iconloader/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/iconloader/.gitignore @@ -0,0 +1 @@ +/build diff --git a/iconloader/build.gradle b/iconloader/build.gradle new file mode 100644 index 0000000..e1550ec --- /dev/null +++ b/iconloader/build.gradle @@ -0,0 +1,37 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 36 +// buildToolsVersion "36.0.0" + + defaultConfig { + minSdkVersion 14 + targetSdkVersion 36 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles 'consumer-rules.pro' + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation 'androidx.appcompat:appcompat:1.3.1' + + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.2.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' + + //磁盘缓存 + implementation 'com.jakewharton:disklrucache:2.0.2' +} diff --git a/iconloader/consumer-rules.pro b/iconloader/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/iconloader/proguard-rules.pro b/iconloader/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/iconloader/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/iconloader/src/androidTest/java/com/uiui/iconloader/ExampleInstrumentedTest.java b/iconloader/src/androidTest/java/com/uiui/iconloader/ExampleInstrumentedTest.java new file mode 100644 index 0000000..3f077f7 --- /dev/null +++ b/iconloader/src/androidTest/java/com/uiui/iconloader/ExampleInstrumentedTest.java @@ -0,0 +1,27 @@ +package com.uiui.iconloader; + +import android.content.Context; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.assertEquals; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + assertEquals("com.ttstd.iconloader.test", appContext.getPackageName()); + } +} diff --git a/iconloader/src/main/AndroidManifest.xml b/iconloader/src/main/AndroidManifest.xml new file mode 100644 index 0000000..1720cbe --- /dev/null +++ b/iconloader/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/iconloader/src/main/java/com/uiui/iconloader/CacheKeyGenerator.java b/iconloader/src/main/java/com/uiui/iconloader/CacheKeyGenerator.java new file mode 100644 index 0000000..2109746 --- /dev/null +++ b/iconloader/src/main/java/com/uiui/iconloader/CacheKeyGenerator.java @@ -0,0 +1,16 @@ +package com.uiui.iconloader; + +import android.content.ComponentName; +import android.content.Intent; + +public class CacheKeyGenerator { + // 生成ComponentName的标准字符串格式(包名/类名) + public static String generateKey(ComponentName componentName) { + return componentName.flattenToShortString(); // 如:com.example.app/.MainActivity + } + + // 从Intent中提取ComponentName作为键 + public static String generateKey(Intent intent) { + return generateKey(intent.getComponent()); + } +} diff --git a/iconloader/src/main/java/com/uiui/iconloader/IconCacheManager.java b/iconloader/src/main/java/com/uiui/iconloader/IconCacheManager.java new file mode 100644 index 0000000..b37a97c --- /dev/null +++ b/iconloader/src/main/java/com/uiui/iconloader/IconCacheManager.java @@ -0,0 +1,79 @@ +package com.uiui.iconloader; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.util.Log; + +import androidx.collection.LruCache; + +import com.jakewharton.disklrucache.DiskLruCache; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +public class IconCacheManager { + private static final String TAG = "IconCacheManager"; + private LruCache mMemoryCache; + private DiskLruCache mDiskCache; + + public IconCacheManager(Context context) { + // 内存缓存(最大8MB) + int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); + int cacheSize = maxMemory / 8; + mMemoryCache = new LruCache(cacheSize) { + @Override + protected int sizeOf(String key, Drawable icon) { + if (icon instanceof BitmapDrawable) { + return ((BitmapDrawable) icon).getBitmap().getByteCount() / 1024; + } + return 1; // 非Bitmap对象按1KB计算 + } + }; + + // 磁盘缓存(50MB) + File cacheDir = new File(context.getCacheDir(), "icon_cache"); + try { + mDiskCache = DiskLruCache.open(cacheDir, 1, 1, 50 * 1024 * 1024); + } catch (Exception e) { + Log.e(TAG, "IconCacheManager: " + e.getMessage()); + } + } + + public Drawable getFromMemory(String key) { + return mMemoryCache.get(key); + } + + public Drawable getFromDisk(String key) { + try { + DiskLruCache.Snapshot snapshot = mDiskCache.get(key); + if (snapshot != null) { + InputStream in = snapshot.getInputStream(0); + return Drawable.createFromStream(in, null); + } + } catch (IOException e) { + Log.e("IconCache", "Disk read error", e); + } + return null; + } + + public void putToMemory(String key, Drawable icon) { + mMemoryCache.put(key, icon); + } + + + public void putToDisk(String key, Drawable icon) { + try { + DiskLruCache.Editor editor = mDiskCache.edit(key); + if (icon instanceof BitmapDrawable) { + Bitmap bitmap = ((BitmapDrawable) icon).getBitmap(); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, editor.newOutputStream(0)); + editor.commit(); + } + } catch (IOException e) { + Log.e("IconCache", "Disk write error", e); + } + } +} \ No newline at end of file diff --git a/iconloader/src/main/java/com/uiui/iconloader/IconLoader.java b/iconloader/src/main/java/com/uiui/iconloader/IconLoader.java new file mode 100644 index 0000000..2c31f76 --- /dev/null +++ b/iconloader/src/main/java/com/uiui/iconloader/IconLoader.java @@ -0,0 +1,97 @@ +package com.uiui.iconloader; + +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.graphics.drawable.AdaptiveIconDrawable; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build; + +import androidx.loader.content.AsyncTaskLoader; + +public class IconLoader extends AsyncTaskLoader { + private final ComponentName mComponentName; // 关键:使用ComponentName标识组件 + private final IconCacheManager mCache; + + public IconLoader(Context context, ComponentName componentName, IconCacheManager cache) { + super(context); + mComponentName = componentName; + mCache = cache; + } + + @Override + protected void onStartLoading() { + String key = CacheKeyGenerator.generateKey(mComponentName); + // 1. 检查内存缓存 + Drawable cachedIcon = mCache.getFromMemory(key); + if (cachedIcon != null) { + deliverResult(cachedIcon); // 直接返回缓存 + return; + } + forceLoad(); // 触发后台加载 + } + + @Override + public Drawable loadInBackground() { + String key = CacheKeyGenerator.generateKey(mComponentName); + + // 1. 检查内存缓存 + Drawable cachedIcon = mCache.getFromMemory(key); + if (cachedIcon != null) return cachedIcon; + + // 2. 检查磁盘缓存 + Drawable diskCached = mCache.getFromDisk(key); + if (diskCached != null) { + mCache.putToMemory(key, diskCached); + return diskCached; + } + + // 3. 从PackageManager加载组件专属图标 + try { + PackageManager pm = getContext().getPackageManager(); + ActivityInfo info = pm.getActivityInfo(mComponentName, 0); // 获取ActivityInfo + Drawable rawIcon = info.loadIcon(pm); // 加载该组件的图标 + + // 4. 图标处理(压缩/主题适配) + Drawable processedIcon = processIcon(rawIcon); + + // 5. 更新缓存 + mCache.putToMemory(key, processedIcon); + mCache.putToDisk(key, processedIcon); + return processedIcon; + } catch (PackageManager.NameNotFoundException e) { + return null; + } + } + + private Drawable processIcon(Drawable icon) { + // 自适应图标处理(Android 8.0+) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O + && icon instanceof AdaptiveIconDrawable) { + AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) icon; + int targetColor = isDarkMode() ? Color.WHITE : Color.BLACK; + adaptiveIcon.setColorFilter(targetColor, PorterDuff.Mode.SRC_ATOP); // 根据主题切换单色模式 + } + + // 图标尺寸压缩(避免内存溢出) + if (icon instanceof BitmapDrawable) { + Bitmap bitmap = ((BitmapDrawable) icon).getBitmap(); + int scaledWidth = bitmap.getWidth() / 2; + int scaledHeight = bitmap.getHeight() / 2; + Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, scaledWidth, scaledHeight, true); + return new BitmapDrawable(getContext().getResources(), scaledBitmap); + } + return icon; + } + + private boolean isDarkMode() { + return (getContext().getResources().getConfiguration().uiMode + & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; + } +} \ No newline at end of file diff --git a/iconloader/src/test/java/com/uiui/iconloader/ExampleUnitTest.java b/iconloader/src/test/java/com/uiui/iconloader/ExampleUnitTest.java new file mode 100644 index 0000000..2e86c2d --- /dev/null +++ b/iconloader/src/test/java/com/uiui/iconloader/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.uiui.iconloader; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/niceimageview/.gitignore b/niceimageview/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/niceimageview/.gitignore @@ -0,0 +1 @@ +/build diff --git a/niceimageview/build.gradle b/niceimageview/build.gradle new file mode 100644 index 0000000..25dcbf5 --- /dev/null +++ b/niceimageview/build.gradle @@ -0,0 +1,48 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 28 + + defaultConfig { + minSdkVersion 14 + targetSdkVersion 28 + } + + buildTypes { + iPlay50PDebug {} + iPlay50PRelease {} + U807Debug {} + U807Release {} + iPlay50Debug {} + iPlay50Release {} + zhanRuiDebug {} + zhanRuiRelease {} + iPlay50SEDebug {} + iPlay50SERelease {} + iPlay50ProDebug {} + iPlay50ProRelease {} + T1102Debug {} + T1102Release {} + iPlay50miniDebug {} + iPlay50miniRelease {} + iPlay5013Debug {} + iPlay5013Release {} + iPlay50miniProDebug {} + iPlay50miniProRelease {} + XPadDebug {} + XPadRelease {} + teclast8183Debug {} + teclast8183Release {} + G10PDebug {} + G10PRelease {} + debug {} + release {} + } + +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation 'com.android.support:appcompat-v7:28.0.0' +} diff --git a/niceimageview/proguard-rules.pro b/niceimageview/proguard-rules.pro new file mode 100644 index 0000000..e43f365 --- /dev/null +++ b/niceimageview/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For menu details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/niceimageview/src/main/AndroidManifest.xml b/niceimageview/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6dab987 --- /dev/null +++ b/niceimageview/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/niceimageview/src/main/java/com/shehuan/niv/NiceImageView.java b/niceimageview/src/main/java/com/shehuan/niv/NiceImageView.java new file mode 100644 index 0000000..2d31326 --- /dev/null +++ b/niceimageview/src/main/java/com/shehuan/niv/NiceImageView.java @@ -0,0 +1,338 @@ +package com.shehuan.niv; + + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.RectF; +import android.graphics.Xfermode; +import android.os.Build; +import android.util.AttributeSet; + +import androidx.annotation.ColorInt; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatImageView; + +public class NiceImageView extends AppCompatImageView { + private Context context; + + private boolean isCircle; // 是否显示为圆形,如果为圆形则设置的corner无效 + private boolean isCoverSrc; // border、inner_border是否覆盖图片 + private int borderWidth; // 边框宽度 + private int borderColor = Color.WHITE; // 边框颜色 + private int innerBorderWidth; // 内层边框宽度 + private int innerBorderColor = Color.WHITE; // 内层边框充色 + + private int cornerRadius; // 统一设置圆角半径,优先级高于单独设置每个角的半径 + private int cornerTopLeftRadius; // 左上角圆角半径 + private int cornerTopRightRadius; // 右上角圆角半径 + private int cornerBottomLeftRadius; // 左下角圆角半径 + private int cornerBottomRightRadius; // 右下角圆角半径 + + private int maskColor; // 遮罩颜色 + + private Xfermode xfermode; + + private int width; + private int height; + private float radius; + + private float[] borderRadii; + private float[] srcRadii; + + private RectF srcRectF; // 图片占的矩形区域 + private RectF borderRectF; // 边框的矩形区域 + + private Paint paint; + private Path path; // 用来裁剪图片的ptah + private Path srcPath; // 图片区域大小的path + + public NiceImageView(Context context) { + this(context, null); + } + + public NiceImageView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public NiceImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + this.context = context; + + TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.NiceImageView, 0, 0); + for (int i = 0; i < ta.getIndexCount(); i++) { + int attr = ta.getIndex(i); + if (attr == R.styleable.NiceImageView_is_cover_src) { + isCoverSrc = ta.getBoolean(attr, isCoverSrc); + } else if (attr == R.styleable.NiceImageView_is_circle) { + isCircle = ta.getBoolean(attr, isCircle); + } else if (attr == R.styleable.NiceImageView_border_width) { + borderWidth = ta.getDimensionPixelSize(attr, borderWidth); + } else if (attr == R.styleable.NiceImageView_border_color) { + borderColor = ta.getColor(attr, borderColor); + } else if (attr == R.styleable.NiceImageView_inner_border_width) { + innerBorderWidth = ta.getDimensionPixelSize(attr, innerBorderWidth); + } else if (attr == R.styleable.NiceImageView_inner_border_color) { + innerBorderColor = ta.getColor(attr, innerBorderColor); + } else if (attr == R.styleable.NiceImageView_corner_radius) { + cornerRadius = ta.getDimensionPixelSize(attr, cornerRadius); + } else if (attr == R.styleable.NiceImageView_corner_top_left_radius) { + cornerTopLeftRadius = ta.getDimensionPixelSize(attr, cornerTopLeftRadius); + } else if (attr == R.styleable.NiceImageView_corner_top_right_radius) { + cornerTopRightRadius = ta.getDimensionPixelSize(attr, cornerTopRightRadius); + } else if (attr == R.styleable.NiceImageView_corner_bottom_left_radius) { + cornerBottomLeftRadius = ta.getDimensionPixelSize(attr, cornerBottomLeftRadius); + } else if (attr == R.styleable.NiceImageView_corner_bottom_right_radius) { + cornerBottomRightRadius = ta.getDimensionPixelSize(attr, cornerBottomRightRadius); + } else if (attr == R.styleable.NiceImageView_mask_color) { + maskColor = ta.getColor(attr, maskColor); + } + } + ta.recycle(); + + borderRadii = new float[8]; + srcRadii = new float[8]; + + borderRectF = new RectF(); + srcRectF = new RectF(); + + paint = new Paint(); + path = new Path(); + + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1) { + xfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN); + } else { + xfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT); + srcPath = new Path(); + } + + calculateRadii(); + clearInnerBorderWidth(); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + width = w; + height = h; + + initBorderRectF(); + initSrcRectF(); + } + + @Override + protected void onDraw(Canvas canvas) { + // 使用图形混合模式来显示指定区域的图片 + canvas.saveLayer(srcRectF, null, Canvas.ALL_SAVE_FLAG); + if (!isCoverSrc) { + float sx = 1.0f * (width - 2 * borderWidth - 2 * innerBorderWidth) / width; + float sy = 1.0f * (height - 2 * borderWidth - 2 * innerBorderWidth) / height; + // 缩小画布,使图片内容不被borders覆盖 + canvas.scale(sx, sy, width / 2.0f, height / 2.0f); + } + super.onDraw(canvas); + paint.reset(); + path.reset(); + if (isCircle) { + path.addCircle(width / 2.0f, height / 2.0f, radius, Path.Direction.CCW); + } else { + path.addRoundRect(srcRectF, srcRadii, Path.Direction.CCW); + } + + paint.setAntiAlias(true); + paint.setStyle(Paint.Style.FILL); + paint.setXfermode(xfermode); + //9.0及以上系统图片一次圆一次方的解决办法 + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1) { + canvas.drawPath(path, paint); + } else { + srcPath.addRect(srcRectF, Path.Direction.CCW); + // 计算tempPath和path的差集 + srcPath.op(path, Path.Op.DIFFERENCE); + canvas.drawPath(srcPath, paint); + srcPath.reset();//1 + } + paint.setXfermode(null); + + // 绘制遮罩 + if (maskColor != 0) { + paint.setColor(maskColor); + canvas.drawPath(path, paint); + } + // 恢复画布 + canvas.restore(); + // 绘制边框 + drawBorders(canvas); + } + + private void drawBorders(Canvas canvas) { + if (isCircle) { + if (borderWidth > 0) { + drawCircleBorder(canvas, borderWidth, borderColor, radius - borderWidth / 2.0f); + } + if (innerBorderWidth > 0) { + drawCircleBorder(canvas, innerBorderWidth, innerBorderColor, radius - borderWidth - innerBorderWidth / 2.0f); + } + } else { + if (borderWidth > 0) { + drawRectFBorder(canvas, borderWidth, borderColor, borderRectF, borderRadii); + } + } + } + + private void drawCircleBorder(Canvas canvas, int borderWidth, int borderColor, float radius) { + initBorderPaint(borderWidth, borderColor); + path.addCircle(width / 2.0f, height / 2.0f, radius, Path.Direction.CCW); + canvas.drawPath(path, paint); + } + + private void drawRectFBorder(Canvas canvas, int borderWidth, int borderColor, RectF rectF, float[] radii) { + initBorderPaint(borderWidth, borderColor); + path.addRoundRect(rectF, radii, Path.Direction.CCW); + canvas.drawPath(path, paint); + } + + private void initBorderPaint(int borderWidth, int borderColor) { + path.reset(); + paint.setStrokeWidth(borderWidth); + paint.setColor(borderColor); + paint.setStyle(Paint.Style.STROKE); + } + + /** + * 计算外边框的RectF + */ + private void initBorderRectF() { + if (!isCircle) { + borderRectF.set(borderWidth / 2.0f, borderWidth / 2.0f, width - borderWidth / 2.0f, height - borderWidth / 2.0f); + } + } + + /** + * 计算图片原始区域的RectF + */ + private void initSrcRectF() { + if (isCircle) { + radius = Math.min(width, height) / 2.0f; + srcRectF.set(width / 2.0f - radius, height / 2.0f - radius, width / 2.0f + radius, height / 2.0f + radius); + } else { + srcRectF.set(0, 0, width, height); + if (isCoverSrc) { + srcRectF = borderRectF; + } + } + } + + /** + * 计算RectF的圆角半径 + */ + private void calculateRadii() { + if (isCircle) { + return; + } + if (cornerRadius > 0) { + for (int i = 0; i < borderRadii.length; i++) { + borderRadii[i] = cornerRadius; + srcRadii[i] = cornerRadius - borderWidth / 2.0f; + } + } else { + borderRadii[0] = borderRadii[1] = cornerTopLeftRadius; + borderRadii[2] = borderRadii[3] = cornerTopRightRadius; + borderRadii[4] = borderRadii[5] = cornerBottomRightRadius; + borderRadii[6] = borderRadii[7] = cornerBottomLeftRadius; + + srcRadii[0] = srcRadii[1] = cornerTopLeftRadius - borderWidth / 2.0f; + srcRadii[2] = srcRadii[3] = cornerTopRightRadius - borderWidth / 2.0f; + srcRadii[4] = srcRadii[5] = cornerBottomRightRadius - borderWidth / 2.0f; + srcRadii[6] = srcRadii[7] = cornerBottomLeftRadius - borderWidth / 2.0f; + } + } + + private void calculateRadiiAndRectF(boolean reset) { + if (reset) { + cornerRadius = 0; + } + calculateRadii(); + initBorderRectF(); + invalidate(); + } + + /** + * 目前圆角矩形情况下不支持inner_border,需要将其置0 + */ + private void clearInnerBorderWidth() { + if (!isCircle) { + this.innerBorderWidth = 0; + } + } + + public void isCoverSrc(boolean isCoverSrc) { + this.isCoverSrc = isCoverSrc; + initSrcRectF(); + invalidate(); + } + + public void isCircle(boolean isCircle) { + this.isCircle = isCircle; + clearInnerBorderWidth(); + initSrcRectF(); + invalidate(); + } + + public void setBorderWidth(int borderWidth) { + this.borderWidth = Utils.dp2px(context, borderWidth); + calculateRadiiAndRectF(false); + } + + public void setBorderColor(@ColorInt int borderColor) { + this.borderColor = borderColor; + invalidate(); + } + + public void setInnerBorderWidth(int innerBorderWidth) { + this.innerBorderWidth = Utils.dp2px(context, innerBorderWidth); + clearInnerBorderWidth(); + invalidate(); + } + + public void setInnerBorderColor(@ColorInt int innerBorderColor) { + this.innerBorderColor = innerBorderColor; + invalidate(); + } + + public void setCornerRadius(int cornerRadius) { + this.cornerRadius = Utils.dp2px(context, cornerRadius); + calculateRadiiAndRectF(false); + } + + public void setCornerTopLeftRadius(int cornerTopLeftRadius) { + this.cornerTopLeftRadius = Utils.dp2px(context, cornerTopLeftRadius); + calculateRadiiAndRectF(true); + } + + public void setCornerTopRightRadius(int cornerTopRightRadius) { + this.cornerTopRightRadius = Utils.dp2px(context, cornerTopRightRadius); + calculateRadiiAndRectF(true); + } + + public void setCornerBottomLeftRadius(int cornerBottomLeftRadius) { + this.cornerBottomLeftRadius = Utils.dp2px(context, cornerBottomLeftRadius); + calculateRadiiAndRectF(true); + } + + public void setCornerBottomRightRadius(int cornerBottomRightRadius) { + this.cornerBottomRightRadius = Utils.dp2px(context, cornerBottomRightRadius); + calculateRadiiAndRectF(true); + } + + public void setMaskColor(@ColorInt int maskColor) { + this.maskColor = maskColor; + invalidate(); + } +} diff --git a/niceimageview/src/main/java/com/shehuan/niv/Utils.java b/niceimageview/src/main/java/com/shehuan/niv/Utils.java new file mode 100644 index 0000000..f9958ad --- /dev/null +++ b/niceimageview/src/main/java/com/shehuan/niv/Utils.java @@ -0,0 +1,11 @@ +package com.shehuan.niv; + + +import android.content.Context; + +public class Utils { + public static int dp2px(Context context, float dipValue) { + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (dipValue * scale + 0.5f); + } +} diff --git a/niceimageview/src/main/res/values/attrs.xml b/niceimageview/src/main/res/values/attrs.xml new file mode 100644 index 0000000..c7c51cd --- /dev/null +++ b/niceimageview/src/main/res/values/attrs.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/niceimageview/src/main/res/values/strings.xml b/niceimageview/src/main/res/values/strings.xml new file mode 100644 index 0000000..1a1d942 --- /dev/null +++ b/niceimageview/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + niceimageview + diff --git a/settings.gradle b/settings.gradle index 0996c8b..08cf151 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ rootProject.name='拨号助手' -include ':app' +include ':app', ':niceimageview', ':iconloader'