好吧想际,其實(shí)V1.3.5 已經(jīng)在上個(gè)月下旬做完了,現(xiàn)在才來寫總結(jié)溪厘,但凡勤快點(diǎn)都不會(huì)拖到現(xiàn)在胡本,害呀。這里標(biāo)題是V1.3.0畸悬,但其實(shí)這個(gè)版本做出來沒有上線侧甫,直接上的V1.3.5。所以就一并總結(jié)了蹋宦。
V1.3相對(duì)于V1.2主要增加的功能就是創(chuàng)建二維碼和收藏披粟,但其實(shí)關(guān)于創(chuàng)建二維碼的代碼很簡(jiǎn)單,本篇文章的主要內(nèi)容也不是單純的創(chuàng)建冷冗。下邊是本文的主要內(nèi)容:
可以看到總結(jié)的點(diǎn)比較散守屉,隨便一個(gè)單獨(dú)拿出來都可以水一篇文章了,文章雖然水蒿辙,但都是干貨哈
一 . 單選框的定制和使用RadioGroup和RadioButton
我們知道谷歌提供了一個(gè)單選控件RadioButton拇泛,這個(gè)通常是需要和RadioGroup一起使用的。
多個(gè)RadioButton置于一個(gè)RadioGroup思灌,然后通過RadioGroup去控制其中的RadioButton俺叭。
RadioGroup是繼承于LinearLayout的,所以用起來也很順手习瑰。
1.布局
這里我需要的是三個(gè)橫向排布的單選框绪颖,所以布局如下:
<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加密方式
其實(shí)就是一個(gè)ViewGroup設(shè)置方向?yàn)闄M向后,排布幾個(gè)radiobutton,就這么簡(jiǎn)單柠横,當(dāng)然還有其他屬性上的設(shè)置窃款,等會(huì)慢慢講,現(xiàn)在給每個(gè)控件加上id就可以了
2.改變樣式
我們知道牍氛,原本的radio button是非常丑的晨继,就像這個(gè):
要是直接這么上去,產(chǎn)品和ui都要罵娘了搬俊,所以要設(shè)計(jì)radio button的樣式紊扬,就像這樣:
接下來請(qǐng)參考上邊的布局代碼一起閱讀。
第一步
去掉radio button右邊的小圓點(diǎn)唉擂,這個(gè)非常簡(jiǎn)單:只需要一個(gè)屬性
android:button="@null"
當(dāng)然也可以通過這個(gè)屬性設(shè)置這個(gè)button的樣式餐屎,我們不需要就直接去掉。
第二步
設(shè)置選擇選中和未選中的邊框顏色
一個(gè)個(gè)來玩祟,邊框?qū)嶋H上就是背景腹缩,背景一般采用drawable來繪制,所以新建一個(gè)命名為bg_radio的selector drawable文件空扎,
不懂drawable的同學(xué)移步:http://www.reibang.com/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就不給了谭胚,其實(shí)這里的bg_edit_text_normal和bg_edit_text_focused饵史,就是一個(gè)圓角,1dp寬邊框的不同顏色的樣式。關(guān)鍵點(diǎn)其實(shí)的這里item對(duì)應(yīng)的state_checked屬性橙困,指定了選中和未選中應(yīng)該用什么樣式填物。
然后在radio button 中設(shè)置屬性:
android:background="@drawable/bg_radio"
第三步
設(shè)置選擇選中和未選中的字體顏色
同樣的步驟毛萌,新建一個(gè)命名為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>
最后設(shè)置radio button屬性:
android:textColor="@drawable/bg_radio_text"
現(xiàn)在樣式就改完了,感覺還行甫煞,接下來實(shí)現(xiàn)功能菇曲。
3.獲取選擇的內(nèi)容
我們要點(diǎn)擊使用時(shí),需要獲取一下當(dāng)前選了什么
非常簡(jiǎn)單抚吠,就使用radio group的一條屬性就可以了:
when (wifi_encryption_radio.checkedRadioButtonId) {
R.id.radio1 -> {
encryption = "WPA"
}
R.id.radio2 -> {
encryption = "WPE"
}
R.id.radio3 -> {//沒有加密類型
password = ""
encryption = ""
}
}
4.給定默認(rèn)值
本來radio group默認(rèn)是什么都不選的常潮,但是如果需要默認(rèn)選擇一個(gè),可以這么設(shè)置:
//默認(rèn)選擇第一個(gè)
wifi_encryption_radio.check(R.id.radio1)
5.選擇監(jiān)聽
有的小伙伴又有需求了楷力,說之前獲取選擇內(nèi)容只是用戶最終選擇的結(jié)果喊式,但是我想要用戶每次選擇我都知道選了哪個(gè),怎么辦
這個(gè)時(shí)候設(shè)置給radio group設(shè)置一下選擇監(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的布局
就像這個(gè)樣子:
在點(diǎn)擊刪除后萧朝,原本的收藏按鈕消失岔留,取而代之的是刪除按鈕,這該怎么實(shí)現(xiàn)呢检柬?
非常簡(jiǎn)單:在adapter中添加一個(gè)全局變量
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
}
最后在需要改變布局的時(shí)候這樣做:
fun changeDeleteLayout() {
mAdapter.isShowDeleteBtn=! mAdapter.isShowDeleteBtn
//item發(fā)生改變 重新繪制item布局
mAdapter.notifyItemRangeChanged(0,mData.size)
}
這里的notifyItemRangeChanged
非常關(guān)鍵献联,它表示一定范圍內(nèi)item內(nèi)容修改,需要重新繪制。如果把范圍擴(kuò)大到全部里逆,則會(huì)重新繪制所有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);
}
在附贈(zèng)一個(gè)刪除item的效果,當(dāng)你從mData中移除了某一項(xiàng)后原押,
請(qǐng)調(diào)用notifyItemRemoved而不是notifyDataSetChanged
fun removeItem(position: Int){
mData.remove(mData[position])
mAdapter.notifyItemRemoved(position)
}
mAdapter.notifyItemRemoved(position)
表示某個(gè)位置的內(nèi)容被移除了胁镐,這時(shí)會(huì)有動(dòng)畫效果體現(xiàn)出來(這個(gè)動(dòng)畫也可以自定義)
三 . 軟鍵盤
在我們程序中,難免會(huì)遇到使用輸入框诸衔,這個(gè)時(shí)候就會(huì)彈出軟鍵盤盯漂,那么關(guān)于軟鍵盤和輸入框的一些注意點(diǎn)我就寫在這了。
1.軟鍵盤的彈出方式
彈出方式分別是:鍵盤覆蓋頁面笨农,鍵盤擠占頁面布局就缆,鍵盤頂起整個(gè)頁面(不覆蓋,不擠占)磁餐,自定義方式(監(jiān)聽根布局Layout 的Size改變违崇,獲得軟鍵盤高度阿弃,動(dòng)態(tài)修改頁面)诊霹,等等
參考:https://www.cnblogs.com/jerehedu/p/4194125.html
處理方式:項(xiàng)目的AndroidManifest.xml文件中界面對(duì)應(yīng)的<activity>里修改屬性
例子:這會(huì)使屏幕整體上移
android:windowSoftInputMode="stateVisible|adjustResize"
關(guān)于windowSoftInputMode
的一些知識(shí)點(diǎn):
activity主窗口與軟鍵盤的交互模式,可以用來避免輸入法面板遮擋問題渣淳。
它的設(shè)置必須是下面列表中的一個(gè)值脾还,或一個(gè)”state…”值加一個(gè)”adjust…”值的組合:(值之間采用 | 分開)
列表:
各值的含義:
【A】stateUnspecified:軟鍵盤的狀態(tài)并沒有指定,系統(tǒng)將選擇一個(gè)合適的狀態(tài)或依賴于主題的設(shè)置
【B】stateUnchanged:當(dāng)這個(gè)activity出現(xiàn)時(shí)入愧,軟鍵盤將一直保持在上一個(gè)activity里的狀態(tài)鄙漏,無論是隱藏還是顯示
【C】stateHidden:用戶選擇activity時(shí),軟鍵盤總是被隱藏
【D】stateAlwaysHidden:當(dāng)該Activity主窗口獲取焦點(diǎn)時(shí)棺蛛,軟鍵盤也總是被隱藏的
【E】stateVisible:軟鍵盤通常是可見的
【F】stateAlwaysVisible:用戶選擇activity時(shí)怔蚌,軟鍵盤總是顯示的狀態(tài)
【G】adjustUnspecified:默認(rèn)設(shè)置,通常由系統(tǒng)自行決定是隱藏還是顯示
【H】adjustResize:該Activity總是調(diào)整屏幕的大小以便留出軟鍵盤的空間
【I】adjustPan:當(dāng)前窗口的內(nèi)容將自動(dòng)移動(dòng)以便當(dāng)前焦點(diǎn)從不被鍵盤覆蓋和用戶能總是看到輸入內(nèi)容的部分
2.軟鍵盤彈起收回的監(jiān)聽
Android系統(tǒng)并沒有直接提供監(jiān)聽鍵盤彈起收回的方法旁赊,只能通過一些特殊的方式來監(jiān)聽桦踊。比如下邊這種,通過監(jiān)聽Layout高度的改變终畅,來確認(rèn)鍵盤是否彈起收回籍胯。有一個(gè)工具類如下:
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 //紀(jì)錄根視圖的顯示高度
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ā)生改變或者視圖樹中的某個(gè)視圖的可視狀態(tài)發(fā)生改變
rootView.getViewTreeObserver().addOnGlobalLayoutListener(object :
ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
//獲取當(dāng)前根視圖在屏幕上顯示的大小
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
}
}
})
}
}
用法:
//設(shè)置鍵盤的監(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.指定輸入框的輸入方式
這個(gè)其實(shí)是edittext的屬性,修改inputType妖爷。
例子:editText.inputType = InputType.TYPE_CLASS_NUMBER
這個(gè)就表示輸入框只想要純數(shù)字蝶涩,其他的輸入類型可以自己研究下。
輸入框的錯(cuò)誤提示:editText.error = "輸入內(nèi)容不可為空"
聚焦和非聚焦ui樣式的改變(通過drawable)
https://blog.csdn.net/tracydragonlxy/article/details/100558915
其他關(guān)于輸入框的知識(shí)點(diǎn)都很簡(jiǎn)單,需要的時(shí)候一搜就可以了。
最后推薦一個(gè)還不錯(cuò)的自定義輸入框:
https://github.com/wrapp-archive/floatlabelededittext
四 . 一個(gè)非常好用的時(shí)間選擇器
https://github.com/JZXiang/TimePickerDialog
五 .圖片保存和分享
1.圖片保存
圖片保存通常就是將bitmap在手機(jī)上保存為jpg,png等格式圖片绿聘。
這里有幾個(gè)注意點(diǎn):
1.文件讀寫權(quán)限
2.判斷手機(jī)是否有外部存儲(chǔ)卡暗挑,若沒有則只能保存在App內(nèi)部存儲(chǔ)
3.圖片保存后并不會(huì)直接在相冊(cè)里顯示,而是要發(fā)出廣播通知系統(tǒng)刷新媒體庫
我的一個(gè)工具類在這里斜友,比較清晰炸裆,可以作為一個(gè)參考,后期再加上接口回調(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對(duì)象保存到本地相冊(cè)
*
**/
class SaveImageUnit {
//讀寫權(quán)限!
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)建一個(gè)
private fun checkDir() {
val state = Environment.getExternalStorageState()
if (Environment.MEDIA_MOUNTED == state) {
//如果有外部?jī)?nèi)存卡可進(jìn)行讀寫 則建在外部?jī)?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)部存儲(chǔ)上
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")
//準(zhǔn)備好發(fā)出廣播 通知系統(tǒng)媒體 刷新相冊(cè) 在相冊(cè)中顯示出圖片
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)
//判斷信息識(shí)別碼 根據(jù)不同的識(shí)別碼進(jìn)行不同動(dòng)作
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緩存目錄準(zhǔn)備分享 耗時(shí)操作 請(qǐng)最好在異步線程調(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分享出去惯殊。
這里有兩個(gè)坑:
首先保存的位置應(yīng)該是App的緩存文件夾(系統(tǒng)隨時(shí)回收),不會(huì)占用過多的空間也殖,產(chǎn)生垃圾文件土思。
其次,如果保存在了緩存文件夾忆嗜,則系統(tǒng)不允許App直接將文件uri暴露出去己儒,而要通過FileProvider
file provider用法:
http://www.reibang.com/p/f0b2cf0e0353
然后關(guān)于分享文件可以看上邊的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/*" //設(shè)置分享內(nèi)容的類型:圖片
try {
startActivity(
Intent.createChooser(
shareIntent,
getString("Share")
)
)
} catch (e: Exception) {
Log.d("圖片分享", e.toString())
}
}