序言
接下来该说说后面几个功能了:调用系统相机拍照、心情历程记录、历史心情历程查看。前面几个功能有兴趣的可以看看前面一篇文章:【基于高德api与和风api的天气记录App】一个集成的小demo,主要用于记录学习过程中设计及构思——上
app功能拆解
调用系统相机拍照
android 7.0之后的调用需要使用provider,不然的话容易发生报错闪退的情况,而Android 7.0之前则还是可以随便调用相机进行拍照的,这里只讲讲Android 7.0之后改怎么搞。
首先,show the fucking code:
capturebtn.setOnClickListener { btn ->
val path = externalCacheDir?.absolutePath.toString() + "Camera/"
val direcFile = File(path)
if (!direcFile.exists()) {
direcFile.mkdirs()
}
mFile = File(path, "${System.currentTimeMillis()}_home.jpg")
if (!mFile.exists()) {
mFile.createNewFile()
}
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK and Intent.FLAG_GRANT_READ_URI_PERMISSION)
putExtra(MediaStore.EXTRA_OUTPUT, FileProvider.getUriForFile(this@TempActivity, "com.example.weathertest.fileprovider", mFile))
}
startActivityForResult(intent, 2)
}
............
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path
name="external_files"
path="."/>
</paths>
..........
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.example.weathertest.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
这里定义了一个provider用来转换uri给系统相机使用,其他的倒没有什么变化。
后面从系统相机界面回来的时候逻辑也很简单,先判断下file的length,是否存在数据,存在,咱就展示,不存在,那就直接return得了。然后把 uri 转递给展示拍照数据的界面即可,show the fucking code:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == 2) {
if (mFile.length() <= 0){
mFile.delete()
Log.e("gaorui", "onActivityResult - return")
return
}
captureFragment = CaptureFragment()
fragmentMgr = supportFragmentManager
captureFragment?.let {
it.arguments = Bundle().apply {
putString("fromFile", Uri.fromFile(mFile).toString())
}
fragmentMgr!!.beginTransaction()
.add(R.id.temp_manager, it, "captureFragment")
.commitNowAllowingStateLoss()
}
}
}
进入拍照数据返回界面后,把传递过来的uri 存储在list中,然后构建个 recyclerView 用来展示,因为在此界面,还是支持继续调用相机拍照的,所以使用单一的imageView的话不太适合,另外拍几张也不固定,最终选择了recyclerView。list数组中一直存有一个图片,用来点击跳转拍照的,仿照微信发朋友圈的界面,这里没有设计跳转进入图库选择资源,全都是进入系统相机拍照,show the fucking code:
............
val bundle = arguments
val fileUri = Uri.parse(bundle?.getString("fromFile"))
fileList.add(TempCaptureImageData(true, fileUri, null))
fileList.add(TempCaptureImageData(false, null, R.drawable.mango_pic))
............
temp_capture_fragment_recyclerview.setOnItemClickListener(object :RecyclerViewExt.OnItemClickListener{
override fun onItemClick(
parent: RecyclerView.Adapter<*>?,
vh: RecyclerView.ViewHolder?,
position: Int
) {
Toast.makeText(context, " onItemClick - ${vh?.position}", Toast.LENGTH_SHORT).show()
if (!fileList[position].isUri) {
val path = activity?.externalCacheDir?.absolutePath.toString() + "Camera/"
val direcFile = File(path)
if (!direcFile.exists()) {
direcFile.mkdirs()
}
mFile = File(path, "${System.currentTimeMillis()}_home.jpg")
if (!mFile.exists()) {
mFile.createNewFile()
}
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
putExtra(MediaStore.EXTRA_OUTPUT,
activity?.let {
FileProvider.getUriForFile(it, "com.example.weathertest.fileprovider", mFile)
})
}
startActivityForResult(intent, 3)
}
}
override fun onItemLongClick(vh: RecyclerView.ViewHolder?, position: Int) {
Toast.makeText(context, " onItemLongClick", Toast.LENGTH_SHORT).show()
}
})
此界面仿照微信朋友圈发表界面,也定义了一个发表button,然后由此button调用宿主activity的方法进行更新recyclerView,展示数据,show the fucking code:
capture_fragment_broadcast.setOnClickListener {
(activity as TempActivity).sendTextOrImageFromFragment(fileList, capture_fragment_want_to_say.text.toString())
}
这个地方就牵扯到了数据库了,这里没有使用花钱的云,花钱的都不香,hhh,这里使用Android自带的sqlite数据库进行存储,后面有钱再考虑把数据迁移存储到云上,先专注流程,凑合看。
在进入天气界面的时候,oncreate中初始化一个数据库FriendCircle,然后再从数据库中拿出数据,recyclerview更新数据。
先来看看数据库的流程吧,首先继承SQLiteOpenHelper进行建表等操作,然后创建个mangaer 来管理sqlite的CURD操作,有点子SQL基础的话这块还是蛮简单 的。show the fucking code:
class MyDataBaseHelper(mContext: Context, name:String, factory:SQLiteDatabase.CursorFactory?, version:Int)
: SQLiteOpenHelper(mContext, name, factory, version) {
val mConTxt = mContext
val CREATE_BASE:String = "create table FriendCircle (" +
"id integer primary key autoincrement," +
"author BLOB," +
"dateOrName text," +
"wantToSay text," +
"groupkey integer DEFAULT 0," +
"wantShowPic BLOB)"
override fun onCreate(db: SQLiteDatabase?) {
db?.let {
it.execSQL(CREATE_BASE)
Toast.makeText(mConTxt, "create success", Toast.LENGTH_SHORT).show()
}
}
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
db?.let {
it.execSQL("drop table if exists FriendCircle")
onCreate(it)
}
}
}
.............
class MyDataBaseManager {
companion object {
fun setDB(base: SQLiteDatabase) {
sqlLitebase = base
}
fun closeDB() {
Log.e("gaorui", "MyDataBaseManager - closeDB ")
sqlLitebase.close()
}
fun addDataToDB(author:ByteArray, date:String, wantToSay: String?, addPic:ByteArray?, groupOrNot:Int?) : Long {
val values = ContentValues().let {
it.put("author", author)
it.put("dateOrName", date)
it.put("wantToSay", wantToSay)
it.put("wantShowPic", addPic)
groupOrNot?.let { grouKey ->
it.put("groupkey", grouKey)
}
it
}
val result = sqlLitebase.insert("FriendCircle", null, values)
Log.e("gaorui", "addDataToDB - insert - result = $result")
return result
}
fun updateData(index:Int, groupOrNot:Int) : Int{
val values = ContentValues().let {
it.put("groupkey", groupOrNot)
it
}
val result = sqlLitebase.update("FriendCircle", values, "id = ?", arrayOf("$index"))
Log.e("gaorui", "addDataToDB - update - result = $result")
return result
}
fun selectAllData(recycleLoading:Boolean) : ArrayList<TempCaptureDataTransition>?{
try {
val cursor = if (recycleLoading) {
Log.e("gaorui", "selectAllData - first - mCurrentId = $mCurrentId")
if (mCurrentId == 0) {
sqlLitebase.query("FriendCircle", null,
null ,
null,null,null,"id desc")
} else {
sqlLitebase.query("FriendCircle", null,
"id < ?" ,
arrayOf("$mCurrentId"),null,null,"id desc")
}
} else {
sqlLitebase.query("FriendCircle", null,
null ,
null,null,null,"id desc")
}
while (cursor.moveToNext()) {
itemId = cursor.getInt(cursor.getColumnIndex("id"))
author = cursor.getBlob(cursor.getColumnIndex("author"))
dateName = cursor.getString(cursor.getColumnIndex("dateOrName"))
if (recycleLoading && itemId >= mHasInitId ) {
Log.e("gaorui", "selectAllData - itemId = $itemId, mHasInitId = $mHasInitId , so continue")
continue
}
if (mCurrentId != 0 && recycleLoading && itemId >= mCurrentId ) {
Log.e("gaorui", "selectAllData - itemId = $itemId, mCurrentId = $mCurrentId , so continue")
continue
}
wantToSay = cursor.getString(cursor.getColumnIndex("wantToSay"))
Log.e("gaorui", "selectAllData - current - wantToSay = $wantToSay")
addPic = cursor.getBlob(cursor.getColumnIndex("wantShowPic"))
groupOrNot = cursor.getInt(cursor.getColumnIndex("groupkey"))
Log.e("gaorui", "selectAllData - current - groupOrNot = $groupOrNot")
if (groupOrNot != 0) {
if (lastGroupKey == -1) {
byteListData.clear()
if (isGroupFirst) {
lastGroupKey = groupOrNot
isGroupFirst = false
transitionItem = TempCaptureDataTransitionItem(dateName, wantToSay, author)
}
byteListData.add(addPic)
Log.e("gaorui", "selectAllData - currentgroup - NEW - newSIZE = ${byteListData.size}")
} else if (lastGroupKey == groupOrNot) {
byteListData.add(addPic)
Log.e("gaorui", "selectAllData - currentgroup - add - newSIZE = ${byteListData.size}")
} else {
lastGroupKey = groupOrNot
Log.e("gaorui", "selectAllData - currentgroup - anotherAdd - size = ${byteListData.size}")
val tempData = TempCaptureDataTransition(transitionItem!!.dateOrName, transitionItem.wantToSay, transitionItem.icon, byteListData, false)
captureData.add(tempData)
byteListData.clear()
if (captureData.size >= 2) {
isGroupFirst = true
hasBlocked = true
itemId = lastItemId
break
}
transitionItem = TempCaptureDataTransitionItem(dateName, wantToSay, author)
byteListData.add(addPic)
Log.e("gaorui", "selectAllData - currentgroup - anotherAdd - newSize = ${byteListData.size}")
}
} else {
if (!isGroupFirst) {
isGroupFirst = true
Log.e("gaorui", "selectAllData - single - group - size = ${byteListData.size}")
TempCaptureDataTransition(transitionItem!!.dateOrName, transitionItem.wantToSay, transitionItem.icon, byteListData, false)
captureData.add(tempData)
byteListData.clear()
if (captureData.size >= 2) {
itemId = lastItemId
hasBlocked = true
break
}
}
lastGroupKey = -1
val listData = ArrayList<ByteArray?>()
listData.add(addPic)
val tempData = TempCaptureDataTransition(dateName, wantToSay, author, listData, false)
captureData.add(tempData)
Log.e("gaorui", "selectAllData - single - add - newSize = ${byteListData.size}")
if (captureData.size >= 2) {
hasBlocked = true
break
}
}
lastItemId = itemId
hasBlocked = false
}
Log.e("gaorui", "selectAllData - current - mCurrentId = $mCurrentId - isLast = ${cursor.isLast}")
if (recycleLoading && (hasBlocked || !cursor.moveToNext())) {
mCurrentId = itemId
Log.e("gaorui", "selectAllData - current - mCurrentId = $mCurrentId")
}
if (!recycleLoading && (hasBlocked || !cursor.moveToNext())) {
mHasInitId = itemId
Log.e("gaorui", "selectAllData - init - mHasInitId = $mHasInitId")
}
if (!isGroupFirst) {
Log.e("gaorui", "selectAllData - end - addAll - size = ${byteListData.size}")
val tempData = TempCaptureDataTransition(transitionItem!!.dateOrName, transitionItem.wantToSay, transitionItem.icon, byteListData, false)
captureData.add(tempData)
}
cursor.close()
Log.e("gaorui", "selectAllData - size - captureData = ${captureData.size}")
return captureData
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
}
}
建表的类没啥好说的,网上一大堆,manager管理类倒是得说说了。这里为了简化工作量,一个demo要什么鼻子眼睛,刨除了删除的功能,保留了update、insert以及最重要的query功能,update和insert没啥好说的,可以参考其他大佬的文章,这里query的设计我说下思路:
1、判断是否属于recycler的滑动查询更新,因为我这边设计的查询更新有两种,一就是初始化的时候更新,读出2个item,二就是滑动recycler进行的查询更新,再次读出2个item。如果不属于recycler那么就读一种url,如果属于recycler滑动查询,那么就读另外一种url,两种url都是倒叙查询
2、记录id。当第一次的初始化更新完毕,记录下initID,当后面发生了recyler滑动查询的时候,也记录下ecyler滑动id
3、判断是否加入list。当目前item 的id 小于initID以及recyler滑动id的时候,就加入待显示的list,直到数据库末尾或者达到2的max值为止。
4、临近两次都是以group的形式存在。这种情况要区分开本次读的数据和上次拿到的item的数据是不一致的,千万不能混淆,不然就会发生数据被覆盖的情况。
5、临近两次,一次是group,一次是single。这种情况要区分下single在group前,还是在group后的情况,要考虑两种,不能漏了。
6、来回多试试,查询的功能也就ok了。
感觉这个功能有点子像图库的不同展示列表,包括了单一的照片、连拍、gif、或者视频等,当然目前做的这个功能比不上图库,但是可以以小见大。
后面如果要优化的话,可以考虑数据库分表建索引,android本身自带的sqlite操作语句有点局限性,考虑使用SQL语句,调用db的execSQL方法,进行union或者笛卡尔链接等。后面也考虑加上数据库图片删除的功能,有兴趣再说。
这里把数据库的东西放到这里来叙述刚刚好,因为接下来还是和数据库打交道了。
心情历程记录
这里的记录界面,就是上一步用来展示调用系统相机拍照数据的界面。分为text、recylerView、button,三类。当text该写的话语写完,图片也拍完了,点击了button 发表,这里又分为两步了,一是更新到当前recycler中,二是插入到数据库中。更新到recyler中没啥好说的,就正常加list,无论是调用notifyItemchanged也好,还是notifyiteminsert也罢,调用就行了,主要是第二步的插入数据库。这个时候要区分是否是group的图片,如果是单一照片的话,直接调用manager类的add操作即可,如果是group图片的话,首先将第一张图片优先插入数据库,返回插入id之后再次更新第一张图片的group key,然后依次将list数据结合groupkey依次插入数据库中即可。ok,show the fucking code:
thread {
val imageList = ArrayList<Bitmap?>()
for (tmpData in arrayList) {
if (!tmpData.isUri){
continue
}
val bitm = UriToBitmap(tmpData)?.let {
imageList.add(it)
it
}
}
Log.e("ymc", "sendTextOrImageFromFragment - toDayDate = $toDayDate , wether = ${temp_placetemp_weatherkind.text}")
val tempData = TempCaptureData(false, toDayDate + " ${temp_placetemp_weatherkind.text} - $placeName", sayWord, BitmapFactory.decodeResource(resources, R.drawable.mango_pic), imageList, false)
GlobalScope.launch {
val authorBit = BitmapFactory.decodeResource(resources, R.drawable.mango_pic)
var addpic :Bitmap?
val authorbyteOutputStream = ByteArrayOutputStream()
val picbyteOutputStream = ByteArrayOutputStream()
authorBit.compress(Bitmap.CompressFormat.PNG, 100, authorbyteOutputStream)
Log.e("ymc", "sendTextOrImageFromFragment - imageList.size = ${imageList.size}")
if (imageList.size > 1) {
var dataresult:Long? = null
for ((index, pic) in imageList.withIndex()) {
picbyteOutputStream.reset()
pic?.compress(Bitmap.CompressFormat.PNG, 100, picbyteOutputStream)
if (index == 0) {
dataresult = MyDataBaseManager.addDataToDB(authorbyteOutputStream.toByteArray(), tempData.dateOrName, sayWord, picbyteOutputStream.toByteArray(), null)
Log.e("ymc", "sendTextOrImageFromFragment - first insert = $dataresult")
if (dataresult > 0) {
val firstresult = MyDataBaseManager.updateData(dataresult.toInt(), dataresult.toInt())
Log.e("ymc", "sendTextOrImageFromFragment - first update = $firstresult")
}
} else{
val otherresult = MyDataBaseManager.addDataToDB(authorbyteOutputStream.toByteArray(), tempData.dateOrName, sayWord, picbyteOutputStream.toByteArray(), dataresult?.toInt())
Log.e("ymc", "sendTextOrImageFromFragment - other insert = $otherresult")
}
}
} else {
picbyteOutputStream.reset()
addpic = imageList[0]
addpic?.compress(Bitmap.CompressFormat.PNG, 100, picbyteOutputStream)
val dataresult = MyDataBaseManager.addDataToDB(authorbyteOutputStream.toByteArray(), tempData.dateOrName, sayWord, picbyteOutputStream.toByteArray(), null)
Log.e("ymc", "sendTextOrImageFromFragment - single insert = $dataresult")
}
}
if (mIsFirstEvent) {
mIsFirstEvent = false
tempCaptureArrayList.removeFirst()
}
tempCaptureArrayList.add(0, tempData)
runOnUiThread {
tempCaptureAdapter.notifyDataSetChanged()
place_temp_recyclerView_selfinfo.layoutManager?.scrollToPosition(0)
}
}
到这里的话,基本上是ok了。
历史心情历程查看
此功能的逻辑在上方也有叙述,这里再赘述下。这个历史查看界面,目前布局在查询天气界面的下方,当首次进入的时候,如果数据库中没有数据的话,显示自定义的一个无效item,如果数据库中存在数据,那么读取2条item用来展示,主要逻辑就是这样,show the fucking code:
fun tryLoadData(recycleLoad:Boolean) {
GlbalScope.aunch(Dispatchers.IO) {
/* just for sleep,so we can see progressCircle. */
try {
Thread.sleep(1000)
}catch (e: Exception) {
e.printStackTrace()
}
val tempList = yDataBaseManager.selectAllData(recycleLoad)
if (mInLoadingNext) {
mInLoadingNext = false
removeLoaItem()
tempLst?.let {
if (it.size == 0) {
HasReachedEnd = true
}
}
}
if (tempList != null) {
tempList.forEach { temptransition ->
var tempBit : itmap? = null
val listtemp = ArrayList<Bitmap?>()
temptranstion.wantShowImage?.let {
it.forEach {
tempBit = null
it?.let {
tempBit = BitmaFactory.decodeByteArray(it, 0, it.size)
}
temBit?.let {
listtemp.add(it)
}
}
val authorBit = BitapFactory.decodeByteArray(temptransition.icon, 0, temptransition.icon.size)
val tempData = TempCaptureData(false, temptransiton.dateOrName, temptransition.wantToSay, authorit, listtemp, false)
if (tempCaptureArrayList.size == 1 && tempCaptureArayList[0].isRemoved) {
tempCaptureArrayList.removeAt(0)
}
tempCaptueArrayList.add(tempata)
mIsFirstEet = false
mDataReady = true
place_temp_recyclerViewselfinfo.pst {
tempCaptureAapter.notifyDatSetChanged()
}
}
}
if (reycleLoad) {
runOnUihread {
tepCaptureAdapter.notifyDataStChanged()
}
}
}
}
到这里的话,整个demo app的功能就拆解完了,代码书写比较粗糙,专注于流程的设计,其他凑合看看,不对的地方欢迎指导
源码地址:
https://gitee.com/kanecong/weather-test