本文最后更新于:1 个月前
GifFun 是一个由《第一行代码》作者郭霖开源的一个项目。用户可以登录分享 Gif 图,查看别人分享的 Gif 图等。
关于在学习 GifFun 项目中的一些收获
在自定义 Application 中进行全局的初始化操作。
封装 SharedPreferences 的操作。
定义常量标识符。
导入 module
api project('module name')
umeng(数据统计)、 Litepal、 gson、 eventbus(事件发布订阅,观察者?)、 okhttp、Glide图片加载、
implementation 'jp.wasabeef:glide-transformations:4.1.0'
、'de.hdodenhof:circleimageview:2.1.0'
、七牛云、Android-Image-Cropper、filebrowser、PhotoView接口指定方法,继承接口则有此方法
设置 activity 的基类,就像《第二行代码》里讲的
日志操作的扩展工具类,就像《第二行代码》里讲的
AndroidVersion.kt
looper
集成 Toast 方法的 Context 使用 Application 的 Context,当前所在代码类有 Context 则使用当前的。
Nickname(昵称)
isNotEmpty(str) 等价于 str != null && str.length > 0
isNotBlank(str) 等价于 str != null && str.length > 0 && str.trim().length() > 0@JvmStatic
指定如果它是函数,则需要从此元素生成额外的静态方法。如果此元素是属性,则应生成额外的静态 getter / setter 方法。api 和 implementation
implementation “androidx.palette:palette:1.0.0” 拾色器
actionStart()
setupViews() 初始化布局控件 UserInfo modify
CountDownTimer 计时器
com.quxianggif.core
单位转换工具类,会根据手机的分辨率来进行单位转换
/**
* 根据手机的分辨率将dp转成为px
*/
fun dp2px(dp: Float): Int {
val scale = GifFun.getContext().resources.displayMetrics.density
return (dp * scale + 0.5f).toInt()
}
/**
* 根据手机的分辨率将px转成dp
*/
fun px2dp(px: Float): Int {
val scale = GifFun.getContext().resources.displayMetrics.density
return (px / scale + 0.5f).toInt()
}
Toast
private var toast: Toast? = null
/**
* 弹出Toast信息。如果不是在主线程中调用此方法,Toast信息将会不显示。
*
* @param content
* Toast中显示的内容
*/
@SuppressLint("ShowToast")
@JvmOverloads
fun showToast(content: String, duration: Int = Toast.LENGTH_SHORT) {
if (Looper.myLooper() == Looper.getMainLooper()) {
if (toast == null) {
toast = Toast.makeText(GifFun.getContext(), content, duration)
} else {
toast?.setText(content)
}
toast?.show()
}
}
/**
* 切换到主线程后弹出Toast信息。此方法不管是在子线程还是主线程中,都可以成功弹出Toast信息。
*
* @param content
* Toast中显示的内容
* @param duration
* Toast显示的时长
*/
@SuppressLint("ShowToast")
@JvmOverloads
fun showToastOnUiThread(content: String, duration: Int = Toast.LENGTH_SHORT) {
GifFun.getHandler().post {
if (toast == null) {
toast = Toast.makeText(GifFun.getContext(), content, duration)
} else {
toast?.setText(content)
}
toast?.show()
}
}
com.quxianggif.main
comments
取消数据改变时的动画,防止闪烁
(recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
当知道 Adapter 内 Item 的改变不会影响 RecyclerView 宽高的时候,可以设置为 true 让 RecyclerView 避免重新计算大小,并通过 Adapter 的增删改查方法刷新 RecyclerView。在需要改变宽高的时候就用
notifyDataSetChanged()
刷新。recyclerView.setHasFixedSize(true)
自定义 RecyclerView,根据 item 高度改变 RecyclerView 高度
class TopCommentsRecyclerView : RecyclerView { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet) : super(context, attrs) constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super( context, attrs, defStyleAttr ) override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { // AT_MOST参数表示控件可以自由调整大小,最大不超过Integer.MAX_VALUE/4 val height = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE shr 2, MeasureSpec.AT_MOST) super.onMeasure(widthMeasureSpec, height) } }
common
将状态栏设置成透明
/** * 将状态栏设置成透明。只适配Android 5.0以上系统的手机。 */ protected fun transparentStatusBar() { if (AndroidVersion.hasLollipop()) { val decorView = window.decorView decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE window.statusBarColor = Color.TRANSPARENT } }
隐藏软键盘
/** * 隐藏软键盘。 */ fun hideSoftKeyboard() { try { val view = currentFocus if (view != null) { val binder = view.windowToken val manager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager manager.hideSoftInputFromWindow(binder, InputMethodManager.HIDE_NOT_ALWAYS) } } catch (e: Exception) { logWarn(TAG, e.message, e) } }
显示软键盘
/** * 显示软键盘。 */ fun showSoftKeyboard(editText: EditText?) { try { if (editText != null) { editText.requestFocus() val manager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager manager.showSoftInput(editText, 0) } } catch (e: Exception) { logWarn(TAG, e.message, e) } }
跳转到应用设置界面
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) val uri = Uri.fromParts("package", GlobalUtil.appPackage, null) intent.data = uri
feeds
ImageView、TextView 等控件具有
android:layout_gravity
属性发布文章时间显示
private fun getDraftTime(draftMillis: Long): String { val currentMillis = System.currentTimeMillis() val calendar = Calendar.getInstance() calendar.timeInMillis = currentMillis val currentYear = calendar.get(Calendar.YEAR) val currentMonth = calendar.get(Calendar.MONTH) val currentDay = calendar.get(Calendar.DAY_OF_MONTH) calendar.timeInMillis = draftMillis val draftYear = calendar.get(Calendar.YEAR) val draftMonth = calendar.get(Calendar.MONTH) val draftDay = calendar.get(Calendar.DAY_OF_MONTH) return if (currentYear == draftYear && currentMonth == draftMonth && currentDay == draftDay) { // 当天的草稿只显示时间 SimpleDateFormat("HH:mm", Locale.getDefault()).format(Date(draftMillis)) } else { if (currentYear == draftYear) { // 当年的草稿只显示月日 SimpleDateFormat("MM-dd", Locale.getDefault()).format(Date(draftMillis)) } else { // 隔年的草稿显示完整年月日 SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date(draftMillis)) } } }
ContextMenu
SparseArray
init.ui
/**
* 跳转到下一个Activity。如果在闪屏界面停留的时间还不足规定最短停留时间,则会在这里等待一会,保证闪屏界面不至于一闪而过。
*/
@Synchronized
open fun forwardToNextActivity(hasNewVersion: Boolean, version: Version?) {
if (!isForwarding) { // 如果正在跳转或已经跳转到下一个界面,则不再重复执行跳转
isForwarding = true
val currentTime = System.currentTimeMillis()
val timeSpent = currentTime - enterTime
if (timeSpent < MIN_WAIT_TIME) {
GlobalUtil.sleep(MIN_WAIT_TIME - timeSpent)
}
runOnUiThread {
if (GifFun.isLogin()) {
MainActivity.actionStart(this)
finish()
} else {
if (isActive) {
LoginActivity.actionStartWithTransition(this, logoView, hasNewVersion, version)
} else {
LoginActivity.actionStart(this, hasNewVersion, version)
finish()
}
}
}
}
}
util
ActivityCollector.kt
/** * 应用中所有Activity的管理器,可用于一键杀死所有Activity。 */ object ActivityCollector { private const val TAG = "ActivityCollector" private val activityList = ArrayList<WeakReference<Activity>?>() fun size(): Int { return activityList.size } fun add(weakRefActivity: WeakReference<Activity>?) { activityList.add(weakRefActivity) } fun remove(weakRefActivity: WeakReference<Activity>?) { val result = activityList.remove(weakRefActivity) logDebug(TAG, "remove activity reference $result") } fun finishAll() { if (activityList.isNotEmpty()) { for (activityWeakReference in activityList) { val activity = activityWeakReference?.get() if (activity != null && !activity.isFinishing) { activity.finish() } } activityList.clear() } } }
ColorUtils.kt
/** * Utility methods for working with colors. */ object ColorUtils { private const val IS_LIGHT = 0 private const val IS_DARK = 1 private const val LIGHTNESS_UNKNOWN = 2 private const val TAG = "ColorUtils" /** * Set the alpha component of `color` to be `alpha`. */ @CheckResult @ColorInt fun modifyAlpha(@ColorInt color: Int, @IntRange(from = 0, to = 255) alpha: Int): Int { return color and 0x00ffffff or (alpha shl 24) } /** * Set the alpha component of `color` to be `alpha`. */ @CheckResult @ColorInt fun modifyAlpha(@ColorInt color: Int, @FloatRange(from = 0.0, to = 1.0) alpha: Float): Int { return modifyAlpha(color, (255f * alpha).toInt()) } /** * 判断传入的图片的颜色属于深色还是浅色。 * @param bitmap * 图片的Bitmap对象。 * @return 返回true表示图片属于深色,返回false表示图片属于浅色。 */ fun isBitmapDark(palette: Palette?, bitmap: Bitmap): Boolean { val isDark: Boolean val lightness = ColorUtils.isDark(palette) if (lightness == ColorUtils.LIGHTNESS_UNKNOWN) { isDark = ColorUtils.isDark(bitmap, bitmap.width / 2, 0) } else { isDark = lightness == ColorUtils.IS_DARK } return isDark } /** * Checks if the most populous color in the given palette is dark * * * Annoyingly we have to return this Lightness 'enum' rather than a boolean as palette isn't * guaranteed to find the most populous color. */ fun isDark(palette: Palette?): Int { val mostPopulous = getMostPopulousSwatch(palette) ?: return LIGHTNESS_UNKNOWN return if (isDark(mostPopulous.hsl)) IS_DARK else IS_LIGHT } /** * Determines if a given bitmap is dark. This extracts a palette inline so should not be called * with a large image!! If palette fails then check the color of the specified pixel */ fun isDark(bitmap: Bitmap, backupPixelX: Int, backupPixelY: Int): Boolean { // first try palette with a small color quant size val palette = Palette.from(bitmap).maximumColorCount(3).generate() return if (palette.swatches.size > 0) { isDark(palette) == IS_DARK } else { // if palette failed, then check the color of the specified pixel isDark(bitmap.getPixel(backupPixelX, backupPixelY)) } } /** * Convert to HSL & check that the lightness value */ fun isDark(@ColorInt color: Int): Boolean { val hsl = FloatArray(3) android.support.v4.graphics.ColorUtils.colorToHSL(color, hsl) return isDark(hsl) } /** * Check that the lightness value (0–1) */ fun isDark(hsl: FloatArray): Boolean { // @Size(3) logDebug(TAG, "hsl[2] is " + hsl[2]) return hsl[2] < 0.8f } fun getMostPopulousSwatch(palette: Palette?): Palette.Swatch? { var mostPopulous: Palette.Swatch? = null if (palette != null) { for (swatch in palette.swatches) { if (mostPopulous == null || swatch.population > mostPopulous.population) { mostPopulous = swatch } } } return mostPopulous } }
DateUtil.kt
/** * 时间和日期工具类。 */ object DateUtil { private const val MINUTE = (60 * 1000).toLong() private const val HOUR = 60 * MINUTE private const val DAY = 24 * HOUR private const val WEEK = 7 * DAY private const val MONTH = 4 * WEEK private const val YEAR = 365 * DAY /** * 根据传入的Unix时间戳,获取转换过后更加易读的时间格式。 * @param dateMillis * Unix时间戳 * @return 转换过后的时间格式,如2分钟前,1小时前。 */ fun getConvertedDate(dateMillis: Long): String { val currentMillis = System.currentTimeMillis() val timePast = currentMillis - dateMillis if (timePast > -MINUTE) { // 采用误差一分钟以内的算法,防止客户端和服务器时间不同步导致的显示问题 when { timePast < HOUR -> { var pastMinutes = timePast / MINUTE if (pastMinutes <= 0) { pastMinutes = 1 } return pastMinutes.toString() + GlobalUtil.getString(R.string.minutes_ago) } timePast < DAY -> { var pastHours = timePast / HOUR if (pastHours <= 0) { pastHours = 1 } return pastHours.toString() + GlobalUtil.getString(R.string.hours_ago) } timePast < WEEK -> { var pastDays = timePast / DAY if (pastDays <= 0) { pastDays = 1 } return pastDays.toString() + GlobalUtil.getString(R.string.days_ago) } timePast < MONTH -> { var pastDays = timePast / WEEK if (pastDays <= 0) { pastDays = 1 } return pastDays.toString() + GlobalUtil.getString(R.string.weeks_ago) } else -> return getDate(dateMillis) } } else { return getDateAndTime(dateMillis) } } fun getTimeLeftTip(timeLeft: Long) = when { timeLeft > YEAR -> { val year = (timeLeft / YEAR) + 1 year.toString() + GlobalUtil.getString(R.string.year) } timeLeft > MONTH -> { val month = (timeLeft / MONTH) + 1 month.toString() + GlobalUtil.getString(R.string.month) } timeLeft > DAY -> { val day = (timeLeft / DAY) + 1 day.toString() + GlobalUtil.getString(R.string.day) } timeLeft > HOUR -> { val hour = (timeLeft / HOUR) + 1 hour.toString() + GlobalUtil.getString(R.string.hour) } timeLeft > MINUTE -> { val minute = (timeLeft / MINUTE) + 1 minute.toString() + GlobalUtil.getString(R.string.minute) } else -> { "1" + GlobalUtil.getString(R.string.minute) } } fun isBlockedForever(timeLeft: Long) = timeLeft > 5 * YEAR private fun getDate(dateMillis: Long): String { val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) return sdf.format(Date(dateMillis)) } private fun getDateAndTime(dateMillis: Long): String { val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()) return sdf.format(Date(dateMillis)) } }
DeviceInfo.kt
/** * 提供所有与设备相关的信息。 */ object DeviceInfo { /** * 获取当前设备屏幕的宽度,以像素为单位。 * * @return 当前设备屏幕的宽度。 */ val screenWidth: Int get() { val windowManager = GifFun.getContext().getSystemService(Context.WINDOW_SERVICE) as WindowManager val metrics = DisplayMetrics() if (AndroidVersion.hasJellyBeanMR1()) { windowManager.defaultDisplay.getRealMetrics(metrics) } else { windowManager.defaultDisplay.getMetrics(metrics) } return metrics.widthPixels } /** * 获取当前设备屏幕的高度,以像素为单位。 * * @return 当前设备屏幕的高度。 */ val screenHeight: Int get() { val windowManager = GifFun.getContext().getSystemService(Context.WINDOW_SERVICE) as WindowManager val metrics = DisplayMetrics() if (AndroidVersion.hasJellyBeanMR1()) { windowManager.defaultDisplay.getRealMetrics(metrics) } else { windowManager.defaultDisplay.getMetrics(metrics) } return metrics.heightPixels } }
com.quxianggif.network
MD5.kt
/** * MD5加密辅助工具类。 */ public class MD5 { private static final char[] DIGITS_UPPER = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; /** * 对传入的字符串进行MD5加密。 * * @param origin 原始字符串。 * @return 经过MD5加密后的字符串。 */ public static String encrypt(String origin) { try { MessageDigest digest = MessageDigest.getInstance("MD5"); digest.update(origin.getBytes(Charset.defaultCharset())); return new String(toHex(digest.digest())); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return ""; } /** * 获取文件的MD5值。 * * @param path 文件的路径 * @return 文件的MD5值。 */ public static String getFileMD5(String path) { try { FileInputStream fis = new FileInputStream(path); MessageDigest md = MessageDigest.getInstance("MD5"); byte[] buffer = new byte[1024]; int length; while ((length = fis.read(buffer, 0, 1024)) != -1) { md.update(buffer, 0, length); } BigInteger bigInt = new BigInteger(1, md.digest()); return bigInt.toString(16).toUpperCase(); } catch (Exception e) { e.printStackTrace(); } return ""; } private static char[] toHex(byte[] data) { char[] toDigits = DIGITS_UPPER; int l = data.length; char[] out = new char[l << 1]; // two characters form the hex value. for (int i = 0, j = 0; i < l; i++) { out[j++] = toDigits[(0xF0 & data[i]) >>> 4]; out[j++] = toDigits[0x0F & data[i]]; } return out; } }
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!