1. 程式人生 > >自定義View框架完全解析

自定義View框架完全解析

前言

       在Android中有很多的控制元件來供大家使用,但是和強大的IOS相比,Android所提供的控制元件平淡了許多,由於Android的開源可以讓每個開發者都能建立自己的檢視控制元件來滿足自己的需求,正因為這樣就出現各種各樣的自定義控制元件,久而久之就形成了自定義檢視框架。

這裡介紹兩種方法

1、給每一個需要配置自定義屬性的子控制元件外面包裹一層自定義容器

2、自定義LayoutInflater將xml佈局載入進來(例項化裡面每一個控制元件,並且將控制元件裡面的引數設定到控制元件類身上)

Part 1、子控制元件外面包裹一層自定義容器

效果~

    

功能分析:

外面一層為自定義ScrollView用來監聽滑動,裡面是一個容器,這裡為什麼要是自定義的呢?這裡的自定義容器作用是為了動態的為每個View包裹一層容器,如果你不適用自定義的容器那你將要對每一個控制元件包裹一層自定義容器,這樣便不能實現自定義View框架了。

接下來說一下具體的流程:

首先我們先定義一些自定義屬性

    <declare-styleable name="DiscrollView_LayoutParams">
        <attr name="discrollve_alpha" format="boolean"/>
        <attr name="discrollve_scaleX" format="boolean"/>
        <attr name="discrollve_scaleY" format="boolean"/>
        <attr name="discrollve_fromBgColor" format="color"/>
        <attr name="discrollve_toBgColor" format="color"/>
        <attr name="discrollve_translation"/>
        <attr name="discrollve_rotate" format="integer"></attr>
    </declare-styleable>

    <attr name="discrollve_translation">
        <flag name="fromTop" value="0x01"/>
        <flag name="fromBottom" value="0x02"/>
        <flag name="fromLeft" value="0x04"/>
        <flag name="fromRight" value="0x08"/>
    </attr>
然後將自定義屬性應用到佈局中
<?xml version="1.0" encoding="utf-8"?>
<com.andly.administrator.andly_animframe.copydiscrollview.MyScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:discrollve="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_copy_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.andly.administrator.andly_animframe.copydiscrollview.CopyMainActivity">

    <com.andly.administrator.andly_animframe.copydiscrollview.MyLinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TextView
            android:layout_width="match_parent"
            android:layout_height="600dp"
            android:background="@android:color/white"
            android:textColor="@android:color/black"
            android:textSize="25sp"
            android:padding="25dp"
            tools:visibility="gone"
            android:gravity="center"
            android:fontFamily="serif"
            android:text="" />
        <View
            android:layout_width="match_parent"
            android:layout_height="200dp"
            discrollve:discrollve_fromBgColor="@android:color/holo_red_dark"
            discrollve:discrollve_toBgColor="@android:color/holo_blue_dark"
            />
        <ImageView
            android:layout_width="200dp"
            android:layout_height="150dp"
            discrollve:discrollve_alpha="true"
            discrollve:discrollve_translation="fromLeft|fromBottom"
            android:src="@drawable/pic2" />
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="20dp"
            android:fontFamily="serif"
            android:gravity="center"
            android:text="眼見范冰冰與李晨在一起了,孩子會取名李冰冰;李冰冰唯有嫁給範偉,生個孩子叫范冰冰,方能扳回一城。"
            android:textSize="23sp"
            discrollve:discrollve_alpha="true"
            discrollve:discrollve_translation="fromBottom" />
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="20dp"
            android:layout_gravity="center"
            android:src="@drawable/pic4"
            discrollve:discrollve_scaleX="true"
            discrollve:discrollve_scaleY="true"  />
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="20dp"
            android:layout_gravity="center"
            android:src="@drawable/pic5"
            discrollve:discrollve_translation="fromLeft|fromBottom"
            discrollve:discrollve_rotate="360"/>
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="20dp"
            android:layout_gravity="center"
            android:src="@drawable/pic6"
            discrollve:discrollve_translation="fromLeft|fromBottom"
            />

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="20dp"
            android:layout_gravity="center"
            android:src="@drawable/pic7"
            discrollve:discrollve_translation="fromLeft|fromBottom"
            />
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="20dp"
            android:layout_gravity="center"
            android:src="@drawable/pic8"
            discrollve:discrollve_translation="fromLeft|fromBottom"
            />
    </com.andly.administrator.andly_animframe.copydiscrollview.MyLinearLayout>
</com.andly.administrator.andly_animframe.copydiscrollview.MyScrollView>

接下來自定義LinearLayout,它的作用不光為每個控制元件包裹一層容器還需要將自定義屬性傳遞給包裹的容器

    @Override
    public void addView(View child, int index, ViewGroup.LayoutParams params) {
        MyLayoutParams ps = (MyLayoutParams) params;
        if (!isCustomValue(ps)) {
            super.addView(child, index, params);
        } else {
            layout = new MyFramementLayout(getContext());
            layout.setDiscrollve_alpha(ps.discrollve_alpha);
            layout.setDiscrollve_scaleX(ps.discrollve_scaleX);
            layout.setDiscrollve_scaleY(ps.discrollve_scaleY);
            layout.setDiscrollve_fromBgColor(ps.discrollve_fromBgColor);
            layout.setDiscrollve_toBgColor(ps.discrollve_toBgColor);
            layout.setDiscrollve_translation(ps.discrollve_translation);
            layout.setDiscrollve_rotate(ps.discrollve_rotate);
            layout.addView(child);
            super.addView(layout, index, params);
        }

    }
思想就是在super.addView之前將child包裹一層FragmentLayout,其中params包含著自定義屬性的東西,至此我們來看一下setContentView的原始碼,一步步呼叫之後執行
    void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs,
            boolean finishInflate) throws XmlPullParserException, IOException {
        final int depth = parser.getDepth();
        int type;
        while (((type = parser.next()) != XmlPullParser.END_TAG ||
                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
            if (type != XmlPullParser.START_TAG) {
                continue;
            }
            final String name = parser.getName();
            
            if (TAG_REQUEST_FOCUS.equals(name)) {
                parseRequestFocus(parser, parent);
            } else if (TAG_INCLUDE.equals(name)) {
                if (parser.getDepth() == 0) {
                    throw new InflateException("<include /> cannot be the root element");
                }
                parseInclude(parser, parent, attrs);
            } else if (TAG_MERGE.equals(name)) {
                throw new InflateException("<merge /> must be the root element");
            } else if (TAG_1995.equals(name)) {
                final View view = new BlinkLayout(mContext, attrs);
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                rInflate(parser, view, attrs, true);
                viewGroup.addView(view, params);                
            } else {
                final View view = createViewFromTag(parent, name, attrs);
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                rInflate(parser, view, attrs, true);
                viewGroup.addView(view, params);
            }
        }
        if (finishInflate) parent.onFinishInflate();
    }
從原始碼看出在呼叫super.addView之前呼叫了params=root.generateLayoutParams(attrs),將AttributeSet轉化為LayoutParams,於此我們可以在此方法裡面來得到相應的自定義屬性
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        //解析xml
        return new MyLayoutParams(this.getContext(), attrs);
    }    @Override
    public static class MyLayoutParams extends LinearLayout.LayoutParams {
        boolean discrollve_alpha;
        boolean discrollve_scaleX;
        boolean discrollve_scaleY;
        int discrollve_fromBgColor;
        int discrollve_toBgColor;
        int discrollve_translation;
        int discrollve_rotate;

        public MyLayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
            TypedArray ta = c.obtainStyledAttributes(attrs, R.styleable.DiscrollView_LayoutParams);
            discrollve_alpha = ta.getBoolean(R.styleable.DiscrollView_LayoutParams_discrollve_alpha, false);
            discrollve_scaleX = ta.getBoolean(R.styleable.DiscrollView_LayoutParams_discrollve_scaleX, false);
            discrollve_scaleY = ta.getBoolean(R.styleable.DiscrollView_LayoutParams_discrollve_scaleY, false);
            discrollve_fromBgColor = ta.getInt(R.styleable.DiscrollView_LayoutParams_discrollve_fromBgColor, -1);
            discrollve_toBgColor = ta.getInt(R.styleable.DiscrollView_LayoutParams_discrollve_toBgColor, -1);
            discrollve_translation = ta.getInt(R.styleable.DiscrollView_LayoutParams_discrollve_translation, -1);
            discrollve_rotate = ta.getInt(R.styleable.DiscrollView_LayoutParams_discrollve_rotate,-1);
            ta.recycle();
        }
    }
接下來我們只需要定義包裹的容器就可以在獲取到相應的自定義屬性,注意的是在此包裹容器中需要有和MyLayoutParams類中相同的屬性並實現相應的動畫
    @Override
    public void scrollViews(float rate) {//rate:0 - 1
        if (discrollve_alpha) {
            setAlpha(rate);
        }
        if (discrollve_scaleX) {
            setScaleX(rate);
        }
        if (discrollve_scaleY) {
            setScaleY(rate);
        }
        if (isTranslation(TRANSLATION_FROM_TOP)) {//從-height~0
            setTranslationY(-mHeight * (1 - rate));
        }
        if (isTranslation(TRANSLATION_FROM_BOTTOM)) {//從height~0
            setTranslationY(mHeight * (1 - rate));
        }
        if (isTranslation(TRANSLATION_FROM_LEFT)) {//-width~0
            setTranslationX(-mWidth * (1 - rate));
        }
        if (isTranslation(TRANSLATION_FROM_RIGHT)) {//width~0
            setTranslationX(mWidth * (1 - rate));
        }
        if(discrollve_rotate != -1){
            setRotation(discrollve_rotate*rate);
        }
        //顏色漸變
        if (discrollve_fromBgColor != -1 && discrollve_toBgColor != -1) {
            setBackgroundColor((Integer) mEvaluator.evaluate(rate, discrollve_fromBgColor, discrollve_toBgColor));
        }

    }
最後只需要在自定義ScrollView在滑動監聽開啟動畫
    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        mHeight = getHeight();
        for (int i = 0; i < mContent.getChildCount(); i++) {
            if (!(mContent.getChildAt(i) instanceof MyScrollViewInterface)) {//這裡過濾掉沒有定義自定義屬性的控制元件
                continue;
            }
            MyScrollViewInterface viewInterface = (MyScrollViewInterface) mContent.getChildAt(i);
            View view = mContent.getChildAt(i);//得到包裹的容器
            int childHeight = view.getHeight();//包裹容器的高度
            int childTop = view.getTop();
            int absoluteTop = childTop - t;//包裹容器頂部到螢幕頂部
            if(absoluteTop <= mHeight){
                int childChangeHeight = mHeight - absoluteTop;//得到包裹容器未顯示的高度
                viewInterface.scrollViews(clamp(childChangeHeight / (float) childHeight, 1.0f, 0f));
            }
        }
    }
    private float clamp(float s, float max, float min) {
        return Math.max(Math.min(s, max), min);
    }
這樣就定義了簡易的自定義View框架

Part 2、自定義LayoutInflater將xml佈局載入進來

效果~

    

根據上面的原始碼分析繼續進入便走到了LayoutInflater的createViewFromTag的方法

    /*
     * default visibility so the BridgeInflater can override it.
     */
    View createViewFromTag(View parent, String name, AttributeSet attrs) {
        ......
        try {
            View view;
            if (mFactory2 != null) view = mFactory2.onCreateView(parent, name, mContext, attrs);
            else if (mFactory != null) view = mFactory.onCreateView(name, mContext, attrs);
            else view = null;

            if (view == null && mPrivateFactory != null) {
                view = mPrivateFactory.onCreateView(parent, name, mContext, attrs);
            }
            //(2)
            if (view == null) {
                if (-1 == name.indexOf('.')) {
                    view = onCreateView(parent, name, attrs);
                } else {
                    view = createView(name, null, attrs);
                }
            }

            if (DEBUG) System.out.println("Created view is: " + view);
            return view;
        ......
    }

通過上面的程式碼不難想象我們可以自定義LayoutInflater類傳入一個自定義的Factory類,然後在自定義的Factory類onCreateView方法裡面獲取到每個子View的屬性,但值得注意的是通過這種方法來實現的話,需要實現(2)下面程式碼

首先我們先定義自定義屬性

    <attr name="a_in" format="float" />
    <attr name="a_out" format="float" />
    <attr name="x_in" format="float" />
    <attr name="x_out" format="float" />
    <attr name="y_in" format="float" />
    <attr name="y_out" format="float" />
定義自己的檢視解析器
public class ParallaxLayoutInflater extends LayoutInflater {
	private ParallaxFragment fragment;
	protected ParallaxLayoutInflater(LayoutInflater original, Context newContext,ParallaxFragment fragment) {
		super(original, newContext);
		this.fragment = fragment;
		//重新設定佈局載入器的工廠
		//工廠:建立佈局檔案中所有的檢視
		setFactory(new ParallaxFactory(this));
	}
	@Override
	public LayoutInflater cloneInContext(Context newContext) {
		return new ParallaxLayoutInflater(this,newContext,fragment);
	}
}
自定義解析工廠類
	class ParallaxFactory implements LayoutInflater.Factory{
		private LayoutInflater inflater;
		private final String[] sClassPrefix = {
				"android.widget.",
				"android.view."
		};
		public ParallaxFactory(LayoutInflater inflater) {
			this.inflater = inflater;
		}
		@Override
		public View onCreateView(String name, Context context,AttributeSet attrs) {
			View view = null;
			if (view == null) {
				view = createViewOrFailQuietly(name,context,attrs);//建立View的過程
			}
			if (view != null) {
				setViewTag(view,context,attrs);//獲取自定義的屬性,通過相應的標籤關聯到檢視上
				fragment.getParallaxViews().add(view);
			}
			return view;
		}
	}
tips:

1、在onCreateView方法中建立View

	private View createViewOrFailQuietly(String name, String prefix,Context context,AttributeSet attrs) {
			try {
				return inflater.createView(name, prefix, attrs);//通過系統的Inflater來建立檢視
			} catch (Exception e) {
				return null;
			}
		}
		private View createViewOrFailQuietly(String name, Context context,AttributeSet attrs) {
			if (name.contains(".")) {//自定義控制元件將字首設定為空
				createViewOrFailQuietly(name, null, context, attrs);
			}
			for (String prefix : sClassPrefix) {//系統檢視,這裡通過對每個字首都進行遍歷判空來判斷是否為真正的字首
				View view = createViewOrFailQuietly(name, prefix, context, attrs);
				if (view != null) {
					return view;
				}
			}
			return null;
		}
2、獲取自定義屬性
	private void setViewTag(View view, Context context, AttributeSet attrs) {
			int[] attrIds = {
					R.attr.a_in,
					R.attr.a_out,
					R.attr.x_in,
					R.attr.x_out,
					R.attr.y_in,
					R.attr.y_out};
			TypedArray a = context.obtainStyledAttributes(attrs, attrIds);
			if (a != null && a.length() > 0) {
				ParallaxViewTag tag = new ParallaxViewTag();
				tag.alphaIn = a.getFloat(0, 0f);
				tag.alphaOut = a.getFloat(1, 0f);
				tag.xIn = a.getFloat(2, 0f);
				tag.xOut = a.getFloat(3, 0f);
				tag.yIn = a.getFloat(4, 0f);
				tag.yOut = a.getFloat(5, 0f);
				view.setTag(R.id.parallax_view_tag,tag);
			}
			a.recycle();

		}
最後就是在ViewPager監聽滑動事件onPageScrolled方法中進行動畫
	@Override
	public void onPageScrolled(int position, float positionOffset,
			int positionOffsetPixels) {
		this.containerWidth = getWidth();
		//獲取到進入的頁面
		ParallaxFragment inFragment = null;
		try {
			inFragment = fragments.get(position - 1);
		} catch (Exception e) {}
		
		//獲取到退出的頁面
		ParallaxFragment outFragment = null;
		try {
			outFragment = fragments.get(position);
		} catch (Exception e) {}
		
		if (inFragment != null) {
			List<View> inViews = inFragment.getParallaxViews();//獲取到Fragment上的所有檢視,實現動畫效果
			if (inViews != null) {
				for (View view : inViews) {
					ParallaxViewTag tag = (ParallaxViewTag) view.getTag(R.id.parallax_view_tag);//獲取標籤,從標籤中獲取所有動畫的引數
					if (tag == null) {//tag為空的時候沒有設定自定義屬性
						continue;
					}
				ViewHelper.setTranslationY(view, (containerWidth - positionOffsetPixels) * tag.yIn);
				ViewHelper.setTranslationX(view, (containerWidth - positionOffsetPixels) * tag.xIn);
			}
			}
		}
		
		if(outFragment != null){
			List<View> outViews = outFragment.getParallaxViews();
			if (outViews != null) {
				for (View view : outViews) {
					ParallaxViewTag tag = (ParallaxViewTag) view.getTag(R.id.parallax_view_tag);
					if (tag == null) {
						continue;
					}
					ViewHelper.setTranslationY(view, 0 - positionOffsetPixels * tag.yOut);
					ViewHelper.setTranslationX(view, 0 - positionOffsetPixels * tag.xOut);
				}
			}
		}
		
	}
相應的演算法比較簡單,下面通過判斷pasition的位置來動態改變小人的顯示和隱藏
	@Override
	public void onPageSelected(int position) {
		if (position == adapter.getCount() - 1) {
			iv_man.setVisibility(INVISIBLE);
		}else{
			iv_man.setVisibility(VISIBLE);
		}
	}
通過判斷滑動和停止滑動來動態改變小人的動畫執行與停止
	@Override
	public void onPageScrollStateChanged(int state) {
		AnimationDrawable animation = (AnimationDrawable) iv_man.getBackground();
		switch (state) {
		case ViewPager.SCROLL_STATE_DRAGGING:
			animation.start();
			break;
			
		case ViewPager.SCROLL_STATE_IDLE:
			animation.stop();
			break;
			
		default:
			break;
		}
	}
效果~