Android 自定義控件基礎

目錄
一、前言
二、基礎代碼
(1)自定義View基礎流程
(2)onDraw()方法
(3)onMeasure()方法
三、動畫
(1)補間動畫
(2)幀動畫
(3)屬性動畫
四、SVG標簽使用
五、練習Demo
六、Demo地址
七、內容推薦


一、前言

自定義控件一直是Android很重要的一部分,但是大部分時間我們都在處理業務邏輯而很少自己去寫控件。因為現在開源的控件比較多,自定義寫起來也比較麻煩。但是當我們需要的時候可能就忘了,所以菜鳥作者就買了本書打算系統的學習一遍。這一篇也算是《Android 自定義控件開發入門與實戰》讀后感。邊學邊工作也花了一個多月寫過幾篇筆記,不過后面的隱藏掉了。感覺有點啰嗦所以就湊成這一篇總結。

不管感興趣與不感興趣可以瀏覽一遍,積累一下基礎。如果要深入學習這里也推薦兩位大神博客

http://www.lxweimin.com/p/146e5cec4863

**https://blog.csdn.net/harvic880925 **啟艦《Android 自定義控件開發入門與實戰》

二、基礎代碼

《 自定義控件開發入門與實戰》這本書我這里主要分成兩部分總結:1.自定義View 2.動畫

這里主要根據練習時候寫的Demo來進行基礎的回顧。先從自定義View開始

(1)自定義View基礎流程

  • 1.繼承View,或者繼承ViewGroup,以及View/ViewGroup的派生類
  • 2.測量 :onMeasure(),用來控制自定義View的寬高
  • 3.位置:onLayout(),在繼承ViewGroup的時候需要重寫該方法,用來控制子View擺放的位置。
  • 4.繪圖:onDraw(),自定義View的主要方法,需要重載或重寫該方法來繪制所需要的控件。

先從簡單的開始講起:

(2)onDraw()方法

為什么先說這方法呢 ? 因為前面兩個方法不說。我們也可以通過這個方法來簡單的實現自定義控件。

先上菜(代碼),在介紹這道菜的好處

    fun initPaint(){
        //創建畫筆
        paint = Paint()
        //設置畫筆顏色
        paint?.setColor(Color.WHITE)
        //設置填充樣式 1.Paint.Style.FILL僅填充內部  2.Paint.Style.FILL_AND_STROKE填充內部和描邊 3.Paint.Style.STROKE僅描邊
        paint?.setStyle(Paint.Style.STROKE)
        //設置畫筆寬度 注:畫筆樣式為Paint.Style.FILL時不顯示效果
        paint?.setStrokeWidth(5f)

        paint1 = Paint()
        //設置畫筆顏色
        paint1?.setColor(Color.GREEN)
        //設置填充樣式 1.Paint.Style.FILL僅填充內部  2.Paint.Style.FILL_AND_STROKE填充內部和描邊 3.Paint.Style.STROKE僅描邊
        paint1?.setStyle(Paint.Style.STROKE)
        //設置畫筆寬度 注:畫筆樣式為Paint.Style.FILL時不顯示效果
        paint1?.setStrokeWidth(5f)
    }
    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        //平移畫布 起始點起始點從(X:0,Y:0)變成(X:100,Y:100)
        canvas?.translate(0f, 100f)
        //裁剪畫布 構造方法很多 都是以clip開頭
        canvas?.clipRect(0, 0, 500, 500)
        //設置畫布顏色
        canvas?.drawColor(Color.BLACK)
//        canvas.drawARGB(0,0,0,255);
//        canvas.drawRGB(0,0,255);

//        繪制直線 startX/startY:起始點X/Y坐標  stopX/stopY:終點X/Y坐標
        canvas?.drawLine(100f, 50f, 450f, 50f, paint)
        //繪制點
        canvas?.drawPoint(50f, 50f, paint)

        //繪制矩形====== >
        val rect = Rect(50, 100, 450, 450)//創建矩形工具類
        //繪制矩形方法
        canvas?.drawRect(rect, paint)//第一種構造方法
//        Rect/RectF用來保存int/float類型數值的矩形結構
//        RectF rectf = new RectF(200,200,400,400);
//        canvas.drawRect(rectf);第二種構造方法
//        canvas.drawRect(200,200,400,400,paint);第三種構造方法
        //   <====== 繪制矩形方法

//      起始點變成(X:50,Y:100)
        canvas?.translate(50f, 100f)
        //繪制路徑 ====== >
        val path = Path()
        //設置起始點
        path.moveTo(100f, 50f)
        //第一條直線的終點也是第二條直線的起點
        path.lineTo(50f, 200f)
        path.lineTo(150f, 100f)
        path.lineTo(50f, 100f)
        path.lineTo(150f, 200f)
//        path.lineTo(100, 50);
        //閉環
        path.close()
        //繪制路徑方法
        canvas?.drawPath(path, paint)
        // < ======繪制路徑

        //繪制弧線  ===== >
        val path1 = Path()
        val rect1 = RectF(200f, 50f, 350f, 200f)
        //弧線主要方法 oval 生成橢圓的矩形,startAngle 弧開始角度, sweepAngle 持續角度
        path1.arcTo(rect1, 180f, 180f, false)
        canvas?.drawPath(path1, paint)
        // < ===== 繪制弧線

//      起始點變成(X:50,Y:100)
        canvas?.translate(50f, 250f)
        //繪制區域 ==== >
        val region = Region(0, 0, 200, 50)
        val region1 = Region(150, -50, 300, 50)
        drawRegion(canvas, region, paint)
        drawRegion(canvas, region1, paint1)
        //區域操作
        // (1)Op.DIFFERENCE:顯示region與region1不同區域
        // (2)Op.INTERSECT:顯示region與region1相交區域
        // (3)Op.UNION:顯示region與region1組合在一起區域
        // (4)Op.XOR:顯示region與region1相交之外區域
        // (5)Op.REVERSE_DIFFERENCE:顯示region1與region不同區域
        // (6)Op.REPLACE:顯示region1區域
        region.op(region1, Region.Op.INTERSECT)
        paint1?.setColor(Color.GRAY)
        drawRegion(canvas, region, paint1)
        // < ==== 繪制區域

        //保存當前畫布狀態
        canvas?.save()
        //恢復到上一層保存的狀態
//        canvas.restore();
    }

    fun drawRegion(canvas : Canvas?,region : Region,paint: Paint?){
        var iter = RegionIterator(region)
        var r = Rect()
        while (iter.next(r)){
            canvas?.drawRect(r,paint)
        }
    }

看完代碼我們知道,繪圖的前提是先準備好一只畫筆(paint),要準備一只怎樣的畫筆呢。這時候我們可以根據Paint 類提供的API來定義畫筆樣式。

畫筆準備好之后,我們就要考慮繪制什么樣的圖形呢,要繪制在什么地方。這時候我們需要一張畫布,用來顯示繪制的圖形。

而onDraw(canvas: Canvas?)方法剛好提供了一個畫布,并且通過Canvas類提供的API,我們可以繪制各式各樣的圖形。當然常用的繪圖方法就看上面的代碼。都用詳細的注釋。

總結:1.準備畫筆 2.利用畫布繪圖

注意:通常要在自定義View的構造函數中初始化好畫筆,不要在onDraw()方法中去初始畫筆。因為每次刷新畫布的時候都會不斷調用ondraw()創建新的畫筆,消耗內存....

(3)onMeasure()方法

如果要通過下面屬性來控制自定義View的大小時,我們需要重載onMeasure()方法來控制畫布(自定義View)的大小,

android:layout_width="200dp"
android:layout_height="250dp"

怎么控制呢。

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        //widthMeasureSpec/heightMeasureSpec 由mode+size兩部分組成
        //獲取模式
        val widthMode = MeasureSpec.getMode(widthMeasureSpec)
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)
        //獲取數值
        val widthSize = MeasureSpec.getSize(widthMeasureSpec)
        val heightSize = MeasureSpec.getSize(heightMeasureSpec)

        when(widthMode){
            //父元素決定子元素的確切大小,子元素將被限定在給定的邊界里而忽略它本身的大小 :match_parent 或者具體指 200dp
            MeasureSpec.EXACTLY ->{
                //根據需求計算寬度大小
                 testWidth = widthSize - 100
            }
            //子元素至多達到指定大小的值 : wrap_content
            MeasureSpec.AT_MOST ->{}
            //父元素不對子元素施加任何束縛,子元素可以得到任意想要的大小
            MeasureSpec.UNSPECIFIED ->{}
        }

        when(heightMode){
            MeasureSpec.EXACTLY ->{
                //根據需求計算高度大小
                testHeight = heightSize - 100
            }
            MeasureSpec.AT_MOST ->{}
            MeasureSpec.UNSPECIFIED ->{}
        }
        //計算出的寬度和高度 可以通過setMeasuredDimension方法進行設置
        setMeasuredDimension(testWidth!!, testHeight!!)
    }

通過上面的代碼我們可以簡單的計算出所需要的寬高。

只通過上面簡單的代碼很難明白測量的意義,所以這里提供一個博客供大家參考,篇幅太長就不在這里多寫 畢竟是總結..

https://www.cnblogs.com/yishujun/p/5560838.html

onLayout()方法在這里也不介紹,因為只用在自定義ViewGroup的時候才會使用。邏輯也會比較復雜。

三、動畫

這本書好幾張都講了動畫,不過這里不打算詳細介紹。自定義View才是重點。所以這邊只貼一部分常用代碼。供回憶用。

(1)補間動畫

(代碼使用這里就不貼了)

<!--透明度-->
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="3000"
    android:fromAlpha="0"
    android:toAlpha="1"
    android:fillAfter = "true"
    android:repeatCount = "infinite"
    android:repeatMode = "reverse"
    android:interpolator = "@android:anim/accelerate_interpolator"
    />
<!--旋轉-->
<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="3000"
    android:fromDegrees="0"
    android:toDegrees="360"
    android:pivotX="50%"
    android:pivotY="50%"
    android:fillAfter = "true"
    android:repeatCount = "infinite"
    android:repeatMode = "reverse"
    android:interpolator = "@android:anim/linear_interpolator"
/>
<!--縮放-->
<scale xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="3000"
        android:fromXScale="0"
        android:toXScale="2"
        android:fromYScale="0"
        android:toYScale="2"
        android:pivotX="50%"
        android:pivotY="50%"
        android:fillAfter = "true"
        android:repeatCount = "infinite"
        android:repeatMode = "reverse"
        android:interpolator = "@android:anim/linear_interpolator"
    />
<!--移動-->
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromXDelta="0"
    android:toXDelta="1000"
    android:duration="3000"
    android:fillAfter = "true"
    android:repeatCount = "infinite"
    android:repeatMode = "reverse"
    android:interpolator = "@android:anim/linear_interpolator"
/>
<!--組合動畫-->
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
     android:duration="3000"
     android:fillAfter="true"
     android:interpolator="@android:anim/linear_interpolator"
     android:repeatMode="reverse"
>
    <alpha
            android:fromAlpha="0"
            android:toAlpha="1"
            android:repeatCount="infinite"
    />
    <rotate
            android:pivotX="50%"
            android:pivotY="50%"
            android:fromDegrees="0"
            android:toDegrees="360"
            android:repeatCount="infinite"
    />
    <scale
            android:fromXScale="0"
            android:fromYScale="0"
            android:pivotY="50%"
            android:toXScale="1"
            android:toYScale="1"
            android:repeatCount="infinite"
    />
    <translate
            android:fromXDelta="0"
            android:toXDelta="1000"
            android:repeatCount="infinite"
    />
</set>

使用方式:

val loadAnimation = AnimationUtils.loadAnimation(this, R.anim.alpha)
view.startAnimation(TweenAnimation.getAlpha())

(2)幀動畫

object FrameAnimation {
    /**
     * AnimationDrawable常用屬性
     * start():開始播放逐幀動畫。
     * stop():停止播放逐幀動畫。
     * getDuration(int index):得到指定index的幀的持續時間
     * getFrame(int index):得到指定index的幀所對應的Drawable對象
     * getNumberOfFrames():得到當前AnimationDrawable的所有幀數量
     * isRunning():判斷當前AnimationDrawable是否正在播放
     * setOneShot(boolean oneShot):設置AnimationDrawable是否執行一次
     * isOneShot():判斷當前AnimationDrawable是否執行一次
     * addFrame(Drawable frame,int duration):為AnimationDrawable添加1幀,并設置持續時間
     */
    fun getAnimation(context: Context):AnimationDrawable{
        val animationDrawable = AnimationDrawable()
        for (x in 1..3){
            val resources = context.resources.getIdentifier("frame"+x,"mipmap",context.packageName)
            val drawable = context.resources.getDrawable(resources)
            animationDrawable.addFrame(drawable,500)
        }
        animationDrawable.isOneShot = false
        return animationDrawable
    }
}
image.gif
使用方式:val animation = FrameAnimation.getAnimation(this)iv_main.setBackgroundDrawable(animation)animation.start()

(3)屬性動畫

object ValueAnimation {
    /**
     * ValueAnimator常用函數
     * ofInt(int... values)、ofFloat(float... values) 、ofObject:設置動畫變化過程中的值
     * setDuration(long duration):設置動畫時長,單位是毫秒
     * getAnimatedValue():獲取ValueAnimator在運動時當前運動點的值
     * start():開始動畫
     * setRepeatCount(int value):設置循環次數,設置為INFINITE表示無限循環
     * setRepeatMode(int value):設置循環模式 ValueAnimation.REVERSE倒序重新開始 ValueAnimation.RESTART正序重新開始
     * addUpdateListener(AnimatorUpdateListener listener) : 監聽動畫過程中值的實時變化
     * addListener(AnimatorListener listener):監聽動畫變化時的4個狀態
     * removeUpdateListener(AnimatorUpdateListener listener):移除指定監聽
     * removeAllUpdateListeners():移除所有監聽
     * removeListener(AnimatorListener listener):移除AnimatorUpdateListener
     * removeAllListeners():移除AnimatorListener
     * setInterpolator():設置插值器
     * cancel():取消動畫
     * setStartDelay(long startDelay):延時多久開始,單位是毫秒
     * clone():克隆一個ValueAnimator實例
     */
    fun init(view : View):ValueAnimator{
        val animator = ValueAnimator.ofInt(0, 500)

        animator.duration = 3000
        animator.repeatCount = ValueAnimator.INFINITE
        animator.repeatMode = ValueAnimator.REVERSE
        animator.setEvaluator(MyEvaluator)
        animator.addUpdateListener({animation ->
            val value = animation.animatedValue as Int
            view.layout(view.left,view.top,value+view.left,view.bottom)
        })
        return animator
    }

    /**
     * 開始動畫
     */
    fun startAnimator(view : View){
        val init = init(view)
        init.start()
    }

    /**
     * 停止動畫
     */
    fun  stopAnimator(view : View){
        val init = init(view)
        init.removeAllUpdateListeners()
        init.cancel()
    }
}
使用方式:ValueAnimation.startAnimator(iv_main)

ObjectAnimation代碼使用:

object ObjectAnimation {
    /**
     * 組合動畫
     */
    fun start(tv1 : View){
        var background = ObjectAnimator.ofInt(tv1,"BackgroundColor", Color.BLACK,Color.BLUE,Color.RED)
        background.setRepeatCount(ValueAnimator.INFINITE)
        var alpha = ObjectAnimator.ofFloat(tv1,"alpha",0f,1f,0f,0.5f)
        alpha.setRepeatCount(ValueAnimator.INFINITE)
        var animatorSet = AnimatorSet()
        animatorSet.playTogether(background,alpha)
        animatorSet.setDuration(3000)
        animatorSet.start()
    }

    /**
     * PropertyValuesHolder 保存了動畫過程中所需要操作的屬性和對應的值。
     * //設置動畫的Evaluator
     * public void setEvaluator(TypeEvaluator evaluator)
     * //用于設置ofFloat()所對應的動畫值列表
     * public void setFloatValues(float... values)
     * //用于設置ofInt()所對應的動畫值列表
     * public void setIntValues(int... values)
     * //用于設置ofKeyframes()所對應的動畫值列表
     * public void setKeyframes(Keyframe... values)
     * //用于設置ofObject()所對應的動畫值列表
     * public void setObjectValues(Object... values)
     * //設置動畫屬性名
     * public void setPropertyName(String propertyName)
     */
    fun property(view : View){
        val Background = PropertyValuesHolder.ofInt("BackgroundColor", Color.BLACK,Color.BLUE,Color.RED)
        val alpha = PropertyValuesHolder.ofFloat("alpha",0f,1f,0f,0.5f)
        val holder = ObjectAnimator.ofPropertyValuesHolder(view,Background, alpha)
        holder.duration = 3000
        holder.interpolator = AccelerateInterpolator()
        holder.repeatCount = Animation.INFINITE
        holder.repeatMode = REVERSE
        holder.start()
    }
    /**
     * KeyFrame 提供方便地控制動畫速率問題。
     */
    fun keyFrame(view : View){
        val holder = PropertyValuesHolder.ofFloat("alpha", 0f,1f)
        //fraction表示當前的顯示進度  value:表示動畫當前所在的數值位置。
        val keyframe = Keyframe.ofFloat(0.1f, 0.1f)
        PropertyValuesHolder.ofKeyframe("alpha",keyframe)
        val valuesHolder = ObjectAnimator.ofPropertyValuesHolder(view, holder)
        valuesHolder.duration = 3000
        valuesHolder.repeatCount = Animation.INFINITE
        valuesHolder.repeatMode = REVERSE
        valuesHolder.start()
    }

    /**
     * ViewPropertyAnimator Android3.1中新增ViewPropertyAnimator機制,給默認屬性提供了一種更加便捷的用法。
     */
    fun viewProperty(view : View){
        view.animate().alpha(0.5f).translationX(500f).rotation(180f).setDuration(5000).scaleX(2f)
    }
}

使用方式:

 R.id.btn_propertyValuesHolder ->{
                ObjectAnimation.property(iv_main)
            }
            R.id.btn_keyFrame ->{
                ObjectAnimation.keyFrame(iv_main)
            }
            R.id.btn_animate ->{
                ObjectAnimation.viewProperty(iv_main)
            }
objectAnimator文件使用:
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
                android:valueType="floatType"
                android:valueFrom="1"
                android:valueTo="3"
                android:startOffset="0"
                android:duration="2000"
                android:interpolator="@android:anim/linear_interpolator"
                android:propertyName="scaleY"
                android:repeatCount="infinite"
                android:repeatMode="reverse"/>

使用方式

//xml動畫
val loadAnimator = AnimatorInflater.loadAnimator(this, R.animator.object_animation)
loadAnimator.setTarget(iv_main)
loadAnimator.start()

不解釋光貼代碼可能看起來很亂,但動畫確實不是這篇的重點,只是書中有提到。所以把練習時寫的代碼給貼出來好復習。如果看起來很累大家可以跳過這一部分。

四、SVG標簽使用

Google在Android 5.0中增加了對SVG圖形的支持。因為SVG的占用空間比Bitmap小。所以這里也稍微提一下使用方式。

首先定義SVG標簽:

svg_vector.xml

<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="100dp"
    android:height="50dp"
    android:viewportWidth="100"
    android:viewportHeight="100"
>
    <path
            android:name="bar"
            android:pathData="M0,23 L60,23 L30,90 Z"
            android:strokeWidth="2"
            android:strokeColor="@android:color/darker_gray"/>
</vector>
        <!--width與height屬性:表示該SVG圖形的具體大小。-->
        <!--viewportWidth與viewportHeight屬性:表示SVG圖形劃分的比例。-->
        <!--path中字母M表示moveTo,字母L表示lineTo。-->
        <!--vector標簽指定的是畫布帶下,而path標簽則指定的是路徑內容-->
        <!--(2)path標簽-->
        <!--android:name:聲明一個標記,類似于ID,便于對其做動畫的時候順利地找到該節點。-->
        <!--android:pathData:對SVG矢量圖的描述。-->
        <!--android:strokeWidth:畫筆的寬度。-->
        <!--android:fillColor:填充顏色。-->
        <!--android:fillAlpha:填充顏色的透明度。-->
        <!--android:strokeColor:描邊顏色。-->
        <!--android:strokeWidth:描邊寬度。-->
        <!--android:strokeAlpha:描邊透明度。-->
        <!--android:strokeLineJoin:用于指定折線拐角形狀,取值有miter、round、bevel。-->
        <!--android:strokeLineCap:畫出線條的終點形狀,取值有butt/round/square-->
        <!--android:strokeMiterLimit:設置斜角的上限。-->
        <!--android:trimPathStart:用于指定路徑從哪開始,取值為0~1,表示路徑開始位置的百分比。-->
        <!--android:trimPathEnd:用于指定路徑的結束位置,取值為0~1,表示路徑結束位置的百分比。-->
        <!--android:trimPathOffset:用于指定結果路徑的位移距離,取值為0~1,當取值為0時,不進行位移;當取值為1時,位移整條路徑的長度。-->
        <!--android:pathData:在path標簽中,主要通過pathData屬性來指定SVG圖像的顯示內容。-->

        <!--pathData屬性除M和L指令以外,還有更多的指令:-->
        <!--M = moveto(M X,Y):將畫筆移動到指定的坐標位置。-->
        <!--L = lineto(L X,Y):畫直線到指定的坐標位置。-->
        <!--H = horizontal lineto(H X):畫水平線到指定的X坐標位置。-->
        <!--V = vertical lineto(V Y):畫垂直線到指定的Y坐標位置。-->
        <!--C = curveto(C X1,Y1,X2,Y2,ENDX,ENDY):三階貝濟埃曲線。-->
        <!--S = smooth curveto(S X2,Y2,ENDX,ENDY):三階貝濟埃曲線。-->
        <!--Q = quadratic Belzier curve(Q X,Y,ENDX,ENDY):二階貝濟埃曲線。-->
        <!--T = smooth quadratic Belzier curveto(T ENDX,ENDY):映射前面路徑后的終點。-->
        <!--A = elliptical Arc(A RX,RY,XROTATION,FLAG1,FLAG2,X,Y):弧線。-->
        <!--Z = closepath():關閉路徑。-->

        <!--group標簽常用屬性:-->
        <!--android:name:組的名字,用于與動畫相關聯-->
        <!--android:rotation:指定該組圖像的旋轉度數-->
        <!--android:pivotX:定義縮放和旋轉改組時的X參考點-->
        <!--android:pivotY:定義縮放和旋轉該組時的Y參考點-->
        <!--android:scaleX:指定該組X軸縮放大小-->
        <!--android:scaleY:指定該組Y軸縮放大小-->
        <!--android:translateX:指定該組沿X軸平移的距離-->
        <!--android:translateY:指定該組沿Y軸平移的距離-->

標簽與動畫綁定在一起

<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
                 android:drawable="@drawable/svg_vector">
    <target
            android:name="bar"
            android:animation="@animator/svg_animation"/>

</animated-vector>

svg_animation.xml

<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
                android:propertyName="trimPathStart"
                android:valueFrom="0"
                android:valueTo="1"
                android:duration="2000"/>

使用方式:

val create = AnimatedVectorDrawableCompat.create(this, R.drawable.svg_animation)iv_main.setImageDrawable(create)

簡單略過...,一切解釋盡在代碼中

五、練習Demo

看完這本書后發現了幾個有趣的控件。例:放大鏡 ,刮刮卡

這里就不展示代碼了,都寫在練習的項目當中,若有興趣可以下載下來看看。

六、Demo地址

練習Demo地址Github:https://github.com/DayorNight/CustomView

另外介紹個人開發的一個Android工具類

Github:https://github.com/DayorNight/BLCS

apk下載體驗地址:https://www.pgyer.com/BLCS

也可以掃碼下載

七、內容推薦

CSDN:《Android 自定義控件基礎》
《Android 數據庫知識回顧》???????
《Android 《Android移動性能實戰》學習筆記》
《Android 下載安裝應用APK封裝(適配8.0)》
《Android Notification通知簡單封裝(適配8.0)???????》???????
《Android 仿RxDialog自定義DialogFragment》

如果你覺得寫的不錯或者對您有所幫助的話

不妨頂一個【微笑】,別忘了點贊、收藏、加關注哈

看在花了這么多時間整理寫成文章分享給大家的份上,記得手下留情哈

您的每個舉動都是對我莫大的支持

image
image.gif

?

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 最近大家都在@微信君,請它老人家送頂圣誕帽!無意間發現有些APP可以自行添加各類圣誕帽,紅綠自選… 制作步驟如下:...
    小鹿美洋洋閱讀 404評論 0 0
  • 四種力量拔除自大的壞種子 事件 與員工溝通時,會有態度不好不耐煩表現 思維空性 我對他人的態度來自于我自大的壞種子...
    里喻棋閱讀 149評論 0 0
  • 呆迷,今天我們再開圣誕晚會,不知道那來的那么多情緒,突然感覺做人好難,難到忘了自己的夢想,止于此步,但是盡管如此,...
    沫小凱閱讀 256評論 0 0
  • 晚上的時候我常常坐在窗臺邊,看著一輪明月從樹葉底下慢慢升起來。千百年來,月亮就這樣靜靜的升上來,沒有悲,沒有喜,悄...
    播音1801B伍潔閱讀 144評論 0 0