본문 바로가기

Mobile/ApplusForm

MagazineA01 소스 분석_앱플러스폼(ApplusForm, AppForm)

ApplusForm에서 제공하는 Templete 중 MagazineA01에 대한 강좌입니다.

홈페이지에서 소개하는 내용을 먼저 보겠습니다.

Magazine Template A01 Type
Magazine Template은 e-book, 잡지, 카타로그 등 디지털 잡지 및 콘텐츠 서비스 제공에 적합한 형식입니다.
* 주요기능
가로/세로 화면 전환, 화면 확대/축소, 화면 전환 효과 등
* 활용분야
e-book, 디지털 매거진, 앱진, 카타로그, 브로슈어, 팜플렛 등
* 특징
부드러운 책장 넘김 효과, 가로/세로 UI 전환, 책장 형식 UI 등
최종 업데이트 일자 : 2013.08.21 / 지원 OS : Android 2.3 ~ , iOS 5.0 ~

MagazineA01은 활용 방법에 따라 이미지기반의 만화책, 잡지 앱을 만들수도 있고, 전단지와 같이 홍보용 웹페이지를 여러페이지 단위로 보여주고 싶을 때에도 활용할 수 있습니다. 홈페이지에서도 스크린샷을 볼 수 있지만 어떤 화면을 제공하는지 잠시 보겠습니다. iBooks처럼 여러 책을 책장에 배치하듯 보여주는 기능과 책장넘기는 효과를 이용하여 책을 보는 기능으로 구성되어 있습니다.

1 2

ApplusForm.com에서 Android 혹은 iOS Project를 다운로드받습니다. 여기서는 Android용을 받았습니다. 소스는 둘다 동일하므로 어느 프로젝트를 받아도 무관합니다.
project

소스를 받아 Eclipse에 Import하면 위와 같이 프로젝트가 추가됩니다. 다른 부분은 Android에 관련된 내용이고 assets/moml폴더에 있는 소스에 실제 구현되어 있으므로 이 부분만을 보면 폴더구조는 개발자가 편한대로 만들어도 되지만 여기서는 /data, /res, /ui로 구분되어 있습니다.

/data : 책 이미지들...  
/res : 화면을 구성하는 리소스들...(이미지, 동영상, 소리 등)  
/ui : 화면  

/data/cartoons폴더를 읽어 책 목록을 구성하고 책의 페이지를 출력하므로 책을 추가하고 싶다면 /data/cartoons폴더에 표지가 될 이미지를 폴더이름과 동일하게 넣어두면 됩니다.

applicationInfo.xml에서 ui/start.xml가 시작 페이지로 지정되어 있으므로 이 페이지를 먼저 보겠습니다.

<UI>
<UILAYOUT portrait="320,480" landscape="320,480">
    <NAVIGATIONCONTAINER id="navi" layout="0,0,320,480" selectedItem="toonList">
        <VIEWITEM id="toonList" src="toonList.xml" transitionInEffect="none" transitionOutEffect="none">
            <VIEWITEM id="toonWebView" src="toonWebView.xml" transitionInEffect="none" transitionOutEffect="none"/>
            <VIEWITEM id="about" src="about.xml" transitionInEffect="none" transitionOutEffect="none"/>
        </VIEWITEM>
    </NAVIGATIONCONTAINER>
    <!-- 화면을 가득 채우는 애니메이션에 사용됨 -->
    <IMAGE id="zoomImg" layout="0,0,75,112" visible="invisible"/> 
</UILAYOUT>
</UI>

위 소스의 NAVIGATIONCONTAINER의 Navigation 구조도는 다음과 같습니다.
Navigation 구조도

UI는 NAVIGATIONCONTAINER로 구성되어 있으며 기본값은 selectedItem=“toonList”로 지정되어 있으므로 toonList에 해당하는 VIEWITEM인 toonList.xml이 로드됩니다.

<FUNCTIONCALL cmd="userVariable.toonList = file.dir('/data/cartoons', '', '')"/>
<FUNCTIONCALL cmd="userVariable.curPage = 0"/>
<FUNCTIONCALL cmd="function.initDB"/>

실행에 앞서 실행중에 필요한 변수 및 DB를 초기화 합니다. FUNCTIONCALL은 UI를 생성한 후 즉시 호출되므로 UI가 그려지기 전에 반영되어야 하는 값은 이때 호출하는 것보다 onCreate에서 처리하는 것이 좋습니다. FUNCTIONLIST의 initDB()와 htmlDataString(html) Function은 차후에 설명하겠습니다.

이제 toonList.xml를 보겠습니다. toonList.xml에서는 책장과 같은 UI를 구성하고, assets/data/cartoons폴더를 읽어와 책장에 책을 배치하는 기능을 제공합니다.

세로 화면(portrait)

<UILAYOUT portrait="320,480">
...중략...
</UILAYOUT>
potrait

가로화면(landscape)

<UILAYOUT landscape="480,320">
...중략...
</UILAYOUT>
landscape

세로 화면과 가로화면은 width, height가 서로 다르므로 UILAYOUT에서 portrait와 landscape attribute로 크기를 지정해주고, UI의 좌표나 크기들을 가로 및 세로 화면에 맞게 구성해주어야 합니다. 가로화면을 사용하지 않거나 세로화면을 사용하지 않는 경우에는 가로 및 세로 화면 중 하나만 지정해줍니다. 화면을 돌렸을 때 상황에 맞춰 UILAYOUT이 화면에 표시 됩니다.

<FUNCTIONCALL cmd="userVariable.toonList = file.dir('/data/cartoons', '', '')"/>

file.dir function은 userVariable.toonList에 /data/cartoons폴더의 내용을 xml로 생성하여 넣어줍니다. userVariable.toonList의 내용을 보면 아래와 같습니다.

<?xml version="1.0" encoding="utf-8"?>
<FILES>
    <FILE path="storage:/..../MagazineA01.app/moml/data/cartoons/popeye01.jpg" />
    <FILE path="storage:/..../MagazineA01.app/moml/data/cartoons/popeye02.jpg" />
    <FILE path="storage:/..../MagazineA01.app/moml/data/cartoons/popeye03.jpg" />
    <FILE path="storage:/..../MagazineA01.app/moml/data/cartoons/popeye04.jpg" />
    <FILE path="storage:/..../MagazineA01.app/moml/data/cartoons/popeye06.jpg" />
</FILES>

이제 책 표지 이미지를 읽어와 리스트에 출력해줍니다.

        <!-- 컨텐츠를 3개씩 출력함 -->
        <LIST layout="0,44,320,436" dataSource="{xmlProcessing.groupByOrder(file.dir('/data/cartoons', 'jpg|png', ''), '/FILES/FILE', 3, 'ITEMGROUP')}" 
                    dataList="/FILES/ITEMGROUP" defaultImg="#d3af71" divider="#00ffffff">
            <LISTLAYOUT condition="math.num(xpath.evaluate('count(./FILE)')) == 3">
                <WINDOW layout="0,0,320,139" align="linear:horizontal|left" defaultImg="/res/standItem.png">
                    <BUTTON layout="75,112" margin="20,10,10,0" defaultImg="{xpath.evaluate('./FILE[1]/@path')}" onClick="{function.selToon(xpath.evaluate('./FILE[1]/@path'), caller.left, caller.top+parent.parent.parent.top)}"/>
                    <BUTTON layout="75,112" margin="20,10,10,0" defaultImg="{xpath.evaluate('./FILE[2]/@path')}" onClick="{function.selToon(xpath.evaluate('./FILE[2]/@path'), caller.left, caller.top+parent.parent.parent.top)}"/>
                    <BUTTON layout="75,112" margin="20,10,10,0" defaultImg="{xpath.evaluate('./FILE[3]/@path')}" onClick="{function.selToon(xpath.evaluate('./FILE[3]/@path'), caller.left, caller.top+parent.parent.parent.top)}"/>
                </WINDOW>
            </LISTLAYOUT>
            <LISTLAYOUT condition="math.num(xpath.evaluate('count(./FILE)')) == 2">
                <WINDOW layout="0,0,320,139" align="linear:horizontal|left" defaultImg="/res/standItem.png">
                    <BUTTON layout="75,112" margin="20,10,10,0" defaultImg="{xpath.evaluate('./FILE[1]/@path')}" onClick="{function.selToon(xpath.evaluate('./FILE[1]/@path'), caller.left, caller.top+parent.parent.parent.top)}"/>
                    <BUTTON layout="75,112" margin="20,10,10,0" defaultImg="{xpath.evaluate('./FILE[2]/@path')}" onClick="{function.selToon(xpath.evaluate('./FILE[2]/@path'), caller.left, caller.top+parent.parent.parent.top)}"/>
                </WINDOW>
            </LISTLAYOUT>
            <LISTLAYOUT condition="math.num(xpath.evaluate('count(./FILE)')) == 1">
                <WINDOW layout="0,0,320,139" align="linear:horizontal|left" defaultImg="/res/standItem.png">
                    <BUTTON layout="75,112" margin="20,10,10,0" defaultImg="{xpath.evaluate('./FILE[1]/@path')}" onClick="{function.selToon(xpath.evaluate('./FILE[1]/@path'), caller.left, caller.top+parent.parent.parent.top)}"/>
                </WINDOW>
            </LISTLAYOUT>
        </LIST>

LIST는 1열로 출력하도록 되어 있지만 xmlProcessing.groupByOrder function을 이용하여 3열로 출력하도록 data를 재구성합니다.

<FILES>
    <ITEMGROUP>
        <FILE path="storage:/..../MagazineA01.app/moml/data/cartoons/popeye01.jpg"/>
        <FILE path="storage:/..../MagazineA01.app/moml/data/cartoons/popeye02.jpg"/>
        <FILE path="storage:/..../MagazineA01.app/moml/data/cartoons/popeye03.jpg"/>
    </ITEMGROUP>
    <ITEMGROUP>
        <FILE path="storage:/..../MagazineA01.app/moml/data/cartoons/popeye04.jpg"/>
        <FILE path="storage:/..../MagazineA01.app/moml/data/cartoons/popeye06.jpg"/>
    </ITEMGROUP>
</FILES>

ITEMGROUP은 1행이고 FILE은 열이라고 볼 수 있습니다. 한 행에 BUTTON을 무조건 세개씩 출력해도 문제는 없지만 불필요하고 오동작하는 BUTTON이 생성될 수 있으므로,

<LISTLAYOUT condition="math.num(xpath.evaluate('count(./FILE)')) == 3">

LISTLAYOUT에 condition으로 개수를 얻어와 화면을 구성하도록 처리했습니다. 여러 열로 구성하는 경우에는 유용하게 사용될 수 있는 코드입니다.

이제 LIST의 BUTTON을 터치하면 selToon function이 호출됩니다.

    <FUNCTION id="selToon(path, left, top)">
        <FUNCTIONITEM cmd="userVariable.toonPath = string.sub(path, 0, string.len(path) - 4)"/>
        <FUNCTIONITEM cmd="root.zoomImg.left = left, root.zoomImg.top = top"/>
        <FUNCTIONITEM cmd="root.zoomImg.defaultImg=path"/>
        <FUNCTIONITEM condition="userVariable.rotate == 'p'"
                cmd="device.os.platform == 'iOS'?animation.flyOut('root.zoomImg', -root.zoomImg.left, -root.zoomImg.top, 320,480, 'easeOut6', 1000, ''):''"
                elseCmd="device.os.platform == 'iOS'?animation.flyOut('root.zoomImg', -root.zoomImg.left, -root.zoomImg.top, 320,960, 'easeOut6', 1000, ''):''"/>
        <!-- 애니메이션이 끝나기 전에 미리 이동시켜둠. -->
        <FUNCTIONITEM cmd="function.goSelToon" delay="500"/>
    </FUNCTION>
    <FUNCTION id="goSelToon">
        <FUNCTIONITEM cmd="root.navi.selectedItem = 'toonWebView'"/>
        <FUNCTIONITEM cmd="userVariable.curPage = 0"/>
    </FUNCTION>

selToon에서는 선택한 이미지 path와 위치를 얻어와 화면 전체에 꽉차는 애니메이션을 실행한 후, userVariable.toonPath 에 선택한 만화의 실제 내용이 들어 있는 path를 보관하고, goSelToon function을 실행하여 선택한 만화를 실제로 보는 화면인 toonWebView로 이동합니다.

만화는 webview로 보여주고 있는데, Android에서는 webview의 로딩에 부하가 많이 걸려 애니메이션의 동작에 많은 영향을 주는 문제가 있었습니다. 따라서 device.os.platform으로 현재 실행되는 platform의 종류를 얻어와 iOS에서만 애니메이션이 실행되도록 하였습니다.

toonWebView는 선택한 만화의 이미지들을 페이지를 넘겨보듯이 이전과 다음으로 이동하는 기능이 있습니다. 이동시 poly 애니메이션을 사용하여 책장넘기는 효과를 적용했습니다.

    <FUNCTION id="onCreate">
        <FUNCTIONITEM cmd="userVariable.toonData = file.dir(userVariable.toonPath, 'jpg|png', '')"/>
        <FUNCTIONITEM cmd="userVariable.totalPage = math.num(xpath.evaluateEx(userVariable.toonData, 'count(/FILES/FILE)'))"/>
        <FUNCTIONITEM condition="userVariable.curPage &gt; 0" cmd="userVariable.curPage = userVariable.curPage - 1"/>
        <FUNCTIONITEM cmd="function.nextPage"/>
        <FUNCTIONITEM cmd="userVariable.canMovePage = 'true'"/>
    </FUNCTION>

onCreate function에서는 데이터를 셋팅하고 초기 값들을 설정합니다.
onCreate는 UI의

<CONTAINER layout="0,0,0,0" visible="invisible" onOrientationChanged="function.onRotate" onCreate="{function.onCreate}" />  

에서 호출됩니다. userVariable.toonData에 toonList.xml의 selToon function에서 지정한 path를 보관하는 userVariable.toonPath로부터 file목록을 읽어와 xml로 보관됩니다. 이외에 총 페이지수와 현재 페이지 값을 설정하고, nextPage function을 호출하여 처음 보여줄 페이지를 출력합니다.

다음페이지와 이전 페이지를 읽어와 화면에 배치하는 부분은 다음과 같습니다.

    <!-- 다음 페이지로 이동할 때 이미지를 얻어옴 -->
    <FUNCTION id="nextPage">
        <FUNCTIONITEM cmd="userVariable.curPage = userVariable.curPage + 1"/>
        <FUNCTIONITEM cmd="function.pageSet('next')"/>
    </FUNCTION>
    <!-- 이전 페이지로 이동할 때 이미지를 얻어옴 -->

        <FUNCTIONITEM condition="device.os.platform == 'iOS'"
        cmd="userVariable.imgWebUrl = string.replace(userVariable.imgWebUrl, 'storage', 'file')"
        elseCmd="userVariable.imgWebUrl = string.replace(userVariable.imgWebUrl, 'embed:/', 'file:///android_asset/' )"/>

        <!-- <FUNCTIONITEM cmd="userVariable.imgWebUrl = string.encoding(userVariable.imgWebUrl, 'utf-8')"/> -->

        <FUNCTIONITEM cmd="toonImg.loadSrc = function.root.htmlDataString('&lt;img src=\'' +userVariable.imgWebUrl+'\' width=\'100%\' /&gt;')"/>
        <FUNCTIONITEM condition="stat == 'prev'" cmd="function.prevPageFromDB" delay="500"/>

portrait:
<WEBVIEW id="toonImg" layout="0,0,320,480" loadSrc="" clientControl="false" supportZoom="true"/>

landscape:
<WEBVIEW id="toonImg" layout="0,0,480,320" loadSrc="" clientControl="false" supportZoom="true"/>

WEBVIEW의 id가 landscape일 경우와 portrait일 경우가 동일하게 설정된 것을 볼 수 있습니다. document내에서 동일한 id가 정의되면 안되지만 다른 UILAYOUT에서는 id를 동일하게 지정하여, 가로화면과 세로화면에서 동일한 id로 UI에 접근할 수 있도록 하고 있습니다.
위의 소스에서 예를 들면, pageSet function에서

<FUNCTIONITEM cmd="toonImg.loadSrc = function.root.htmlDataString('&lt;img src=\'' ..중략.."/>  

toonImg는 가로와 세로모드에 모두 동일한 id로 layout만 다르게 구성되어 있으므로, 값을 지정할때 가로모드와 세로모드를 구분하지 않고 동일한 id인 toonImg의 값을 접근할 수 있습니다.

이전 및 다음 이미지를 찾아 로드하는 것은 비교적 간단한 작업입니다. userVariable.curPage의 값을 증가 및 감소시킨 후

xpath.evaluateEx(userVariable.toonData, '/FILES/FILE[' + userVariable.curPage + ']/@path')  

로 원하는 위치의 이미지 path를 얻어와 WEBVIEW에 출력합니다. WEBVIEW의 loadSrc는 http://로 시작하는 url뿐만 아니라 html문서 자체도 읽을 수 있으므로 start.xml의 htmlDataString function에서 간단하게 만든 html문서를 로드하도록 했습니다.
'/FILES/FILE[' + userVariable.curPage + ']/@path'는 xpath 정규식이므로 강좌를 참고하면 유용하게 활용할 수 있습니다.

책장을 넘기는 효과는

<FUNCTIONITEM cmd="animation.polyOut('toonFlipImg', 0, 0, -toonImg.width + 5, -50, 0, 0, -toonImg.width + 1, 50, 'easeIn3', 500, 'userVariable.canMovePage = \'true\'')" delay="100"/>
<FUNCTIONITEM cmd="animation.polyIn('toonFlipImg', 0, 0, -toonImg.width + 5, -50, 0, 0, -toonImg.width + 1, 50, 'easeIn3', 500, 'function.prevPage')"/>

으로 쉽게 구현할 수 있습니다.

책장을 넘길 때, 배경이 흰색으로 보인다면 책을 넘기는 듯한 효과가 반감되므로 다음과 같은 과정을 거쳐 출력합니다.

다음 페이지로 이동.
nextpage
Function이 호출되는 순서는 flipNextPage function -> doFlipNextpage function (nextPage function -> pageSet function) 입니다.
이전 페이지로 이동.
prev.png
Function이 호출되는 순서는 flipPrevPage function -> doFlipPrevpage function -> prevPage function -> pageSet function 입니다.

구현된 내용은 전체 소스를 보면서 실행해보면 이해하기 쉽습니다.
MagazineA01의 동작 방식과 소스를 대략 분석해보았습니다.