Android 音乐APP(四)显示专辑图片、本地数据库、自定义通知栏样式、通知栏显示

简介: Android 音乐APP(四)显示专辑图片、本地数据库、自定义通知栏样式、通知栏显示

Android 音乐APP 显示专辑图片、本地数据库、自定义通知栏样式、通知栏显示



前言


 近段时间,写作的时间没有那么多,之前的项目要改动一些功能,所以这个文章也是断断续续才写好,好在我还没有忘记这个事情,写文章有没有人看不重要,我更在意这个过程,记录思路和心路历程,好了,进入正题吧。


正文


在上一篇文章中,实现了播放音乐和自动下一曲,并且自定义了一个播放音乐的圆环进度条,但是播放布局那里如果一直显示一个我写上去的默认图片感觉还是不太好,用户体验不强,参考了其他的音乐播放器的播放信息栏时,我发现会根据当前歌曲,显示这个专辑的图片,于是我也打算这么做。


① 显示专辑封面图片


首先先改动一下这个进度条的绘制半径,这样看起来的效果就是它会覆盖图片的白色边框,同时我改变了这个的颜色。

20201028153523100.png


就会出现这样的效果。


20201028153808590.png


因为专辑图片并不是每一首歌都能有,有的时候在你的本地没有找到的,所以,就用默认的图标,找到就用专辑封面。


同时更改LocalMusicActivity中的initAnimation方法中的动画时长,之前是3s,现在改成6s,体验会更好。


20201028153948769.png


在MusicUtils中新增如下方法。

  /**
     * 获取专辑封面
     *
     * @param context 上下文
     * @param path    歌曲路径
     * @return
     */
    public static Bitmap getAlbumPicture(Context context, String path) {
        //歌曲检索
        MediaMetadataRetriever mmr = new MediaMetadataRetriever();
        //设置数据源
        mmr.setDataSource(path);
        //获取图片数据
        byte[] data = mmr.getEmbeddedPicture();
        Bitmap albumPicture = null;
        if (data != null) {
            //获取bitmap对象
            albumPicture = BitmapFactory.decodeByteArray(data, 0, data.length);
            //获取宽高
            int width = albumPicture.getWidth();
            int height = albumPicture.getHeight();
            // 创建操作图片用的Matrix对象
            Matrix matrix = new Matrix();
            // 计算缩放比例
            float sx = ((float) 120 / width);
            float sy = ((float) 120 / height);
            // 设置缩放比例
            matrix.postScale(sx, sy);
            // 建立新的bitmap,其内容是对原bitmap的缩放后的图
            albumPicture = Bitmap.createBitmap(albumPicture, 0, 0, width, height, matrix, false);
        } else {
            //从歌曲文件读取不出来专辑图片时用来代替的默认专辑图片
            albumPicture = BitmapFactory.decodeResource(context.getResources(), R.mipmap.icon_music);
            int width = albumPicture.getWidth();
            int height = albumPicture.getHeight();
            // 创建操作图片用的Matrix对象
            Matrix matrix = new Matrix();
            // 计算缩放比例
            float sx = ((float) 120 / width);
            float sy = ((float) 120 / height);
            // 设置缩放比例
            matrix.postScale(sx, sy);
            // 建立新的bitmap,其内容是对原bitmap的缩放后的图
            albumPicture = Bitmap.createBitmap(albumPicture, 0, 0, width, height, matrix, false);
        }
        return albumPicture;
    }


这里面就是通过点击时过去到歌曲的路径,通过路径去拿图片数据,为空的话则使用默认的图片,不为空则通过BitmapFactory.decodeByteArray将图片数据流转换为Bitmap,然后设置缩放比列,然后赋值返回。


下面要去调用这个方法了。

20201028154929967.png


一目了然,现在你只要运行起来就可以了,下面看一下运行效果。

image.gif


这样看起来是不是效果更好呢?没骗你吧!

② 本地数据库


 之前的数据来源是通过扫描本地本地本地文件夹来获取到的,虽然我给了一个缓存用于记录当前是否有缓存歌曲,但是为了后面使用的方便,还是要使用本地数据库来操作数据会比较好,都知道Android使用的本地数据库是Sqlite。当然还有现在JetPack火热的Room数据库。不过我主要还是用Sqlite,第一次这个有很多成熟的框架,不需要需繁琐的sql语句,第二JetPack是Google18年才推出,目前来说有一定的受众群体,但是还不够,深思熟虑之下还是不去使用新的Room数据库了。既然要是用成熟的框架,那么肯定会有第三方的依赖库。


在app下面的build.gradle中添加如下依赖

  //Android SQLite操作框架
    implementation 'org.litepal.guolindev:core:3.1.1'


然后Sync,这个框架我个人觉得挺好用的,省了我很多事情,郭神出品,必属精品。

然后在main下新建一个assets文件夹,


20201106092134614.png


在文件夹下面新建一个File文件,取名litepal.xml


20201106092328189.png


然后先进入到Song这个实体里面,继承 LitePalSupport,现在它就具备操作数据库表的能力了,增删改查不在话下


2020110609252266.png


然后回到litepal.xml,里面现在是空的,不过不要紧,按照框架的要求写入就可以了。如下所示,如果你的包名和我不一致记得要改呀。


<?xml version="1.0" encoding="utf-8"?>
<litepal>
    <!--数据库名称-->
    <dbname value="GoodMusic" />
    <!--数据库版本-->
    <version value="1" />
    <!--映射模型-->
    <list>
        <mapping class="com.llw.goodmusic.bean.Song"/>
    </list>
</litepal>


做了这一步就还差最后一步,那就是初始化。进入到MusicApplication,在onCreate方法中进行初始化。

20201106092921139.png


那么现在你运行项目的时候,这个时候你会发现,数据库和表都已经创建好了。那么怎么证明这一点呢?很简单,你可以在MainActivity中的initData,写入如下代码:


    List<Song> list = LitePal.findAll(Song.class);
        BLog.d(TAG,list.size() + "");


如果打印结果是0那就说明已经创建好了这个表,只是里面目前没有数据而已,如果你报错了的话,那肯定是哪里不对造成了。


数据表已经创建好了,下面自然就要写入数据了,之前我是在LocalMusicActivity中进行数据的扫描,然后写到这个页面的列表里,那么现在我就要写到数据库里。打开Constant,增加一个全局变量


20201106094006130.png


然后打开LocalMusicActivity。新增加一个成员变量

  /**
     * 本地音乐数据  不是缓存
     */
    private boolean localMusicData = false;

下面对之前的getMusicList方法做代码改动。

  /**
     * 获取音乐列表
     */
    private void getMusicList() {
        localMusicData = SPUtils.getBoolean(Constant.LOCAL_MUSIC_DB, false, context);
        //清除列表数据
        mList.clear();
        if (localMusicData) {
            //有数据则读取本地数据库的数据
            BLog.d(TAG, "读取本地数据库 ====>");
            mList = LitePal.findAll(Song.class);
        } else {
            //没有数据则扫描本地文件夹获取音乐数据
            BLog.d(TAG, "扫描本地文件夹 ====>");
            mList = MusicUtils.getMusicData(this);
        }
        if (mList != null && mList.size() > 0) {
            //显示本地音乐
            showLocalMusicData();
            if (!localMusicData) {
                //添加到本地数据库中
                addLocalDB();
            }
        } else {
            show("兄嘚,你是一无所有啊~");
        }
    }


进入这个方法之后,先获取系统的缓存,本地数据库是否有数据,第一次进来当然是没有的,所以执行false中的逻辑,这个时候通过扫描本地文件夹获取数据mList = MusicUtils.getMusicData(this); ,之后就是显示数据了,这个不用管,关键在于判断当前有没有本地数据,!localMusicData就是表示 false。所以添加到本地,调用addLocalDB();。该方法如下


  /**
     * 添加到本地数据库
     */
    private void addLocalDB() {
        new Handler().post(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < mList.size(); i++) {
                    Song song = new Song();
                    song.setSinger(mList.get(i).getSinger());
                    song.setSong(mList.get(i).getSong());
                    song.setAlbumId(mList.get(i).getAlbumId());
                    song.setAlbum(mList.get(i).getAlbum());
                    song.setPath(mList.get(i).getPath());
                    song.setDuration(mList.get(i).getDuration());
                    song.setSize(mList.get(i).getSize());
                    song.setCheck(mList.get(i).isCheck());
                    song.save();
                }
                List<Song> list = LitePal.findAll(Song.class);
                if (list.size() > 0) {
                    SPUtils.putBoolean(Constant.LOCAL_MUSIC_DB, true, context);
                    BLog.d(TAG, "添加到本地数据库的音乐:" + list.size() + "首");
                }
            }
        });


这里因为添加数据是在耗时操作,所以新开一个线程去进行,然后对列表进行遍历保存。然后设置缓存值为true ,当全部遍历完成之后再查询一下,添加到数据库里面的数据有多少条。到这一步,数据就已经添加到本地数据库了。那么这个时候你再次进入到LocalMusicActivity中时,就会直接查询本地的数据库了。而在显示数据之后,也不会重复添加数据到本地数据库了。那么这就完了吗?还没有的,现在是只有一个页面知道当前数据有多少,MainActivty对此还是一无所知。在MainActivity中创建一个全局变量。


private List<Song> mList;


同时我改变了进入本地音乐这个入口的布局。

      <!--本地音乐-->
            <LinearLayout
                android:id="@+id/lay_local_music"
                android:layout_width="@dimen/dp_120"
                android:layout_height="@dimen/dp_120"
                android:background="@drawable/shape_app_color_radius_5"
                android:foreground="?android:attr/selectableItemBackground"
                android:gravity="center"
                android:onClick="onClick"
                android:orientation="vertical">
                <ImageView
                    android:layout_width="@dimen/dp_40"
                    android:layout_height="@dimen/dp_40"
                    android:src="@mipmap/icon_local" />
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="@dimen/dp_8"
                    android:text="本地音乐"
                    android:textColor="@color/white"
                    android:textSize="@dimen/sp_14" />
                <TextView
                    android:id="@+id/tv_local_music_num"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="@dimen/dp_4"
                    android:text="0"
                    android:textColor="@color/white_8"
                    android:textSize="@dimen/sp_12" />
            </LinearLayout>


加了一个本地音乐的数量。这样用户可以更好的感知当前音乐有多少,而不用到LocalMusicActivity中查看了。布局有了,下面自然要改动UI了。回到MainActivity,


  /**
     * 本地音乐数量
     */
    private TextView tvLocalMusicNum;


然后在initData下,绑定这个控件。


20201106100025613.png


现在这个控件可以正常使用了,重写onResume方法


  @Override
    protected void onResume() {
        super.onResume();
        BLog.d(TAG, "onResume");
        mList = LitePal.findAll(Song.class);
        tvLocalMusicNum.setText(String.valueOf(mList.size()));
    }


在这里显示歌曲的数量。然后可以运行测试一波,看看效果如何。


2020110610071615.gif


效果显著,下面进入下一环境,通知栏的显示。

③ 自定义通知栏样式、通知栏显示


 说道后台播放,可能不了解的人觉得很难,一听头都大了,后台这两个字,一听就是要掉头发的节奏,首先不要有这样的心理,代码又不是洪荒猛兽,又不会吃了你,所以不要怕,恐惧会让你止步不前的,进而颓废。后台,相信你在了解Android的四大组件的时候就知道了,后台最多的是什么?Service,就是服务,这个东西你是看不见的,所以理解起来就没有那么容易,但是你能感觉得到。比如过放音乐,你是听到的音乐。其实放音乐对于用户来说就是禁止的,因为你看不出什么名堂,你是用听的,为了让用户知道现在正在播放音乐,就会有播放的进度条,播放的状态,这些动态效果的支撑就来源于后台的服务,比如你就拿现在这个APP来说,你现在播放一首歌,然后你点击home键回到手机桌面,音乐还是在播放的,这时候音乐就是在后台的,但是你看不见,你能听见。


 说了这么多都是概念性的东西,为什么要用服务在后台播放音乐呢?音乐你播放一首歌,就是在整个APP任何地方你都要知道这个歌曲当前的状态和播放进度,针对于这个需求,用Service来实现无疑是最好的方式,没有之一。下面在com.llw.goodmusic下新建一个service包,

20201028160843647.png


然后新建一个Service。命名为MusicService。


20201028160939927.png


点击Finish就创建完成了。我自己创建一个MusicService类然后继承Service也是一样的呀。那么我这样创建有什么好处呢?打开AndroidManifest.xml


20201028161128330.png


可以看到,自动生成了Service的配置,就不需要我们手动再去写配置了,有的时候写代码往往会忘记这一步,通过AS来创建Service就避免了这个问题,何乐而不为呢?


 现在MusicService创建好了,那么这个服务要做什么事情呢?首先要播放音乐,然后就是通知栏显示,之后才是通知栏和Activity之间的通信。


在layout下创建一个notification.xml。里面的代码如下:

<?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="wrap_content"
    android:orientation="horizontal">
    <!--专辑封面图-->
    <ImageView
        android:id="@+id/iv_album_cover"
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:src="@mipmap/icon_notification_default" />
    <LinearLayout
        android:gravity="center_vertical"
        android:paddingStart="@dimen/dp_12"
        android:paddingEnd="@dimen/dp_6"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="@dimen/dp_64">
        <!--歌曲信息-->
        <LinearLayout
            android:gravity="center_vertical"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">
            <TextView
                android:id="@+id/tv_notification_song_name"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:ellipsize="marquee"
                android:focusable="true"
                android:focusableInTouchMode="true"
                android:marqueeRepeatLimit="marquee_forever"
                android:singleLine="true"
                android:text="歌曲名"
                android:textColor="@color/black"
                android:textSize="14sp" />
            <TextView
                android:layout_marginStart="@dimen/dp_12"
                android:id="@+id/tv_notification_singer"
                android:layout_width="0dp"
                android:layout_weight="1"
                android:singleLine="true"
                android:layout_height="wrap_content"
                android:text="歌手名"
                android:textSize="@dimen/sp_12" />
            <ImageButton
                android:id="@+id/btn_notification_close"
                android:layout_width="@dimen/dp_20"
                android:layout_height="@dimen/dp_20"
                android:background="@color/transparent"
                android:src="@drawable/close_gray" />
        </LinearLayout>
        <!--歌曲控制-->
        <LinearLayout
            android:layout_marginTop="@dimen/dp_4"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center">
            <!--上一曲-->
            <ImageButton
                android:id="@+id/btn_notification_previous"
                android:layout_width="@dimen/dp_30"
                android:layout_height="@dimen/dp_30"
                android:background="@null"
                android:scaleType="fitCenter"
                android:src="@drawable/previous_black" />
            <!--播放/暂停-->
            <ImageButton
                android:id="@+id/btn_notification_play"
                android:layout_width="@dimen/dp_30"
                android:layout_height="@dimen/dp_30"
                android:layout_marginStart="@dimen/dp_30"
                android:layout_marginEnd="@dimen/dp_30"
                android:background="@null"
                android:scaleType="fitCenter"
                android:src="@drawable/play_black" />
            <!--下一曲-->
            <ImageButton
                android:id="@+id/btn_notification_next"
                android:layout_width="@dimen/dp_30"
                android:layout_height="@dimen/dp_30"
                android:background="@null"
                android:scaleType="fitCenter"
                android:src="@drawable/next_black" />
        </LinearLayout>
    </LinearLayout>
</LinearLayout>


con_notification_default.png


20201106101327834.png


close_gray.xml


<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:tint="#8a8a8a"
    android:viewportWidth="24.0"
    android:viewportHeight="24.0">
    <path
        android:fillColor="@android:color/white"
        android:pathData="M18.3,5.71c-0.39,-0.39 -1.02,-0.39 -1.41,0L12,10.59 7.11,5.7c-0.39,-0.39 -1.02,-0.39 -1.41,0 -0.39,0.39 -0.39,1.02 0,1.41L10.59,12 5.7,16.89c-0.39,0.39 -0.39,1.02 0,1.41 0.39,0.39 1.02,0.39 1.41,0L12,13.41l4.89,4.89c0.39,0.39 1.02,0.39 1.41,0 0.39,-0.39 0.39,-1.02 0,-1.41L13.41,12l4.89,-4.89c0.38,-0.38 0.38,-1.02 0,-1.4z" />
</vector>


previous_black.xml

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="36dp"
    android:height="36dp"
    android:tint="#000000"
    android:viewportWidth="24.0"
    android:viewportHeight="24.0">
    <path
        android:fillColor="@android:color/white"
        android:pathData="M7,6c0.55,0 1,0.45 1,1v10c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1L6,7c0,-0.55 0.45,-1 1,-1zM10.66,12.82l5.77,4.07c0.66,0.47 1.58,-0.01 1.58,-0.82L18.01,7.93c0,-0.81 -0.91,-1.28 -1.58,-0.82l-5.77,4.07c-0.57,0.4 -0.57,1.24 0,1.64z" />
</vector>


play_black.xml

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="36dp"
    android:height="36dp"
    android:tint="#000000"
    android:viewportWidth="24.0"
    android:viewportHeight="24.0">
    <path
        android:fillColor="@android:color/white"
        android:pathData="M8,6.82v10.36c0,0.79 0.87,1.27 1.54,0.84l8.14,-5.18c0.62,-0.39 0.62,-1.29 0,-1.69L9.54,5.98C8.87,5.55 8,6.03 8,6.82z" />
</vector>


pause_black.xml

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="36dp"
    android:height="36dp"
    android:tint="#000000"
    android:viewportWidth="24.0"
    android:viewportHeight="24.0">
    <path
        android:fillColor="@android:color/white"
        android:pathData="M8,19c1.1,0 2,-0.9 2,-2L10,7c0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2v10c0,1.1 0.9,2 2,2zM14,7v10c0,1.1 0.9,2 2,2s2,-0.9 2,-2L18,7c0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2z" />
</vector>


next_black.xml

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="36dp"
    android:height="36dp"
    android:tint="#000000"
    android:viewportWidth="24.0"
    android:viewportHeight="24.0">
    <path
        android:fillColor="@android:color/white"
        android:pathData="M7.58,16.89l5.77,-4.07c0.56,-0.4 0.56,-1.24 0,-1.63L7.58,7.11C6.91,6.65 6,7.12 6,7.93v8.14c0,0.81 0.91,1.28 1.58,0.82zM16,7v10c0,0.55 0.45,1 1,1s1,-0.45 1,-1V7c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1z" />
</vector>


一共六个图标,一个png格式,其余五个为xml格式。

下面进入到MusicService中。里面的代码如下:

  private static final String TAG = "MusicService";
  public class MusicBinder extends Binder {
        public MusicService getService() {
            return MusicService.this;
        }
    }
    @Override
    public IBinder onBind(Intent intent) {
        super.onBind(intent);
        return new MusicBinder();
    }
   @Override
    public void onCreate() {
        super.onCreate();
        BLog.d(TAG, "onCreate");
    }


通过绑定的方式启动服务,回到MainActivity,创建变量

  private MusicService.MusicBinder musicBinder;
    private MusicService musicService;


然后创建服务连接器

  /**
     * 服务连接
     */
    private ServiceConnection connection = new ServiceConnection() {
        /**
         * 连接服务
         * @param name
         * @param service
         */
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            musicBinder = (MusicService.MusicBinder) service;
            musicService = musicBinder.getService();
            BLog.d(TAG, "Service与Activity已连接");
        }
        //断开服务
        @Override
        public void onServiceDisconnected(ComponentName name) {
            musicBinder = null;
        }
    };


在initData方法中绑定服务。

    //绑定服务
        Intent serviceIntent = new Intent(context, MusicService.class);
        bindService(serviceIntent, connection, BIND_AUTO_CREATE);


也要在页面销毁时解绑

  @Override
    protected void onDestroy() {
        super.onDestroy();
        unbindService(connection);
        System.exit(0);
    }


我在initData中加了日志的打印

BLog.d(TAG, "initData");


下面运行一下:


2020110610300383.png


从日志上可以得出,在initData中绑定时,会创建一个服务连接,在连接时对服务进行创建,创建之后连接这个服务,此时服务在后台运行,下面就要在服务中显示通知栏。

  private static NotificationManager manager;
  /**
     * 显示通知
     */
    private void showNotification() {
        String channelId = "play_control";
        String channelName = "播放控制";
        int importance = NotificationManager.IMPORTANCE_HIGH;
        createNotificationChannel(channelId, channelName, importance);
        RemoteViews remoteViews = new RemoteViews(this.getPackageName(), R.layout.notification);
        Notification notification = new NotificationCompat.Builder(this, "play_control")
                .setWhen(System.currentTimeMillis())
                .setSmallIcon(R.mipmap.icon_big_logo)
                .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.icon_big_logo))
                .setCustomContentView(remoteViews)
                .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
                .setAutoCancel(false)
                .setOnlyAlertOnce(true)
                .setOngoing(true)
                .build();
        //发送通知
        manager.notify(1, notification);
    }
  /**
     * 创建通知渠道
     *
     * @param channelId   渠道id
     * @param channelName 渠道名称
     * @param importance  渠道重要性
     */
    @TargetApi(Build.VERSION_CODES.O)
    private void createNotificationChannel(String channelId, String channelName, int importance) {
        NotificationChannel channel = new NotificationChannel(channelId, channelName, importance);
        channel.enableLights(false);
        channel.enableVibration(false);
        channel.setVibrationPattern(new long[]{0});
        channel.setSound(null, null);
        //获取系统通知服务
        manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        manager.createNotificationChannel(channel);
    }


然后在onCreate中调用showNotification();即可。运行之后你就能看到通知了,至于通知栏的点击和通知栏按钮的点击我放到下一篇文章来进行讲解。


结语


 说实话写到这里的时候除了很多的状态,音乐APP的坑太多了,太多的细节要去想了,而且写代码的过程中思路经常断,因为还有公司的工作要做,断断续续才写好。


相关文章
|
2月前
|
Android开发 开发者
安卓应用开发中的自定义视图
【9月更文挑战第37天】在安卓开发的海洋中,自定义视图犹如一座座小岛,等待着勇敢的探索者去发现其独特之处。本文将带领你踏上这段旅程,从浅滩走向深海,逐步揭开自定义视图的神秘面纱。
42 3
|
2月前
|
数据可视化 Android开发 开发者
安卓应用开发中的自定义View组件
【10月更文挑战第5天】在安卓应用开发中,自定义View组件是提升用户交互体验的利器。本篇将深入探讨如何从零开始创建自定义View,包括设计理念、实现步骤以及性能优化技巧,帮助开发者打造流畅且富有创意的用户界面。
93 0
|
4月前
|
供应链 物联网 区块链
未来触手可及:探索新兴技术的趋势与应用安卓开发中的自定义视图:从基础到进阶
【8月更文挑战第30天】随着科技的飞速发展,新兴技术如区块链、物联网和虚拟现实正在重塑我们的世界。本文将深入探讨这些技术的发展趋势和应用场景,带你领略未来的可能性。
|
4月前
|
测试技术 Android开发 Python
探索软件测试的艺术:从基础到高级安卓应用开发中的自定义视图
【8月更文挑战第29天】在软件开发的世界中,测试是不可或缺的一环。它如同艺术一般,需要精细的技巧和深厚的知识。本文旨在通过浅显易懂的语言,引领读者从软件测试的基础出发,逐步深入到更复杂的测试策略和工具的使用,最终达到能够独立进行高效测试的水平。我们将一起探索如何通过不同的测试方法来确保软件的质量和性能,就像艺术家通过不同的色彩和笔触来完成一幅画作一样。
|
1月前
|
搜索推荐 前端开发 Android开发
安卓应用开发中的自定义视图实现
【10月更文挑战第30天】在安卓开发的海洋中,自定义视图是那抹不可或缺的亮色,它为应用界面的个性化和交互体验的提升提供了无限可能。本文将深入探讨如何在安卓平台创建自定义视图,并展示如何通过代码实现这一过程。我们将从基础出发,逐步引导你理解自定义视图的核心概念,然后通过一个实际的代码示例,详细讲解如何将理论应用于实践,最终实现一个美观且具有良好用户体验的自定义控件。无论你是想提高自己的开发技能,还是仅仅出于对安卓开发的兴趣,这篇文章都将为你提供价值。
|
1月前
|
Android开发 开发者 UED
安卓开发中自定义View的实现与性能优化
【10月更文挑战第28天】在安卓开发领域,自定义View是提升应用界面独特性和用户体验的重要手段。本文将深入探讨如何高效地创建和管理自定义View,以及如何通过代码和性能调优来确保流畅的交互体验。我们将一起学习自定义View的生命周期、绘图基础和事件处理,进而探索内存和布局优化技巧,最终实现既美观又高效的安卓界面。
33 5
|
3月前
|
存储 缓存 编解码
Android经典面试题之图片Bitmap怎么做优化
本文介绍了图片相关的内存优化方法,包括分辨率适配、图片压缩与缓存。文中详细讲解了如何根据不同分辨率放置图片资源,避免图片拉伸变形;并通过示例代码展示了使用`BitmapFactory.Options`进行图片压缩的具体步骤。此外,还介绍了Glide等第三方库如何利用LRU算法实现高效图片缓存。
71 20
Android经典面试题之图片Bitmap怎么做优化
|
2月前
|
小程序 JavaScript API
微信小程序开发之:保存图片到手机,使用uni-app 开发小程序;还有微信原生保存图片到手机
这篇文章介绍了如何在uni-app和微信小程序中实现将图片保存到用户手机相册的功能。
765 0
微信小程序开发之:保存图片到手机,使用uni-app 开发小程序;还有微信原生保存图片到手机
|
2月前
|
XML 前端开发 Java
安卓应用开发中的自定义View组件
【10月更文挑战第5天】自定义View是安卓应用开发的一块基石,它为开发者提供了无限的可能。通过掌握其原理和实现方法,可以创造出既美观又实用的用户界面。本文将引导你了解自定义View的创建过程,包括绘制技巧、事件处理以及性能优化等关键步骤。
|
3月前
|
Android开发 开发者
安卓开发中的自定义视图:从入门到精通
【9月更文挑战第19天】在安卓开发的广阔天地中,自定义视图是一块充满魔力的土地。它不仅仅是代码的堆砌,更是艺术与科技的完美结合。通过掌握自定义视图,开发者能够打破常规,创造出独一无二的用户界面。本文将带你走进自定义视图的世界,从基础概念到实战应用,一步步展示如何用代码绘出心中的蓝图。无论你是初学者还是有经验的开发者,这篇文章都将为你打开一扇通往创意和效率的大门。让我们一起探索自定义视图的秘密,将你的应用打造成一件艺术品吧!
67 10