扫码 项目总结-V1.3.0

好吧,其实V1.3.5 已经在上个月下旬做完了,现在才来写总结,但凡勤快点都不会拖到现在,害呀。这里标题是V1.3.0,但其实这个版本做出来没有上线,直接上的V1.3.5。所以就一并总结了。
V1.3相对于V1.2主要增加的功能就是创建二维码收藏,但其实关于创建二维码的代码很简单,本篇文章的主要内容也不是单纯的创建。下边是本文的主要内容:

V1.3.0总结

可以看到总结的点比较散,随便一个单独拿出来都可以水一篇文章了,文章虽然水,但都是干货哈

一 . 单选框的定制和使用RadioGroup和RadioButton

我们知道谷歌提供了一个单选控件RadioButton,这个通常是需要和RadioGroup一起使用的。
多个RadioButton置于一个RadioGroup,然后通过RadioGroup去控制其中的RadioButton。
RadioGroup是继承于LinearLayout的,所以用起来也很顺手。

1.布局

这里我需要的是三个横向排布的单选框,所以布局如下:

            

                

                

                
            

主要目的就是让用户选择wifi加密方式
其实就是一个ViewGroup设置方向为横向后,排布几个radiobutton,就这么简单,当然还有其他属性上的设置,等会慢慢讲,现在给每个控件加上id就可以了

2.改变样式

我们知道,原本的radio button是非常丑的,就像这个:


radio button

要是直接这么上去,产品和ui都要骂娘了,所以要设计radio button的样式,就像这样:

效果图

接下来请参考上边的布局代码一起阅读。
第一步
去掉radio button右边的小圆点,这个非常简单:只需要一个属性
android:button="@null"
当然也可以通过这个属性设置这个button的样式,我们不需要就直接去掉。

第二步
设置选择选中和未选中的边框颜色
一个个来,边框实际上就是背景,背景一般采用drawable来绘制,所以新建一个命名为bg_radio的selector drawable文件,
不懂drawable的同学移步:https://www.jianshu.com/p/f01b1af15a88
内容如下:
android:background="@drawable/bg_radio"




    
    

具体的shape就不给了,其实这里的bg_edit_text_normal和bg_edit_text_focused,就是一个圆角,1dp宽边框的不同颜色的样式。关键点其实的这里item对应的state_checked属性,指定了选中和未选中应该用什么样式。
然后在radio button 中设置属性:
android:background="@drawable/bg_radio"
第三步
设置选择选中和未选中的字体颜色
同样的步骤,新建一个命名为bg_radio_text的selector drawable文件,
内容如下:



    
    

最后设置radio button属性:
android:textColor="@drawable/bg_radio_text"

现在样式就改完了,感觉还行,接下来实现功能。

3.获取选择的内容

我们要点击使用时,需要获取一下当前选了什么
非常简单,就使用radio group的一条属性就可以了:

                when (wifi_encryption_radio.checkedRadioButtonId) {
                    R.id.radio1 -> {
                        encryption = "WPA"
                    }
                    R.id.radio2 -> {
                        encryption = "WPE"
                    }
                    R.id.radio3 -> {//没有加密类型
                        password = ""
                        encryption = ""
                    }
                }
4.给定默认值

本来radio group默认是什么都不选的,但是如果需要默认选择一个,可以这么设置:

 //默认选择第一个
wifi_encryption_radio.check(R.id.radio1)
5.选择监听

有的小伙伴又有需求了,说之前获取选择内容只是用户最终选择的结果,但是我想要用户每次选择我都知道选了哪个,怎么办
这个时候设置给radio group设置一下选择监听就可以了:

        wifi_encryption_radio.setOnCheckedChangeListener(object:RadioGroup.OnCheckedChangeListener{
            override fun onCheckedChanged(group: RadioGroup?, checkedId: Int) {
                when (checkedId) {
                    R.id.radio1 -> {
                        
                    }
                    R.id.radio2 -> {
                        
                    }
                    R.id.radio3 -> {
                       
                    }
                }
            }
        })

二 . RecycleView统一修改所有item的布局

就像这个样子:

效果图

在点击删除后,原本的收藏按钮消失,取而代之的是删除按钮,这该怎么实现呢?
非常简单:在adapter中添加一个全局变量isShowDeleteBtn一开始置为false
然后在onBindViewHolder方法中加入以下代码:

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
......省略代码

        if (isShowDeleteBtn) {
            holder.deleteBtn.visibility=View.VISIBLE
            holder.collectBtn.visibility=View.GONE
        } else {
            holder.deleteBtn.visibility=View.GONE
           holder.collectBtn.visibility=View.VISIBLE
    }

最后在需要改变布局的时候这样做:

    fun changeDeleteLayout() {
        mAdapter.isShowDeleteBtn=! mAdapter.isShowDeleteBtn
        //item发生改变 重新绘制item布局
        mAdapter.notifyItemRangeChanged(0,mData.size)
    }

这里的notifyItemRangeChanged非常关键,它表示一定范围内item内容修改,需要重新绘制。如果把范围扩大到全部,则会重新绘制所有item。
源码在这里:感兴趣可以看看注释说明

        /**
         * Notify any registered observers that the itemCount items starting at
         * position positionStart have changed.
         * Equivalent to calling notifyItemRangeChanged(position, itemCount, null);.
         *
         * 

This is an item change event, not a structural change event. It indicates that * any reflection of the data in the given position range is out of date and should * be updated. The items in the given range retain the same identity.

* * @param positionStart Position of the first item that has changed * @param itemCount Number of items that have changed * * @see #notifyItemChanged(int) */ public final void notifyItemRangeChanged(int positionStart, int itemCount) { mObservable.notifyItemRangeChanged(positionStart, itemCount); }

在附赠一个删除item的效果,当你从mData中移除了某一项后,
请调用notifyItemRemoved而不是notifyDataSetChanged

    fun removeItem(position: Int){
        mData.remove(mData[position])
        mAdapter.notifyItemRemoved(position)
    }

mAdapter.notifyItemRemoved(position)表示某个位置的内容被移除了,这时会有动画效果体现出来(这个动画也可以自定义)

三 . 软键盘

在我们程序中,难免会遇到使用输入框,这个时候就会弹出软键盘,那么关于软键盘和输入框的一些注意点我就写在这了。

1.软键盘的弹出方式

弹出方式分别是:键盘覆盖页面,键盘挤占页面布局,键盘顶起整个页面(不覆盖,不挤占),自定义方式(监听根布局Layout 的Size改变,获得软键盘高度,动态修改页面),等等
参考:https://www.cnblogs.com/jerehedu/p/4194125.html
处理方式:项目的AndroidManifest.xml文件中界面对应的里修改属性
例子:这会使屏幕整体上移

android:windowSoftInputMode="stateVisible|adjustResize" 

关于windowSoftInputMode的一些知识点:
activity主窗口与软键盘的交互模式,可以用来避免输入法面板遮挡问题。
它的设置必须是下面列表中的一个值,或一个”state…”值加一个”adjust…”值的组合:(值之间采用 | 分开)
列表:
各值的含义:

【A】stateUnspecified:软键盘的状态并没有指定,系统将选择一个合适的状态或依赖于主题的设置

【B】stateUnchanged:当这个activity出现时,软键盘将一直保持在上一个activity里的状态,无论是隐藏还是显示

【C】stateHidden:用户选择activity时,软键盘总是被隐藏

【D】stateAlwaysHidden:当该Activity主窗口获取焦点时,软键盘也总是被隐藏的

【E】stateVisible:软键盘通常是可见的

【F】stateAlwaysVisible:用户选择activity时,软键盘总是显示的状态

【G】adjustUnspecified:默认设置,通常由系统自行决定是隐藏还是显示

【H】adjustResize:该Activity总是调整屏幕的大小以便留出软键盘的空间

【I】adjustPan:当前窗口的内容将自动移动以便当前焦点从不被键盘覆盖和用户能总是看到输入内容的部分

2.软键盘弹起收回的监听

Android系统并没有直接提供监听键盘弹起收回的方法,只能通过一些特殊的方式来监听。比如下边这种,通过监听Layout高度的改变,来确认键盘是否弹起收回。有一个工具类如下:


import android.app.Activity
import android.graphics.Rect
import android.view.View
import android.view.ViewTreeObserver


/**
 * Created by liujinhua on 15/10/25.
 */
class SoftKeyBoardListener(activity: Activity) {
    private val rootView: View//activity的根视图
    var rootViewVisibleHeight: Int //纪录根视图的显示高度

    private var onSoftKeyBoardChangeListener: OnSoftKeyBoardChangeListener? = null
    private fun setOnSoftKeyBoardChangeListener(onSoftKeyBoardChangeListener: OnSoftKeyBoardChangeListener) {
        this.onSoftKeyBoardChangeListener = onSoftKeyBoardChangeListener
    }

    interface OnSoftKeyBoardChangeListener {
        fun keyBoardShow(height: Int)
        fun keyBoardHide(height: Int)
    }

    companion object {
        fun setListener(
            activity: Activity?,
            onSoftKeyBoardChangeListener: OnSoftKeyBoardChangeListener?
        ) {
            val softKeyBoardListener = activity?.let { SoftKeyBoardListener(it) }
            if (onSoftKeyBoardChangeListener != null) {
                softKeyBoardListener!!.setOnSoftKeyBoardChangeListener(onSoftKeyBoardChangeListener)
            }
        }
    }

    init {
        //获取activity的根视图
        rootView = activity.getWindow().getDecorView()
        val r = Rect()
        rootView.getWindowVisibleDisplayFrame(r)
        rootViewVisibleHeight = r.height()
        //监听视图树中全局布局发生改变或者视图树中的某个视图的可视状态发生改变
        rootView.getViewTreeObserver().addOnGlobalLayoutListener(object :
            ViewTreeObserver.OnGlobalLayoutListener {
            override fun onGlobalLayout() {
                //获取当前根视图在屏幕上显示的大小
                val r = Rect()
                rootView.getWindowVisibleDisplayFrame(r)
                val visibleHeight: Int = r.height()
                println("" + visibleHeight)
                if (rootViewVisibleHeight == 0) {
                    rootViewVisibleHeight = visibleHeight
                    return
                }

                //根视图显示高度没有变化,可以看作软键盘显示/隐藏状态没有改变
                if (rootViewVisibleHeight == visibleHeight) {
                    return
                }

                //根视图显示高度变小超过200,可以看作软键盘显示了
                if (rootViewVisibleHeight - visibleHeight > 200) {
                    if (onSoftKeyBoardChangeListener != null) {
                        onSoftKeyBoardChangeListener!!.keyBoardShow(rootViewVisibleHeight - visibleHeight)
                    }
                    rootViewVisibleHeight = visibleHeight
                    return
                }

                //根视图显示高度变大超过200,可以看作软键盘隐藏了
                if (visibleHeight - rootViewVisibleHeight > 200) {
                    if (onSoftKeyBoardChangeListener != null) {
                        onSoftKeyBoardChangeListener!!.keyBoardHide(visibleHeight - rootViewVisibleHeight)
                    }
                    rootViewVisibleHeight = visibleHeight
                    return
                }
            }
        })
    }
}

用法:

       //设置键盘的监听
        SoftKeyBoardListener.setListener(activity, object : OnSoftKeyBoardChangeListener {
            override fun keyBoardShow(height: Int) {
                Log.d("键盘监听", "弹起")
   
            }

            override fun keyBoardHide(height: Int) {
                Log.d("键盘监听", "回收")
    
            }
        })
3.指定输入框的输入方式

这个其实是edittext的属性,修改inputType。
例子:editText.inputType = InputType.TYPE_CLASS_NUMBER
这个就表示输入框只想要纯数字,其他的输入类型可以自己研究下。
输入框的错误提示:editText.error = "输入内容不可为空"
聚焦和非聚焦ui样式的改变(通过drawable)
https://blog.csdn.net/tracydragonlxy/article/details/100558915
其他关于输入框的知识点都很简单,需要的时候一搜就可以了。
最后推荐一个还不错的自定义输入框:
https://github.com/wrapp-archive/floatlabelededittext

四 . 一个非常好用的时间选择器

https://github.com/JZXiang/TimePickerDialog

五 .图片保存和分享

1.图片保存

图片保存通常就是将bitmap在手机上保存为jpg,png等格式图片。
这里有几个注意点:
1.文件读写权限
2.判断手机是否有外部存储卡,若没有则只能保存在App内部存储
3.图片保存后并不会直接在相册里显示,而是要发出广播通知系统刷新媒体库
我的一个工具类在这里,比较清晰,可以作为一个参考,后期再加上接口回调,将保存结果成功或失败回调出去。

import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.os.Environment
import android.os.Handler
import android.os.Message
import android.util.Log
import android.widget.Toast
import androidx.browser.customtabs.CustomTabsClient.getPackageName
import androidx.core.content.FileProvider
import com.matrix.framework.utils.DirUtils.getCacheDir
import com.qr.scanlife.R
import com.qr.scanlife.base.App
import java.io.File
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException
import kotlin.concurrent.thread

/**
 * 保存图片工具类 将bitmap对象保存到本地相册
 *
 **/

class SaveImageUnit {

    //读写权限!
    companion object {
        val instance: SaveImageUnit by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { SaveImageUnit() }
    }

    //保存图片的文件夹地址
    var appDir: File? = null
    private val TAG = "图片保存"
    val context = App.context
    var mediaScanIntent: Intent? = null
    val saveSucCode = 2211

    //检查保存的文件夹是否存在 不存在则创建一个
    private fun checkDir() {
        val state = Environment.getExternalStorageState()
        if (Environment.MEDIA_MOUNTED == state) {
            //如果有外部内存卡可进行读写 则建在外部内存卡上
            appDir = File(
                Environment.getExternalStorageDirectory().absolutePath + File.separator + Environment.DIRECTORY_PICTURES + File.separator + context.getText(
                    R.string.app_name
                )
            )
            if (!appDir!!.exists()) {
                appDir!!.mkdir()
            }
        } else {//否则将文件夹建在 APP内部存储上
            appDir =
                File(context.filesDir.absolutePath + File.separator + context.getText(R.string.app_name))
            if (!appDir!!.exists()) {
                appDir!!.mkdir()
            }
        }
        Log.d(TAG, "图片文件夹地址${appDir?.absolutePath}")
    }

    //保存bitmap到指定文件夹 并发出广播通知系统刷新媒体库
    fun saveBitmap(bitmap: Bitmap, imageName: String) {
        Toast.makeText(context, App.context.getText(R.string.saving), Toast.LENGTH_SHORT).show()
        checkDir()

        val file = File(appDir, "$imageName.jpg")
        //准备好发出广播 通知系统媒体 刷新相册 在相册中显示出图片
        mediaScanIntent =
            Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file))

        Log.d(TAG, "图片地址${file.absolutePath}")
        thread {
            try {
                val fileOutputStream = FileOutputStream(file)
                /**
                 * quality:100
                 * 为不压缩
                 */
                bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fileOutputStream)
                fileOutputStream.flush()
                fileOutputStream.close()
                val msg = Message()
                msg.what = saveSucCode
                msg.obj = file.absolutePath
                handler.sendMessage(msg)
            } catch (e: FileNotFoundException) {
                e.printStackTrace()
                Log.d(TAG, "保存失败1${e.message}")
                Toast.makeText(context, "${App.context.getText(R.string.save_failed)}:${e.message}", Toast.LENGTH_SHORT).show()
            } catch (e: IOException) {
                e.printStackTrace()
                Log.d(TAG, "保存失败2${e.message}")
                Toast.makeText(context, "${App.context.getText(R.string.save_failed)}:${e.message}", Toast.LENGTH_SHORT).show()
            }
        }
    }

    @SuppressLint("HandlerLeak")
    private val handler = object : Handler() {
        //接收信息
        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)
            //判断信息识别码 根据不同的识别码进行不同动作
            when (msg.what) {
                saveSucCode -> {
                    context.sendBroadcast(mediaScanIntent)
                    val path: String? = msg.obj as? String
                    Toast.makeText(context, "${App.context.getText(R.string.save_success)} ${App.context.getText(R.string.image_path)}:$path", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }

    //保存文件到app缓存目录准备分享 耗时操作 请最好在异步线程调用
    fun cacheBitmapForShare(bitmap: Bitmap): Uri? {
        val dir = File(getCacheDir().absolutePath + File.separator + "Share")
        if (!dir.exists()) {
            dir.mkdir()
        }
        val file = File(dir,"Share${System.currentTimeMillis()}.jpg" )
        Log.d(TAG, "图片缓存地址${file.absolutePath}")
        try {
            val fileOutputStream = FileOutputStream(file)
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fileOutputStream)
            fileOutputStream.flush()
            fileOutputStream.close()
            val uri=FileProvider.getUriForFile(context,context.packageName+".fileProvider",file)
            return uri
        } catch (e: FileNotFoundException) {
            e.printStackTrace()
            Log.d(TAG, "缓存失败1${e.message}")
        } catch (e: IOException) {
            e.printStackTrace()
            Log.d(TAG, "缓存失败2${e.message}")
        }
        return null
    }
}
2.图片分享

图片分享之前需要将图片保存,然后将保存的文件uri作为分享内容使用intent分享出去。
这里有两个坑:
首先保存的位置应该是App的缓存文件夹(系统随时回收),不会占用过多的空间,产生垃圾文件。
其次,如果保存在了缓存文件夹,则系统不允许App直接将文件uri暴露出去,而要通过FileProvider
file provider用法:
https://www.jianshu.com/p/f0b2cf0e0353
然后关于分享文件可以看上边的cacheBitmapForShare方法,具体用法:

    private fun shareImg(bitmap:Bitmap) {
        val uri = SaveImageUnit.instance.cacheBitmapForShare(bitmap)
        Log.d("图片分享uri", uri.toString())
        val shareIntent = Intent(Intent.ACTION_SEND)
        shareIntent.putExtra(Intent.EXTRA_STREAM, uri)
        shareIntent.type = "image/*" //设置分享内容的类型:图片
        try {
            startActivity(
                Intent.createChooser(
                    shareIntent,
                    getString("Share")
                )
            )
        } catch (e: Exception) {
            Log.d("图片分享", e.toString())
        }
    }

你可能感兴趣的:(扫码 项目总结-V1.3.0)