本文最后更新于:1 个月前

GifFun 是一个由《第一行代码》作者郭霖开源的一个项目。用户可以登录分享 Gif 图,查看别人分享的 Gif 图等。

关于在学习 GifFun 项目中的一些收获

  1. 在自定义 Application 中进行全局的初始化操作。

  2. 封装 SharedPreferences 的操作。

  3. 定义常量标识符。

  4. 导入 module api project('module name')

  5. 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

  6. 接口指定方法,继承接口则有此方法

  7. 设置 activity 的基类,就像《第二行代码》里讲的

  8. 日志操作的扩展工具类,就像《第二行代码》里讲的

  9. AndroidVersion.kt

  10. looper

  11. 集成 Toast 方法的 Context 使用 Application 的 Context,当前所在代码类有 Context 则使用当前的。

  12. Nickname(昵称)

  13. isNotEmpty(str) 等价于 str != null && str.length > 0
    isNotBlank(str) 等价于 str != null && str.length > 0 && str.trim().length() > 0

  14. @JvmStatic 指定如果它是函数,则需要从此元素生成额外的静态方法。如果此元素是属性,则应生成额外的静态 getter / setter 方法。

  15. api 和 implementation

  16. implementation “androidx.palette:palette:1.0.0” 拾色器

  17. actionStart()

  18. setupViews() 初始化布局控件 UserInfo modify

  19. 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;
        }
    }