Android自定义视图教程

发表于 5年以前  | 总阅读数:858 次

Android的UI元素都是基于View(屏幕中单个元素)和ViewGroup(元素的集合),Android有许多自带的组件和布局,比如Button、TextView、RelativeLayout。在app开发过程中我们需要自定义视图组件来满足我们的需求。通过继承自View或者View的子类,覆写onDraw或者onTouchEvent等方法来覆盖视图的行为。

创建完全自定义的组件

创建自定义的组件主要围绕着以下五个方面:

  • 绘图(Drawing): 控制视图的渲染,通常通过覆写onDraw方法来实现
  • 交互(Interaction): 控制用户和视图的交互方式,比如OnTouchEvent,gestures
  • 尺寸(Measurement): 控制视图内容的维度,通过覆写onMeasure方法
  • 属性(Attributes): 在XML中定义视图的属性,使用TypedArray来获取属性值
  • 持久化(Persistence): 配置发生改变时保存和恢复状态,通过onSaveInstanceState和onRestoreInstanceState

举个栗子,假设我们想创建一个图形允许用户点击的时候改变形状(方形、圆形、三角形)。如下所示:

定义视图类

我们创建一个ShapeSelectorView继承自View,实现必要的构造器,如下所示:

public class ShapeSelectorView extends View {
      // We must provide a constructor that takes a Context and an AttributeSet.
      // This constructor allows the UI to create and edit an instance of your view.
      public ShapeSelectorView(Context context, AttributeSet attrs) {
        super(context, attrs);
        }
    }

添加视图到布局中

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
        <com.codepath.example.customviewdemo.ShapeSelectorView
        android:id="@+id/shapeSelector"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_alignParentRight="true"
        android:layout_alignParentTop="true"
        android:layout_alignParentLeft="true" />
    </RelativeLayout>

接下来我们定义一个命名空间app,这个命名空间允许Android自动解析而不需要指定具体的包名。

自定义属性

视图可以通过XML来配置属性和样式,你需要想清楚要添加那些自定义的属性,比如我们想让用户可以选择形状的颜色、是否显示形状的名称,比如我们想让视图可以像下面一样配置:

<com.codepath.example.customviewdemo.ShapeSelectorView
        app:shapeColor="#7f0000"
        app:displayShapeName="true"
        android:id="@+id/shapeSelector"
        ... />

为了能够定义shapeColor和displayShapeName,我们需要在res/values/attrs.xml中配置:

<?xml version="1.0" encoding="utf-8"?>
    <resources>
       <declare-styleable name="ShapeSelectorView">
           <attr name="shapeColor" format="color" />
           <attr name="displayShapeName" format="boolean" />
       </declare-styleable>
    </resources>

对于每个你想自定义的属性你需要定义attr节点,每个节点有name和format属性,format属性是我们期望的值的类型,比如color,dimension,boolean,integer,float等。一旦定义好了属性,你可以像使用自带属性一样使用他们,唯一的区别在于你的自定义属性属于一个不同的命名空间,你可以在根视图的layout里面定义命名空间,一般情况下你只需要这样子指定:http://schemas.android.com/apk/res/<package_name>,但是你可以使用http://schemas.android.com/apk/res-auto自动解析命名空间。

<?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
        <com.codepath.example.customviewdemo.ShapeSelectorView
           app:shapeColor="#7f0000"
           app:displayShapeName="true"
           android:id="@+id/shapeSelector"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:layout_above="@+id/btnSelect"
           android:layout_alignParentLeft="true"
           android:layout_below="@+id/tvPrompt" />
    </RelativeLayout>

应用自定义属性

在前面我们定义了shapeColor和displayShapeName两个属性值,我们需要提取这两个属性值来用在自定义的视图中,可以使用TypedArray和obtainStyledAttributes方法来完成,如下所示:

public class ShapeSelectorView extends View {
      private int shapeColor;
      private boolean displayShapeName;

      public ShapeSelectorView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setupAttributes(attrs);
      }

      private void setupAttributes(AttributeSet attrs) {
        // Obtain a typed array of attributes
        TypedArray a = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ShapeSelectorView, 0, 0);
        // Extract custom attributes into member variables
        try {
          shapeColor = a.getColor(R.styleable.ShapeSelectorView_shapeColor, Color.BLACK);
          displayShapeName = a.getBoolean(R.styleable.ShapeSelectorView_displayShapeName, false);
        } finally {
          // TypedArray objects are shared and must be recycled.
          a.recycle();
        }
      }
    }

接下来添加一些getter和setter方法:

public class ShapeSelectorView extends View {
      // ...
      public boolean isDisplayingShapeName() {
        return displayShapeName;
      }

      public void setDisplayingShapeName(boolean state) {
        this.displayShapeName = state;
        invalidate();
        requestLayout();
      }

      public int getShapeColor() {
        return shapeColor;
      }

      public void setShapeColor(int color) {
        this.shapeColor = color;
        invalidate();
        requestLayout();
      }
    }

当视图属性发生改变的时候可能需要重新绘图,你需要调用invalidate()和requestLayout()来刷新显示。

画图

假设我们要使用前面的属性画一个长方形,所有的绘图都是在onDraw方法里执行,使用Canvas对象来绘图,如下所示:

public class ShapeSelectorView extends View {
      // ...
      private int shapeWidth = 100;
      private int shapeHeight = 100;
      private int textXOffset = 0;
      private int textYOffset = 30;
      private Paint paintShape;

      // ...
      public ShapeSelectorView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setupAttributes(attrs);
        setupPaint();
      }

      @Override
      protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawRect(0, 0, shapeWidth, shapeHeight, paintShape);
        if (displayShapeName) {
          canvas.drawText("Square", shapeWidth + textXOffset, shapeHeight + textXOffset, paintShape);
        }
      }

      private void setupPaint() { 
          paintShape = new Paint();
          paintShape.setStyle(Style.FILL);
          paintShape.setColor(shapeColor);
          paintShape.setTextSize(30);
       }
    }

这段代码就会根据XML里设置的shapeColor来画图,根据displayShapeName属性来决定是否显示图形的名称,结果如下图:

更多画图的教程可以参考这里 Custom 2D Drawing Tutorial

计算尺寸

为了更好的理解自定义视图的宽度和高度,我们需要定义onMeasure方法,这个方法根据视图的内容来决定它的宽度和高度,在这里宽度和高度是由形状和下面的文本决定的,如下所示:

public class ShapeSelectorView extends View {
      @Override
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // Defines the extra padding for the shape name text
        int textPadding = 10;
        int contentWidth = shapeWidth;

        // Resolve the width based on our minimum and the measure spec
        int minw = contentWidth + getPaddingLeft() + getPaddingRight();
        int w = resolveSizeAndState(minw, widthMeasureSpec, 0);

        // Ask for a height that would let the view get as big as it can
        int minh = shapeHeight + getPaddingBottom() + getPaddingTop();
        if (displayShapeName) { 
        minh += textYOffset + textPadding;
        }
        int h = resolveSizeAndState(minh, heightMeasureSpec, 0);

        // Calling this method determines the measured width and height
        // Retrieve with getMeasuredWidth or getMeasuredHeight methods later
        setMeasuredDimension(w, h);
      }
    }

宽度和高度都是基于MeasureSpec来讨论的,一个MeasureSpec封装了父布局传递给子布局的布局要求,每个MeasureSpec代表了一组宽度和高度的要求。一个MeasureSpec由大小和模式组成。它有三种模式:UNSPECIFIED(未指定),父元素未给子元素施加任何束缚,子元素可以得到任意想要的大小;EXACTLY(完全),父元素决定子元素的确切大小,子元素将被限定在给定的边界里而忽略它本身大小;AT_MOST(至多),子元素至多达到指定大小的值。resolveSizeAndState()方法根据视图想要的大小和MeasureSpec返回一个合适的值,最后你需要调用setMeasureDimension()方法生效。

不同形状之间切换

如果想实现用户点击之后改变形状,需要在onTouchEvent方法里添加自定义逻辑:

public class ShapeSelectorView extends View {
      // ...
      private String[] shapeValues = { "square", "circle", "triangle" };
      private int currentShapeIndex = 0;

      // Change the currentShapeIndex whenever the shape is clicked
      @Override
      public boolean onTouchEvent(MotionEvent event) {
        boolean result = super.onTouchEvent(event);
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
          currentShapeIndex ++;
          if (currentShapeIndex > (shapeValues.length - 1)) {
        currentShapeIndex = 0;
          }
          postInvalidate();
          return true;
        }
        return result;
      }
    }

现在不管什么时候视图被单击,选择的形状的下标会改变,调用postInvalisate()方法后会显示一个不同的形状,接下来更新onDraw()方法来实现更改形状的逻辑:

public class ShapeSelectorView extends View {
      // ...

      @Override
      protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        String shapeSelected = shapeValues[currentShapeIndex];
        if (shapeSelected.equals("square")) {
          canvas.drawRect(0, 0, shapeWidth, shapeHeight, paintShape);
          textXOffset = 0;
        } else if (shapeSelected.equals("circle")) {
          canvas.drawCircle(shapeWidth / 2, shapeHeight / 2, shapeWidth / 2, paintShape);
          textXOffset = 12;
        } else if (shapeSelected.equals("triangle")) {
          canvas.drawPath(getTrianglePath(), paintShape);
          textXOffset = 0;
        }
        if (displayShapeName) {
          canvas.drawText(shapeSelected, 0 + textXOffset, shapeHeight + textYOffset, paintShape);
        }
      }

      protected Path getTrianglePath() {
        Point p1 = new Point(0, shapeHeight), p2 = null, p3 = null;
        p2 = new Point(p1.x + shapeWidth, p1.y);
        p3 = new Point(p1.x + (shapeWidth / 2), p1.y - shapeHeight);
        Path path = new Path();
        path.moveTo(p1.x, p1.y);
        path.lineTo(p2.x, p2.y);
        path.lineTo(p3.x, p3.y);
        return path;
      }

      // ...
    }

现在每次点击都会显示一个不同的形状,结果如下:

接下来添加一个获取形状的方法:

public class ShapeSelectorView extends View {
      // ...
      // Returns selected shape name
      public String getSelectedShape() {
        return shapeValues[currentShapeIndex];
      }
    }

保存视图的状态

当配置发生改变的时候(比如屏幕旋转)视图需要保存它们的状态,你可以实现onSaveInstanceState()和onRestoreInstanceState()方法来保存和恢复视图状态,如下所示:

public class ShapeSelectorView extends View {
      // This is the view state for this shape selector
      private int currentShapeIndex = 0;

      @Override
      public Parcelable onSaveInstanceState() {
        // Construct bundle
        Bundle bundle = new Bundle();
        // Store base view state
        bundle.putParcelable("instanceState", super.onSaveInstanceState());
        // Save our custom view state to bundle
        bundle.putInt("currentShapeIndex", this.currentShapeIndex);
        // ... store any other custom state here ...
        // Return the bundle
        return bundle;
      }

      @Override
      public void onRestoreInstanceState(Parcelable state) {
        // Checks if the state is the bundle we saved
        if (state instanceof Bundle) {
          Bundle bundle = (Bundle) state;
          // Load back our custom view state
          this.currentShapeIndex = bundle.getInt("currentShapeIndex");
          // ... load any other custom state here ...
          // Load base view state back
          state = bundle.getParcelable("instanceState");
        }
        // Pass base view state on to super
        super.onRestoreInstanceState(state);
      }
    }

一旦你实现了这些保存和恢复的逻辑,当手机配置改变的时候你的视图能够自动保存状态。

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:8月以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:8月以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:8月以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:8月以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:8月以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:8月以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:8月以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:8月以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:8月以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:8月以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:8月以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:8月以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:8月以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:8月以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:8月以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:8月以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:8月以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:8月以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:8月以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:8月以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  236913次阅读
vscode超好用的代码书签插件Bookmarks 1年以前  |  7119次阅读
 目录