好吧,其實V1.3.5 已經(jīng)在上個月下旬做完了,現(xiàn)在才來寫總結(jié),但凡勤快點都不會拖到現(xiàn)在,害呀。這里標題是V1.3.0,但其實這個版本做出來沒有上線,直接上的V1.3.5。所以就一并總結(jié)了。
V1.3相對于V1.2主要增加的功能就是創(chuàng)建二維碼和收藏,但其實關于創(chuàng)建二維碼的代碼很簡單,本篇文章的主要內(nèi)容也不是單純的創(chuàng)建。下邊是本文的主要內(nèi)容:

可以看到總結(jié)的點比較散,隨便一個單獨拿出來都可以水一篇文章了,文章雖然水,但都是干貨哈
一 . 單選框的定制和使用RadioGroup和RadioButton
我們知道谷歌提供了一個單選控件RadioButton,這個通常是需要和RadioGroup一起使用的。
多個RadioButton置于一個RadioGroup,然后通過RadioGroup去控制其中的RadioButton。
RadioGroup是繼承于LinearLayout的,所以用起來也很順手。
1.布局
這里我需要的是三個橫向排布的單選框,所以布局如下:
<RadioGroup
android:id="@+id/wifi_encryption_radio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="30dp"
android:layout_marginVertical="10dp"
android:orientation="horizontal"
android:visibility="gone">
<RadioButton
android:id="@+id/radio1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/bg_radio"
android:button="@null"
android:gravity="center"
android:padding="8dp"
android:text="@string/WPA_WPA2"
android:textColor="@drawable/bg_radio_text"
android:textSize="15sp" />
<RadioButton
android:id="@+id/radio2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:layout_weight="1"
android:background="@drawable/bg_radio"
android:button="@null"
android:gravity="center"
android:padding="8dp"
android:text="@string/WPE"
android:textColor="@drawable/bg_radio_text"
android:textSize="15sp" />
<RadioButton
android:id="@+id/radio3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/bg_radio"
android:button="@null"
android:gravity="center"
android:padding="8dp"
android:text="@string/no_encryption"
android:textColor="@drawable/bg_radio_text"
android:textSize="15sp" />
</RadioGroup>
主要目的就是讓用戶選擇wifi加密方式
其實就是一個ViewGroup設置方向為橫向后,排布幾個radiobutton,就這么簡單,當然還有其他屬性上的設置,等會慢慢講,現(xiàn)在給每個控件加上id就可以了
2.改變樣式
我們知道,原本的radio button是非常丑的,就像這個:

要是直接這么上去,產(chǎn)品和ui都要罵娘了,所以要設計radio button的樣式,就像這樣:

接下來請參考上邊的布局代碼一起閱讀。
第一步
去掉radio button右邊的小圓點,這個非常簡單:只需要一個屬性
android:button="@null"當然也可以通過這個屬性設置這個button的樣式,我們不需要就直接去掉。
第二步
設置選擇選中和未選中的邊框顏色
一個個來,邊框?qū)嶋H上就是背景,背景一般采用drawable來繪制,所以新建一個命名為bg_radio的selector drawable文件,
不懂drawable的同學移步:http://www.itdecent.cn/p/f01b1af15a88
內(nèi)容如下:
android:background="@drawable/bg_radio"
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:state_checked="false"
android:drawable="@drawable/bg_edit_text_normal" />
<item
android:state_checked="true"
android:drawable="@drawable/bg_edit_text_focused" />
</selector>
具體的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文件,
內(nèi)容如下:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true" android:color="@color/theme_color" />
<item android:state_checked="false" android:color="@color/gray_text" />
</selector>
最后設置radio button屬性:
android:textColor="@drawable/bg_radio_text"
現(xiàn)在樣式就改完了,感覺還行,接下來實現(xiàn)功能。
3.獲取選擇的內(nèi)容
我們要點擊使用時,需要獲取一下當前選了什么
非常簡單,就使用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.選擇監(jiān)聽
有的小伙伴又有需求了,說之前獲取選擇內(nèi)容只是用戶最終選擇的結(jié)果,但是我想要用戶每次選擇我都知道選了哪個,怎么辦
這個時候設置給radio group設置一下選擇監(jiān)聽就可以了:
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統(tǒng)一修改所有item的布局
就像這個樣子:

在點擊刪除后,原本的收藏按鈕消失,取而代之的是刪除按鈕,這該怎么實現(xiàn)呢?
非常簡單:在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發(fā)生改變 重新繪制item布局
mAdapter.notifyItemRangeChanged(0,mData.size)
}
這里的notifyItemRangeChanged非常關鍵,它表示一定范圍內(nèi)item內(nèi)容修改,需要重新繪制。如果把范圍擴大到全部,則會重新繪制所有item。
源碼在這里:感興趣可以看看注釋說明
/**
* Notify any registered observers that the <code>itemCount</code> items starting at
* position <code>positionStart</code> have changed.
* Equivalent to calling <code>notifyItemRangeChanged(position, itemCount, null);</code>.
*
* <p>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.</p>
*
* @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中移除了某一項后,
請調(diào)用notifyItemRemoved而不是notifyDataSetChanged
fun removeItem(position: Int){
mData.remove(mData[position])
mAdapter.notifyItemRemoved(position)
}
mAdapter.notifyItemRemoved(position)表示某個位置的內(nèi)容被移除了,這時會有動畫效果體現(xiàn)出來(這個動畫也可以自定義)
三 . 軟鍵盤
在我們程序中,難免會遇到使用輸入框,這個時候就會彈出軟鍵盤,那么關于軟鍵盤和輸入框的一些注意點我就寫在這了。
1.軟鍵盤的彈出方式
彈出方式分別是:鍵盤覆蓋頁面,鍵盤擠占頁面布局,鍵盤頂起整個頁面(不覆蓋,不擠占),自定義方式(監(jiān)聽根布局Layout 的Size改變,獲得軟鍵盤高度,動態(tài)修改頁面),等等
參考:https://www.cnblogs.com/jerehedu/p/4194125.html
處理方式:項目的AndroidManifest.xml文件中界面對應的<activity>里修改屬性
例子:這會使屏幕整體上移
android:windowSoftInputMode="stateVisible|adjustResize"
關于windowSoftInputMode的一些知識點:
activity主窗口與軟鍵盤的交互模式,可以用來避免輸入法面板遮擋問題。
它的設置必須是下面列表中的一個值,或一個”state…”值加一個”adjust…”值的組合:(值之間采用 | 分開)
列表:
各值的含義:
【A】stateUnspecified:軟鍵盤的狀態(tài)并沒有指定,系統(tǒng)將選擇一個合適的狀態(tài)或依賴于主題的設置
【B】stateUnchanged:當這個activity出現(xiàn)時,軟鍵盤將一直保持在上一個activity里的狀態(tài),無論是隱藏還是顯示
【C】stateHidden:用戶選擇activity時,軟鍵盤總是被隱藏
【D】stateAlwaysHidden:當該Activity主窗口獲取焦點時,軟鍵盤也總是被隱藏的
【E】stateVisible:軟鍵盤通常是可見的
【F】stateAlwaysVisible:用戶選擇activity時,軟鍵盤總是顯示的狀態(tài)
【G】adjustUnspecified:默認設置,通常由系統(tǒng)自行決定是隱藏還是顯示
【H】adjustResize:該Activity總是調(diào)整屏幕的大小以便留出軟鍵盤的空間
【I】adjustPan:當前窗口的內(nèi)容將自動移動以便當前焦點從不被鍵盤覆蓋和用戶能總是看到輸入內(nèi)容的部分
2.軟鍵盤彈起收回的監(jiān)聽
Android系統(tǒng)并沒有直接提供監(jiān)聽鍵盤彈起收回的方法,只能通過一些特殊的方式來監(jiān)聽。比如下邊這種,通過監(jiān)聽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()
//監(jiān)聽視圖樹中全局布局發(fā)生改變或者視圖樹中的某個視圖的可視狀態(tài)發(fā)生改變
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
}
//根視圖顯示高度沒有變化,可以看作軟鍵盤顯示/隱藏狀態(tài)沒有改變
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
}
}
})
}
}
用法:
//設置鍵盤的監(jiān)聽
SoftKeyBoardListener.setListener(activity, object : OnSoftKeyBoardChangeListener {
override fun keyBoardShow(height: Int) {
Log.d("鍵盤監(jiān)聽", "彈起")
}
override fun keyBoardHide(height: Int) {
Log.d("鍵盤監(jiān)聽", "回收")
}
})
3.指定輸入框的輸入方式
這個其實是edittext的屬性,修改inputType。
例子:editText.inputType = InputType.TYPE_CLASS_NUMBER
這個就表示輸入框只想要純數(shù)字,其他的輸入類型可以自己研究下。
輸入框的錯誤提示:editText.error = "輸入內(nèi)容不可為空"
聚焦和非聚焦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內(nèi)部存儲
3.圖片保存后并不會直接在相冊里顯示,而是要發(fā)出廣播通知系統(tǒng)刷新媒體庫
我的一個工具類在這里,比較清晰,可以作為一個參考,后期再加上接口回調(diào),將保存結(jié)果成功或失敗回調(diào)出去。
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
//檢查保存的文件夾是否存在 不存在則創(chuàng)建一個
private fun checkDir() {
val state = Environment.getExternalStorageState()
if (Environment.MEDIA_MOUNTED == state) {
//如果有外部內(nèi)存卡可進行讀寫 則建在外部內(nèi)存卡上
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內(nèi)部存儲上
appDir =
File(context.filesDir.absolutePath + File.separator + context.getText(R.string.app_name))
if (!appDir!!.exists()) {
appDir!!.mkdir()
}
}
Log.d(TAG, "圖片文件夾地址${appDir?.absolutePath}")
}
//保存bitmap到指定文件夾 并發(fā)出廣播通知系統(tǒng)刷新媒體庫
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")
//準備好發(fā)出廣播 通知系統(tǒng)媒體 刷新相冊 在相冊中顯示出圖片
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)
//判斷信息識別碼 根據(jù)不同的識別碼進行不同動作
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緩存目錄準備分享 耗時操作 請最好在異步線程調(diào)用
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作為分享內(nèi)容使用intent分享出去。
這里有兩個坑:
首先保存的位置應該是App的緩存文件夾(系統(tǒng)隨時回收),不會占用過多的空間,產(chǎn)生垃圾文件。
其次,如果保存在了緩存文件夾,則系統(tǒng)不允許App直接將文件uri暴露出去,而要通過FileProvider
file provider用法:
http://www.itdecent.cn/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/*" //設置分享內(nèi)容的類型:圖片
try {
startActivity(
Intent.createChooser(
shareIntent,
getString("Share")
)
)
} catch (e: Exception) {
Log.d("圖片分享", e.toString())
}
}