Android 精通自定义视图(1)

本项目Demo: https://github.com/liaozhoubei/CustomViewDemo

Android给开发者定义了很多图像样式,但是由于需求不同,想修改其中的视图样式,又或者想做出自己喜欢的样式,这时改怎么办?
这篇文章就是教你怎样修改或者制作这些样式的。

本文有以下几个内容:

1、修改ProgressBar的样式

2、使用动画的菜单视图

3、轮播图的实现

4、显示有下拉框的视图

5、自定义的开关视图

6、下拉刷新和加载更多的实现

7、侧边栏视图的实现

链接地址:

扩展阅读:

Android 精通自定义视图(2) http://www.jianshu.com/p/092e126b623f

Android 精通自定义视图(3) http://www.jianshu.com/p/1660479e76ef

Android 精通自定义视图(4) http://www.jianshu.com/p/850e387fc9d8

Android 精通自定义视图(5) http://www.jianshu.com/p/93feac19c396

现在就让我们开启自定义视图之旅吧!

修改ProgressBar样式

在自定义视图之前我们先热热身子,找个简单的事情来转换一下情绪。

Android的ProgressBar的原始样式是一条直线,看上去非常的粗糙,所以我们想将其修改为下面这种样子:

progressbar.gif

那么我们应该如何着手呢?其实这时我们可以查看Android自身是怎么制定样式的,然后通过学习他们制定样式的方法来修改ProgressBar。

找到我们SDK的安装目录,然后进入platforms目录,随便选择一个Android版本,在这里我选择了android-16,然后进去\data\res\values,找到其中的styles.xml文件,这个就是Android放置系统样式的地方了。

现在我们要查找到ProgressBar的Horizontal样式是如何制定的,直接在styles.xml中搜索,找到了以下代码:

<style name="Widget.ProgressBar.Horizontal">
    <item name="android:indeterminateOnly">false</item>
    <item name="android:progressDrawable">@android:drawable/progress_horizontal</item>
    <item name="android:indeterminateDrawable">@android:drawable/progress_indeterminate_horizontal</item>
    <item name="android:minHeight">20dip</item>
    <item name="android:maxHeight">20dip</item>
</style>

通过分析,我们发现ProgressBar是通过设定android:progressDrawable来设定样式的,那么其中drawable/progress_horizontal又是什么呢?再次到drawable中搜索到progress_horizontal.XML文件,打开发现:

<layer-list xmlns:android="http://schemas.android.com/apk/res/android">

<item android:id="@android:id/background">
    ···
</item>

<item android:id="@android:id/secondaryProgress">
    ···
</item>

<item android:id="@android:id/progress">
  ···
</item>

</layer-list>

这下我们被这些代码晃得眼花缭乱的,这个layer-list是什么东西?

别怕,我们可以直接去andorid的开发者官网查看答案,开发者官网是andorid最大的学习资料,有不懂的地方直接去哪里找就是了。

果然我们找到了layer-list的信息,从它的信息上得知,这个一个管理图片资源的图片对象,它可以按照顺序,将图片一层层的叠加上去。

好了,我们已经明白了layer-list,那么我们怎么使用它呢?别怕,开发者官网也把使用方法给我们了,直接把代码拿下,自己修改一下就好了。

首先在项目的res目录下创建drawable目录,然后新建style_progress.xml,添加一下代码:

<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >

<item android:id="@android:id/background">
    <bitmap
        android:gravity="center"
        android:src="@drawable/security_progress_bg" />
</item>
<item android:id="@android:id/secondaryProgress">
    <bitmap
        android:gravity="center"
        android:src="@drawable/security_progress" />
</item>
<item android:id="@android:id/progress">
    <bitmap
        android:gravity="center"
        android:src="@drawable/security_progress" />
</item>

</layer-list>

其中security_progress_bg、security_progress 都是另外的图片资源,在获得了drawable资源之后,就在layout中新建activity_progressbar.xml布局文件,添加一下代码:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin" >

<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_below="@+id/progressBar1"
    android:layout_centerHorizontal="true"
    android:layout_marginTop="64dp"
    android:onClick="startProgress"
    android:text="start Progress" />

<ProgressBar
    android:id="@+id/progressBar1"
    style="?android:attr/progressBarStyleHorizontal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentTop="true"
    android:layout_centerHorizontal="true"
    android:layout_marginTop="126dp"
    android:max="100"
    android:progress="20"
    android:progressDrawable="@drawable/style_progress" />

</RelativeLayout>

最后再新建的ProgressBarActivity中调用自定义的ProgressBar:

public class ProgressBarActivity extends Activity {
private ProgressBar progressBar1;
private int progress;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_progressbar);
    progressBar1 = (ProgressBar) findViewById(R.id.progressBar1);
    progressBar1.setProgress(0);
}

public void startProgress(View v) {
    progress = 0;
    new Thread() {
        public void run() {
            for (int i = 0; i <= 100; i++) {
                SystemClock.sleep(100);
                progress++;
                progressBar1.setProgress(progress);
            }
        };
    }.start();

}
}

这时我们的自定义的ProgressBar就完成了!小伙伴们也可以自定义出更酷更炫的ProgressBar样式哦,不管是条状的,还是环形的,都是可以自己定义的。而且Android其他控件的布局也是可以通过查看Android自身的样式来修改。

实现动画自定义菜单

ProgressBar做完之后,我们想实现一个更酷更炫的能动的菜单视图,如下:

youkumenu.gif

在这个视图中,我们点击中间三条横线的菜单时,最外层菜单如果存在就隐藏,不存在就显示;点击最里面的主页按钮时,则是将最外层和中间的菜单隐藏,或者显示中间菜单;当我们点击Menu(虚拟机要设置有实体键)时,就会将所有的菜单隐藏或显示。

那么这个看起来又酷又炫的动画效果是怎么实现的呢?其实其中的核心就是使用RotateAnimation这个API,以及对动画的处理。

我们先上代码:

public class AnimationUtil {
// 数值大小在动画运行时会变化,大于0表示动画开始了,等于或小于0位动画结束了
public static int runningAnimationCount = 0;

public static void RotateAnimationOut(RelativeLayout layout, long delay) {
    // 获得layout视图中子控件的个数
    int childCount = layout.getChildCount();
    for (int i = 0; i < childCount; i ++) {
        // 设置layout子控件为不可点击
        layout.getChildAt(i).setEnabled(false);
    }
    // 设置基于自身的选择动画
    RotateAnimation rotateAnimation = new RotateAnimation(0, -180, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 1.0f);
    rotateAnimation.setDuration(500);
    // 设置动画的延时时间
    rotateAnimation.setStartOffset(delay);
    // 设置动画停留在结束位置
    rotateAnimation.setFillAfter(true);
    // 设置动画的监听
    rotateAnimation.setAnimationListener(new MyAnimationLisenter());
    layout.startAnimation(rotateAnimation);
}

public static void RotateAnimationIn(RelativeLayout layout, long delay) {
    // 获得layout视图中子控件的个数
    int childCount = layout.getChildCount();
    for (int i = 0; i < childCount; i ++) {
        // 设置layout子控件为不可点击
        layout.getChildAt(i).setEnabled(true);
    }
    RotateAnimation rotateAnimation = new RotateAnimation(-180, 0, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 1.0f);
    rotateAnimation.setDuration(500);
    rotateAnimation.setStartOffset(delay);
    rotateAnimation.setFillAfter(true);
    layout.startAnimation(rotateAnimation);        
}

private static class MyAnimationLisenter implements AnimationListener{

    @Override
    public void onAnimationStart(Animation animation) {
        runningAnimationCount++;            
    }

    @Override
    public void onAnimationEnd(Animation animation) {
        runningAnimationCount--;            
    }

    @Override
    public void onAnimationRepeat(Animation animation) {
        
    }
    
}
}

AnimationUtil这是一个工具类,一个被使用的类。相信学过RotateAnimation这个API的小伙伴们对于里面的大多数代码都不会感到陌生。

RotateAnimation是Animation的子类,是一个旋转的动画类,使用它能够达到旋转视图的效果,当然还有透明效果AlphaAnimation、缩放效果ScaleAnimation, 位移效果TranslateAnimation这些动画,这里我就不一一介绍了。

我们看RotateAnimation的构造方法:

RotateAnimation rotateAnimation = new RotateAnimation(0, -180, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 1.0f);

其中的开头的参数(0, -180)代表的是视图的旋转角度从0读的选择为-180度。(Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 1.0f)中Animation.RELATIVE_TO_SELF代表这个动画是基于自身旋转的,0.5f表示旋转的中心点为视图宽度的中心位置,1.0f代表的是视图的高度。

menu旋转中心点.png

这些设定好之后,得到选择的中心点,视图将会从显示的位置旋转到消失的位置。

int childCount = layout.getChildCount();
    for (int i = 0; i < childCount; i ++) {
        // 设置layout子控件为不可点击
        layout.getChildAt(i).setEnabled(false);
    }

上面这段代码的意思就是获得所有的对象,然后设置为不可点击。为什么要这么做呢?这就不得不说到补间动画的特点了,补间动画会将视图的位置或者透明度发生变化,但其实际的控件是没有变化的,简单的说就是障眼法。因此如果设置了点击事件,控件仍在当前的位置,就会导致控件可被点击,对于用户来说控件都看不见了,仍然可被点击就是个bug,所以我们需要在视图移出去的时候让视图不可被点击,然后移进来的时候让视图可再次被点击。

最后就是工具类中的内部类MyAnimationLisenter,这是一个负责监听的内部类,它所负责的工作就是给runningAnimationCount赋值。

private static class MyAnimationLisenter implements AnimationListener{

    @Override
    public void onAnimationStart(Animation animation) {
        runningAnimationCount++;            
    }

    @Override
    public void onAnimationEnd(Animation animation) {
        runningAnimationCount--;            
    }

    @Override
    public void onAnimationRepeat(Animation animation) {
        
    }
    
}

为什么要给runningAnimationCount赋值呢?原因是我们想点击按钮启动动画之后,突然再次点击按钮,这时我们不想再次启动动画,我们想让动画结束之后才能启动新的动画,这时我们就要监听动画的状态,传出runningAnimationCount的值。如果它大于0,那么动画就正在启动,取消新的点击事件,如果没有大于0,就执行新的动画。

接下来就是调用工具类的AnimationMenuActivity,代码如下:

public class AnimationMenuActivity extends Activity implements OnClickListener{
private RelativeLayout rl_level1;
private RelativeLayout rl_level2;
private RelativeLayout rl_level3;
private boolean isDisplaylevel3 = true;
private boolean isDisplaylevel2 = true;
private boolean isDisplaylevel1 = true;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    requestWindowFeature(Window.FEATURE_NO_TITLE);
    setContentView(R.layout.activity_youkumenu);
    
    initView();
}
// 初始化控件
private void initView() {
    findViewById(R.id.ib_home).setOnClickListener(this);
    findViewById(R.id.ib_menu).setOnClickListener(this);
    
    rl_level1 = (RelativeLayout) findViewById(R.id.rl_level1);
    rl_level2 = (RelativeLayout) findViewById(R.id.rl_level2);
    rl_level3 = (RelativeLayout) findViewById(R.id.rl_level3);
}

@Override
public void onClick(View v) {
    // 如果在动画正在运行的时候点击,那么直接返回,不执行新的动画
    if (AnimationUtil.runningAnimationCount > 0) {
        return;
    }
    
    switch (v.getId()) {
    case R.id.ib_home:
        // 点击了主页按钮
        if (isDisplaylevel2) {
            // 设置延时时间
            long delay = 0;
            if (isDisplaylevel3) {
                // 如果菜单已经显示,那么设置不显示
                AnimationUtil.RotateAnimationOut(rl_level3, 0);
                isDisplaylevel3 = false;
                // 当第三级菜单存在是,设置延时时间为200,然后程序往下运行时,二级菜单将会延时执行
                delay += 200;
            }
            AnimationUtil.RotateAnimationOut(rl_level2,delay);
        } else {
            AnimationUtil.RotateAnimationIn(rl_level2, 0);
        }
        isDisplaylevel2 = !isDisplaylevel2;
        break;
        
    case R.id.ib_menu:
        // 点击了菜单按钮
        if (isDisplaylevel3) {
            // 如果菜单已经显示,那么设置不显示
            AnimationUtil.RotateAnimationOut(rl_level3, 0);
        } else {
            // 如果菜单不显示,那么选择显示出来
            AnimationUtil.RotateAnimationIn(rl_level3, 0);
        }
        isDisplaylevel3 = !isDisplaylevel3;
        break;

    default:
        break;
    }
}
// 按下物理按键menu的时候(使用虚拟机时要设置成有物理按键)
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
    if (AnimationUtil.runningAnimationCount > 0) {
        return true;
    }
    if (keyCode == KeyEvent.KEYCODE_MENU) {
        long delay = 0;
        //判断是否存在1级菜单没有隐藏
        if (isDisplaylevel1) {
            
            if (isDisplaylevel3){
                // 隐藏三级菜单
                AnimationUtil.RotateAnimationOut(rl_level3, delay);
                isDisplaylevel3 = false;
                delay += 200;
            }
            if (isDisplaylevel2) {
                // 隐藏二级菜单
                AnimationUtil.RotateAnimationOut(rl_level2, delay);
                isDisplaylevel2 = false;
                delay += 200;
            }
            AnimationUtil.RotateAnimationOut(rl_level1, delay);
        } else {
            // 如果菜单都被隐藏,那么现实出来
            AnimationUtil.RotateAnimationIn(rl_level1, 0);
            AnimationUtil.RotateAnimationIn(rl_level2, 200);
            AnimationUtil.RotateAnimationIn(rl_level3, 300);
            
            isDisplaylevel2 = true;    
            isDisplaylevel3 = true;    
        }
        isDisplaylevel1 = !isDisplaylevel1;
        return true;
        
    }
    // 返回true则代表在onKeyDown中使用了点击事件,这样点击事件就不会被其他代码使用
    return super.onKeyDown(keyCode, event);
}
}

这一大串的代码看下来心好累,已经没兴趣做其他的事情了。但是且慢,这些代码其实并没有太过复杂的地方。

这么多的代码其实只说明了两件事情,就是button按键点击事件和(实体)菜单menu点击事件。

在按键点击事件,也就是onClick()方法中,我们做出了判断,如果点击了中间的菜单键R.id.ib_menu,那么就判断是否最外层的菜单存在,如果存在就隐藏,不存在的显示,同时设置isDisplaylevel3的布尔值,作为下次点击事件的判断依据。
如果点击了最里面的主页按键R.id.ib_home,那就判断中间菜单和最外层菜单是否存在,如果两者都在,那么隐藏两者,如果只有中间菜单在那么隐藏中间菜单,如果都不在,就显示中间菜单。

最后就是onKeyDown()实体按键点击事件,这个需要设置虚拟机有实体按键时才会生效。这个按键判断所有的菜单是否存在,哪个菜单存在就因此哪个,如果全部都不在那么就全部显示。

当然,大家还要注意到一点,那就是菜单显示的顺序问题,但所有的菜单都存在时从最外层开始隐藏,然后到中间在到最里面。如果都不在时,就从最里面开始出现。这个菜单的显示顺序也是要注意的一点。

本项目Demo: https://github.com/liaozhoubei/CustomViewDemo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,881评论 4 368
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,052评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,598评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,407评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,823评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,872评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,037评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,778评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,505评论 1 247
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,745评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,233评论 1 264
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,568评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,231评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,141评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,939评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,954评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,784评论 2 275

推荐阅读更多精彩内容