自定義View

自定義View有多種形式膜蛔,可以繼承自View,也可以繼承自ViewGroup脖阵,還可以直接繼承Andorid系統(tǒng)現(xiàn)有的View組件皂股,比如TextView、ImageView命黔、LinearLayout等呜呐,且每種方式都有它的使用場景。

  • 繼承View
    這種方式需要重寫onDraw方法悍募,主要用于實(shí)現(xiàn)一些不規(guī)則的布局效果蘑辑,通過xml布局不容易實(shí)現(xiàn)的情況下使用該方式,采用這種方式需要我們自己支持wrap_content并且處理padding等坠宴。

  • 繼承ViewGroup
    該方式主要用于實(shí)現(xiàn)自定義的布局洋魂,把幾個(gè)View重新組合在一起,形成一個(gè)整體的View喜鼓。這種方式比較復(fù)雜副砍,需要合適地處理ViewGroup的測量和布局這兩個(gè)過程,并同時(shí)處理子元素的測量和布局過程庄岖。

  • 繼承系統(tǒng)現(xiàn)有的View
    這種方式適用于擴(kuò)展現(xiàn)有View的功能址晕,比較常見,開發(fā)者不需要自己處理wrap_content并且處理padding等顿锰。

  • 繼承系統(tǒng)現(xiàn)有的ViewGroup
    和方法2類似谨垃,但方法2更接近View的底層實(shí)現(xiàn)。

自定義View的注意事項(xiàng)

1硼控、直接繼承View或者ViewGroup的控件刘陶,如果不在onMeasure方法中對wrao_content做特出處理,那么當(dāng)外界在布局中使用wrap_content時(shí)就無法達(dá)到預(yù)期的效果牢撼,必須要讓這種方式自定義的View支持wrap_content匙隔。

2、另外熏版,還需要處理padding纷责,如果不處理padding,那么padding屬性在這個(gè)自定義View中是不起作用的撼短,如果是繼承自ViewGroup再膳,還需要在onMeasure和onLayout方法中考慮padding和子元素的margin對布局造成的影響,否則自定義View的padding和子元素的margin會不起作用曲横。

3喂柒、當(dāng)自定義View中需要停止線程或者動畫時(shí)不瓶,可以在onDetachedFromWindow方法中執(zhí)行停止動畫或線程的操作。因?yàn)楫?dāng)包含此View的Activity退出或者當(dāng)前View被remove時(shí)灾杰,View的onDetachedFromWindow方法會被調(diào)用蚊丐,和此方法對應(yīng)的是onAttachedToWindow,當(dāng)包含此View的Activity啟動時(shí)艳吠,View的onAttachedToWindow方法會被調(diào)用麦备。且當(dāng)View變得不可見時(shí)我們也需要停止線程和動畫,否則可能會造成內(nèi)存泄漏昭娩。

4凛篙、當(dāng)自定義View帶有滑動嵌套情形時(shí),要處理好滑動沖突题禀,否則會影響View的效果鞋诗。

5膀捷、在自定義View的內(nèi)部最好不要使用Handler迈嘹,因?yàn)樗鼉?nèi)部已經(jīng)提供了post系統(tǒng)的方法,可完全代替Handler的作用全庸。

針對第一二點(diǎn)秀仲,需要處理支持wrap_content的情況時(shí),我們可重寫onMeasure方法壶笼。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(mWidth, mHeight);
    } else if (widthMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(mWidth, heightMeasureSpec);
    } else {
        setMeasuredDimension(widthMeasureSpec, mHeight);
    }

}

當(dāng)然我們需要先定義mWidth和mHeight這兩個(gè)寬高默認(rèn)值神僵,這個(gè)就隨便定義了。接下來處理padding的情況覆劈,也就是計(jì)算時(shí)我們要考慮四個(gè)padding值保礼,比如畫圓心時(shí),需要這樣寫:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    int paddingLeft = getPaddingLeft();
    int paddingRight = getPaddingRight();
    int paddingTop = getPaddingTop();
    int paddingBottom = getPaddingBottom();

    int width = getWidth() - paddingLeft - paddingRight;
    int height = getHeight() - paddingBottom - paddingTop;

    // 大圓半徑
    int radius = Math.min(width, height) / 2;
    canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, radius, borderPaint);

    // 小圓半徑
    int smallRadius = radius - (int) borderWidth;
    canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, smallRadius, mPaint);
}

開發(fā)者還可以根據(jù)自己的需求為自定義View添加自定義屬性责语,一般在xml中屬性以android開頭的都是系統(tǒng)自帶的屬性炮障,添加自定義屬性有如下幾個(gè)步驟:

首先,在values文件夾下創(chuàng)建自定義屬性的xml坤候,一般是attrs.xml胁赢,然后在這個(gè)xml文件中定義自定義屬性集合名,集合中就可以自定義多個(gè)屬性了白筹。比如:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CircleView">

        <attr name="circle_color" format="color" />
        <attr name="border_color" format="color" />
        <attr name="border_width" format="dimension" />
    </declare-styleable>

</resources>

在自定義View的初始化方法中需要獲取到這幾個(gè)自定義的屬性:

public class CircleView extends View {


    private Paint mPaint, borderPaint;
    private int mWidth, mHeight;
    private int circleColor, borderColor;
    private float borderWidth;

    public CircleView(Context context) {
        this(context, null);
    }

    public CircleView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

}
private void init(Context context, AttributeSet attrs) {
    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
    circleColor = a.getColor(R.styleable.CircleView_circle_color, Color.GREEN);
    borderColor = a.getColor(R.styleable.CircleView_border_color, Color.WHITE);
    borderWidth = a.getDimension(R.styleable.CircleView_border_width, 0);
    a.recycle();

    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setColor(circleColor);

    borderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    borderPaint.setColor(borderColor);

    mWidth = mHeight = 200;
}

最后在xml中使用自定義View并添加自定義屬性:

<com.shenhuniurou.viewdemo.CircleView
        android:id="@+id/circleView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="20dp"
        android:background="@color/colorAccent"
        android:padding="10dp"
        app:border_color="@android:color/holo_blue_light"
        app:border_width="5dp"
        app:circle_color="@android:color/holo_orange_light" />

效果如圖所示:

我們知道在xml中使用自定義屬性時(shí)智末,前綴是app:,而且必須在布局文件開頭添加schemas聲明:xmlns:app="http://schemas.android.com/apk/res-auto"徒河,當(dāng)然這個(gè)前綴app可以自定義系馆,但必須保證這個(gè)前綴和布局中自定義屬性的前綴保持一致。比如你的聲明是xmlns:shenhuniurou="http://schemas.android.com/apk/res-auto"顽照,那么自定義屬性就得這么寫:shenhuniurou:border_width="5dp"它呀。

以上是我們基于View來繼承的自定義View的實(shí)現(xiàn),下面我們來試試?yán)^承ViewGroup的自定義View。

package com.shenhuniurou.viewdemo;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;

/**
 * Created by Daniel on 2017/8/5.
 */

public class CustomViewPager extends ViewGroup {

    private Scroller mScroller;
    private VelocityTracker mVelocityTracker;

    private int mChildWidth;// 子View的寬度
    private int mChildIndex;// 子View的位置索引
    private int mChildrenSize;// 子View個(gè)數(shù)

    private int mLastXIntercept = 0;
    private int mLastYIntercept = 0;

    private int mLastX = 0;
    private int mLastY = 0;


    public CustomViewPager(Context context) {
        this(context, null);
    }

    public CustomViewPager(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mScroller = new Scroller(getContext());
        mVelocityTracker = VelocityTracker.obtain();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();

        switch (ev.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;
        }
        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);
                // 計(jì)算水平速度
                float xVelocity = mVelocityTracker.getXVelocity();

                // 這里了是模擬ViewPager快速滑動時(shí)下隧,即使只滑動了一小段距離,也可以滑到下一頁去
                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;

                mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
                invalidate();

                mVelocityTracker.clear();

                break;
        }

        mLastX = x;
        mLastY = y;

        return true;
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }

    @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, measuredHeight);
        } 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;
            }
        }
    }


    @Override
    protected void onDetachedFromWindow() {
        mVelocityTracker.recycle();
        super.onDetachedFromWindow();
    }


}

package com.shenhuniurou.viewdemo;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.ListView;

/**
 * Created by Daniel on 2017/8/5.
 */

public class CustomListView extends ListView {

    private CustomViewPager customViewPager;

    // 分別記錄上次滑動的坐標(biāo)
    private int mLastX = 0;
    private int mLastY = 0;

    public CustomListView(Context context) {
        this(context, null);
    }

    public CustomListView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomListView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    public void setCustomViewPager(CustomViewPager customViewPager) {
        this.customViewPager = customViewPager;
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                customViewPager.requestDisallowInterceptTouchEvent(true);
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                if (Math.abs(deltaX) > Math.abs(deltaY)) {
                    customViewPager.requestDisallowInterceptTouchEvent(false);
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                break;
            }
            default:
                break;
        }

        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(event);
    }

}
<com.shenhuniurou.viewdemo.CustomViewPager
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent">


</com.shenhuniurou.viewdemo.CustomViewPager>
private void initView() {
    LayoutInflater inflater = getLayoutInflater();
    mListContainer = (CustomViewPager) findViewById(R.id.container);
    int screenWidth = MyUtils.getScreenMetrics(this).widthPixels;

    // 往ViewGroup中添加ListView谓媒,這里是把含有ListView的一整個(gè)布局加進(jìn)去
    for (int i = 0; i < 5; i++) {
        ViewGroup layout = (ViewGroup) inflater.inflate(R.layout.content_layout, mListContainer, false);
        layout.getLayoutParams().width = screenWidth;
        TextView textView = (TextView) layout.findViewById(R.id.title);
        textView.setText("第 " + (i + 1) + "頁");
        layout.setBackgroundColor(Color.rgb(255 / (i + 10), 255 / (i + 10), 10));
        addListView(layout);
        mListContainer.addView(layout);
    }
}

private void addListView(ViewGroup layout) {
    CustomListView listView = (CustomListView) layout.findViewById(R.id.list);
    List<String> datas = new ArrayList<>();
    for (int i = 0; i < 50; i++) {
        datas.add("item " + i);
    }

    ArrayAdapter<String> adapter = new ArrayAdapter<>(this, R.layout.content_list_item, R.id.name, datas);
    listView.setCustomViewPager(mListContainer);
    listView.setAdapter(adapter);
    listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
        @Override
        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
            Toast.makeText(MainActivity.this, "click item", Toast.LENGTH_SHORT).show();

        }
    });
}

主要關(guān)注的就是onMeasure方法中對于寬高是wrap_content的處理和onLayout方法中對子元素的布局淆院,CustomListView是為了解決橫向滑動沖突而自定義的ListView,最后的效果圖:

當(dāng)然這都是一些最基本的自定義View句惯,想要達(dá)到隨心所欲的自定義View的境界土辩,推薦看這兩個(gè)系列:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市抢野,隨后出現(xiàn)的幾起案子拷淘,更是在濱河造成了極大的恐慌,老刑警劉巖指孤,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件启涯,死亡現(xiàn)場離奇詭異,居然都是意外死亡恃轩,警方通過查閱死者的電腦和手機(jī)结洼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來叉跛,“玉大人松忍,你說我怎么就攤上這事】昀澹” “怎么了鸣峭?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長酥艳。 經(jīng)常有香客問我摊溶,道長,這世上最難降的妖魔是什么玖雁? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任更扁,我火速辦了婚禮,結(jié)果婚禮上赫冬,老公的妹妹穿的比我還像新娘浓镜。我一直安慰自己,他們只是感情好劲厌,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布膛薛。 她就那樣靜靜地躺著,像睡著了一般补鼻。 火紅的嫁衣襯著肌膚如雪哄啄。 梳的紋絲不亂的頭發(fā)上雅任,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天,我揣著相機(jī)與錄音咨跌,去河邊找鬼沪么。 笑死,一個(gè)胖子當(dāng)著我的面吹牛锌半,可吹牛的內(nèi)容都是我干的禽车。 我是一名探鬼主播,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼刊殉,長吁一口氣:“原來是場噩夢啊……” “哼殉摔!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起记焊,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤逸月,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后遍膜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體碗硬,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年捌归,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了肛响。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片岭粤。...
    茶點(diǎn)故事閱讀 40,102評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡惜索,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出剃浇,到底是詐尸還是另有隱情巾兆,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布虎囚,位于F島的核電站角塑,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏淘讥。R本人自食惡果不足惜圃伶,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蒲列。 院中可真熱鬧窒朋,春花似錦、人聲如沸蝗岖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽抵赢。三九已至欺劳,卻和暖如春唧取,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背划提。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工枫弟, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人鹏往。 一個(gè)月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓媒区,卻偏偏與公主長得像,于是被迫代替她去往敵國和親掸犬。 傳聞我的和親對象是個(gè)殘疾皇子袜漩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容