Android 精通自定义视图(1)

作者: 宝塔山上的猫 | 来源:发表于2016-07-26 15:31 被阅读472次

    本项目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

    相关文章

      网友评论

        本文标题:Android 精通自定义视图(1)

        本文链接:https://www.haomeiwen.com/subject/vkogjttx.html