<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                #### 4.4.3 自定義View示例 4.4.1節和4.4.2節分別介紹了自定義View的類別和注意事項,本節將通過幾個實際的例子來演示如何自定義一個規范的View,通過本節的例子再結合上面兩節的內容,可以讓讀者更好地掌握自定義View。下面仍然按照自定義View的分類來介紹具體的實現細節。 * 1.繼承View重寫onDraw方法 這種方法主要用于實現一些不規則的效果,一般需要重寫onDraw方法。采用這種方式需要自己支持wrap_content,并且padding也需要自己處理。下面通過一個具體的例子來演示如何實現這種自定義View。 為了更好地展示一些平時不容易注意到的問題,這里選擇實現一個很簡單的自定義控件,簡單到只是繪制一個圓,盡管如此,需要注意的細節還是很多的。為了實現一個規范的控件,在實現過程中必須考慮到wrap_content模式以及padding,同時為了提高便捷性,還要對外提供自定義屬性。我們先來看一下最簡單的實現,代碼如下所示。 public class CircleView extends View { private int mColor = Color.RED; private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); public CircleView(Context context) { super(context); init(); } public CircleView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public CircleView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { mPaint.setColor(mColor); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int width = getWidth(); int height = getHeight(); int radius = Math.min(width, height) / 2; canvas.drawCircle(width / 2, height / 2, radius, mPaint); } } 上面的代碼實現了一個具有圓形效果的自定義View,它會在自己的中心點以寬/高的最小值為直徑繪制一個紅色的實心圓,它的實現很簡單,并且上面的代碼相信大部分初學者都能寫出來,但是不得不說,上面的代碼只是一種初級的實現,并不是一個規范的自定義View,為什么這么說呢?我們通過調整布局參數來對比一下。 請看下面的布局: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#ffffff" android:orientation="vertical" > <com.ryg.chapter_4.ui.CircleView android:id="@+id/circleView1" android:layout_width="match_parent" android:layout_height="100dp" android:background="#000000"/> </LinearLayout> 再看一下運行的效果,如圖4-3中的(1)所示,這是我們預期的效果。接著再調整CircleView的布局參數,為其設置20dp的margin,調整后的布局如下所示。 :-: ![](https://img.kancloud.cn/88/14/881479e4efef146f41c7920cbe7e3166_1032x613.png) 圖4-3 CircleView運行效果圖 ``` <com.ryg.chapter_4.ui.CircleView android:id="@+id/circleView1" android:layout_width=" match_parent" android:layout_height="100dp" android:layout_margin="20dp" android:background="#000000"/> ``` 運行后看一下效果,如圖4-3中的(2)所示,這也是我們預期的效果,這說明margin屬性是生效的。這是因為margin屬性是由父容器控制的,因此不需要在CircleView中做特殊處理。再調整CircleView的布局參數,為其設置20dp的padding,如下所示。 ``` <com.ryg.chapter_4.ui.CircleView android:id="@+id/circleView1" android:layout_width="match_parent" android:layout_height="100dp" android:layout_margin="20dp" android:padding="20dp" android:background="#000000"/> ``` 運行后看一下效果,如圖4-3中的(3)所示。結果發現padding根本沒有生效,這就是我們在前面提到的直接繼承自View和ViewGroup的控件,padding是默認無法生效的,需要自己處理。再調整一下CircleView的布局參數,將其寬度設置為wrap_content,如下所示。 <com.ryg.chapter_4.ui.CircleView android:id="@+id/circleView1" android:layout_width="wrap_content" android:layout_height="100dp" android:layout_margin="20dp" android:padding="20dp" android:background="#000000"/> 運行后看一下效果,如圖4-3中的(4)所示,結果發現wrap_content并沒有達到預期的效果。對比下(3)和(4)的效果圖,發現寬度使用wrap_content和使用match_parent沒有任何區別。的確是這樣的,這一點在前面也已經提到過:對于直接繼承自View的控件,如果不對wrap_content做特殊處理,那么使用wrap_content就相當于使用match_parent。 為了解決上面提到的幾種問題,我們需要做如下處理: 首先,針對wrap_content的問題,其解決方法在4.3.1節中已經做了詳細的介紹,這里只需要指定一個wrap_content模式的默認寬/高即可,比如選擇200px作為默認的寬/高。 其次,針對padding的問題,也很簡單,只要在繪制的時候考慮一下padding即可,因此我們需要對onDraw稍微做一下修改,修改后的代碼如下所示。 protected void onDraw(Canvas canvas) { super.onDraw(canvas); final int paddingLeft = getPaddingLeft(); final int paddingRight = getPaddingLeft(); final int paddingTop = getPaddingLeft(); final int paddingBottom = getPaddingLeft(); int width = getWidth() - paddingLeft - paddingRight; int height = getHeight() - paddingTop - paddingBottom; int radius = Math.min(width, height) / 2; canvas.drawCircle(paddingLeft + width / 2, paddingTop + height/2, radius, mPaint); } 上面的代碼很簡單,中心思想就是在繪制的時候考慮到View四周的空白即可,其中圓心和半徑都會考慮到View四周的padding,從而做相應的調整。 <com.ryg.chapter_4.ui.CircleView android:id="@+id/circleView1" android:layout_width="wrap_content" android:layout_height="100dp" android:layout_margin="20dp" android:padding="20dp" android:background="#000000"/> 針對上面的布局參數,我們再次運行一下,結果如圖4-4中的(1)所示,可以發現布局參數中的wrap_content和padding均生效了。 :-: ![](https://img.kancloud.cn/2b/a4/2ba44ed12c4a2ad4c72fe7df8f444f58_1359x403.png) 圖4-4 CircleView運行效果圖 最后,為了讓我們的View更加容易使用,很多情況下我們還需要為其提供自定義屬性,像android:layout_width和android:padding這種以android開頭的屬性是系統自帶的屬性,那么如何添加自定義屬性呢?這也不是什么難事,遵循如下幾步: 第一步,在values目錄下面創建自定義屬性的XML,比如attrs.xml,也可以選擇類似于attrs_circle_view.xml等這種以attrs_開頭的文件名,當然這個文件名并沒有什么限制,可以隨便取名字。針對本例來說,選擇創建attrs.xml文件,文件內容如下: <? xml version="1.0" encoding="utf-8"? > <resources> <declare-styleable name="CircleView"> <attr name="circle_color" format="color" /> </declare-styleable> </resources> 在上面的XML中聲明了一個自定義屬性集合“CircleView”,在這個集合里面可以有很多自定義屬性,這里只定義了一個格式為“color”的屬性“circle_color”,這里的格式color指的是顏色。除了顏色格式,自定義屬性還有其他格式,比如reference是指資源id, dimension是指尺寸,而像string、integer和boolean這種是指基本數據類型。除了列舉的這些還有其他類型,這里就不一一描述了,讀者查看一下文檔即可,這并沒有什么難度。 第二步,在View的構造方法中解析自定義屬性的值并做相應處理。對于本例來說,我們需要解析circle_color這個屬性的值,代碼如下所示。 public CircleView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable. CircleView); mColor = a.getColor(styleable.CircleView_circle_color, Color.RED); a.recycle(); init(); } 這看起來很簡單,首先加載自定義屬性集合CircleView,接著解析CircleView屬性集合中的circle_color屬性,它的id為R.styleable.CircleView_circle_color。在這一步驟中,如果在使用時沒有指定circle_color這個屬性,那么就會選擇紅色作為默認的顏色值,解析完自定義屬性后,通過recycle方法來實現資源,這樣CircleView中所做的工作就完成了。 第三步,在布局文件中使用自定義屬性,如下所示。 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#ffffff" android:orientation="vertical" > <com.ryg.chapter_4.ui.CircleView android:id="@+id/circleView1" android:layout_width="wrap_content" android:layout_height="100dp" android:layout_margin="20dp" app:circle_color="@color/light_green" android:padding="20dp" android:background="#000000"/> </LinearLayout> 上面的布局文件中有一點需要注意,首先,為了使用自定義屬性,必須在布局文件中添加schemas聲明:xmlns:app=http://schemas.android.com/apk/res-auto。在這個聲明中,app是自定義屬性的前綴,當然可以換其他名字,但是CircleView中的自定義屬性的前綴必須和這里的一致,然后就可以在CircleView中使用自定義屬性了,比如:app:circle_color= "@color/light_green"。另外,也有按照如下方式聲明`schemas:xmlns:app=http:// schemas.android.com/apk/res/com. ryg.chapter_4`,這種方式會在apk/res/后面附加應用的包名。但是這兩種方式并沒有本質區別,筆者比較喜歡的是xmlns:app=http://schemas.android.com/ apk/res-auto這種聲明方式。 到這里自定義屬性的使用過程就完成了,運行一下程序,效果如圖4-4中的(2)所示,很顯然,CircleView的自定義屬性circle_color生效了。下面給出CircleView的完整代碼,這時的CircleView已經是一個很規范的自定義View了,如下所示。 public class CircleView extends View { private int mColor = Color.RED; private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); public CircleView(Context context) { super(context); init(); } public CircleView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CircleView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable. CircleView); mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED); a.recycle(); init(); } private void init() { mPaint.setColor(mColor); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(200, 200); } else if (widthSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(200, heightSpecSize); } else if (heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(widthSpecSize, 200); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); final int paddingLeft = getPaddingLeft(); final int paddingRight = getPaddingLeft(); final int paddingTop = getPaddingLeft(); final int paddingBottom = getPaddingLeft(); int width = getWidth() - paddingLeft - paddingRight; int height = getHeight() - paddingTop - paddingBottom; int radius = Math.min(width, height) / 2; canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, radius, mPaint); } } * 2.繼承ViewGroup派生特殊的Layout 這種方法主要用于實現自定義的布局,采用這種方式稍微復雜一些,需要合適地處理ViewGroup的測量、布局這兩個過程,并同時處理子元素的測量和布局過程。在第3章的3.5.3節中,我們分析了滑動沖突的兩種方式并實現了兩個自定義View:HorizontalScroll-ViewEx和StickyLayout,其中HorizontalScrollViewEx就是通過繼承ViewGroup來實現的自定義View,這里會再次分析它的measure和layout過程。 需要說明的是,如果要采用此種方法實現一個很規范的自定義View,是有一定的代價的,這點通過查看LinearLayout等的源碼就知道,它們的實現都很復雜。對于Horizontal-ScrollViewEx來說,這里不打算實現它的方方面面,僅僅是完成主要功能,但是需要規范化的地方會給出說明。 這里再回顧一下HorizontalScrollViewEx的功能,它主要是一個類似于ViewPager的控件,也可以說是一個類似于水平方向的LinearLayout的控件,它內部的子元素可以進行水平滑動并且子元素的內部還可以進行豎直滑動,這顯然是存在滑動沖突的,但是HorizontalScrollViewEx內部解決了水平和豎直方向的滑動沖突問題。關于HorizontalScrollViewEx是如何解決滑動沖突的,請參看第3章的相關內容。這里有一個假設,那就是所有子元素的寬/高都是一樣的。下面主要看一下它的onMeasure和onLayout方法的實現,先看onMeasure,如下所示。 ``` protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int measuredWidth = 0; int measuredHeight = 0; final int childCount = getChildCount(); measureChildren(widthMeasureSpec, heightMeasureSpec); int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); if (childCount == 0) { setMeasuredDimension(0, 0); } else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) { final View childView = getChildAt(0); measuredWidth = childView.getMeasuredWidth() * childCount; measuredHeight = childView.getMeasuredHeight(); setMeasuredDimension(measuredWidth, measuredHeight); } else if (heightSpecMode == MeasureSpec.AT_MOST) { final View childView = getChildAt(0); measuredHeight = childView.getMeasuredHeight(); setMeasuredDimension(widthSpaceSize, childView.getMeasured- Height()); } else if (widthSpecMode == MeasureSpec.AT_MOST) { final View childView = getChildAt(0); measuredWidth = childView.getMeasuredWidth() * childCount; setMeasuredDimension(measuredWidth, heightSpaceSize); } } ``` 這里說明一下上述代碼的邏輯,首先會判斷是否有子元素,如果沒有子元素就直接把自己的寬/高設為0;然后就是判斷寬和高是不是采用了wrap_content,如果寬采用了wrap_content,那么HorizontalScrollViewEx的寬度就是所有子元素的寬度之和;如果高度采用了wrap_content,那么HorizontalScrollViewEx的高度就是第一個子元素的高度。 上述代碼不太規范的地方有兩點:第一點是沒有子元素的時候不應該直接把寬/高設為0,而應該根據LayoutParams中的寬/高來做相應處理;第二點是在測量HorizontalScrollViewEx的寬/高時沒有考慮到它的padding以及子元素的margin,因為它的padding以及子元素的margin會影響到HorizontalScrollViewEx的寬/高。這是很好理解的,因為不管是自己的padding還是子元素的margin,占用的都是HorizontalScrollViewEx的空間。 接著再看一下HorizontalScrollViewEx的onLayout方法,如下所示。 protected void onLayout(boolean changed, int l, int t, int r, int b) { int childLeft = 0; final int childCount = getChildCount(); mChildrenSize = childCount; for (int i = 0; i < childCount; i++) { final View childView = getChildAt(i); if (childView.getVisibility() ! = View.GONE) { final int childWidth = childView.getMeasuredWidth(); mChildWidth = childWidth; childView.layout(childLeft, 0, childLeft + childWidth, childView.getMeasuredHeight()); childLeft += childWidth; } } } 上述代碼的邏輯并不復雜,其作用是完成子元素的定位。首先會遍歷所有的子元素,如果這個子元素不是處于GONE這個狀態,那么就通過layout方法將其放置在合適的位置上。從代碼上來看,這個放置過程是由左向右的,這和水平方向的LinearLayout比較類似。上述代碼的不完美之處仍然在于放置子元素的過程沒有考慮到自身的padding以及子元素的margin,而從一個規范的控件的角度來看,這些都是應該考慮的。下面給出Horizontal-ScrollViewEx的完整代碼,如下所示。 public class HorizontalScrollViewEx extends ViewGroup { private static final String TAG = "HorizontalScrollViewEx"; private int mChildrenSize; private int mChildWidth; private int mChildIndex; // 分別記錄上次滑動的坐標 private int mLastX = 0; private int mLastY = 0; // 分別記錄上次滑動的坐標(onInterceptTouchEvent) private int mLastXIntercept = 0; private int mLastYIntercept = 0; private Scroller mScroller; private VelocityTracker mVelocityTracker; public HorizontalScrollViewEx(Context context) { super(context); init(); } public HorizontalScrollViewEx(Context context, AttributeSet attrs) { super(context, attrs); init(); } public HorizontalScrollViewEx(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } private void init() { if (mScroller == null) { mScroller = new Scroller(getContext()); mVelocityTracker = VelocityTracker.obtain(); } } @Override public boolean onInterceptTouchEvent(MotionEvent event) { boolean intercepted = false; int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { intercepted = false; if (! mScroller.isFinished()) { mScroller.abortAnimation(); intercepted = true; } break; } case MotionEvent.ACTION_MOVE: { int deltaX = x - mLastXIntercept; int deltaY = y - mLastYIntercept; if (Math.abs(deltaX) > Math.abs(deltaY)) { intercepted = true; } else { intercepted = false; } break; } case MotionEvent.ACTION_UP: { intercepted = false; break; } default: break; } Log.d(TAG, "intercepted=" + intercepted); mLastX = x; mLastY = y; mLastXIntercept = x; mLastYIntercept = y; return intercepted; } @Override public boolean onTouchEvent(MotionEvent event) { mVelocityTracker.addMovement(event); int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { if (! mScroller.isFinished()) { mScroller.abortAnimation(); } break; } case MotionEvent.ACTION_MOVE: { int deltaX = x - mLastX; int deltaY = y - mLastY; scrollBy(-deltaX, 0); break; } case MotionEvent.ACTION_UP: { int scrollX = getScrollX(); mVelocityTracker.computeCurrentVelocity(1000); float xVelocity = mVelocityTracker.getXVelocity(); if (Math.abs(xVelocity) >= 50) { mChildIndex = xVelocity > 0 ? mChildIndex -1 : mChildIndex + 1; } else { mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth; } mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize -1)); int dx = mChildIndex * mChildWidth - scrollX; smoothScrollBy(dx, 0); mVelocityTracker.clear(); break; } default: break; } mLastX = x; mLastY = y; return true; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int measuredWidth = 0; int measuredHeight = 0; final int childCount = getChildCount(); measureChildren(widthMeasureSpec, heightMeasureSpec); int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); if (childCount == 0) { setMeasuredDimension(0, 0); } else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) { final View childView = getChildAt(0); measuredWidth = childView.getMeasuredWidth() * childCount; measuredHeight = childView.getMeasuredHeight(); setMeasuredDimension(measuredWidth, measuredHeight); } else if (heightSpecMode == MeasureSpec.AT_MOST) { final View childView = getChildAt(0); measuredHeight = childView.getMeasuredHeight(); setMeasuredDimension(widthSpaceSize, childView.getMeasured- Height()); } else if (widthSpecMode == MeasureSpec.AT_MOST) { final View childView = getChildAt(0); measuredWidth = childView.getMeasuredWidth() * childCount; setMeasuredDimension(measuredWidth, heightSpaceSize); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childLeft = 0; final int childCount = getChildCount(); mChildrenSize = childCount; for (int i = 0; i < childCount; i++) { final View childView = getChildAt(i); if (childView.getVisibility() ! = View.GONE) { final int childWidth = childView.getMeasuredWidth(); mChildWidth = childWidth; childView.layout(childLeft, 0, childLeft + childWidth, childView.getMeasuredHeight()); childLeft += childWidth; } } } private void smoothScrollBy(int dx, int dy) { mScroller.startScroll(getScrollX(), 0, dx, 0, 500); invalidate(); } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); postInvalidate(); } } @Override protected void onDetachedFromWindow() { mVelocityTracker.recycle(); super.onDetachedFromWindow(); } } 繼承特定的View(比如TextView)和繼承特定的ViewGroup(比如LinearLayout)這兩種方式比較簡單,這里就不再舉例說明了,關于第3章中提到的StickyLayout的具體實現,大家可以參看筆者在Github上的開源項目:https://github.com/singwhatiwanna/Pinned-HeaderExpandableListView。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看