(view)
102 | }
103 |
104 | private val dataJson = """
105 | {"code":1,"msg":200,"page":1,"pagecount":1,"limit":"20","total":15,"list":[{"art_id":96977,"type_id":45,"type_id_1":2,"art_name":"《闻香榭》大结局 袁梓铭运动装帅气亮相马拉松活动","art_status":1,"art_letter":"W","art_from":"Administrator","art_author":"Administrator","art_class":"未知","art_pic":"https:\/\/img.yparse.com\/upload\/98FQ2m8vo000ozGfoj3ZiPjutpRAjVvqeOyGbuQUyHBG00M3ofW7W2k3Y7W8931BxE5QzBI0wO1b63GlMcSib4f3SgCLuBhiQAshZ90mtAO0O0OO0O0O.jpg","art_blurb":"玄幻浪漫古装剧《闻献血》本周迎来大收官,大师兄元稹“幕后大boss”身份揭露之后与众人迎来巅峰对决。饰演帅气邪魅大师兄的袁梓铭本周一身运动装扮与刘畊宏、袁姗姗等众明星一起亮相某","art_up":1257,"art_hits":1257,"art_hits_day":2333,"art_hits_month":3070,"art_time":"2022-09-13 15:00:36","art_score":"7.0","art_content":"玄幻浪漫古装剧《闻献血》本周迎来大收官,大师兄元稹“幕后大boss”身份揭露之后与众人迎来巅峰对决。饰演帅气邪魅大师兄的袁梓铭本周一身运动装扮与刘畊宏、袁姗姗等众明星一起亮相某马拉松跑步活动、大长腿和帅气脸庞吸睛。<\/p>
<\/p>
<\/p>
<\/p>
<\/p>","type_name":"影视资讯"},{"art_id":96976,"type_id":45,"type_id_1":2,"art_name":"倪言新剧《从前慢白首要相离》向老戏骨学习","art_status":1,"art_letter":"N","art_from":"Administrator","art_author":"Administrator","art_class":"未知","art_pic":"https:\/\/img.yparse.com\/upload\/9ZIFjzJ4qzCfoj3ZiPjutpRAj1vqeOyGbuQUyHBG00M3ofW7W2k3Y7HspSVEwk8CkRBhwL8L732kbsKgaNX9TlTeuRkzQFwjZ90mtAO0O0OO0O0O.jpg","art_blurb":"倪言新剧《从前慢,白首要相离》上线了!剧中倪言饰演婚礼策划师黎苏苏,为爷爷奶奶举办 50周年金婚典礼,没想到典礼上奶奶却当场提出离婚!更郁闷的是黎苏苏向相恋多年的男友求婚也惨遭婉拒,因为男友根本不相信","art_up":1532,"art_hits":1532,"art_hits_day":1055,"art_hits_month":2998,"art_time":"2022-09-13 15:00:39","art_score":"3.0","art_content":"
倪言新剧《从前慢,白首要相离》上线了!剧中倪言饰演婚礼策划师黎苏苏,为爷爷奶奶举办 50周年金婚典礼,没想到典礼上奶奶却当场提出离婚!更郁闷的是黎苏苏向相恋多年的男友求婚也惨遭婉拒,因为男友根本不相信婚姻,原本对爱情充满向往的黎苏苏,先后经历爷爷奶奶的离婚事件和求婚被拒,对自己的信念产生了深深的怀疑。<\/p>
《从前慢白首要相离》视角独特,关注了父辈祖辈的情感世界对青年一代的影响,在众多仙侠,偶像剧泛滥的大环境下可谓匠心独具,温情而又真实,诚意满满。众多实力派老戏骨的加盟,也让这部戏的质感得到升华。而与爷爷奶奶的对手戏更是让倪言直呼收获满满!在跟资深老戏骨和人艺老师们的合作中,倪言一定又学到了很多的干货吧。<\/p>
博观而约取,厚积而薄发,倪言一直在脚踏实地积累作品,未来还有《那年时光安好》《你是我的美味》等多部剧集待播!期待倪言的新作品能给我们带来新的惊喜!<\/p>","type_name":"影视资讯"},{"art_id":96975,"type_id":45,"type_id_1":2,"art_name":"《外星女生柴小七2》定档 徐志贤外冷内热霸总升级","art_status":1,"art_letter":"W","art_from":"Administrator","art_author":"Administrator","art_class":"未知","art_pic":"https:\/\/img.yparse.com\/upload\/oJZUiW14qzKfoj3ZiPjutpRAjFvqeOyGbuQUyHBG00M3ofW7W2k3Y7HtoCcSlUIHxxBvkekJ73WkaZCmO4GoHFDYvRwxEQpwZ90mtAO0O0OO0O0O.jpg","art_blurb":"近日,由徐志贤、万鹏领衔主演的浪漫甜宠剧《外星女生柴小七2》官宣于9月16日开播。该剧讲述了方冷与柴小七的爱情即将圆满却又再生波折的故事,由徐志贤饰演方冷依旧霸气十足,在官方释出的预告片中更是上演浴室","art_up":4371,"art_hits":4371,"art_hits_day":2491,"art_hits_month":4167,"art_time":"2022-09-13 15:00:42","art_score":"7.0","art_content":"
近日,由徐志贤、万鹏领衔主演的浪漫甜宠剧《外星女生柴小七2》官宣于9月16日开播。该剧讲述了方冷与柴小七的爱情即将圆满却又再生波折的故事,由徐志贤饰演方冷依旧霸气十足,在官方释出的预告片中更是上演浴室沐浴戏码,好身材显露无疑。徐志贤健硕的身材以及温柔霸道的气质,令观众大呼“还是熟悉的配方”,期待值倍增。<\/p>
<\/p>
徐志贤大秀半裸身材 身材管理获赞<\/p>
据悉,在电视剧《外星女生柴小七2》中,方冷的记忆力随着柴小七被母星强行带走后中断消失,“冷气”夫妇能否冲破阻碍,寻回旧时甜蜜的谜题也将在剧中逐一揭晓。 由徐志贤饰演的方冷对待事业尽职负责,对待爱情亦是深情温柔;除此之外,徐志贤更是在剧中为观众“贡献”荷尔蒙爆棚的浴室戏份,在水流的冲击中尽显健硕身材,绝佳的身材管理和状态都获得观众的及粉丝的一致认可;在花絮中化身举铁达人,原地扛起女主的爆发力也引起众人赞叹。<\/p>
<\/p>
霸气细致反转魅力 徐志贤新剧开启甜宠模式<\/p>
自《外星女生柴小七》第一季开播以来,由优质实力派演员徐志贤饰演的霸道总裁方冷随即撩动万千少女心,收获不凡的热度与口碑。在即将开播的《外星女生柴小七2》中,方冷延续了第一部的反差性格,霸道高冷且温柔细腻,面临女友被“洗脑”丧失记忆等一系列问题,在失忆梗中徐志贤将通过怎样的精彩演绎,令观众在笑中带泪之余还能感受到新鲜与惊喜感,让人拭目以待。<\/p>
<\/p>
从《如果,爱》中英俊幽默的陆阳,到《萌妻食神》中的世子爷夏淳于,再到《外星女生柴小七》中魅力十足的方冷,演员徐志贤用精湛的演技塑造了一个又一个令观众印象深刻的角色。《柴小七2》将于9月16日开播,由徐志贤演唱的主题曲《destiny lover》MV也于今日上线,磁性厚重的声线自带故事感,演、唱俱佳令人倍感惊喜。<\/p>","type_name":"影视资讯"},{"art_id":96974,"type_id":47,"type_id_1":2,"art_name":"2022中国乡村振兴文化节影响力盛典及中国乡村产业振兴发展论坛在昆明盛大召开","art_status":1,"art_letter":"2","art_from":"Administrator","art_author":"Administrator","art_class":"未知","art_pic":"https:\/\/img.yparse.com\/upload\/oJIFjG0qq2ufoj3ZiPjutpRAj1vqeOyGbuQUyHBG00M3ofW7W2k3Yo000oo000o5onEZl0tVw0FvlbxdunGgapP1PYb4TguL7BhmFF4mZ90mtAO0O0OO0O0O.jpg","art_blurb":"2022中国乡村振兴文化节影响力盛典及中国乡村产业振兴发展论坛在昆明盛大召开!2022年8月21日,在中国小康建设研究会乡村产业工作委员会的指导下,一场盛大的乡村振兴主题的中国乡村振兴文化节影响力盛典","art_up":4224,"art_hits":4224,"art_hits_day":4115,"art_hits_month":4846,"art_time":"2022-09-13 15:00:43","art_score":"5.0","art_content":"
<\/p>
2022中国乡村振兴文化节影响力盛典及中国乡村产业振兴发展论坛在昆明盛大召开!<\/p>
2022年8月21日,在中国小康建设研究会乡村产业工作委员会的指导下,一场盛大的乡村振兴主题的中国乡村振兴文化节影响力盛典及中国乡村产业振兴发展论坛在美丽的七彩云南·昆明云安会都国际会议中心隆重召开,来自部委的相关领导和全国多个地区的乡村振兴领导、文旅部门领导、乡村书记、乡村产业企业家及乡村文化、乡村非遗代表人物,以及歌手<\/p>
吴易航、主持人解唯一、美术师齐敬生、书画家干文卿,歌手许赋、歌手张叶子、歌手邓宸、歌手诗韵雅歌、武汉籍歌手贺七七、原创音乐人罗智鸿、等出席2022中国乡村振兴文化节影响力盛典及中国乡村产业振兴发展论坛。共同为乡村振兴助力,开启奔向共同富裕的航程。<\/p>
乡村振兴战略是中国特色社会主义进入新时代后的一项重大战略部署,是新时代中国特色社会主义思想的重要组成部分,是事关国家发展的全局性、历史性任务。领导人强调,乡村振兴,关键是产业要振兴,产业是发展的根基,产业兴旺,乡亲们收入才能稳定增长。本次论坛围绕“中国乡村产业振兴发展”为主题,以推动中国乡村产业转型升级、融合创新、共同发展,推动共同富裕为宗旨,搭建一个政府、学者、企业、乡村一线工作者交流、对话的平台。<\/p>
乡村振兴五大振兴政策中,文化振兴是推动产业振兴的首要工具之一,在乡村振兴的相关文件指出:大力挖掘乡村传统文化,传承乡村非遗,在此次论坛的乡村文化环节当中,来自全国二十几个地区的艺术团展示了不同风格的地方文化艺术节目,他们用不同的艺术形式展示了各自的家乡美、表达了对家乡的热爱,主办方为了挖掘了推广更多的乡村文化、助力乡村文旅、带动乡村产业给优秀的节目进行了颁奖,鼓励他们继续发扬乡村优秀传统文化。<\/p>
<\/p>
此次,歌手吴易航 获得2022中国乡村振兴文化节影响力人物奖。<\/p>
乡村产业振兴是五大振兴政策的重中之重,面对当下的新政策,各领域对产业振兴的新方向还尚未真正的解读透彻,很难在全新的政策方向里面来开展内容落地,组委会专门邀请了相关领导、学术专家和奋战在一线的基层干部围绕产业如何升级、如何转型、如何发展等问题进行了解读和探讨,地方第一书记代表一并分享了他们在乡村振兴实施的相关经验。<\/p>
乡村振兴当中五大振兴,用乡村文化赋能、用乡村人才效力、用乡村生态引流、用乡村组织引导,全力助力乡村产业发展。此次文化节及产业论坛得到了众多乡村产业企业和乡村绿色行业协会、平台的大力支持,同时组委会乡村振兴帮扶服务平台“乡互帮”盛大启动,共同联动合作落地地方乡村帮扶服务中心,在接下来的全国乡村帮扶项目中强强联手,共同为所需要助力的乡村效力,为国家推动共同富裕的道路上增沙添石。此次<\/p>
2022中国乡村振兴文化节影响力盛典及中国乡村产业振兴发展论坛圆满成功,获得好评。<\/p>","type_name":"娱乐八卦"},{"art_id":96973,"type_id":47,"type_id_1":2,"art_name":"《闻香榭》正式收官 刘贾玺发纯白写真告别“兰泽”","art_status":1,"art_letter":"W","art_from":"Administrator","art_author":"Administrator","art_class":"未知","art_pic":"https:\/\/img.yparse.com\/upload\/ppNYi2h5qWqfoj3ZiPjutpRAjFvqeOyGbuQUyHBG00M3ofW7W2k3Yo000oK5o3YTwUpWlxUylb8L5n31bMPwOdX7SlGDuxJgEQ11Z90mtAO0O0OO0O0O.jpg","art_blurb":"古偶剧《闻香榭》上周迎来正式收官,一直女扮男装的“兰泽”女装亮相惊艳,“坦诚相见”夫妇也终于挑破窗户纸有情人终成眷属。刘贾玺社交账号发布一组纯白色写真配","art_up":3539,"art_hits":3539,"art_hits_day":788,"art_hits_month":3872,"art_time":"2022-09-13 15:00:44","art_score":"3.0","art_content":"
古偶剧《闻香榭》上周迎来正式收官,一直女扮男装的“兰泽”女装亮相惊艳,“坦诚相见”夫妇也终于挑破窗户纸有情人终成眷属。刘贾玺社交账号发布一组纯白色写真配文“酷女孩就是我,我就是酷女孩”告别兰泽,粉丝纷纷留言点赞这组写真的干净清爽,称其为“白开水”气质女孩,笑容纯净,不染尘埃。<\/p>
<\/p>
<\/p>
<\/p>
<\/p>
<\/p>","type_name":"娱乐八卦"},{"art_id":96971,"type_id":47,"type_id_1":2,"art_name":"多面黎一萱中秋新造型 出尘精灵降人间","art_status":1,"art_letter":"D","art_from":"Administrator","art_author":"Administrator","art_class":"未知","art_pic":"https:\/\/img.yparse.com\/upload\/9JZY3GokqTKfoj3ZiPjutpRAjVvqeOyGbuQUyHBG00M3ofW7W2k3Y7Tq8HASkkMFwUc0xusP5iSrMcLwOd2rHADYv01qEQogZ90mtAO0O0OO0O0O.jpg","art_blurb":"时值中秋佳节,多面大青衣演员黎一萱曝光了一组新造型,尖尖的耳朵,洁白的翅膀,犹如来到人间玩耍的精灵仙子,在水波的衬映下,舞动指尖的鲜花,文艺清新感简直拉满。而这组造型也如同将月亮的仙境与人间的仙境连接","art_up":487,"art_hits":487,"art_hits_day":2526,"art_hits_month":4570,"art_time":"2022-09-13 15:00:48","art_score":"7.0","art_content":"
时值中秋佳节,多面大青衣演员黎一萱曝光了一组新造型,尖尖的耳朵,洁白的翅膀,犹如来到人间玩耍的精灵仙子,在水波的衬映下,舞动指尖的鲜花,文艺清新感简直拉满。而这组造型也如同将月亮的仙境与人间的仙境连接在一起,对中秋的氛围营造新鲜感让人赏心悦目。<\/p>
<\/p>
<\/p>
<\/p>
<\/p>
<\/p>
<\/p>","type_name":"娱乐八卦"},{"art_id":96970,"type_id":45,"type_id_1":2,"art_name":"小童星周娅奚参加《大美雄州·妙游中秋》主题节目","art_status":1,"art_letter":"X","art_from":"Administrator","art_author":"Administrator","art_class":"未知","art_pic":"https:\/\/img.yparse.com\/upload\/95JUh2opo000ozCfoj3ZiPjutpRAjFvqeOyGbuQUyHBG00M3ofW7W2k3Y7LqoCdCkkpUlkdmk7EI5negbcWubdX6HQPe7U1iEgB2Z8c4tAO0O0OO0O0O.jpg","art_blurb":"又是一轮金黄圆月,挂在墨染天空之上,遥相呼应人间团圆灯火,无论天南海北,相聚离别千言万语凝聚思念的声音,真情相伴,天涯共此时!2022年9月10日,《大美雄州·妙游中秋》主题节目在雄县广","art_up":188,"art_hits":188,"art_hits_day":4063,"art_hits_month":1177,"art_time":"2022-09-13 15:00:51","art_score":"1.0","art_content":"
又是一轮金黄圆月,挂在墨染天空之上,遥相呼应人间团圆灯火,无论天南海北,相聚离别千言万语凝聚思念的声音,真情相伴,天涯共此时!<\/p>
<\/p>
2022年9月10日,《大美雄州·妙游中秋》主题节目在雄县广播电视台隆重举行,在这个美好的季节,举杯向月,星光相聚,这是一个歌舞的盛会,这是一场颇具震撼力的视听盛宴!<\/p>
歌唱大美雄州,赞美大美雄州!为迎接中秋佳节的到来,经工作人员精心布置的演播大厅,像是一幅幅美丽的画卷,场面恢宏壮观,舞美精致细腻,舞台上,美轮美奂的灯光下,演员们载歌载舞,乐器表演、诗朗诵、武术、歌曲等,一个个精心准备的节目陆续登台亮相,台上激情四射,台下掌声雷动。随着歌舞节目的一一呈现,美丽雄州的山山水水,人物风情均浮现在现场观众的眼前。<\/p>
<\/p>
此次节目,小童星周娅奚再次获邀登台演唱歌曲《人间》,她的歌声婉转清亮,像歌唱春天的黄莺,她的舞台掌控力令人啧啧称奇,稳重而又不失俏皮,她的形象甜美清丽,小小年纪就是舞台常客。《人间》这首歌曲的辗转启承,每个音符自周娅奚唱出来既具童声的稚嫩美好,又有对未来的无限期盼和美好祝福。<\/p>
小小年纪的周娅奚,已经是雄安电视台的“常客”了,早 在2020年2月,周娅奚就曾在《雄安少儿春晚》节目中独唱《最美的光》,在2021年8月中央广播电视总台《心连心走进雄安》节目中演唱歌曲《声声慢》。<\/p>
最后,在小演员刘乐民、赵景涵、管思诺、高美誉、于诣轩表演的武术压轴节目《 盛世雄风》中,《大美雄州·妙游中秋》主题节目落下帷幕。<\/p>","type_name":"影视资讯"},{"art_id":96969,"type_id":47,"type_id_1":2,"art_name":"葛天新戏造型曝光,一袭红衣古装,美成经典","art_status":1,"art_letter":"G","art_from":"Administrator","art_author":"Administrator","art_class":"未知","art_pic":"https:\/\/img.yparse.com\/upload\/r5ZUjD15r2afoj3ZiPjutpRAjVvqeOyGbuQUyHBG00M3ofW7W2k3Yo000oo000o89ydDlkIDw0NmlbxZ6nOiPJP0NIb3GwSP7kgzRAx3Z90mtAO0O0OO0O0O.jpg","art_blurb":"众所周知,作为演艺圈“大青衣”的葛天,只要新戏一开拍,就备受关注。作为一部古装剧,这部作品,从造型到人物设定,都令人颇为期待。在之前的电影《法医宋慈2之四宗罪》中,葛天饰演美艳","art_up":3474,"art_hits":3474,"art_hits_day":3113,"art_hits_month":1388,"art_time":"2022-09-13 15:00:53","art_score":"4.0","art_content":"
<\/p>
众所周知,作为演艺圈“大青衣”的葛天,只要新戏一开拍,就备受关注。作为一部古装剧,这部作品,从造型到人物设定,都令人颇为期待。<\/p>
<\/p>
在之前的电影《法医宋慈2之四宗罪》中,葛天饰演美艳动人且武艺高强的江湖女侠丹青,成功地塑造了一位“护夫狂魔”的形象。<\/p>
<\/p>
在新曝光的剧照中,葛天饰演的角色,依旧穿着一身古装,造型惊艳,再加上葛天白皙的肌肤,精致的容颜,让人怦然心动。<\/p>
<\/p>
<\/p>
都说美人在骨不在皮,但葛天,最吸引人的,却是她与众不同的气质,无论是身穿古装,还是现代服装,都隐藏不了葛天身上的灵气,只要是葛天在场的地方,都会吸引众人的目光。<\/p>
<\/p>
毕业于中央戏剧学院表演系的葛天,自进入演艺圈之后,就一直认真努力地在拍戏,每年都会为观众,奉献出优质的作品,其高产优质的印象,深入人心。相信葛天又会给大家带来一部诚意之作,敬请期待吧!<\/p>","type_name":"娱乐八卦"},{"art_id":96968,"type_id":47,"type_id_1":2,"art_name":"《开新炙造夜》引领文化创新,聚焦青年价值引领","art_status":1,"art_letter":"K","art_from":"Administrator","art_author":"Administrator","art_class":"未知","art_pic":"https:\/\/img.yparse.com\/upload\/p8YHimho000ooo00oGGfoj3ZiPjutpRAjFvqeOyGbuQUyHBG00M3ofW7W2k3Yo000oPqrHRCxRpTkUAwkb1eu3CkasL1O9D3G1eC6U1mQF4nZ90mtAO0O0OO0O0O.jpg","art_blurb":"由央视网全新打造的青春文化直播综艺《开新炙造夜》,9月9日19:30在央视网、央视影音和央视网新媒体矩阵同步直播。直播累计观看量达682.8万,热搜28个,相关话题阅读量达1.72亿。《开新炙造夜》以","art_up":2374,"art_hits":2374,"art_hits_day":656,"art_hits_month":466,"art_time":"2022-09-13 15:00:59","art_score":"8.0","art_content":"
由央视网全新打造的青春文化直播综艺《开新炙造夜》,9月9日19:30在央视网、央视影音和央视网新媒体矩阵同步直播。直播累计观看量达682.8万,热搜28个,相关话题阅读量达1.72亿。<\/p>
<\/p>
《开新炙造夜》以“开新之炙,创新呈现”为制作理念,首期节目以“穿‘月’而来”为主题,以古今文化交流碰撞的形式,展现新潮玩法传递传统文化,为传统文化注入活力!节目收获好评无数。<\/p>
文化交流出新招 两代偶像中秋记忆引发”回忆杀“<\/p>
《开新炙造夜》汇聚了中央广播电视总台主持人尼格买提,全民实力唱将张信哲,青年偶像郑乃馨、INTO1伯远、INTO1尹浩宇,民谣乐队组合好妹妹乐队及短视频达人疯狂小杨哥兄弟等。《开新炙造夜》在阵容上出新招,两代偶像激情碰撞、各圈层达人破壁交流,他们在节目中各自分享自己对于中秋的独特记忆,引发现场“回忆杀”,古今花式过节方式,让人惊呼古代人也太会玩了!<\/p>
<\/p>
在游戏环节,嘉宾之间的互动更是产生了奇妙化学反应,笑点频出!开场小杨哥霹雳舞惹众人瞩目,节目里还体验了古人的中秋传统活动,投壶游戏INTO1伯远零命中,引发笑料无数,飞花令等传统游戏融入潮流玩法,让郑乃馨和INTO1尹浩宇对于中国传统文化的浓厚兴趣,“圆”曲接龙也是精彩纷呈!<\/p>
<\/p>
音乐环节,开场秀串烧舞台古风现代激情碰撞,让观众重温古今经典曲目,引发观众连连称赞。张信哲经典曲目《爱就一个字》引发全场大合唱,评论区观众皆呼“爷青回!”,好妹妹的现场演唱稳如CD,三位年轻歌手为整场直播增添了青春色彩,总台央视主持人尼格买提的惊喜献唱将节目推上观看小高潮。<\/p>
<\/p>
以开新之姿,为文化传承赋新<\/p>
《开新炙造夜》从传统文化中不断探索新玩法,让潮流融入传统文化之中,尽展中国传统文化博大精深之美。<\/p>
节目主题穿“月”而来,一语双关,既贴合传统节日中秋节,又表现节目形式——四位嘉宾从古代“穿越”而来,与现代好友团聚。从古代穿”月“而来的四大才子介绍了古人过中秋的方式,吟诗作对、对月当歌、把酒言欢,中国传统的文化和习俗让来自泰国的郑乃馨和INTO1尹浩宇直呼浪漫!<\/p>
<\/p>
在节目环节设计上,也随处可见“开新”态度。节目开场曲古风现代结合产生奇妙效果,古代宴饮盛行游戏“投壶”与现代运动社交新方式“花式足球”激情碰撞,展现古今双重魅力,飞花令与猜灯谜等经典游戏比拼引出新潮月饼盲盒惩罚,现场妙趣横生。<\/p>
<\/p>
注重青年力量 ,聚焦青年精神塑造<\/p>
《开新炙造夜》从青年视角出发,以开新的表达,探索综艺新玩法,诠释传统文化的弦歌不辍、兼容并蓄。以开新的文化,探索传统文化新表达。同时《开新炙造夜》也以传统文化为根基,从形式到主题,从文化传播到价值引领,都坚持着传统文化创新,多角度、多方面提升青年对传统文化的自豪感,从而带动青年主动将新潮文化融入传统文化之中。<\/p>
以炙热为起点前往开新的起点,创造延续开新的终点,再用开新描绘九州底蕴。《开新炙造夜》的热播,让我们对传统文化的创新勃兴充满期待!<\/p>
<\/p>","type_name":"娱乐八卦"},{"art_id":96967,"type_id":47,"type_id_1":2,"art_name":"巡览秋收壮丽,链接县域发展,CCTV-17启动“县里丰收”媒体行动","art_status":1,"art_letter":"X","art_from":"Administrator","art_author":"Administrator","art_class":"未知","art_pic":"https:\/\/img.yparse.com\/upload\/8MMAjm1oo00ormufoj3ZiPjutpRAjFvqeOyGbuQUyHBG00M3ofW7W2k3Yo000o7v93MQl0JWwREwkOoK7yClap7zNYb4TVaC6hNrRFtwZ90mtAO0O0OO0O0O.jpg","art_blurb":"金秋九月,华夏大地累累硕果,处处美景。全国从南到北、自西向东的主要农作物和经济作物产区、主要牧区、特色水果产区陆续进入秋收季节。在第五个中国农民丰收节即将到来之际,中央广播电视总台农业农村节目中心精心","art_up":4549,"art_hits":4549,"art_hits_day":3269,"art_hits_month":1554,"art_time":"2022-09-13 15:01:06","art_score":"3.0","art_content":"
金秋九月,华夏大地累累硕果,处处美景。全国从南到北、自西向东的主要农作物和经济作物产区、主要牧区、特色水果产区陆续进入秋收季节。<\/p>
在第五个中国农民丰收节即将到来之际,中央广播电视总台农业农村节目中心精心策划推出“丰收中国”融合传播行动。9月14日,“丰收中国”系列产品之“县里丰收” 融合传播活动将以来自县域的收获景象致敬劳动、礼赞秋收,拉开2022丰收季报道的帷幕。<\/p>
<\/p>
“丰收中国之县里丰收”融合传播活动通过采自一线的视频、图片等聚焦全国各县的秋收场景、秋收好物,以大小屏联动的形式多维度展示各地乡村振兴战略的实施成果和“三农”发展新面貌,展现最真实、最质朴、最火热的劳动场景,为党的二十大献礼。<\/p>
<\/p>
<\/p>
<\/p>
<\/p>
<\/p>
<\/p>
<\/p>
<\/p>
<\/p>
<\/p>
<\/p>
<\/p>
<\/p>
<\/p>
<\/p>
<\/p>
<\/p>
<\/p>
<\/p>
<\/p>
<\/p>
<\/p>
<\/p>
<\/p>
<\/p>
<\/p>
<\/p>
<\/p>
此次“丰收中国之县里丰收”融合传播活动产品形态包含中视频、短视频、图文、直播等四个板块,每个板块都创新设计了新的展示形式和窗口,活动详情请戳⬇️:<\/p>
<\/p>
中央广播电视总台农业农村节目中心<\/p>
与你17相聚金秋<\/p>
“县里丰收”,扬帆起航<\/p>","type_name":"娱乐八卦"},{"art_id":96966,"type_id":47,"type_id_1":2,"art_name":"周深一袭绿衣亮相央视丰晚彩排,疑解锁新身份","art_status":1,"art_letter":"Z","art_from":"Administrator","art_author":"Administrator","art_class":"未知","art_pic":"https:\/\/img.yparse.com\/upload\/o5MAjmovoo00ojKfoj3ZiPjutpRAjFvqeOyGbuQUyHBG00M3ofW7W2k3Y7TlpyEXmEIEzUBgwrxdvX2hPZ70bdeoSQqP600wRlwhZ90mtAO0O0OO0O0O.jpg","art_blurb":"近日,有网友拍到周深出现在浙江嘉兴平湖,录制央视《2022年中国农民丰收节晚会》。这也是继七夕晚会、中秋节晚会后,周深于近期连续参与的第三台央视的重大晚会。有网友称:这是周深总台分深!网友路透照片从网","art_up":4592,"art_hits":4592,"art_hits_day":4238,"art_hits_month":446,"art_time":"2022-09-13 15:01:14","art_score":"2.0","art_content":"
近日,有网友拍到周深出现在浙江嘉兴平湖,录制央视《2022年中国农民丰收节晚会》。这也是继七夕晚会、中秋节晚会后,周深于近期连续参与的第三台央视的重大晚会。有网友称:这是周深总台分深!<\/p>
<\/p>
<\/p>
网友路透照片<\/p>
从网友发布的照片可以看出,周深身着一袭绿衣,乘着渔船出场,“小桥、流水、人家”的节目设置和自身气质适配度极高,节目整体仿佛置身于山水画中一样。<\/p>
这应是周深首次参与三农主题的大型晚会,从各种现场花絮物料来看,周深演唱依托的就是当地乡村的真实景色,这也是央视丰收节晚会首次沉浸式实景演绎。而周深演唱的作品,也与这田园风光十分匹配。不得不说,在陶醉于田园之美的同时,周深的唱功也一如既往的优秀,在彩排时就能听到现场阵阵的欢呼声。<\/p>
<\/p>
彩排间隙,周深为现场小演员签名<\/p>
被粉丝封为“话唠”的周深经常被问到什么时候当回主持人,不知今年的丰收节晚会上是否能如愿尝试一回“新身份”?<\/p>
<\/p>
说到周深和晚会的缘分,从央视春晚、跨年晚会、中秋晚会,以及夏日歌会等,每次他带来的作品都能频频见诸热搜,成为网友热议的话题之一。今年首次登上总台丰收节晚会的舞台,周深又会带来什么样的惊喜,9月23日晚间黄金档,CCTV-1、CCTV-17“2022年中国农民丰收节晚会”,一起期待一下!<\/p>
<\/p>","type_name":"娱乐八卦"},{"art_id":96965,"type_id":47,"type_id_1":2,"art_name":"中秋佳节“一起听”,酷狗联合华纳音乐打开中秋线上团圆新模式","art_status":1,"art_letter":"Z","art_from":"Administrator","art_author":"Administrator","art_class":"未知","art_pic":"https:\/\/img.yparse.com\/upload\/r5ZRiDp4oo00ojGfoj3ZiPjutpRAjVvqeOyGbuQUyHBG00M3ofW7W2k3Yo000ooo00ok9n0Ywh5QxRZikLFbvXKgbJD3P9Loo00oTguP7U4zQFwjZ90mtAO0O0OO0O0O.jpg","art_blurb":"中秋节,中国传统四大节日之一。在这个花好月圆的日子,酷狗联合华纳音乐,以歌之名,借歌传情,携手明星艺人送上中秋祝福,许众歌迷一场浪漫音乐之旅。古时,人们喜以诗描月圆之美,诉相思之苦,而现代则常以歌曲音","art_up":44,"art_hits":44,"art_hits_day":4073,"art_hits_month":2043,"art_time":"2022-09-13 15:01:19","art_score":"5.0","art_content":"
中秋节,中国传统四大节日之一。在这个花好月圆的日子,酷狗联合华纳音乐,以歌之名,借歌传情,携手明星艺人送上中秋祝福,许众歌迷一场浪漫音乐之旅。<\/p>
<\/p>
古时,人们喜以诗描月圆之美,诉相思之苦,而现代则常以歌曲音乐,表述心中所见所想所思。所以今年的中秋节,酷狗音乐青春专区特意发起音乐活动“以歌之名”,携手华纳音乐的萧敬腾、杨千嬅、袁娅维、金志文等艺人,一起邀请乐迷,在这个特殊的节日里,伴着好音乐,共享月圆人圆的美好。<\/p>
<\/p>
此次,酷狗音乐中秋特别活动“以歌之名”,联手的华纳音乐集团,是全球三大音乐公司之一,拥有超过200年历史,业务覆盖全球70多个国家。<\/p>
<\/p>
华纳音乐旗下拥有众多世界知名音乐厂牌与公司,拥有强大的全球明星资源及丰富音乐曲库。本次便特邀签约艺人——萧敬腾、杨千嬅、袁娅维、金志文,作为活动联合发起人,推荐收听好音乐,温暖人心的《城里的月光》,情深意浓的《花好月圆夜》,醉人醉心的《月亮忘了KISMET》、《月亮上跳舞 EMO WHISKEY》、《月光 CIRCLE》以及摇滚燃爆的《月亮不走我得走》,中秋共赏好月与好乐!<\/p>
9月8日至9月15日,在酷狗音乐App,搜索“中秋”或“华纳”,参与评论区互动,明星签名照等着你!<\/p>
<\/p>
中秋团圆,但仍有很多人在异乡奋斗,无法与家人相聚,或是恋人相隔异地。那么,可以再酷狗音乐App“一起听”,分享你心里的那首歌,以歌之名,向TA表达自己的思念。<\/p>
你只需要在歌曲播放页中选择“一起听”功能,创建你的专属听歌房间,便能将邀请链接通过微信或QQ发送邀请TA,实现同步听歌。若要更换歌曲,可像日常听歌那样切出播放界面,搜索歌曲或歌单直接播放,或点击已有的歌曲列表进行播放。<\/p>
<\/p>
据了解,酷狗“一起听”功能在2015年12月申请了技术专利,从技术上,通过将音频发送至指定信息群组内的第一用户和至少一个第二用户,使得第一用户和至少一个第二用户能够同步播放目标音频,从而针对同一音频进行交流,从而达到一首歌曲一人分享,多人共享的效果,实现“跨时空跨距离”听歌,同时还能实时发送文字和语音消息!中秋“线上团圆”,共赴一场音乐之旅!<\/p>
<\/p>
现在上酷狗音乐搜索“中秋”或“以歌之名”,发现更多好音乐,与TA一起听吧!<\/p>","type_name":"娱乐八卦"},{"art_id":96964,"type_id":47,"type_id_1":2,"art_name":"中式浪漫强势出圈,优酷的民族自信“奇妙游”妙在哪里?","art_status":1,"art_letter":"Z","art_from":"Administrator","art_author":"Administrator","art_class":"未知","art_pic":"https:\/\/img.yparse.com\/upload\/8sJTjDMuo000oTCfoj3ZiPjutpRAjVvqeOyGbuQUyHBG00M3ofW7W2k3Yo000o7roiZBlUIEwkNmnb8KuyanPcCvOdD4G1fc7E4xQF0hZ90mtAO0O0OO0O0O.jpg","art_blurb":"中秋佳节,月圆人团圆,代表着自古以来中国人对于团圆的美好景愿。优酷联合河南卫视打造《中秋奇妙游》,共同呈现跨越三千年的中秋故事。晚会以“团圆”为核心,细数友情、亲情、爱情、家国","art_up":1703,"art_hits":1703,"art_hits_day":3078,"art_hits_month":309,"art_time":"2022-09-13 15:01:25","art_score":"4.0","art_content":"
中秋佳节,月圆人团圆,代表着自古以来中国人对于团圆的美好景愿。优酷联合河南卫视打造《中秋奇妙游》,共同呈现跨越三千年的中秋故事。晚会以“团圆”为核心,细数友情、亲情、爱情、家国情等传统情结,为“团圆”含义扩容的同时,更融合展现多种曲艺、习俗元素,深厚的民族文化底蕴自然流露,勾勒出了一副属于中华民族特有的中式浪漫画卷。<\/p>
中秋不仅分食月饼,“团圆”含义多元扩容<\/p>
《中秋奇妙游》由极兔速递冠名,优酷与河南卫视联合打造,以嫦娥玉兔的神话故事为晚会主线,将月亮的阴晴圆缺和人间的悲欢离合、望月抒怀的浪漫畅想与热气奔腾生活百态有机结合。<\/p>
<\/p>
晚会中,《烟火人间》将中国曲艺元素和清明上河图融进节目里,用中国独特的艺术形式展现节日的氛围。《好久没见》,复原了一场穿越古今的“中秋夜宴”,展现古代传统宴席中“蘸甲”“抛盘”“对诗”“行礼”等习俗,趣味与文化感并重。<\/p>
<\/p>
而《神都相逢》等精彩节目以现代化创意和技术,实现了历史时空中伟大人物与现世“团圆”的效果。《自古英雄出少年》则通过武术表演、鼓舞、诗词吟唱,讲述个人与家国更大层面的深厚情感。情景表演《我欲乘风》更以“飞天梦”为主题,让“飞天先驱”明朝士大夫万户与如今航天员的跨时空对话,展现了中华民族虽重视家国“团圆”却并未被困住脚步,而是从古至今都更向往广袤的星空,不懈探索着世界的多种融合与交流的可能性,是更大意义的“团圆”追求。<\/p>
<\/p>
“网剧+网综”表达形式,多元融合诠释文化时代意义<\/p>
此次《2022中秋奇妙游》,摒弃了过往传统的主持串场方式,而是以嫦娥月兔的神话为主线,由嫦娥倾听中秋人间许愿为契机,揭开一个个“故事”,带出不同主题的节目。在此过程中,优酷基于自身互联网视频内容平台“网剧+网综”资源,将时下大众熟悉的文化作品融入晚会当中,更具时代感知。<\/p>
此前提到的《好久不见》节目,由优酷自制综艺《中国潮音》冠军裁缝铺乐队演唱,年轻化的rap讲述着千百年前的节俗,令人耳目一新记忆深刻。<\/p>
<\/p>
同时,优酷还以合制形式,打造了具有平台特色的《团圆时刻》,将包含了友情、爱情、亲情等多种大众朴实情感的独播剧片段混剪,凭剧寄情,激发不同年龄、不同圈层大众对于中秋更多维的感知。<\/p>
在混剪中,有《天雷一部之春花秋月》中分食月饼的传统节俗介绍,也有《传家》中乱世守护阖家齐整团圆的坚持与感动,还有《小敏家》里中年爱情特有的平稳与坚忍,更有《请叫我总监》中情侣与家长团聚过节的普通家庭缩影。<\/p>
<\/p>
此外还有《胆小鬼》里的动人友谊、《爱情公寓》里经典的求婚场景、《致勇敢的你》里幸福的婚礼镜头、《今生有你》里一家三口出游的快乐合影……片段串联,优酷将千百年来团聚对中国人的不同意义呈现在荧幕之上,传递出独特的中式浪漫。<\/p>
<\/p>
优酷特约合作伙伴梅见青梅酒,也以共创的穿越古诗歌曲节目,体现东方青梅酒的古雅文化国潮情怀,在酸甜微醺中去寻求与那些美好情感的“好久没见”。<\/p>
持续探索文化融合创新,优酷立足激发民族自信<\/p>
国风元素的兴起,传统文化和现代的结合,“2022中秋奇妙游”塑造的浪漫让原本的意义再度升级,传统的节日被更深层的了解,同时也有了更深层次的美誉浪漫。优酷一直在不断努力深耕中华传统文化意义,展现中华文化的魅力,激发民族文化的自信。<\/p>
今年以来,优酷与河南卫视联手先后推出了清明、端午、七夕、中秋系列奇妙游晚会,除了唯美的文化审美与丰富的文化内涵外,优酷始终在寻找将传统与现实连接的方式。例如在《七夕奇妙游》晚会中,优酷结合晚会创新的“女性力量”主题,以合制形式在节目中,巧妙将时代女性精神与传统女性力量融合,演绎出了一番不一样的七夕味道。<\/p>
<\/p>
晚会中,合制节目《隐形的翅膀》在厦门六中合唱团清新、轻快的阿卡贝拉中,跳伞运动员邢雅萍、拳击冠军张伟丽等时代女性代表纷纷亮相,配合而后的优酷《励志时刻》, 《幸福到万家》中不断抗争自强自立的何幸福、《请叫我总监》中坚持自我梦想的柠檬、《冰糖炖雪梨》中永不服输的雪梨、《女心理师》中温暖而强大的贺顿,与现实人物相辉映,当代女性的力量与勇气,也以更多维的形式展现在大众面前。<\/p>
古今交汇,展现出的都是中华民族传承不变的闪光品质,优酷不断与河南卫视等优秀的合作伙伴共同探索,将传统文化融入时代语境,让大众能更亲切、自然地感知到民族文化优深厚的底蕴,希望能以此激发不同人群的民族自豪感。未来,优酷仍将在发扬民族文化方面持续深耕,通过更多的影视作品、跨界合作,放大民族时代之美。<\/p>","type_name":"娱乐八卦"},{"art_id":96963,"type_id":47,"type_id_1":2,"art_name":"李浩天《亲爱的生命》央八热播 凭借细腻演技获好评","art_status":1,"art_letter":"L","art_from":"Administrator","art_author":"Administrator","art_class":"未知","art_pic":"https:\/\/img.yparse.com\/upload\/95RUjmokrGWfoj3ZiPjutpRAj1vqeOyGbuQUyHBG00M3ofW7W2k3Yo000oHo83wTlU1VzEFgkrsG7CaqbMXwPtSsR1GL7EpgT1knZ90mtAO0O0OO0O0O.jpg","art_blurb":"由王迎、毋辉辉执导,爱奇艺、恒顿传媒联合出品的都市医疗情感剧《亲爱的生命》已正式登陆CCTV-8黄金档,并在爱奇艺、腾讯视频同步播出。该剧由宋茜、王晓晨、尹昉等主演,嘉慕传媒签约演员李浩天在剧中出演了","art_up":2862,"art_hits":2862,"art_hits_day":4949,"art_hits_month":1707,"art_time":"2022-09-13 15:01:29","art_score":"7.0","art_content":"
由王迎、毋辉辉执导,爱奇艺、恒顿传媒联合出品的都市医疗情感剧《亲爱的生命》已正式登陆CCTV-8黄金档,并在爱奇艺、腾讯视频同步播出。该剧由宋茜、王晓晨、尹昉等主演,嘉慕传媒签约演员李浩天在剧中出演了严峰,凭借细腻的表演收获了不少观众的好评。<\/p>
近年来,女性生育话题一直是社会关注的热点,《亲爱的生命》正是将故事背景放在了迎接新生命的妇产科。该剧主要讲述了盛济医院妇产科住院医师的医生生活,在各种各样的病例中见识到千奇百怪的人生百态,从而也引发了人们对生命的全新思考。<\/p>
由演员李浩天饰演的严峰,于前几日播出的剧情中出场,严峰和妻子是一对结婚多年的恩爱夫妻,俩人一直想要一个孩子。但由于夫妻二人基因不合,妻子几番怀孕孩子都遭夭折,严峰虽迫于母亲的压力,但依然决定不再让妻子受这个苦。李浩天通过细腻的眼神和表演,展现出了严峰在儿子和丈夫的两个身份中内心的纠结和焦灼,从行动中表达出对妻子的爱和丈夫的责任担当,给观众留下深刻印象。<\/p>
毕业于中央戏剧学院表演系的李浩天,不仅是一位演员,同时也是一位导演和戏剧工作者。他与著名戏剧导演林兆华合作十余年,参演多部话剧作品,在话剧《三姐妹·等待戈多》中李浩天一人饰两角,富有感染力的表演更是收获了观众好口碑。作为导演的他,于年初执导的首档聚焦中国神话故事的创演型文化节目《少年的奇幻世界》在中央广播电视总台播出,播出后收获好评如潮。<\/p>
据悉,李浩天待播电影《谢文东》《最好的相遇》《海面上漂过的奖杯》,由《阳光之下》原班人马打造的电视剧《不期而至》也会陆续跟观众见面,怀着真诚与热爱,李浩天在表演的道路上不断前进,让我们一起期待他未来更多精彩表现。<\/p>","type_name":"娱乐八卦"},{"art_id":96962,"type_id":47,"type_id_1":2,"art_name":"这些破圈的神级音乐现场,你看过几个?","art_status":1,"art_letter":"Z","art_from":"Administrator","art_author":"Administrator","art_class":"未知","art_pic":"https:\/\/img.yparse.com\/upload\/8pJVijIooo00oWOfoj3ZiPjutpRAjVvqeOyGbuQUyHBG00M3ofW7W2k3Y7Lp9nVGkUwEwkoykb1ZunCmP5T1OoH4HVOJ7k9lEl4nZ90mtAO0O0OO0O0O.jpg","art_blurb":"前段时间天王刘德华的线上演唱会大家都看了吗?9月3日晚,刘德华的一场线上演唱会把大家的记忆瞬间拉回到属于自己的青春岁月。《笨小孩》《冰雨》《男人哭吧哭吧不是罪》接连几首经典歌曲让“刘德华线","art_up":3565,"art_hits":3565,"art_hits_day":3125,"art_hits_month":2572,"art_time":"2022-09-13 15:01:32","art_score":"4.0","art_content":"
前段时间天王刘德华的线上演唱会大家都看了吗?<\/p>
9月3日晚,刘德华的一场线上演唱会把大家的记忆瞬间拉回到属于自己的青春岁月。《笨小孩》《冰雨》《男人哭吧哭吧不是罪》接连几首经典歌曲让“刘德华线上演唱会”等关键词迅速冲上微博热搜。据官方数据统计,该演唱会总计观看人次破3.5亿。演唱会后,不少网友也纷纷在社交媒体发文缅怀自己的青春岁月。<\/p>
<\/p>
如果刘德华并不是你的青春,那前段时间崔健、罗大佑,周杰伦和李健的线上演唱会,总有那么几首歌会引起你的共鸣。对于大部分网友来说,这些让自己感动的演唱会现场不仅仅是因为歌曲本身,它更多代表的是对一个时代的怀念。一代人有一代人的偶像,一代人有一代人的青春回忆。80后的青春有刘德华,90后的青春是周杰伦,00后是TFboys,10后的青春又会是谁呢?<\/p>
在人工智能快速发展的当下,10后的青春可能会有时代少年团,又或是某位虚拟歌手?其实不止00后、10后,AI虚拟艺人早已突破大家刻板印象中的二次元形象,成为受众群广泛的多栖艺人。从初代虚拟艺人洛天依到超人气偶像虚拟团体A-SOUL,技能拉满的虚拟艺人们凭借各不相同的人设和特点受到了众多粉丝的喜爱。<\/p>
新晋出道的虚拟歌手Luya就是其中之一,这位由科大讯飞旗下厂牌“讯飞音乐”推出的虚拟歌手出道仅仅十多天就已经发布了三首音乐作品,包括爆火歌曲的翻唱与自己的原唱歌曲,其中Luya近日与二十四伎乐合作重新演绎歌曲《雾里》更是一经发布就收到来自网友的大量“彩虹屁”。<\/p>
<\/p>
在Luya个人社交媒体“LUYA不吃卤鸭”发布的歌曲视频里,穿着一身汉服的Luya,用有着超高辨识度的“Luya专属音色”和知名乐团二十四伎乐合作演绎歌曲。虚拟歌手加上国风民乐的搭配,产生别样的火花,让网友直呼“yyds”。<\/p>
而此次Luya与二十四伎乐合作的歌曲《雾里》也是讯飞音乐出品的爆款歌曲之一。讯飞音乐相比于传统音乐公司“主推歌手”的方式,致力于打造爆款音乐,用“以歌带人”的方式完成歌曲和歌手的破圈。加上大热的歌曲《雾里》,讯飞音乐出品的歌曲总播放量突破400亿次,70多首歌曲播放过亿,QQ音乐、酷狗音乐及网易云音乐热歌榜上榜歌曲已达数百首。除了大量的出圈歌曲,讯飞音乐也有了自己的艺人经纪业务,签约了游鸿明、简弘亦等艺人,与此同时还培养了一批新生代歌手如姚六一、霄磊等。<\/p>
作为讯飞音乐推出的首位虚拟歌手,Luya虽然出道仅仅十几天,却已经演绎了三首歌曲,这样的成绩不禁让人对Luya的星途充满期待。此外,据讯飞音乐官方消息,讯飞音乐与AKB48一起打造的全新小分队Holidaygirls也发布了属于自己的音乐作品,期待Luya和Holidaygirls未来给大家带来更多的惊喜!我们一起拭目以待吧~<\/p>","type_name":"娱乐八卦"}]}
106 | """.trimIndent()
107 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sunhy/demo/command/StartActivityCommand.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.command
2 |
3 | import android.content.Intent
4 | import com.blankj.utilcode.util.GsonUtils
5 | import com.blankj.utilcode.util.LogUtils
6 | import com.blankj.utilcode.util.ProcessUtils
7 | import com.google.gson.JsonObject
8 | import com.sunhy.demo.activity.NewsListActivity
9 | import com.sunhy.demo.apt.annotations.JsBridgeCommand
10 | import com.sunhy.demo.base.BaseApplication
11 | import com.sunhy.demo.web.IBridgeInvokeWebProcess
12 | import com.sunhy.demo.web.bridge.IBridgeCommand
13 |
14 | @JsBridgeCommand(name = "startActivity")
15 | class StartActivityCommand : IBridgeCommand {
16 | override fun exec(params: JsonObject?, callback: IBridgeInvokeWebProcess?) {
17 | LogUtils.e("StartActivityCommand", "当前执行进程:${ProcessUtils.getCurrentProcessName()}")
18 | if (params != null && params["pageName"] != null) {
19 | when (params["pageName"].asString) {
20 | "TestActivity" -> {
21 | val intent = Intent(BaseApplication.getInstance(), NewsListActivity::class.java)
22 | intent.putExtra("data", "我是一个好人")
23 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
24 | BaseApplication.getInstance().startActivity(intent)
25 |
26 | val key = getCallbackKey(params)
27 | if (!key.isNullOrEmpty()) {
28 | val data = mapOf("message" to "startActivity success!!")
29 | callback?.handleBridgeCallback(key, GsonUtils.toJson(data))
30 | }
31 | }
32 | }
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
13 |
14 |
20 |
21 |
27 |
28 |
34 |
35 |
41 |
42 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_news_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
14 |
15 |
19 |
20 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_news.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
14 |
15 |
23 |
24 |
28 |
29 |
36 |
37 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RDSunhy/WebViewSimpleDemo/334b89a85eb2ee0e94833a4c50cc086bc85c7335/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RDSunhy/WebViewSimpleDemo/334b89a85eb2ee0e94833a4c50cc086bc85c7335/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RDSunhy/WebViewSimpleDemo/334b89a85eb2ee0e94833a4c50cc086bc85c7335/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RDSunhy/WebViewSimpleDemo/334b89a85eb2ee0e94833a4c50cc086bc85c7335/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RDSunhy/WebViewSimpleDemo/334b89a85eb2ee0e94833a4c50cc086bc85c7335/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RDSunhy/WebViewSimpleDemo/334b89a85eb2ee0e94833a4c50cc086bc85c7335/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RDSunhy/WebViewSimpleDemo/334b89a85eb2ee0e94833a4c50cc086bc85c7335/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RDSunhy/WebViewSimpleDemo/334b89a85eb2ee0e94833a4c50cc086bc85c7335/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RDSunhy/WebViewSimpleDemo/334b89a85eb2ee0e94833a4c50cc086bc85c7335/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RDSunhy/WebViewSimpleDemo/334b89a85eb2ee0e94833a4c50cc086bc85c7335/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | WebViewSimpleDemo
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/test/java/com/sunhy/demo/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/apt-annotations/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/apt-annotations/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'java-library'
3 | id 'org.jetbrains.kotlin.jvm'
4 | }
5 |
6 | java {
7 | sourceCompatibility = JavaVersion.VERSION_1_8
8 | targetCompatibility = JavaVersion.VERSION_1_8
9 | }
10 |
11 | dependencies {
12 | implementation "org.jetbrains.kotlin:kotlin-stdlib:1.6.10"
13 | }
--------------------------------------------------------------------------------
/apt-annotations/src/main/java/com/sunhy/demo/apt/annotations/JsBridgeCommand.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.apt.annotations
2 |
3 | @kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
4 | @Target(AnnotationTarget.CLASS)
5 | annotation class JsBridgeCommand(
6 | val name: String
7 | )
--------------------------------------------------------------------------------
/apt-processor/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/apt-processor/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'java'
3 | id 'java-library'
4 | id 'kotlin'
5 | id 'kotlin-kapt'
6 | }
7 |
8 | java {
9 | sourceCompatibility = JavaVersion.VERSION_1_8
10 | targetCompatibility = JavaVersion.VERSION_1_8
11 | }
12 |
13 | dependencies {
14 | implementation project(path: ':apt-annotations')
15 | implementation "org.jetbrains.kotlin:kotlin-stdlib:1.6.10"
16 | implementation "com.squareup:kotlinpoet:1.8.0"
17 | implementation "com.google.auto.service:auto-service:1.0"
18 | kapt "com.google.auto.service:auto-service:1.0"
19 | }
--------------------------------------------------------------------------------
/apt-processor/src/main/java/com/sunhy/demo/apt/processor/JsBridgeCommandProcessor.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.apt.processor
2 |
3 | import com.google.auto.service.AutoService
4 | import com.squareup.kotlinpoet.*
5 | import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
6 | import com.sunhy.demo.apt.annotations.JsBridgeCommand
7 | import java.io.IOException
8 | import javax.annotation.processing.*
9 | import javax.lang.model.SourceVersion
10 | import javax.lang.model.element.TypeElement
11 | import javax.lang.model.util.Elements
12 | import javax.lang.model.util.Types
13 |
14 | @AutoService(Processor::class)
15 | class JsBridgeCommandProcessor : AbstractProcessor() {
16 |
17 | companion object {
18 | private const val TAG = "JsBridgeCommandProcessor_APT"
19 | }
20 |
21 | private lateinit var mElementUtils: Elements
22 | private lateinit var mTypeUtils: Types
23 | private lateinit var mFilerUtils: Filer
24 | private lateinit var mMessagerUtils: Messager
25 |
26 | override fun init(processingEnv: ProcessingEnvironment) {
27 | super.init(processingEnv)
28 | mElementUtils = processingEnv.elementUtils
29 | mTypeUtils = processingEnv.typeUtils
30 | mFilerUtils = processingEnv.filer
31 | mMessagerUtils = processingEnv.messager
32 | }
33 |
34 | //指定处理的版本
35 | override fun getSupportedSourceVersion(): SourceVersion {
36 | return SourceVersion.latestSupported()
37 | }
38 |
39 | //给到需要处理的注解
40 | override fun getSupportedAnnotationTypes(): MutableSet {
41 | val types: LinkedHashSet = LinkedHashSet()
42 | getSupportedAnnotations().forEach { clazz: Class ->
43 | types.add(clazz.canonicalName)
44 | }
45 |
46 | return types
47 | }
48 |
49 | private fun getSupportedAnnotations(): Set> {
50 | val annotations: LinkedHashSet> = LinkedHashSet()
51 | // 需要解析的自定义注解
52 | annotations.add(JsBridgeCommand::class.java)
53 | return annotations
54 | }
55 |
56 | override fun process(
57 | p0: MutableSet?,
58 | roundEnvironment: RoundEnvironment?
59 | ): Boolean {
60 | println("$TAG start")
61 | // 要自动注册的 Command Map
62 | val commandMap = mutableMapOf()
63 | roundEnvironment?.getElementsAnnotatedWith(JsBridgeCommand::class.java)
64 | ?.forEach { element ->
65 | (element as? TypeElement)?.let { item ->
66 | // 全类名
67 | val clz = item.qualifiedName.toString()
68 | println("$TAG getClz = $clz")
69 |
70 | // 获取注解参数 name
71 | val name = item.getAnnotation(JsBridgeCommand::class.java).name
72 | println("$TAG getName = $name")
73 |
74 | // 放入map
75 | commandMap[name] = clz
76 | }
77 | }
78 |
79 | // 生成代码 路径
80 | val packageName = "com.sunhy.demo.apt"
81 |
82 | // 生成类方法
83 | val registerMethodBuilder = FunSpec.builder("autoRegist")
84 | .addComment("web jsbridge command auto load")
85 |
86 | // 定义局部变量
87 | val arrayMap = ClassName("android.util", "ArrayMap")
88 | val iBridgeCommand = ClassName("com.sunhy.demo.web.bridge", "IBridgeCommand")
89 | val arrayMapCommand = arrayMap.parameterizedBy(String::class.asTypeName(), iBridgeCommand)
90 | registerMethodBuilder.addStatement("val commandMap = %L()", arrayMapCommand)
91 |
92 | commandMap.forEach { (key, value) ->
93 | registerMethodBuilder.addStatement("commandMap[%S] = $value()", key)
94 | }
95 |
96 | // 方法返回类型
97 | registerMethodBuilder.returns(arrayMapCommand)
98 | registerMethodBuilder.addStatement("return commandMap")
99 |
100 | // 生成伴生对象
101 | val companionObject = TypeSpec.companionObjectBuilder()
102 | .addFunction(registerMethodBuilder.build())
103 | .build()
104 |
105 | // 生成类
106 | val clazzBuilder = TypeSpec.classBuilder("JsBridgeUtil")
107 | .addType(companionObject)
108 |
109 | //生成类文件
110 | val classFile = FileSpec.builder(packageName, "JsBridgeUtil")
111 | .addType(clazzBuilder.build())
112 | .build()
113 |
114 | //输出到文件
115 | try {
116 | mFilerUtils.let { filer -> classFile.writeTo(filer) }
117 | } catch (e: IOException) {
118 | println(e.message)
119 | }
120 |
121 | return false
122 |
123 | }
124 |
125 | }
--------------------------------------------------------------------------------
/base/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/base/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'org.jetbrains.kotlin.android'
4 | }
5 |
6 | android {
7 | compileSdk 32
8 |
9 | defaultConfig {
10 | minSdk 21
11 | targetSdk 32
12 |
13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
14 | consumerProguardFiles "consumer-rules.pro"
15 | }
16 |
17 | buildTypes {
18 | release {
19 | minifyEnabled false
20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 | compileOptions {
24 | sourceCompatibility JavaVersion.VERSION_1_8
25 | targetCompatibility JavaVersion.VERSION_1_8
26 | }
27 | kotlinOptions {
28 | jvmTarget = '1.8'
29 | }
30 | }
31 |
32 | dependencies {
33 |
34 | implementation 'androidx.core:core-ktx:1.7.0'
35 | implementation 'androidx.appcompat:appcompat:1.3.0'
36 | implementation 'com.google.android.material:material:1.4.0'
37 | testImplementation 'junit:junit:4.13.2'
38 | androidTestImplementation 'androidx.test.ext:junit:1.1.3'
39 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
40 |
41 | api 'com.blankj:utilcodex:1.30.6'
42 | api 'com.squareup.okhttp3:logging-interceptor:4.9.1'
43 | api 'com.squareup.okhttp3:okhttp:4.9.1'
44 | api 'com.squareup.retrofit2:retrofit:2.9.0'
45 | api 'com.squareup.retrofit2:converter-gson:2.9.0'
46 |
47 | }
--------------------------------------------------------------------------------
/base/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RDSunhy/WebViewSimpleDemo/334b89a85eb2ee0e94833a4c50cc086bc85c7335/base/consumer-rules.pro
--------------------------------------------------------------------------------
/base/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/base/src/androidTest/java/com/sunhy/demo/base/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.base
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.sunhy.demo.base.test", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/base/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/base/src/main/java/com/sunhy/demo/base/BaseApplication.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.base
2 |
3 | import android.app.Application
4 | import android.util.Log
5 | import kotlin.properties.Delegates
6 |
7 | open class BaseApplication: Application() {
8 |
9 | companion object {
10 | private var mApplication: BaseApplication by Delegates.notNull()
11 |
12 | fun getInstance(): BaseApplication{
13 | Log.e("BaseApplication", getProcessName())
14 | return mApplication
15 | }
16 | }
17 |
18 | override fun onCreate() {
19 | super.onCreate()
20 | mApplication = this
21 | }
22 |
23 | fun getPN(): String = getProcessName()
24 | }
--------------------------------------------------------------------------------
/base/src/main/java/com/sunhy/demo/base/entity/BaseEntity.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.base.entity
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | class BaseEntity {
6 |
7 | @SerializedName("list")
8 | var list: List = mutableListOf()
9 | }
--------------------------------------------------------------------------------
/base/src/main/java/com/sunhy/demo/base/entity/News.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.base.entity
2 |
3 | data class News(
4 | val art_author: String = "",
5 | val art_blurb: String = "",
6 | val art_class: String = "",
7 | val art_content: String = "",
8 | val art_from: String = "",
9 | val art_hits: Int = 0,
10 | val art_hits_day: Int = 0,
11 | val art_hits_month: Int = 0,
12 | val art_id: Int = 0,
13 | val art_letter: String = "",
14 | val art_name: String = "",
15 | val art_pic: String = "",
16 | val art_score: String = "",
17 | val art_status: Int = 0,
18 | val art_time: String = "",
19 | val art_up: Int = 0,
20 | val type_id: Int = 0,
21 | val type_id_1: Int = 0,
22 | val type_name: String = ""
23 | )
--------------------------------------------------------------------------------
/base/src/main/java/com/sunhy/demo/base/entity/UserInfo.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.base.entity
2 |
3 | data class UserInfo(
4 | val username: String,
5 | val token: String
6 | )
--------------------------------------------------------------------------------
/base/src/main/java/com/sunhy/demo/base/http/HttpUtils.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.base.http
2 |
3 | import okhttp3.OkHttpClient
4 | import okhttp3.logging.HttpLoggingInterceptor
5 | import retrofit2.Retrofit
6 | import retrofit2.converter.gson.GsonConverterFactory
7 | import java.util.concurrent.TimeUnit
8 |
9 | object HttpUtils {
10 |
11 | val apiService: WebApiService by lazy {
12 | Retrofit.Builder()
13 | .baseUrl("https://www.baidu.com")
14 | .addConverterFactory(GsonConverterFactory.create())
15 | .client(getOkHttpClient().build())
16 | .build()
17 | .create(WebApiService::class.java)
18 | }
19 |
20 | private fun getOkHttpClient(): OkHttpClient.Builder {
21 | return OkHttpClient.Builder().apply {
22 | connectTimeout(30, TimeUnit.SECONDS)
23 | readTimeout(30, TimeUnit.SECONDS)
24 | writeTimeout(30, TimeUnit.SECONDS)
25 | addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.HEADERS))
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/base/src/main/java/com/sunhy/demo/base/http/WebApiService.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.base.http
2 |
3 | import com.sunhy.demo.base.entity.BaseEntity
4 | import com.sunhy.demo.base.entity.News
5 | import okhttp3.ResponseBody
6 | import retrofit2.http.GET
7 | import retrofit2.http.HeaderMap
8 | import retrofit2.http.Query
9 | import retrofit2.http.Url
10 |
11 | interface WebApiService {
12 | @GET
13 | suspend fun download(
14 | @Url url: String = "",
15 | @HeaderMap header: Map
16 | ): ResponseBody
17 |
18 | @GET
19 | suspend fun getNewsList(@Url url: String): BaseEntity
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/base/src/main/java/com/sunhy/demo/base/utils/LoginUtils.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.base.utils
2 |
3 | import com.blankj.utilcode.util.GsonUtils
4 | import com.sunhy.demo.base.entity.UserInfo
5 |
6 | object LoginUtils {
7 |
8 | private var userInfo: UserInfo? = null
9 |
10 | fun getUserInfo(): String{
11 | return GsonUtils.toJson(userInfo)
12 | }
13 |
14 | // 模拟登陆
15 | fun login(){
16 | this.userInfo = UserInfo("孙先森@", "ASDJKLQJDKL12KLDKL3KLJ1234KL12KLLDA")
17 | }
18 | }
--------------------------------------------------------------------------------
/base/src/test/java/com/sunhy/demo/base/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.base
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | id 'com.android.application' version '7.2.2' apply false
4 | id 'com.android.library' version '7.2.2' apply false
5 | id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
6 | id 'org.jetbrains.kotlin.jvm' version '1.6.10' apply false
7 | }
8 |
9 | task clean(type: Delete) {
10 | delete rootProject.buildDir
11 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RDSunhy/WebViewSimpleDemo/334b89a85eb2ee0e94833a4c50cc086bc85c7335/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Sep 06 10:51:51 CST 2022
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/module_web/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/module_web/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'org.jetbrains.kotlin.android'
4 | id 'kotlin-kapt'
5 | }
6 |
7 | android {
8 | compileSdk 32
9 |
10 | defaultConfig {
11 | minSdk 21
12 | targetSdk 32
13 |
14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
15 | consumerProguardFiles "consumer-rules.pro"
16 | }
17 |
18 | buildTypes {
19 | release {
20 | minifyEnabled false
21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
22 | }
23 | }
24 | compileOptions {
25 | sourceCompatibility JavaVersion.VERSION_1_8
26 | targetCompatibility JavaVersion.VERSION_1_8
27 | }
28 | kotlinOptions {
29 | jvmTarget = '1.8'
30 | }
31 | dataBinding {
32 | enabled = true
33 | }
34 | kapt {
35 | generateStubs = true
36 | }
37 | }
38 |
39 | dependencies {
40 |
41 | implementation 'androidx.core:core-ktx:1.7.0'
42 | implementation 'androidx.appcompat:appcompat:1.3.0'
43 | implementation 'com.google.android.material:material:1.4.0'
44 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
45 | testImplementation 'junit:junit:4.13.2'
46 | androidTestImplementation 'androidx.test.ext:junit:1.1.3'
47 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
48 |
49 | implementation project(':base')
50 |
51 | implementation 'androidx.lifecycle:lifecycle-livedata-core-ktx:2.4.0'
52 | implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.0'
53 |
54 | implementation 'com.github.bumptech.glide:glide:4.12.0'
55 | implementation 'com.github.bumptech.glide:gifencoder-integration:4.12.0'
56 | annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
57 |
58 | api project(path: ':apt-annotations')
59 | kapt project(path: ':apt-processor')
60 | }
--------------------------------------------------------------------------------
/module_web/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RDSunhy/WebViewSimpleDemo/334b89a85eb2ee0e94833a4c50cc086bc85c7335/module_web/consumer-rules.pro
--------------------------------------------------------------------------------
/module_web/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/module_web/src/androidTest/java/com/sunhy/demo/web/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.web
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.sunhy.demo.web.test", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/module_web/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
10 |
11 |
15 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/module_web/src/main/aidl/com/sunhy/demo/web/IBridgeInvokeMainProcess.aidl:
--------------------------------------------------------------------------------
1 | // IBridgeInvokeMainProcess.aidl
2 | package com.sunhy.demo.web;
3 |
4 | // Declare any non-default types here with import statements
5 | import com.sunhy.demo.web.IBridgeInvokeWebProcess;
6 |
7 | interface IBridgeInvokeMainProcess {
8 | /**
9 | * Demonstrates some basic types that you can use as parameters
10 | * and return values in AIDL.
11 | */
12 | void handleBridgeInvoke(String command, String params, IBridgeInvokeWebProcess bridgeCallback);
13 | }
--------------------------------------------------------------------------------
/module_web/src/main/aidl/com/sunhy/demo/web/IBridgeInvokeWebProcess.aidl:
--------------------------------------------------------------------------------
1 | // IBridgeInvokeWebProcess.aidl
2 | package com.sunhy.demo.web;
3 |
4 | // Declare any non-default types here with import statements
5 |
6 | interface IBridgeInvokeWebProcess {
7 | /**
8 | * Demonstrates some basic types that you can use as parameters
9 | * and return values in AIDL.
10 | */
11 | void handleBridgeCallback(String callback, String params);
12 | }
--------------------------------------------------------------------------------
/module_web/src/main/assets/css/news.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/module_web/src/main/assets/js/bridge.js:
--------------------------------------------------------------------------------
1 | var jsBridge = {};
2 |
3 | // 系统判断
4 | jsBridge.os = {
5 | 'isAndroid': Boolean(navigator.userAgent.match(/android/ig)),
6 | 'isIOS': Boolean(navigator.userAgent.match(/iphone|ipod|iOS/ig))
7 | };
8 |
9 | // 回调 map
10 | jsBridge.mapCallbacks = {}
11 |
12 | // 回调处理
13 | jsBridge.postBridgeCallback = function(key, data){
14 | var obj = jsBridge.mapCallbacks[key];
15 | if(obj.callback){
16 | obj.callback(data);
17 | delete jsBridge.mapCallbacks[key];
18 | }else{
19 | console.log('jsBridge postBridgeCallback', 'callback not found: ' + key)
20 | }
21 | }
22 |
23 | // 发送命令
24 | jsBridge.sendCommand = function(command, params, callback) {
25 | var message = {
26 | 'command': command
27 | }
28 | if (params && typeof params === 'object') { // 支持传参
29 | message['params'] = params
30 | }
31 | if (callback && typeof callback === 'function') { // 支持回调
32 | var key = generateCallbackKey() // 生成回调key
33 | jsBridge.mapCallbacks[key] = { 'callback': callback }
34 | message['params'] ['bridgeCallback'] = key
35 | }
36 | console.log('jsBridge sendCommand message', JSON.stringify(message))
37 | console.log('jsBridge sendCommand mapCallbacks', JSON.stringify(jsBridge.mapCallbacks))
38 | if (jsBridge.os.isAndroid) { // android 桥接
39 | window.bridge.sendCommand(JSON.stringify(message))
40 | } else if (jsBridge.os.isIOS) { // ios 桥接
41 | window.webkit.messageHandlers.bridge.sendCommand(JSON.stringify(message))
42 | }
43 | }
44 |
45 | window.jsBridge = jsBridge;
46 |
47 | // 生成回调key
48 | function generateCallbackKey(){
49 | return "bridgeCallback_" + new Date().getTime() + "_" + randomCode();
50 | }
51 |
52 | // 随机码 防止并发重复
53 | function randomCode(){
54 | var code = ""
55 | for(var i = 0; i < 6; i++){
56 | code += Math.floor(Math.random() * 10)
57 | }
58 | return code;
59 | }
60 |
61 | console.log('bridge.js load success!!')
--------------------------------------------------------------------------------
/module_web/src/main/assets/js/jquery.lazyload.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Lazy Load - jQuery plugin for lazy loading images
3 | *
4 | * Copyright (c) 2007-2013 Mika Tuupola
5 | *
6 | * Licensed under the MIT license:
7 | * http://www.opensource.org/licenses/mit-license.php
8 | *
9 | * Project home:
10 | * http://www.appelsiini.net/projects/lazyload
11 | *
12 | * Version: 1.9.3
13 | *
14 | */
15 |
16 | (function($, window, document, undefined) {
17 | var $window = $(window);
18 |
19 | $.fn.lazyload = function(options) {
20 | var elements = this;
21 | var $container;
22 | var settings = {
23 | threshold : 0,
24 | failure_limit : 0,
25 | event : "scroll",
26 | effect : "show",
27 | container : window,
28 | data_attribute : "original",
29 | skip_invisible : true,
30 | appear : null,
31 | load : null,
32 | placeholder : "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC"
33 | };
34 |
35 | function update() {
36 | var counter = 0;
37 |
38 | elements.each(function() {
39 | var $this = $(this);
40 | if (settings.skip_invisible && !$this.is(":visible")) {
41 | return;
42 | }
43 | if ($.abovethetop(this, settings) ||
44 | $.leftofbegin(this, settings)) {
45 | /* Nothing. */
46 | } else if (!$.belowthefold(this, settings) &&
47 | !$.rightoffold(this, settings)) {
48 | $this.trigger("appear");
49 | /* if we found an image we'll load, reset the counter */
50 | counter = 0;
51 | } else {
52 | if (++counter > settings.failure_limit) {
53 | return false;
54 | }
55 | }
56 | });
57 |
58 | }
59 |
60 | if(options) {
61 | /* Maintain BC for a couple of versions. */
62 | if (undefined !== options.failurelimit) {
63 | options.failure_limit = options.failurelimit;
64 | delete options.failurelimit;
65 | }
66 | if (undefined !== options.effectspeed) {
67 | options.effect_speed = options.effectspeed;
68 | delete options.effectspeed;
69 | }
70 |
71 | $.extend(settings, options);
72 | }
73 |
74 | /* Cache container as jQuery as object. */
75 | $container = (settings.container === undefined ||
76 | settings.container === window) ? $window : $(settings.container);
77 |
78 | /* Fire one scroll event per scroll. Not one scroll event per image. */
79 | if (0 === settings.event.indexOf("scroll")) {
80 | $container.bind(settings.event, function() {
81 | return update();
82 | });
83 | }
84 |
85 | this.each(function() {
86 | var self = this;
87 | var $self = $(self);
88 |
89 | self.loaded = false;
90 |
91 | /* If no src attribute given use data:uri. */
92 | if ($self.attr("src") === undefined || $self.attr("src") === false) {
93 | if ($self.is("img")) {
94 | $self.attr("src", settings.placeholder);
95 | }
96 | }
97 |
98 | /* When appear is triggered load original image. */
99 | $self.one("appear", function() {
100 | if (!this.loaded) {
101 | if (settings.appear) {
102 | var elements_left = elements.length;
103 | settings.appear.call(self, elements_left, settings);
104 | }
105 | $(" ")
106 | .bind("load", function() {
107 |
108 | var original = $self.attr("data-" + settings.data_attribute);
109 | $self.hide();
110 | if ($self.is("img")) {
111 | $self.attr("src", original);
112 | } else {
113 | $self.css("background-image", "url('" + original + "')");
114 | }
115 | $self[settings.effect](settings.effect_speed);
116 |
117 | self.loaded = true;
118 |
119 | /* Remove image from array so it is not looped next time. */
120 | var temp = $.grep(elements, function(element) {
121 | return !element.loaded;
122 | });
123 | elements = $(temp);
124 |
125 | if (settings.load) {
126 | var elements_left = elements.length;
127 | settings.load.call(self, elements_left, settings);
128 | }
129 | })
130 | .attr("src", $self.attr("data-" + settings.data_attribute));
131 | }
132 | });
133 |
134 | /* When wanted event is triggered load original image */
135 | /* by triggering appear. */
136 | if (0 !== settings.event.indexOf("scroll")) {
137 | $self.bind(settings.event, function() {
138 | if (!self.loaded) {
139 | $self.trigger("appear");
140 | }
141 | });
142 | }
143 | });
144 |
145 | /* Check if something appears when window is resized. */
146 | $window.bind("resize", function() {
147 | update();
148 | });
149 |
150 | /* With IOS5 force loading images when navigating with back button. */
151 | /* Non optimal workaround. */
152 | if ((/(?:iphone|ipod|ipad).*os 5/gi).test(navigator.appVersion)) {
153 | $window.bind("pageshow", function(event) {
154 | if (event.originalEvent && event.originalEvent.persisted) {
155 | elements.each(function() {
156 | $(this).trigger("appear");
157 | });
158 | }
159 | });
160 | }
161 |
162 | /* Force initial check if images should appear. */
163 | $(document).ready(function() {
164 | update();
165 | });
166 |
167 | return this;
168 | };
169 |
170 | /* Convenience methods in jQuery namespace. */
171 | /* Use as $.belowthefold(element, {threshold : 100, container : window}) */
172 |
173 | $.belowthefold = function(element, settings) {
174 | var fold;
175 |
176 | if (settings.container === undefined || settings.container === window) {
177 | fold = (window.innerHeight ? window.innerHeight : $window.height()) + $window.scrollTop();
178 | } else {
179 | fold = $(settings.container).offset().top + $(settings.container).height();
180 | }
181 |
182 | return fold <= $(element).offset().top - settings.threshold;
183 | };
184 |
185 | $.rightoffold = function(element, settings) {
186 | var fold;
187 |
188 | if (settings.container === undefined || settings.container === window) {
189 | fold = $window.width() + $window.scrollLeft();
190 | } else {
191 | fold = $(settings.container).offset().left + $(settings.container).width();
192 | }
193 |
194 | return fold <= $(element).offset().left - settings.threshold;
195 | };
196 |
197 | $.abovethetop = function(element, settings) {
198 | var fold;
199 |
200 | if (settings.container === undefined || settings.container === window) {
201 | fold = $window.scrollTop();
202 | } else {
203 | fold = $(settings.container).offset().top;
204 | }
205 |
206 | return fold >= $(element).offset().top + settings.threshold + $(element).height();
207 | };
208 |
209 | $.leftofbegin = function(element, settings) {
210 | var fold;
211 |
212 | if (settings.container === undefined || settings.container === window) {
213 | fold = $window.scrollLeft();
214 | } else {
215 | fold = $(settings.container).offset().left;
216 | }
217 |
218 | return fold >= $(element).offset().left + settings.threshold + $(element).width();
219 | };
220 |
221 | $.inviewport = function(element, settings) {
222 | return !$.rightoffold(element, settings) && !$.leftofbegin(element, settings) &&
223 | !$.belowthefold(element, settings) && !$.abovethetop(element, settings);
224 | };
225 |
226 | /* Custom selectors for your convenience. */
227 | /* Use as $("img:below-the-fold").something() or */
228 | /* $("img").filter(":below-the-fold").something() which is faster */
229 |
230 | $.extend($.expr[":"], {
231 | "below-the-fold" : function(a) { return $.belowthefold(a, {threshold : 0}); },
232 | "above-the-top" : function(a) { return !$.belowthefold(a, {threshold : 0}); },
233 | "right-of-screen": function(a) { return $.rightoffold(a, {threshold : 0}); },
234 | "left-of-screen" : function(a) { return !$.rightoffold(a, {threshold : 0}); },
235 | "in-viewport" : function(a) { return $.inviewport(a, {threshold : 0}); },
236 | /* Maintain BC for couple of versions. */
237 | "above-the-fold" : function(a) { return !$.belowthefold(a, {threshold : 0}); },
238 | "right-of-fold" : function(a) { return $.rightoffold(a, {threshold : 0}); },
239 | "left-of-fold" : function(a) { return !$.rightoffold(a, {threshold : 0}); }
240 | });
241 |
242 | })(jQuery, window, document);
243 |
--------------------------------------------------------------------------------
/module_web/src/main/assets/template_news.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
57 |
58 |
--------------------------------------------------------------------------------
/module_web/src/main/assets/test_command_bridge.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
17 |
18 |
19 |
20 |
21 |
调用原生 App 展示 Toast
22 |
调用原生 App 展示 Toast 并且回调
23 |
启动主进程 Activity
24 |
获取主进程 UserInfo
25 |
暂无信息
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/module_web/src/main/assets/test_default_bridge.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
16 |
17 |
18 |
19 |
20 | 调用原生 App 展示 Toast
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/module_web/src/main/java/com/sunhy/demo/web/BaseWebView.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.web
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.graphics.Bitmap
6 | import android.graphics.Canvas
7 | import android.util.AttributeSet
8 | import android.util.Log
9 | import android.view.ViewGroup
10 | import android.webkit.JavascriptInterface
11 | import android.webkit.WebView
12 | import android.webkit.WebViewClient
13 | import android.widget.Toast
14 | import androidx.lifecycle.Lifecycle
15 | import androidx.lifecycle.LifecycleEventObserver
16 | import androidx.lifecycle.LifecycleOwner
17 | import com.blankj.utilcode.util.GsonUtils
18 | import com.blankj.utilcode.util.LogUtils
19 | import com.google.gson.JsonSyntaxException
20 | import com.sunhy.demo.web.bridge.JsBridgeInvokeDispatcher
21 | import com.sunhy.demo.web.bridge.JsBridgeMessage
22 | import com.sunhy.demo.web.core.BaseWebChromeClient
23 | import com.sunhy.demo.web.core.BaseWebViewClient
24 | import com.sunhy.demo.web.utils.WebUtil
25 |
26 | open class BaseWebView @JvmOverloads constructor(
27 | context: Context, attrs: AttributeSet? = null
28 | ) : WebView(context, attrs), LifecycleEventObserver {
29 |
30 | private val TAG = "BaseWebView"
31 |
32 | init {
33 | // WebView 调试模式开关
34 | setWebContentsDebuggingEnabled(true)
35 | // 不显示滚动条
36 | isVerticalScrollBarEnabled = false
37 | isHorizontalScrollBarEnabled = false
38 | // 初始化设置
39 | WebUtil.defaultSettings(context, this)
40 |
41 | JsBridgeInvokeDispatcher.getInstance().bindService()
42 | addJavascriptInterface(this, "bridge")
43 | }
44 |
45 | /**
46 | * 桥接
47 | */
48 | @JavascriptInterface
49 | fun sendCommand(json: String?) {
50 | LogUtils.d(TAG, "sendCommand()", "json: $json")
51 | if (json.isNullOrEmpty()) {
52 | LogUtils.e(TAG, "sendCommand json is null or empty")
53 | return
54 | }
55 | try {
56 | val message = GsonUtils.fromJson(json, JsBridgeMessage::class.java)
57 | JsBridgeInvokeDispatcher.getInstance().sendCommand(this, message)
58 | } catch (e: JsonSyntaxException) {
59 | LogUtils.e(TAG, "sendCommand() catch. json: $json", e.message)
60 | e.printStackTrace()
61 | }
62 | }
63 |
64 | /**
65 | * 桥接回调
66 | */
67 | fun postBridgeCallback(key: String?, data: String?) {
68 | post {
69 | evaluateJavascript("javascript:window.jsBridge.postBridgeCallback(`$key`, `$data`)") {}
70 | }
71 | }
72 |
73 | @JavascriptInterface
74 | fun showToast(message: String){
75 | Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
76 | }
77 |
78 | /**
79 | * 获取当前url
80 | */
81 | override fun getUrl(): String? {
82 | return super.getOriginalUrl() ?: return super.getUrl()
83 | }
84 |
85 | override fun canGoBack(): Boolean {
86 | val backForwardList = copyBackForwardList()
87 | val currentIndex = backForwardList.currentIndex - 1
88 | if (currentIndex >= 0) {
89 | val item = backForwardList.getItemAtIndex(currentIndex)
90 | if (item?.url == "about:blank") {
91 | return false
92 | }
93 | }
94 | return super.canGoBack()
95 | }
96 |
97 | /**
98 | * 设置 WebView 生命管控(自动回调生命周期方法)
99 | */
100 | fun setLifecycleOwner(owner: LifecycleOwner) {
101 | owner.lifecycle.addObserver(this)
102 | }
103 |
104 | /**
105 | * 生命周期回调
106 | */
107 | override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
108 |
109 | when (event) {
110 | Lifecycle.Event.ON_RESUME -> {
111 | Log.d(TAG, "onStateChanged -> ON_RESUME")
112 | onResume()
113 | }
114 | Lifecycle.Event.ON_STOP -> {
115 | Log.d(TAG, "onStateChanged -> ON_STOP")
116 | onPause()
117 | }
118 | Lifecycle.Event.ON_DESTROY -> {
119 | Log.d(TAG, "onStateChanged -> ON_DESTROY")
120 | source.lifecycle.removeObserver(this)
121 | onDestroy()
122 | }
123 | }
124 | }
125 |
126 | /**
127 | * 生命周期 onResume()
128 | */
129 | @SuppressLint("SetJavaScriptEnabled")
130 | override fun onResume() {
131 | super.onResume()
132 | settings.javaScriptEnabled = true
133 | }
134 |
135 | /**
136 | * 生命周期 onPause()
137 | */
138 | override fun onPause() {
139 | super.onPause()
140 | }
141 |
142 | /**
143 | * 生命周期 onDestroy()
144 | * 父类没有 需要自己写
145 | */
146 | open fun onDestroy() {
147 | settings.javaScriptEnabled = false
148 | removeBlankMonitorRunnable()
149 | WebViewPool.getInstance().recycle(this)
150 | }
151 |
152 | /**
153 | * 释放资源操作
154 | */
155 | open fun release() {
156 | (parent as ViewGroup?)?.removeView(this)
157 | removeAllViews()
158 | stopLoading()
159 | setCustomWebViewClient(null)
160 | setCustomWebChromeClient(null)
161 | loadUrl("about:blank")
162 | clearHistory()
163 | }
164 |
165 | fun setCustomWebViewClient(client: BaseWebViewClient?) {
166 | if (client == null) {
167 | super.setWebViewClient(WebViewClient())
168 | } else {
169 | super.setWebViewClient(client)
170 | }
171 | }
172 |
173 | fun setCustomWebChromeClient(client: BaseWebChromeClient?) {
174 | super.setWebChromeClient(client)
175 | }
176 |
177 | interface BlankMonitorCallback {
178 | fun onBlank()
179 | }
180 |
181 | private var mBlankMonitorCallback: BlankMonitorCallback? = null
182 |
183 | fun setBlankMonitorCallback(callback: BlankMonitorCallback){
184 | this.mBlankMonitorCallback = callback
185 | }
186 |
187 | private val mBlankMonitorRunnable by lazy { BlankMonitorRunnable() }
188 |
189 | /**
190 | * 调用后
191 | * 5s 后开始执行白屏检测任务 时间可以适当修改
192 | */
193 | fun postBlankMonitorRunnable() {
194 | Log.d(TAG, "白屏检测任务 5s 后执行")
195 | removeCallbacks(mBlankMonitorRunnable)
196 | postDelayed(mBlankMonitorRunnable, 5000)
197 | }
198 |
199 | /**
200 | * 取消白屏检测任务
201 | */
202 | fun removeBlankMonitorRunnable() {
203 | Log.d(TAG, "白屏检测任务取消执行")
204 | removeCallbacks(mBlankMonitorRunnable)
205 | }
206 |
207 | inner class BlankMonitorRunnable : Runnable {
208 |
209 | private val TAG = "BlankMonitorRunnable"
210 |
211 | override fun run() {
212 | val task = Thread {
213 | // 根据宽高的 1/6 创建 bitmap
214 | val dstWidth = measuredWidth / 6
215 | val dstHeight = measuredHeight / 6
216 | val snapshot = Bitmap.createBitmap(dstWidth, dstHeight, Bitmap.Config.ARGB_8888)
217 | // 绘制 view 到 bitmap
218 | val canvas = Canvas(snapshot)
219 | draw(canvas)
220 |
221 | // 像素点总数
222 | val pixelCount = (snapshot.width * snapshot.height).toFloat()
223 | var whitePixelCount = 0 // 白色像素点计数
224 | var otherPixelCount = 0 // 其他颜色像素点计数
225 | // 遍历 bitmap 像素点
226 | for (x in 0 until snapshot.width) {
227 | for (y in 0 until snapshot.height) {
228 | // 计数 其实记录一种就可以
229 | if (snapshot.getPixel(x, y) == -1) {
230 | whitePixelCount++
231 | }else{
232 | otherPixelCount++
233 | }
234 | }
235 | }
236 |
237 | Log.d(TAG, "白屏检测任务:像素点总数为 $pixelCount")
238 | Log.d(TAG, "白屏检测任务:白色像素点计数为 $whitePixelCount")
239 | Log.d(TAG, "白屏检测任务:其他颜色像素点计数为 $otherPixelCount")
240 |
241 | // 回收 bitmap
242 | snapshot.recycle()
243 |
244 | if (whitePixelCount == 0) {
245 | return@Thread
246 | }
247 |
248 | // 计算白色像素点占比 (计算其他颜色也一样)
249 | val percentage: Float = whitePixelCount / pixelCount * 100
250 | // 如果超过阈值 触发白屏提醒
251 | if (percentage > 95) {
252 | post {
253 | mBlankMonitorCallback?.onBlank()
254 | }
255 | }
256 | }
257 | task.start()
258 | }
259 | }
260 | }
--------------------------------------------------------------------------------
/module_web/src/main/java/com/sunhy/demo/web/TemplateWebView.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.web
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import android.view.ViewGroup
6 |
7 | class TemplateWebView(context: Context, attrs: AttributeSet? = null) : BaseWebView(context, attrs) {
8 |
9 | override fun release() {
10 | evaluateJavascript("javascript:clearData()") {}
11 | (parent as ViewGroup?)?.removeView(this)
12 | removeAllViews()
13 | }
14 |
15 | override fun onDestroy() {
16 | TemplateWebViewPool.getInstance().recycle(this)
17 | }
18 | }
--------------------------------------------------------------------------------
/module_web/src/main/java/com/sunhy/demo/web/TemplateWebViewPool.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.web
2 |
3 | import android.content.Context
4 | import android.content.MutableContextWrapper
5 | import android.util.Log
6 | import com.sunhy.demo.web.core.BaseWebChromeClient
7 | import com.sunhy.demo.web.core.BaseWebViewClient
8 | import java.util.*
9 |
10 | class TemplateWebViewPool private constructor() {
11 |
12 | companion object {
13 |
14 | private const val TAG = "TemplateWebViewPool"
15 |
16 | @Volatile
17 | private var instance: TemplateWebViewPool? = null
18 |
19 | fun getInstance(): TemplateWebViewPool {
20 | return instance ?: synchronized(this) {
21 | instance ?: TemplateWebViewPool().also { instance = it }
22 | }
23 | }
24 | }
25 |
26 | private val sPool = Stack()
27 | private val lock = byteArrayOf()
28 | private var maxSize = 1
29 |
30 | /**
31 | * 设置 webview 池容量
32 | */
33 | fun setMaxPoolSize(size: Int) {
34 | synchronized(lock) { maxSize = size }
35 | }
36 |
37 | /**
38 | * 初始化webview 放在list中
39 | */
40 | fun init(context: Context, initSize: Int = maxSize) {
41 | for (i in 0 until initSize) {
42 | val view = TemplateWebView(MutableContextWrapper(context))
43 | // 初始化时就加载模板
44 | view.loadUrl("file:///android_asset/template_news.html")
45 | sPool.push(view)
46 | }
47 | }
48 |
49 | /**
50 | * 获取webview
51 | */
52 | fun getWebView(context: Context): TemplateWebView {
53 | synchronized(lock) {
54 | val webView: TemplateWebView
55 | if (sPool.size > 0) {
56 | webView = sPool.pop()
57 | Log.d(TAG, "getWebView from pool")
58 | } else {
59 | webView = TemplateWebView(MutableContextWrapper(context))
60 | // 初始化时就加载模板
61 | webView.loadUrl("file:///android_asset/template_news.html")
62 | Log.d(TAG, "getWebView from create")
63 | }
64 |
65 | val contextWrapper = webView.context as MutableContextWrapper
66 | contextWrapper.baseContext = context
67 |
68 | // 默认设置
69 | webView.webChromeClient = BaseWebChromeClient()
70 | webView.webViewClient = BaseWebViewClient()
71 | return webView
72 | }
73 | }
74 |
75 | /**
76 | * 回收 WebView
77 | */
78 | fun recycle(webView: TemplateWebView) {
79 | // 释放资源
80 | webView.release()
81 |
82 | // 根据池容量判断是否销毁 【也可以增加其他条件 如手机低内存等等】
83 | val contextWrapper = webView.context as MutableContextWrapper
84 | contextWrapper.baseContext = webView.context.applicationContext
85 | synchronized(lock) {
86 | if (sPool.size < maxSize) {
87 | sPool.push(webView)
88 | } else {
89 | webView.destroy()
90 | }
91 | }
92 | }
93 | }
--------------------------------------------------------------------------------
/module_web/src/main/java/com/sunhy/demo/web/WebViewPool.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.web
2 |
3 | import android.content.Context
4 | import android.content.MutableContextWrapper
5 | import android.util.Log
6 | import com.sunhy.demo.web.core.BaseWebChromeClient
7 | import com.sunhy.demo.web.core.BaseWebViewClient
8 | import java.util.*
9 |
10 | class WebViewPool private constructor() {
11 |
12 | companion object {
13 |
14 | private const val TAG = "WebViewPool"
15 |
16 | @Volatile
17 | private var instance: WebViewPool? = null
18 |
19 | fun getInstance(): WebViewPool {
20 | return instance ?: synchronized(this) {
21 | instance ?: WebViewPool().also { instance = it }
22 | }
23 | }
24 | }
25 |
26 | private val sPool = Stack()
27 | private val lock = byteArrayOf()
28 | private var maxSize = 1
29 |
30 | /**
31 | * 设置 webview 池容量
32 | */
33 | fun setMaxPoolSize(size: Int) {
34 | synchronized(lock) { maxSize = size }
35 | }
36 |
37 | /**
38 | * 初始化webview 放在list中
39 | */
40 | fun init(context: Context, initSize: Int = maxSize) {
41 | for (i in 0 until initSize) {
42 | val view = BaseWebView(MutableContextWrapper(context))
43 | view.webChromeClient = BaseWebChromeClient()
44 | view.webViewClient = BaseWebViewClient()
45 | sPool.push(view)
46 | }
47 | }
48 |
49 | /**
50 | * 获取webview
51 | */
52 | fun getWebView(context: Context): BaseWebView {
53 | synchronized(lock) {
54 | val webView: BaseWebView
55 | if (sPool.size > 0) {
56 | webView = sPool.pop()
57 | Log.d(TAG, "getWebView from pool")
58 | } else {
59 | webView = BaseWebView(MutableContextWrapper(context))
60 | Log.d(TAG, "getWebView from create")
61 | }
62 |
63 | val contextWrapper = webView.context as MutableContextWrapper
64 | contextWrapper.baseContext = context
65 | return webView
66 | }
67 | }
68 |
69 | /**
70 | * 回收 WebView
71 | */
72 | fun recycle(webView: BaseWebView) {
73 | // 释放资源
74 | webView.release()
75 |
76 | // 根据池容量判断是否销毁 【也可以增加其他条件 如手机低内存等等】
77 | val contextWrapper = webView.context as MutableContextWrapper
78 | contextWrapper.baseContext = webView.context.applicationContext
79 | synchronized(lock) {
80 | if (sPool.size < maxSize) {
81 | sPool.push(webView)
82 | } else {
83 | webView.destroy()
84 | }
85 | }
86 | }
87 | }
--------------------------------------------------------------------------------
/module_web/src/main/java/com/sunhy/demo/web/activity/NewsDetailActivity.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.web.activity
2 |
3 | import android.widget.RelativeLayout
4 | import com.sunhy.demo.web.R
5 | import com.sunhy.demo.web.core.BaseTemplateWebActivity
6 | import com.sunhy.demo.web.core.BaseWebActivity
7 | import com.sunhy.demo.web.databinding.ActivityNewsDetailBinding
8 |
9 | class NewsDetailActivity : BaseTemplateWebActivity() {
10 |
11 | override fun getLayoutId() = R.layout.activity_news_detail
12 |
13 | override fun initView() {
14 |
15 | mBinding.webContainer.addView(
16 | mWebView,
17 | RelativeLayout.LayoutParams(
18 | RelativeLayout.LayoutParams.MATCH_PARENT,
19 | RelativeLayout.LayoutParams.MATCH_PARENT
20 | )
21 | )
22 |
23 | mWebView.setLifecycleOwner(this)
24 |
25 | val title = intent.getStringExtra("title")?:"获取title失败"
26 | val tag = intent.getStringExtra("tag")?:""
27 | val content = intent.getStringExtra("content")?:""
28 | mWebView.evaluateJavascript("javascript:setNewsData(`$title`, `$tag`, `$content`)") {}
29 | }
30 |
31 | override fun initData() {
32 | }
33 | }
--------------------------------------------------------------------------------
/module_web/src/main/java/com/sunhy/demo/web/activity/WebActivity.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.web.activity
2 |
3 | import android.util.Log
4 | import android.widget.RelativeLayout
5 | import androidx.appcompat.app.AlertDialog
6 | import com.sunhy.demo.base.utils.LoginUtils
7 | import com.sunhy.demo.web.BaseWebView
8 | import com.sunhy.demo.web.R
9 | import com.sunhy.demo.web.core.BaseWebActivity
10 | import com.sunhy.demo.web.databinding.ActivityWebBinding
11 |
12 | class WebActivity : BaseWebActivity() {
13 |
14 | override fun getLayoutId(): Int = R.layout.activity_web
15 |
16 | override fun initView() {
17 | mBinding.webContainer.addView(
18 | mWebView,
19 | RelativeLayout.LayoutParams(
20 | RelativeLayout.LayoutParams.MATCH_PARENT,
21 | RelativeLayout.LayoutParams.MATCH_PARENT
22 | )
23 | )
24 |
25 | mWebView.setLifecycleOwner(this)
26 | mWebView.setBlankMonitorCallback(object : BaseWebView.BlankMonitorCallback {
27 | override fun onBlank() {
28 | AlertDialog.Builder(this@WebActivity)
29 | .setTitle("提示")
30 | .setMessage("检测到页面发生异常,是否重新加载?")
31 | .setPositiveButton("重新加载") { dialog, _ ->
32 | dialog.dismiss()
33 | mWebView.reload()
34 | }
35 | .setNegativeButton("返回上一页") { dialog, _ ->
36 | dialog.dismiss()
37 | onBackPressed()
38 | }
39 | .create()
40 | .show()
41 | }
42 | })
43 | }
44 |
45 | override fun initData() {
46 | Log.e("web进程获取 UserInfo", LoginUtils.getUserInfo())
47 | val url = intent.getStringExtra("url")?:"https://www.baidu.com"
48 | mWebView.loadUrl(url)
49 | }
50 |
51 | }
--------------------------------------------------------------------------------
/module_web/src/main/java/com/sunhy/demo/web/bridge/BridgeCommandHandler.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.web.bridge
2 |
3 | import android.os.Handler
4 | import android.os.Looper
5 | import android.util.ArrayMap
6 | import com.blankj.utilcode.util.GsonUtils
7 | import com.blankj.utilcode.util.LogUtils
8 | import com.blankj.utilcode.util.ProcessUtils
9 | import com.google.gson.JsonObject
10 | import com.sunhy.demo.apt.JsBridgeUtil
11 | import com.sunhy.demo.web.IBridgeInvokeMainProcess
12 | import com.sunhy.demo.web.IBridgeInvokeWebProcess
13 |
14 | class BridgeCommandHandler: IBridgeInvokeMainProcess.Stub() {
15 |
16 | companion object {
17 | private const val TAG = "BridgeCommandHandler"
18 |
19 | @Volatile
20 | private var sInstance: BridgeCommandHandler? = null
21 |
22 | fun getInstance(): BridgeCommandHandler {
23 | if (sInstance == null) {
24 | synchronized(this) {
25 | if (sInstance == null) {
26 | LogUtils.e(
27 | "BridgeCommandHandler",
28 | "初始化 当前进程:${ProcessUtils.getCurrentProcessName()}"
29 | )
30 | sInstance = BridgeCommandHandler()
31 | }
32 | }
33 | }
34 | return sInstance!!
35 | }
36 | }
37 |
38 | // 用于切线程
39 | private val mHandle = Handler(Looper.getMainLooper())
40 |
41 | // 命令注册 暂时用 map 手动添加 后续修改
42 | // private val mCommandMap by lazy {
43 | // val map = ArrayMap().apply {
44 | // put("showToast", ToastCommand())
45 | // }
46 | // return@lazy map
47 | // }
48 | private val mCommandMap: ArrayMap by lazy { JsBridgeUtil.autoRegist() }
49 |
50 | // 暴露给外部方法 分发调用
51 | override fun handleBridgeInvoke(command: String?, params: String?, bridgeCallback: IBridgeInvokeWebProcess?) {
52 | LogUtils.e(TAG, "handleBridgeInvoke()", "当前进程:${ProcessUtils.getCurrentProcessName()}")
53 | // map 中存在命令 则执行
54 | if (mCommandMap.contains(command)) {
55 | mHandle.post { // 切换到主线程 获取命令 执行
56 | mCommandMap[command]!!.exec(parseParams(params), bridgeCallback)
57 | }
58 | } else {
59 | LogUtils.e(TAG, "handleBridgeInvoke()", "command[${command}] is not register!")
60 | }
61 | }
62 |
63 | private fun parseParams(params: String?): JsonObject? {
64 | if (params.isNullOrEmpty()) {
65 | return null
66 | }
67 | return GsonUtils.fromJson(params, JsonObject::class.java)
68 | }
69 | }
--------------------------------------------------------------------------------
/module_web/src/main/java/com/sunhy/demo/web/bridge/IBridgeCallbackInterface.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.web.bridge
2 |
3 | @Deprecated("已经改写为AIDL文件 IBridgeInvokeWebProcess.aidl")
4 | interface IBridgeCallbackInterface {
5 | /**
6 | * callback 回调key
7 | * params 参数 json 格式
8 | */
9 | fun handleBridgeCallback(callback: String, params: String)
10 | }
--------------------------------------------------------------------------------
/module_web/src/main/java/com/sunhy/demo/web/bridge/IBridgeCommand.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.web.bridge
2 |
3 | import com.google.gson.JsonObject
4 | import com.sunhy.demo.web.IBridgeInvokeWebProcess
5 |
6 | interface IBridgeCommand {
7 | fun exec(params: JsonObject?, callback: IBridgeInvokeWebProcess?)
8 |
9 | fun getCallbackKey(params: JsonObject?): String? {
10 | if (params == null) {
11 | return null
12 | }
13 | if (params["bridgeCallback"] == null) {
14 | return null
15 | }
16 | return params["bridgeCallback"].asString
17 | }
18 | }
--------------------------------------------------------------------------------
/module_web/src/main/java/com/sunhy/demo/web/bridge/JsBridgeInvokeDispatcher.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.web.bridge
2 |
3 | import android.content.ComponentName
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.content.ServiceConnection
7 | import android.os.IBinder
8 | import com.blankj.utilcode.util.GsonUtils
9 | import com.blankj.utilcode.util.LogUtils
10 | import com.blankj.utilcode.util.ProcessUtils
11 | import com.google.gson.JsonObject
12 | import com.sunhy.demo.base.BaseApplication
13 | import com.sunhy.demo.web.BaseWebView
14 | import com.sunhy.demo.web.IBridgeInvokeMainProcess
15 | import com.sunhy.demo.web.IBridgeInvokeWebProcess
16 | import com.sunhy.demo.web.service.BridgeCommandService
17 |
18 | class JsBridgeInvokeDispatcher : ServiceConnection{
19 | companion object {
20 | private const val TAG = "JsBridgeInvokeDispatcher"
21 |
22 | @Volatile
23 | private var sInstance: JsBridgeInvokeDispatcher? = null
24 |
25 | fun getInstance(): JsBridgeInvokeDispatcher {
26 | if (sInstance == null) {
27 | synchronized(this) {
28 | if (sInstance == null) {
29 | sInstance = JsBridgeInvokeDispatcher()
30 | }
31 | }
32 | }
33 | return sInstance!!
34 | }
35 | }
36 |
37 | private var iBridgeInvokeMainProcess: IBridgeInvokeMainProcess? = null
38 |
39 | fun bindService() {
40 | LogUtils.d(TAG, "bindService()")
41 | if (iBridgeInvokeMainProcess == null) {
42 | val i = Intent(BaseApplication.getInstance(), BridgeCommandService::class.java)
43 | BaseApplication.getInstance().bindService(i, this, Context.BIND_AUTO_CREATE)
44 | }
45 | }
46 |
47 | fun unbindService() {
48 | LogUtils.d(TAG, "unbindService()")
49 | iBridgeInvokeMainProcess = null
50 | BaseApplication.getInstance().unbindService(this)
51 | }
52 |
53 | override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
54 | iBridgeInvokeMainProcess = IBridgeInvokeMainProcess.Stub.asInterface(service)
55 | }
56 |
57 | override fun onServiceDisconnected(name: ComponentName?) {
58 | iBridgeInvokeMainProcess = null
59 | }
60 |
61 | fun sendCommand(view: BaseWebView, message: JsBridgeMessage?) {
62 | LogUtils.d(TAG, "sendCommand()", "message: $message")
63 | if (checkMessage(message)) {
64 | // 校验命令通过后 执行命令
65 | excuteCommand(view, message)
66 | }
67 | }
68 |
69 | // 校验命令、参数 合法性
70 | private fun checkMessage(message: JsBridgeMessage?): Boolean {
71 | if (message == null) {
72 | LogUtils.e(TAG, "send()", "jsBridgeParam is null")
73 | return false
74 | }
75 | if (message.command.isNullOrEmpty()) {
76 | LogUtils.e(TAG, "send()", "jsBridgeParam.commend is null")
77 | return false
78 | }
79 | return true
80 | }
81 |
82 | //执行命令
83 | private fun excuteCommand(view: BaseWebView, message: JsBridgeMessage?) {
84 | val callback = object : IBridgeInvokeWebProcess.Stub() {
85 | override fun handleBridgeCallback(callback: String, params: String) {
86 | LogUtils.e(TAG, "当前进程: ${ProcessUtils.getCurrentProcessName()}")
87 | view.postBridgeCallback(callback, params)
88 | }
89 | }
90 | // BridgeCommandHandler.getInstance()
91 | // .handleBridgeInvoke(message?.command, parseParams(message?.params), callback)
92 | if (iBridgeInvokeMainProcess != null){
93 | iBridgeInvokeMainProcess?.handleBridgeInvoke(message?.command, parseParams(message?.params), callback)
94 | }else{
95 | LogUtils.e(TAG, "excuteCommand()", "iBridgeInvokeMainProcess is null")
96 | }
97 | }
98 |
99 | private fun parseParams(params: JsonObject?): String {
100 | if (params == null) {
101 | return ""
102 | }
103 |
104 | return GsonUtils.toJson(params)
105 | }
106 | }
--------------------------------------------------------------------------------
/module_web/src/main/java/com/sunhy/demo/web/bridge/JsBridgeMessage.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.web.bridge
2 |
3 | import com.google.gson.JsonObject
4 | import com.google.gson.annotations.SerializedName
5 |
6 | /**
7 | * 桥接通信数据格式
8 | */
9 | data class JsBridgeMessage(
10 | @SerializedName("command")
11 | val command: String?, // 命令
12 | @SerializedName("params")
13 | val params: JsonObject?, // 参数
14 | )
--------------------------------------------------------------------------------
/module_web/src/main/java/com/sunhy/demo/web/broadcast/WebViewInitBoastcast.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.web.broadcast
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.Context
5 | import android.content.Intent
6 | import com.blankj.utilcode.util.LogUtils
7 | import com.sunhy.demo.web.TemplateWebViewPool
8 | import com.sunhy.demo.web.WebViewPool
9 | import kotlin.math.min
10 |
11 | class WebViewInitBoastcast: BroadcastReceiver() {
12 |
13 | override fun onReceive(context: Context, intent: Intent?) {
14 | LogUtils.d("WebViewInitBoastcast", "initWebViewPool")
15 | initWebViewPool(context)
16 | }
17 |
18 | private fun initWebViewPool(context: Context) {
19 | // 根据手机 CPU 核心数(或者手机内存)设置缓存池容量
20 | WebViewPool.getInstance().setMaxPoolSize(min(Runtime.getRuntime().availableProcessors(), 3))
21 | WebViewPool.getInstance().init(context)
22 |
23 | // 加载本地模板用的 WebView 复用池
24 | TemplateWebViewPool.getInstance().setMaxPoolSize(min(Runtime.getRuntime().availableProcessors(), 3))
25 | TemplateWebViewPool.getInstance().init(context)
26 | }
27 | }
--------------------------------------------------------------------------------
/module_web/src/main/java/com/sunhy/demo/web/command/ToastCommand.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.web.command
2 |
3 | import com.blankj.utilcode.util.GsonUtils
4 | import com.blankj.utilcode.util.ToastUtils
5 | import com.google.gson.JsonObject
6 | import com.sunhy.demo.apt.annotations.JsBridgeCommand
7 | import com.sunhy.demo.web.IBridgeInvokeWebProcess
8 | import com.sunhy.demo.web.bridge.IBridgeCallbackInterface
9 | import com.sunhy.demo.web.bridge.IBridgeCommand
10 |
11 | @JsBridgeCommand(name = "showToast")
12 | class ToastCommand : IBridgeCommand {
13 | override fun exec(params: JsonObject?, callback: IBridgeInvokeWebProcess?) {
14 | if (params != null && params["message"] != null) {
15 | ToastUtils.showShort(params["message"].asString)
16 | //回调 测试 返回一个 message 给 web 端
17 | val key = getCallbackKey(params)
18 | if (!key.isNullOrEmpty()) {
19 | val data = mapOf("message" to "showToast is success!!")
20 | callback?.handleBridgeCallback(key, GsonUtils.toJson(data))
21 | }
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/module_web/src/main/java/com/sunhy/demo/web/command/UserInfoCommand.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.web.command
2 |
3 | import com.blankj.utilcode.util.LogUtils
4 | import com.blankj.utilcode.util.ProcessUtils
5 | import com.google.gson.JsonObject
6 | import com.sunhy.demo.apt.annotations.JsBridgeCommand
7 | import com.sunhy.demo.base.utils.LoginUtils
8 | import com.sunhy.demo.web.IBridgeInvokeWebProcess
9 | import com.sunhy.demo.web.bridge.IBridgeCommand
10 |
11 | @JsBridgeCommand(name = "getUserInfo")
12 | class UserInfoCommand : IBridgeCommand{
13 |
14 | override fun exec(params: JsonObject?, callback: IBridgeInvokeWebProcess?) {
15 | val userInfoJson = LoginUtils.getUserInfo()
16 | LogUtils.e("UserInfoCommand", "当前进程: ${ProcessUtils.getCurrentProcessName()}", userInfoJson)
17 | val key = getCallbackKey(params)
18 | if (!key.isNullOrEmpty()) {
19 | callback?.handleBridgeCallback(key, userInfoJson)
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/module_web/src/main/java/com/sunhy/demo/web/core/BaseTemplateWebActivity.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.web.core
2 |
3 | import android.os.Bundle
4 | import android.view.KeyEvent
5 | import androidx.appcompat.app.AppCompatActivity
6 | import androidx.databinding.DataBindingUtil
7 | import androidx.databinding.ViewDataBinding
8 | import com.sunhy.demo.web.TemplateWebViewPool
9 | import com.sunhy.demo.web.WebViewPool
10 |
11 | abstract class BaseTemplateWebActivity: AppCompatActivity() {
12 |
13 | protected lateinit var mBinding: B
14 |
15 | protected val mWebView by lazy { TemplateWebViewPool.getInstance().getWebView(this) }
16 |
17 | abstract fun getLayoutId(): Int
18 |
19 | abstract fun initView()
20 |
21 | abstract fun initData()
22 |
23 | override fun onCreate(savedInstanceState: Bundle?) {
24 | super.onCreate(savedInstanceState)
25 | mBinding = DataBindingUtil.setContentView(this, getLayoutId())
26 | mBinding.lifecycleOwner = this
27 |
28 | initView()
29 | initData()
30 | }
31 |
32 | override fun onBackPressed() {
33 | if (mWebView.canGoBack()) {
34 | mWebView.goBack()
35 | return
36 | }
37 | super.onBackPressed()
38 | }
39 |
40 | override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
41 | if (keyCode == KeyEvent.KEYCODE_BACK) {
42 | if (mWebView.canGoBack()) {
43 | mWebView.goBack()
44 | return true
45 | }
46 | }
47 | return super.onKeyDown(keyCode, event)
48 | }
49 |
50 | }
--------------------------------------------------------------------------------
/module_web/src/main/java/com/sunhy/demo/web/core/BaseWebActivity.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.web.core
2 |
3 | import android.os.Bundle
4 | import android.view.KeyEvent
5 | import androidx.appcompat.app.AppCompatActivity
6 | import androidx.databinding.DataBindingUtil
7 | import androidx.databinding.ViewDataBinding
8 | import com.sunhy.demo.web.WebViewPool
9 |
10 | abstract class BaseWebActivity: AppCompatActivity() {
11 |
12 | protected lateinit var mBinding: B
13 |
14 | protected val mWebView by lazy { WebViewPool.getInstance().getWebView(this) }
15 |
16 | abstract fun getLayoutId(): Int
17 |
18 | abstract fun initView()
19 |
20 | abstract fun initData()
21 |
22 | override fun onCreate(savedInstanceState: Bundle?) {
23 | super.onCreate(savedInstanceState)
24 | mBinding = DataBindingUtil.setContentView(this, getLayoutId())
25 | mBinding.lifecycleOwner = this
26 |
27 | initView()
28 | initData()
29 | }
30 |
31 | override fun onBackPressed() {
32 | if (mWebView.canGoBack()) {
33 | mWebView.goBack()
34 | return
35 | }
36 | super.onBackPressed()
37 | }
38 |
39 | override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
40 | if (keyCode == KeyEvent.KEYCODE_BACK) {
41 | if (mWebView.canGoBack()) {
42 | mWebView.goBack()
43 | return true
44 | }
45 | }
46 | return super.onKeyDown(keyCode, event)
47 | }
48 |
49 | }
--------------------------------------------------------------------------------
/module_web/src/main/java/com/sunhy/demo/web/core/BaseWebChromeClient.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.web.core
2 |
3 | import android.util.Log
4 | import android.webkit.ConsoleMessage
5 | import android.webkit.JsResult
6 | import android.webkit.WebChromeClient
7 | import android.webkit.WebView
8 | import androidx.appcompat.app.AlertDialog
9 |
10 | class BaseWebChromeClient : WebChromeClient() {
11 |
12 | private val TAG = "BaseWebChromeClient"
13 |
14 | /**
15 | * 网页控制台输入日志
16 | */
17 | override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
18 | Log.d(TAG, "onConsoleMessage() -> ${consoleMessage.message()}")
19 | return super.onConsoleMessage(consoleMessage)
20 | }
21 |
22 | /**
23 | * 网页警告弹框
24 | */
25 | override fun onJsAlert(
26 | view: WebView,
27 | url: String,
28 | message: String,
29 | result: JsResult
30 | ): Boolean {
31 | AlertDialog.Builder(view.context)
32 | .setTitle("警告")
33 | .setMessage(message)
34 | .setPositiveButton("确认") { dialog, which ->
35 | dialog?.dismiss()
36 | result.confirm()
37 | }
38 | .setNegativeButton("取消") { dialog, which ->
39 | dialog?.dismiss()
40 | result.cancel()
41 | }
42 | .create()
43 | .show()
44 | return true
45 | }
46 |
47 | /**
48 | * 网页弹出确认弹窗
49 | */
50 | override fun onJsConfirm(
51 | view: WebView,
52 | url: String,
53 | message: String,
54 | result: JsResult
55 | ): Boolean {
56 | AlertDialog.Builder(view.context)
57 | .setTitle("警告")
58 | .setMessage(message)
59 | .setPositiveButton("确认") { dialog, which ->
60 | dialog?.dismiss()
61 | result.confirm()
62 | }
63 | .setNegativeButton("取消") { dialog, which ->
64 | dialog?.dismiss()
65 | result.cancel()
66 | }
67 | .create()
68 | .show()
69 | return true
70 | }
71 | }
--------------------------------------------------------------------------------
/module_web/src/main/java/com/sunhy/demo/web/core/BaseWebViewClient.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.web.core
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.graphics.Bitmap
6 | import android.net.Uri
7 | import android.net.http.SslError
8 | import android.os.Build
9 | import android.util.Log
10 | import android.webkit.*
11 | import androidx.annotation.RequiresApi
12 | import androidx.appcompat.app.AlertDialog
13 | import com.bumptech.glide.Glide
14 | import com.sunhy.demo.base.http.HttpUtils
15 | import com.sunhy.demo.web.BaseWebView
16 | import com.sunhy.demo.web.utils.WebUtil
17 | import kotlinx.coroutines.runBlocking
18 | import okio.ByteString.Companion.encodeUtf8
19 | import java.io.File
20 |
21 | class BaseWebViewClient : WebViewClient() {
22 |
23 | private val TAG = "BaseWebViewClient"
24 |
25 | override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
26 | super.onPageStarted(view, url, favicon)
27 | if (view is BaseWebView){
28 | view.postBlankMonitorRunnable()
29 | }
30 | }
31 |
32 | override fun onPageFinished(view: WebView, url: String) {
33 | super.onPageFinished(view, url)
34 | if (view is BaseWebView){
35 | view.removeBlankMonitorRunnable()
36 | }
37 | }
38 |
39 | /**
40 | * 证书校验错误
41 | */
42 | @SuppressLint("WebViewClientOnReceivedSslError")
43 | override fun onReceivedSslError(
44 | view: WebView,
45 | handler: SslErrorHandler,
46 | error: SslError
47 | ) {
48 | AlertDialog.Builder(view.context)
49 | .setTitle("提示")
50 | .setMessage("当前网站安全证书已过期或不可信\n是否继续浏览?")
51 | .setPositiveButton("继续浏览") { dialog, which ->
52 | dialog?.dismiss()
53 | handler.proceed()
54 | }
55 | .setNegativeButton("返回上一页") { dialog, which ->
56 | dialog?.dismiss()
57 | handler.cancel()
58 | }
59 | .create()
60 | .show()
61 | }
62 |
63 | @RequiresApi(Build.VERSION_CODES.M)
64 | override fun onReceivedError(
65 | view: WebView,
66 | request: WebResourceRequest,
67 | error: WebResourceError
68 | ) {
69 | if (request.isForMainFrame) {
70 | onReceivedError(
71 | view,
72 | error.errorCode,
73 | error.description.toString(),
74 | request.url.toString()
75 | )
76 | }
77 | }
78 |
79 | override fun onReceivedError(
80 | view: WebView?,
81 | errorCode: Int,
82 | description: String?,
83 | failingUrl: String?
84 | ) {
85 | super.onReceivedError(view, errorCode, description, failingUrl)
86 | }
87 |
88 | override fun shouldOverrideUrlLoading(
89 | view: WebView,
90 | request: WebResourceRequest
91 | ): Boolean {
92 | return shouldOverrideUrlLoading(view, request.url.toString())
93 | }
94 |
95 | override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
96 | val scheme = Uri.parse(url).scheme ?: return false
97 | when (scheme) {
98 | "com/sunhy/demo/base/http", "https" -> view.loadUrl(url)
99 | // 处理其他协议
100 | //"tel" -> {}
101 | }
102 | return true
103 | }
104 |
105 | override fun shouldInterceptRequest(
106 | view: WebView,
107 | request: WebResourceRequest
108 | ): WebResourceResponse? {
109 | Log.d(TAG, "拦截资源URL:${request.url}")
110 | Log.d(TAG, "拦截资源HEADER:${request.requestHeaders}")
111 |
112 | var webResourceResponse: WebResourceResponse? = null
113 |
114 | // 如果是 assets 目录下的文件
115 | if (isAssetsResource(request)) {
116 | webResourceResponse = assetsResourceRequest(view.context, request)
117 | }
118 |
119 | // 如果是可以缓存的文件
120 | if (isCacheResource(request)) {
121 | webResourceResponse = cacheResourceRequest(view.context, request)
122 | }
123 |
124 | if (webResourceResponse == null) {
125 | webResourceResponse = super.shouldInterceptRequest(view, request)
126 | }
127 | return webResourceResponse
128 | }
129 |
130 | private fun isAssetsResource(webRequest: WebResourceRequest): Boolean {
131 | val url = webRequest.url.toString()
132 | return url.startsWith("file:///android_asset/")
133 | }
134 |
135 | /**
136 | * assets 文件请求
137 | */
138 | private fun assetsResourceRequest(
139 | context: Context,
140 | webRequest: WebResourceRequest
141 | ): WebResourceResponse? {
142 | val url = webRequest.url.toString()
143 | try {
144 | val filenameIndex = url.lastIndexOf("/") + 1
145 | val filename = url.substring(filenameIndex)
146 | val suffixIndex = url.lastIndexOf(".")
147 | val suffix = url.substring(suffixIndex + 1)
148 | val webResourceResponse = WebResourceResponse(
149 | getMimeTypeFromUrl(url),
150 | "UTF-8",
151 | context.assets.open("$suffix/$filename")
152 | )
153 | webResourceResponse.responseHeaders = mapOf("access-control-allow-origin" to "*")
154 | return webResourceResponse
155 | } catch (e: Exception) {
156 | e.printStackTrace()
157 | }
158 | return null
159 | }
160 |
161 | /**
162 | * 判断是否是可以被缓存等资源
163 | */
164 | private fun isCacheResource(webRequest: WebResourceRequest): Boolean {
165 | val url = webRequest.url.toString()
166 | val extension = MimeTypeMap.getFileExtensionFromUrl(url)
167 | return extension == "ico" || extension == "bmp" || extension == "gif"
168 | || extension == "jpeg" || extension == "jpg" || extension == "png"
169 | || extension == "svg" || extension == "webp" || extension == "css"
170 | || extension == "js" || extension == "json" || extension == "eot"
171 | || extension == "otf" || extension == "ttf" || extension == "woff"
172 | }
173 |
174 | /**
175 | * 可缓存文件请求
176 | */
177 | private fun cacheResourceRequest(
178 | context: Context,
179 | webRequest: WebResourceRequest
180 | ): WebResourceResponse? {
181 | val url = webRequest.url.toString()
182 | val mimeType = getMimeTypeFromUrl(url)
183 |
184 | // WebView 中的图片利用 Glide 加载(能够和App其他页面共用缓存)
185 | if (isImageResource(webRequest)) {
186 | return try {
187 | val file = Glide.with(context).download(url).submit().get()
188 | val webResourceResponse = WebResourceResponse(mimeType, "UTF-8", file.inputStream())
189 | webResourceResponse.responseHeaders = mapOf("access-control-allow-origin" to "*")
190 | webResourceResponse
191 | } catch (e: Exception) {
192 | e.printStackTrace()
193 | null
194 | }
195 | }
196 |
197 | /**
198 | * 其他文件缓存逻辑
199 | * 1.寻找缓存文件,本地有缓存直接返回缓存文件
200 | * 2.无缓存,下载到本地后返回
201 | * 注意!!!
202 | * 一定要确保文件下载完整,我这里采用下载完成后给文件加 "success-" 前缀的方法
203 | */
204 | val webCachePath = WebUtil.getWebViewCachePath(context)
205 | val cacheFilePath =
206 | webCachePath + File.separator + "success-" + url.encodeUtf8().md5().hex() // 自定义文件命名规则
207 | val cacheFile = File(cacheFilePath)
208 | if (!cacheFile.exists() || !cacheFile.isFile) { // 本地不存在 则开始下载
209 | // 下载文件
210 | val sourceFilePath = webCachePath + File.separator + url.encodeUtf8().md5().hex()
211 | val sourceFile = File(sourceFilePath)
212 | runBlocking {
213 | try {
214 | HttpUtils.apiService.download(url, webRequest.requestHeaders).use {
215 | it.byteStream().use { inputStream ->
216 | sourceFile.writeBytes(inputStream.readBytes())
217 | }
218 | }
219 | // 下载完成后增加 "success-" 前缀 代表文件无损 【防止io流被异常中断导致文件损坏 无法判断】
220 | sourceFile.renameTo(cacheFile)
221 | } catch (e: Exception) {
222 | e.printStackTrace()
223 | // 发生异常删除文件
224 | sourceFile.deleteOnExit()
225 | cacheFile.deleteOnExit()
226 | }
227 | }
228 | }
229 |
230 | // 缓存文件存在则返回
231 | if (cacheFile.exists() && cacheFile.isFile) {
232 | val webResourceResponse =
233 | WebResourceResponse(mimeType, "UTF-8", cacheFile.inputStream())
234 | webResourceResponse.responseHeaders = mapOf("access-control-allow-origin" to "*")
235 | return webResourceResponse
236 | }
237 | return null
238 | }
239 |
240 | /**
241 | * 判断是否是图片
242 | * 有些文件存储没有后缀,也可以根据自家服务器域名等等
243 | */
244 | private fun isImageResource(webRequest: WebResourceRequest): Boolean {
245 | val url = webRequest.url.toString()
246 | val extension = MimeTypeMap.getFileExtensionFromUrl(url)
247 | return extension == "ico" || extension == "bmp" || extension == "gif"
248 | || extension == "jpeg" || extension == "jpg" || extension == "png"
249 | || extension == "svg" || extension == "webp"
250 | }
251 |
252 | /**
253 | * 根据 url 获取文件类型
254 | */
255 | private fun getMimeTypeFromUrl(url: String): String {
256 | try {
257 | val extension = MimeTypeMap.getFileExtensionFromUrl(url)
258 | if (extension.isNotBlank() && extension != "null") {
259 | if (extension == "json") {
260 | return "application/json"
261 | }
262 | return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) ?: "*/*"
263 | }
264 | } catch (e: Exception) {
265 | e.printStackTrace()
266 | }
267 | return "*/*"
268 | }
269 |
270 | }
--------------------------------------------------------------------------------
/module_web/src/main/java/com/sunhy/demo/web/service/BridgeCommandService.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.web.service
2 |
3 | import android.app.Service
4 | import android.content.Intent
5 | import android.os.IBinder
6 | import com.sunhy.demo.web.bridge.BridgeCommandHandler
7 |
8 | class BridgeCommandService: Service() {
9 | override fun onBind(intent: Intent?): IBinder {
10 | return BridgeCommandHandler.getInstance()
11 | }
12 | }
--------------------------------------------------------------------------------
/module_web/src/main/java/com/sunhy/demo/web/utils/WebUtil.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.web.utils
2 |
3 | import android.content.Context
4 | import android.graphics.Color
5 | import android.view.View
6 | import android.webkit.WebSettings
7 | import android.webkit.WebView
8 | import com.sunhy.demo.web.R
9 | import java.io.File
10 |
11 | object WebUtil {
12 |
13 | /**
14 | * 获取 WebView 缓存文件目录
15 | */
16 | fun getWebViewCachePath(context: Context): String{
17 | return context.filesDir.absolutePath + "/webCache"
18 | }
19 |
20 | fun defaultSettings(context: Context, webView: WebView) {
21 | // 白色背景
22 | webView.setBackgroundColor(Color.TRANSPARENT)
23 | webView.setBackgroundResource(R.color.white)
24 |
25 | // 如果访问的页面中要与Javascript交互,则webview必须设置支持Javascript
26 | // 若加载的 html 里有JS 在执行动画等操作,会造成资源浪费(CPU、电量)
27 | // 在 onStop 和 onResume 里分别把 setJavaScriptEnabled() 给设置成 false 和 true 即可
28 | webView.settings.javaScriptEnabled = true // 允许加载js
29 | webView.overScrollMode = View.OVER_SCROLL_NEVER
30 | webView.isNestedScrollingEnabled = false // 默认支持嵌套滑动
31 |
32 | // 设置自适应屏幕,两者合用
33 | webView.settings.useWideViewPort = true
34 | webView.settings.loadWithOverviewMode = true
35 | // 是否支持缩放,默认为true
36 | webView.settings.setSupportZoom(false)
37 | // 是否使用内置的缩放控件
38 | webView.settings.builtInZoomControls = false
39 | // 是否显示原生的缩放控件
40 | webView.settings.displayZoomControls = false
41 | // 设置文本缩放 默认 100
42 | webView.settings.textZoom = 100
43 | // 是否保存密码
44 | webView.settings.savePassword = false
45 | // 是否可以访问文件
46 | webView.settings.allowFileAccess = true
47 | // 是否支持通过js打开新窗口
48 | webView.settings.javaScriptCanOpenWindowsAutomatically = true
49 | // 是否支持自动加载图片
50 | webView.settings.loadsImagesAutomatically = true
51 | webView.settings.blockNetworkImage = false
52 | // 设置编码格式
53 | webView.settings.defaultTextEncodingName = "utf-8"
54 | webView.settings.layoutAlgorithm = WebSettings.LayoutAlgorithm.NORMAL
55 | // 是否启用 DOM storage API
56 | webView.settings.domStorageEnabled = true
57 | // 是否启用 database storage API 功能
58 | webView.settings.databaseEnabled = true
59 | // 配置当安全源试图从不安全源加载资源时WebView的行为
60 | webView.settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
61 |
62 | // 设置缓存模式
63 | webView.settings.cacheMode = WebSettings.LOAD_DEFAULT
64 | // 开启 Application Caches 功能
65 | webView.settings.setAppCacheEnabled(true)
66 | // 设置 Application Caches 缓存目录
67 | val cachePath = getWebViewCachePath(context)
68 | val cacheDir = File(cachePath)
69 | // 设置缓存目录
70 | if (!cacheDir.exists() && !cacheDir.isDirectory) {
71 | cacheDir.mkdirs()
72 | }
73 | webView.settings.setAppCachePath(cachePath)
74 | }
75 | }
--------------------------------------------------------------------------------
/module_web/src/main/res/layout/activity_news_detail.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/module_web/src/main/res/layout/activity_web.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/module_web/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFFFFF
4 |
--------------------------------------------------------------------------------
/module_web/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/module_web/src/test/java/com/sunhy/demo/web/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.sunhy.demo.web
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 | rootProject.name = "WebViewSimpleDemo"
16 | include ':app'
17 | include ':module_web'
18 | include ':apt-annotations'
19 | include ':apt-processor'
20 | include ':base'
21 |
--------------------------------------------------------------------------------