Android中自定義控件的寫法

一:自定義控件

假如我們在寫一個程序時,需要圓形的頭像,而原有圖片是方形的,此時我們一般有兩種方式可以將方形頭像處理爲圓形顯示:第一就是利用畫布畫筆,採用混合模式中的DST來處理方形頭像,將其處理爲圓形後再拿到imageview 中顯示,一般代碼實現如下:

private static Bitmap getCircleAvatar(Context context,Bitmap avatar) {
Bitmap bitmap=Bitmap.createBitmap(avatar.getWidth(), avatar.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas=new Canvas(bitmap);
Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.BLACK);
float radius=Math.min(avatar.getWidth(), avatar.getHeight())/2;
//混合模式中的DST
canvas.drawCircle(avatar.getWidth()/2, avatar.getHeight()/2, radius, paint);
paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));
//混合模式中的SRC
canvas.drawBitmap(avatar, 0, 0,paint);
//畫白邊
paint.setColor(Color.WHITE);
paint.setStyle(Paint.Style.STROKE);
float strokeWidth=TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 2,context.getResources().getDisplayMetrics());
paint.setStrokeWidth(strokeWidth);
canvas.drawCircle(avatar.getWidth()/2, avatar.getHeight()/2, radius, paint);
return bitmap;
}

第二種方式就是對原有圖片不做任何處理,而是直接對imageview進行處理,讓其將方形頭像直接顯示爲圓形頭像,一般寫法分以下幾步:

1.我們需要在res/values/attrs下新建一個xml文件,並在裏面定義相關屬性,代碼可如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CircleView">
        <attr name="boder_width" format="dimension"></attr>
        <attr name="boder_color" format="color"></attr>
    </declare-styleable>
</resources>

2.我們現在src的包下先建立一個類,這裏暫且命名爲CircleView,讓他繼承自ImageView,這樣我們就可以省去很多定義ImageView的代碼,大部分的屬性都是可以使用的,然後在ImageView中的一個構造方法中對相關屬性進行初始化,並改寫ImageView默認的setImageBitmap方法,代碼實現可如下:

public class CircleView extends ImageView{
int borderWidth;
int borderColor;

public CircleView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray t=context.obtainStyledAttributes(attrs, R.styleable.CircleView);
borderWidth=t.getDimensionPixelSize(R.styleable.CircleView_boder_width, 0);
borderColor=t.getColor(R.styleable.CircleView_boder_color, Color.WHITE);
t.recycle();
}

@Override
public void setImageBitmap(Bitmap bm) {
// bm-->xfermode-->bitmap
Bitmap bitmap=Bitmap.createBitmap(bm.getWidth(), bm.getHeight(), Config.ARGB_8888);
Canvas canvas=new Canvas(bitmap);
Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.BLACK);
float radius=Math.min(bm.getWidth(), bm.getHeight())/2;
canvas.drawCircle(bm.getWidth()/2, bm.getHeight()/2, radius, paint);
paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));
canvas.drawBitmap(bm, 0,0, paint);

paint.setStyle(Style.STROKE);
paint.setStrokeWidth(borderWidth);
paint.setColor(borderColor);
canvas.drawCircle(bm.getWidth()/2, bm.getHeight()/2, radius, paint);
setScaleType(ScaleType.CENTER);
super.setImageBitmap(bitmap);
}


}

3.在MainActivity的佈局文件中引入寫好的CircleView屬性,並對其進行定義,代碼實現可如下:

<com.abcxxx.myviewdemo.CircleView 
   android:id="@+id/cv"
   android:layout_width="200dp"
   android:layout_height="200dp"
   android:layout_centerInParent="true"
   android:src="@drawable/ic_launcher"
   myapp:boder_width="2dp"
   myapp:boder_color="#ff00ff00"
   />

這裏我們需要注意一點的是,我們需要在MainActivity的佈局文件根屬性中添加如下命名空間註釋,代碼如下:

xmlns:myapp="http://schemas.android.com/apk/res/com.xxxx.myviewdemo"

說明:其中myapp是可以隨意自定義的,一般起名爲myapp

com.xxxx.myviewdemo,爲項目包名,並非CircleView所在包的包名,其餘內容可與根佈局的xmlns:android=" "下內容相同,刪除最後的android即可

4.在MainActivity中初始化CircleView,並將其顯示出來。

CircleView cv;

cv=(CircleView) findViewById(R.id.cv);
Bitmap bm=BitmapFactory.decodeResource(getResources(), R.drawable.b);
cv.setImageBitmap(bm);

 

這樣一個自定義view控件就寫好了,以上基本實現了將一個方形頭像顯示爲圓形頭像的轉變,還有更多實現方法及功能,有待研究和實現。

二:自定義View之組合式自定義view

 

組合式自定義View就是把現有的各種View根據需求組合到一起。
步驟:
1)利用佈局文件,將需要的這些View“擺放”好。
2)寫自定義View類,繼承自第一步中佈局文件的根佈局。
3)自定義View的構造器中,利用LayoutInflater方法將第一步的佈局膨脹後添加到自定義View中。
4)根據需求,添加一些操作組合中各個子View的API
以音樂播放器爲例,相關代碼實現如下:
1.首先是自定義view佈局:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >
    <FrameLayout 
        android:id="@+id/disc"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="15dp">
        <ImageView 
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/disc"/>
        <com.demo.musicplayer.myview.CircleImageView 
            android:id="@+id/disc_albumpic"
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:src="@drawable/cover_default"
            android:layout_gravity="center"/>
    </FrameLayout>
    <ImageView 
        android:id="@+id/pin"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="top"
        android:layout_marginLeft="15dp"
        android:layout_marginTop="10dp"
        android:src="@drawable/pin"/>
</FrameLayout>

2.由於上面的佈局我使用了FrameLayout根佈局,因此,在下面自定義view類時,需要繼承自FrameLayout,實現如下:

package com.demo.musicplayer.myview;

import android.content.Context;
import android.graphics.Bitmap;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.LinearInterpolator;
import android.view.animation.RotateAnimation;
import android.widget.FrameLayout;
import android.widget.ImageView;

import com.demo.musicplayer.R;

public class DiscView extends FrameLayout{
ImageView pin;//唱針
CircleImageView civ;//顯示專輯圖片的圓形view
FrameLayout disc;//黑色唱片+專輯圖片

public DiscView(Context context, AttributeSet attrs) {
super(context, attrs);
LayoutInflater inflater =(LayoutInflater) context.getSystemService(context.LAYOUT_INFLATER_SERVICE);
View view=inflater.inflate(R.layout.discview_layout, this);

pin=(ImageView) view.findViewById(R.id.pin);
civ=(CircleImageView) view.findViewById(R.id.disc_albumpic);
disc=(FrameLayout) view.findViewById(R.id.disc);
}
//設置專輯圖片bitmap
public void setAlbumPic(Bitmap bitmap){
civ.setImageBitmap(bitmap);
}
//設置專輯圖片resId
public void setAlbumPic(int resId){
civ.setImageResource(resId);
}
//播放歌曲時,讓唱片滾動,讓唱針移動
public void startRotate(){
pin.clearAnimation();
disc.clearAnimation();
//唱針的旋轉動畫
RotateAnimation pinAnim=new RotateAnimation(0, 
20, 
Animation.RELATIVE_TO_SELF, 
0.0f, 
Animation.RELATIVE_TO_SELF, 
0.0f);
pinAnim.setDuration(2000);
//當動畫執行完畢的時候,動畫影像停留住
pinAnim.setFillAfter(true);
//唱盤動畫
RotateAnimation discAnim=new RotateAnimation(0, 
359, 
Animation.RELATIVE_TO_SELF, 
0.5f, 
Animation.RELATIVE_TO_SELF, 
0.5f);
discAnim.setDuration(5000);
discAnim.setRepeatCount(Animation.INFINITE);
//爲動畫設置一個加速器讓唱盤勻速轉動
discAnim.setInterpolator(new LinearInterpolator());

pin.startAnimation(pinAnim);
disc.startAnimation(discAnim);
}
//歌曲停止播放,動畫停止
public void stopRotate(){
pin.clearAnimation();
disc.clearAnimation();
//唱針的旋轉動畫
RotateAnimation pinAnim=new RotateAnimation(20, 
0, 
Animation.RELATIVE_TO_SELF, 
0.0f, 
Animation.RELATIVE_TO_SELF, 
0.0f);
pinAnim.setDuration(2000);
pin.startAnimation(pinAnim);
}
public CircleImageView getCircleImageView(){
return civ;
}

}

3.在主佈局界面引入自定義view佈局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/bg"
    android:orientation="vertical" >

    <include
        android:id="@+id/headerview"
        layout="@layout/headerview_layout" />

    <com.demo.musicplayer.myview.DiscView
        android:id="@+id/discview"
        android:layout_width="240dp"
        android:layout_height="240dp"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="10dp" />

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_marginTop="10dp"
        android:orientation="horizontal" >

        <ImageView
            android:id="@+id/favorite"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_alignParentLeft="true"
            android:layout_marginLeft="5dp"
            android:src="@drawable/play_favorite_selector" />

        <ImageView
            android:id="@+id/download"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_alignParentRight="true"
            android:layout_marginRight="5dp"
            android:src="@drawable/download" />
    </RelativeLayout>

    <SeekBar
        android:id="@+id/sbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        android:layout_marginRight="10dp"
        android:layout_marginTop="5dp" />

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="30dp"
        android:layout_marginTop="5dp"
        android:orientation="horizontal" >

        <TextView
            android:id="@+id/current"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_alignParentLeft="true"
            android:layout_marginLeft="5dp"
            android:text="01:42"
            android:textSize="16sp" />

        <TextView
            android:id="@+id/duration"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_alignParentRight="true"
            android:layout_marginRight="5dp"
            android:text="01:42"
            android:textSize="16sp" />
    </RelativeLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="35dp"
        android:layout_marginLeft="20dp"
        android:layout_marginRight="20dp"
        android:layout_marginTop="10dp"
        android:orientation="horizontal" >

        <ImageButton
            android:id="@+id/previous"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:gravity="right"
            android:src="@drawable/previous" />

        <ImageButton
            android:id="@+id/pause"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:src="@drawable/play_pause_selector" />

        <ImageButton
            android:id="@+id/next"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:src="@drawable/next" />
    </LinearLayout>
</LinearLayout>

三、搜索框的首字母索引側邊欄(結合listview下拉刷新)

效果類似如下:

實現步驟如下:

1.先在mainactivity中對搜索框的相關參數進行初始化,將數據處理放到listview顯示,代碼實現如下:

public class CitySearchActivity extends Activity {

EditText etCityname;
ListView listView;
List<String> result;
ArrayAdapter<String> adapter;

List<CitynameBean> cities;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_city_search);

if(MyApp.cities!=null && MyApp.cities.size()>0){
cities = new ArrayList<CitynameBean>(MyApp.cities);
Log.d("TAG","城市名稱:"+cities.toString());
}else{
//TODO 發起網絡訪問獲得cities
}

listView = (ListView) findViewById(R.id.lv_citysearch_cities);
listView.setOnItemClickListener(new OnItemClickListener() {

@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int arg2,
long arg3) {
String city = adapter.getItem(arg2);
Intent data = new Intent();
data.putExtra("city", city);
setResult(RESULT_OK, data );
finish();
}
});
etCityname = (EditText) findViewById(R.id.et_citysearch_cityname);
etCityname.addTextChangedListener(new TextWatcher() {

@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}

@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
}

@Override
public void afterTextChanged(Editable s) {
//獲得用戶輸入的內容並根據用戶輸入的內容進行篩選
if(s.length()<2){
return;
}
if(cities==null){
return;
}
String py = s.toString().toUpperCase();
List<String> temp = new ArrayList<String>();
for(CitynameBean bean:cities){
if(bean.getPyName().contains(py)){
temp.add(bean.getCityName());
}
}
if(temp.size()>0){
result = new ArrayList<String>(temp);
adapter = new ArrayAdapter<String>(CitySearchActivity.this, android.R.layout.simple_list_item_1,result);
listView.setAdapter(adapter);
}
}
});
}
}

這裏要注意搜索框edittext的監聽器的添加:addTextChangedListener,其對象爲TextWatcher(),在重寫的三個方法中,只能對public void afterTextChanged(Editable s) 這個方法添加相應的方法體,同時注意對獲得用戶輸入的內容並根據用戶輸入的內容進行篩選,採取相應的動作。

2.自定義搜索框側邊首字母索引

首先定義一個類,這裏命名爲MyLetterView讓他繼承自view,代碼實現可如下:

public class MyLetterView extends View{

public static String[] letters={"熱門",
"A","B","C","D","E","F",
"G","H","I","J","K","L","M","N","O","P",
"Q","R","S","T","U","V","W","X","Y","Z"};

private Paint paint;

private OnTouchLetterListener listener;

public void setOnTouchLetterListener(OnTouchLetterListener listener){
this.listener = listener;
}

public MyLetterView(Context context, AttributeSet attrs) {
super(context, attrs);
initPaint();
}

private void initPaint() {
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 11, getResources().getDisplayMetrics()));
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//自定義View分配給每個字符串的高度
int height = getHeight() / letters.length;
//自定義View分配給每個字符串的寬度
int width = getWidth();

for(int i=0;i<letters.length;i++){
//計算要繪製的字符串所佔的空間大小
Rect bounds = new Rect();
paint.getTextBounds(letters[i], 0, letters[i].length(), bounds );
//畫字符串的起始橫座標
float x = width/2 -bounds.width()/2;
//畫字符串的起始縱座標
float y = height/2+bounds.height()/2+height*i;
canvas.drawText(letters[i], x, y, paint);
}
}

@Override
public boolean onTouchEvent(MotionEvent event) {

int action = event.getAction();
//落下,移動,擡起,取消
switch (action) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
//自定義View獲得灰色背景
setBackgroundColor(Color.LTGRAY);
//根據手指當前位置,換算出可能當前手指位置的字符
float yPos = event.getY();
int idx = (int) ((yPos*letters.length)/getHeight());
//如果設置了監聽器,就調用監聽器的回調方法,將字符傳入
if(idx>=0 && idx<letters.length){
if(listener!=null){
listener.onTouchLetter(letters[idx]);
}
}
break;
default:
setBackgroundColor(Color.TRANSPARENT);
break;
}
return true;
}
public interface OnTouchLetterListener{
void onTouchLetter(String letter);
}
}

3.在引入的第三方下拉刷新架包中重寫MyRoateLoadingLayout這個類中方法,可分爲以下幾步實現:

自定義PullToRefreshListView的頭佈局
步驟1:修改PullToRefresh核心庫中的PullToRefreshListView的頭部佈局文件
pull_to_refresh_header_vertical
將原佈局文件中包含兩個TextView的LinearLayout整體刪除或者註釋掉。同時應該將包含ImageView的FrameLayout挪到屏幕的中間。
步驟2:隨着佈局文件的修改,LoadingLayout類中會有報錯。原因是兩個TextView(mHeaderText和mSubHeaderText;)屬性無法找到相應的id。這兩個TextView就是我們在第一步中從佈局文件中刪除掉的TextView。因此在LoadingLayout中,將所有與這兩個TextView相關的代碼、屬性等都註釋掉或者刪除掉。
步驟3:重新運行一下PullToRefresh的demo項目LauncherActivity,看一下修改後的效果。
步驟4:模擬RotateLoadingLayout寫一個自己的MyLoadingLayout。重點實現裏面的3個方法即可:
onPullImpl 下拉過程中不斷被調用
refreshingImpl 刷新過程中被調用
getDefaultDrawableResId
在onPullImpl中,根據下拉的距離,不斷的爲mHeaderImage設定圖片。在refreshingImpl方法中,設定一個幀動畫。
步驟5:重寫PullToRefreshBase類中的createLoadingLayout方法。該方法中會去調用mLoadingAnimationStyle的createLoadingLayout方法。修改mLoadingAnimationStyle的createLoadingLayout方法的返回值。
將return new RotateLoadingLayout(context, mode, scrollDirection, attrs);改爲return new MyLoadingLayout(context, mode, scrollDirection, attrs);
代碼實現可如下:

public class MyLoadingLayout extends LoadingLayout {
private int[] resId = {R.drawable.dropdown_anim_00,
R.drawable.dropdown_anim_01,
R.drawable.dropdown_anim_02,
R.drawable.dropdown_anim_03,
R.drawable.dropdown_anim_04,
R.drawable.dropdown_anim_05,
R.drawable.dropdown_anim_06,
R.drawable.dropdown_anim_07,
R.drawable.dropdown_anim_08,
R.drawable.dropdown_anim_09,
R.drawable.dropdown_anim_10};//以上均爲圖片資源ID
public MyLoadingLayout(Context context, Mode mode, Orientation scrollDirection, TypedArray attrs) {
super(context, mode, scrollDirection, attrs);
}
public void onLoadingDrawableSet(Drawable imageDrawable) {
}
protected void onPullImpl(float scaleOfLayout) {
// 隨着下拉,不斷的會調用該方法
//scaleofLayout的值從0開始,隨着下拉距離不斷的增大而增大
if(scaleOfLayout<=1){
int idx = (int) Math.ceil(scaleOfLayout*10);
Drawable d = getResources().getDrawable(resId[idx]);
d.setLevel(100);
ScaleDrawable sd = new ScaleDrawable(d, Gravity.CENTER, (10-idx)/10.0f, (10-idx)/10.0f);
mHeaderImage.setImageDrawable(sd);
}else{
mHeaderImage.setImageResource(resId[10]);
}
}
@Override
protected void refreshingImpl() {
//鬆手後開始刷新,調用該方法
mHeaderImage.setImageDrawable(getResources().getDrawable(R.drawable.refreshing_anim));

//這裏注意類型動畫類型的強制轉換
((AnimationDrawable)(mHeaderImage.getDrawable())).start();
}
@Override
protected void resetImpl() {
}
@Override
protected void pullToRefreshImpl() {
// NO-OP
}
@Override
protected void releaseToRefreshImpl() {
// NO-OP
}
@Override
protected int getDefaultDrawableResId() {
return R.drawable.dropdown_anim_00;
}
}

幀動畫的佈局文件pulltorefreshlibrary/res/drawable/refreshing_anim可寫成如下這樣:

<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android" >
    <item android:drawable="@drawable/dropdown_loading_00" android:duration="200"/>
    <item android:drawable="@drawable/dropdown_loading_01" android:duration="200"/>
    <item android:drawable="@drawable/dropdown_loading_02" android:duration="200"/>


</animation-list>

其中dropdown_loading_00....等爲連貫動作的圖片名。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章