Android自定義view實(shí)現(xiàn)滾動(dòng)選擇控件詳解
前言
上篇文章通過(guò)一個(gè)有header和footer的滾動(dòng)控件(Viewgroup)學(xué)了下MeasureSpec、onMeasure以及onLayout,接下來(lái)就用一個(gè)滾動(dòng)選擇的控件(View)來(lái)學(xué)一下onDraw的使用,并且了解下在XML自定義控件參數(shù)。
需求
這里就是一個(gè)滾動(dòng)選擇文字的控件,還是挺常見(jiàn)的,之前用別人的,現(xiàn)在選擇手撕一個(gè),核心思想如下:
1、有三層不同大小及透明度的選項(xiàng),選中項(xiàng)放在中間
2、接受一個(gè)列表的數(shù)據(jù),靜態(tài)時(shí)顯示三個(gè)值,滾動(dòng)時(shí)顯示四個(gè)值
3、滑動(dòng)會(huì)造成三個(gè)選項(xiàng)滾動(dòng),大小透明度發(fā)生變化,會(huì)有一個(gè)新的選項(xiàng)出現(xiàn)
4、滾動(dòng)一定距離后,判定是否選中一個(gè)項(xiàng)目,并觸發(fā)動(dòng)畫滾動(dòng)到選定項(xiàng) 效果圖
編寫代碼
老實(shí)說(shuō)下面寫的代碼并不好,特別是TextItem的繪制,本來(lái)是可以通過(guò)一個(gè)數(shù)學(xué)函數(shù),根據(jù)滑動(dòng)距離來(lái)映射縮放比例及位置的,如有需要可以參考這個(gè)控件,忘了是幾年前從哪里抄的了,里面對(duì)文字的控制寫的很好。
下面是我手撕的代碼:
import android.animation.ValueAnimator import android.annotation.SuppressLint import android.content.Context import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.util.AttributeSet import android.util.TypedValue import android.view.MotionEvent import android.view.View import android.view.View.MeasureSpec.* import androidx.core.animation.addListener import com.silencefly96.module_common.R import kotlin.math.abs import kotlin.math.min /** * 滾動(dòng)選擇文字控件 * 核心思想 * 1、有三層不同大小及透明度的選項(xiàng),選中項(xiàng)放在中間 * 2、接受一個(gè)列表的數(shù)據(jù),靜態(tài)時(shí)顯示三個(gè)值,滾動(dòng)時(shí)顯示四個(gè)值 * 3、滑動(dòng)會(huì)造成三個(gè)選項(xiàng)滾動(dòng),大小透明度發(fā)生變化,會(huì)有一個(gè)新的選項(xiàng)出現(xiàn) * 4、滾動(dòng)一定距離后,判定是否選中一個(gè)項(xiàng)目,并觸發(fā)動(dòng)畫滾動(dòng)到選定項(xiàng) */ class ScrollSelectView @JvmOverloads constructor( context: Context, attributeSet: AttributeSet? = null, defStyleAttr: Int = 0, ) : View(context, attributeSet, defStyleAttr){ //默認(rèn)字體透明度,大小不應(yīng)該指定,應(yīng)該根據(jù)view的高度按百分比適應(yīng) companion object{ const val DEFAULT_MAIN_TRANSPARENCY = 255 const val DEFAULT_SECOND_TRANSPARENCY = (255 * 0.5f).toInt() //三種item類型 const val ITEM_TYPE_MAIN = 1 const val ITEM_TYPE_SECOND = 2 const val ITEM_TYPE_NEW = 3 } //兩層字體大小及透明度 private var mainSize: Float = 0f private var secondSize: Float = 0f private val mainAlpha: Int private val secondAlpha: Int //主次item高度,由主item所占比例決定 private val mainItemPercent: Float private var mainHeight: Float = 0f private var secondHeight: Float = 0f //字體相對(duì)于框的縮放比例 private val textScanSize: Int //切換項(xiàng)目的y軸滑動(dòng)距離門限值 private var itemChangeYCapacity: Float //釋放滑動(dòng)動(dòng)畫效果間隔 private var afterUpAnimatorPeriod: Int //數(shù)據(jù) @Suppress("MemberVisibilityCanBePrivate") var mData: List<String>? = null //選擇數(shù)據(jù)index @Suppress("MemberVisibilityCanBePrivate") var mCurrentIndex: Int = 0 //繪制的item列表 private var mItemList: MutableList<TextItem> = ArrayList() //單次事件序列累計(jì)滑動(dòng)值 private var mScrollY: Float = 0f //上次事件縱坐標(biāo) private var mLastY: Float = 0f //畫筆 private val mPaint: Paint init { //讀取XML參數(shù),設(shè)置相關(guān)屬性 val attrArr = context.obtainStyledAttributes(attributeSet, R.styleable.ScrollSelectView) //三層字體透明度設(shè)置,未設(shè)置使用默認(rèn)值 mainAlpha = attrArr.getInteger(R.styleable.ScrollSelectView_mainAlpha, DEFAULT_MAIN_TRANSPARENCY) secondAlpha = attrArr.getInteger(R.styleable.ScrollSelectView_secondAlpha, DEFAULT_SECOND_TRANSPARENCY) textScanSize = attrArr.getInteger(R.styleable.ScrollSelectView_textScanSize, 2) //取到的值限定為dp值,需要轉(zhuǎn)換 itemChangeYCapacity = attrArr.getDimension(R.styleable.ScrollSelectView_changeItemYCapacity, 0f) itemChangeYCapacity = dp2px(context, itemChangeYCapacity).toFloat() afterUpAnimatorPeriod = attrArr.getInteger(R.styleable.ScrollSelectView_afterUpAnimatorPeriod, 300) //獲取主item所占比例,在onMeasure中計(jì)算得到主次item高度 mainItemPercent = attrArr.getFraction( R.styleable.ScrollSelectView_mainItemPercent, 1,1,0.5f) //回收 attrArr.recycle() //設(shè)置畫筆,在構(gòu)造中初始化,不要寫在onDraw里面,onDraw會(huì)不斷觸發(fā) mPaint = Paint().apply { flags = Paint.ANTI_ALIAS_FLAG style = Paint.Style.FILL //該方法即為設(shè)置基線上那個(gè)點(diǎn)究竟是left,center,還是right textAlign = Paint.Align.CENTER color = Color.BLACK } //創(chuàng)建四個(gè)TextItem mItemList.apply { add(TextItem(ITEM_TYPE_SECOND, true)) add(TextItem(ITEM_TYPE_MAIN)) add(TextItem(ITEM_TYPE_SECOND)) add(TextItem(ITEM_TYPE_NEW)) } } //設(shè)置控件的默認(rèn)大小,實(shí)際viewgroup不需要 override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) //根據(jù)父容器給定大小,設(shè)置自身寬高,wrap_content需要用到默認(rèn)值 val width = getSizeFromMeasureSpec(300, widthMeasureSpec) val height = getSizeFromMeasureSpec(200, heightMeasureSpec) //Log.e("TAG", "onMeasure: height=$height") //設(shè)置測(cè)量寬高,一定要設(shè)置,不然錯(cuò)給你看 setMeasuredDimension(width, height) //如果滑動(dòng)距離門限值沒(méi)有確定,應(yīng)該根據(jù)view大小設(shè)定,默認(rèn)高度的二分之一 itemChangeYCapacity = if(itemChangeYCapacity == 0f) height / 2f else itemChangeYCapacity //有比例計(jì)算主次item高度 mainHeight = height * mainItemPercent secondHeight = height * (1 - mainItemPercent) / 2 //得到自身高度后,就可以按比例設(shè)置字體大小了 mainSize = mainHeight / textScanSize secondSize = secondHeight / textScanSize } //根據(jù)MeasureSpec確定默認(rèn)寬高,MeasureSpec限定了該view可用的大小 private fun getSizeFromMeasureSpec(defaultSize: Int, measureSpec: Int): Int { //獲取MeasureSpec內(nèi)模式和尺寸 val mod = getMode(measureSpec) val size = getSize(measureSpec) return when (mod) { EXACTLY -> size AT_MOST -> min(defaultSize, size) else -> defaultSize //MeasureSpec.UNSPECIFIED } } override fun onDraw(canvas: Canvas?) { super.onDraw(canvas) //繪制顯示的text,最多同時(shí)顯示4個(gè) //Log.e("TAG", "onDraw: mScrollY=$mScrollY") for (item in mItemList) { item.draw(mScrollY / itemChangeYCapacity ,mPaint, canvas) } } @SuppressLint("ClickableViewAccessibility") override fun onTouchEvent(event: MotionEvent?): Boolean { event?.let { when(event.action) { MotionEvent.ACTION_DOWN -> { //按下開(kāi)始計(jì)算滑動(dòng)距離 mScrollY = 0f mLastY = it.y } MotionEvent.ACTION_MOVE -> move(event) MotionEvent.ACTION_UP -> stopMove() } } //view是最末端了,應(yīng)該攔截touch事件,不然事件序列將舍棄 return true } private fun move(e: MotionEvent) { val dy = mLastY - e.y //更新mLastY值 mLastY = e.y //設(shè)置滑動(dòng)范圍,到達(dá)頂部不能下拉,到達(dá)底部不能上拉 if (dy == 0f) return //為什么會(huì)有兩次0??? if (dy < 0 && mCurrentIndex - 1 == 0) return if (dy > 0 && mCurrentIndex + 1 == mData!!.size - 1) return //累加滑動(dòng)距離 mScrollY += dy //如果滑動(dòng)距離切換了選中值,重繪前修改選中值 if (mScrollY >= itemChangeYCapacity) changeItem(mCurrentIndex + 1) else if (mScrollY <= -itemChangeYCapacity) changeItem(mCurrentIndex - 1) //滑動(dòng)后觸發(fā)重繪,繪制時(shí)處理滑動(dòng)效果 //Log.e("TAG", "move: mScrollY=$mScrollY") invalidate() } //統(tǒng)一修改mCurrentIndex,各個(gè)TextItem需要復(fù)位 private fun changeItem(index: Int) { mCurrentIndex = index //消耗滑動(dòng)距離 mScrollY = 0f //對(duì)各個(gè)TextItem復(fù)位,重新調(diào)用setup即可 for (item in mItemList) { item.setup() } } private fun stopMove() { //結(jié)束滑動(dòng)后判定,滑動(dòng)距離超過(guò)itemChangeYCapacity一半就切換了選中項(xiàng) val terminalScrollY: Float = when { mScrollY > itemChangeYCapacity / 2f -> itemChangeYCapacity mScrollY < -itemChangeYCapacity / 2f -> -itemChangeYCapacity //滑動(dòng)沒(méi)有達(dá)到切換選中項(xiàng)效果,應(yīng)該恢復(fù)到原先狀態(tài) else -> 0f } //這里使用ValueAnimator處理剩余的距離,模擬滑動(dòng)到需要的位置 val animator = ValueAnimator.ofFloat(mScrollY, terminalScrollY) //Log.e("TAG", "stopMove: mScrollY=$mScrollY, terminalScrollY=$terminalScrollY") animator.addUpdateListener { animation -> //Log.e("TAG", "stopMove: " + animation.animatedValue as Float) mScrollY = animation.animatedValue as Float invalidate() } //動(dòng)畫結(jié)束時(shí)要更新選中的項(xiàng)目 animator.addListener (onEnd = { if (mScrollY == itemChangeYCapacity) changeItem(mCurrentIndex + 1) else if (mScrollY == -itemChangeYCapacity) changeItem(mCurrentIndex - 1) }) //滑動(dòng)動(dòng)畫總時(shí)間應(yīng)該和距離有關(guān) val percent = terminalScrollY / (itemChangeYCapacity / 2f) animator.duration = (afterUpAnimatorPeriod * abs(percent)).toLong() //animator.duration = afterUpAnimatorPeriod.toLong() animator.start() } //單位轉(zhuǎn)換 @Suppress("SameParameterValue") private fun dp2px(context: Context, dpVal: Float): Int { return TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, dpVal, context.resources .displayMetrics ).toInt() } //依賴于控件寬高及數(shù)據(jù),需要在onMeasure之后初始化一下 inner class TextItem( private val type: Int, //上下兩個(gè)次item移動(dòng)時(shí)是對(duì)稱的,要?jiǎng)?chuàng)建時(shí)確定 private val isTopSecond: Boolean = false ){ private var index: Int = 0 private var x: Float = 0f private var y: Float = 0f private var textSize: Float = 0f private var alpha: Int = 0 private var height: Float = 0f private var dSize = 0f private var dAlpha = 0 private var dY = 0f private var isInit = false fun setup() { //x為中心即可 x = measuredWidth / 2f //根據(jù)類型設(shè)置屬性 when(type) { ITEM_TYPE_MAIN -> { index = mCurrentIndex y = measuredHeight / 2f textSize = mainSize alpha = mainAlpha height = mainHeight } ITEM_TYPE_SECOND -> { index = if (isTopSecond) mCurrentIndex - 1 else mCurrentIndex + 1 y = if (isTopSecond) secondHeight / 2f else measuredHeight - secondHeight / 2f textSize = secondSize alpha = secondAlpha height = secondHeight } else -> { index = mCurrentIndex + 2 //初始化時(shí)未確定位置 y = 0F textSize = 0f alpha = 0 height = 0f } } } private fun calculate(delta: Float) { //根據(jù)類型得到變化值 when(type) { ITEM_TYPE_MAIN -> { //無(wú)論向那邊移動(dòng)都應(yīng)該是變小 dSize = (mainSize - secondSize) * -abs(delta) dAlpha = ((mainAlpha - secondAlpha) * -abs(delta)).toInt() //主次item中線之間的距離,delta>0頁(yè)面上移,y減小 dY = (mainHeight + secondHeight) / 2f * -delta } ITEM_TYPE_SECOND -> { //以上面為準(zhǔn),下面對(duì)稱即可 if (isTopSecond && delta > 0 || !isTopSecond && delta < 0) { //項(xiàng)目變?yōu)橄В底冃? dSize = secondSize * -abs(delta) dAlpha = (secondAlpha * -abs(delta)).toInt() //消失的高度為次item的高度一半 //上面item消失時(shí)y減小,下面item消失時(shí)y增加 dY = secondHeight / 2f * if (isTopSecond) -abs(delta) else abs(delta) }else { //項(xiàng)目變?yōu)檫x中,值變大 dSize = (mainSize - secondSize) * abs(delta) dAlpha = ((mainAlpha - secondAlpha) * abs(delta)).toInt() //上面item變?yōu)檫x中時(shí)y增加,下面item變?yōu)檫x中時(shí)y減小 dY = (mainHeight + secondHeight) / 2f * if (isTopSecond) abs(delta) else -abs(delta) } } else -> { //新項(xiàng)目終態(tài)就是次item,無(wú)論怎么移動(dòng)值都是變大的 dSize = secondSize * abs(delta) dAlpha = (secondAlpha * abs(delta)).toInt() //從邊沿移動(dòng)到次item位置,即次item高度一半 //delta>0從下面出現(xiàn),y應(yīng)該變小,delta<0從上面出現(xiàn),y變大 dY = secondHeight / 2f * -delta //移動(dòng)時(shí)才確定新item的y和index y = if (delta > 0) measuredHeight.toFloat() else 0f index = if (delta > 0) mCurrentIndex + 2 else mCurrentIndex - 2 } } } fun draw(delta: Float, paint: Paint, canvas: Canvas?) { //確保在onMeasure后初始化 if (!isInit) { setup() isInit = true } //計(jì)算屬性變化 calculate(delta) //修改畫筆并繪制,注意變化的值都是相對(duì)于原來(lái)的值,不要去修改原來(lái)的值 paint.textSize = textSize + dSize paint.alpha = alpha + dAlpha canvas?.drawText(getText(), x, getBaseline(paint, y + dY), paint) } private fun getText(): String { //判定范圍 if (index < 0 || index >= mData!!.size) return "" return mData!![index] } private fun getBaseline(paint: Paint, tempY: Float): Float { //繪制字體的參數(shù),受字體大小樣式影響 val fmi = paint.fontMetricsInt //top為基線到字體上邊框的距離(負(fù)數(shù)),bottom為基線到字體下邊框的距離(正數(shù)) //基線中間點(diǎn)的y軸計(jì)算公式,即中心點(diǎn)加上字體高度的一半,基線中間點(diǎn)x就是中心點(diǎn)x return tempY - (fmi.top + fmi.bottom) / 2f } } }
主要問(wèn)題
自定義XML參數(shù)
這個(gè)網(wǎng)上應(yīng)該有很多教程了,主要就是要?jiǎng)?chuàng)建一個(gè)value里面的xml來(lái)定義屬性,在XML使用的使用引入命名空間,并使用這些屬性,最后在控件代碼中讀取參數(shù)值。下面是這個(gè)控件的自定義屬性:
res->value->scrolll_select_view_style.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="ScrollSelectView"> <attr name="mainAlpha" format="integer"/> <attr name="secondAlpha" format="integer"/> <attr name="textScanSize" format="integer"/> <attr name="changeItemYCapacity" format="dimension"/> <attr name="afterUpAnimatorPeriod" format="integer"/> <attr name="mainItemPercent" format="fraction"/> </declare-styleable> </resources>
這里有六個(gè)控制屬性,mainAlpha和secondAlpha是設(shè)定中間和第二層文字的透明度,值為[0,255];textScanSize是文字相對(duì)于文字框的縮放比例,比如設(shè)置為2的話文字大小就是文字框的一半;changeItemYCapacity是滑動(dòng)導(dǎo)致item切換的距離,單位為dp,即手指滑過(guò)這么長(zhǎng)的距離就切換了選中項(xiàng);afterUpAnimatorPeriod是手指抬起時(shí),恢復(fù)到默認(rèn)狀態(tài)或者跳轉(zhuǎn)到指定狀態(tài)的最大時(shí)間間隔,動(dòng)畫時(shí)間會(huì)根據(jù)滑動(dòng)距離占changeItemYCapacity的比例計(jì)算得到;mainItemPercent是中間item占整個(gè)控件的高度比例,大小為[0,100],第二層的item的高度為剩下高度的一半。
Paint的初始化
Paint的初始化也是一個(gè)老生常談的問(wèn)題了,不能在onDraw里面創(chuàng)建,因?yàn)閛nDraw會(huì)頻繁被調(diào)用,同類對(duì)象也不應(yīng)該在onDraw里面創(chuàng)建。
控件的默認(rèn)大小
自定義view需要在onMeasure設(shè)定默認(rèn)大小,不然使用wrap_content的時(shí)候會(huì)出問(wèn)題,前幾篇文章和注釋都寫的很清楚了。
和寬高有關(guān)的默認(rèn)屬性設(shè)置
所有和控件寬高有關(guān)的默認(rèn)屬性都需要在onMeasure中設(shè)置,另外TextItem的setup中用到了measuredWidth和measuredHeight,也需要在onMeasure后初始化,這里就在第一次繪制的時(shí)候初始化了。
文字繪制到中心位置
通過(guò)paint要將文字繪制到中心位置,需要結(jié)合textAlign(Paint.Align.CENTER)和fontMetricsInt計(jì)算得到繪制的縱坐標(biāo)位置。
滑動(dòng)邏輯處理
因?yàn)檫@個(gè)繼承的是View,所以在onTouchEvent中消耗事件即可,一定要對(duì)ACTION_DOWN事件返回true。
移動(dòng)時(shí)做了三步操作,一是判定是否能夠移動(dòng),二是累加移動(dòng)dy并觸發(fā)重繪,三是判斷滑動(dòng)距離是否切換了選中值,達(dá)到切換條件時(shí),應(yīng)該修改選中的index、累加的滑動(dòng)距離,并將各個(gè)item重置到原來(lái)的位置及狀態(tài)(ps.好像就是改了個(gè)index。。。)。這里有點(diǎn)不太好理解,就是每當(dāng)滑動(dòng)達(dá)到切換條件后,就修改選中,重置各個(gè)item,邏輯上是一個(gè)跳躍性修改,但是對(duì)于draw來(lái)說(shuō),只是繪制的新的圖像罷了。
滑動(dòng)結(jié)束后觸發(fā)動(dòng)畫滾動(dòng)到選定項(xiàng)
當(dāng)滑動(dòng)結(jié)束后應(yīng)該將控件滑動(dòng)到一個(gè)選中項(xiàng),即讓選中項(xiàng)放置到中間,有三種情況,一個(gè)是沒(méi)有切換,一個(gè)是上一個(gè)index,一個(gè)是后一個(gè)index。這里利用了ValueAnimator去模擬滑動(dòng),首先計(jì)算到最終狀態(tài)的滑動(dòng)距離,然后從當(dāng)前滑動(dòng)距離不斷更新,最后到達(dá)最終狀態(tài)的滑動(dòng)距離。只要更新累加的滑動(dòng)值,觸發(fā)重繪,在onDraw中會(huì)和move一樣進(jìn)行處理。最后在動(dòng)畫結(jié)束后,要和move一樣切換選中值及一些其他操作。
滑動(dòng)導(dǎo)致的文字的繪制
文字的繪制我封裝到了TextItem這個(gè)內(nèi)部類里,還是比較復(fù)雜,可能有三層item吧,不應(yīng)該搞太多的。
繪制邏輯主要分成兩部分,一部分是靜態(tài)的位置及狀態(tài),一部分是滑動(dòng)導(dǎo)致的變化值計(jì)算。靜態(tài)的位置及狀態(tài)寫在setup里面,還是比較簡(jiǎn)單的,就是根據(jù)type去確定。
滑動(dòng)導(dǎo)致的變化值計(jì)算就復(fù)雜多了,主要就是根據(jù)滑動(dòng)距離和滑動(dòng)切換項(xiàng)目的門限值確定一個(gè)百分比delta,再根據(jù)delta去計(jì)算變換的屬性值。實(shí)際上想簡(jiǎn)單點(diǎn),不就是屬性變大或者屬性變小的問(wèn)題么,首先根據(jù)delta的正負(fù)值確定滑動(dòng)的方向,再通過(guò)滑動(dòng)方向去確定該類型的item應(yīng)該屬性變大或者屬性變小,一個(gè)一個(gè)去設(shè)置就行了,就是代碼比較多而已。
寫到這里我發(fā)現(xiàn)前面我說(shuō)應(yīng)該用一個(gè)數(shù)學(xué)函數(shù)去映射的,我這abs(delta)不就是一個(gè)映射函數(shù)么。。果然寫代碼和寫文章更能讓直接深刻理解內(nèi)容。這里abs(delta)換成拋物線確實(shí)可能會(huì)好看些,有機(jī)會(huì)改改。
使用
使用起來(lái)很簡(jiǎn)單,直接設(shè)定數(shù)據(jù)和初始index就可以,暫時(shí)沒(méi)有動(dòng)態(tài)切換,可以重寫下setter函數(shù),invalidate()就可以了。
xml
<com.silencefly96.module_common.view.ScrollSelectView android:id="@+id/hhView" android:layout_width="match_parent" android:layout_height="300dp" android:background="@color/teal_700" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent"/>
code
binding.hhView.mData = ArrayList<String>().apply{ add("第一個(gè)") add("第二個(gè)") add("第三個(gè)") add("第四個(gè)") add("第五個(gè)") } binding.hhView.mCurrentIndex = 2
范圍限定偶爾不生效
實(shí)際運(yùn)行起來(lái)我發(fā)現(xiàn)對(duì)范圍限定偶爾會(huì)不生效,暫時(shí)還沒(méi)有解決,應(yīng)該是動(dòng)畫沒(méi)有完成就滑動(dòng)造成的,不太好辦,當(dāng)然這個(gè)控件主要目的是學(xué)習(xí)onDraw,寫的這么復(fù)雜了,目的已經(jīng)達(dá)到了,要學(xué)習(xí)繪制的話后面再寫幾個(gè)控件吧。
到此這篇關(guān)于Android自定義view實(shí)現(xiàn)滾動(dòng)選擇控件詳解的文章就介紹到這了,更多相關(guān)Android滾動(dòng)選擇內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android開(kāi)發(fā)中LayoutInflater用法詳解
這篇文章主要介紹了Android開(kāi)發(fā)中LayoutInflater用法,結(jié)合實(shí)例形式分析了LayoutInflater類的功能、作用、使用方法及相關(guān)注意事項(xiàng),需要的朋友可以參考下2016-08-08Android使用ListView實(shí)現(xiàn)下拉刷新及上拉顯示更多的方法
這篇文章主要介紹了Android使用ListView實(shí)現(xiàn)下拉刷新及上拉顯示更多的方法,結(jié)合實(shí)例形式分析了ListView滾動(dòng)刷新與加載的相關(guān)操作技巧,需要的朋友可以參考下2017-02-02Android 簡(jiǎn)易手勢(shì)密碼開(kāi)源庫(kù)詳解
本文主要介紹Android 簡(jiǎn)易手勢(shì)密碼,這里主要介紹手勢(shì)密碼如何實(shí)現(xiàn)及簡(jiǎn)單的示例代碼,有需要的同學(xué)可以參考下2016-08-08基于DownloadManager的簡(jiǎn)單下載器編寫小結(jié)
Android自帶的DownloadManager是一個(gè)很好的下載文件的工具。該類在API level 9之后出現(xiàn),它已經(jīng)幫我們處理了下載失敗、重新下載等功能,整個(gè)下載過(guò)程全部交給系統(tǒng)負(fù)責(zé),不需要我們過(guò)多的處理,非常的nice。關(guān)鍵的是用起來(lái)也很簡(jiǎn)單,稍微封裝一下就可以幾句話搞定下載2017-12-12Android中Permission權(quán)限機(jī)制的具體使用
這篇文章主要介紹了Android中Permission權(quán)限機(jī)制的具體使用,本文講解了權(quán)限級(jí)別 protection level、ICC(inter-component communication)權(quán)限保護(hù)等內(nèi)容,需要的朋友可以參考下2015-04-04Android應(yīng)用中使用ViewPager實(shí)現(xiàn)類似QQ的界面切換效果
這篇文章主要介紹了Android應(yīng)用中使用ViewPager實(shí)現(xiàn)類似QQ的界面切換效果的示例,文中的例子重寫了PagerAdapter,并且講解了如何解決Android下ViewPager和PagerAdapter中調(diào)用notifyDataSetChanged失效的問(wèn)題,需要的朋友可以參考下2016-03-03android 下載時(shí)文件名是中文和空格會(huì)報(bào)錯(cuò)解決方案
項(xiàng)目中遇到了下載文件文件名是中文而且還有空格如果不對(duì)連接進(jìn)行處理下載就會(huì)報(bào)錯(cuò)要想解決這個(gè)問(wèn)題只需對(duì)你的url進(jìn)行編碼然后替換空格用編碼表示,感興趣的朋友可以詳細(xì)了解下2013-01-01Android編程向服務(wù)器發(fā)送請(qǐng)求時(shí)出現(xiàn)中文亂碼問(wèn)題的解決方法
這篇文章主要介紹了Android編程向服務(wù)器發(fā)送請(qǐng)求時(shí)出現(xiàn)中文亂碼問(wèn)題的解決方法,實(shí)例分析了Android參數(shù)傳遞過(guò)程中中文亂碼的解決技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-11-11Android 訪問(wèn)文件權(quán)限的四種模式介紹
這篇文章主要介紹了Android 訪問(wèn)文件權(quán)限的四種模式介紹的相關(guān)資料,非常不錯(cuò)具有參考借鑒價(jià)值,需要的朋友可以參考下2016-06-06Android實(shí)現(xiàn)一周時(shí)間早中晚排班表
項(xiàng)目需求需要實(shí)現(xiàn)一個(gè)動(dòng)態(tài)添加,修改一周早中晚時(shí)間排班表,文章給大家提供了實(shí)現(xiàn)代碼,需要的朋友參考下吧2018-07-07